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 timedLog currentTime currentPrice feedNote direction digitizer maxDebt direction oldPrice startingPrice strategyCrest maxInvestment maxDrawdown 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"; // Which exchange are we using ("Bitget", "ByBit" etc.) settableWithVar S exchangeName; new Ref tradingAccount; // Optional: Exchange we get our price data from, if different from above settableWithVar S priceDataFromExchange; // An identifier of the trading account (as of yet unused) settableWithVar S accountName; // 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; // should migrate to the timedLog completely settableWithVar new LS log; // O is always a string for now settableWithVar new L> timedLog; 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; // maximum crypto we can bet on settableWithVar double maxCrypto = infinity(); settableWithVar double maxInvestment; // highest value of coinProfit settableWithVar double strategyCrest; // Maximum drawdown seen on account (realized+unrealized) settableWithVar double maxDrawdown; settableWithVar double cellSize = 1; // position options settableWithVar double leverage = 1; settableWithVar double marginPerPosition = 1; // in USDT // optional (for compounding): how much of the account's equity to risk per position settableWithVar double riskPerTrade; // in percent // Equity risked in last trade settableWithVar double riskedEquity; 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; // Set to the actual ticker used by the system settableWithVar transient TickerSequence actualTicker; // How many minutes to wait after closing a position // before a new one can be opened (0 for no cooldown) settableWithVar double cooldownMinutes; // An image showing this strategy's current state settable transient BufferedImage image; settable transient byte[] imageForWebServer; // Initial assumed equity for backtest. // If not zero, marginPerPosition will be scaled up and down // relative to compoundingBaseEquity+realizedCoinProfit() settableWithVar double compoundingBaseEquity; // Strategies are listed according to this key field (alphanum-IC) settableWithVar S listOrder; // add fields here A log(A o) { if (o != null) { S s = str(o); long time = currentTime(); log.add(printIf(logToPrint, "[" + formatLocalDateWithSeconds(time) + "] " + s)); timedLog.add(withTimestamp(time, s)); change(); } ret o; } void logVars(O... o) { log(renderVars(o)); } LS activationStatus() { ret llNempties( active ? "Active" : "Inactive", exchangeName, 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; // TODO: This doesn't belong here settableWithVar int direction; // maximum debt seen settableWithVar double maxDebt; //settableWithVar double minCoinProfit; //settableWithVar double maxBoundCoin; class Position { gettable S positionID = aGlobalID(); 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 S openOrderID; // open order ID returned from platform settable bool closedOnMarket; settable bool dontCloseOnMarket; settable S comment; settable L juicers; // highest and lowest unrealized P&L seen (after leverage) settable double crest = negativeInfinity(); settable double antiCrest = infinity(); G22TradingStrategy strategy() { ret G22TradingStrategy.this; } { if (!dynamicObjectIsLoading()) { marginToUse = marginToUseForNewPosition(); 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()); } settable Double imaginaryProfit; double coinProfit() { try object imaginaryProfit; ret coinProfitAtPrice(workingPrice()); } bool isOpenError() { ret openError != null; } double coinProfitAtPrice(double price) { ret isOpenError() ? 0 : 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; if (!isOpenError()) closedPositions.add(this); addToRealizedStats(); closeOnMarket(); log(this); //print(this); //printPositions(); } Position partialClose(double amount, O closeReason) { new Position p; p.cryptoAmount = cryptoAmount-amount; log("Partial close (" + amount + ") of " + this + ", creating remainder position with amount " + p.cryptoAmount + ". Reason: " + closeReason); cryptoAmount = amount; ret openPosition(p, (int) direction, closeReason); } void makeFake(double coinProfit) { closeReason("Fake"); imaginaryProfit(coinProfit); closedPositions.add(this); addToRealizedStats(); log(this); } void addToRealizedStats { double cp = coinProfit(); realizedProfit += profit(); realizedCoinProfit += cp; if (cp > 0) { realizedWins++; realizedCoinWins += cp; } else { realizedLosses++; realizedCoinLosses += cp; } change(); } S winnerOrLoser() { var profit = coinProfit(); if (profit == 0) ret "NEUTRAL"; if (profit > 0) ret "WIN"; ret "LOSS"; } // Position.toString toString { ret commaCombine( spaceCombine( !closed() ? null : winnerOrLoser() + appendBracketed(strOrNull(closeReason)) + appendBracketed(comment) + " " + formatLocalDateWithSeconds(closingTime()) + " " + formatProfit(profit()) + "% (" + marginCoin + " " + formatMarginProfit(coinProfit()) + ")" + " (min " + formatProfit(antiCrest()) + ", max " + formatProfit(crest()) + ")" + ", " + formatDouble1(leverage) + "X " + upper(type()) + " held " + formatHoursMinutesColonSeconds(duration()), ), "opened " + formatLocalDateWithSeconds(openingTime()) + (openReason == null ? "" : " " + roundBracket(str(openReason))), "before leverage: " + formatProfit(profitBeforeLeverage()) + "%", "margin: " + marginCoin + " " + formatMarginPrice(margin()), "crypto: " + formatPrice(cryptoAmount), "opening price: " + formatPriceX(openingPrice) + (isNaN(digitizedOpeningPrice()) ? "" : " (digitized: " + formatPrice(digitizedOpeningPrice()) + ")") + (openingStep == 0 ? "" : " @ step " + openingStep), !closed() ? null : "closing price: " + formatPriceX(closingPrice) + (closingStep == 0 ? "" : " @ step " + closingStep), ); } void closeOnMarket { if (dontCloseOnMarket) ret; if (isOpenError()) 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); var order = new IFuturesMarket.OpenOrder() .clientOrderID(positionID()) .holdSide(HoldSide.fromInt(direction)) .cryptoAmount(cryptoAmount) .leverage(leverage) .isCross(true); market.openPosition(order); openOrderID(order.orderID()); openedOnMarket(true); change(); } } catch e { openError(toPersistableThrowable(e)); log("Open error: " + getStackTrace(e)); positionsThatFailedToOpen.add(this); close("Open error"); } } void updateStats { double profit = profit(); crest(max(crest, profit)); antiCrest(min(antiCrest, profit)); } void addJuicer(AbstractJuicer juicer) { juicers(listCreateAndAdd(juicers, juicer)); } void removeJuicer(AbstractJuicer juicer) { juicers(removeDyn(juicers, juicer)); } // Call this when position was closed manually on the platform void notification_closedOutsideOfStrategy() { closedOnMarket(true); close("Closed outside of strategy"); } } // end of Position gettable double currentPrice = 0; gettable double oldPrice = Double.NaN; gettable double startingPrice = Double.NaN; settable long startTime; settableWithVar double realizedProfit; settableWithVar double realizedCoinProfit; settableWithVar int realizedWins; settableWithVar double realizedCoinWins; settableWithVar int realizedLosses; settableWithVar 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 { double coinProfit = coinProfit(); maxDebt(max(maxDebt, debt())); //minCoinProfit = min(minCoinProfit, coinProfit); //maxBoundCoin = max(maxBoundCoin, boundCoin()); maxInvestment(max(maxInvestment, investment())); strategyCrest(max(strategyCrest, coinProfit)); maxDrawdown(max(maxDrawdown, strategyCrest-coinProfit)); if (takeCoinProfitEnabled() && coinProfit >= takeCoinProfit) { log("Taking coin profit."); closeMyself(); } for (p : openPositions) p.updateStats(); } 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); bool inCooldown() { if (cooldownMinutes <= 0) false; if (nempty(openPositions)) true; var lastClosed = last(closedPositions); if (lastClosed == null) false; var minutes = toMinutes(currentTime()-lastClosed.closingTime); ret minutes < cooldownMinutes; } void assureCanOpen(Position p) { if (cooldownMinutes > 0) { if (nempty(openPositions)) fail("Already have an open position"); var lastClosed = last(closedPositions); if (lastClosed != null) { var minutes = toMinutes(currentTime()-lastClosed.closingTime); if (minutes < cooldownMinutes) fail("Last position closed " + formatMinutes(fromMinutes(minutes)) + " ago, cooldown is " + cooldownMinutes); } } }

