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

1161
LINES

< > BotCompany Repo | #1036209 // G22TradingStrategy

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

Libraryless. Click here for Pure Java version (30808L/195K).

1  
abstract concept G22TradingStrategy extends ConceptWithChangeListeners is Comparable<G22TradingStrategy>, Juiceable {
2  
  S fieldsToReset aka fieldsToReset_G22TradingStrategy() {
3  
    ret [[
4  
      globalID active
5  
      q
6  
      archived primed usingLiveData 
7  
      backDataFed startTime deactivated closedItself
8  
      log timedLog currentTime currentPrice feedNote
9  
      direction
10  
      digitizer maxDebt direction oldPrice startingPrice
11  
      strategyCrest
12  
      maxInvestment maxDrawdown
13  
      realizedProfit realizedCoinProfit
14  
      realizedWins realizedCoinWins
15  
      realizedLosses realizedCoinLosses
16  
      stepCount stepSince
17  
      openPositions closedPositions positionsThatFailedToOpen
18  
      drift driftSystem
19  
    ]];
20  
  }
21  
  
22  
  settable bool verbose;
23  
  
24  
  gettable S globalID = aGlobalID();
25  
  
26  
  // user-given name, overriding the technical name
27  
  settable S customName;
28  
  
29  
  // user-given comment
30  
  settable S comment;
31  
32  
  gettable transient Q q = startQ();
33  
  
34  
  // link to market (optional)
35  
  transient settable IFuturesMarket market;
36  
  
37  
  settable bool mergePositionsForMarket;
38  
  
39  
  settableWithVar S cryptoCoin;
40  
  settableWithVar S marginCoin = "USDT";
41  
  
42  
  // Which exchange are we using ("Bitget", "ByBit" etc.)
43  
  settableWithVar S exchangeName;
44  
  
45  
  new Ref<G22TradingAccount> tradingAccount;
46  
  
47  
  // Optional: Exchange we get our price data from, if different from above
48  
  settableWithVar S priceDataFromExchange;
49  
  
50  
  // An identifier of the trading account (as of yet unused)
51  
  settableWithVar S accountName;
52  
  
53  
  // primed = started but waiting for first price
54  
  settableWithVar bool primed;
55  
  
56  
  settableWithVar bool usingLiveData;
57  
  settableWithVar bool doingRealTrades;
58  
  settableWithVar bool demoCoin;
59  
  settableWithVar bool active;
60  
  
61  
  // set to date when strategy ended itself
62  
  settableWithVar long closedItself;
63  
  
64  
  // set to date when strategy was deactivated (by itself or by the user)
65  
  settableWithVar long deactivated;
66  
  
67  
  // moved to archive (out of main list)
68  
  settableWithVar bool archived;
69  
  
70  
  // area where this strategy is held, e.g. "Candidates"
71  
  settableWithVar S area;
72  
  
73  
  settableWithVar double adversity = 0.2;
74  
  
75  
  // should migrate to the timedLog completely
76  
  settableWithVar new LS log;
77  
  
78  
  // O is always a string for now
79  
  settableWithVar new L<WithTimestamp<O>> timedLog;
80  
  
81  
  settableWithVar double epsilon = 1e-6;
82  
  
83  
  // increment of crypto we can bet on
84  
  settableWithVar double cryptoStep = 0.0001;
85  
  
86  
  // minimum crypto we need to bet on
87  
  settableWithVar double minCrypto = 0.0001;
88  
  
89  
  // maximum crypto we can bet on
90  
  settableWithVar double maxCrypto = infinity();
91  
  
92  
  settableWithVar double maxInvestment;
93  
  
94  
  // highest value of coinProfit
95  
  settableWithVar double strategyCrest;
96  
  
97  
  // Maximum drawdown seen on account (realized+unrealized)
98  
  settableWithVar double maxDrawdown;
99  
  
100  
  settableWithVar double cellSize = 1;
101  
  
102  
  // position options
103  
  settableWithVar double leverage = 1;
104  
  settableWithVar double marginPerPosition = 1; // in USDT
105  
  
106  
  // optional (for compounding): how much of the account's equity to risk per position
107  
  settableWithVar double riskPerTrade; // in percent
108  
  
109  
  // Equity risked in last trade
110  
  settableWithVar double riskedEquity;
111  
  
112  
  settableWithVar bool showClosedPositionsBackwards = true;
113  
  
114  
  // End strategy when this margin coin profit is reached.
115  
  settableWithVar double takeCoinProfit = infinity();
116  
  settableWithVar bool takeCoinProfitEnabled;
117  
  
118  
  // How many hours of recent back data this algorithm likes
119  
  // to be fed with prior to running live.
120  
  settableWithVar double backDataHoursWanted;
121  
  
122  
  // Did we feed the back data?
123  
  settableWithVar bool backDataFed;
124  
  
125  
  // Juicer that is used to stop the whole strategy depending
126  
  // on its profit (optional)
127  
  settableWithVar AbstractJuicer strategyJuicer;
128  
  
129  
  swappable long currentTime() { ret now(); }
130  
  
131  
  settable transient bool logToPrint = true;
132  
  
133  
  // Note from price feeder
134  
  settableWithVar S feedNote;
135  
  
136  
  // optimization to save fees
137  
  settableWithVar bool useDriftSystem;
138  
  
139  
  // The drift system we are connected to
140  
  settableWithVar G22DriftSystem driftSystem;
141  
  
142  
  // Set to the actual ticker used by the system
143  
  settableWithVar transient TickerSequence actualTicker;
144  
  
145  
  // How many minutes to wait after closing a position
146  
  // before a new one can be opened (0 for no cooldown)
147  
  settableWithVar double cooldownMinutes;
148  
  
149  
  // An image showing this strategy's current state
150  
  settable transient BufferedImage image;
151  
  settable transient byte[] imageForWebServer;
152  
  
153  
  // Initial assumed equity for backtest.
154  
  // If not zero, marginPerPosition will be scaled up and down
155  
  // relative to compoundingBaseEquity+realizedCoinProfit()
156  
  settableWithVar double compoundingBaseEquity;
157  
  
158  
  // Use the exchange's TP & SL features
159  
  settableWithVar bool tpSlOnPlatform;
160  
  
161  
  // Strategies are listed according to this key field (alphanum-IC)
162  
  settableWithVar S listOrder;
163  
  
164  
  // add fields here
165  
  
166  
  <A> A log(A o) {
167  
    if (o != null) {
168  
      S s = str(o);
169  
      long time = currentTime();
170  
      log.add(printIf(logToPrint, "[" + formatLocalDateWithSeconds(time) + "] " + s));
171  
      timedLog.add(withTimestamp(time, s));
172  
      change();
173  
    }
174  
    ret o;
175  
  }
176  
  
177  
  void logVars(O... o) {
178  
    log(renderVars(o));
179  
  }
180  
  
181  
  LS activationStatus() {
182  
    ret llNempties(
183  
      active ? "Active" : "Inactive",
184  
      exchangeName,
185  
      empty(cryptoCoin) ? null
186  
        : "coin: " + cryptoCoin + " over " + marginCoin
187  
          + stringIf(demoCoin, " (demo coin)"),
188  
      stringIf(market != null, "connected to market"),
189  
      stringIf(usingLiveData, "using live data"),
190  
      stringIf(doingRealTrades, "real trades")
191  
    );
192  
  }
193  
  
194  
  double unrealizedProfit() {
195  
    double sum = 0;
196  
    for (p : openPositions())
197  
      sum += p.profit();
198  
    ret sum;
199  
  }
200  
  
201  
  double openMargins() {
202  
    double sum = 0;
203  
    for (p : openPositions())
204  
      sum += p.margin();
205  
    ret sum;
206  
  }
207  
  
208  
  double unrealizedCoinProfit() {
209  
    ret unrealizedCoinProfitAtPrice(currentPrice);
210  
  }
211  
  
212  
  double unrealizedCoinProfitAtPrice(double price) {
213  
    double sum = 0;
214  
    for (p : openPositions)
215  
      sum += p.coinProfitAtPrice(price);
216  
    ret sum;
217  
  }
218  
  
219  
  L<Position> positionsInDirection(int direction) {
220  
    ret filter(openPositions, p -> sign(p.direction) == sign(direction));
221  
  }
222  
  
223  
  L<Position> longPositions() { ret positionsInDirection(1); }
224  
  L<Position> shortPositions() { ret positionsInDirection(-1); }
225  
  
226  
  L<Position> shortPositionsAtOrBelowPrice(double openingPrice) {
227  
    ret filter(openPositions, p -> p.isShort() && p.openingPrice <= openingPrice);
228  
  }
229  
  
230  
  L<Position> longPositionsAtOrAbovePrice(double openingPrice) {
231  
    ret filter(openPositions, p -> p.isLong() && p.openingPrice >= openingPrice);
232  
  }
233  
  
234  
  L<Position> negativePositions() {
235  
    ret filter(openPositions, p -> p.profit() < 0);
236  
  }
237  
  
238  
  double debt() {
239  
    ret max(0, -unrealizedCoinProfit());
240  
  }
241  
  
242  
  // TODO: remove
243  
  double profit() {
244  
    ret realizedProfit+unrealizedProfit();
245  
  }
246  
  
247  
  double coinProfit() {
248  
    ret realizedCoinProfit+unrealizedCoinProfit();
249  
  }
250  
  
251  
  double coinProfitAtPrice(double price) {
252  
    ret realizedCoinProfit+unrealizedCoinProfitAtPrice(price);
253  
  }
254  
  
255  
  S formatProfit(double x) {
256  
    ret plusMinusFix(formatDouble1(x));
257  
  }
258  
  
259  
  settableWithVar PriceDigitizer2 digitizer;
260  
  
261  
  // TODO: This doesn't belong here
262  
  settableWithVar int direction;
263  
  
264  
  // maximum debt seen
265  
  settableWithVar double maxDebt;
266  
  //settableWithVar double minCoinProfit;
267  
  //settableWithVar double maxBoundCoin;
268  
269  
  class Position {
270  
    gettable S positionID = aGlobalID();
271  
272  
    settable double marginToUse;
273  
    settable double openingPrice;
274  
    settable double direction;
275  
    settable double digitizedOpeningPrice;
276  
    gettable double closingPrice = Double.NaN;
277  
    long openingStep, closingStep;
278  
    gettable long openingTime;
279  
    gettable long closingTime;
280  
    double leverage;
281  
    settable double cryptoAmount;
282  
    settable O openReason;
283  
    settable O openError;
284  
    settable O closeReason;
285  
    settable O closeError;
286  
    gettable double margin;
287  
    settable bool openedOnMarket;
288  
    settable S openOrderID; // open order ID returned from platform
289  
    settable bool closedOnMarket;
290  
    settable bool dontCloseOnMarket;
291  
    settable S comment;
292  
    settable L<AbstractJuicer> juicers;
293  
    
294  
    settable Double tpPrice;
295  
    settable Double slPrice;
296  
    
297  
    // highest and lowest unrealized P&L seen (after leverage)
298  
    settable double crest = negativeInfinity();
299  
    settable double antiCrest = infinity();
300  
301  
    G22TradingStrategy strategy() { ret G22TradingStrategy.this; }
302  
    
303  
    {
304  
      if (!dynamicObjectIsLoading()) {
305  
        marginToUse = marginToUseForNewPosition();
306  
        openingStep = stepCount;
307  
        leverage = G22TradingStrategy.this.leverage;
308  
      }
309  
    }
310  
    
311  
    bool isLong() { ret direction > 0; }
312  
    bool isShort() { ret direction < 0; }
313  
    
314  
    bool closed() { ret !isNaN(closingPrice); }
315  
    S type() { ret trading_directionToPositionType(direction); }
316  
    
317  
    long closingOrCurrentTime() { ret closed() ? closingTime() : currentTime(); }
318  
    
319  
    long duration() { ret closingOrCurrentTime()-openingTime(); }
320  
    
321  
    double profitAtPrice(double price) {
322  
      ret profitAtPriceBeforeLeverage(price)*leverage;
323  
    }
324  
    
325  
    double profitAtPriceBeforeLeverage(double price) {
326  
      ret ((price-openingPrice)/openingPrice*direction*100-adversity);
327  
    }
328  
    
329  
    double workingPrice() {
330  
      ret closed() ? closingPrice : currentPrice();
331  
    }
332  
    
333  
    double profit() {
334  
      ret profitAtPrice(workingPrice());
335  
    }
336  
    
337  
    double profitBeforeLeverage() {
338  
      ret profitAtPriceBeforeLeverage(workingPrice());
339  
    }
340  
    
341  
    settable Double imaginaryProfit;
342  
    
343  
    double coinProfit() {
344  
      try object imaginaryProfit;
345  
      ret coinProfitAtPrice(workingPrice());
346  
    }
347  
    
348  
    bool isOpenError() { ret openError != null; }
349  
    
350  
    double coinProfitAtPrice(double price) {
351  
      ret isOpenError() ? 0 : profitAtPrice(price)/100*margin();
352  
    }
353  
    
354  
    void close(O closeReason) {
355  
      if (closed()) fail("Can't close again");
356  
      if (closeReason != null) closeReason(closeReason);
357  
      closingPrice = currentPrice();
358  
      closingTime = currentTime();
359  
      closingStep = stepCount;
360  
      closeOnMarket();
361  
      
362  
      // This only executes when the market close succeeds
363  
      postClose();
364  
    }
365  
    
366  
    void postClose() {
367  
      openPositions.remove(this);
368  
      if (!isOpenError()) 
369  
        closedPositions.add(this);
370  
      addToRealizedStats();
371  
      log(this);
372  
      //print(this);
373  
      //printPositions();
374  
    }
375  
    
376  
    Position partialClose(double amount, O closeReason) {
377  
      new Position p;
378  
      p.cryptoAmount = cryptoAmount-amount;
379  
      log("Partial close (" + amount + ") of " + this + ", creating remainder position with amount " + p.cryptoAmount + ". Reason: " + closeReason);
380  
      cryptoAmount = amount;
381  
      ret openPosition(p, (int) direction, closeReason);
382  
    }
383  
    
384  
    void makeFake(double coinProfit) {
385  
      closeReason("Fake");
386  
      imaginaryProfit(coinProfit);
387  
      closedPositions.add(this);
388  
      addToRealizedStats();
389  
      log(this);
390  
    }
391  
    
392  
    void addToRealizedStats {
393  
      double cp = coinProfit();
394  
      realizedProfit += profit();
395  
      realizedCoinProfit += cp;
396  
      if (cp > 0) {
397  
        realizedWins++;
398  
        realizedCoinWins += cp;
399  
      } else {
400  
        realizedLosses++;
401  
        realizedCoinLosses += cp;
402  
      }
403  
      change();
404  
    }
405  
    
406  
    S winnerOrLoser() {
407  
      var profit = coinProfit();
408  
      if (profit == 0) ret "NEUTRAL";
409  
      if (profit > 0) ret "WIN";
410  
      ret "LOSS";
411  
    }
412  
    
413  
    // Position.toString
414  
    toString {
415  
      ret commaCombine(
416  
        spaceCombine(
417  
          !closed() ? null :
418  
            winnerOrLoser()
419  
              + appendBracketed(strOrNull(closeReason))
420  
              + appendBracketed(comment)
421  
              + " " + formatLocalDateWithSeconds(closingTime())
422  
              + " " + formatProfit(profit()) + "% (" + marginCoin + " " + formatMarginProfit(coinProfit()) + ")"
423  
              + " (min " + formatProfit(antiCrest())
424  
              + ", max " + formatProfit(crest()) + ")"
425  
              + ", " + formatDouble1(leverage) + "X " + upper(type())
426  
              + " held " + formatHoursMinutesColonSeconds(duration()),
427  
          
428  
        ),
429  
        openingTime() == 0 ? null : "opened " + formatLocalDateWithSeconds(openingTime())
430  
          + (openReason == null ? "" : " " + roundBracket(str(openReason))),
431  
        "before leverage: " + formatProfit(profitBeforeLeverage()) + "%",
432  
        "margin: " + marginCoin + " " + formatMarginPrice(margin()),
433  
        "crypto: " + formatPrice(cryptoAmount),
434  
        "opening price: " + formatPriceX(openingPrice)
435  
           + (isNaN(digitizedOpeningPrice()) ? "" : " (digitized: " + formatPrice(digitizedOpeningPrice())
436  
           + ")") + (openingStep == 0 ? "" : " @ step " + openingStep),
437  
        !closed() ? null : "closing price: " + formatPriceX(closingPrice)
438  
          + (closingStep == 0 ? "" : " @ step " + closingStep),
439  
        );
440  
    }
441  
    
442  
    bool shouldCloseOnMarket() {
443  
      ret !dontCloseOnMarket && !closedOnMarket;
444  
    }
445  
    
446  
    void closeOnMarket {
447  
      if (!shouldCloseOnMarket()) ret;
448  
      if (isOpenError()) ret with log("Not closing because open error: " + this);
449  
      try {
450  
        if (market != null) {
451  
          log("Closing on market: " + this);
452  
          market.closePosition(new IFuturesMarket.CloseOrder()
453  
            .holdSide(HoldSide.fromInt(direction))
454  
            .cryptoAmount(cryptoAmount)
455  
            .leverage(leverage));
456  
          closedOnMarket(true);
457  
          change();
458  
        }
459  
      } on fail e {
460  
        closeError(toPersistableThrowable(e));
461  
      }
462  
    }
463  
    
464  
    void open {
465  
      margin = cryptoAmount*openingPrice/leverage;
466  
      log("Opening: " + this);
467  
      openingTime = currentTime();
468  
      openPositions.add(this);
469  
      change();
470  
    }
471  
    
472  
    void openOnMarket {
473  
      try {
474  
        if (market != null) {
475  
          log("Opening on market: " + this);
476  
          var order = new IFuturesMarket.OpenOrder()
477  
            .clientOrderID(positionID())
478  
            .holdSide(HoldSide.fromInt(direction))
479  
            .cryptoAmount(cryptoAmount)
480  
            .leverage(leverage)
481  
            .isCross(true);
482  
            
483  
          if (tpSlOnPlatform) {
484  
            if (tpPrice != null)
485  
              order.takeProfitPrice(tpPrice);
486  
            if (slPrice != null)
487  
              order.stopLossPrice(slPrice);
488  
          }
489  
          
490  
          market.openPosition(order);
491  
          openOrderID(order.orderID());
492  
          openedOnMarket(true);
493  
          change();
494  
        }
495  
      } catch e {
496  
        openError(toPersistableThrowable(e));
497  
        log("Open error: " + getStackTrace(e));
498  
        positionsThatFailedToOpen.add(this);
499  
        close("Open error");
500  
      }
501  
    }
502  
    
503  
    void updateStats {
504  
      double profit = profit();
505  
      crest(max(crest, profit));
506  
      antiCrest(min(antiCrest, profit));
507  
    }
508  
    
509  
    void addJuicer(AbstractJuicer juicer) {
510  
      juicers(listCreateAndAdd(juicers, juicer));
511  
    }
512  
    
513  
    void removeJuicer(AbstractJuicer juicer) {
514  
      juicers(removeDyn(juicers, juicer));
515  
    }
516  
    
517  
    // Call this when position was closed manually on the platform
518  
    void notification_closedOutsideOfStrategy() {
519  
      closedOnMarket(true);
520  
      close("Closed outside of strategy");
521  
    }
522  
    
523  
  } // end of Position
524  
      
525  
  gettable double currentPrice = 0;
526  
  
527  
  gettable double oldPrice = Double.NaN;
528  
  gettable double startingPrice = Double.NaN;
529  
  settable long startTime;
530  
  settableWithVar double realizedProfit;
531  
  settableWithVar double realizedCoinProfit;
532  
  settableWithVar int realizedWins;
533  
  settableWithVar double realizedCoinWins;
534  
  settableWithVar int realizedLosses;
535  
  settableWithVar double realizedCoinLosses;
536  
  settableWithVar long stepCount;
537  
  settableWithVar long stepSince;
538  
  new LinkedHashSet<Position> openPositions;
539  
  gettable new L<Position> closedPositions;
540  
  gettable new L<Position> positionsThatFailedToOpen;
541  
  
542  
  void closeOnMarketMerged(Cl<? extends Position> positions) {
543  
    if (market == null) ret;
544  
    var list = filter(positions, -> .shouldCloseOnMarket());
545  
    closeOnMarketMerged_oneDirection(filter(list, -> .isShort()));
546  
    closeOnMarketMerged_oneDirection(filter(list, -> .isLong()));
547  
  }
548  
  
549  
  // all positions must have the same direction
550  
  void closeOnMarketMerged_oneDirection(L<? extends Position> positions) {
551  
    if (empty(positions)) ret;
552  
    
553  
    double direction = first(positions).direction;
554  
    double cryptoAmount = doubleSum(positions, -> .cryptoAmount);
555  
    log("Closing on market: " + positions);
556  
    
557  
    try {
558  
      market.closePosition(new IFuturesMarket.CloseOrder()
559  
        .holdSide(HoldSide.fromInt(direction))
560  
        .cryptoAmount(cryptoAmount);
561  
        
562  
      for (p : positions)
563  
        p.closedOnMarket(true);
564  
      change();
565  
    } catch e {
566  
      var e2 = toPersistableThrowable(e);
567  
      for (p : positions)
568  
        p.closeError(e2);
569  
      throw e;
570  
    }
571  
  }
572  
573  
  bool hasPosition(double price, double direction) {
574  
    ret findPosition(price, direction) != null;
575  
  }
576  
  
577  
  Position closePosition(double price, double direction, O closeReason) {
578  
    var p = findPosition(price, direction);
579  
    p?.close(closeReason);
580  
    ret p;
581  
  }
582  
  
583  
  void closePositions(Cl<? extends Position> positions, O closeReason default null) {
584  
    if (mergePositionsForMarket)
585  
      closeOnMarketMerged(positions);
586  
    forEach(positions, -> .close(closeReason));
587  
  }
588  
  
589  
  Position findPosition(double digitizedPrice, double direction) {
590  
    ret firstThat(openPositions(), p ->
591  
      diffRatio(p.digitizedOpeningPrice(), digitizedPrice) <= epsilon() && sign(p.direction) == sign(direction));
592  
  }
593  
  
594  
  void printPositions {
595  
    print(colonCombine(n2(openPositions, "open position"),
596  
      joinWithComma(openPositions)));
597  
  }
598  
  
599  
  bool started() { ret !isNaN(startingPrice); }
600  
  void prices(double... prices) {
601  
    fOr (price : prices) {
602  
      if (!active()) ret;
603  
      price(price);
604  
    }
605  
  }
606  
  
607  
  swappable PriceCells makePriceCells(double basePrice) {
608  
    ret new GeometricPriceCells(basePrice, cellSize);
609  
  }
610  
  
611  
  double digitizedPrice() { ret digitizer == null ? Double.NaN : digitizer.digitizedPrice(); }
612  
  double lastDigitizedPrice() { ret digitizer == null ? Double.NaN : digitizer.lastDigitizedPrice(); }
613  
  int digitizedCellNumber() { ret digitizer == null ? 0 : digitizer.cellNumber(); }
614  
  
615  
  void handleNewPriceInQ(double price) {
616  
    q.add(-> price(price));
617  
  }
618  
619  
  void nextStep {
620  
    ++stepCount;
621  
    stepSince(currentTime());
622  
  }
623  
  
624  
  void afterStep {
625  
    double coinProfit = coinProfit();
626  
    maxDebt(max(maxDebt, debt()));
627  
    //minCoinProfit = min(minCoinProfit, coinProfit);
628  
    //maxBoundCoin = max(maxBoundCoin, boundCoin());
629  
    maxInvestment(max(maxInvestment, investment()));
630  
    strategyCrest(max(strategyCrest, coinProfit));
631  
    maxDrawdown(max(maxDrawdown, strategyCrest-coinProfit));
632  
    if (takeCoinProfitEnabled() && coinProfit >= takeCoinProfit) {
633  
      log("Taking coin profit.");
634  
      closeMyself();
635  
    }
636  
    
637  
    for (p : openPositions) p.updateStats();
638  
  }
639  
  
640  
  double investment() {
641  
    ret boundCoin()-realizedCoinProfit;
642  
  }
643  
  
644  
  double boundCoin() {
645  
    ret max(openMargins(), max(0, -coinProfit()));
646  
  }
647  
  
648  
  double fromCellNumber(double cellNumber) { ret cells().fromCellNumber(cellNumber); }
649  
  double toCellNumber(double price) { ret cells().toCellNumber(price); }
650  
  
651  
  L<Position> shortPositionsAtOrBelowDigitizedPrice(double openingPrice) {
652  
    ret filter(openPositions, p -> p.isShort() && p.digitizedOpeningPrice() <= openingPrice);
653  
  }
654  
  
655  
  L<Position> shortPositionsAtOrAboveDigitizedPrice(double openingPrice) {
656  
    ret filter(openPositions, p -> p.isShort() && p.digitizedOpeningPrice() >= openingPrice);
657  
  }
658  
  
659  
  L<Position> longPositionsAtOrAboveDigitizedPrice(double openingPrice) {
660  
    ret filter(openPositions, p -> p.isLong() && p.digitizedOpeningPrice() >= openingPrice);
661  
  }
662  
  
663  
  L<Position> longPositionsAtOrBelowDigitizedPrice(double openingPrice) {
664  
    ret filter(openPositions, p -> p.isLong() && p.digitizedOpeningPrice() <= openingPrice);
665  
  }
666  
  
667  
  PriceCells cells aka priceCells() {
668  
    ret digitizer?.cells;
669  
  }
670  
  
671  
  S formatPriceX(double price) {
672  
    if (isNaN(price)) ret "-";
673  
    S s = formatPrice(price);
674  
    if (cells() == null) ret s;
675  
    double num = cells().priceToCellNumber(price);
676  
    ret s + " (C" + formatDouble2(num) + ")";
677  
  }
678  
  
679  
  abstract void price aka currentPrice(double price);
680  
681  
  bool inCooldown() {  
682  
    if (cooldownMinutes <= 0) false;
683  
    if (nempty(openPositions)) true;
684  
    var lastClosed = last(closedPositions);
685  
    if (lastClosed == null) false;
686  
    var minutes = toMinutes(currentTime()-lastClosed.closingTime);
687  
    ret minutes < cooldownMinutes;
688  
  }
689  
  
690  
  void assureCanOpen(Position p) {
691  
    if (cooldownMinutes > 0) {
692  
      if (nempty(openPositions))
693  
        fail("Already have an open position");
694  
      var lastClosed = last(closedPositions);
695  
      if (lastClosed != null) {
696  
        var minutes = toMinutes(currentTime()-lastClosed.closingTime);
697  
        if (minutes < cooldownMinutes)
698  
          fail("Last position closed " + formatMinutes(fromMinutes(minutes)) + " ago, cooldown is " + cooldownMinutes);
699  
      }
700  
    }
701  
  }
702  
  
703  
  <P extends Position> P openPosition(P p, int direction, O openReason default null) {
704  
    try {
705  
      assureCanOpen(p);
706  
  
707  
      p.openReason(openReason);
708  
      var price = digitizedPrice();
709  
      var realPrice = currentPrice();
710  
      logVars("openPosition", +realPrice, +price, +digitizer);
711  
      if ((isNaN(price) || price == 0) && digitizer != null) {
712  
        price = digitizer.digitizeIndividually(currentPrice());
713  
        print("digitized individually: " + price);
714  
      }
715  
      p.openingPrice(realPrice);
716  
      p.direction(direction);
717  
      p.digitizedOpeningPrice(price);
718  
      
719  
      // Calculate quantity (cryptoAmount) from margin
720  
      // unless cryptoAmount is already set
721  
      
722  
      if (p.cryptoAmount == 0)
723  
        p.cryptoAmount = p.marginToUse/realPrice*leverage;
724  
        
725  
      // Round cryptoAmount to the allowed increments
726  
      p.cryptoAmount = roundTo(cryptoStep, p.cryptoAmount);
727  
      
728  
      // Clamp to minimum/maximum order
729  
      p.cryptoAmount = clamp(p.cryptoAmount, minCrypto, maxCrypto);
730  
      
731  
      log(renderVars("openPosition", +marginPerPosition, +realPrice, +leverage, cryptoAmount := p.cryptoAmount, +cryptoStep));
732  
      p.open();
733  
      //print("Opening " + p);
734  
      //printPositions();
735  
      p.openOnMarket();
736  
      ret p;
737  
    } on fail e {
738  
      log(e);
739  
    }
740  
  }
741  
  
742  
  LS status() {
743  
    double mulProf = multiplicativeProfit();
744  
    ret llNonNulls(
745  
      empty(comment) ? null : "Comment: " + comment,
746  
      "Profit: " + marginCoin + " " + plusMinusFix(formatMarginPrice(coinProfit())),
747  
      "Realized profit: " + marginCoin + " " + formatMarginProfit(realizedCoinProfit) + " from " + n2(closedPositions, "closed position")
748  
        + " (" + formatMarginProfit(realizedCoinWins) + " from " + n2(realizedWins, "win")
749  
        + ", " + formatMarginProfit(realizedCoinLosses) + " from " + n2(realizedLosses, "loss", "losses") + ")",
750  
      "Unrealized profit: " + marginCoin + " " + formatMarginProfit(unrealizedCoinProfit()) + " in " + n2(openPositions, "open position"),
751  
      isNaN(mulProf) ? null : "Multiplicative profit: " + formatProfit(mulProf) + "%",
752  
      //baseToString(),
753  
      !primed() ? null : "Primed",
754  
      !started() ? null : "Started. current price: " + formatPriceX(currentPrice)
755  
        + (isNaN(digitizedPrice()) ? "" : ", digitized: " + formatPriceX(digitizedPrice())),
756  
      (riskPerTrade == 0 ? "" : "Risk per trade: " + formatDouble1(riskPerTrade) + "%. ")
757  
      + "Position size: " + marginCoin + " " + formatPrice(marginPerPosition) + "x" + formatDouble1(leverage) + " = " + marginCoin + " " + formatPrice(positionSize()),
758  
      !usingCells() ? null : "Cell size: " + formatCellSize(cellSize),
759  
      spaceCombine("Step " + n2(stepCount), renderStepSince()),
760  
      //"Debt: " + marginCoin + " " + formatMarginProfit(debt()) + " (max seen: " + formatMarginProfit(maxDebt) + ")",
761  
      "Investment used: " + marginCoin + " " + formatMarginPrice(maxInvestment()),
762  
      strategyJuicer == null ?: "Strategy juicer: " + strategyJuicer,
763  
      //"Drift: " + cryptoCoin + " " + plusMinusFix(formatCryptoAmount(drift())),
764  
    );
765  
  }
766  
  
767  
  S renderStepSince() {
768  
    if (stepSince == 0) ret "";
769  
    ret "since " + (active()
770  
      ? formatHoursMinutesColonSeconds(currentTime()-stepSince)
771  
      : formatLocalDateWithSeconds(stepSince));
772  
  }
773  
  
774  
  LS fullStatus() {
775  
    ret listCombine(
776  
      tradingAccount,
777  
      status(),
778  
      "",
779  
      n2(openPositions, "open position") + ":",
780  
      reversed(openPositions),
781  
      "",
782  
      n2(closedPositions, "closed position")
783  
        + " (" + (showClosedPositionsBackwards ? "latest first" : "oldest first") + "):",
784  
      showClosedPositionsBackwards ? reversed(closedPositions) : closedPositions,
785  
    );
786  
  }
787  
  
788  
  void feed(PricePoint pricePoint) {
789  
    if (!active()) ret;
790  
    setTime(pricePoint.timestamp);
791  
    price(pricePoint.price);
792  
  }
793  
  
794  
  void feed(TickerSequence ts) {
795  
    if (!active()) ret;
796  
    if (ts == null) ret;
797  
    for (pricePoint : ts.pricePoints())
798  
      feed(pricePoint);
799  
  }
800  
  
801  
  public int compareTo(G22TradingStrategy s) {
802  
    ret s == null ? 1 : cmp(coinProfit(), s.coinProfit());
803  
  }
804  
  
805  
  // Returns closed positions
806  
  L<Position> closeAllPositions(O reason default "User close") {
807  
    var positions = openPositions();
808  
    closePositions(positions, reason);
809  
    ret (L) positions;
810  
  }
811  
  
812  
  void closeMyself() {
813  
    closedItself(currentTime());
814  
    closeAllPositionsAndDeactivate();
815  
  }
816  
  
817  
  void closeAllPositionsAndDeactivate {
818  
    deactivate();
819  
    closeAllPositions();
820  
  }
821  
  
822  
  void deactivate {
823  
    market(null);
824  
    if (!active) ret;
825  
    active(false);
826  
    deactivated(currentTime());
827  
    log("Strategy deactivated.");
828  
  }
829  
  
830  
  void reset aka reset_G22TradingStrategy() {
831  
    resetFields(this, fieldsToReset());
832  
    change();
833  
  }
834  
  
835  
  selfType emptyClone aka emptyClone_G22TradingStrategy() {
836  
    var clone = shallowCloneToUnlistedConcept(this);
837  
    clone.reset();
838  
    ret clone;
839  
  }
840  
  
841  
  L<Position> allPositions() {
842  
    ret concatLists(openPositions, closedPositions);
843  
  }
844  
  
845  
  L<Position> sortedPositions() {
846  
    var allPositions = allPositions();
847  
    ret sortedByCalculatedField(allPositions, -> .openingTime());
848  
  }
849  
  
850  
  bool positionsAreNonOverlapping() {
851  
    for (a, b : unpair overlappingPairs(sortedPositions()))
852  
      if (b.openingTime() < a.closingTime())
853  
        false;
854  
    true;
855  
  }
856  
  
857  
  // Profit when applying all positions (somewhat theoretical because you
858  
  // might go below platform limits)
859  
  // Also only works when positions are linear
860  
  double multiplicativeProfit() {
861  
    if (!positionsAreNonOverlapping()) ret Double.NaN;
862  
    double profit = 1;
863  
    for (p : sortedPositions())
864  
      profit *= 1+p.profit()/100;
865  
    ret (profit-1)*100;
866  
  }
867  
  
868  
  bool haveBackData() { ret backDataHoursWanted == 0 | backDataFed; }
869  
  
870  
  bool didRealTrades() {
871  
    ret any(allPositions(), p -> p.openedOnMarket() || p.closedOnMarket());
872  
  }
873  
  
874  
  S formatCellSize(double cellSize) {
875  
    ret formatPercentage(cellSize, 3);
876  
  }
877  
  
878  
  S areaDesc() {
879  
    if (eq(area, "Candidates")) ret "Candidate";
880  
    ret nempty(area) ? area : archived ? "Archived" : "";
881  
  }
882  
  
883  
  selfType setTime aka currentTime(long time) {
884  
    int age = ifloor(ageInHours());
885  
    long lastMod = mod(currentTime()-startTime, hoursToMS(1));
886  
    currentTime = -> time;
887  
    if (ifloor(ageInHours()) > age)
888  
      log("Hourly profit log: " + formatMarginProfit(coinProfit()));
889  
    this;
890  
  }
891  
  
892  
  double ageInHours() { ret startTime == 0 ? 0 : msToHours(currentTime()-startTime); }
893  
  
894  
  // only use near start of strategy, untested otherwise
895  
  selfType changeCellSize(double newCellSize) {
896  
    double oldCellSize = cellSize();
897  
    if (oldCellSize == newCellSize) this;
898  
    cellSize(newCellSize);
899  
    if (digitizer != null)
900  
      digitizer.swapPriceCells(makePriceCells(priceCells().basePrice()));
901  
    log("Changed cell size from " + oldCellSize + " to " + newCellSize);
902  
    this;
903  
  }
904  
  
905  
  bool hasClosedItself() { ret closedItself != 0; }
906  
  
907  
  public double juiceValue() { ret coinProfit(); }
908  
  
909  
  // drift is our cumulative delta (=sum of all signed position
910  
  // quantities)
911  
  double drift() {
912  
    double drift = 0;
913  
    for (p : openPositions())
914  
      drift += p.cryptoAmount()*p.direction();
915  
    ret drift;
916  
  }
917  
  
918  
  S formatCryptoAmount(double amount) {
919  
    ret formatDouble3(amount);
920  
  }
921  
  
922  
  Position openShort() { ret openPosition(-1); }
923  
  Position openLong() { ret openPosition(1); }
924  
  
925  
  Position openPosition(int direction, O openReason default null) {
926  
    new Position p;
927  
    p.marginToUse = marginToUseForNewPosition();
928  
    ret openPosition(p, direction, openReason);
929  
  }
930  
  
931  
  // Open or close positions until the drift (delta) is
932  
  // equal to targetDrift (or as close as the platform's
933  
  // restrictions allow).
934  
  // Returns the list of positions opened or closed
935  
  // (Function is in dev.)
936  
  L<Position> adjustDrift(double targetDrift, O reason default null) {
937  
    new L<Position> changeList;
938  
    
939  
    // target 0? close all
940  
    if (targetDrift == 0)
941  
      ret closeAllPositions(reason);
942  
    
943  
    double drift = drift();
944  
    
945  
    // target already reached? done
946  
    if (drift == targetDrift) ret changeList;
947  
    
948  
    int direction = sign(targetDrift);
949  
    
950  
    // Are we changing direction? Then close everything first.
951  
    if (sign(drift) != direction)
952  
      changeList.addAll(closeAllPositions(reason));
953  
    
954  
    // Now we know targetDrift and drift have the same sign.
955  
    double diff = abs(targetDrift)-abs(drift);
956  
    
957  
    // Round to allow increments
958  
    diff = roundTo(cryptoStep, diff);
959  
    
960  
    if (diff > 0) {
961  
      // We need to open a new position - that's easy
962  
      
963  
      new Position p;
964  
      p.cryptoAmount = diff;
965  
      changeList.add(openPosition(p, direction, reason));
966  
    } else {
967  
      double toClose = -diff;
968  
      
969  
      // We need to close positions - that's a bit more tricky
970  
      
971  
      // Filter by direction to be sure in case there is
972  
      // hedging (you shouldn't do hedging, really, though)
973  
      var positions = filter(openPositions(), p -> p.direction() == direction);
974  
      
975  
      // Let's look for an exact size match first.
976  
      // We rounded to cryptoStep, so using == is ok.
977  
      var _toClose = toClose;
978  
      var exactMatch = firstThat(positions, p -> p.cryptoAmount == _toClose);
979  
      
980  
      if (exactMatch != null) {
981  
        exactMatch.close(reason);
982  
        changeList.add(exactMatch);
983  
        ret changeList;
984  
      }
985  
      
986  
      // No exact match. Go through positions starting with
987  
      // the oldest one.
988  
      
989  
      for (p : positions) {
990  
        toClose = roundTo(cryptoStep, toClose);
991  
        if (toClose == 0) break;
992  
        if (toClose >= p.cryptoAmount) {
993  
          // Need to close the whole position
994  
          toClose -= p.cryptoAmount;
995  
          p.close(reason);
996  
          changeList.add(p);
997  
        } else {
998  
          // Need a partial close.
999  
          
1000  
          changeList.add(p);
1001  
          var remainderPosition = p.partialClose(toClose, reason);
1002  
          changeList.add(remainderPosition);
1003  
        }
1004  
      }
1005  
    }
1006  
    
1007  
    ret changeList;
1008  
  }
1009  
  
1010  
  L<Position> winners() { ret filter(closedPositions(), p -> p.coinProfit() > 0); }
1011  
  L<Position> losers() { ret filter(closedPositions(), p -> p.coinProfit() < 0); }
1012  
  
1013  
  L<? extends Position> openPositions() { ret cloneList(openPositions); }
1014  
  
1015  
  double winRate() {
1016  
    ret l(winners()) * 100.0 / l(closedPositions);
1017  
  }
1018  
  
1019  
  bool usingCells() { true; }
1020  
  
1021  
  double positionSize() {
1022  
    ret marginToUseForNewPosition()*leverage;
1023  
  }
1024  
  
1025  
  double marginToUseForNewPosition() {
1026  
    ret scaleMargin(marginPerPosition);
1027  
  }
1028  
  
1029  
  // to simulate compounding in backtest
1030  
  double scaleMargin(double margin) {
1031  
    // Live? Then no scaling.
1032  
    if (usingLiveData) ret margin;
1033  
    
1034  
    ret margin*compoundingFactor();
1035  
  }
1036  
  
1037  
  double compoundingFactor() {
1038  
    // No compounding selected?
1039  
    if (compoundingBaseEquity == 0) ret 1;
1040  
    
1041  
    ret max(0, remainingEquity()/compoundingBaseEquity);
1042  
  }
1043  
  
1044  
  // only for backtesting
1045  
  double remainingEquity() {
1046  
    ret compoundingBaseEquity+realizedCoinProfit();
1047  
  }
1048  
  
1049  
  class RiskToMargin {
1050  
    // in percent
1051  
    settable double riskForTrade = riskPerTrade;
1052  
    
1053  
    settable double stopLossPercent;
1054  
    settable double price = currentPrice();
1055  
    settable double fullEquity;
1056  
    
1057  
    double riskedEquity() {
1058  
      ret fullEquity*riskForTrade/100;
1059  
    }
1060  
    
1061  
    double qty() {
1062  
      ret riskedEquity()/(price*stopLossPercent/100);
1063  
    }
1064  
    
1065  
    double margin aka get() {
1066  
      ret qty()*price/leverage();
1067  
    }
1068  
  }
1069  
  
1070  
  void fixRealizedStats {
1071  
    realizedProfit(0);
1072  
    realizedCoinProfit(0);
1073  
    realizedWins(0);
1074  
    realizedCoinWins(0);
1075  
    realizedLosses(0);
1076  
    realizedCoinLosses(0);
1077  
    for (p : closedPositions()) p.addToRealizedStats();
1078  
  }
1079  
  
1080  
  // p must not be open
1081  
  bool deletePosition(Position p) {
1082  
    if (closedPositions.remove(p) || positionsThatFailedToOpen.remove(p)) {
1083  
      fixRealizedStats();
1084  
      true;
1085  
    }
1086  
    false;
1087  
  }
1088  
  
1089  
  void deletePositionsThatFailedToOpen() {
1090  
    for (p : cloneList(positionsThatFailedToOpen))
1091  
      deletePosition(p);
1092  
  }
1093  
  
1094  
  void deleteAllPositions() {
1095  
    for (p : cloneList(closedPositions))
1096  
      deletePosition(p);
1097  
  }
1098  
  
1099  
  bool hasRiskPerTrade() { ret riskPerTrade != 0; }
1100  
  
1101  
  Position addFakePosition(double coinProfit) {
1102  
    new Position p;
1103  
    p.makeFake(coinProfit);
1104  
    ret p;
1105  
  }
1106  
  
1107  
  // If there is exactly one open position:
1108  
  // Open another position in the same direction
1109  
  // with same size
1110  
  Position pyramid(O openReason default "Pyramiding") {
1111  
    ret extendedPyramid(2, openReason);
1112  
  }
1113  
1114  
  // level: pyramid level to be reached
1115  
  // (must be one higher than current number of positions
1116  
  // to trigger - no double adding at once)
1117  
  Position extendedPyramid(int level, O openReason default "Pyramiding") {
1118  
    if (l(openPositions()) != level-1)
1119  
      null;
1120  
      
1121  
    var p = first(openPositions);
1122  
    new Position p2;
1123  
    p2.cryptoAmount(p.cryptoAmount);
1124  
    ret openPosition(p2, sign(p.direction), openReason);
1125  
  }
1126  
  
1127  
  // level: pyramid level to be reached
1128  
  // can do double adding
1129  
  L<Position> extendedPyramid2(int level, O openReason default "Pyramiding") {
1130  
    new L<Position> newPositions;
1131  
    
1132  
    while (l(openPositions()) < level && l(newPositions) < level) {
1133  
      var p = first(openPositions);
1134  
      new Position p2;
1135  
      p2.cryptoAmount(p.cryptoAmount);
1136  
      newPositions.add(p2);
1137  
      openPosition(p2, sign(p.direction), openReason);
1138  
    }
1139  
    
1140  
    ret newPositions;
1141  
  }
1142  
  
1143  
  Position newPosition() {
1144  
    ret new Position;
1145  
  }
1146  
  
1147  
  void postCloseFix() {
1148  
    for (p : openPositions())
1149  
      if (p.closed()) try {
1150  
        p.postClose();
1151  
      } catch e {
1152  
        log(e);
1153  
      }
1154  
  }
1155  
  
1156  
  void dryCloseAll() {
1157  
    for (p : openPositions())
1158  
      p.dontCloseOnMarket(true);
1159  
    closeAllPositions();
1160  
  }
1161  
}

download  show line numbers  debug dex  old transpilations   

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

Comments [hide]

ID Author/Program Comment Date
2144 ubataecj 555 2024-05-01 03:52:27
2142 ubataecj @@yT0NO 2024-05-01 03:38:41
2141 ubataecj 555????%2527%2522\'\" 2024-05-01 03:38:41
2140 ubataecj 555'" 2024-05-01 03:38:41
2139 ubataecj 555'||DBMS_PIPE.RECEIVE_MESSAGE(CHR(98)||CHR(98)||CHR(98),15)||' 2024-05-01 03:38:41
2135 ubataecj 555*DBMS_PIPE.RECEIVE_MESSAGE(CHR(99)||CHR(99)||CHR(99),15) 2024-05-01 03:38:37
2133 ubataecj 555v1xDKDb2')) OR 528=(SELECT 528 FROM PG_SLEEP(15))-- 2024-05-01 03:38:34
2131 ubataecj 555t5BAWglH') OR 402=(SELECT 402 FROM PG_SLEEP(15))-- 2024-05-01 03:38:28
2129 ubataecj 5557t8zcvB8' OR 234=(SELECT 234 FROM PG_SLEEP(15))-- 2024-05-01 03:38:25
2127 ubataecj 555-1)) OR 88=(SELECT 88 FROM PG_SLEEP(15))-- 2024-05-01 03:38:21
2125 ubataecj 555-1) OR 510=(SELECT 510 FROM PG_SLEEP(15))-- 2024-05-01 03:38:18
2123 ubataecj 555-1 OR 510=(SELECT 510 FROM PG_SLEEP(15))-- 2024-05-01 03:38:13
2121 ubataecj 555bz5UAASv'; waitfor delay '0:0:15' -- 2024-05-01 03:38:09
2119 ubataecj 555-1 waitfor delay '0:0:15' -- 2024-05-01 03:38:07
2117 ubataecj 555-1); waitfor delay '0:0:15' -- 2024-05-01 03:38:05
2115 ubataecj 555-1; waitfor delay '0:0:15' -- 2024-05-01 03:38:00
2113 ubataecj (select(0)from(select(sleep(15)))v)/*'+(select(0)from(select(sleep(15)))v)+'"+(select(0)from(select(sleep(15)))v)+"*/ 2024-05-01 03:37:57
2109 ubataecj 5550"XOR(555*if(now()=sysdate(),sleep(15),0))XOR"Z 2024-05-01 03:37:53
2106 ubataecj 5550'XOR(555*if(now()=sysdate(),sleep(15),0))XOR'Z 2024-05-01 03:37:51
2103 ubataecj 555*if(now()=sysdate(),sleep(15),0) 2024-05-01 03:37:49
2100 ubataecj -1" OR 2+540-540-1=0+0+0+1 -- 2024-05-01 03:37:46
2099 ubataecj -1' OR 2+480-480-1=0+0+0+1 or 'b3MLjESj'=' 2024-05-01 03:37:46
2098 ubataecj -1' OR 2+65-65-1=0+0+0+1 -- 2024-05-01 03:37:46
2097 ubataecj -1 OR 2+107-107-1=0+0+0+1 2024-05-01 03:37:46
2096 ubataecj -1 OR 2+606-606-1=0+0+0+1 -- 2024-05-01 03:37:46
2095 ubataecj 555 2024-05-01 03:37:46
2087 ubataecj 555 2024-05-01 03:37:42
2084 ubataecj 555 2024-05-01 03:37:39
2083 ubataecj 555 2024-05-01 03:37:39
2082 ubataecj 555 2024-05-01 03:37:39
2081 ubataecj 555 2024-05-01 03:37:39
2075 ubataecj 555 2024-05-01 03:37:37
2072 ubataecj 555 2024-05-01 03:37:33
2069 ubataecj 555 2024-05-01 03:37:31
2066 ubataecj 555 2024-05-01 03:37:28
2063 ubataecj 555 2024-05-01 03:37:25
2060 ubataecj 555 2024-05-01 03:37:22
2058 ubataecj 555 2024-05-01 03:37:19
2055 ubataecj 555 2024-05-01 03:37:15
2052 ubataecj 555 2024-05-01 03:37:12
2049 ubataecj 555 2024-05-01 03:37:10
2048 ubataecj 555 2024-05-01 03:37:09
2047 ubataecj 555 2024-05-01 03:37:09
2046 ubataecj 555 2024-05-01 03:37:09
2045 ubataecj 555 2024-05-01 03:37:09
2044 ubataecj 555 2024-05-01 03:37:08
2036 ubataecj 555 2024-05-01 03:37:01

add comment

Snippet ID: #1036209
Snippet name: G22TradingStrategy
Eternal ID of this version: #1036209/294
Text MD5: 46dd664b85495355aa9f212738d7dad8
Transpilation MD5: 95c0c67ec9386d4de26a51eca1697b9b
Author: stefan
Category: javax / trading
Type: JavaX fragment (include)
Public (visible to everyone): Yes
Archived (hidden from active list): No
Created/modified: 2025-05-14 15:14:30
Source code size: 36196 bytes / 1161 lines
Pitched / IR pitched: No / No
Views / Downloads: 1317 / 2664
Version history: 293 change(s)
Referenced in: [show references]