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; settable bool alwaysOpenTwoPositions; // position options settable double leverage = 1; settable double marginPerPosition = 1; // in USDT // link to market (optional) settable IFuturesMarket market; settable double adversity = 0.2; settable new LS log; // internal settable PriceDigitizer2 digitizer; gettable int direction; settable double epsilon = 1e-6; // maximum debt seen gettable double maxDebt; // increment of crypto we can bet on settable double cryptoStep = 0.0001; // minimum crypto we need to bet on settable double minCrypto = 0.0001; record noeq Position(double openingPrice, double direction, double digitizedOpeningPrice) { double closingPrice = Double.NaN; long openingStep = stepCount, closingStep; long openingTime = currentTime(), closingTime; double leverage = Corridor.this.leverage; 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)/openingPrice*direction*leverage*100-adversity; } 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(); closingStep = stepCount; closedPositions.add(this); realizedProfit += profit(); print(this); printPositions(); } toString { ret commaCombine( spaceCombine( closed() ? "Closed" : null, upper(type()) ), !closed() ? null : or(closeReason, "unknown reason"), "profit: " + formatProfit(profit()), "crypto: " + /*formatPrice*/(cryptoAmount), "opening price: " + formatPriceX(openingPrice) + " @ step " + openingStep, !closed() ? null : "closing price: " + formatPriceX(closingPrice) + " @ step " + closingStep, //"digitized: " + formatPrice(digitizedOpeningPrice()), ); } } gettable double currentPrice = 0; gettable double oldPrice = Double.NaN; gettable double startingPrice = Double.NaN; gettable int lastDirection; gettable double realizedProfit; gettable long 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); double cryptoAmount = marginPerPosition/price*leverage; cryptoAmount = roundTo(cryptoStep, cryptoAmount); p.cryptoAmount = max(minCrypto, cryptoAmount); 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 -> 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 (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 PriceDigitizer2(priceCells); digitizer.verbose(verbose); //digitizer.init(startingPrice); print("Starting CORRIDOR at " + startingPrice + " +/- " + cellSize); if (openTwoPositionsAtStart) { ++stepCount; 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 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; 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(); 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) : longPositionsAtOrAboveDigizitedPrice(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()); } double fromCellNumber(double cellNumber) { ret cells().fromCellNumber(cellNumber); } double toCellNumber(double price) { ret cells().toCellNumber(price); } 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 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, -unrealizedProfit()); } double profit() { ret realizedProfit+unrealizedProfit(); } LS status() { var r = currentCorridorPriceRange(); ret ll( "Current price: " + formatPriceX(currentPrice) + ", digitized: " + formatPriceX(digitizedPrice()), "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) + ")", ); } LS fullStatus() { ret listCombine( status(), "", n2(openPositions, "open position") + ":", openPositions, "", n2(closedPositions, "closed position") + ":", reversed(closedPositions) ); } S formatProfit(double x) { ret dropMinusFromZero(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) { if (isNaN(price)) ret "-"; double num = cells().priceToCellNumber(price); ret formatPrice(price) + " (C" + formatDouble2(num) + ")"; } }