abstract concept G22TradingStrategy extends ConceptWithChangeListeners is Comparable { S fieldsToReset() { ret [[ globalID active q archived primed usingLiveData doingRealTrades backDataFed startTime log currentTime currentPrice feedNote direction lastDirection digitizer maxDebt direction oldPrice startingPrice maxInvestment realizedProfit realizedCoinProfit realizedWins realizedCoinWins realizedLosses realizedCoinLosses stepCount stepSince openPositions closedPositions ]]; } settable bool verbose; gettable S globalID = aGlobalID(); // user-given comment settable S comment; gettable transient Q q = startQ(); // link to market (optional) transient settable IFuturesMarket market; settable bool mergePositionsForMarket; settableWithVar S cryptoCoin; settableWithVar S marginCoin = "USDT"; // primed = started but waiting for first price settableWithVar bool primed; settableWithVar bool usingLiveData; settableWithVar bool doingRealTrades; settableWithVar bool demoCoin; settableWithVar bool active; // set to date when strategy ended itself settableWithVar long closedItself; // moved to archive (out of main list) settableWithVar bool archived; // area where this strategy is held, e.g. "Candidates" settableWithVar S area; settableWithVar double adversity = 0.2; settableWithVar new LS log; settableWithVar double epsilon = 1e-6; // increment of crypto we can bet on settableWithVar double cryptoStep = 0.0001; // minimum crypto we need to bet on settableWithVar double minCrypto = 0.0001; settableWithVar double maxInvestment; settableWithVar double cellSize = 1; // position options settableWithVar double leverage = 1; settableWithVar double marginPerPosition = 1; // in USDT settableWithVar bool showClosedPositionsBackwards = true; // End strategy when this margin coin profit is reached. settableWithVar double takeCoinProfit = infinity(); settableWithVar bool takeCoinProfitEnabled; // How many hours of recent back data this algorithm likes // to be fed with prior to running live. settableWithVar double backDataHoursWanted; // Did we feed the back data? settableWithVar bool backDataFed; swappable long currentTime() { ret now(); } settable transient bool logToPrint = true; // Note from price feeder settableWithVar S feedNote; void log(O o) { if (o != null) { log.add(printIf(logToPrint, "[" + formatLocalDateWithSeconds(currentTime()) + "] " + str(o))); change(); } } LS activationStatus() { ret llNempties( active ? "Active" : "Inactive", empty(cryptoCoin) ? null : "coin: " + cryptoCoin + " over " + marginCoin + stringIf(demoCoin, " (demo coin)"), stringIf(market != null, "connected to market"), stringIf(usingLiveData, "using live data"), stringIf(doingRealTrades, "real trades") ); } 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() { ret unrealizedCoinProfitAtPrice(currentPrice); } double unrealizedCoinProfitAtPrice(double price) { double sum = 0; for (p : openPositions) sum += p.coinProfitAtPrice(price); 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 negativePositions() { ret filter(openPositions, p -> p.profit() < 0); } double debt() { ret max(0, -unrealizedCoinProfit()); } double profit() { ret realizedProfit+unrealizedProfit(); } double coinProfit() { ret realizedCoinProfit+unrealizedCoinProfit(); } double coinProfitAtPrice(double price) { ret realizedCoinProfit+unrealizedCoinProfitAtPrice(price); } S formatProfit(double x) { ret plusMinusFix(formatDouble1(x)); } settableWithVar PriceDigitizer2 digitizer; settableWithVar int direction; // maximum debt seen settableWithVar double maxDebt; //settableWithVar double minCoinProfit; //settableWithVar double maxBoundCoin; class Position { settable double openingPrice; settable double direction; settable double digitizedOpeningPrice; gettable 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; settable bool dontCloseOnMarket; G22TradingStrategy strategy() { ret G22TradingStrategy.this; } { if (!dynamicObjectIsLoading()) { openingStep = stepCount; leverage = G22TradingStrategy.this.leverage; openingTime = currentTime(); } } bool isLong() { ret direction > 0; } bool isShort() { ret direction < 0; } bool closed() { ret !isNaN(closingPrice); } S type() { ret trading_directionToPositionType(direction); } long closingOrCurrentTime() { ret closed() ? closingTime() : currentTime(); } long duration() { ret closingOrCurrentTime()-openingTime(); } double profitAtPrice(double price) { ret profitAtPriceBeforeLeverage(price)*leverage; } double profitAtPriceBeforeLeverage(double price) { ret ((price-openingPrice)/openingPrice*direction*100-adversity); } double workingPrice() { ret closed() ? closingPrice : currentPrice(); } double profit() { ret profitAtPrice(workingPrice()); } double profitBeforeLeverage() { ret profitAtPriceBeforeLeverage(workingPrice()); } double coinProfit() { ret coinProfitAtPrice(currentPrice); } double coinProfitAtPrice(double price) { ret profitAtPrice(price)/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); double cp = coinProfit(); realizedProfit += profit(); realizedCoinProfit += cp; if (cp > 0) { realizedWins++; realizedCoinWins += cp; } else { realizedLosses++; realizedCoinLosses += cp; } change(); closeOnMarket(); //print(this); //printPositions(); } // Position.toString toString { ret commaCombine( spaceCombine( !closed() ? null : "Closed " + formatLocalDateWithSeconds(closingTime()) + ", duration " + formatHoursMinutesColonSeconds(duration()), formatDouble1(leverage) + "X " + upper(type()) ), "opened " + formatLocalDateWithSeconds(openingTime()), !closed() ? null : or(closeReason, "unknown reason"), "profit: " + formatProfit(profit()) + "% (" + marginCoin + " " + formatMarginProfit(coinProfit()) + ")", "before leverage: " + formatProfit(profitBeforeLeverage()) + "%", "margin: " + marginCoin + " " + formatMarginPrice(margin()), "crypto: " + formatPrice(cryptoAmount), "opening price: " + formatPriceX(openingPrice) + " (digitized: " + formatPrice(digitizedOpeningPrice()) + ") @ step " + openingStep, !closed() ? null : "closing price: " + formatPriceX(closingPrice) + " @ step " + closingStep, ); } void closeOnMarket { if (dontCloseOnMarket) ret; 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); change(); } 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; settable long startTime; gettable int lastDirection; gettable double realizedProfit; gettable double realizedCoinProfit; gettable int realizedWins; gettable double realizedCoinWins; gettable int realizedLosses; gettable double realizedCoinLosses; settableWithVar long stepCount; settableWithVar long stepSince; gettable new LinkedHashSet openPositions; gettable new L closedPositions; void closeOnMarketMerged(Cl positions) { if (market == null) ret; closeOnMarketMerged_oneDirection(filter(positions, -> .isShort())); closeOnMarketMerged_oneDirection(filter(positions, -> .isLong())); } // all positions must have the same direction void closeOnMarketMerged_oneDirection(L positions) { if (empty(positions)) ret; double direction = first(positions).direction; double cryptoAmount = doubleSum(positions, -> .cryptoAmount); log("Closing on market: " + positions); try { market.closePosition(new IFuturesMarket.CloseOrder() .holdSide(HoldSide.fromInt(direction)) .cryptoAmount(cryptoAmount); for (p : positions) p.closedOnMarket(true); change(); } catch e { var e2 = toPersistableThrowable(e); for (p : positions) p.closeError(e2); } } 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) { if (mergePositionsForMarket) { for (p : positions) p.dontCloseOnMarket(true).close(closeReason); closeOnMarketMerged(positions); } else forEach(positions, -> .close(closeReason)); } Position findPosition(double digitizedPrice, double direction) { ret firstThat(openPositions(), p -> diffRatio(p.digitizedOpeningPrice(), digitizedPrice) <= epsilon() && p.direction == direction); } void printPositions { print(colonCombine(n2(openPositions, "open position"), joinWithComma(openPositions))); } bool started() { ret !isNaN(startingPrice); } void prices(double... prices) { fOr (price : prices) { if (!active()) ret; price(price); } } swappable PriceCells makePriceCells(double basePrice) { ret new GeometricPriceCells(basePrice, cellSize); } double digitizedPrice() { ret digitizer.digitizedPrice(); } double lastDigitizedPrice() { ret digitizer.lastDigitizedPrice(); } void handleNewPriceInQ(double price) { q.add(-> price(price)); } void nextStep { ++stepCount; stepSince(currentTime()); } void afterStep { maxDebt(max(maxDebt, debt())); //minCoinProfit = min(minCoinProfit, coinProfit()); //maxBoundCoin = max(maxBoundCoin, boundCoin()); maxInvestment(max(maxInvestment, investment())); if (takeCoinProfitEnabled() && coinProfit() >= takeCoinProfit) { log("Taking coin profit."); closeMyself(); } } 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); } 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); } PriceCells cells aka priceCells() { ret digitizer?.cells; } S formatPriceX(double price) { if (isNaN(price)) ret "-"; double num = cells().priceToCellNumber(price); ret formatPrice(price) + " (C" + formatDouble2(num) + ")"; } abstract void price aka currentPrice(double price);

P openPosition(P p, int direction) { var price = digitizedPrice(); var realPrice = currentPrice(); printVars("openPosition", +realPrice, +price, +digitizer); if (isNaN(price) || price == 0) { price = digitizer.digitizeIndividually(currentPrice()); print("digitized individually: " + price); } p.openingPrice(realPrice); p.direction(direction); p.digitizedOpeningPrice(price); double cryptoAmount = marginPerPosition/realPrice*leverage; cryptoAmount = roundTo(cryptoStep, cryptoAmount); log(renderVars("openPosition", +marginPerPosition, +realPrice, +leverage, +cryptoAmount, +cryptoStep)); p.cryptoAmount = max(minCrypto, cryptoAmount); p.open(); //print("Opening " + p); //printPositions(); p.openOnMarket(); ret p; } LS status() { double mulProf = multiplicativeProfit(); ret llNonNulls( empty(comment) ? null : "Comment: " + comment, "Profit: " + marginCoin + " " + plusMinusFix(formatMarginPrice(coinProfit())), "Realized profit: " + marginCoin + " " + formatMarginProfit(realizedCoinProfit) + " from " + n2(closedPositions, "closed position") + " (" + formatMarginProfit(realizedCoinWins) + " from " + n2(realizedWins, "win") + ", " + formatMarginProfit(realizedCoinLosses) + " from " + n2(realizedLosses, "loss", "losses") + ")", "Unrealized profit: " + marginCoin + " " + formatMarginProfit(unrealizedCoinProfit()) + " in " + n2(openPositions, "open position"), isNaN(mulProf) ? null : "Multiplicative profit: " + formatProfit(mulProf) + "%", //baseToString(), !primed() ? null : "Primed", !started() ? null : "Started. current price: " + formatPriceX(currentPrice) + ", digitized: " + formatPriceX(digitizedPrice()), "Leverage: " + leverage + ", margin per position: " + marginCoin + " " + formatPrice(marginPerPosition), "Cell size: " + formatCellSize(cellSize), spaceCombine("Step " + n2(stepCount), renderStepSince()), //"Debt: " + marginCoin + " " + formatMarginProfit(debt()) + " (max seen: " + formatMarginProfit(maxDebt) + ")", "Investment used: " + marginCoin + " " + formatMarginPrice(maxInvestment()), ); } S renderStepSince() { if (stepSince == 0) ret ""; ret "since " + (active() ? formatHoursMinutesColonSeconds(currentTime()-stepSince) : formatLocalDateWithSeconds(stepSince)); } LS fullStatus() { ret listCombine( status(), "", n2(openPositions, "open position") + ":", reversed(openPositions), "", n2(closedPositions, "closed position") + ":", showClosedPositionsBackwards ? reversed(closedPositions) : closedPositions, ); } void feed(PricePoint pricePoint) { if (!active()) ret; setTime(pricePoint.timestamp); price(pricePoint.price); } void feed(TickerSequence ts) { if (!active()) ret; if (ts == null) ret; for (pricePoint : ts.pricePoints()) feed(pricePoint); } public int compareTo(G22TradingStrategy s) { ret s == null ? 1 : cmp(coinProfit(), s.coinProfit()); } void closeAllPositions { closePositions(cloneList(openPositions()), "User close"); } void closeMyself() { closedItself(currentTime()); closeAllPositionsAndDeactivate(); } void closeAllPositionsAndDeactivate { deactivate(); closeAllPositions(); } void deactivate { if (!active) ret; active(false); log("Strategy deactivated."); } void reset { resetFields(this, fieldsToReset()); change(); } selfType emptyClone() { var clone = shallowCloneToUnlistedConcept(this); clone.reset(); ret clone; } L allPositions() { ret concatLists(openPositions, closedPositions); } L sortedPositions() { var allPositions = allPositions(); ret sortedByCalculatedField(allPositions, -> .openingTime()); } bool positionsAreNonOverlapping() { for (a, b : unpair overlappingPairs(sortedPositions())) if (b.openingTime() < a.closingTime()) false; true; } // Profit when applying all positions (somewhat theoretical because you // might go below platform limits) // Also only works when positions are linear double multiplicativeProfit() { if (!positionsAreNonOverlapping()) ret Double.NaN; double profit = 1; for (p : sortedPositions()) profit *= 1+p.profit()/100; ret (profit-1)*100; } bool haveBackData() { ret backDataHoursWanted == 0 | backDataFed; } bool didRealTrades() { ret any(allPositions(), p -> p.openedOnMarket() || p.closedOnMarket()); } S formatCellSize(double cellSize) { ret formatPercentage(cellSize, 3); } S areaDesc() { if (eq(area, "Candidates")) ret "Candidate"; ret nempty(area) ? area : archived ? "Archived" : ""; } selfType setTime(long time) { int age = ifloor(ageInHours()); long lastMod = mod(currentTime()-startTime, hoursToMS(1)); currentTime = -> time; if (ifloor(ageInHours()) > age) log("Hourly profit log: " + formatMarginProfit(profit())); this; } double ageInHours() { ret startTime == 0 ? 0 : msToHours(currentTime()-startTime); } // only use near start of strategy, untested otherwise selfType changeCellSize(double newCellSize) { cellSize(newCellSize); if (digitizer != null) digitizer.swapPriceCells(makePriceCells(priceCells().basePrice())); this; } }