Libraryless. Click here for Pure Java version (30654L/194K).
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 | openPositions.remove(this); |
364 | if (!isOpenError()) |
365 | closedPositions.add(this); |
366 | addToRealizedStats(); |
367 | log(this); |
368 | //print(this); |
369 | //printPositions(); |
370 | } |
371 | |
372 | Position partialClose(double amount, O closeReason) { |
373 | new Position p; |
374 | p.cryptoAmount = cryptoAmount-amount; |
375 | log("Partial close (" + amount + ") of " + this + ", creating remainder position with amount " + p.cryptoAmount + ". Reason: " + closeReason); |
376 | cryptoAmount = amount; |
377 | ret openPosition(p, (int) direction, closeReason); |
378 | } |
379 | |
380 | void makeFake(double coinProfit) { |
381 | closeReason("Fake"); |
382 | imaginaryProfit(coinProfit); |
383 | closedPositions.add(this); |
384 | addToRealizedStats(); |
385 | log(this); |
386 | } |
387 | |
388 | void addToRealizedStats { |
389 | double cp = coinProfit(); |
390 | realizedProfit += profit(); |
391 | realizedCoinProfit += cp; |
392 | if (cp > 0) { |
393 | realizedWins++; |
394 | realizedCoinWins += cp; |
395 | } else { |
396 | realizedLosses++; |
397 | realizedCoinLosses += cp; |
398 | } |
399 | change(); |
400 | } |
401 | |
402 | S winnerOrLoser() { |
403 | var profit = coinProfit(); |
404 | if (profit == 0) ret "NEUTRAL"; |
405 | if (profit > 0) ret "WIN"; |
406 | ret "LOSS"; |
407 | } |
408 | |
409 | // Position.toString |
410 | toString { |
411 | ret commaCombine( |
412 | spaceCombine( |
413 | !closed() ? null : |
414 | winnerOrLoser() |
415 | + appendBracketed(strOrNull(closeReason)) |
416 | + appendBracketed(comment) |
417 | + " " + formatLocalDateWithSeconds(closingTime()) |
418 | + " " + formatProfit(profit()) + "% (" + marginCoin + " " + formatMarginProfit(coinProfit()) + ")" |
419 | + " (min " + formatProfit(antiCrest()) |
420 | + ", max " + formatProfit(crest()) + ")" |
421 | + ", " + formatDouble1(leverage) + "X " + upper(type()) |
422 | + " held " + formatHoursMinutesColonSeconds(duration()), |
423 | |
424 | ), |
425 | openingTime() == 0 ? null : "opened " + formatLocalDateWithSeconds(openingTime()) |
426 | + (openReason == null ? "" : " " + roundBracket(str(openReason))), |
427 | "before leverage: " + formatProfit(profitBeforeLeverage()) + "%", |
428 | "margin: " + marginCoin + " " + formatMarginPrice(margin()), |
429 | "crypto: " + formatPrice(cryptoAmount), |
430 | "opening price: " + formatPriceX(openingPrice) |
431 | + (isNaN(digitizedOpeningPrice()) ? "" : " (digitized: " + formatPrice(digitizedOpeningPrice()) |
432 | + ")") + (openingStep == 0 ? "" : " @ step " + openingStep), |
433 | !closed() ? null : "closing price: " + formatPriceX(closingPrice) |
434 | + (closingStep == 0 ? "" : " @ step " + closingStep), |
435 | ); |
436 | } |
437 | |
438 | void closeOnMarket { |
439 | if (dontCloseOnMarket) ret; |
440 | if (isOpenError()) ret with log("Not closing because open error: " + this); |
441 | try { |
442 | if (market != null) { |
443 | log("Closing on market: " + this); |
444 | market.closePosition(new IFuturesMarket.CloseOrder() |
445 | .holdSide(HoldSide.fromInt(direction)) |
446 | .cryptoAmount(cryptoAmount) |
447 | .leverage(leverage)); |
448 | closedOnMarket(true); |
449 | change(); |
450 | } |
451 | } on fail e { |
452 | closeError(toPersistableThrowable(e)); |
453 | } |
454 | } |
455 | |
456 | void open { |
457 | margin = cryptoAmount*openingPrice/leverage; |
458 | log("Opening: " + this); |
459 | openingTime = currentTime(); |
460 | openPositions.add(this); |
461 | change(); |
462 | } |
463 | |
464 | void openOnMarket { |
465 | try { |
466 | if (market != null) { |
467 | log("Opening on market: " + this); |
468 | var order = new IFuturesMarket.OpenOrder() |
469 | .clientOrderID(positionID()) |
470 | .holdSide(HoldSide.fromInt(direction)) |
471 | .cryptoAmount(cryptoAmount) |
472 | .leverage(leverage) |
473 | .isCross(true); |
474 | |
475 | if (tpSlOnPlatform) { |
476 | if (tpPrice != null) |
477 | order.takeProfitPrice(tpPrice); |
478 | if (slPrice != null) |
479 | order.stopLossPrice(slPrice); |
480 | } |
481 | |
482 | market.openPosition(order); |
483 | openOrderID(order.orderID()); |
484 | openedOnMarket(true); |
485 | change(); |
486 | } |
487 | } catch e { |
488 | openError(toPersistableThrowable(e)); |
489 | log("Open error: " + getStackTrace(e)); |
490 | positionsThatFailedToOpen.add(this); |
491 | close("Open error"); |
492 | } |
493 | } |
494 | |
495 | void updateStats { |
496 | double profit = profit(); |
497 | crest(max(crest, profit)); |
498 | antiCrest(min(antiCrest, profit)); |
499 | } |
500 | |
501 | void addJuicer(AbstractJuicer juicer) { |
502 | juicers(listCreateAndAdd(juicers, juicer)); |
503 | } |
504 | |
505 | void removeJuicer(AbstractJuicer juicer) { |
506 | juicers(removeDyn(juicers, juicer)); |
507 | } |
508 | |
509 | // Call this when position was closed manually on the platform |
510 | void notification_closedOutsideOfStrategy() { |
511 | closedOnMarket(true); |
512 | close("Closed outside of strategy"); |
513 | } |
514 | |
515 | } // end of Position |
516 | |
517 | gettable double currentPrice = 0; |
518 | |
519 | gettable double oldPrice = Double.NaN; |
520 | gettable double startingPrice = Double.NaN; |
521 | settable long startTime; |
522 | settableWithVar double realizedProfit; |
523 | settableWithVar double realizedCoinProfit; |
524 | settableWithVar int realizedWins; |
525 | settableWithVar double realizedCoinWins; |
526 | settableWithVar int realizedLosses; |
527 | settableWithVar double realizedCoinLosses; |
528 | settableWithVar long stepCount; |
529 | settableWithVar long stepSince; |
530 | new LinkedHashSet<Position> openPositions; |
531 | gettable new L<Position> closedPositions; |
532 | gettable new L<Position> positionsThatFailedToOpen; |
533 | |
534 | void closeOnMarketMerged(Cl<? extends Position> positions) { |
535 | if (market == null) ret; |
536 | closeOnMarketMerged_oneDirection(filter(positions, -> .isShort())); |
537 | closeOnMarketMerged_oneDirection(filter(positions, -> .isLong())); |
538 | } |
539 | |
540 | // all positions must have the same direction |
541 | void closeOnMarketMerged_oneDirection(L<? extends Position> positions) { |
542 | if (empty(positions)) ret; |
543 | |
544 | double direction = first(positions).direction; |
545 | double cryptoAmount = doubleSum(positions, -> .cryptoAmount); |
546 | log("Closing on market: " + positions); |
547 | |
548 | try { |
549 | market.closePosition(new IFuturesMarket.CloseOrder() |
550 | .holdSide(HoldSide.fromInt(direction)) |
551 | .cryptoAmount(cryptoAmount); |
552 | |
553 | for (p : positions) |
554 | p.closedOnMarket(true); |
555 | change(); |
556 | } catch e { |
557 | var e2 = toPersistableThrowable(e); |
558 | for (p : positions) |
559 | p.closeError(e2); |
560 | } |
561 | } |
562 | |
563 | bool hasPosition(double price, double direction) { |
564 | ret findPosition(price, direction) != null; |
565 | } |
566 | |
567 | Position closePosition(double price, double direction, O closeReason) { |
568 | var p = findPosition(price, direction); |
569 | p?.close(closeReason); |
570 | ret p; |
571 | } |
572 | |
573 | void closePositions(Cl<? extends Position> positions, O closeReason default null) { |
574 | if (mergePositionsForMarket) { |
575 | for (p : positions) |
576 | p.dontCloseOnMarket(true).close(closeReason); |
577 | closeOnMarketMerged(positions); |
578 | } else |
579 | forEach(positions, -> .close(closeReason)); |
580 | } |
581 | |
582 | Position findPosition(double digitizedPrice, double direction) { |
583 | ret firstThat(openPositions(), p -> |
584 | diffRatio(p.digitizedOpeningPrice(), digitizedPrice) <= epsilon() && sign(p.direction) == sign(direction)); |
585 | } |
586 | |
587 | void printPositions { |
588 | print(colonCombine(n2(openPositions, "open position"), |
589 | joinWithComma(openPositions))); |
590 | } |
591 | |
592 | bool started() { ret !isNaN(startingPrice); } |
593 | void prices(double... prices) { |
594 | fOr (price : prices) { |
595 | if (!active()) ret; |
596 | price(price); |
597 | } |
598 | } |
599 | |
600 | swappable PriceCells makePriceCells(double basePrice) { |
601 | ret new GeometricPriceCells(basePrice, cellSize); |
602 | } |
603 | |
604 | double digitizedPrice() { ret digitizer == null ? Double.NaN : digitizer.digitizedPrice(); } |
605 | double lastDigitizedPrice() { ret digitizer == null ? Double.NaN : digitizer.lastDigitizedPrice(); } |
606 | int digitizedCellNumber() { ret digitizer == null ? 0 : digitizer.cellNumber(); } |
607 | |
608 | void handleNewPriceInQ(double price) { |
609 | q.add(-> price(price)); |
610 | } |
611 | |
612 | void nextStep { |
613 | ++stepCount; |
614 | stepSince(currentTime()); |
615 | } |
616 | |
617 | void afterStep { |
618 | double coinProfit = coinProfit(); |
619 | maxDebt(max(maxDebt, debt())); |
620 | //minCoinProfit = min(minCoinProfit, coinProfit); |
621 | //maxBoundCoin = max(maxBoundCoin, boundCoin()); |
622 | maxInvestment(max(maxInvestment, investment())); |
623 | strategyCrest(max(strategyCrest, coinProfit)); |
624 | maxDrawdown(max(maxDrawdown, strategyCrest-coinProfit)); |
625 | if (takeCoinProfitEnabled() && coinProfit >= takeCoinProfit) { |
626 | log("Taking coin profit."); |
627 | closeMyself(); |
628 | } |
629 | |
630 | for (p : openPositions) p.updateStats(); |
631 | } |
632 | |
633 | double investment() { |
634 | ret boundCoin()-realizedCoinProfit; |
635 | } |
636 | |
637 | double boundCoin() { |
638 | ret max(openMargins(), max(0, -coinProfit())); |
639 | } |
640 | |
641 | double fromCellNumber(double cellNumber) { ret cells().fromCellNumber(cellNumber); } |
642 | double toCellNumber(double price) { ret cells().toCellNumber(price); } |
643 | |
644 | L<Position> shortPositionsAtOrBelowDigitizedPrice(double openingPrice) { |
645 | ret filter(openPositions, p -> p.isShort() && p.digitizedOpeningPrice() <= openingPrice); |
646 | } |
647 | |
648 | L<Position> shortPositionsAtOrAboveDigitizedPrice(double openingPrice) { |
649 | ret filter(openPositions, p -> p.isShort() && p.digitizedOpeningPrice() >= openingPrice); |
650 | } |
651 | |
652 | L<Position> longPositionsAtOrAboveDigitizedPrice(double openingPrice) { |
653 | ret filter(openPositions, p -> p.isLong() && p.digitizedOpeningPrice() >= openingPrice); |
654 | } |
655 | |
656 | L<Position> longPositionsAtOrBelowDigitizedPrice(double openingPrice) { |
657 | ret filter(openPositions, p -> p.isLong() && p.digitizedOpeningPrice() <= openingPrice); |
658 | } |
659 | |
660 | PriceCells cells aka priceCells() { |
661 | ret digitizer?.cells; |
662 | } |
663 | |
664 | S formatPriceX(double price) { |
665 | if (isNaN(price)) ret "-"; |
666 | S s = formatPrice(price); |
667 | if (cells() == null) ret s; |
668 | double num = cells().priceToCellNumber(price); |
669 | ret s + " (C" + formatDouble2(num) + ")"; |
670 | } |
671 | |
672 | abstract void price aka currentPrice(double price); |
673 | |
674 | bool inCooldown() { |
675 | if (cooldownMinutes <= 0) false; |
676 | if (nempty(openPositions)) true; |
677 | var lastClosed = last(closedPositions); |
678 | if (lastClosed == null) false; |
679 | var minutes = toMinutes(currentTime()-lastClosed.closingTime); |
680 | ret minutes < cooldownMinutes; |
681 | } |
682 | |
683 | void assureCanOpen(Position p) { |
684 | if (cooldownMinutes > 0) { |
685 | if (nempty(openPositions)) |
686 | fail("Already have an open position"); |
687 | var lastClosed = last(closedPositions); |
688 | if (lastClosed != null) { |
689 | var minutes = toMinutes(currentTime()-lastClosed.closingTime); |
690 | if (minutes < cooldownMinutes) |
691 | fail("Last position closed " + formatMinutes(fromMinutes(minutes)) + " ago, cooldown is " + cooldownMinutes); |
692 | } |
693 | } |
694 | } |
695 | |
696 | <P extends Position> P openPosition(P p, int direction, O openReason default null) { |
697 | try { |
698 | assureCanOpen(p); |
699 | |
700 | p.openReason(openReason); |
701 | var price = digitizedPrice(); |
702 | var realPrice = currentPrice(); |
703 | logVars("openPosition", +realPrice, +price, +digitizer); |
704 | if ((isNaN(price) || price == 0) && digitizer != null) { |
705 | price = digitizer.digitizeIndividually(currentPrice()); |
706 | print("digitized individually: " + price); |
707 | } |
708 | p.openingPrice(realPrice); |
709 | p.direction(direction); |
710 | p.digitizedOpeningPrice(price); |
711 | |
712 | // Calculate quantity (cryptoAmount) from margin |
713 | // unless cryptoAmount is already set |
714 | |
715 | if (p.cryptoAmount == 0) |
716 | p.cryptoAmount = p.marginToUse/realPrice*leverage; |
717 | |
718 | // Round cryptoAmount to the allowed increments |
719 | p.cryptoAmount = roundTo(cryptoStep, p.cryptoAmount); |
720 | |
721 | // Clamp to minimum/maximum order |
722 | p.cryptoAmount = clamp(p.cryptoAmount, minCrypto, maxCrypto); |
723 | |
724 | log(renderVars("openPosition", +marginPerPosition, +realPrice, +leverage, cryptoAmount := p.cryptoAmount, +cryptoStep)); |
725 | p.open(); |
726 | //print("Opening " + p); |
727 | //printPositions(); |
728 | p.openOnMarket(); |
729 | ret p; |
730 | } on fail e { |
731 | log(e); |
732 | } |
733 | } |
734 | |
735 | LS status() { |
736 | double mulProf = multiplicativeProfit(); |
737 | ret llNonNulls( |
738 | empty(comment) ? null : "Comment: " + comment, |
739 | "Profit: " + marginCoin + " " + plusMinusFix(formatMarginPrice(coinProfit())), |
740 | "Realized profit: " + marginCoin + " " + formatMarginProfit(realizedCoinProfit) + " from " + n2(closedPositions, "closed position") |
741 | + " (" + formatMarginProfit(realizedCoinWins) + " from " + n2(realizedWins, "win") |
742 | + ", " + formatMarginProfit(realizedCoinLosses) + " from " + n2(realizedLosses, "loss", "losses") + ")", |
743 | "Unrealized profit: " + marginCoin + " " + formatMarginProfit(unrealizedCoinProfit()) + " in " + n2(openPositions, "open position"), |
744 | isNaN(mulProf) ? null : "Multiplicative profit: " + formatProfit(mulProf) + "%", |
745 | //baseToString(), |
746 | !primed() ? null : "Primed", |
747 | !started() ? null : "Started. current price: " + formatPriceX(currentPrice) |
748 | + (isNaN(digitizedPrice()) ? "" : ", digitized: " + formatPriceX(digitizedPrice())), |
749 | (riskPerTrade == 0 ? "" : "Risk per trade: " + formatDouble1(riskPerTrade) + "%. ") |
750 | + "Position size: " + marginCoin + " " + formatPrice(marginPerPosition) + "x" + formatDouble1(leverage) + " = " + marginCoin + " " + formatPrice(positionSize()), |
751 | !usingCells() ? null : "Cell size: " + formatCellSize(cellSize), |
752 | spaceCombine("Step " + n2(stepCount), renderStepSince()), |
753 | //"Debt: " + marginCoin + " " + formatMarginProfit(debt()) + " (max seen: " + formatMarginProfit(maxDebt) + ")", |
754 | "Investment used: " + marginCoin + " " + formatMarginPrice(maxInvestment()), |
755 | strategyJuicer == null ?: "Strategy juicer: " + strategyJuicer, |
756 | //"Drift: " + cryptoCoin + " " + plusMinusFix(formatCryptoAmount(drift())), |
757 | ); |
758 | } |
759 | |
760 | S renderStepSince() { |
761 | if (stepSince == 0) ret ""; |
762 | ret "since " + (active() |
763 | ? formatHoursMinutesColonSeconds(currentTime()-stepSince) |
764 | : formatLocalDateWithSeconds(stepSince)); |
765 | } |
766 | |
767 | LS fullStatus() { |
768 | ret listCombine( |
769 | tradingAccount, |
770 | status(), |
771 | "", |
772 | n2(openPositions, "open position") + ":", |
773 | reversed(openPositions), |
774 | "", |
775 | n2(closedPositions, "closed position") |
776 | + " (" + (showClosedPositionsBackwards ? "latest first" : "oldest first") + "):", |
777 | showClosedPositionsBackwards ? reversed(closedPositions) : closedPositions, |
778 | ); |
779 | } |
780 | |
781 | void feed(PricePoint pricePoint) { |
782 | if (!active()) ret; |
783 | setTime(pricePoint.timestamp); |
784 | price(pricePoint.price); |
785 | } |
786 | |
787 | void feed(TickerSequence ts) { |
788 | if (!active()) ret; |
789 | if (ts == null) ret; |
790 | for (pricePoint : ts.pricePoints()) |
791 | feed(pricePoint); |
792 | } |
793 | |
794 | public int compareTo(G22TradingStrategy s) { |
795 | ret s == null ? 1 : cmp(coinProfit(), s.coinProfit()); |
796 | } |
797 | |
798 | // Returns closed positions |
799 | L<Position> closeAllPositions(O reason default "User close") { |
800 | var positions = openPositions(); |
801 | closePositions(positions, reason); |
802 | ret (L) positions; |
803 | } |
804 | |
805 | void closeMyself() { |
806 | closedItself(currentTime()); |
807 | closeAllPositionsAndDeactivate(); |
808 | } |
809 | |
810 | void closeAllPositionsAndDeactivate { |
811 | deactivate(); |
812 | closeAllPositions(); |
813 | } |
814 | |
815 | void deactivate { |
816 | market(null); |
817 | if (!active) ret; |
818 | active(false); |
819 | deactivated(currentTime()); |
820 | log("Strategy deactivated."); |
821 | } |
822 | |
823 | void reset aka reset_G22TradingStrategy() { |
824 | resetFields(this, fieldsToReset()); |
825 | change(); |
826 | } |
827 | |
828 | selfType emptyClone aka emptyClone_G22TradingStrategy() { |
829 | var clone = shallowCloneToUnlistedConcept(this); |
830 | clone.reset(); |
831 | ret clone; |
832 | } |
833 | |
834 | L<Position> allPositions() { |
835 | ret concatLists(openPositions, closedPositions); |
836 | } |
837 | |
838 | L<Position> sortedPositions() { |
839 | var allPositions = allPositions(); |
840 | ret sortedByCalculatedField(allPositions, -> .openingTime()); |
841 | } |
842 | |
843 | bool positionsAreNonOverlapping() { |
844 | for (a, b : unpair overlappingPairs(sortedPositions())) |
845 | if (b.openingTime() < a.closingTime()) |
846 | false; |
847 | true; |
848 | } |
849 | |
850 | // Profit when applying all positions (somewhat theoretical because you |
851 | // might go below platform limits) |
852 | // Also only works when positions are linear |
853 | double multiplicativeProfit() { |
854 | if (!positionsAreNonOverlapping()) ret Double.NaN; |
855 | double profit = 1; |
856 | for (p : sortedPositions()) |
857 | profit *= 1+p.profit()/100; |
858 | ret (profit-1)*100; |
859 | } |
860 | |
861 | bool haveBackData() { ret backDataHoursWanted == 0 | backDataFed; } |
862 | |
863 | bool didRealTrades() { |
864 | ret any(allPositions(), p -> p.openedOnMarket() || p.closedOnMarket()); |
865 | } |
866 | |
867 | S formatCellSize(double cellSize) { |
868 | ret formatPercentage(cellSize, 3); |
869 | } |
870 | |
871 | S areaDesc() { |
872 | if (eq(area, "Candidates")) ret "Candidate"; |
873 | ret nempty(area) ? area : archived ? "Archived" : ""; |
874 | } |
875 | |
876 | selfType setTime aka currentTime(long time) { |
877 | int age = ifloor(ageInHours()); |
878 | long lastMod = mod(currentTime()-startTime, hoursToMS(1)); |
879 | currentTime = -> time; |
880 | if (ifloor(ageInHours()) > age) |
881 | log("Hourly profit log: " + formatMarginProfit(coinProfit())); |
882 | this; |
883 | } |
884 | |
885 | double ageInHours() { ret startTime == 0 ? 0 : msToHours(currentTime()-startTime); } |
886 | |
887 | // only use near start of strategy, untested otherwise |
888 | selfType changeCellSize(double newCellSize) { |
889 | double oldCellSize = cellSize(); |
890 | if (oldCellSize == newCellSize) this; |
891 | cellSize(newCellSize); |
892 | if (digitizer != null) |
893 | digitizer.swapPriceCells(makePriceCells(priceCells().basePrice())); |
894 | log("Changed cell size from " + oldCellSize + " to " + newCellSize); |
895 | this; |
896 | } |
897 | |
898 | bool hasClosedItself() { ret closedItself != 0; } |
899 | |
900 | public double juiceValue() { ret coinProfit(); } |
901 | |
902 | // drift is our cumulative delta (=sum of all signed position |
903 | // quantities) |
904 | double drift() { |
905 | double drift = 0; |
906 | for (p : openPositions()) |
907 | drift += p.cryptoAmount()*p.direction(); |
908 | ret drift; |
909 | } |
910 | |
911 | S formatCryptoAmount(double amount) { |
912 | ret formatDouble3(amount); |
913 | } |
914 | |
915 | Position openShort() { ret openPosition(-1); } |
916 | Position openLong() { ret openPosition(1); } |
917 | |
918 | Position openPosition(int direction, O openReason default null) { |
919 | new Position p; |
920 | p.marginToUse = marginToUseForNewPosition(); |
921 | ret openPosition(p, direction, openReason); |
922 | } |
923 | |
924 | // Open or close positions until the drift (delta) is |
925 | // equal to targetDrift (or as close as the platform's |
926 | // restrictions allow). |
927 | // Returns the list of positions opened or closed |
928 | // (Function is in dev.) |
929 | L<Position> adjustDrift(double targetDrift, O reason default null) { |
930 | new L<Position> changeList; |
931 | |
932 | // target 0? close all |
933 | if (targetDrift == 0) |
934 | ret closeAllPositions(reason); |
935 | |
936 | double drift = drift(); |
937 | |
938 | // target already reached? done |
939 | if (drift == targetDrift) ret changeList; |
940 | |
941 | int direction = sign(targetDrift); |
942 | |
943 | // Are we changing direction? Then close everything first. |
944 | if (sign(drift) != direction) |
945 | changeList.addAll(closeAllPositions(reason)); |
946 | |
947 | // Now we know targetDrift and drift have the same sign. |
948 | double diff = abs(targetDrift)-abs(drift); |
949 | |
950 | // Round to allow increments |
951 | diff = roundTo(cryptoStep, diff); |
952 | |
953 | if (diff > 0) { |
954 | // We need to open a new position - that's easy |
955 | |
956 | new Position p; |
957 | p.cryptoAmount = diff; |
958 | changeList.add(openPosition(p, direction, reason)); |
959 | } else { |
960 | double toClose = -diff; |
961 | |
962 | // We need to close positions - that's a bit more tricky |
963 | |
964 | // Filter by direction to be sure in case there is |
965 | // hedging (you shouldn't do hedging, really, though) |
966 | var positions = filter(openPositions(), p -> p.direction() == direction); |
967 | |
968 | // Let's look for an exact size match first. |
969 | // We rounded to cryptoStep, so using == is ok. |
970 | var _toClose = toClose; |
971 | var exactMatch = firstThat(positions, p -> p.cryptoAmount == _toClose); |
972 | |
973 | if (exactMatch != null) { |
974 | exactMatch.close(reason); |
975 | changeList.add(exactMatch); |
976 | ret changeList; |
977 | } |
978 | |
979 | // No exact match. Go through positions starting with |
980 | // the oldest one. |
981 | |
982 | for (p : positions) { |
983 | toClose = roundTo(cryptoStep, toClose); |
984 | if (toClose == 0) break; |
985 | if (toClose >= p.cryptoAmount) { |
986 | // Need to close the whole position |
987 | toClose -= p.cryptoAmount; |
988 | p.close(reason); |
989 | changeList.add(p); |
990 | } else { |
991 | // Need a partial close. |
992 | |
993 | changeList.add(p); |
994 | var remainderPosition = p.partialClose(toClose, reason); |
995 | changeList.add(remainderPosition); |
996 | } |
997 | } |
998 | } |
999 | |
1000 | ret changeList; |
1001 | } |
1002 | |
1003 | L<Position> winners() { ret filter(closedPositions(), p -> p.coinProfit() > 0); } |
1004 | L<Position> losers() { ret filter(closedPositions(), p -> p.coinProfit() < 0); } |
1005 | |
1006 | L<? extends Position> openPositions() { ret cloneList(openPositions); } |
1007 | |
1008 | double winRate() { |
1009 | ret l(winners()) * 100.0 / l(closedPositions); |
1010 | } |
1011 | |
1012 | bool usingCells() { true; } |
1013 | |
1014 | double positionSize() { |
1015 | ret marginToUseForNewPosition()*leverage; |
1016 | } |
1017 | |
1018 | double marginToUseForNewPosition() { |
1019 | ret scaleMargin(marginPerPosition); |
1020 | } |
1021 | |
1022 | // to simulate compounding in backtest |
1023 | double scaleMargin(double margin) { |
1024 | // Live? Then no scaling. |
1025 | if (usingLiveData) ret margin; |
1026 | |
1027 | ret margin*compoundingFactor(); |
1028 | } |
1029 | |
1030 | double compoundingFactor() { |
1031 | // No compounding selected? |
1032 | if (compoundingBaseEquity == 0) ret 1; |
1033 | |
1034 | ret max(0, remainingEquity()/compoundingBaseEquity); |
1035 | } |
1036 | |
1037 | // only for backtesting |
1038 | double remainingEquity() { |
1039 | ret compoundingBaseEquity+realizedCoinProfit(); |
1040 | } |
1041 | |
1042 | class RiskToMargin { |
1043 | // in percent |
1044 | settable double riskForTrade = riskPerTrade; |
1045 | |
1046 | settable double stopLossPercent; |
1047 | settable double price = currentPrice(); |
1048 | settable double fullEquity; |
1049 | |
1050 | double riskedEquity() { |
1051 | ret fullEquity*riskForTrade/100; |
1052 | } |
1053 | |
1054 | double qty() { |
1055 | ret riskedEquity()/(price*stopLossPercent/100); |
1056 | } |
1057 | |
1058 | double margin aka get() { |
1059 | ret qty()*price/leverage(); |
1060 | } |
1061 | } |
1062 | |
1063 | void fixRealizedStats { |
1064 | realizedProfit(0); |
1065 | realizedCoinProfit(0); |
1066 | realizedWins(0); |
1067 | realizedCoinWins(0); |
1068 | realizedLosses(0); |
1069 | realizedCoinLosses(0); |
1070 | for (p : closedPositions()) p.addToRealizedStats(); |
1071 | } |
1072 | |
1073 | // p must not be open |
1074 | bool deletePosition(Position p) { |
1075 | if (closedPositions.remove(p) || positionsThatFailedToOpen.remove(p)) { |
1076 | fixRealizedStats(); |
1077 | true; |
1078 | } |
1079 | false; |
1080 | } |
1081 | |
1082 | void deletePositionsThatFailedToOpen() { |
1083 | for (p : cloneList(positionsThatFailedToOpen)) |
1084 | deletePosition(p); |
1085 | } |
1086 | |
1087 | void deleteAllPositions() { |
1088 | for (p : cloneList(closedPositions)) |
1089 | deletePosition(p); |
1090 | } |
1091 | |
1092 | bool hasRiskPerTrade() { ret riskPerTrade != 0; } |
1093 | |
1094 | Position addFakePosition(double coinProfit) { |
1095 | new Position p; |
1096 | p.makeFake(coinProfit); |
1097 | ret p; |
1098 | } |
1099 | |
1100 | // If there is exactly one open position: |
1101 | // Open another position in the same direction |
1102 | // with same size |
1103 | Position pyramid(O openReason default "Pyramiding") { |
1104 | ret extendedPyramid(2, openReason); |
1105 | } |
1106 | |
1107 | // level: pyramid level to be reached |
1108 | // (must be one higher than current number of positions |
1109 | // to trigger - no double adding at once) |
1110 | Position extendedPyramid(int level, O openReason default "Pyramiding") { |
1111 | if (l(openPositions()) != level-1) |
1112 | null; |
1113 | |
1114 | var p = first(openPositions); |
1115 | new Position p2; |
1116 | p2.cryptoAmount(p.cryptoAmount); |
1117 | ret openPosition(p2, sign(p.direction), openReason); |
1118 | } |
1119 | |
1120 | Position newPosition() { |
1121 | ret new Position; |
1122 | } |
1123 | } |
download show line numbers debug dex old transpilations
Travelled to 4 computer(s): elmgxqgtpvxh, iveijnkanddl, mqqgnosmbjvj, wnsclhtenguj
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 |
1938 | ubataecj | 555 | 2024-05-01 03:36:05 |
Snippet ID: | #1036209 |
Snippet name: | G22TradingStrategy |
Eternal ID of this version: | #1036209/283 |
Text MD5: | 9540869df39e35db32189a07d0ae1685 |
Transpilation MD5: | 3b11026bdf947e8a6f1741e38a182559 |
Author: | stefan |
Category: | javax / trading |
Type: | JavaX fragment (include) |
Public (visible to everyone): | Yes |
Archived (hidden from active list): | No |
Created/modified: | 2024-07-07 06:27:05 |
Source code size: | 35291 bytes / 1123 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 1150 / 2404 |
Version history: | 282 change(s) |
Referenced in: | [show references] |