@gbozee/ultimate 0.0.2-7 → 0.0.2-70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2122 @@
1
+ // src/helpers/trade_signal.ts
2
+ function determine_close_price({
3
+ entry,
4
+ pnl,
5
+ quantity,
6
+ leverage = 1,
7
+ kind = "long"
8
+ }) {
9
+ const dollar_value = entry / leverage;
10
+ const position = dollar_value * quantity;
11
+ if (position) {
12
+ const percent = pnl / position;
13
+ const difference = position * percent / quantity;
14
+ const result = kind === "long" ? difference + entry : entry - difference;
15
+ return result;
16
+ }
17
+ return 0;
18
+ }
19
+ function determine_pnl(entry, close_price, quantity, kind = "long", contract_size) {
20
+ if (contract_size) {
21
+ const direction = kind === "long" ? 1 : -1;
22
+ return quantity * contract_size * direction * (1 / entry - 1 / close_price);
23
+ }
24
+ const difference = kind === "long" ? close_price - entry : entry - close_price;
25
+ return difference * quantity;
26
+ }
27
+ function* _get_zones({
28
+ current_price,
29
+ focus,
30
+ percent_change,
31
+ places = "%.5f"
32
+ }) {
33
+ let last = focus;
34
+ let focus_high = last * (1 + percent_change);
35
+ let focus_low = last * Math.pow(1 + percent_change, -1);
36
+ if (focus_high > current_price) {
37
+ while (focus_high > current_price) {
38
+ yield to_f(last, places);
39
+ focus_high = last;
40
+ last = focus_high * Math.pow(1 + percent_change, -1);
41
+ focus_low = last * Math.pow(1 + percent_change, -1);
42
+ }
43
+ } else {
44
+ if (focus_high <= current_price) {
45
+ while (focus_high <= current_price) {
46
+ yield to_f(focus_high, places);
47
+ focus_low = focus_high;
48
+ last = focus_low * (1 + percent_change);
49
+ focus_high = last * (1 + percent_change);
50
+ }
51
+ } else {
52
+ while (focus_low <= current_price) {
53
+ yield to_f(focus_high, places);
54
+ focus_low = focus_high;
55
+ last = focus_low * (1 + percent_change);
56
+ focus_high = last * (1 + percent_change);
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ class Signal {
63
+ focus;
64
+ budget;
65
+ percent_change = 0.02;
66
+ price_places = "%.5f";
67
+ decimal_places = "%.0f";
68
+ zone_risk = 1;
69
+ fee = 0.08 / 100;
70
+ support;
71
+ risk_reward = 4;
72
+ resistance;
73
+ risk_per_trade;
74
+ increase_size = false;
75
+ additional_increase = 0;
76
+ minimum_pnl = 0;
77
+ take_profit;
78
+ increase_position = false;
79
+ minimum_size;
80
+ first_order_size;
81
+ gap = 10;
82
+ max_size = 0;
83
+ constructor({
84
+ focus,
85
+ budget,
86
+ percent_change = 0.02,
87
+ price_places = "%.5f",
88
+ decimal_places = "%.0f",
89
+ zone_risk = 1,
90
+ fee = 0.06 / 100,
91
+ support,
92
+ risk_reward = 4,
93
+ resistance,
94
+ risk_per_trade,
95
+ increase_size = false,
96
+ additional_increase = 0,
97
+ minimum_pnl = 0,
98
+ take_profit,
99
+ increase_position = false,
100
+ minimum_size = 0,
101
+ first_order_size = 0,
102
+ gap = 10,
103
+ max_size = 0
104
+ }) {
105
+ this.minimum_size = minimum_size;
106
+ this.first_order_size = first_order_size;
107
+ this.focus = focus;
108
+ this.budget = budget;
109
+ this.percent_change = percent_change;
110
+ this.price_places = price_places;
111
+ this.decimal_places = decimal_places;
112
+ this.zone_risk = zone_risk;
113
+ this.fee = fee;
114
+ this.support = support;
115
+ this.risk_reward = risk_reward;
116
+ this.resistance = resistance;
117
+ this.risk_per_trade = risk_per_trade;
118
+ this.increase_size = increase_size;
119
+ this.additional_increase = additional_increase;
120
+ this.minimum_pnl = minimum_pnl;
121
+ this.take_profit = take_profit;
122
+ this.increase_position = increase_position;
123
+ this.gap = gap;
124
+ this.max_size = max_size;
125
+ }
126
+ build_entry({
127
+ current_price,
128
+ stop_loss,
129
+ pnl,
130
+ stop_percent,
131
+ kind = "long",
132
+ risk,
133
+ no_of_trades = 1,
134
+ take_profit
135
+ }) {
136
+ let _stop_loss = stop_loss;
137
+ if (!_stop_loss && stop_percent) {
138
+ _stop_loss = kind === "long" ? current_price * Math.pow(1 + stop_percent, -1) : current_price * Math.pow(1 + stop_percent, 1);
139
+ }
140
+ const percent_change = _stop_loss ? Math.max(current_price, _stop_loss) / Math.min(current_price, _stop_loss) - 1 : this.percent_change;
141
+ const _no_of_trades = no_of_trades || this.risk_reward;
142
+ let _resistance = current_price * Math.pow(1 + percent_change, 1);
143
+ const derivedConfig = {
144
+ ...this,
145
+ percent_change,
146
+ focus: current_price,
147
+ resistance: _resistance,
148
+ risk_per_trade: risk / this.risk_reward,
149
+ minimum_pnl: pnl,
150
+ risk_reward: _no_of_trades,
151
+ take_profit: take_profit || this.take_profit,
152
+ support: kind === "long" ? _stop_loss : this.support
153
+ };
154
+ const instance = new Signal(derivedConfig);
155
+ if (kind === "short") {
156
+ }
157
+ let result = instance.get_bulk_trade_zones({ current_price, kind });
158
+ return result;
159
+ return result?.filter((x) => {
160
+ let pp = parseFloat(this.decimal_places.replace("%.", "").replace("f", ""));
161
+ if (pp < 3) {
162
+ return true;
163
+ }
164
+ if (kind === "long") {
165
+ return x.entry > x.stop + 0.5;
166
+ }
167
+ return x.entry + 0.5 < x.stop;
168
+ });
169
+ }
170
+ get risk() {
171
+ return this.budget * this.percent_change;
172
+ }
173
+ get min_trades() {
174
+ return parseInt(this.risk.toString());
175
+ }
176
+ get min_price() {
177
+ const number = this.price_places.replace("%.", "").replace("f", "");
178
+ return 1 * Math.pow(10, -parseInt(number));
179
+ }
180
+ build_opposite_order({
181
+ current_price,
182
+ kind = "long"
183
+ }) {
184
+ let _current_price = current_price;
185
+ if (kind === "long") {
186
+ _current_price = current_price * Math.pow(1 + this.percent_change, -1);
187
+ }
188
+ const result = this.special_build_orders({
189
+ current_price: _current_price,
190
+ kind
191
+ });
192
+ const first_price = result[result.length - 1].entry;
193
+ const stop = result[0].stop;
194
+ const instance = new Signal({ ...this, take_profit: stop });
195
+ const new_kind = kind === "long" ? "short" : "long";
196
+ return instance.build_orders({
197
+ current_price: first_price,
198
+ kind: new_kind
199
+ });
200
+ }
201
+ special_build_orders({
202
+ current_price,
203
+ kind = "long"
204
+ }) {
205
+ let orders = this.build_orders({ current_price, kind });
206
+ if (orders?.length > 1) {
207
+ orders = this.build_orders({ current_price: orders[1].entry, kind });
208
+ }
209
+ if (orders.length > 0) {
210
+ const new_kind = kind === "long" ? "short" : "long";
211
+ let opposite_order = this.build_orders({
212
+ current_price: orders[orders.length - 1].entry,
213
+ kind: new_kind
214
+ });
215
+ this.take_profit = opposite_order[0].stop;
216
+ orders = this.build_orders({
217
+ current_price: orders[orders.length - 1].entry,
218
+ kind
219
+ });
220
+ }
221
+ return orders;
222
+ }
223
+ build_orders({
224
+ current_price,
225
+ kind = "long",
226
+ limit = false,
227
+ replace_focus = false,
228
+ max_index = 0,
229
+ min_index = 2
230
+ }) {
231
+ const focus = this.focus;
232
+ if (replace_focus) {
233
+ this.focus = current_price;
234
+ }
235
+ const new_kind = kind === "long" ? "short" : "long";
236
+ const take_profit = this.take_profit;
237
+ this.take_profit = undefined;
238
+ let result = this.get_bulk_trade_zones({
239
+ current_price,
240
+ kind: new_kind,
241
+ limit
242
+ });
243
+ if (result?.length) {
244
+ let oppositeStop = result[0]["sell_price"];
245
+ let oppositeEntry = result[result.length - 1]["entry"];
246
+ let tradeLength = this.risk_reward + 1;
247
+ let percentChange = Math.abs(1 - Math.max(oppositeEntry, oppositeStop) / Math.min(oppositeEntry, oppositeStop)) / tradeLength;
248
+ let newTrades = [];
249
+ for (let x = 0;x < tradeLength; x++) {
250
+ newTrades.push(oppositeStop * Math.pow(1 + percentChange, x));
251
+ }
252
+ if (kind === "short") {
253
+ newTrades = [];
254
+ for (let x = 0;x < tradeLength; x++) {
255
+ newTrades.push(oppositeStop * Math.pow(1 + percentChange, x * -1));
256
+ }
257
+ }
258
+ this.take_profit = take_profit;
259
+ newTrades = newTrades.map((r) => this.to_f(r));
260
+ if (kind === "long") {
261
+ if (newTrades[1] > current_price) {
262
+ const start = newTrades[0];
263
+ newTrades = [];
264
+ for (let x = 0;x < tradeLength; x++) {
265
+ newTrades.push(start * Math.pow(1 + percentChange, x * -1));
266
+ }
267
+ newTrades.sort();
268
+ }
269
+ }
270
+ const newR = this.process_orders({
271
+ current_price,
272
+ stop_loss: newTrades[0],
273
+ trade_zones: newTrades,
274
+ kind
275
+ });
276
+ return newR;
277
+ }
278
+ this.focus = focus;
279
+ return result;
280
+ }
281
+ build_orders_old({
282
+ current_price,
283
+ kind = "long",
284
+ limit = false,
285
+ replace_focus = false,
286
+ max_index = 0,
287
+ min_index = 2
288
+ }) {
289
+ const focus = this.focus;
290
+ if (replace_focus) {
291
+ this.focus = current_price;
292
+ }
293
+ const result = this.get_bulk_trade_zones({ current_price, kind, limit });
294
+ if (result?.length) {
295
+ let next_focus;
296
+ if (kind == "long") {
297
+ next_focus = current_price * (1 + this.percent_change);
298
+ } else {
299
+ next_focus = current_price * Math.pow(1 + this.percent_change, -1);
300
+ }
301
+ let new_result = this.get_bulk_trade_zones({
302
+ current_price: next_focus,
303
+ kind,
304
+ limit
305
+ });
306
+ if (new_result?.length) {
307
+ for (let i of result) {
308
+ let condition = kind === "long" ? (a, b) => a >= b : (a, b) => a <= b;
309
+ let potentials = new_result.filter((x) => condition(x["entry"], i["risk_sell"])).map((x) => x["entry"]);
310
+ if (potentials.length && max_index) {
311
+ if (kind === "long") {
312
+ console.log("slice: ", potentials.slice(0, max_index));
313
+ i["risk_sell"] = Math.max(...potentials.slice(0, max_index));
314
+ } else {
315
+ i["risk_sell"] = Math.min(...potentials.slice(0, max_index));
316
+ }
317
+ i["pnl"] = this.to_df(determine_pnl(i["entry"], i["risk_sell"], i["quantity"], kind));
318
+ }
319
+ }
320
+ }
321
+ }
322
+ this.focus = focus;
323
+ return result;
324
+ }
325
+ get_bulk_trade_zones({
326
+ current_price,
327
+ kind = "long",
328
+ limit = false
329
+ }) {
330
+ const futures = this.get_future_zones({ current_price, kind });
331
+ const original = this.zone_risk;
332
+ if (futures) {
333
+ const values = futures;
334
+ if (values) {
335
+ let trade_zones = values.sort();
336
+ if (this.resistance) {
337
+ trade_zones = trade_zones.filter((x) => this.resistance ? x <= this.resistance : true);
338
+ if (kind === "short") {
339
+ trade_zones = trade_zones.sort((a, b) => b - a);
340
+ }
341
+ }
342
+ if (trade_zones.length > 0) {
343
+ const stop_loss = trade_zones[0];
344
+ const result = this.process_orders({
345
+ current_price,
346
+ stop_loss,
347
+ trade_zones,
348
+ kind
349
+ });
350
+ if (!result.length) {
351
+ if (kind === "long") {
352
+ let m_z = this.get_margin_range(futures[0]);
353
+ if (m_z && m_z[0] < current_price && current_price !== m_z[1]) {
354
+ return this.get_bulk_trade_zones({
355
+ current_price: m_z[1],
356
+ kind,
357
+ limit
358
+ });
359
+ }
360
+ }
361
+ }
362
+ this.zone_risk = original;
363
+ return result;
364
+ }
365
+ }
366
+ }
367
+ this.zone_risk = original;
368
+ }
369
+ get_future_zones({
370
+ current_price,
371
+ kind = "long",
372
+ raw
373
+ }) {
374
+ if (raw) {
375
+ }
376
+ const margin_range = this.get_margin_range(current_price, kind);
377
+ let margin_zones = this.get_margin_zones({ current_price });
378
+ let remaining_zones = margin_zones.filter((x) => JSON.stringify(x) != JSON.stringify(margin_range));
379
+ if (margin_range) {
380
+ const difference = Math.abs(margin_range[0] - margin_range[1]);
381
+ const spread = to_f(difference / this.risk_reward, this.price_places);
382
+ let entries;
383
+ const percent_change = this.percent_change / this.risk_reward;
384
+ if (kind === "long") {
385
+ entries = Array.from({ length: Math.floor(this.risk_reward) + 1 }, (_, x) => to_f(margin_range[1] - spread * x, this.price_places));
386
+ } else {
387
+ entries = Array.from({ length: Math.floor(this.risk_reward) + 1 }, (_, x) => to_f(margin_range[1] * Math.pow(1 + percent_change, x), this.price_places));
388
+ }
389
+ if (Math.min(...entries) < this.to_f(current_price) && this.to_f(current_price) < Math.max(...entries)) {
390
+ return entries.sort((a, b) => a - b);
391
+ }
392
+ if (remaining_zones.length > 0) {
393
+ let new_range = remaining_zones[0];
394
+ let entries2 = [];
395
+ let x = 0;
396
+ if (new_range) {
397
+ while (entries2.length < this.risk_reward + 1) {
398
+ if (kind === "long") {
399
+ let value = this.to_f(new_range[1] - spread * x);
400
+ if (value <= current_price) {
401
+ entries2.push(value);
402
+ }
403
+ } else {
404
+ let value = this.to_f(new_range[1] * Math.pow(1 + percent_change, x));
405
+ if (value >= current_price) {
406
+ entries2.push(value);
407
+ }
408
+ }
409
+ x += 1;
410
+ }
411
+ }
412
+ return entries2.sort((a, b) => a - b);
413
+ }
414
+ if (remaining_zones.length === 0 && this.to_f(current_price) <= Math.min(...entries)) {
415
+ const next_focus = margin_range[0] * Math.pow(1 + this.percent_change, -1);
416
+ let entries2 = [];
417
+ let x = 0;
418
+ while (entries2.length < this.risk_reward + 1) {
419
+ if (kind === "long") {
420
+ let value = this.to_f(next_focus - spread * x);
421
+ if (value <= this.to_f(current_price)) {
422
+ entries2.push(value);
423
+ }
424
+ } else {
425
+ let value = this.to_f(next_focus * Math.pow(1 + percent_change, x));
426
+ if (value >= this.to_f(current_price)) {
427
+ entries2.push(value);
428
+ }
429
+ }
430
+ x += 1;
431
+ }
432
+ return entries2.sort((a, b) => a - b);
433
+ }
434
+ return entries.sort((a, b) => a - b);
435
+ }
436
+ return [];
437
+ }
438
+ to_f(value, places) {
439
+ return to_f(value, places || this.price_places);
440
+ }
441
+ get_margin_zones({
442
+ current_price,
443
+ kind = "long"
444
+ }) {
445
+ if (this.support && kind === "long") {
446
+ let result = [];
447
+ let start = current_price;
448
+ let counter = 0;
449
+ while (start > this.support) {
450
+ let v = this.get_margin_range(start);
451
+ if (v) {
452
+ result.push(v);
453
+ start = v[0] - this.min_price;
454
+ counter += 1;
455
+ }
456
+ if (counter > 10) {
457
+ break;
458
+ }
459
+ }
460
+ return result;
461
+ }
462
+ if (this.resistance) {
463
+ let result = [];
464
+ let start = current_price;
465
+ let counter = 0;
466
+ while (start < this.resistance) {
467
+ let v = this.get_margin_range(start);
468
+ if (v) {
469
+ result.push(v);
470
+ start = v[1] + this.min_price;
471
+ }
472
+ if (counter > 10) {
473
+ break;
474
+ }
475
+ }
476
+ return result;
477
+ }
478
+ return [this.get_margin_range(current_price)];
479
+ }
480
+ get_margin_range(current_price, kind = "long") {
481
+ const diff = -this.min_price;
482
+ const zones = _get_zones({
483
+ current_price: current_price + diff,
484
+ focus: this.focus,
485
+ percent_change: this.percent_change,
486
+ places: this.price_places
487
+ }) || [];
488
+ const top_zones = [];
489
+ for (const i of zones) {
490
+ if (i < 0.00000001) {
491
+ break;
492
+ }
493
+ top_zones.push(this.to_f(i));
494
+ }
495
+ if (top_zones.length > 0) {
496
+ const result = top_zones[top_zones.length - 1];
497
+ return [this.to_f(result), this.to_f(result * (1 + this.percent_change))];
498
+ }
499
+ return null;
500
+ }
501
+ process_orders({
502
+ current_price,
503
+ stop_loss,
504
+ trade_zones,
505
+ kind = "long"
506
+ }) {
507
+ const number_of_orders = trade_zones.slice(1).length;
508
+ let take_profit = stop_loss * (1 + 2 * this.percent_change);
509
+ if (kind === "short") {
510
+ take_profit = stop_loss * Math.pow(1 + 2 * this.percent_change, -1);
511
+ }
512
+ if (this.take_profit) {
513
+ take_profit = this.take_profit;
514
+ }
515
+ if (number_of_orders > 0) {
516
+ const risk_per_trade = this.get_risk_per_trade(number_of_orders);
517
+ let limit_orders = trade_zones.slice(1).filter((x) => x <= this.to_f(current_price));
518
+ let market_orders = trade_zones.slice(1).filter((x) => x > this.to_f(current_price));
519
+ if (kind === "short") {
520
+ limit_orders = trade_zones.slice(1).filter((x) => x >= this.to_f(current_price));
521
+ market_orders = trade_zones.slice(1).filter((x) => x < this.to_f(current_price));
522
+ }
523
+ if (market_orders.length === 1) {
524
+ limit_orders = limit_orders.concat(market_orders);
525
+ market_orders = [];
526
+ }
527
+ const increase_position = Boolean(this.support) && this.increase_position;
528
+ const market_trades = limit_orders.length > 0 ? market_orders.map((x, i) => {
529
+ const defaultStopLoss = i === 0 ? limit_orders[limit_orders.length - 1] : market_orders[i - 1];
530
+ const y = this.build_trade_dict({
531
+ entry: x,
532
+ stop: increase_position ? this.support : defaultStopLoss,
533
+ risk: risk_per_trade,
534
+ arr: market_orders,
535
+ index: i,
536
+ kind,
537
+ start: market_orders.length + limit_orders.length,
538
+ take_profit
539
+ });
540
+ return y;
541
+ }).filter((y) => y) : [];
542
+ let total_incurred_market_fees = 0;
543
+ if (market_trades.length > 0) {
544
+ let first = market_trades[0];
545
+ if (first) {
546
+ total_incurred_market_fees += first.incurred;
547
+ total_incurred_market_fees += first.fee;
548
+ }
549
+ }
550
+ const default_gap = this.gap;
551
+ const gap_pairs = createGapPairs(limit_orders, default_gap);
552
+ const limit_trades = (limit_orders.map((x, i) => {
553
+ let _base = limit_orders[i - 1];
554
+ let _stops = gap_pairs.find((o) => o[0] === x);
555
+ if (!_stops) {
556
+ return;
557
+ }
558
+ if (_stops) {
559
+ _base = _stops[1];
560
+ }
561
+ const defaultStopLoss = i === 0 ? stop_loss : _base;
562
+ const new_stop = kind === "long" ? this.support : stop_loss;
563
+ const y = this.build_trade_dict({
564
+ entry: x,
565
+ stop: (this.increase_position ? new_stop : defaultStopLoss) || defaultStopLoss,
566
+ risk: risk_per_trade,
567
+ arr: limit_orders,
568
+ index: i,
569
+ new_fees: total_incurred_market_fees,
570
+ kind,
571
+ start: market_orders.length + limit_orders.length,
572
+ take_profit
573
+ });
574
+ if (y) {
575
+ y.new_stop = defaultStopLoss;
576
+ }
577
+ return y !== null ? y : undefined;
578
+ }) || []).filter((y) => y !== undefined).filter((y) => {
579
+ const min_options = [0.001, 0.002, 0.003];
580
+ if (min_options.includes(this.minimum_size)) {
581
+ return y.quantity <= 0.03;
582
+ }
583
+ return true;
584
+ });
585
+ let total_orders = limit_trades.concat(market_trades);
586
+ if (kind === "short") {
587
+ }
588
+ if (this.minimum_size && total_orders.length > 0) {
589
+ let payload = total_orders;
590
+ let greater_than_min_size = total_orders.filter((o) => o ? o.quantity >= this.minimum_size : true);
591
+ let less_than_min_size = total_orders.filter((o) => o ? o.quantity < this.minimum_size : true) || total_orders;
592
+ less_than_min_size = groupIntoPairsWithSumLessThan(less_than_min_size, this.minimum_size, "quantity", this.first_order_size);
593
+ less_than_min_size = less_than_min_size.map((q, i) => {
594
+ let avg_entry = determine_average_entry_and_size(q.map((o) => ({
595
+ price: o.entry,
596
+ quantity: o.quantity
597
+ })), this.decimal_places, this.price_places);
598
+ let candidate = q[0];
599
+ candidate.entry = avg_entry.price;
600
+ candidate.quantity = avg_entry.quantity;
601
+ return candidate;
602
+ });
603
+ less_than_min_size = less_than_min_size.map((q, i) => {
604
+ let new_stop = q.new_stop;
605
+ if (i > 0) {
606
+ new_stop = less_than_min_size[i - 1].entry;
607
+ }
608
+ return {
609
+ ...q,
610
+ new_stop
611
+ };
612
+ });
613
+ if (greater_than_min_size.length !== less_than_min_size.length) {
614
+ payload = greater_than_min_size.concat(less_than_min_size);
615
+ }
616
+ return payload;
617
+ }
618
+ return total_orders;
619
+ }
620
+ return [];
621
+ }
622
+ get_risk_per_trade(number_of_orders) {
623
+ if (this.risk_per_trade) {
624
+ return this.risk_per_trade;
625
+ }
626
+ return this.zone_risk / number_of_orders;
627
+ }
628
+ build_trade_dict({
629
+ entry,
630
+ stop,
631
+ risk,
632
+ arr,
633
+ index,
634
+ new_fees = 0,
635
+ kind = "long",
636
+ start = 0,
637
+ take_profit
638
+ }) {
639
+ const considered = arr.map((x, i) => i).filter((i) => i > index);
640
+ const with_quantity = considered.map((x) => {
641
+ const q = determine_position_size({
642
+ entry: arr[x],
643
+ stop: arr[x - 1],
644
+ budget: risk,
645
+ places: this.decimal_places
646
+ });
647
+ if (!q) {
648
+ return;
649
+ }
650
+ if (this.minimum_size) {
651
+ if (q < this.minimum_size) {
652
+ return;
653
+ }
654
+ }
655
+ return { quantity: q, entry: arr[x] };
656
+ }).filter((x) => x);
657
+ if (this.increase_size) {
658
+ const arr_length = with_quantity.length;
659
+ with_quantity.forEach((x, i) => {
660
+ if (x) {
661
+ x.quantity = x.quantity * (arr_length - i);
662
+ }
663
+ });
664
+ }
665
+ const fees = with_quantity.map((x) => {
666
+ return this.to_df(this.fee * x.quantity * x.entry);
667
+ });
668
+ const previous_risks = with_quantity.map((x) => {
669
+ return this.to_df(risk);
670
+ });
671
+ const multiplier = start - index;
672
+ const incurred_fees = fees.reduce((a, b) => a + b, 0) + previous_risks.reduce((a, b) => a + b, 0);
673
+ if (index === 0) {
674
+ }
675
+ let quantity = determine_position_size({
676
+ entry,
677
+ stop,
678
+ budget: risk,
679
+ places: this.decimal_places,
680
+ min_size: this.minimum_size
681
+ });
682
+ if (!quantity) {
683
+ return;
684
+ }
685
+ if (this.increase_size) {
686
+ quantity = quantity * multiplier;
687
+ const new_risk = determine_pnl(entry, stop, quantity, kind);
688
+ risk = Math.abs(new_risk);
689
+ }
690
+ const fee = this.to_df(this.fee * quantity * entry);
691
+ const increment = Math.abs(arr.length - (index + 1));
692
+ let pnl = this.to_df(risk) * (this.risk_reward + increment);
693
+ if (this.minimum_pnl) {
694
+ pnl = this.minimum_pnl + fee;
695
+ }
696
+ let sell_price = determine_close_price({ entry, pnl, quantity, kind });
697
+ if (take_profit && !this.minimum_pnl) {
698
+ sell_price = take_profit;
699
+ pnl = this.to_df(determine_pnl(entry, sell_price, quantity, kind));
700
+ pnl = pnl + fee;
701
+ sell_price = determine_close_price({ entry, pnl, quantity, kind });
702
+ }
703
+ let risk_sell = sell_price;
704
+ return {
705
+ entry,
706
+ risk: this.to_df(risk),
707
+ quantity,
708
+ sell_price: this.to_f(sell_price),
709
+ risk_sell: this.to_f(risk_sell),
710
+ stop,
711
+ pnl,
712
+ fee,
713
+ net: this.to_df(pnl - fee),
714
+ incurred: this.to_df(incurred_fees + new_fees),
715
+ stop_percent: this.to_df(Math.abs(entry - stop) / entry)
716
+ };
717
+ }
718
+ to_df(currentPrice, places = "%.3f") {
719
+ return to_f(currentPrice, places);
720
+ }
721
+ }
722
+
723
+ // src/helpers/pnl.ts
724
+ function determine_position_size2(entry, stop, budget) {
725
+ let stop_percent = Math.abs(entry - stop) / entry;
726
+ let size = budget / stop_percent / entry;
727
+ return size;
728
+ }
729
+ function determine_risk(entry, stop, quantity) {
730
+ let stop_percent = Math.abs(entry - stop) / entry;
731
+ let risk = quantity * stop_percent * entry;
732
+ return risk;
733
+ }
734
+ function determine_close_price2(entry, pnl, quantity, kind, single = false, leverage = 1) {
735
+ const dollar_value = entry / leverage;
736
+ const position = dollar_value * quantity;
737
+ if (position) {
738
+ let percent = pnl / position;
739
+ let difference = position * percent / quantity;
740
+ let result;
741
+ if (kind === "long") {
742
+ result = difference + entry;
743
+ } else {
744
+ result = entry - difference;
745
+ }
746
+ if (single) {
747
+ return result;
748
+ }
749
+ return result;
750
+ }
751
+ return 0;
752
+ }
753
+ function determine_amount_to_sell(entry, quantity, sell_price, pnl, kind, places = "%.3f") {
754
+ const _pnl = determine_pnl2(entry, sell_price, quantity, kind);
755
+ const ratio = pnl / to_f2(Math.abs(_pnl), places);
756
+ quantity = quantity * ratio;
757
+ return to_f2(quantity, places);
758
+ }
759
+ function determine_pnl2(entry, close_price, quantity, kind, contract_size, places = "%.2f") {
760
+ if (contract_size) {
761
+ const direction = kind === "long" ? 1 : -1;
762
+ return quantity * contract_size * direction * (1 / entry - 1 / close_price);
763
+ }
764
+ let difference = entry - close_price;
765
+ if (kind === "long") {
766
+ difference = close_price - entry;
767
+ }
768
+ return to_f2(difference * quantity, places);
769
+ }
770
+ function position(entry, quantity, kind, leverage = 1) {
771
+ const direction = { long: 1, short: -1 };
772
+ return parseFloat((direction[kind] * quantity * (entry / leverage)).toFixed(3));
773
+ }
774
+ function to_f2(value, places) {
775
+ if (value) {
776
+ let pp = parseInt(places.replace("%.", "").replace("f", ""));
777
+ return parseFloat(value.toFixed(pp));
778
+ }
779
+ return value;
780
+ }
781
+ var value = {
782
+ determine_risk,
783
+ determine_position_size: determine_position_size2,
784
+ determine_close_price: determine_close_price2,
785
+ determine_pnl: determine_pnl2,
786
+ position,
787
+ determine_amount_to_sell,
788
+ to_f: to_f2
789
+ };
790
+ var pnl_default = value;
791
+
792
+ // src/helpers/trade_utils.ts
793
+ function profitHelper(longPosition, shortPosition, config, contract_size, balance = 0) {
794
+ let long = { takeProfit: 0, quantity: 0, pnl: 0 };
795
+ let short = { takeProfit: 0, quantity: 0, pnl: 0 };
796
+ if (longPosition) {
797
+ long = config?.getSize2(longPosition, contract_size, balance) || null;
798
+ }
799
+ if (shortPosition) {
800
+ short = config?.getSize2(shortPosition, contract_size, balance) || null;
801
+ }
802
+ return { long, short };
803
+ }
804
+ function getParamForField(self, configs, field, isGroup) {
805
+ if (isGroup === "group" && field === "checkbox") {
806
+ return configs.filter((o) => o.kind === field && o.group === true).map((o) => {
807
+ let _self = self;
808
+ let value2 = _self[o.name];
809
+ return { ...o, value: value2 };
810
+ });
811
+ }
812
+ let r = configs.find((o) => o.name == field);
813
+ if (r) {
814
+ let oo = self;
815
+ let tt = oo[r.name] || "";
816
+ r.value = tt;
817
+ }
818
+ return r;
819
+ }
820
+ function getTradeEntries(entry, min_size, kind, size, spread = 0) {
821
+ let result = [];
822
+ let index = 0;
823
+ let no_of_trades = size > min_size ? Math.round(size / min_size) : 1;
824
+ while (index < no_of_trades) {
825
+ if (kind === "long") {
826
+ result.push({ entry: entry - index * spread, size: min_size });
827
+ } else {
828
+ result.push({ entry: entry + index * spread, size: min_size });
829
+ }
830
+ index = index + 1;
831
+ }
832
+ return result;
833
+ }
834
+ function extractValue(_param, condition) {
835
+ let param;
836
+ if (condition) {
837
+ try {
838
+ let value2 = JSON.parse(_param || "[]");
839
+ param = value2.map((o) => parseFloat(o));
840
+ } catch (error) {
841
+ }
842
+ } else {
843
+ param = parseFloat(_param);
844
+ }
845
+ return param;
846
+ }
847
+ function asCoins(symbol) {
848
+ let _type = symbol.toLowerCase().includes("usdt") ? "usdt" : "coin";
849
+ if (symbol.toLowerCase() == "btcusdt") {
850
+ _type = "usdt";
851
+ }
852
+ let result = _type === "usdt" ? symbol.toLowerCase().includes("usdt") ? "USDT" : "BUSD" : symbol.toUpperCase().split("USD_")[0];
853
+ if (symbol.toLowerCase().includes("-")) {
854
+ result = result.split("-")[0];
855
+ }
856
+ if (symbol.toLowerCase() == "usdt-usd") {
857
+ }
858
+ let result2 = _type == "usdt" ? symbol.split(result)[0] : result;
859
+ if (result.includes("-")) {
860
+ result2 = result;
861
+ }
862
+ return result2;
863
+ }
864
+ var SpecialCoins = ["NGN", "USDT", "BUSD", "PAX", "USDC", "EUR"];
865
+ function allCoins(symbols) {
866
+ let r = symbols.map((o, i) => asCoins(o));
867
+ return [...new Set(r), ...SpecialCoins];
868
+ }
869
+ function formatPrice(value2, opts = {}) {
870
+ const { locale = "en-US", currency = "USD" } = opts;
871
+ const formatter = new Intl.NumberFormat(locale, {
872
+ currency,
873
+ style: "currency",
874
+ maximumFractionDigits: 2
875
+ });
876
+ return formatter.format(value2);
877
+ }
878
+ function to_f(value2, places = "%.1f") {
879
+ let v = typeof value2 === "string" ? parseFloat(value2) : value2;
880
+ const formattedValue = places.replace("%.", "").replace("f", "");
881
+ return parseFloat(v.toFixed(parseInt(formattedValue)));
882
+ }
883
+ function determine_stop_and_size(entry, pnl, take_profit, kind = "long") {
884
+ const difference = kind === "long" ? take_profit - entry : entry - take_profit;
885
+ const quantity = pnl / difference;
886
+ return Math.abs(quantity);
887
+ }
888
+ var range = (start, stop, step = 1) => Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + i * step);
889
+ function determine_amount_to_sell2(entry, quantity, sell_price, pnl, kind, places = "%.3f") {
890
+ const _pnl = determine_pnl(entry, sell_price, quantity, kind);
891
+ const ratio = pnl / to_f(Math.abs(_pnl), places);
892
+ quantity = quantity * ratio;
893
+ return to_f(quantity, places);
894
+ }
895
+ function determine_position_size({
896
+ entry,
897
+ stop,
898
+ budget,
899
+ percent,
900
+ min_size,
901
+ notional_value,
902
+ as_coin = true,
903
+ places = "%.3f"
904
+ }) {
905
+ let stop_percent = stop ? Math.abs(entry - stop) / entry : percent;
906
+ if (stop_percent && budget) {
907
+ let size = budget / stop_percent;
908
+ let notion_value = size * entry;
909
+ if (notional_value && notional_value > notion_value) {
910
+ size = notional_value / entry;
911
+ }
912
+ if (as_coin) {
913
+ size = size / entry;
914
+ if (min_size && min_size === 1) {
915
+ return to_f(Math.round(size), places);
916
+ }
917
+ }
918
+ return to_f(size, places);
919
+ }
920
+ return;
921
+ }
922
+ function determine_remaining_entry({
923
+ risk,
924
+ max_size,
925
+ stop_loss,
926
+ kind,
927
+ position: position2
928
+ }) {
929
+ const avg_entry = determine_avg_entry_based_on_max_size({
930
+ risk,
931
+ max_size,
932
+ stop_loss,
933
+ kind
934
+ });
935
+ const result = avg_entry * max_size - position2.quantity * position2.entry;
936
+ return result / (max_size - position2.quantity);
937
+ }
938
+ function determine_avg_entry_based_on_max_size({
939
+ risk,
940
+ max_size,
941
+ stop_loss,
942
+ kind = "long"
943
+ }) {
944
+ const diff = max_size * stop_loss;
945
+ if (kind === "long") {
946
+ return (risk + diff) / max_size;
947
+ }
948
+ return (risk - diff) / max_size;
949
+ }
950
+ function determine_average_entry_and_size(orders, places = "%.3f", price_places = "%.1f") {
951
+ const sum_values = orders.reduce((sum, order) => sum + order.price * order.quantity, 0);
952
+ const total_quantity = orders.reduce((sum, order) => sum + order.quantity, 0);
953
+ const avg_price = total_quantity ? to_f(sum_values / total_quantity, price_places) : 0;
954
+ return {
955
+ entry: avg_price,
956
+ price: avg_price,
957
+ quantity: to_f(total_quantity, places)
958
+ };
959
+ }
960
+ var createArray = (start, stop, step) => {
961
+ const result = [];
962
+ let current = start;
963
+ while (current <= stop) {
964
+ result.push(current);
965
+ current += step;
966
+ }
967
+ return result;
968
+ };
969
+ var groupBy = (xs, key) => {
970
+ return xs.reduce((rv, x) => {
971
+ (rv[x[key]] = rv[x[key]] || []).push(x);
972
+ return rv;
973
+ }, {});
974
+ };
975
+ function fibonacci_analysis({
976
+ support,
977
+ resistance,
978
+ kind = "long",
979
+ trend = "long",
980
+ places = "%.1f"
981
+ }) {
982
+ const swing_high = trend === "long" ? resistance : support;
983
+ const swing_low = trend === "long" ? support : resistance;
984
+ const ranges = [0, 0.236, 0.382, 0.5, 0.618, 0.789, 1, 1.272, 1.414, 1.618];
985
+ const fib_calc = (p, h, l) => p * (h - l) + l;
986
+ const fib_values = ranges.map((x) => fib_calc(x, swing_high, swing_low)).map((x) => to_f(x, places));
987
+ if (kind === "short") {
988
+ return trend === "long" ? fib_values.reverse() : fib_values;
989
+ } else {
990
+ return trend === "short" ? fib_values.reverse() : fib_values;
991
+ }
992
+ return fib_values;
993
+ }
994
+ var groupIntoPairs = (arr, size) => {
995
+ const result = [];
996
+ for (let i = 0;i < arr.length; i += size) {
997
+ result.push(arr.slice(i, i + size));
998
+ }
999
+ return result;
1000
+ };
1001
+ var groupIntoPairsWithSumLessThan = (arr, targetSum, key = "quantity", firstSize = 0) => {
1002
+ if (firstSize) {
1003
+ const totalSize = arr.reduce((sum, order) => sum + order[key], 0);
1004
+ const remainingSize = totalSize - firstSize;
1005
+ let newSum = 0;
1006
+ let newArray = [];
1007
+ let lastIndex = 0;
1008
+ for (let i = 0;i < arr.length; i++) {
1009
+ if (newSum < remainingSize) {
1010
+ newSum += arr[i][key];
1011
+ newArray.push(arr[i]);
1012
+ lastIndex = i;
1013
+ }
1014
+ }
1015
+ const lastGroup = arr.slice(lastIndex + 1);
1016
+ const previousPair = groupInPairs(newArray, key, targetSum);
1017
+ if (lastGroup.length > 0) {
1018
+ previousPair.push(lastGroup);
1019
+ }
1020
+ return previousPair;
1021
+ }
1022
+ return groupInPairs(arr, key, targetSum);
1023
+ };
1024
+ function groupInPairs(_arr, key, targetSum) {
1025
+ const result = [];
1026
+ let currentSum = 0;
1027
+ let currentGroup = [];
1028
+ for (let i = 0;i < _arr.length; i++) {
1029
+ currentSum += _arr[i][key];
1030
+ currentGroup.push(_arr[i]);
1031
+ if (currentSum >= targetSum) {
1032
+ result.push(currentGroup);
1033
+ currentGroup = [];
1034
+ currentSum = 0;
1035
+ }
1036
+ }
1037
+ return result;
1038
+ }
1039
+ var computeTotalAverageForEachTrade = (trades, config) => {
1040
+ let _take_profit = config.take_profit;
1041
+ let kind = config.kind;
1042
+ let entryToUse = kind === "short" ? Math.min(config.entry, config.stop) : Math.max(config.entry, config.stop);
1043
+ let _currentEntry = config.currentEntry || entryToUse;
1044
+ let less = trades.filter((p) => kind === "long" ? p.entry <= _currentEntry : p.entry >= _currentEntry);
1045
+ let rrr = trades.map((r, i) => {
1046
+ let considered = [];
1047
+ if (kind === "long") {
1048
+ considered = trades.filter((p) => p.entry > _currentEntry);
1049
+ } else {
1050
+ considered = trades.filter((p) => p.entry < _currentEntry);
1051
+ }
1052
+ const x_pnl = 0;
1053
+ const remaining = less.filter((o) => {
1054
+ if (kind === "long") {
1055
+ return o.entry >= r.entry;
1056
+ }
1057
+ return o.entry <= r.entry;
1058
+ });
1059
+ if (remaining.length === 0) {
1060
+ return { ...r, pnl: x_pnl };
1061
+ }
1062
+ const start = kind === "long" ? Math.max(...remaining.map((o) => o.entry)) : Math.min(...remaining.map((o) => o.entry));
1063
+ considered = considered.map((o) => ({ ...o, entry: start }));
1064
+ considered = considered.concat(remaining);
1065
+ let avg_entry = determine_average_entry_and_size([
1066
+ ...considered.map((o) => ({
1067
+ price: o.entry,
1068
+ quantity: o.quantity
1069
+ })),
1070
+ {
1071
+ price: _currentEntry,
1072
+ quantity: config.currentQty || 0
1073
+ }
1074
+ ], config.decimal_places, config.price_places);
1075
+ let _pnl = r.pnl;
1076
+ let sell_price = r.sell_price;
1077
+ let entry_pnl = r.pnl;
1078
+ if (_take_profit) {
1079
+ _pnl = pnl_default.determine_pnl(avg_entry.price, _take_profit, avg_entry.quantity, kind);
1080
+ sell_price = _take_profit;
1081
+ entry_pnl = pnl_default.determine_pnl(r.entry, _take_profit, avg_entry.quantity, kind);
1082
+ }
1083
+ const loss = pnl_default.determine_pnl(avg_entry.price, r.stop, avg_entry.quantity, kind);
1084
+ let new_stop = r.new_stop;
1085
+ const entry_loss = pnl_default.determine_pnl(r.entry, new_stop, avg_entry.quantity, kind);
1086
+ let min_profit = 0;
1087
+ let min_entry_profit = 0;
1088
+ if (config.min_profit) {
1089
+ min_profit = pnl_default.determine_close_price(avg_entry.price, config.min_profit, avg_entry.quantity, kind);
1090
+ min_entry_profit = pnl_default.determine_close_price(r.entry, config.min_profit, avg_entry.quantity, kind);
1091
+ }
1092
+ let x_fee = r.fee;
1093
+ if (config.fee) {
1094
+ x_fee = config.fee * r.stop * avg_entry.quantity;
1095
+ }
1096
+ let tp_close = pnl_default.determine_close_price(r.entry, Math.abs(entry_loss) * (config.rr || 1) + x_fee, avg_entry.quantity, kind);
1097
+ return {
1098
+ ...r,
1099
+ x_fee: to_f(x_fee, "%.2f"),
1100
+ avg_entry: avg_entry.price,
1101
+ avg_size: avg_entry.quantity,
1102
+ entry_pnl: to_f(entry_pnl, "%.2f"),
1103
+ entry_loss: to_f(entry_loss, "%.2f"),
1104
+ min_entry_pnl: to_f(min_entry_profit, "%.2f"),
1105
+ pnl: _pnl,
1106
+ neg_pnl: to_f(loss, "%.2f"),
1107
+ sell_price,
1108
+ close_p: to_f(tp_close, "%.2f"),
1109
+ min_pnl: to_f(min_profit, "%.2f"),
1110
+ new_stop
1111
+ };
1112
+ });
1113
+ return rrr;
1114
+ };
1115
+ function getDecimalPlaces(numberString) {
1116
+ let parts = numberString.toString().split(".");
1117
+ if (parts.length == 2) {
1118
+ return parts[1].length;
1119
+ }
1120
+ return 0;
1121
+ }
1122
+ function createGapPairs(arr, gap, item) {
1123
+ if (arr.length === 0) {
1124
+ return [];
1125
+ }
1126
+ const result = [];
1127
+ const firstElement = arr[0];
1128
+ for (let i = arr.length - 1;i >= 0; i--) {
1129
+ const current = arr[i];
1130
+ const gapIndex = i - gap;
1131
+ const pairedElement = gapIndex < 0 ? firstElement : arr[gapIndex];
1132
+ if (current !== pairedElement) {
1133
+ result.push([current, pairedElement]);
1134
+ }
1135
+ }
1136
+ if (item) {
1137
+ let r = result.find((o) => o[0] === item);
1138
+ return r ? [r] : [];
1139
+ }
1140
+ return result;
1141
+ }
1142
+ function logWithLineNumber(...args) {
1143
+ const stack = new Error().stack;
1144
+ const lines = stack?.split(`
1145
+ `).slice(2).map((line) => line.trim());
1146
+ const lineNumber = lines?.[0]?.split(":").pop();
1147
+ console.log(`${lineNumber}:`, ...args);
1148
+ }
1149
+ function computeSellZones(payload) {
1150
+ const { entry, exit, zones = 10 } = payload;
1151
+ const gap = exit / entry;
1152
+ const factor = Math.pow(gap, 1 / zones);
1153
+ const spread = factor - 1;
1154
+ return Array.from({ length: zones }, (_, i) => entry * Math.pow(1 + spread, i));
1155
+ }
1156
+ // src/helpers/shared.ts
1157
+ function buildConfig(app_config, {
1158
+ take_profit,
1159
+ entry,
1160
+ stop,
1161
+ raw_instance,
1162
+ risk,
1163
+ no_of_trades,
1164
+ min_profit = 0,
1165
+ risk_reward,
1166
+ kind,
1167
+ increase,
1168
+ gap,
1169
+ rr = 1,
1170
+ price_places = "%.1f",
1171
+ decimal_places = "%.3f"
1172
+ }) {
1173
+ let fee = app_config.fee / 100;
1174
+ let working_risk = risk || app_config.risk_per_trade;
1175
+ let trade_no = no_of_trades || app_config.risk_reward;
1176
+ const config = {
1177
+ focus: app_config.focus,
1178
+ fee,
1179
+ budget: app_config.budget,
1180
+ risk_reward: risk_reward || trade_no,
1181
+ support: app_config.support,
1182
+ resistance: app_config.resistance,
1183
+ price_places: app_config.price_places || price_places,
1184
+ decimal_places,
1185
+ percent_change: app_config.percent_change / app_config.tradeSplit,
1186
+ risk_per_trade: working_risk,
1187
+ take_profit: take_profit || app_config.take_profit,
1188
+ increase_position: increase,
1189
+ minimum_size: app_config.min_size,
1190
+ entry,
1191
+ stop,
1192
+ kind: app_config.kind,
1193
+ gap,
1194
+ min_profit: min_profit || app_config.min_profit,
1195
+ rr: rr || 1
1196
+ };
1197
+ const instance = new Signal(config);
1198
+ if (raw_instance) {
1199
+ return instance;
1200
+ }
1201
+ if (!stop) {
1202
+ return [];
1203
+ }
1204
+ const condition = (kind === "long" ? entry > app_config.support : entry >= app_config.support) && stop >= app_config.support * 0.999;
1205
+ if (kind === "short") {
1206
+ console.log("condition", condition, entry === stop);
1207
+ }
1208
+ console.log({ entry, support: app_config.support, stop });
1209
+ const result = entry === stop ? [] : condition ? instance.build_entry({
1210
+ current_price: entry,
1211
+ stop_loss: stop,
1212
+ risk: working_risk,
1213
+ kind: kind || app_config.kind,
1214
+ no_of_trades: trade_no
1215
+ }) || [] : [];
1216
+ return computeTotalAverageForEachTrade(result, config);
1217
+ }
1218
+ function buildAvg({
1219
+ _trades,
1220
+ kind
1221
+ }) {
1222
+ let avg = determine_average_entry_and_size(_trades?.map((r) => ({
1223
+ price: r.entry,
1224
+ quantity: r.quantity
1225
+ })) || []);
1226
+ const stop_prices = _trades.map((o) => o.stop);
1227
+ const stop_loss = kind === "long" ? Math.min(...stop_prices) : Math.max(...stop_prices);
1228
+ avg.pnl = pnl_default.determine_pnl(avg.price, stop_loss, avg.quantity, kind);
1229
+ return avg;
1230
+ }
1231
+ function sortedBuildConfig(app_config, options) {
1232
+ const sorted = buildConfig(app_config, options).sort((a, b) => app_config.kind === "long" ? a.entry - b.entry : b.entry - b.entry).filter((x) => {
1233
+ if (app_config.symbol === "BTCUSDT") {
1234
+ return x.quantity <= 0.03;
1235
+ }
1236
+ return true;
1237
+ });
1238
+ return sorted.map((k, i) => {
1239
+ const arrSet = sorted.slice(0, i + 1);
1240
+ const avg_values = determine_average_entry_and_size(arrSet.map((u) => ({ price: u.entry, quantity: u.quantity })), app_config.decimal_places, app_config.price_places);
1241
+ return {
1242
+ ...k,
1243
+ reverse_avg_entry: avg_values.price,
1244
+ reverse_avg_quantity: avg_values.quantity
1245
+ };
1246
+ });
1247
+ }
1248
+ function get_app_config_and_max_size(config, payload) {
1249
+ const app_config = {
1250
+ kind: payload.kind,
1251
+ entry: payload.entry,
1252
+ stop: payload.stop,
1253
+ risk_per_trade: config.risk,
1254
+ risk_reward: config.risk_reward || 199,
1255
+ support: to_f(config.support, config.price_places),
1256
+ resistance: to_f(config.resistance, config.price_places),
1257
+ focus: payload.entry,
1258
+ fee: 0,
1259
+ percent_change: config.stop_percent / 100,
1260
+ tradeSplit: 1,
1261
+ gap: 1,
1262
+ min_size: config.min_size,
1263
+ budget: 0,
1264
+ price_places: config.price_places,
1265
+ decimal_places: config.decimal_places,
1266
+ min_profit: config.profit_percent * config.profit / 100
1267
+ };
1268
+ const initialResult = sortedBuildConfig(app_config, {
1269
+ entry: app_config.entry,
1270
+ stop: app_config.stop,
1271
+ kind: app_config.kind,
1272
+ risk: app_config.risk_per_trade,
1273
+ risk_reward: app_config.risk_reward,
1274
+ increase: true,
1275
+ gap: app_config.gap,
1276
+ price_places: app_config.price_places,
1277
+ decimal_places: app_config.decimal_places
1278
+ });
1279
+ const max_size = initialResult[0]?.avg_size;
1280
+ const last_value = initialResult[0];
1281
+ const entries = initialResult.map((x) => ({
1282
+ entry: x.entry,
1283
+ avg_entry: x.avg_entry,
1284
+ avg_size: x.avg_size,
1285
+ neg_pnl: x.neg_pnl,
1286
+ quantity: x.quantity
1287
+ }));
1288
+ return {
1289
+ app_config,
1290
+ max_size,
1291
+ last_value,
1292
+ entries
1293
+ };
1294
+ }
1295
+ function buildAppConfig(config, payload) {
1296
+ const { app_config, max_size, last_value, entries } = get_app_config_and_max_size({
1297
+ ...config,
1298
+ risk: payload.risk,
1299
+ profit: payload.profit || 500,
1300
+ risk_reward: payload.risk_reward,
1301
+ accounts: [],
1302
+ reduce_percent: 90,
1303
+ reverse_factor: 1,
1304
+ profit_percent: 0,
1305
+ kind: payload.entry > payload.stop ? "long" : "short",
1306
+ symbol: payload.symbol
1307
+ }, {
1308
+ entry: payload.entry,
1309
+ stop: payload.stop,
1310
+ kind: payload.entry > payload.stop ? "long" : "short"
1311
+ });
1312
+ app_config.max_size = max_size;
1313
+ app_config.entry = payload.entry || app_config.entry;
1314
+ app_config.stop = payload.stop || app_config.stop;
1315
+ app_config.last_value = last_value;
1316
+ app_config.entries = entries;
1317
+ return app_config;
1318
+ }
1319
+ function getOptimumStopAndRisk(app_config, params) {
1320
+ const { max_size, target_stop } = params;
1321
+ const isLong = app_config.kind === "long";
1322
+ const stopRange = Math.abs(app_config.entry - target_stop) * 0.5;
1323
+ let low_stop = isLong ? target_stop - stopRange : Math.max(target_stop - stopRange, app_config.entry);
1324
+ let high_stop = isLong ? Math.min(target_stop + stopRange, app_config.entry) : target_stop + stopRange;
1325
+ let optimal_stop = target_stop;
1326
+ let best_stop_result = null;
1327
+ let best_entry_diff = Infinity;
1328
+ console.log(`Finding optimal stop for ${isLong ? "LONG" : "SHORT"} position. Target: ${target_stop}, Search range: ${low_stop} to ${high_stop}`);
1329
+ let iterations = 0;
1330
+ const MAX_ITERATIONS = 50;
1331
+ while (Math.abs(high_stop - low_stop) > 0.1 && iterations < MAX_ITERATIONS) {
1332
+ iterations++;
1333
+ const mid_stop = (low_stop + high_stop) / 2;
1334
+ const result = sortedBuildConfig(app_config, {
1335
+ entry: app_config.entry,
1336
+ stop: mid_stop,
1337
+ kind: app_config.kind,
1338
+ risk: app_config.risk_per_trade,
1339
+ risk_reward: app_config.risk_reward,
1340
+ increase: true,
1341
+ gap: app_config.gap,
1342
+ price_places: app_config.price_places,
1343
+ decimal_places: app_config.decimal_places
1344
+ });
1345
+ if (result.length === 0) {
1346
+ if (isLong) {
1347
+ low_stop = mid_stop;
1348
+ } else {
1349
+ high_stop = mid_stop;
1350
+ }
1351
+ continue;
1352
+ }
1353
+ const first_entry = result[0].entry;
1354
+ const entry_diff = Math.abs(first_entry - target_stop);
1355
+ console.log(`Stop: ${mid_stop.toFixed(2)}, First Entry: ${first_entry.toFixed(2)}, Diff: ${entry_diff.toFixed(2)}`);
1356
+ if (entry_diff < best_entry_diff) {
1357
+ best_entry_diff = entry_diff;
1358
+ optimal_stop = mid_stop;
1359
+ best_stop_result = result;
1360
+ }
1361
+ if (first_entry < target_stop) {
1362
+ if (isLong) {
1363
+ low_stop = mid_stop;
1364
+ } else {
1365
+ low_stop = mid_stop;
1366
+ }
1367
+ } else {
1368
+ if (isLong) {
1369
+ high_stop = mid_stop;
1370
+ } else {
1371
+ high_stop = mid_stop;
1372
+ }
1373
+ }
1374
+ if (entry_diff < 1)
1375
+ break;
1376
+ }
1377
+ console.log(`Found optimal stop: ${optimal_stop.toFixed(2)} that gives first_entry: ${best_stop_result?.[0]?.entry?.toFixed(2)}, difference: ${best_entry_diff.toFixed(2)} after ${iterations} iterations`);
1378
+ let low_risk = 10;
1379
+ let high_risk = params.highest_risk || 2000;
1380
+ let optimal_risk = app_config.risk_per_trade;
1381
+ let best_size_result = best_stop_result;
1382
+ let best_size_diff = Infinity;
1383
+ console.log(`Finding optimal risk_per_trade for size target: ${max_size} (never exceeding it)`);
1384
+ iterations = 0;
1385
+ while (Math.abs(high_risk - low_risk) > 0.1 && iterations < MAX_ITERATIONS) {
1386
+ iterations++;
1387
+ const mid_risk = (low_risk + high_risk) / 2;
1388
+ const result = sortedBuildConfig(app_config, {
1389
+ entry: app_config.entry,
1390
+ stop: optimal_stop,
1391
+ kind: app_config.kind,
1392
+ risk: mid_risk,
1393
+ risk_reward: app_config.risk_reward,
1394
+ increase: true,
1395
+ gap: app_config.gap,
1396
+ price_places: app_config.price_places,
1397
+ decimal_places: app_config.decimal_places
1398
+ });
1399
+ if (result.length === 0) {
1400
+ high_risk = mid_risk;
1401
+ continue;
1402
+ }
1403
+ const first_entry = result[0];
1404
+ const avg_size = first_entry.avg_size;
1405
+ console.log(`Risk: ${mid_risk.toFixed(2)}, Avg Size: ${avg_size.toFixed(4)}, Target: ${max_size}`);
1406
+ if (avg_size <= max_size) {
1407
+ const size_diff = max_size - avg_size;
1408
+ if (size_diff < best_size_diff) {
1409
+ best_size_diff = size_diff;
1410
+ optimal_risk = mid_risk;
1411
+ best_size_result = result;
1412
+ }
1413
+ low_risk = mid_risk;
1414
+ } else {
1415
+ high_risk = mid_risk;
1416
+ }
1417
+ if (best_size_diff < 0.001)
1418
+ break;
1419
+ }
1420
+ console.log(`Found optimal risk_per_trade: ${optimal_risk.toFixed(2)} that gives avg_size: ${best_size_result?.[0]?.avg_size?.toFixed(4)}, difference: ${best_size_diff.toFixed(4)} after ${iterations} iterations`);
1421
+ let final_risk = optimal_risk;
1422
+ let final_result = best_size_result;
1423
+ if (best_size_result?.[0]?.avg_size < max_size) {
1424
+ console.log(`Current avg_size (${best_size_result?.[0].avg_size.toFixed(4)}) is less than target (${max_size}). Increasing risk to reach target...`);
1425
+ let current_risk = optimal_risk;
1426
+ const risk_increment = 5;
1427
+ iterations = 0;
1428
+ while (iterations < MAX_ITERATIONS) {
1429
+ iterations++;
1430
+ current_risk += risk_increment;
1431
+ if (current_risk > 5000)
1432
+ break;
1433
+ const result = sortedBuildConfig(app_config, {
1434
+ entry: app_config.entry,
1435
+ stop: optimal_stop,
1436
+ kind: app_config.kind,
1437
+ risk: current_risk,
1438
+ risk_reward: app_config.risk_reward,
1439
+ increase: true,
1440
+ gap: app_config.gap,
1441
+ price_places: app_config.price_places,
1442
+ decimal_places: app_config.decimal_places
1443
+ });
1444
+ if (result.length === 0)
1445
+ continue;
1446
+ const avg_size = result[0].avg_size;
1447
+ console.log(`Increased Risk: ${current_risk.toFixed(2)}, New Avg Size: ${avg_size.toFixed(4)}`);
1448
+ if (avg_size >= max_size) {
1449
+ final_risk = current_risk;
1450
+ final_result = result;
1451
+ console.log(`Target size reached! Final risk: ${final_risk.toFixed(2)}, Final avg_size: ${avg_size.toFixed(4)}`);
1452
+ break;
1453
+ }
1454
+ }
1455
+ }
1456
+ return {
1457
+ optimal_stop: to_f(optimal_stop, app_config.price_places),
1458
+ optimal_risk: to_f(final_risk, app_config.price_places),
1459
+ avg_size: final_result?.[0]?.avg_size || 0,
1460
+ avg_entry: final_result?.[0]?.avg_entry || 0,
1461
+ result: final_result,
1462
+ first_entry: final_result?.[0]?.entry || 0,
1463
+ neg_pnl: final_result?.[0]?.neg_pnl || 0,
1464
+ risk_reward: app_config.risk_reward,
1465
+ size_diff: Math.abs((final_result?.[0]?.avg_size || 0) - max_size),
1466
+ entry_diff: best_entry_diff
1467
+ };
1468
+ }
1469
+ function generate_config_params(app_config, payload) {
1470
+ const { result, ...optimum } = getOptimumStopAndRisk(app_config, {
1471
+ max_size: app_config.max_size,
1472
+ highest_risk: payload.risk * 2,
1473
+ target_stop: app_config.stop
1474
+ });
1475
+ return {
1476
+ entry: payload.entry,
1477
+ stop: optimum.optimal_stop,
1478
+ avg_size: optimum.avg_size,
1479
+ avg_entry: optimum.avg_entry,
1480
+ risk_reward: payload.risk_reward,
1481
+ neg_pnl: optimum.neg_pnl,
1482
+ risk: payload.risk
1483
+ };
1484
+ }
1485
+ function determine_break_even_price(payload) {
1486
+ const { long_position, short_position, fee_percent = 0.05 / 100 } = payload;
1487
+ const long_notional = long_position.entry * long_position.quantity;
1488
+ const short_notional = short_position.entry * short_position.quantity;
1489
+ const net_quantity = long_position.quantity - short_position.quantity;
1490
+ const net_capital = Math.abs(long_notional - short_notional);
1491
+ const break_even_price = net_capital / (Math.abs(net_quantity) + fee_percent * Math.abs(net_quantity));
1492
+ return {
1493
+ price: break_even_price,
1494
+ direction: net_quantity > 0 ? "long" : "short"
1495
+ };
1496
+ }
1497
+ function determine_amount_to_buy(payload) {
1498
+ const {
1499
+ orders,
1500
+ kind,
1501
+ decimal_places = "%.3f",
1502
+ position: position2,
1503
+ existingOrders
1504
+ } = payload;
1505
+ const totalQuantity = orders.reduce((sum, order) => sum + (order.quantity || 0), 0);
1506
+ let runningTotal = to_f(totalQuantity, decimal_places);
1507
+ let sortedOrders = [...orders].sort((a, b) => (a.entry || 0) - (b.entry || 0));
1508
+ if (kind === "short") {
1509
+ sortedOrders.reverse();
1510
+ }
1511
+ const withCumulative = [];
1512
+ for (const order of sortedOrders) {
1513
+ withCumulative.push({
1514
+ ...order,
1515
+ cumulative_quantity: runningTotal
1516
+ });
1517
+ runningTotal -= order.quantity;
1518
+ runningTotal = to_f(runningTotal, decimal_places);
1519
+ }
1520
+ let filteredOrders = withCumulative.filter((order) => (order.cumulative_quantity || 0) > position2?.quantity).map((order) => ({
1521
+ ...order,
1522
+ price: order.entry,
1523
+ kind,
1524
+ side: kind.toLowerCase() === "long" ? "buy" : "sell"
1525
+ }));
1526
+ filteredOrders = filteredOrders.filter((k) => !existingOrders.map((j) => j.price).includes(k.price));
1527
+ return filteredOrders;
1528
+ }
1529
+ function generateOptimumAppConfig(config, payload, position2) {
1530
+ let low_risk = payload.start_risk;
1531
+ let high_risk = payload.max_risk || 50000;
1532
+ let best_risk = null;
1533
+ let best_app_config = null;
1534
+ const tolerance = 0.1;
1535
+ const max_iterations = 150;
1536
+ let iterations = 0;
1537
+ console.log(`Starting risk search for ${position2.kind} position. Target Entry: ${position2.entry}, Initial Risk Range: [${low_risk}, ${high_risk}]`);
1538
+ while (high_risk - low_risk > tolerance && iterations < max_iterations) {
1539
+ iterations++;
1540
+ const mid_risk = (low_risk + high_risk) / 2;
1541
+ const {
1542
+ app_config: current_app_config,
1543
+ max_size,
1544
+ last_value,
1545
+ entries
1546
+ } = get_app_config_and_max_size({
1547
+ ...config,
1548
+ risk: mid_risk,
1549
+ profit_percent: config.profit_percent || 0,
1550
+ profit: config.profit || 500,
1551
+ risk_reward: payload.risk_reward,
1552
+ kind: position2.kind,
1553
+ price_places: config.price_places,
1554
+ decimal_places: config.decimal_places,
1555
+ accounts: config.accounts || [],
1556
+ reduce_percent: config.reduce_percent || 90,
1557
+ reverse_factor: config.reverse_factor || 1,
1558
+ symbol: config.symbol || "",
1559
+ support: config.support || 0,
1560
+ resistance: config.resistance || 0,
1561
+ min_size: config.min_size || 0,
1562
+ stop_percent: config.stop_percent || 0
1563
+ }, {
1564
+ entry: payload.entry,
1565
+ stop: payload.stop,
1566
+ kind: position2.kind
1567
+ });
1568
+ current_app_config.max_size = max_size;
1569
+ current_app_config.last_value = last_value;
1570
+ current_app_config.entries = entries;
1571
+ current_app_config.risk_reward = payload.risk_reward;
1572
+ const full_trades = sortedBuildConfig(current_app_config, {
1573
+ entry: current_app_config.entry,
1574
+ stop: current_app_config.stop,
1575
+ kind: current_app_config.kind,
1576
+ risk: current_app_config.risk_per_trade,
1577
+ risk_reward: current_app_config.risk_reward,
1578
+ increase: true,
1579
+ gap: current_app_config.gap,
1580
+ price_places: current_app_config.price_places,
1581
+ decimal_places: current_app_config.decimal_places
1582
+ });
1583
+ console.log(`Iteration ${iterations}: Risk=${mid_risk.toFixed(2)}, Low=${low_risk.toFixed(2)}, High=${high_risk.toFixed(2)}`);
1584
+ if (full_trades.length === 0) {
1585
+ console.log(` -> No trades generated by sortedBuildConfig at Risk=${mid_risk.toFixed(2)}. Adjusting high_risk down.`);
1586
+ high_risk = mid_risk;
1587
+ continue;
1588
+ }
1589
+ const trades = determine_amount_to_buy({
1590
+ orders: full_trades,
1591
+ kind: position2.kind,
1592
+ decimal_places: current_app_config.decimal_places,
1593
+ price_places: current_app_config.price_places,
1594
+ position: position2,
1595
+ existingOrders: []
1596
+ });
1597
+ if (trades.length === 0) {
1598
+ console.log(` -> No trades met quantity requirement after filtering at Risk=${mid_risk.toFixed(2)}. Adjusting low_risk up.`);
1599
+ low_risk = mid_risk;
1600
+ continue;
1601
+ }
1602
+ const last_trade = trades[trades.length - 1];
1603
+ const last_entry = last_trade.entry;
1604
+ console.log(` -> Last Trade Entry (Filtered): ${last_entry.toFixed(4)}`);
1605
+ if (position2.kind === "long") {
1606
+ if (last_entry > position2.entry) {
1607
+ console.log(` -> Constraint VIOLATED (Long): last_entry (${last_entry.toFixed(4)}) > position.entry (${position2.entry.toFixed(4)}). Reducing high_risk.`);
1608
+ high_risk = mid_risk;
1609
+ } else {
1610
+ console.log(` -> Constraint MET (Long): last_entry (${last_entry.toFixed(4)}) <= position.entry (${position2.entry.toFixed(4)}). Storing as best, increasing low_risk.`);
1611
+ best_risk = mid_risk;
1612
+ best_app_config = current_app_config;
1613
+ low_risk = mid_risk;
1614
+ }
1615
+ } else {
1616
+ if (last_entry < position2.entry) {
1617
+ console.log(` -> Constraint VIOLATED (Short): last_entry (${last_entry.toFixed(4)}) < position.entry (${position2.entry.toFixed(4)}). Reducing high_risk.`);
1618
+ high_risk = mid_risk;
1619
+ } else {
1620
+ console.log(` -> Constraint MET (Short): last_entry (${last_entry.toFixed(4)}) >= position.entry (${position2.entry.toFixed(4)}). Storing as best, increasing low_risk.`);
1621
+ best_risk = mid_risk;
1622
+ best_app_config = current_app_config;
1623
+ low_risk = mid_risk;
1624
+ }
1625
+ }
1626
+ }
1627
+ if (iterations >= max_iterations) {
1628
+ console.warn(`generateAppConfig: Reached max iterations (${max_iterations}) without converging. Returning best found result.`);
1629
+ } else if (best_app_config) {
1630
+ console.log(`Search finished. Best Risk: ${best_risk?.toFixed(2)}, Final Last Entry: ${best_app_config.last_value?.entry?.toFixed(4)}`);
1631
+ } else {
1632
+ console.warn(`generateAppConfig: Could not find a valid risk configuration.`);
1633
+ }
1634
+ if (!best_app_config) {
1635
+ return null;
1636
+ }
1637
+ best_app_config.entries = determine_amount_to_buy({
1638
+ orders: best_app_config.entries,
1639
+ kind: position2.kind,
1640
+ decimal_places: best_app_config.decimal_places,
1641
+ price_places: best_app_config.price_places,
1642
+ position: position2,
1643
+ existingOrders: []
1644
+ });
1645
+ return best_app_config;
1646
+ }
1647
+ function determineOptimumReward(app_config, increase = true, low_range = 30, high_range = 199) {
1648
+ const criterion = app_config.strategy || "quantity";
1649
+ const risk_rewards = createArray(low_range, high_range, 1);
1650
+ let func = risk_rewards.map((trade_no) => {
1651
+ let trades = sortedBuildConfig(app_config, {
1652
+ take_profit: app_config.take_profit,
1653
+ entry: app_config.entry,
1654
+ stop: app_config.stop,
1655
+ no_of_trades: trade_no,
1656
+ risk_reward: trade_no,
1657
+ increase,
1658
+ kind: app_config.kind,
1659
+ gap: app_config.gap,
1660
+ decimal_places: app_config.decimal_places
1661
+ });
1662
+ let total = 0;
1663
+ let max = -Infinity;
1664
+ let min = Infinity;
1665
+ let neg_pnl = trades[0]?.neg_pnl || 0;
1666
+ let entry = trades.at(-1)?.entry;
1667
+ if (!entry) {
1668
+ return null;
1669
+ }
1670
+ for (let trade of trades) {
1671
+ total += trade.quantity;
1672
+ max = Math.max(max, trade.quantity);
1673
+ min = Math.min(min, trade.quantity);
1674
+ entry = app_config.kind === "long" ? Math.max(entry, trade.entry) : Math.min(entry, trade.entry);
1675
+ }
1676
+ return {
1677
+ result: trades,
1678
+ value: trade_no,
1679
+ total,
1680
+ risk_per_trade: app_config.risk_per_trade,
1681
+ max,
1682
+ min,
1683
+ neg_pnl,
1684
+ entry
1685
+ };
1686
+ });
1687
+ func = func.filter((r) => Boolean(r)).filter((r) => {
1688
+ let foundIndex = r?.result.findIndex((e) => e.quantity === r.max);
1689
+ return criterion === "quantity" ? foundIndex === 0 : true;
1690
+ });
1691
+ const highest = criterion === "quantity" ? Math.max(...func.map((o) => o.max)) : Math.min(...func.map((o) => o.entry));
1692
+ const key = criterion === "quantity" ? "max" : "entry";
1693
+ const index = findIndexByCondition(func, app_config.kind, (x) => x[key] == highest, criterion);
1694
+ if (app_config.raw) {
1695
+ return func[index];
1696
+ }
1697
+ return func[index]?.value;
1698
+ }
1699
+ function findIndexByCondition(lst, kind, condition, defaultKey = "neg_pnl") {
1700
+ const found = [];
1701
+ let max_new_diff = 0;
1702
+ let new_lst = lst.map((i, j) => ({
1703
+ ...i,
1704
+ net_diff: lst[j].neg_pnl + lst[j].risk_per_trade
1705
+ }));
1706
+ new_lst.forEach((item, index) => {
1707
+ if (item.net_diff > 0) {
1708
+ found.push(index);
1709
+ }
1710
+ });
1711
+ if (found.length === 0) {
1712
+ max_new_diff = Math.max(...new_lst.map((e) => e.net_diff));
1713
+ found.push(new_lst.findIndex((e) => e.net_diff === max_new_diff));
1714
+ }
1715
+ const sortedFound = found.map((o, index) => ({ ...new_lst[o], index: o })).filter((j) => max_new_diff === 0 ? j.net_diff > 0 : j.net_diff >= max_new_diff).sort((a, b) => {
1716
+ if (a.total !== b.total) {
1717
+ return b.total - a.total;
1718
+ return b.net_diff - a.net_diff;
1719
+ return a.net_diff - b.net_diff;
1720
+ } else {
1721
+ return b.net_diff - a.net_diff;
1722
+ }
1723
+ });
1724
+ if (defaultKey === "quantity") {
1725
+ return sortedFound[0].index;
1726
+ }
1727
+ if (found.length === 1) {
1728
+ return found[0];
1729
+ }
1730
+ if (found.length === 0) {
1731
+ return -1;
1732
+ }
1733
+ const entryCondition = (a, b) => {
1734
+ if (kind == "long") {
1735
+ return a.entry > b.entry;
1736
+ }
1737
+ return a.entry < b.entry;
1738
+ };
1739
+ const maximum = found.reduce((maxIndex, currentIndex) => {
1740
+ return new_lst[currentIndex]["net_diff"] < new_lst[maxIndex]["net_diff"] && entryCondition(new_lst[currentIndex], new_lst[maxIndex]) ? currentIndex : maxIndex;
1741
+ }, found[0]);
1742
+ return maximum;
1743
+ }
1744
+ function computeRiskReward(payload) {
1745
+ const { app_config, entry, stop, risk_per_trade } = payload;
1746
+ const kind = entry > stop ? "long" : "short";
1747
+ app_config.kind = kind;
1748
+ app_config.entry = entry;
1749
+ app_config.stop = stop;
1750
+ app_config.risk_per_trade = risk_per_trade;
1751
+ const result = determineOptimumReward(app_config);
1752
+ return result;
1753
+ }
1754
+ function getRiskReward(payload) {
1755
+ const { entry, stop, risk, global_config } = payload;
1756
+ const { entries, last_value, ...app_config } = buildAppConfig(global_config, {
1757
+ entry,
1758
+ stop,
1759
+ risk_reward: 30,
1760
+ risk,
1761
+ symbol: global_config.symbol
1762
+ });
1763
+ const risk_reward = computeRiskReward({
1764
+ app_config,
1765
+ entry,
1766
+ stop,
1767
+ risk_per_trade: risk
1768
+ });
1769
+ return risk_reward;
1770
+ }
1771
+ // src/helpers/strategy.ts
1772
+ class Strategy {
1773
+ position;
1774
+ dominant_position = "long";
1775
+ config;
1776
+ constructor(payload) {
1777
+ this.position = {
1778
+ long: payload.long,
1779
+ short: payload.short
1780
+ };
1781
+ this.dominant_position = payload.dominant_position || "long";
1782
+ this.config = payload.config;
1783
+ if (!this.config.fee_percent) {
1784
+ this.config.fee_percent = 0.05;
1785
+ }
1786
+ }
1787
+ get price_places() {
1788
+ return this.config.global_config.price_places;
1789
+ }
1790
+ get decimal_places() {
1791
+ return this.config.global_config.decimal_places;
1792
+ }
1793
+ to_f(price) {
1794
+ return to_f(price, this.price_places);
1795
+ }
1796
+ to_df(quantity) {
1797
+ return to_f(quantity, this.decimal_places);
1798
+ }
1799
+ pnl(kind, _position) {
1800
+ const position2 = _position || this.position[kind];
1801
+ const { entry, quantity } = position2;
1802
+ const notional = entry * quantity;
1803
+ let tp_percent = this.config.tp_percent;
1804
+ if (kind == "short") {
1805
+ tp_percent = tp_percent * this.config.short_tp_factor;
1806
+ }
1807
+ const profit = notional * (tp_percent / 100);
1808
+ return this.to_f(profit);
1809
+ }
1810
+ tp(kind) {
1811
+ let position2 = this.position[kind];
1812
+ if (position2.quantity == 0) {
1813
+ const reverse_kind = kind == "long" ? "short" : "long";
1814
+ position2 = this.position[reverse_kind];
1815
+ }
1816
+ const { entry, quantity } = position2;
1817
+ const profit = this.pnl(kind, position2);
1818
+ const diff = profit / (quantity || 1);
1819
+ return this.to_f(kind == "long" ? entry + diff : entry - diff);
1820
+ }
1821
+ calculate_fee(position2) {
1822
+ const { price, quantity } = position2;
1823
+ const fee = price * quantity * this.config.fee_percent / 100;
1824
+ return this.to_f(fee);
1825
+ }
1826
+ get long_tp() {
1827
+ return this.tp("long");
1828
+ }
1829
+ get short_tp() {
1830
+ return this.tp("short");
1831
+ }
1832
+ generateGapClosingAlgorithm(payload) {
1833
+ const { kind } = payload;
1834
+ const { entry, quantity } = this.position[kind];
1835
+ const focus_position = this.position[kind];
1836
+ const reverse_kind = kind == "long" ? "short" : "long";
1837
+ const reverse_position = this.position[reverse_kind];
1838
+ let _entry = this.tp(kind);
1839
+ let _stop = this.tp(reverse_kind);
1840
+ if (!_entry && !_stop) {
1841
+ return null;
1842
+ }
1843
+ const second_payload = {
1844
+ entry: _entry,
1845
+ stop: _stop,
1846
+ risk_reward: this.config.risk_reward,
1847
+ start_risk: this.pnl(reverse_kind),
1848
+ max_risk: this.config.budget
1849
+ };
1850
+ const third_payload = {
1851
+ entry,
1852
+ quantity,
1853
+ kind
1854
+ };
1855
+ console.log({ second_payload, third_payload });
1856
+ const app_config = generateOptimumAppConfig(this.config.global_config, second_payload, third_payload);
1857
+ if (!app_config) {
1858
+ return null;
1859
+ }
1860
+ const { entries, ...rest } = app_config;
1861
+ const risk = this.to_f(rest.risk_per_trade);
1862
+ const adjusted_focus_entries = entries.map((entry2) => {
1863
+ let adjusted_price = entry2.price;
1864
+ if (focus_position.quantity > 0) {
1865
+ if (kind === "long" && entry2.price >= focus_position.entry) {
1866
+ adjusted_price = focus_position.entry;
1867
+ } else if (kind === "short" && entry2.price <= focus_position.entry) {
1868
+ adjusted_price = focus_position.entry;
1869
+ }
1870
+ }
1871
+ return {
1872
+ price: adjusted_price,
1873
+ quantity: entry2.quantity
1874
+ };
1875
+ });
1876
+ const avg = determine_average_entry_and_size(adjusted_focus_entries.concat([
1877
+ {
1878
+ price: entry,
1879
+ quantity
1880
+ }
1881
+ ]), rest.decimal_places, rest.price_places);
1882
+ const focus_loss = this.to_f((avg.price - second_payload.stop) * avg.quantity);
1883
+ let below_reverse_entries = kind === "long" ? entries.filter((u) => {
1884
+ return u.entry < (reverse_position.entry || focus_position.entry);
1885
+ }) : entries.filter((u) => {
1886
+ return u.entry > (reverse_position.entry || focus_position.entry);
1887
+ });
1888
+ const threshold = below_reverse_entries.at(-1);
1889
+ let adjusted_reverse_entries = entries.map((entry2) => {
1890
+ let adjusted_price = entry2.price;
1891
+ if (threshold) {
1892
+ if (reverse_kind === "short" && entry2.price > threshold.entry) {
1893
+ adjusted_price = threshold.entry;
1894
+ } else if (reverse_kind === "long" && entry2.price < threshold.entry) {
1895
+ adjusted_price = threshold.entry;
1896
+ }
1897
+ }
1898
+ return {
1899
+ price: adjusted_price,
1900
+ quantity: entry2.quantity
1901
+ };
1902
+ });
1903
+ const reverse_avg = determine_average_entry_and_size(adjusted_reverse_entries.concat([
1904
+ {
1905
+ price: reverse_position.entry,
1906
+ quantity: reverse_position.quantity
1907
+ }
1908
+ ]), rest.decimal_places, rest.price_places);
1909
+ const reverse_pnl = this.to_f((reverse_avg.price - second_payload.stop) * reverse_avg.quantity);
1910
+ const fee_to_pay = this.calculate_fee({
1911
+ price: avg.entry,
1912
+ quantity: avg.quantity
1913
+ }) + this.calculate_fee({
1914
+ price: reverse_avg.entry,
1915
+ quantity: reverse_avg.quantity
1916
+ });
1917
+ const net_reverse_pnl = reverse_pnl - fee_to_pay;
1918
+ const ratio = net_reverse_pnl / focus_loss;
1919
+ const quantity_to_sell = this.to_df(ratio * avg.quantity);
1920
+ const remaining_quantity = this.to_df(avg.quantity - quantity_to_sell);
1921
+ return {
1922
+ risk,
1923
+ risk_reward: this.config.risk_reward,
1924
+ [kind]: {
1925
+ avg_entry: avg.entry,
1926
+ avg_size: avg.quantity,
1927
+ loss: focus_loss,
1928
+ stop: second_payload.stop,
1929
+ stop_quantity: quantity_to_sell,
1930
+ re_entry_quantity: remaining_quantity,
1931
+ initial_pnl: this.pnl(kind),
1932
+ tp: second_payload.entry
1933
+ },
1934
+ [reverse_kind]: {
1935
+ avg_entry: reverse_avg.entry,
1936
+ avg_size: reverse_avg.quantity,
1937
+ pnl: reverse_pnl,
1938
+ tp: second_payload.stop,
1939
+ re_entry_quantity: remaining_quantity,
1940
+ initial_pnl: this.pnl(reverse_kind)
1941
+ },
1942
+ last_entry: rest.last_value.entry,
1943
+ first_entry: entries.at(-1).entry,
1944
+ threshold
1945
+ };
1946
+ }
1947
+ runIterations(payload) {
1948
+ const { kind, iterations } = payload;
1949
+ const reverse_kind = kind == "long" ? "short" : "long";
1950
+ const result = [];
1951
+ let position2 = {
1952
+ long: this.position.long,
1953
+ short: this.position.short
1954
+ };
1955
+ for (let i = 0;i < iterations; i++) {
1956
+ const instance = new Strategy({
1957
+ long: position2.long,
1958
+ short: position2.short,
1959
+ config: {
1960
+ ...this.config,
1961
+ tp_percent: this.config.tp_percent + i * 4
1962
+ }
1963
+ });
1964
+ const algorithm = instance.generateGapClosingAlgorithm({
1965
+ kind
1966
+ });
1967
+ if (!algorithm) {
1968
+ console.log("No algorithm found");
1969
+ return result;
1970
+ break;
1971
+ }
1972
+ result.push(algorithm);
1973
+ position2[kind] = {
1974
+ entry: algorithm[kind].avg_entry,
1975
+ quantity: algorithm[kind].re_entry_quantity
1976
+ };
1977
+ position2[reverse_kind] = {
1978
+ entry: algorithm[reverse_kind].tp,
1979
+ quantity: algorithm[reverse_kind].re_entry_quantity
1980
+ };
1981
+ }
1982
+ return result;
1983
+ }
1984
+ getPositionAfterTp(payload) {
1985
+ const { kind, include_fees = false } = payload;
1986
+ const focus_position = this.position[kind];
1987
+ const reverse_kind = kind == "long" ? "short" : "long";
1988
+ const reverse_position = this.position[reverse_kind];
1989
+ const focus_tp = this.tp(kind);
1990
+ const focus_pnl = this.pnl(kind);
1991
+ const fees = include_fees ? this.calculate_fee({
1992
+ price: focus_tp,
1993
+ quantity: focus_position.quantity
1994
+ }) * 2 : 0;
1995
+ const expected_loss = (focus_pnl - fees) * this.config.reduce_ratio;
1996
+ const actual_loss = Math.abs(focus_tp - reverse_position.entry) * reverse_position.quantity;
1997
+ const ratio = expected_loss / actual_loss;
1998
+ const loss_quantity = this.to_df(ratio * reverse_position.quantity);
1999
+ const remaining_quantity = this.to_df(reverse_position.quantity - loss_quantity);
2000
+ const diff = focus_pnl - expected_loss;
2001
+ return {
2002
+ [kind]: {
2003
+ entry: focus_tp,
2004
+ quantity: remaining_quantity
2005
+ },
2006
+ [reverse_kind]: {
2007
+ entry: reverse_position.entry,
2008
+ quantity: remaining_quantity
2009
+ },
2010
+ pnl: {
2011
+ [kind]: focus_pnl,
2012
+ [reverse_kind]: -expected_loss,
2013
+ diff
2014
+ },
2015
+ spread: this.to_f(Math.abs(focus_tp - reverse_position.entry) * remaining_quantity)
2016
+ };
2017
+ }
2018
+ getPositionAfterIteration(payload) {
2019
+ const { kind, iterations, with_fees = false } = payload;
2020
+ let _result = this.getPositionAfterTp({
2021
+ kind,
2022
+ include_fees: with_fees
2023
+ });
2024
+ const result = [_result];
2025
+ for (let i = 0;i < iterations - 1; i++) {
2026
+ const instance = new Strategy({
2027
+ long: _result.long,
2028
+ short: _result.short,
2029
+ config: {
2030
+ ...this.config,
2031
+ tp_percent: this.config.tp_percent + (i + 1) * 0.3
2032
+ }
2033
+ });
2034
+ _result = instance.getPositionAfterTp({
2035
+ kind,
2036
+ include_fees: with_fees
2037
+ });
2038
+ result.push(_result);
2039
+ }
2040
+ return result;
2041
+ }
2042
+ generateOppositeTrades(payload) {
2043
+ const { kind, risk_factor = 0.5 } = payload;
2044
+ const entry = this.position[kind].entry;
2045
+ const stop = this.tp(kind);
2046
+ const risk = this.pnl(kind) * risk_factor;
2047
+ const risk_reward = getRiskReward({
2048
+ entry,
2049
+ stop,
2050
+ risk,
2051
+ global_config: this.config.global_config
2052
+ });
2053
+ const { entries, last_value, ...app_config } = buildAppConfig(this.config.global_config, {
2054
+ entry,
2055
+ stop,
2056
+ risk_reward,
2057
+ risk,
2058
+ symbol: this.config.global_config.symbol
2059
+ });
2060
+ const trades_to_place = determine_amount_to_buy({
2061
+ orders: entries,
2062
+ kind: app_config.kind,
2063
+ decimal_places: app_config.decimal_places,
2064
+ price_places: app_config.price_places,
2065
+ position: this.position[app_config.kind],
2066
+ existingOrders: []
2067
+ });
2068
+ const avg = determine_average_entry_and_size(trades_to_place.map((u) => ({
2069
+ price: u.entry,
2070
+ quantity: u.quantity
2071
+ })).concat([
2072
+ {
2073
+ price: this.position[app_config.kind].entry,
2074
+ quantity: this.position[app_config.kind].quantity
2075
+ }
2076
+ ]), app_config.decimal_places, app_config.price_places);
2077
+ const expected_loss = Math.abs(avg.price - stop) * avg.quantity;
2078
+ const profit_percent = to_f(this.pnl(kind) * 100 / (avg.price * avg.quantity), "%.3f");
2079
+ return { ...app_config, avg, loss: -expected_loss, profit_percent };
2080
+ }
2081
+ }
2082
+ export {
2083
+ to_f,
2084
+ sortedBuildConfig,
2085
+ range,
2086
+ profitHelper,
2087
+ logWithLineNumber,
2088
+ groupIntoPairsWithSumLessThan,
2089
+ groupIntoPairs,
2090
+ groupBy,
2091
+ get_app_config_and_max_size,
2092
+ getTradeEntries,
2093
+ getRiskReward,
2094
+ getParamForField,
2095
+ getOptimumStopAndRisk,
2096
+ getDecimalPlaces,
2097
+ generate_config_params,
2098
+ generateOptimumAppConfig,
2099
+ formatPrice,
2100
+ fibonacci_analysis,
2101
+ extractValue,
2102
+ determine_stop_and_size,
2103
+ determine_remaining_entry,
2104
+ determine_position_size,
2105
+ determine_break_even_price,
2106
+ determine_average_entry_and_size,
2107
+ determine_amount_to_sell2 as determine_amount_to_sell,
2108
+ determine_amount_to_buy,
2109
+ determineOptimumReward,
2110
+ createGapPairs,
2111
+ createArray,
2112
+ computeTotalAverageForEachTrade,
2113
+ computeSellZones,
2114
+ computeRiskReward,
2115
+ buildConfig,
2116
+ buildAvg,
2117
+ buildAppConfig,
2118
+ asCoins,
2119
+ allCoins,
2120
+ Strategy,
2121
+ SpecialCoins
2122
+ };