Libraryless. Click here for Pure Java version (25749L/159K).
1 | concept Corridor extends G22TradingStrategy { |
2 | // how many percent the crypto price needs to move |
3 | // for one profitable trade |
4 | settableWithVar double cellSize = 0.2; |
5 | |
6 | // how many cells the corridor has |
7 | settableWithVar int capacity = 6; |
8 | |
9 | settableWithVar int maxPositionsPerDirection = 4; |
10 | |
11 | settableWithVar bool openTwoPositionsAtStart = true; |
12 | settableWithVar bool alwaysOpenTwoPositions; |
13 | |
14 | // position options |
15 | settableWithVar double leverage = 1; |
16 | settableWithVar double marginPerPosition = 1; // in USDT |
17 | |
18 | // internal |
19 | |
20 | // primed = started but waiting for first price |
21 | settableWithVar bool primed; |
22 | |
23 | settableWithVar PriceDigitizer2 digitizer; |
24 | settableWithVar int direction; |
25 | |
26 | settableWithVar double epsilon = 1e-6; |
27 | |
28 | // maximum debt seen |
29 | settableWithVar double maxDebt; |
30 | //settableWithVar double minCoinProfit; |
31 | //settableWithVar double maxBoundCoin; |
32 | |
33 | record noeq Position(double openingPrice, double direction, double digitizedOpeningPrice) { |
34 | double closingPrice = Double.NaN; |
35 | long openingStep, closingStep; |
36 | gettable long openingTime; |
37 | gettable long closingTime; |
38 | double leverage; |
39 | settable double cryptoAmount; |
40 | settable O openError; |
41 | settable O closeReason; |
42 | settable O closeError; |
43 | gettable double margin; |
44 | settable bool openedOnMarket; |
45 | settable bool closedOnMarket; |
46 | |
47 | Corridor strategy() { ret Corridor.this; } |
48 | |
49 | { |
50 | // Corridor.this is not set when unstructuring |
51 | if (!dynamicObjectIsLoading()) { |
52 | openingStep = stepCount; |
53 | leverage = Corridor.this.leverage; |
54 | openingTime = currentTime(); |
55 | } |
56 | } |
57 | |
58 | double digitizedOpeningPrice() { ret digitizedOpeningPrice; } |
59 | |
60 | bool isLong() { ret direction > 0; } |
61 | bool isShort() { ret direction < 0; } |
62 | |
63 | bool closed() { ret !isNaN(closingPrice); } |
64 | S type() { ret trading_directionToPositionType(direction); } |
65 | |
66 | double profitAtPrice(double price) { |
67 | ret ((price-openingPrice)/openingPrice*direction*100-adversity)*leverage; |
68 | } |
69 | |
70 | double profit() { |
71 | ret profitAtPrice(closed() ? closingPrice : currentPrice()); |
72 | } |
73 | |
74 | double coinProfit() { |
75 | ret profit()/100*margin(); |
76 | } |
77 | |
78 | void close(O closeReason) { |
79 | if (closed()) fail("Can't close again"); |
80 | if (closeReason != null) closeReason(closeReason); |
81 | openPositions.remove(this); |
82 | closingPrice = currentPrice(); |
83 | closingTime = currentTime(); |
84 | closingStep = stepCount; |
85 | closedPositions.add(this); |
86 | realizedProfit += profit(); |
87 | realizedCoinProfit += coinProfit(); |
88 | closeOnMarket(); |
89 | //print(this); |
90 | //printPositions(); |
91 | } |
92 | |
93 | // Position.toString |
94 | toString { |
95 | ret commaCombine( |
96 | spaceCombine( |
97 | closed() ? "Closed " + formatLocalDateWithSeconds(closingTime()) : null, |
98 | formatDouble1(leverage) + "X " + upper(type()) |
99 | ), |
100 | "opened " + formatLocalDateWithSeconds(openingTime()), |
101 | !closed() ? null : or(closeReason, "unknown reason"), |
102 | "profit: " + formatProfit(profit()) + "% (" + marginCoin + " " + plusMinusFix(formatPrice(coinProfit())) + ")", |
103 | "margin: " + marginCoin + " " + formatPrice(margin()), |
104 | "crypto: " + formatPrice(cryptoAmount), |
105 | "opening price: " + formatPriceX(openingPrice) |
106 | + " @ step " + openingStep, |
107 | !closed() ? null : "closing price: " + formatPriceX(closingPrice) |
108 | + " @ step " + closingStep, |
109 | //"digitized: " + formatPrice(digitizedOpeningPrice()), |
110 | ); |
111 | } |
112 | |
113 | void closeOnMarket { |
114 | try { |
115 | if (market != null) { |
116 | log("Closing on market: " + this); |
117 | market.closePosition(new IFuturesMarket.CloseOrder() |
118 | .holdSide(HoldSide.fromInt(direction)) |
119 | .cryptoAmount(cryptoAmount)); |
120 | closedOnMarket(true); |
121 | change(); |
122 | } |
123 | } catch e { |
124 | closeError = toPersistableThrowable(e); |
125 | } |
126 | } |
127 | |
128 | void open { |
129 | margin = cryptoAmount*openingPrice/leverage; |
130 | openPositions.add(this); |
131 | } |
132 | |
133 | void openOnMarket { |
134 | try { |
135 | if (market != null) { |
136 | log("Opening on market: " + this); |
137 | market.openPosition(new IFuturesMarket.OpenOrder() |
138 | .holdSide(HoldSide.fromInt(direction)) |
139 | .cryptoAmount(cryptoAmount) |
140 | .leverage(leverage) |
141 | .isCross(true)); |
142 | openedOnMarket(true); |
143 | change(); |
144 | } |
145 | } catch e { |
146 | openError(toPersistableThrowable(e)); |
147 | log("Open error:" + e); |
148 | close("Open error"); |
149 | } |
150 | } |
151 | } |
152 | |
153 | gettable double currentPrice = 0; |
154 | |
155 | gettable double oldPrice = Double.NaN; |
156 | gettable double startingPrice = Double.NaN; |
157 | gettable int lastDirection; |
158 | gettable double realizedProfit; |
159 | gettable double realizedCoinProfit; |
160 | settableWithVar long stepCount; |
161 | settableWithVar long stepSince; |
162 | gettable new LinkedHashSet<Position> openPositions; |
163 | gettable new L<Position> closedPositions; |
164 | |
165 | Position openPosition(int direction) { |
166 | if (l(positionsInDirection(direction)) >= maxPositionsPerDirection) { |
167 | log("Not opening new position in direction " + direction + ", max reached"); |
168 | null; |
169 | } |
170 | |
171 | var price = digitizedPrice(); |
172 | if (isNaN(price)) price = digitizer.digitizeIndividually(currentPrice()); |
173 | var p = new Position(currentPrice(), direction, price); |
174 | double cryptoAmount = marginPerPosition/price*leverage; |
175 | cryptoAmount = roundTo(cryptoStep, cryptoAmount); |
176 | p.cryptoAmount = max(minCrypto, cryptoAmount); |
177 | p.open(); |
178 | //print("Opening " + p); |
179 | //printPositions(); |
180 | p.openOnMarket(); |
181 | ret p; |
182 | } |
183 | |
184 | bool hasPosition(double price, double direction) { |
185 | ret findPosition(price, direction) != null; |
186 | } |
187 | |
188 | Position closePosition(double price, double direction, O closeReason) { |
189 | var p = findPosition(price, direction); |
190 | p?.close(closeReason); |
191 | ret p; |
192 | } |
193 | |
194 | void closePositions(Cl<Position> positions, O closeReason default null) { |
195 | forEach(positions, -> .close(closeReason)); |
196 | } |
197 | |
198 | Position findPosition(double digitizedPrice, double direction) { |
199 | ret firstThat(openPositions(), p -> |
200 | diffRatio(p.digitizedOpeningPrice(), digitizedPrice) <= epsilon() && p.direction == direction); |
201 | } |
202 | |
203 | Position openShort() { ret openPosition(-1); } |
204 | Position openLong() { ret openPosition(1); } |
205 | |
206 | void printPositions { |
207 | print(colonCombine(n2(openPositions, "open position"), |
208 | joinWithComma(openPositions))); |
209 | } |
210 | |
211 | bool started() { ret !isNaN(startingPrice); } |
212 | |
213 | void start { |
214 | if (started()) fail("Already started"); |
215 | if (currentPrice() == 0) |
216 | ret with primed(true); |
217 | |
218 | // Save starting price, create price cells & digitizer |
219 | |
220 | startingPrice = currentPrice(); |
221 | var priceCells = makePriceCells(startingPrice); |
222 | digitizer = new PriceDigitizer2(priceCells); |
223 | digitizer.verbose(verbose); |
224 | //digitizer.init(startingPrice); |
225 | |
226 | print("Starting CORRIDOR at " + startingPrice + " +/- " + cellSize); |
227 | |
228 | if (openTwoPositionsAtStart) { |
229 | nextStep(); |
230 | openPosition(1); |
231 | openPosition(-1); |
232 | afterStep(); |
233 | } |
234 | } |
235 | |
236 | void prices(double... prices) { |
237 | fOr (price : prices) |
238 | price(price); |
239 | } |
240 | |
241 | swappable PriceCells makePriceCells(double basePrice) { |
242 | //ret new RegularPriceCells(basePrice, cellSize); |
243 | ret new GeometricPriceCells(basePrice, cellSize); |
244 | } |
245 | |
246 | double digitizedPrice() { ret digitizer.digitizedPrice(); } |
247 | double lastDigitizedPrice() { ret digitizer.lastDigitizedPrice(); } |
248 | |
249 | void handleNewPriceInQ(double price) { |
250 | q.add(-> price(price)); |
251 | } |
252 | |
253 | void currentPrice aka price(double price) { |
254 | if (currentPrice == price) ret; |
255 | currentPrice = price; |
256 | |
257 | if (!started()) { |
258 | if (primed) { |
259 | primed(false); |
260 | start(); |
261 | } |
262 | ret; |
263 | } |
264 | |
265 | digitizer.digitize(price); |
266 | direction = sign(digitizedPrice()-lastDigitizedPrice()); |
267 | |
268 | // No cell move? |
269 | if (direction == 0) ret with afterStep(); |
270 | |
271 | nextStep(); |
272 | //printWithPrecedingNL("New digitized price: " + digitizedPrice()); |
273 | |
274 | if (started()) |
275 | step(); |
276 | |
277 | print(this); |
278 | } |
279 | |
280 | void nextStep { |
281 | ++stepCount; |
282 | stepSince(now()); |
283 | } |
284 | |
285 | swappable void step { |
286 | double p1 = lastDigitizedPrice(); |
287 | double p2 = digitizedPrice(); |
288 | direction = sign(p2-p1); |
289 | if (direction == 0) ret; |
290 | |
291 | // Find winning positions to close |
292 | |
293 | new L<Position> toClose; |
294 | for (Position p : direction > 0 |
295 | ? longPositionsAtOrBelowDigitizedPrice(p1) |
296 | : shortPositionsAtOrAboveDigitizedPrice(p1)) { |
297 | p.closeReason("WIN"); |
298 | log("Closing winner position at " + currentPrice + " (" + p1 + " -> " + p2 + "): " + p); |
299 | toClose.add(p); |
300 | } |
301 | |
302 | // Dismantle corridor if too large |
303 | double limit = fromCellNumber(toCellNumber(digitizedPrice())-capacity*direction); |
304 | var toDismantle = direction > 0 |
305 | ? shortPositionsAtOrBelowDigitizedPrice(limit) |
306 | : longPositionsAtOrAboveDigitizedPrice(limit); |
307 | print("DISMANTLE LIMIT: " + formatPriceX(limit)); |
308 | for (Position p : toDismantle) { |
309 | p.closeReason("DISMANTLE"); |
310 | toClose.add(p); |
311 | } |
312 | |
313 | //printVars("step", +p1, +p2, +direction, +closed, +opened); |
314 | |
315 | if (nempty(toClose)) |
316 | closePositions(toClose); |
317 | |
318 | // Open backward position here (if not there yet) |
319 | if (!hasPosition(p2, -direction)) |
320 | openPosition(-direction); |
321 | |
322 | // Open forward position if configured as such |
323 | if (alwaysOpenTwoPositions && !hasPosition(p2, direction)) |
324 | openPosition(direction); |
325 | |
326 | afterStep(); |
327 | } |
328 | |
329 | void afterStep { |
330 | maxDebt = max(maxDebt, debt()); |
331 | //minCoinProfit = min(minCoinProfit, coinProfit()); |
332 | //maxBoundCoin = max(maxBoundCoin, boundCoin()); |
333 | maxInvestment = max(maxInvestment, investment()); |
334 | change(); |
335 | } |
336 | |
337 | double investment() { |
338 | ret boundCoin()-realizedCoinProfit; |
339 | } |
340 | |
341 | double boundCoin() { |
342 | ret max(openMargins(), max(0, -coinProfit())); |
343 | } |
344 | |
345 | double fromCellNumber(double cellNumber) { ret cells().fromCellNumber(cellNumber); } |
346 | double toCellNumber(double price) { ret cells().toCellNumber(price); } |
347 | |
348 | double unrealizedProfit() { |
349 | double sum = 0; |
350 | for (p : openPositions) |
351 | sum += p.profit(); |
352 | ret sum; |
353 | } |
354 | |
355 | double openMargins() { |
356 | double sum = 0; |
357 | for (p : openPositions) |
358 | sum += p.margin(); |
359 | ret sum; |
360 | } |
361 | |
362 | double unrealizedCoinProfit() { |
363 | double sum = 0; |
364 | for (p : openPositions) |
365 | sum += p.coinProfit(); |
366 | ret sum; |
367 | } |
368 | |
369 | L<Position> positionsInDirection(int direction) { |
370 | ret filter(openPositions, p -> p.direction == direction); |
371 | } |
372 | |
373 | L<Position> longPositions() { ret positionsInDirection(1); } |
374 | L<Position> shortPositions() { ret positionsInDirection(-1); } |
375 | |
376 | L<Position> shortPositionsAtOrBelowPrice(double openingPrice) { |
377 | ret filter(openPositions, p -> p.isShort() && p.openingPrice <= openingPrice); |
378 | } |
379 | |
380 | L<Position> longPositionsAtOrAbovePrice(double openingPrice) { |
381 | ret filter(openPositions, p -> p.isLong() && p.openingPrice >= openingPrice); |
382 | } |
383 | |
384 | L<Position> shortPositionsAtOrBelowDigitizedPrice(double openingPrice) { |
385 | ret filter(openPositions, p -> p.isShort() && p.digitizedOpeningPrice() <= openingPrice); |
386 | } |
387 | |
388 | L<Position> shortPositionsAtOrAboveDigitizedPrice(double openingPrice) { |
389 | ret filter(openPositions, p -> p.isShort() && p.digitizedOpeningPrice() >= openingPrice); |
390 | } |
391 | |
392 | L<Position> longPositionsAtOrAboveDigitizedPrice(double openingPrice) { |
393 | ret filter(openPositions, p -> p.isLong() && p.digitizedOpeningPrice() >= openingPrice); |
394 | } |
395 | |
396 | L<Position> longPositionsAtOrBelowDigitizedPrice(double openingPrice) { |
397 | ret filter(openPositions, p -> p.isLong() && p.digitizedOpeningPrice() <= openingPrice); |
398 | } |
399 | |
400 | L<Position> negativePositions() { |
401 | ret filter(openPositions, p -> p.profit() < 0); |
402 | } |
403 | |
404 | double debt() { |
405 | ret max(0, -unrealizedCoinProfit()); |
406 | } |
407 | |
408 | double profit() { |
409 | ret realizedProfit+unrealizedProfit(); |
410 | } |
411 | |
412 | double coinProfit() { |
413 | ret realizedCoinProfit+unrealizedCoinProfit(); |
414 | } |
415 | |
416 | S baseToString() { |
417 | ret colonCombine( |
418 | _conceptID() == 0 ? "" : "Strategy " + _conceptID(), |
419 | "Corridor"); |
420 | } |
421 | |
422 | LS status() { |
423 | var r = currentCorridorPriceRange(); |
424 | ret llNonNulls( |
425 | //baseToString(), |
426 | !primed() ? null : "Primed", |
427 | !started() ? null : "Started. current price: " + formatPriceX(currentPrice) + ", digitized: " + formatPriceX(digitizedPrice()), |
428 | "Leverage: " + leverage + ", margin per position: " + marginCoin+ " " + formatPrice(marginPerPosition), |
429 | "Cell size: " + formatPercentage(cellSize, 3) + ", max capacity: " + capacity, |
430 | !started() ? null : "Current corridor: " + (r == null ? "-" : formatPrice(r.start) + " to " + formatPrice(r.end) + " (" + formatPercentage(currentCorridorSizeInPercent(), 3) + ")"), |
431 | "Step " + n2(stepCount) + (stepSince == 0 ? "" : " since " + formatHoursMinutesColonSeconds(currentTime()-stepSince)), |
432 | "Profit: " + marginCoin + " " + plusMinusFix(formatMarginPrice(coinProfit())), |
433 | "Realized profit: " + marginCoin + " " + formatProfit(realizedCoinProfit) + " from " + n2(closedPositions, "closed position"), |
434 | "Unrealized profit: " + marginCoin + " " + formatProfit(unrealizedCoinProfit()) + " in " + n2(openPositions, "open position"), |
435 | "Debt: " + marginCoin + " " + formatProfit(debt()) + " (max seen: " + formatProfit(maxDebt) + ")", |
436 | "Investment used: " + marginCoin + " " + formatMarginPrice(maxInvestment()), |
437 | ); |
438 | } |
439 | |
440 | LS fullStatus() { |
441 | ret listCombine( |
442 | status(), |
443 | "", |
444 | n2(openPositions, "open position") + ":", |
445 | reversed(openPositions), |
446 | "", |
447 | n2(closedPositions, "closed position") + ":", |
448 | reversed(closedPositions) |
449 | ); |
450 | } |
451 | |
452 | S formatProfit(double x) { |
453 | ret plusMinusFix(formatDouble1(x)); |
454 | } |
455 | |
456 | // Corridor.toString |
457 | toString { ret baseToString(); } |
458 | |
459 | double maxCorridorPercentSize() { |
460 | ret capacity*cellSize; |
461 | } |
462 | |
463 | PriceCells cells() { |
464 | ret digitizer?.cells; |
465 | } |
466 | |
467 | DoubleRange currentCorridorPriceRange() { |
468 | double[] openingPrices = mapToDoubleArray(openPositions, ->.openingPrice); |
469 | ret doubleRange(doubleMin(openingPrices), doubleMax(openingPrices)); |
470 | } |
471 | |
472 | double currentCorridorSizeInPercent() { |
473 | DoubleRange r = currentCorridorPriceRange(); |
474 | ret r == null ? 0 : asPercentIncrease(r.end, r.start); |
475 | } |
476 | |
477 | S formatPriceX(double price) { |
478 | if (isNaN(price)) ret "-"; |
479 | double num = cells().priceToCellNumber(price); |
480 | ret formatPrice(price) + " (C" + formatDouble2(num) + ")"; |
481 | } |
482 | |
483 | void reset { |
484 | resetFields(this, [[ |
485 | digitizer maxDebt direction oldPrice startingPrice lastDirection realizedProfit realizedCoinProfit |
486 | stepCount |
487 | openPositions closedPositions |
488 | ]]); |
489 | change(); |
490 | } |
491 | |
492 | Corridor emptyClone() { |
493 | //var clone = shallowClone(this); |
494 | var clone = shallowCloneToUnlistedConcept(this); |
495 | clone.reset(); |
496 | ret clone; |
497 | } |
498 | } |
Began life as a copy of #1036196
download show line numbers debug dex old transpilations
Travelled to 2 computer(s): mqqgnosmbjvj, wnsclhtenguj
No comments. add comment
Snippet ID: | #1036257 |
Snippet name: | Corridor - backup used in the real-money experiment |
Eternal ID of this version: | #1036257/2 |
Text MD5: | a8ece345d4f92a456d8983d17960f1ee |
Transpilation MD5: | 66ca43f9d3185ce1fd51b7e67bc4f158 |
Author: | stefan |
Category: | javax / gazelle 22 |
Type: | JavaX fragment (include) |
Public (visible to everyone): | Yes |
Archived (hidden from active list): | No |
Created/modified: | 2022-11-05 20:13:22 |
Source code size: | 15569 bytes / 498 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 120 / 172 |
Version history: | 1 change(s) |
Referenced in: | [show references] |