concept Corridor extends G22TradingStrategy { // how many percent the crypto price needs to move // for one profitable trade settableWithVar double cellSize = 0.2; // how many cells the corridor has settableWithVar int capacity = 6; settableWithVar int maxPositionsPerDirection = 4; settableWithVar bool openTwoPositionsAtStart = true; settableWithVar bool alwaysOpenTwoPositions; // position options settableWithVar double leverage = 1; settableWithVar double marginPerPosition = 1; // in USDT // internal // primed = started but waiting for first price settableWithVar bool primed; settableWithVar PriceDigitizer2 digitizer; settableWithVar int direction; settableWithVar double epsilon = 1e-6; // maximum debt seen settableWithVar double maxDebt; //settableWithVar double minCoinProfit; //settableWithVar double maxBoundCoin; record noeq Position(double openingPrice, double direction, double digitizedOpeningPrice) { double closingPrice = Double.NaN; long openingStep, closingStep; gettable long openingTime; gettable long closingTime; double leverage; settable double cryptoAmount; settable O openError; settable O closeReason; settable O closeError; gettable double margin; settable bool openedOnMarket; settable bool closedOnMarket; Corridor strategy() { ret Corridor.this; } { // Corridor.this is not set when unstructuring if (!dynamicObjectIsLoading()) { openingStep = stepCount; leverage = Corridor.this.leverage; openingTime = currentTime(); } } double digitizedOpeningPrice() { ret digitizedOpeningPrice; } bool isLong() { ret direction > 0; } bool isShort() { ret direction < 0; } bool closed() { ret !isNaN(closingPrice); } S type() { ret trading_directionToPositionType(direction); } double profitAtPrice(double price) { ret ((price-openingPrice)/openingPrice*direction*100-adversity)*leverage; } double profit() { ret profitAtPrice(closed() ? closingPrice : currentPrice()); } double coinProfit() { ret profit()/100*margin(); } void close(O closeReason) { if (closed()) fail("Can't close again"); if (closeReason != null) closeReason(closeReason); openPositions.remove(this); closingPrice = currentPrice(); closingTime = currentTime(); closingStep = stepCount; closedPositions.add(this); realizedProfit += profit(); realizedCoinProfit += coinProfit(); closeOnMarket(); //print(this); //printPositions(); } // Position.toString toString { ret commaCombine( spaceCombine( closed() ? "Closed " + formatLocalDateWithSeconds(closingTime()) : null, formatDouble1(leverage) + "X " + upper(type()) ), "opened " + formatLocalDateWithSeconds(openingTime()), !closed() ? null : or(closeReason, "unknown reason"), "profit: " + formatProfit(profit()) + "% (" + marginCoin + " " + plusMinusFix(formatPrice(coinProfit())) + ")", "margin: " + marginCoin + " " + formatPrice(margin()), "crypto: " + formatPrice(cryptoAmount), "opening price: " + formatPriceX(openingPrice) + " @ step " + openingStep, !closed() ? null : "closing price: " + formatPriceX(closingPrice) + " @ step " + closingStep, //"digitized: " + formatPrice(digitizedOpeningPrice()), ); } void closeOnMarket { try { if (market != null) { log("Closing on market: " + this); market.closePosition(new IFuturesMarket.CloseOrder() .holdSide(HoldSide.fromInt(direction)) .cryptoAmount(cryptoAmount)); closedOnMarket(true); change(); } } catch e { closeError = toPersistableThrowable(e); } } void open { margin = cryptoAmount*openingPrice/leverage; openPositions.add(this); } void openOnMarket { try { if (market != null) { log("Opening on market: " + this); market.openPosition(new IFuturesMarket.OpenOrder() .holdSide(HoldSide.fromInt(direction)) .cryptoAmount(cryptoAmount) .leverage(leverage) .isCross(true)); openedOnMarket(true); change(); } } catch e { openError(toPersistableThrowable(e)); log("Open error:" + e); close("Open error"); } } } gettable double currentPrice = 0; gettable double oldPrice = Double.NaN; gettable double startingPrice = Double.NaN; gettable int lastDirection; gettable double realizedProfit; gettable double realizedCoinProfit; settableWithVar long stepCount; settableWithVar long stepSince; gettable new LinkedHashSet openPositions; gettable new L closedPositions; Position openPosition(int direction) { if (l(positionsInDirection(direction)) >= maxPositionsPerDirection) { log("Not opening new position in direction " + direction + ", max reached"); null; } var price = digitizedPrice(); if (isNaN(price)) price = digitizer.digitizeIndividually(currentPrice()); var p = new Position(currentPrice(), direction, price); double cryptoAmount = marginPerPosition/price*leverage; cryptoAmount = roundTo(cryptoStep, cryptoAmount); p.cryptoAmount = max(minCrypto, cryptoAmount); p.open(); //print("Opening " + p); //printPositions(); p.openOnMarket(); ret p; } bool hasPosition(double price, double direction) { ret findPosition(price, direction) != null; } Position closePosition(double price, double direction, O closeReason) { var p = findPosition(price, direction); p?.close(closeReason); ret p; } void closePositions(Cl positions, O closeReason default null) { forEach(positions, -> .close(closeReason)); } Position findPosition(double digitizedPrice, double direction) { ret firstThat(openPositions(), p -> diffRatio(p.digitizedOpeningPrice(), digitizedPrice) <= epsilon() && p.direction == direction); } Position openShort() { ret openPosition(-1); } Position openLong() { ret openPosition(1); } void printPositions { print(colonCombine(n2(openPositions, "open position"), joinWithComma(openPositions))); } bool started() { ret !isNaN(startingPrice); } void start { if (started()) fail("Already started"); if (currentPrice() == 0) ret with primed(true); // Save starting price, create price cells & digitizer startingPrice = currentPrice(); var priceCells = makePriceCells(startingPrice); digitizer = new PriceDigitizer2(priceCells); digitizer.verbose(verbose); //digitizer.init(startingPrice); print("Starting CORRIDOR at " + startingPrice + " +/- " + cellSize); if (openTwoPositionsAtStart) { nextStep(); openPosition(1); openPosition(-1); afterStep(); } } void prices(double... prices) { fOr (price : prices) price(price); } swappable PriceCells makePriceCells(double basePrice) { //ret new RegularPriceCells(basePrice, cellSize); ret new GeometricPriceCells(basePrice, cellSize); } double digitizedPrice() { ret digitizer.digitizedPrice(); } double lastDigitizedPrice() { ret digitizer.lastDigitizedPrice(); } void handleNewPriceInQ(double price) { q.add(-> price(price)); } void currentPrice aka price(double price) { if (currentPrice == price) ret; currentPrice = price; if (!started()) { if (primed) { primed(false); start(); } ret; } digitizer.digitize(price); direction = sign(digitizedPrice()-lastDigitizedPrice()); // No cell move? if (direction == 0) ret with afterStep(); nextStep(); //printWithPrecedingNL("New digitized price: " + digitizedPrice()); if (started()) step(); print(this); } void nextStep { ++stepCount; stepSince(now()); } swappable void step { double p1 = lastDigitizedPrice(); double p2 = digitizedPrice(); direction = sign(p2-p1); if (direction == 0) ret; // Find winning positions to close new L toClose; for (Position p : direction > 0 ? longPositionsAtOrBelowDigitizedPrice(p1) : shortPositionsAtOrAboveDigitizedPrice(p1)) { p.closeReason("WIN"); log("Closing winner position at " + currentPrice + " (" + p1 + " -> " + p2 + "): " + p); toClose.add(p); } // Dismantle corridor if too large double limit = fromCellNumber(toCellNumber(digitizedPrice())-capacity*direction); var toDismantle = direction > 0 ? shortPositionsAtOrBelowDigitizedPrice(limit) : longPositionsAtOrAboveDigitizedPrice(limit); print("DISMANTLE LIMIT: " + formatPriceX(limit)); for (Position p : toDismantle) { p.closeReason("DISMANTLE"); toClose.add(p); } //printVars("step", +p1, +p2, +direction, +closed, +opened); if (nempty(toClose)) closePositions(toClose); // Open backward position here (if not there yet) if (!hasPosition(p2, -direction)) openPosition(-direction); // Open forward position if configured as such if (alwaysOpenTwoPositions && !hasPosition(p2, direction)) openPosition(direction); afterStep(); } void afterStep { maxDebt = max(maxDebt, debt()); //minCoinProfit = min(minCoinProfit, coinProfit()); //maxBoundCoin = max(maxBoundCoin, boundCoin()); maxInvestment = max(maxInvestment, investment()); change(); } double investment() { ret boundCoin()-realizedCoinProfit; } double boundCoin() { ret max(openMargins(), max(0, -coinProfit())); } double fromCellNumber(double cellNumber) { ret cells().fromCellNumber(cellNumber); } double toCellNumber(double price) { ret cells().toCellNumber(price); } double unrealizedProfit() { double sum = 0; for (p : openPositions) sum += p.profit(); ret sum; } double openMargins() { double sum = 0; for (p : openPositions) sum += p.margin(); ret sum; } double unrealizedCoinProfit() { double sum = 0; for (p : openPositions) sum += p.coinProfit(); ret sum; } L positionsInDirection(int direction) { ret filter(openPositions, p -> p.direction == direction); } L longPositions() { ret positionsInDirection(1); } L shortPositions() { ret positionsInDirection(-1); } L shortPositionsAtOrBelowPrice(double openingPrice) { ret filter(openPositions, p -> p.isShort() && p.openingPrice <= openingPrice); } L longPositionsAtOrAbovePrice(double openingPrice) { ret filter(openPositions, p -> p.isLong() && p.openingPrice >= openingPrice); } L shortPositionsAtOrBelowDigitizedPrice(double openingPrice) { ret filter(openPositions, p -> p.isShort() && p.digitizedOpeningPrice() <= openingPrice); } L shortPositionsAtOrAboveDigitizedPrice(double openingPrice) { ret filter(openPositions, p -> p.isShort() && p.digitizedOpeningPrice() >= openingPrice); } L longPositionsAtOrAboveDigitizedPrice(double openingPrice) { ret filter(openPositions, p -> p.isLong() && p.digitizedOpeningPrice() >= openingPrice); } L longPositionsAtOrBelowDigitizedPrice(double openingPrice) { ret filter(openPositions, p -> p.isLong() && p.digitizedOpeningPrice() <= openingPrice); } L negativePositions() { ret filter(openPositions, p -> p.profit() < 0); } double debt() { ret max(0, -unrealizedCoinProfit()); } double profit() { ret realizedProfit+unrealizedProfit(); } double coinProfit() { ret realizedCoinProfit+unrealizedCoinProfit(); } S baseToString() { ret colonCombine( _conceptID() == 0 ? "" : "Strategy " + _conceptID(), "Corridor"); } LS status() { var r = currentCorridorPriceRange(); ret llNonNulls( //baseToString(), !primed() ? null : "Primed", !started() ? null : "Started. current price: " + formatPriceX(currentPrice) + ", digitized: " + formatPriceX(digitizedPrice()), "Leverage: " + leverage + ", margin per position: " + marginCoin+ " " + formatPrice(marginPerPosition), "Cell size: " + formatPercentage(cellSize, 3) + ", max capacity: " + capacity, !started() ? null : "Current corridor: " + (r == null ? "-" : formatPrice(r.start) + " to " + formatPrice(r.end) + " (" + formatPercentage(currentCorridorSizeInPercent(), 3) + ")"), "Step " + n2(stepCount) + (stepSince == 0 ? "" : " since " + formatHoursMinutesColonSeconds(currentTime()-stepSince)), "Profit: " + marginCoin + " " + plusMinusFix(formatMarginPrice(coinProfit())), "Realized profit: " + marginCoin + " " + formatProfit(realizedCoinProfit) + " from " + n2(closedPositions, "closed position"), "Unrealized profit: " + marginCoin + " " + formatProfit(unrealizedCoinProfit()) + " in " + n2(openPositions, "open position"), "Debt: " + marginCoin + " " + formatProfit(debt()) + " (max seen: " + formatProfit(maxDebt) + ")", "Investment used: " + marginCoin + " " + formatMarginPrice(maxInvestment()), ); } LS fullStatus() { ret listCombine( status(), "", n2(openPositions, "open position") + ":", reversed(openPositions), "", n2(closedPositions, "closed position") + ":", reversed(closedPositions) ); } S formatProfit(double x) { ret plusMinusFix(formatDouble1(x)); } // Corridor.toString toString { ret baseToString(); } double maxCorridorPercentSize() { ret capacity*cellSize; } PriceCells cells() { ret digitizer?.cells; } DoubleRange currentCorridorPriceRange() { double[] openingPrices = mapToDoubleArray(openPositions, ->.openingPrice); ret doubleRange(doubleMin(openingPrices), doubleMax(openingPrices)); } double currentCorridorSizeInPercent() { DoubleRange r = currentCorridorPriceRange(); ret r == null ? 0 : asPercentIncrease(r.end, r.start); } S formatPriceX(double price) { if (isNaN(price)) ret "-"; double num = cells().priceToCellNumber(price); ret formatPrice(price) + " (C" + formatDouble2(num) + ")"; } void reset { resetFields(this, [[ digitizer maxDebt direction oldPrice startingPrice lastDirection realizedProfit realizedCoinProfit stepCount openPositions closedPositions ]]); change(); } Corridor emptyClone() { //var clone = shallowClone(this); var clone = shallowCloneToUnlistedConcept(this); clone.reset(); ret clone; } }