abstract concept G22TradingStrategy extends ConceptWithChangeListeners is Comparable, Juiceable { S fieldsToReset aka fieldsToReset_G22TradingStrategy() { ret [[ globalID active q archived primed usingLiveData backDataFed startTime deactivated closedItself log currentTime currentPrice feedNote direction digitizer maxDebt direction oldPrice startingPrice maxInvestment realizedProfit realizedCoinProfit realizedWins realizedCoinWins realizedLosses realizedCoinLosses stepCount stepSince openPositions closedPositions positionsThatFailedToOpen drift driftSystem ]]; } settable bool verbose; gettable S globalID = aGlobalID(); // user-given name, overriding the technical name settable S customName; // 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; // set to date when strategy was deactivated (by itself or by the user) settableWithVar long deactivated; // 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; // Juicer that is used to stop the whole strategy depending // on its profit (optional) settableWithVar AbstractJuicer strategyJuicer; swappable long currentTime() { ret now(); } settable transient bool logToPrint = true; // Note from price feeder settableWithVar S feedNote; // optimization to save fees settableWithVar bool useDriftSystem; // The drift system we are connected to settableWithVar G22DriftSystem driftSystem; A log(A o) { if (o != null) { log.add(printIf(logToPrint, "[" + formatLocalDateWithSeconds(currentTime()) + "] " + str(o))); change(); } ret o; } void logVars(O... o) { log(renderVars(o)); } 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 -> sign(p.direction) == sign(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()); } // TODO: remove 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 marginToUse; 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 openReason; 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()) { marginToUse = marginPerPosition; 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(workingPrice()); } 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(); log(this); //print(this); //printPositions(); } S winnerOrLoser() { var profit = coinProfit(); if (profit == 0) ret "NEUTRAL"; if (profit > 0) ret "WINNER"; ret "LOSS"; } // Position.toString toString { ret commaCombine( spaceCombine( !closed() ? null : winnerOrLoser() + " " + formatLocalDateWithSeconds(closingTime()) + ", duration " + formatHoursMinutesColonSeconds(duration()), formatDouble1(leverage) + "X " + upper(type()) ), "opened " + formatLocalDateWithSeconds(openingTime()) + (openReason == null ? "" : " " + roundBracket(str(openReason))), !closed() ? null : "closed because: " + 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; if (openError != null) ret with log("Not closing because open error: " + this); 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; log("Opening: " + this); 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); positionsThatFailedToOpen.add(this); close("Open error"); } } } gettable double currentPrice = 0; gettable double oldPrice = Double.NaN; gettable double startingPrice = Double.NaN; settable long startTime; gettable double realizedProfit; gettable double realizedCoinProfit; gettable int realizedWins; gettable double realizedCoinWins; gettable int realizedLosses; gettable double realizedCoinLosses; settableWithVar long stepCount; settableWithVar long stepSince; new LinkedHashSet openPositions; gettable new L closedPositions; gettable new L positionsThatFailedToOpen; 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() && sign(p.direction) == sign(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 == null ? Double.NaN : digitizer.digitizedPrice(); } double lastDigitizedPrice() { ret digitizer == null ? Double.NaN : digitizer.lastDigitizedPrice(); } int digitizedCellNumber() { ret digitizer == null ? 0 : digitizer.cellNumber(); } 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 "-"; S s = formatPrice(price); if (cells() == null) ret s; double num = cells().priceToCellNumber(price); ret s + " (C" + formatDouble2(num) + ")"; } abstract void price aka currentPrice(double price);

P openPosition(P p, int direction, O openReason default null) { p.openReason(openReason); var price = digitizedPrice(); var realPrice = currentPrice(); logVars("openPosition", +realPrice, +price, +digitizer); if ((isNaN(price) || price == 0) && digitizer != null) { price = digitizer.digitizeIndividually(currentPrice()); print("digitized individually: " + price); } p.openingPrice(realPrice); p.direction(direction); p.digitizedOpeningPrice(price); double cryptoAmount = p.marginToUse/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) + (isNaN(digitizedPrice()) ? "" : ", 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()), strategyJuicer == null ?: "Strategy juicer: " + strategyJuicer, "Drift: " + cryptoCoin + " " + plusMinusFix(formatCryptoAmount(drift())), ); } 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 ? "latest first" : "oldest first") + "):", 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(O reason default "User close") { closePositions(cloneList(openPositions()), reason); } void closeMyself() { closedItself(currentTime()); closeAllPositionsAndDeactivate(); } void closeAllPositionsAndDeactivate { deactivate(); closeAllPositions(); } void deactivate { if (!active) ret; active(false); deactivated(currentTime()); log("Strategy deactivated."); } void reset aka reset_G22TradingStrategy() { resetFields(this, fieldsToReset()); change(); } selfType emptyClone aka emptyClone_G22TradingStrategy() { 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 aka currentTime(long time) { int age = ifloor(ageInHours()); long lastMod = mod(currentTime()-startTime, hoursToMS(1)); currentTime = -> time; if (ifloor(ageInHours()) > age) log("Hourly profit log: " + formatMarginProfit(coinProfit())); this; } double ageInHours() { ret startTime == 0 ? 0 : msToHours(currentTime()-startTime); } // only use near start of strategy, untested otherwise selfType changeCellSize(double newCellSize) { double oldCellSize = cellSize(); if (oldCellSize == newCellSize) this; cellSize(newCellSize); if (digitizer != null) digitizer.swapPriceCells(makePriceCells(priceCells().basePrice())); log("Changed cell size from " + oldCellSize + " to " + newCellSize); this; } bool hasClosedItself() { ret closedItself != 0; } public double juiceValue() { ret coinProfit(); } double drift() { double drift = 0; for (p : cloneList(openPositions())) drift += p.cryptoAmount()*p.direction(); ret drift; } S formatCryptoAmount(double amount) { ret formatDouble3(amount); } Position openShort() { ret openPosition(-1); } Position openLong() { ret openPosition(1); } Position openPosition(int direction, O openReason default null) { new Position p; p.marginToUse = marginPerPosition; ret openPosition(p, direction, openReason); } L winners() { ret filter(closedPositions(), p -> p.coinProfit() > 0); } L losers() { ret filter(closedPositions(), p -> p.coinProfit() < 0); } L openPositions() { ret cloneList(openPositions); } }