P openPosition(P p, int direction, O openReason default null) { try { assureCanOpen(p); 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); // Calculate quantity (cryptoAmount) from margin // unless cryptoAmount is already set if (p.cryptoAmount == 0) p.cryptoAmount = p.marginToUse/realPrice*leverage; // Round cryptoAmount to the allowed increments p.cryptoAmount = roundTo(cryptoStep, p.cryptoAmount); // Clamp to minimum/maximum order p.cryptoAmount = clamp(p.cryptoAmount, minCrypto, maxCrypto); log(renderVars("openPosition", +marginPerPosition, +realPrice, +leverage, cryptoAmount := p.cryptoAmount, +cryptoStep)); p.open(); //print("Opening " + p); //printPositions(); p.openOnMarket(); ret p; } on fail e { log(e); } } 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())), (riskPerTrade == 0 ? "" : "Risk per trade: " + formatDouble1(riskPerTrade) + "%. ") + "Position size: " + marginCoin + " " + formatPrice(marginPerPosition) + "x" + formatDouble1(leverage) + " = " + marginCoin + " " + formatPrice(positionSize()), !usingCells() ? null : "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( tradingAccount, 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()); } // Returns closed positions L closeAllPositions(O reason default "User close") { var positions = openPositions(); closePositions(positions, reason); ret (L) positions; } void closeMyself() { closedItself(currentTime()); closeAllPositionsAndDeactivate(); } void closeAllPositionsAndDeactivate { deactivate(); closeAllPositions(); } void deactivate { market(null); 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(); } // drift is our cumulative delta (=sum of all signed position // quantities) double drift() { double drift = 0; for (p : 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 = marginToUseForNewPosition(); ret openPosition(p, direction, openReason); } // Open or close positions until the drift (delta) is // equal to targetDrift (or as close as the platform's // restrictions allow). // Returns the list of positions opened or closed // (Function is in dev.) L adjustDrift(double targetDrift, O reason default null) { new L changeList; // target 0? close all if (targetDrift == 0) ret closeAllPositions(reason); double drift = drift(); // target already reached? done if (drift == targetDrift) ret changeList; int direction = sign(targetDrift); // Are we changing direction? Then close everything first. if (sign(drift) != direction) changeList.addAll(closeAllPositions(reason)); // Now we know targetDrift and drift have the same sign. double diff = abs(targetDrift)-abs(drift); // Round to allow increments diff = roundTo(cryptoStep, diff); if (diff > 0) { // We need to open a new position - that's easy new Position p; p.cryptoAmount = diff; changeList.add(openPosition(p, direction, reason)); } else { double toClose = -diff; // We need to close positions - that's a bit more tricky // Filter by direction to be sure in case there is // hedging (you shouldn't do hedging, really, though) var positions = filter(openPositions(), p -> p.direction() == direction); // Let's look for an exact size match first. // We rounded to cryptoStep, so using == is ok. var _toClose = toClose; var exactMatch = firstThat(positions, p -> p.cryptoAmount == _toClose); if (exactMatch != null) { exactMatch.close(reason); changeList.add(exactMatch); ret changeList; } // No exact match. Go through positions starting with // the oldest one. for (p : positions) { toClose = roundTo(cryptoStep, toClose); if (toClose == 0) break; if (toClose >= p.cryptoAmount) { // Need to close the whole position toClose -= p.cryptoAmount; p.close(reason); changeList.add(p); } else { // Need a partial close. changeList.add(p); var remainderPosition = p.partialClose(toClose, reason); changeList.add(remainderPosition); } } } ret changeList; } L winners() { ret filter(closedPositions(), p -> p.coinProfit() > 0); } L losers() { ret filter(closedPositions(), p -> p.coinProfit() < 0); } L openPositions() { ret cloneList(openPositions); } double winRate() { ret l(winners()) * 100.0 / l(closedPositions); } bool usingCells() { true; } double positionSize() { ret marginToUseForNewPosition()*leverage; } double marginToUseForNewPosition() { ret scaleMargin(marginPerPosition); } // to simulate compounding in backtest double scaleMargin(double margin) { // Live? Then no scaling. if (usingLiveData) ret margin; ret margin*compoundingFactor(); } double compoundingFactor() { // No compounding selected? if (compoundingBaseEquity == 0) ret 1; ret max(0, remainingEquity()/compoundingBaseEquity); } // only for backtesting double remainingEquity() { ret compoundingBaseEquity+realizedCoinProfit(); } class RiskToMargin { // in percent settable double riskForTrade = riskPerTrade; settable double stopLossPercent; settable double price = currentPrice(); settable double fullEquity; double riskedEquity() { ret fullEquity*riskForTrade/100; } double qty() { ret riskedEquity()/(price*stopLossPercent/100); } double margin aka get() { ret qty()*price/leverage(); } } void fixRealizedStats { realizedProfit(0); realizedCoinProfit(0); realizedWins(0); realizedCoinWins(0); realizedLosses(0); realizedCoinLosses(0); for (p : closedPositions()) p.addToRealizedStats(); } // p must not be open bool deletePosition(Position p) { if (closedPositions.remove(p) || positionsThatFailedToOpen.remove(p)) { fixRealizedStats(); true; } false; } void deletePositionsThatFailedToOpen() { for (p : cloneList(positionsThatFailedToOpen)) deletePosition(p); } void deleteAllPositions() { for (p : cloneList(closedPositions)) deletePosition(p); } bool hasRiskPerTrade() { ret riskPerTrade != 0; } Position addFakePosition(double coinProfit) { new Position p; p.makeFake(coinProfit); ret p; } // If there is exactly one open position: // Open another position in the same direction // with same size Position pyramid(O openReason default "Pyramiding") { ret extendedPyramid(2, openReason); } // level: pyramid level to be reached // (must be one higher than current number of positions // to trigger - no double adding at once) Position extendedPyramid(int level, O openReason default "Pyramiding") { if (l(openPositions()) != level-1) null; var p = first(openPositions); new Position p2; p2.cryptoAmount(p.cryptoAmount); ret openPosition(p2, sign(p.direction), openReason); } }