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
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 |
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] |