Not logged in.  Login/Logout/Register | List snippets | | Create snippet | Upload image | Upload data

1102
LINES

< > BotCompany Repo | #1036209 // G22TradingStrategy

JavaX fragment (include) [tags: use-pretranspiled]

Transpiled version (30581L) is out of date.

abstract concept G22TradingStrategy extends ConceptWithChangeListeners is Comparable<G22TradingStrategy>, 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<G22TradingAccount> 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<WithTimestamp<O>> 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> 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<Position> positionsInDirection(int direction) {
    ret filter(openPositions, p -> sign(p.direction) == sign(direction));
  }
  
  L<Position> longPositions() { ret positionsInDirection(1); }
  L<Position> shortPositions() { ret positionsInDirection(-1); }
  
  L<Position> shortPositionsAtOrBelowPrice(double openingPrice) {
    ret filter(openPositions, p -> p.isShort() && p.openingPrice <= openingPrice);
  }
  
  L<Position> longPositionsAtOrAbovePrice(double openingPrice) {
    ret filter(openPositions, p -> p.isLong() && p.openingPrice >= openingPrice);
  }
  
  L<Position> 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<AbstractJuicer> 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<Position> openPositions;
  gettable new L<Position> closedPositions;
  gettable new L<Position> positionsThatFailedToOpen;
  
  void closeOnMarketMerged(Cl<? extends Position> 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<? extends Position> 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<? extends Position> 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<Position> shortPositionsAtOrBelowDigitizedPrice(double openingPrice) {
    ret filter(openPositions, p -> p.isShort() && p.digitizedOpeningPrice() <= openingPrice);
  }
  
  L<Position> shortPositionsAtOrAboveDigitizedPrice(double openingPrice) {
    ret filter(openPositions, p -> p.isShort() && p.digitizedOpeningPrice() >= openingPrice);
  }
  
  L<Position> longPositionsAtOrAboveDigitizedPrice(double openingPrice) {
    ret filter(openPositions, p -> p.isLong() && p.digitizedOpeningPrice() >= openingPrice);
  }
  
  L<Position> 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 extends Position> 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<Position> 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<Position> allPositions() {
    ret concatLists(openPositions, closedPositions);
  }
  
  L<Position> 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<Position> adjustDrift(double targetDrift, O reason default null) {
    new L<Position> 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<Position> winners() { ret filter(closedPositions(), p -> p.coinProfit() > 0); }
  L<Position> losers() { ret filter(closedPositions(), p -> p.coinProfit() < 0); }
  
  L<? extends Position> 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);
  }
}

download  show line numbers  debug dex  old transpilations   

Travelled to 4 computer(s): elmgxqgtpvxh, iveijnkanddl, mqqgnosmbjvj, wnsclhtenguj

No comments. add comment

Snippet ID: #1036209
Snippet name: G22TradingStrategy
Eternal ID of this version: #1036209/271
Text MD5: fcd57b3e0b9101d7438e547f58618d6a
Author: stefan
Category: javax / trading
Type: JavaX fragment (include)
Public (visible to everyone): Yes
Archived (hidden from active list): No
Created/modified: 2024-04-11 23:54:31
Source code size: 34719 bytes / 1102 lines
Pitched / IR pitched: No / No
Views / Downloads: 779 / 1909
Version history: 270 change(s)
Referenced in: #1003674 - Standard Classes + Interfaces (LIVE continued in #1034167)
#1036211 - TradingSignal (old)
#1036231 - TradingStrategy (old)
#1036258 - G22TradingStrategy backup