set flag Reparse. persistable sclass Corridor { // how many percent the crypto price needs to move // for one profitable trade settable double cellSize = 0.2; // how many cells the corridor has settable int capacity = 6; settable bool verbose; settable bool openTwoPositionsAtStart = true; // position options settable double leverage = 1; settable double marginPerPosition = 1; // in USDT // link to market (optional) settable IFuturesMarket market; settable new LS log; // internal settable PriceDigitizer digitizer; // maximum debt seen gettable double maxDebt; record noeq Position(double openingPrice, double direction, double digitizedOpeningPrice) { double closingPrice = Double.NaN; long openingTime = currentTime(), closingTime; settable double cryptoAmount; settable O closeReason; 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)*direction; } double profit() { ret profitAtPrice(closed() ? closingPrice : currentPrice()); } void close(O closeReason) { if (closed()) fail("Can't close again"); if (closeReason != null) closeReason(closeReason); openPositions.remove(this); closingPrice = currentPrice(); closingTime = currentTime(); closedPositions.add(this); realizedProfit += profit(); print(this); printPositions(); } toString { ret commaCombine( spaceCombine( closed() ? "Closed" : null, upper(type()) ), !closed() ? null : or(closeReason, "unknown reason"), "opening price: " + formatPriceX(openingPrice), //"digitized: " + formatPrice(digitizedOpeningPrice()), "profit: " + formatProfit(profit())); } } gettable double currentPrice = 0; gettable double oldPrice = Double.NaN; gettable double startingPrice = Double.NaN; gettable int lastDirection; gettable double realizedProfit; gettable int stepCount; gettable new LinkedHashSet openPositions; gettable new L closedPositions; Position openPosition(double direction) { var price = digitizedPrice(); if (isNaN(price)) price = digitizer.digitizeIndividually(currentPrice()); var p = new Position(currentPrice(), direction, price); openPositions.add(p); print("Opening " + p); printPositions(); 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 -> p.digitizedOpeningPrice() == digitizedPrice && 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 (isNaN(currentPrice())) fail("Please feed me the first price before starting me"); // Save starting price, create price cells & digitizer startingPrice = currentPrice(); var priceCells = makePriceCells(startingPrice); digitizer = new PriceDigitizer(priceCells); digitizer.verbose(verbose); print("Starting CORRIDOR at " + startingPrice + " +/- " + cellSize); if (openTwoPositionsAtStart) { openPosition(1); openPosition(-1); } } 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 currentPrice aka price(double price) { currentPrice = price; if (digitizer == null) ret; digitizer.digitize(price); if (isNaN(digitizedPrice())) // Haven't crossed a cell limit yet ret; int direction = sign(digitizedPrice()-lastDigitizedPrice()); if (direction == 0) ret; // No cell move ++stepCount; printWithPrecedingNL("New digitized price: " + digitizedPrice()); if (started()) step(); print(this); } swappable void step { double p1 = lastDigitizedPrice(); double p2 = digitizedPrice(); int direction = sign(p2-p1); if (direction == 0) ret; // Find winning positions to close new L toClose; for (Position p : direction > 0 ? longPositionsAtOrAboveDigitizedPrice(p1) : shortPositionsAtOrBelowDigitizedPrice(p1)) { p.closeReason("WIN"); log("Closing winner position at " + currentPrice + " (" + p1 + " -> " + p2 + "): " + p); toClose.add(p); } // Close forward positions "behind us" //Position closed = closePosition(p1, direction); // Dismantle corridor if too large for (Position p : direction > 0 ? shortPositionsAtOrBelowPrice(digitizer.cells.nCellLimitsDown(digitizedPrice(), capacity)) : longPositionsAtOrAbovePrice(digitizer.cells.nCellLimitsUp(digitizedPrice(), capacity))) { p.closeReason("DISMANTLE"); toClose.add(p); } maxDebt = min(maxDebt, debt()); //printVars("step", +p1, +p2, +direction, +closed, +opened); if (nempty(toClose)) closePositions(toClose); // Open backward position here (if not there yet) Position opened = null; if (!hasPosition(p2, -direction)) opened = openPosition(-direction); } double unrealizedProfit() { ret doubleSum(map(openPositions, ->.profit())); } 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 longPositionsAtOrAboveDigitizedPrice(double openingPrice) { ret filter(openPositions, p -> p.isLong() && p.digitizedOpeningPrice() >= openingPrice); } L negativePositions() { ret filter(openPositions, p -> p.profit() < 0); } double debt() { ret doubleSum(map(negativePositions(), ->.profit())); } double profit() { ret realizedProfit+unrealizedProfit(); } LS status() { var r = currentCorridorPriceRange(); ret ll( "Cell size: " + formatPercentage(cellSize, 3), "Max capacity: " + capacity, "Current corridor: " + (r == null ? "-" : formatPrice(r.start) + " to " + formatPrice(r.end) + " (" + formatPercentage(currentCorridorSizeInPercent(), 3) + ")"), "Step " + n2(stepCount), "Profit: " + formatProfit(profit()), "Realized profit: " + formatProfit(realizedProfit) + " from " + n2(closedPositions, "closed position"), "Unrealized profit: " + formatProfit(unrealizedProfit()) + " in " + n2(openPositions, "open position"), "Debt: " + formatProfit(debt()) + " (max seen: " + formatProfit(maxDebt) + ")", ); } S formatProfit(double x) { ret addPlusIfPositive(formatDouble1(x)); } toString { ret commaCombine(status()); } double maxCorridorPercentSize() { ret capacity*cellSize; } PriceCells cells() { ret digitizer?.cells; } swappable long currentTime() { ret now(); } DoubleRange currentCorridorPriceRange() { L openingPrices = map(openPositions, ->.openingPrice); ret doubleRange(doubleMin(openingPrices), doubleMax(openingPrices)); } double currentCorridorSizeInPercent() { DoubleRange r = currentCorridorPriceRange(); ret r == null ? 0 : asPercentIncrease(r.end, r.start); } void log(O o) { if (o != null) log.add(print(str(o))); } S formatPriceX(double price) { double num = cells().priceToCellNumber(price); ret formatPrice(price) + " (C" + formatDouble2(num) + ")"; } }