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