@gbozee/ultimate 0.0.2-99 → 0.0.2-next.4

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.
@@ -1,3 +1,315 @@
1
+ // src/helpers/distributions.ts
2
+ function generateArithmetic(payload) {
3
+ const { margin_range, risk_reward, kind, price_places = "%.1f" } = payload;
4
+ const difference = Math.abs(margin_range[1] - margin_range[0]);
5
+ const spread = difference / risk_reward;
6
+ return Array.from({ length: risk_reward + 1 }, (_, i) => {
7
+ const price = kind === "long" ? margin_range[1] - spread * i : margin_range[0] + spread * i;
8
+ return to_f(price, price_places);
9
+ });
10
+ }
11
+ function generateGeometric(payload) {
12
+ const { margin_range, risk_reward, kind, price_places = "%.1f", percent_change } = payload;
13
+ const effectivePercentChange = percent_change ?? Math.abs(margin_range[1] / margin_range[0] - 1) / risk_reward;
14
+ return Array.from({ length: risk_reward + 1 }, (_, i) => {
15
+ const price = kind === "long" ? margin_range[1] * Math.pow(1 - effectivePercentChange, i) : margin_range[0] * Math.pow(1 + effectivePercentChange, i);
16
+ return to_f(price, price_places);
17
+ });
18
+ }
19
+ function approximateInverseNormal(p) {
20
+ p = Math.max(0.0001, Math.min(0.9999, p));
21
+ if (p < 0.5) {
22
+ const t = Math.sqrt(-2 * Math.log(p));
23
+ const c0 = 2.515517, c1 = 0.802853, c2 = 0.010328;
24
+ const d1 = 1.432788, d2 = 0.189269, d3 = 0.001308;
25
+ return -(t - (c0 + c1 * t + c2 * t * t) / (1 + d1 * t + d2 * t * t + d3 * t * t * t));
26
+ } else {
27
+ const t = Math.sqrt(-2 * Math.log(1 - p));
28
+ const c0 = 2.515517, c1 = 0.802853, c2 = 0.010328;
29
+ const d1 = 1.432788, d2 = 0.189269, d3 = 0.001308;
30
+ return t - (c0 + c1 * t + c2 * t * t) / (1 + d1 * t + d2 * t * t + d3 * t * t * t);
31
+ }
32
+ }
33
+ function generateNormal(payload) {
34
+ const { margin_range, risk_reward, kind, price_places = "%.1f", stdDevFactor = 6 } = payload;
35
+ const mean = (margin_range[0] + margin_range[1]) / 2;
36
+ const stdDev = Math.abs(margin_range[1] - margin_range[0]) / stdDevFactor;
37
+ const skew = kind === "long" ? -0.2 : 0.2;
38
+ const adjustedMean = mean + stdDev * skew;
39
+ const entries = Array.from({ length: risk_reward + 1 }, (_, i) => {
40
+ const p = (i + 0.5) / (risk_reward + 1);
41
+ const z = approximateInverseNormal(p);
42
+ let price = adjustedMean + stdDev * z;
43
+ price = Math.max(margin_range[0], Math.min(margin_range[1], price));
44
+ return to_f(price, price_places);
45
+ });
46
+ return entries.sort((a, b) => a - b);
47
+ }
48
+ function generateExponential(payload) {
49
+ const { margin_range, risk_reward, kind, price_places = "%.1f", lambda } = payload;
50
+ const range = Math.abs(margin_range[1] - margin_range[0]);
51
+ const effectiveLambda = lambda || 2.5;
52
+ return Array.from({ length: risk_reward + 1 }, (_, i) => {
53
+ const t = i / risk_reward;
54
+ const exponentialProgress = 1 - Math.exp(-effectiveLambda * t);
55
+ const price = kind === "long" ? margin_range[1] - range * exponentialProgress : margin_range[0] + range * exponentialProgress;
56
+ return to_f(price, price_places);
57
+ });
58
+ }
59
+ function generateInverseExponential(payload) {
60
+ const { margin_range, risk_reward, kind, price_places = "%.1f", curveFactor = 2 } = payload;
61
+ const range = Math.abs(margin_range[1] - margin_range[0]);
62
+ return Array.from({ length: risk_reward + 1 }, (_, i) => {
63
+ const t = i / risk_reward;
64
+ const progress = (Math.exp(curveFactor * t) - 1) / (Math.exp(curveFactor) - 1);
65
+ const price = kind === "long" ? margin_range[1] - range * progress : margin_range[0] + range * progress;
66
+ return to_f(price, price_places);
67
+ });
68
+ }
69
+ function getEntries(params) {
70
+ const {
71
+ kind,
72
+ distribution,
73
+ margin_range,
74
+ risk_reward,
75
+ price_places = "%.1f",
76
+ distribution_params = {}
77
+ } = params;
78
+ let entries = [];
79
+ switch (distribution) {
80
+ case "arithmetic":
81
+ entries = generateArithmetic({
82
+ margin_range,
83
+ risk_reward,
84
+ kind,
85
+ price_places,
86
+ percent_change: distribution_params?.curveFactor
87
+ });
88
+ break;
89
+ case "geometric":
90
+ entries = generateGeometric({
91
+ margin_range,
92
+ risk_reward,
93
+ kind,
94
+ price_places,
95
+ percent_change: distribution_params?.curveFactor
96
+ });
97
+ break;
98
+ case "normal":
99
+ entries = generateNormal({
100
+ margin_range,
101
+ risk_reward,
102
+ kind,
103
+ price_places,
104
+ stdDevFactor: distribution_params?.stdDevFactor
105
+ });
106
+ break;
107
+ case "exponential":
108
+ entries = generateExponential({
109
+ margin_range,
110
+ risk_reward,
111
+ kind,
112
+ price_places,
113
+ lambda: distribution_params?.lambda
114
+ });
115
+ break;
116
+ case "inverse-exponential":
117
+ entries = generateInverseExponential({
118
+ margin_range,
119
+ risk_reward,
120
+ kind,
121
+ price_places,
122
+ curveFactor: distribution_params?.curveFactor
123
+ });
124
+ break;
125
+ default:
126
+ throw new Error(`Unknown distribution type: ${distribution}`);
127
+ }
128
+ return entries.sort((a, b) => a - b);
129
+ }
130
+ var distributions_default = getEntries;
131
+
132
+ // src/helpers/optimizations.ts
133
+ function calculateTheoreticalKelly({
134
+ current_entry,
135
+ zone_prices,
136
+ kind = "long",
137
+ config = {}
138
+ }) {
139
+ const {
140
+ price_prediction_model = "uniform",
141
+ confidence_factor = 0.6,
142
+ volatility_adjustment = true
143
+ } = config;
144
+ const sorted_prices = zone_prices;
145
+ const current_index = sorted_prices.findIndex((price) => price === current_entry);
146
+ if (current_index === -1)
147
+ return 0.02;
148
+ const win_zones = kind === "long" ? sorted_prices.filter((price) => price > current_entry) : sorted_prices.filter((price) => price < current_entry);
149
+ const lose_zones = kind === "long" ? sorted_prices.filter((price) => price < current_entry) : sorted_prices.filter((price) => price > current_entry);
150
+ const { win_probability, avg_win_ratio, avg_loss_ratio } = calculateZoneProbabilities({
151
+ current_entry,
152
+ win_zones,
153
+ lose_zones,
154
+ price_prediction_model,
155
+ confidence_factor,
156
+ kind
157
+ });
158
+ const odds_ratio = avg_win_ratio / avg_loss_ratio;
159
+ const loss_probability = 1 - win_probability;
160
+ let kelly_fraction = (win_probability * odds_ratio - loss_probability) / odds_ratio;
161
+ if (volatility_adjustment) {
162
+ const zone_volatility = calculateZoneVolatility(sorted_prices);
163
+ const vol_adjustment = 1 / (1 + zone_volatility);
164
+ kelly_fraction *= vol_adjustment;
165
+ }
166
+ kelly_fraction = Math.max(0.005, Math.min(kelly_fraction, 0.5));
167
+ return to_f(kelly_fraction, "%.4f");
168
+ }
169
+ function calculateZoneProbabilities({
170
+ current_entry,
171
+ win_zones,
172
+ lose_zones,
173
+ price_prediction_model,
174
+ confidence_factor,
175
+ kind
176
+ }) {
177
+ if (win_zones.length === 0 && lose_zones.length === 0) {
178
+ return { win_probability: 0.5, avg_win_ratio: 0.02, avg_loss_ratio: 0.02 };
179
+ }
180
+ let win_probability;
181
+ switch (price_prediction_model) {
182
+ case "uniform":
183
+ win_probability = win_zones.length / (win_zones.length + lose_zones.length);
184
+ break;
185
+ case "normal":
186
+ const win_weight = win_zones.reduce((sum, _, idx) => sum + 1 / (idx + 1), 0);
187
+ const lose_weight = lose_zones.reduce((sum, _, idx) => sum + 1 / (idx + 1), 0);
188
+ win_probability = win_weight / (win_weight + lose_weight);
189
+ break;
190
+ case "exponential":
191
+ const exp_win_weight = win_zones.reduce((sum, _, idx) => sum + Math.exp(-idx * 0.5), 0);
192
+ const exp_lose_weight = lose_zones.reduce((sum, _, idx) => sum + Math.exp(-idx * 0.5), 0);
193
+ win_probability = exp_win_weight / (exp_win_weight + exp_lose_weight);
194
+ break;
195
+ default:
196
+ win_probability = 0.5;
197
+ }
198
+ win_probability = win_probability * confidence_factor + (1 - confidence_factor) * 0.5;
199
+ const avg_win_ratio = win_zones.length > 0 ? win_zones.reduce((sum, price) => {
200
+ return sum + Math.abs(price - current_entry) / current_entry;
201
+ }, 0) / win_zones.length : 0.02;
202
+ const avg_loss_ratio = lose_zones.length > 0 ? lose_zones.reduce((sum, price) => {
203
+ return sum + Math.abs(price - current_entry) / current_entry;
204
+ }, 0) / lose_zones.length : 0.02;
205
+ return {
206
+ win_probability: Math.max(0.1, Math.min(0.9, win_probability)),
207
+ avg_win_ratio: Math.max(0.005, avg_win_ratio),
208
+ avg_loss_ratio: Math.max(0.005, avg_loss_ratio)
209
+ };
210
+ }
211
+ function calculateZoneVolatility(zone_prices) {
212
+ if (zone_prices.length < 2)
213
+ return 0;
214
+ const price_changes = zone_prices.slice(1).map((price, i) => Math.abs(price - zone_prices[i]) / zone_prices[i]);
215
+ return price_changes.reduce((sum, change) => sum + change, 0) / price_changes.length;
216
+ }
217
+ function calculateTheoreticalKellyFixed({
218
+ current_entry,
219
+ zone_prices,
220
+ kind = "long",
221
+ config = {}
222
+ }) {
223
+ const {
224
+ price_prediction_model = "uniform",
225
+ confidence_factor = 0.6,
226
+ volatility_adjustment = true
227
+ } = config;
228
+ const sorted_prices = zone_prices;
229
+ const current_index = sorted_prices.findIndex((price) => price === current_entry);
230
+ if (current_index === -1)
231
+ return 0.02;
232
+ let stop_loss;
233
+ let target_zones;
234
+ if (kind === "long") {
235
+ stop_loss = Math.min(...zone_prices);
236
+ target_zones = zone_prices.filter((price) => price > current_entry);
237
+ } else {
238
+ stop_loss = Math.max(...zone_prices);
239
+ target_zones = zone_prices.filter((price) => price < current_entry);
240
+ }
241
+ const risk_amount = Math.abs(current_entry - stop_loss);
242
+ const avg_reward = target_zones.length > 0 ? target_zones.reduce((sum, price) => sum + Math.abs(price - current_entry), 0) / target_zones.length : risk_amount;
243
+ const risk_reward_ratio = avg_reward / risk_amount;
244
+ let position_quality;
245
+ if (kind === "long") {
246
+ const distance_from_stop = current_entry - stop_loss;
247
+ const max_distance = Math.max(...zone_prices) - stop_loss;
248
+ position_quality = 1 - distance_from_stop / max_distance;
249
+ } else {
250
+ const distance_from_stop = stop_loss - current_entry;
251
+ const max_distance = stop_loss - Math.min(...zone_prices);
252
+ position_quality = 1 - distance_from_stop / max_distance;
253
+ }
254
+ let base_probability = 0.5;
255
+ switch (price_prediction_model) {
256
+ case "uniform":
257
+ base_probability = 0.5;
258
+ break;
259
+ case "normal":
260
+ base_probability = 0.3 + position_quality * 0.4;
261
+ break;
262
+ case "exponential":
263
+ base_probability = 0.2 + Math.pow(position_quality, 0.5) * 0.6;
264
+ break;
265
+ }
266
+ const win_probability = base_probability * confidence_factor + (1 - confidence_factor) * 0.5;
267
+ const odds_ratio = Math.max(risk_reward_ratio, 0.5);
268
+ const loss_probability = 1 - win_probability;
269
+ let kelly_fraction = (win_probability * odds_ratio - loss_probability) / odds_ratio;
270
+ if (volatility_adjustment) {
271
+ const zone_volatility = calculateZoneVolatility(sorted_prices);
272
+ const vol_adjustment = 1 / (1 + zone_volatility);
273
+ kelly_fraction *= vol_adjustment;
274
+ }
275
+ kelly_fraction = Math.max(0.005, Math.min(kelly_fraction, 0.5));
276
+ return to_f(kelly_fraction, "%.4f");
277
+ }
278
+ function calculatePositionBasedKelly({
279
+ current_entry,
280
+ zone_prices,
281
+ kind = "long",
282
+ config = {}
283
+ }) {
284
+ const {
285
+ price_prediction_model = "uniform",
286
+ confidence_factor: _confidence_factor = 0.6
287
+ } = config;
288
+ const current_index = zone_prices.findIndex((price) => price === current_entry);
289
+ if (current_index === -1)
290
+ return 0.02;
291
+ const total_zones = zone_prices.length;
292
+ const position_score = (total_zones - current_index) / total_zones;
293
+ let adjusted_score;
294
+ switch (price_prediction_model) {
295
+ case "uniform":
296
+ adjusted_score = 0.5;
297
+ break;
298
+ case "normal":
299
+ adjusted_score = 0.3 + position_score * 0.4;
300
+ break;
301
+ case "exponential":
302
+ adjusted_score = 0.2 + Math.pow(position_score, 0.3) * 0.6;
303
+ break;
304
+ default:
305
+ adjusted_score = 0.5;
306
+ }
307
+ const base_kelly = 0.02;
308
+ const max_kelly = 0.2;
309
+ const kelly_fraction = base_kelly + adjusted_score * (max_kelly - base_kelly);
310
+ return to_f(kelly_fraction, "%.4f");
311
+ }
312
+
1
313
  // src/helpers/trade_signal.ts
2
314
  function determine_close_price({
3
315
  entry,
@@ -64,6 +376,7 @@ class Signal {
64
376
  budget;
65
377
  percent_change = 0.02;
66
378
  price_places = "%.5f";
379
+ distribution_params = {};
67
380
  decimal_places = "%.0f";
68
381
  zone_risk = 1;
69
382
  fee = 0.08 / 100;
@@ -80,8 +393,22 @@ class Signal {
80
393
  first_order_size;
81
394
  gap = 10;
82
395
  max_size = 0;
396
+ use_kelly = false;
397
+ kelly_prediction_model = "exponential";
398
+ kelly_confidence_factor = 0.6;
399
+ kelly_minimum_risk = 0.2;
400
+ kelly_func = "theoretical";
401
+ symbol;
402
+ distribution = {
403
+ long: "arithmetic",
404
+ short: "geometric"
405
+ };
406
+ use_progressive_risk;
407
+ risk_distribution;
408
+ max_quantity = 0.03;
83
409
  constructor({
84
410
  focus,
411
+ symbol,
85
412
  budget,
86
413
  percent_change = 0.02,
87
414
  price_places = "%.5f",
@@ -100,8 +427,25 @@ class Signal {
100
427
  minimum_size = 0,
101
428
  first_order_size = 0,
102
429
  gap = 10,
103
- max_size = 0
430
+ max_size = 0,
431
+ use_kelly = false,
432
+ kelly_prediction_model = "exponential",
433
+ kelly_confidence_factor = 0.6,
434
+ kelly_minimum_risk = 0.2,
435
+ kelly_func = "theoretical",
436
+ full_distribution,
437
+ max_quantity = 0.03,
438
+ distribution_params = {},
439
+ use_progressive_risk,
440
+ risk_distribution
104
441
  }) {
442
+ if (full_distribution) {
443
+ this.distribution = full_distribution;
444
+ }
445
+ this.distribution_params = distribution_params;
446
+ this.use_progressive_risk = use_progressive_risk;
447
+ this.risk_distribution = risk_distribution;
448
+ this.symbol = symbol;
105
449
  this.minimum_size = minimum_size;
106
450
  this.first_order_size = first_order_size;
107
451
  this.focus = focus;
@@ -122,6 +466,12 @@ class Signal {
122
466
  this.increase_position = increase_position;
123
467
  this.gap = gap;
124
468
  this.max_size = max_size;
469
+ this.use_kelly = use_kelly;
470
+ this.kelly_prediction_model = kelly_prediction_model;
471
+ this.kelly_confidence_factor = kelly_confidence_factor;
472
+ this.kelly_minimum_risk = kelly_minimum_risk;
473
+ this.kelly_func = kelly_func;
474
+ this.max_quantity = max_quantity;
125
475
  }
126
476
  build_entry({
127
477
  current_price,
@@ -131,7 +481,15 @@ class Signal {
131
481
  kind = "long",
132
482
  risk,
133
483
  no_of_trades = 1,
134
- take_profit
484
+ take_profit,
485
+ distribution,
486
+ distribution_params = {
487
+ buckets: [
488
+ { zone_percentage: 0.25, risk_percentage: 0.1 },
489
+ { zone_percentage: 0.5, risk_percentage: 0.4 },
490
+ { zone_percentage: 0.25, risk_percentage: 0.5 }
491
+ ]
492
+ }
135
493
  }) {
136
494
  let _stop_loss = stop_loss;
137
495
  if (!_stop_loss && stop_percent) {
@@ -140,20 +498,36 @@ class Signal {
140
498
  const percent_change = _stop_loss ? Math.max(current_price, _stop_loss) / Math.min(current_price, _stop_loss) - 1 : this.percent_change;
141
499
  const _no_of_trades = no_of_trades || this.risk_reward;
142
500
  let _resistance = current_price * Math.pow(1 + percent_change, 1);
501
+ const simple_support = Math.min(current_price, stop_loss);
502
+ const simple_resistance = Math.max(current_price, stop_loss);
503
+ const risk_per_trade = risk / this.risk_reward;
504
+ const use_progressive = distribution_params.use_progressive || this.use_progressive_risk;
505
+ const risk_distribution = use_progressive ? {
506
+ enabled: true,
507
+ total_risk_budget: risk,
508
+ risk_reward: this.risk_reward,
509
+ buckets: distribution_params.buckets || []
510
+ } : undefined;
143
511
  const derivedConfig = {
144
512
  ...this,
145
513
  percent_change,
146
514
  focus: current_price,
147
- resistance: _resistance,
148
- risk_per_trade: risk / this.risk_reward,
515
+ resistance: distribution ? simple_resistance : _resistance,
516
+ risk_per_trade,
149
517
  minimum_pnl: pnl,
150
518
  risk_reward: _no_of_trades,
151
519
  take_profit: take_profit || this.take_profit,
152
- support: kind === "long" ? _stop_loss : this.support
520
+ support: distribution ? simple_support : kind === "long" ? _stop_loss : this.support,
521
+ full_distribution: distribution ? {
522
+ ...this.distribution,
523
+ [kind]: distribution
524
+ } : undefined,
525
+ use_progressive_risk: use_progressive,
526
+ distribution_params,
527
+ risk_distribution
153
528
  };
154
529
  const instance = new Signal(derivedConfig);
155
- if (kind === "short") {
156
- }
530
+ if (kind === "short") {}
157
531
  let result = instance.get_bulk_trade_zones({ current_price, kind });
158
532
  return result;
159
533
  return result?.filter((x) => {
@@ -309,7 +683,6 @@ class Signal {
309
683
  let potentials = new_result.filter((x) => condition(x["entry"], i["risk_sell"])).map((x) => x["entry"]);
310
684
  if (potentials.length && max_index) {
311
685
  if (kind === "long") {
312
- console.log("slice: ", potentials.slice(0, max_index));
313
686
  i["risk_sell"] = Math.max(...potentials.slice(0, max_index));
314
687
  } else {
315
688
  i["risk_sell"] = Math.min(...potentials.slice(0, max_index));
@@ -366,13 +739,31 @@ class Signal {
366
739
  }
367
740
  this.zone_risk = original;
368
741
  }
742
+ get_future_zones_simple({
743
+ current_price,
744
+ kind = "long",
745
+ raw
746
+ }) {
747
+ const margin_zones = [this.support, this.resistance];
748
+ const distribution = this.distribution ? this.distribution[kind] : "geometric";
749
+ let _kind = distribution === "inverse-exponential" ? kind === "long" ? "short" : "long" : kind;
750
+ const entries = distributions_default({
751
+ margin_range: margin_zones,
752
+ kind: _kind,
753
+ distribution,
754
+ risk_reward: this.risk_reward,
755
+ price_places: this.price_places,
756
+ distribution_params: this.distribution_params
757
+ });
758
+ return entries.sort((a, b) => a - b);
759
+ }
369
760
  get_future_zones({
370
761
  current_price,
371
762
  kind = "long",
372
763
  raw
373
764
  }) {
374
- if (raw) {
375
- }
765
+ if (raw) {}
766
+ return this.get_future_zones_simple({ current_price, raw, kind });
376
767
  const margin_range = this.get_margin_range(current_price, kind);
377
768
  let margin_zones = this.get_margin_zones({ current_price });
378
769
  let remaining_zones = margin_zones.filter((x) => JSON.stringify(x) != JSON.stringify(margin_range));
@@ -498,6 +889,24 @@ class Signal {
498
889
  }
499
890
  return null;
500
891
  }
892
+ getZoneRisk(index, totalZones, account) {
893
+ if (!account.risk_distribution?.enabled) {
894
+ return account.risk_per_trade;
895
+ }
896
+ const { buckets, total_risk_budget } = account.risk_distribution;
897
+ const reversedBuckets = [...buckets].reverse();
898
+ let zoneStart = 0;
899
+ for (const bucket of reversedBuckets) {
900
+ const zoneEnd = zoneStart + Math.floor(totalZones * bucket.zone_percentage);
901
+ if (index < zoneEnd) {
902
+ const zonesInBucket = zoneEnd - zoneStart;
903
+ const bucket_total = total_risk_budget * bucket.risk_percentage;
904
+ return bucket_total / zonesInBucket;
905
+ }
906
+ zoneStart = zoneEnd;
907
+ }
908
+ return account.risk_per_trade;
909
+ }
501
910
  process_orders({
502
911
  current_price,
503
912
  stop_loss,
@@ -560,10 +969,26 @@ class Signal {
560
969
  }
561
970
  const defaultStopLoss = i === 0 ? stop_loss : _base;
562
971
  const new_stop = kind === "long" ? this.support : stop_loss;
972
+ let risk_to_use = this.getZoneRisk(i, limit_orders.length, this);
973
+ console.log("index: ", i, " risk: ", risk_to_use);
974
+ if (this.use_kelly) {
975
+ const func = this.kelly_func === "theoretical" ? calculateTheoreticalKelly : this.kelly_func === "position_based" ? calculatePositionBasedKelly : calculateTheoreticalKellyFixed;
976
+ const theoretical_kelly = func({
977
+ current_entry: x,
978
+ zone_prices: limit_orders,
979
+ kind,
980
+ config: {
981
+ price_prediction_model: this.kelly_prediction_model,
982
+ confidence_factor: this.kelly_confidence_factor,
983
+ volatility_adjustment: true
984
+ }
985
+ });
986
+ risk_to_use = theoretical_kelly * risk_per_trade / this.kelly_minimum_risk;
987
+ }
563
988
  const y = this.build_trade_dict({
564
989
  entry: x,
565
990
  stop: (this.increase_position ? new_stop : defaultStopLoss) || defaultStopLoss,
566
- risk: risk_per_trade,
991
+ risk: risk_to_use,
567
992
  arr: limit_orders,
568
993
  index: i,
569
994
  new_fees: total_incurred_market_fees,
@@ -577,14 +1002,13 @@ class Signal {
577
1002
  return y !== null ? y : undefined;
578
1003
  }) || []).filter((y) => y !== undefined).filter((y) => {
579
1004
  const min_options = [0.001, 0.002, 0.003];
580
- if (min_options.includes(this.minimum_size)) {
581
- return y.quantity <= 0.03;
1005
+ if (min_options.includes(this.minimum_size) && this.symbol.toUpperCase().startsWith("BTC")) {
1006
+ return y.quantity <= this.max_quantity;
582
1007
  }
583
1008
  return true;
584
1009
  });
585
1010
  let total_orders = limit_trades.concat(market_trades);
586
- if (kind === "short") {
587
- }
1011
+ if (kind === "short") {}
588
1012
  if (this.minimum_size && total_orders.length > 0) {
589
1013
  let payload = total_orders;
590
1014
  let greater_than_min_size = total_orders.filter((o) => o ? o.quantity >= this.minimum_size : true);
@@ -670,8 +1094,7 @@ class Signal {
670
1094
  });
671
1095
  const multiplier = start - index;
672
1096
  const incurred_fees = fees.reduce((a, b) => a + b, 0) + previous_risks.reduce((a, b) => a + b, 0);
673
- if (index === 0) {
674
- }
1097
+ if (index === 0) {}
675
1098
  let quantity = determine_position_size({
676
1099
  entry,
677
1100
  stop,
@@ -837,8 +1260,7 @@ function extractValue(_param, condition) {
837
1260
  try {
838
1261
  let value2 = JSON.parse(_param || "[]");
839
1262
  param = value2.map((o) => parseFloat(o));
840
- } catch (error) {
841
- }
1263
+ } catch (error) {}
842
1264
  } else {
843
1265
  param = parseFloat(_param);
844
1266
  }
@@ -853,8 +1275,7 @@ function asCoins(symbol) {
853
1275
  if (symbol.toLowerCase().includes("-")) {
854
1276
  result = result.split("-")[0];
855
1277
  }
856
- if (symbol.toLowerCase() == "usdt-usd") {
857
- }
1278
+ if (symbol.toLowerCase() == "usdt-usd") {}
858
1279
  let result2 = _type == "usdt" ? symbol.split(result)[0] : result;
859
1280
  if (result.includes("-")) {
860
1281
  result2 = result;
@@ -1153,7 +1574,64 @@ function computeSellZones(payload) {
1153
1574
  const spread = factor - 1;
1154
1575
  return Array.from({ length: zones }, (_, i) => entry * Math.pow(1 + spread, i));
1155
1576
  }
1577
+ function determineTPSl(payload) {
1578
+ const { sell_ratio = 1, kind, positions, configs, decimal_places } = payload;
1579
+ const long_settings = configs.long;
1580
+ const short_settings = configs.short;
1581
+ const settings = kind === "long" ? long_settings : short_settings;
1582
+ const reverse_kind = kind === "long" ? "short" : "long";
1583
+ const _position = positions[kind];
1584
+ const reverse_position = positions[reverse_kind];
1585
+ const reduce_ratio = settings.reduce_ratio;
1586
+ const notion = _position.entry * _position.quantity;
1587
+ const profit_percent = settings.profit_percent;
1588
+ const places = decimal_places || reverse_position.decimal_places;
1589
+ if (profit_percent) {
1590
+ let quantity = to_f(_position.quantity * sell_ratio, places);
1591
+ const as_float = parseFloat(profit_percent) * sell_ratio;
1592
+ const pnl = to_f(as_float * notion / 100, "%.2f");
1593
+ const diff = pnl / quantity;
1594
+ const tp_price = to_f(kind === "long" ? _position.entry + diff : _position.entry - diff, _position.price_places);
1595
+ const expected_loss = to_f(parseFloat(reduce_ratio) * pnl, "%.2f");
1596
+ let reduce_quantity = 0;
1597
+ if (reverse_position.quantity > 0) {
1598
+ const total_loss = Math.abs(tp_price - reverse_position.entry) * reverse_position.quantity;
1599
+ const ratio = expected_loss / total_loss;
1600
+ reduce_quantity = to_f(reverse_position.quantity * ratio, places);
1601
+ }
1602
+ return {
1603
+ tp_price,
1604
+ pnl,
1605
+ quantity,
1606
+ reduce_quantity,
1607
+ expected_loss
1608
+ };
1609
+ }
1610
+ return {
1611
+ tp_price: 0,
1612
+ pnl: 0,
1613
+ quantity: 0,
1614
+ reduce_quantity: 0,
1615
+ expected_loss: 0
1616
+ };
1617
+ }
1156
1618
  // src/helpers/shared.ts
1619
+ function getMaxQuantity(x, app_config) {
1620
+ let max_quantity = app_config.max_quantity;
1621
+ if (max_quantity) {
1622
+ return x <= max_quantity;
1623
+ }
1624
+ if (app_config.symbol === "BTCUSDT") {
1625
+ max_quantity = 0.03;
1626
+ }
1627
+ if (app_config.symbol?.toLowerCase().startsWith("sol")) {
1628
+ max_quantity = 2;
1629
+ }
1630
+ if (!max_quantity) {
1631
+ return true;
1632
+ }
1633
+ return x <= max_quantity;
1634
+ }
1157
1635
  function buildConfig(app_config, {
1158
1636
  take_profit,
1159
1637
  entry,
@@ -1168,7 +1646,16 @@ function buildConfig(app_config, {
1168
1646
  gap,
1169
1647
  rr = 1,
1170
1648
  price_places = "%.1f",
1171
- decimal_places = "%.3f"
1649
+ decimal_places = "%.3f",
1650
+ use_kelly = false,
1651
+ kelly_confidence_factor = 0.95,
1652
+ kelly_minimum_risk = 0.2,
1653
+ kelly_prediction_model = "exponential",
1654
+ kelly_func = "theoretical",
1655
+ min_avg_size = 0,
1656
+ distribution,
1657
+ distribution_params,
1658
+ use_progressive_risk
1172
1659
  }) {
1173
1660
  let fee = app_config.fee / 100;
1174
1661
  let working_risk = risk || app_config.risk_per_trade;
@@ -1192,7 +1679,16 @@ function buildConfig(app_config, {
1192
1679
  kind: app_config.kind,
1193
1680
  gap,
1194
1681
  min_profit: min_profit || app_config.min_profit,
1195
- rr: rr || 1
1682
+ rr: rr || 1,
1683
+ use_kelly: use_kelly || app_config.kelly?.use_kelly,
1684
+ kelly_confidence_factor: kelly_confidence_factor || app_config.kelly?.kelly_confidence_factor,
1685
+ kelly_minimum_risk: kelly_minimum_risk || app_config.kelly?.kelly_minimum_risk,
1686
+ kelly_prediction_model: kelly_prediction_model || app_config.kelly?.kelly_prediction_model,
1687
+ kelly_func: kelly_func || app_config.kelly?.kelly_func,
1688
+ symbol: app_config.symbol,
1689
+ max_quantity: app_config.max_quantity,
1690
+ distribution_params: distribution_params || app_config.distribution_params,
1691
+ use_progressive_risk
1196
1692
  };
1197
1693
  const instance = new Signal(config);
1198
1694
  if (raw_instance) {
@@ -1202,16 +1698,19 @@ function buildConfig(app_config, {
1202
1698
  return [];
1203
1699
  }
1204
1700
  const condition = (kind === "long" ? entry > app_config.support : entry >= app_config.support) && stop >= app_config.support * 0.999;
1205
- if (kind === "short") {
1206
- }
1701
+ if (kind === "short") {}
1207
1702
  const result = entry === stop ? [] : condition ? instance.build_entry({
1208
1703
  current_price: entry,
1209
1704
  stop_loss: stop,
1210
1705
  risk: working_risk,
1211
1706
  kind: kind || app_config.kind,
1212
- no_of_trades: trade_no
1707
+ no_of_trades: trade_no,
1708
+ distribution,
1709
+ distribution_params
1213
1710
  }) || [] : [];
1214
- return computeTotalAverageForEachTrade(result, config);
1711
+ const new_trades = computeTotalAverageForEachTrade(result, config);
1712
+ let filtered = new_trades.filter((o) => o.avg_size > min_avg_size);
1713
+ return computeTotalAverageForEachTrade(filtered, config);
1215
1714
  }
1216
1715
  function buildAvg({
1217
1716
  _trades,
@@ -1228,13 +1727,7 @@ function buildAvg({
1228
1727
  }
1229
1728
  function sortedBuildConfig(app_config, options) {
1230
1729
  const sorted = buildConfig(app_config, options).sort((a, b) => app_config.kind === "long" ? a.entry - b.entry : b.entry - b.entry).filter((x) => {
1231
- if (app_config.symbol === "BTCUSDT") {
1232
- return x.quantity <= 0.03;
1233
- }
1234
- if (app_config.symbol?.toLowerCase().startsWith("sol")) {
1235
- return x.quantity <= 2;
1236
- }
1237
- return true;
1730
+ return getMaxQuantity(x.quantity, app_config);
1238
1731
  });
1239
1732
  return sorted.map((k, i) => {
1240
1733
  const arrSet = sorted.slice(0, i + 1);
@@ -1265,7 +1758,8 @@ function get_app_config_and_max_size(config, payload) {
1265
1758
  price_places: config.price_places,
1266
1759
  decimal_places: config.decimal_places,
1267
1760
  min_profit: config.profit_percent * config.profit / 100,
1268
- symbol: config.symbol
1761
+ symbol: config.symbol,
1762
+ max_quantity: config.max_quantity
1269
1763
  };
1270
1764
  const initialResult = sortedBuildConfig(app_config, {
1271
1765
  entry: app_config.entry,
@@ -1276,7 +1770,14 @@ function get_app_config_and_max_size(config, payload) {
1276
1770
  increase: true,
1277
1771
  gap: app_config.gap,
1278
1772
  price_places: app_config.price_places,
1279
- decimal_places: app_config.decimal_places
1773
+ decimal_places: app_config.decimal_places,
1774
+ use_kelly: payload.use_kelly,
1775
+ kelly_confidence_factor: payload.kelly_confidence_factor,
1776
+ kelly_minimum_risk: payload.kelly_minimum_risk,
1777
+ kelly_prediction_model: payload.kelly_prediction_model,
1778
+ kelly_func: payload.kelly_func,
1779
+ distribution: payload.distribution,
1780
+ distribution_params: payload.distribution_params
1280
1781
  });
1281
1782
  const max_size = initialResult[0]?.avg_size;
1282
1783
  const last_value = initialResult[0];
@@ -1305,21 +1806,42 @@ function buildAppConfig(config, payload) {
1305
1806
  reverse_factor: 1,
1306
1807
  profit_percent: 0,
1307
1808
  kind: payload.entry > payload.stop ? "long" : "short",
1308
- symbol: payload.symbol
1809
+ symbol: payload.symbol || config.symbol
1309
1810
  }, {
1310
1811
  entry: payload.entry,
1311
1812
  stop: payload.stop,
1312
- kind: payload.entry > payload.stop ? "long" : "short"
1813
+ kind: payload.entry > payload.stop ? "long" : "short",
1814
+ use_kelly: payload.use_kelly,
1815
+ kelly_confidence_factor: payload.kelly_confidence_factor,
1816
+ kelly_minimum_risk: payload.kelly_minimum_risk,
1817
+ kelly_prediction_model: payload.kelly_prediction_model,
1818
+ kelly_func: payload.kelly_func,
1819
+ distribution: payload.distribution,
1820
+ distribution_params: payload.distribution_params
1313
1821
  });
1314
1822
  app_config.max_size = max_size;
1315
1823
  app_config.entry = payload.entry || app_config.entry;
1316
1824
  app_config.stop = payload.stop || app_config.stop;
1317
1825
  app_config.last_value = last_value;
1318
1826
  app_config.entries = entries;
1827
+ app_config.kelly = {
1828
+ use_kelly: payload.use_kelly,
1829
+ kelly_confidence_factor: payload.kelly_confidence_factor,
1830
+ kelly_minimum_risk: payload.kelly_minimum_risk,
1831
+ kelly_prediction_model: payload.kelly_prediction_model,
1832
+ kelly_func: payload.kelly_func
1833
+ };
1834
+ app_config.distribution = payload.distribution;
1835
+ app_config.distribution_params = payload.distribution_params;
1319
1836
  return app_config;
1320
1837
  }
1321
1838
  function getOptimumStopAndRisk(app_config, params) {
1322
- const { max_size, target_stop } = params;
1839
+ const {
1840
+ max_size,
1841
+ target_stop,
1842
+ distribution,
1843
+ distribution_params: _distribution_params
1844
+ } = params;
1323
1845
  const isLong = app_config.kind === "long";
1324
1846
  const stopRange = Math.abs(app_config.entry - target_stop) * 0.5;
1325
1847
  let low_stop = isLong ? target_stop - stopRange : Math.max(target_stop - stopRange, app_config.entry);
@@ -1342,7 +1864,9 @@ function getOptimumStopAndRisk(app_config, params) {
1342
1864
  increase: true,
1343
1865
  gap: app_config.gap,
1344
1866
  price_places: app_config.price_places,
1345
- decimal_places: app_config.decimal_places
1867
+ decimal_places: app_config.decimal_places,
1868
+ distribution,
1869
+ distribution_params: _distribution_params
1346
1870
  });
1347
1871
  if (result.length === 0) {
1348
1872
  if (isLong) {
@@ -1396,7 +1920,9 @@ function getOptimumStopAndRisk(app_config, params) {
1396
1920
  increase: true,
1397
1921
  gap: app_config.gap,
1398
1922
  price_places: app_config.price_places,
1399
- decimal_places: app_config.decimal_places
1923
+ decimal_places: app_config.decimal_places,
1924
+ distribution,
1925
+ distribution_params: _distribution_params
1400
1926
  });
1401
1927
  if (result.length === 0) {
1402
1928
  high_risk = mid_risk;
@@ -1441,7 +1967,9 @@ function getOptimumStopAndRisk(app_config, params) {
1441
1967
  increase: true,
1442
1968
  gap: app_config.gap,
1443
1969
  price_places: app_config.price_places,
1444
- decimal_places: app_config.decimal_places
1970
+ decimal_places: app_config.decimal_places,
1971
+ distribution,
1972
+ distribution_params: _distribution_params
1445
1973
  });
1446
1974
  if (result.length === 0)
1447
1975
  continue;
@@ -1564,7 +2092,8 @@ function generateOptimumAppConfig(config, payload, position2) {
1564
2092
  }, {
1565
2093
  entry: payload.entry,
1566
2094
  stop: payload.stop,
1567
- kind: position2.kind
2095
+ kind: position2.kind,
2096
+ distribution: payload.distribution
1568
2097
  });
1569
2098
  current_app_config.max_size = max_size;
1570
2099
  current_app_config.last_value = last_value;
@@ -1579,7 +2108,8 @@ function generateOptimumAppConfig(config, payload, position2) {
1579
2108
  increase: true,
1580
2109
  gap: current_app_config.gap,
1581
2110
  price_places: current_app_config.price_places,
1582
- decimal_places: current_app_config.decimal_places
2111
+ decimal_places: current_app_config.decimal_places,
2112
+ distribution: payload.distribution
1583
2113
  });
1584
2114
  if (full_trades.length === 0) {
1585
2115
  high_risk = mid_risk;
@@ -1637,7 +2167,16 @@ function generateOptimumAppConfig(config, payload, position2) {
1637
2167
  });
1638
2168
  return best_app_config;
1639
2169
  }
1640
- function determineOptimumReward(app_config, increase = true, low_range = 30, high_range = 199) {
2170
+ function determineOptimumReward(payload) {
2171
+ const {
2172
+ app_config,
2173
+ increase = true,
2174
+ low_range = 1,
2175
+ high_range = 199,
2176
+ target_loss,
2177
+ distribution,
2178
+ max_size
2179
+ } = payload;
1641
2180
  const criterion = app_config.strategy || "quantity";
1642
2181
  const risk_rewards = createArray(low_range, high_range, 1);
1643
2182
  let func = risk_rewards.map((trade_no) => {
@@ -1650,13 +2189,16 @@ function determineOptimumReward(app_config, increase = true, low_range = 30, hig
1650
2189
  increase,
1651
2190
  kind: app_config.kind,
1652
2191
  gap: app_config.gap,
1653
- decimal_places: app_config.decimal_places
2192
+ decimal_places: app_config.decimal_places,
2193
+ distribution,
2194
+ distribution_params: payload.distribution_params
1654
2195
  });
1655
2196
  let total = 0;
1656
2197
  let max = -Infinity;
1657
2198
  let min = Infinity;
1658
2199
  let neg_pnl = trades[0]?.neg_pnl || 0;
1659
2200
  let entry = trades.at(-1)?.entry;
2201
+ let avg_size = trades[0]?.avg_size || 0;
1660
2202
  if (!entry) {
1661
2203
  return null;
1662
2204
  }
@@ -1673,14 +2215,41 @@ function determineOptimumReward(app_config, increase = true, low_range = 30, hig
1673
2215
  risk_per_trade: app_config.risk_per_trade,
1674
2216
  max,
1675
2217
  min,
2218
+ avg_size,
1676
2219
  neg_pnl,
1677
2220
  entry
1678
2221
  };
1679
2222
  });
1680
- func = func.filter((r) => Boolean(r)).filter((r) => {
1681
- let foundIndex = r?.result.findIndex((e) => e.quantity === r.max);
1682
- return criterion === "quantity" ? foundIndex === 0 : true;
1683
- });
2223
+ func = func.filter((r) => Boolean(r));
2224
+ if (max_size !== undefined && max_size > 0) {
2225
+ func = func.filter((r) => r.avg_size <= max_size);
2226
+ }
2227
+ if (target_loss === undefined) {
2228
+ func = func.filter((r) => {
2229
+ let foundIndex = r?.result.findIndex((e) => e.quantity === r.max);
2230
+ return criterion === "quantity" ? foundIndex === 0 : true;
2231
+ });
2232
+ }
2233
+ if (target_loss !== undefined) {
2234
+ const validResults = func.filter((r) => Math.abs(r.neg_pnl) <= target_loss);
2235
+ if (validResults.length > 0) {
2236
+ validResults.sort((a, b) => {
2237
+ const diffA = target_loss - Math.abs(a.neg_pnl);
2238
+ const diffB = target_loss - Math.abs(b.neg_pnl);
2239
+ return diffA - diffB;
2240
+ });
2241
+ if (app_config.raw) {
2242
+ return validResults[0];
2243
+ }
2244
+ return validResults[0]?.value;
2245
+ } else {
2246
+ func.sort((a, b) => Math.abs(a.neg_pnl) - Math.abs(b.neg_pnl));
2247
+ if (app_config.raw) {
2248
+ return func[0];
2249
+ }
2250
+ return func[0]?.value;
2251
+ }
2252
+ }
1684
2253
  const highest = criterion === "quantity" ? Math.max(...func.map((o) => o.max)) : Math.min(...func.map((o) => o.entry));
1685
2254
  const key = criterion === "quantity" ? "max" : "entry";
1686
2255
  const index = findIndexByCondition(func, app_config.kind, (x) => x[key] == highest, criterion);
@@ -1714,6 +2283,7 @@ function findIndexByCondition(lst, kind, condition, defaultKey = "neg_pnl") {
1714
2283
  return b.net_diff - a.net_diff;
1715
2284
  }
1716
2285
  });
2286
+ console.log("found", sortedFound);
1717
2287
  if (defaultKey === "quantity") {
1718
2288
  return sortedFound[0].index;
1719
2289
  }
@@ -1734,58 +2304,154 @@ function findIndexByCondition(lst, kind, condition, defaultKey = "neg_pnl") {
1734
2304
  }, found[0]);
1735
2305
  return maximum;
1736
2306
  }
2307
+ function determineOptimumRisk(config, payload, params) {
2308
+ const { highest_risk, tolerance = 0.01, max_iterations = 200 } = params;
2309
+ let low_risk = 1;
2310
+ let high_risk = Math.max(highest_risk * 5, payload.risk * 10);
2311
+ let best_risk = payload.risk;
2312
+ let best_neg_pnl = 0;
2313
+ let best_diff = Infinity;
2314
+ console.log(`Finding optimal risk for target neg_pnl: ${highest_risk}`);
2315
+ let iterations = 0;
2316
+ while (iterations < max_iterations && high_risk - low_risk > tolerance) {
2317
+ iterations++;
2318
+ const mid_risk = (low_risk + high_risk) / 2;
2319
+ const test_payload = {
2320
+ ...payload,
2321
+ risk: mid_risk
2322
+ };
2323
+ const { last_value } = buildAppConfig(config, test_payload);
2324
+ if (!last_value || !last_value.neg_pnl) {
2325
+ high_risk = mid_risk;
2326
+ continue;
2327
+ }
2328
+ const current_neg_pnl = Math.abs(last_value.neg_pnl);
2329
+ const diff = Math.abs(current_neg_pnl - highest_risk);
2330
+ console.log(`Iteration ${iterations}: Risk=${mid_risk.toFixed(2)}, neg_pnl=${current_neg_pnl.toFixed(2)}, diff=${diff.toFixed(2)}`);
2331
+ if (diff < best_diff) {
2332
+ best_diff = diff;
2333
+ best_risk = mid_risk;
2334
+ best_neg_pnl = current_neg_pnl;
2335
+ }
2336
+ if (diff <= tolerance) {
2337
+ console.log(`Converged! Optimal risk: ${mid_risk.toFixed(2)}`);
2338
+ break;
2339
+ }
2340
+ if (current_neg_pnl < highest_risk) {
2341
+ low_risk = mid_risk;
2342
+ } else {
2343
+ high_risk = mid_risk;
2344
+ }
2345
+ }
2346
+ const final_payload = {
2347
+ ...payload,
2348
+ risk: best_risk
2349
+ };
2350
+ const final_result = buildAppConfig(config, final_payload);
2351
+ return {
2352
+ optimal_risk: to_f(best_risk, "%.2f"),
2353
+ achieved_neg_pnl: to_f(best_neg_pnl, "%.2f"),
2354
+ target_neg_pnl: to_f(highest_risk, "%.2f"),
2355
+ difference: to_f(best_diff, "%.2f"),
2356
+ iterations,
2357
+ converged: best_diff <= tolerance,
2358
+ last_value: final_result.last_value,
2359
+ entries: final_result.entries,
2360
+ app_config: final_result
2361
+ };
2362
+ }
1737
2363
  function computeRiskReward(payload) {
1738
- const { app_config, entry, stop, risk_per_trade } = payload;
2364
+ const {
2365
+ app_config,
2366
+ entry,
2367
+ stop,
2368
+ risk_per_trade,
2369
+ target_loss,
2370
+ distribution,
2371
+ high_range,
2372
+ max_size
2373
+ } = payload;
1739
2374
  const kind = entry > stop ? "long" : "short";
1740
2375
  app_config.kind = kind;
1741
2376
  app_config.entry = entry;
1742
2377
  app_config.stop = stop;
1743
2378
  app_config.risk_per_trade = risk_per_trade;
1744
- const result = determineOptimumReward(app_config);
2379
+ const result = determineOptimumReward({
2380
+ app_config,
2381
+ target_loss,
2382
+ distribution,
2383
+ distribution_params: payload.distribution_params,
2384
+ high_range,
2385
+ max_size
2386
+ });
1745
2387
  return result;
1746
2388
  }
1747
2389
  function getRiskReward(payload) {
1748
- const { entry, stop, risk, global_config } = payload;
2390
+ const {
2391
+ high_range,
2392
+ max_size,
2393
+ entry,
2394
+ stop,
2395
+ risk,
2396
+ global_config,
2397
+ force_exact_risk = false,
2398
+ target_loss,
2399
+ distribution,
2400
+ risk_factor = 1
2401
+ } = payload;
1749
2402
  const { entries, last_value, ...app_config } = buildAppConfig(global_config, {
1750
2403
  entry,
1751
2404
  stop,
1752
2405
  risk_reward: 30,
1753
2406
  risk,
1754
- symbol: global_config.symbol
2407
+ symbol: global_config.symbol,
2408
+ distribution,
2409
+ distribution_params: payload.distribution_params
1755
2410
  });
1756
2411
  const risk_reward = computeRiskReward({
1757
2412
  app_config,
1758
2413
  entry,
1759
2414
  stop,
1760
- risk_per_trade: risk
2415
+ risk_per_trade: risk,
2416
+ high_range,
2417
+ target_loss,
2418
+ distribution,
2419
+ distribution_params: payload.distribution_params,
2420
+ max_size
1761
2421
  });
2422
+ if (force_exact_risk) {
2423
+ const new_risk_per_trade = determineOptimumRisk(global_config, {
2424
+ entry,
2425
+ stop,
2426
+ risk_reward,
2427
+ risk,
2428
+ symbol: global_config.symbol,
2429
+ distribution,
2430
+ distribution_params: payload.distribution_params
2431
+ }, {
2432
+ highest_risk: risk * risk_factor
2433
+ }).optimal_risk;
2434
+ return { risk: new_risk_per_trade, risk_reward };
2435
+ }
1762
2436
  return risk_reward;
1763
2437
  }
1764
2438
  function computeProfitDetail(payload) {
1765
2439
  const {
1766
2440
  focus_position,
1767
2441
  strategy,
2442
+ pnl,
1768
2443
  price_places = "%.1f",
1769
2444
  reduce_position,
1770
2445
  decimal_places,
1771
- reverse_position
2446
+ reverse_position,
2447
+ full_ratio = 1
1772
2448
  } = payload;
1773
- let reward_factor = strategy.reward_factor;
1774
- let risk = strategy.risk;
1775
- if (strategy.max_reward_factor === 0) {
1776
- reward_factor = strategy.reward_factor;
1777
- }
1778
- if (focus_position.avg_qty >= focus_position.quantity && strategy.max_reward_factor) {
1779
- reward_factor = to_f(focus_position.quantity * strategy.max_reward_factor / focus_position.avg_qty, "%.4f");
1780
- } else {
1781
- reward_factor = strategy.reward_factor;
1782
- }
1783
- const full_pnl = reward_factor * risk;
1784
- const profit_percent = to_f(full_pnl * 100 / (focus_position.avg_price * focus_position.avg_qty), "%.4f");
1785
- const pnl = to_f(focus_position.entry * focus_position.quantity * profit_percent / 100, "%.2f");
2449
+ let reward_factor = strategy?.reward_factor || 1;
2450
+ const profit_percent = to_f(pnl * 100 / (focus_position.avg_price * focus_position.avg_qty), "%.4f");
1786
2451
  const diff = pnl / focus_position.quantity;
1787
2452
  const sell_price = to_f(focus_position.kind === "long" ? focus_position.entry + diff : focus_position.entry - diff, price_places);
1788
2453
  let loss = 0;
2454
+ let full_loss = 0;
1789
2455
  let expected_loss = 0;
1790
2456
  let quantity = 0;
1791
2457
  let new_pnl = pnl;
@@ -1793,6 +2459,8 @@ function computeProfitDetail(payload) {
1793
2459
  loss = Math.abs(reduce_position.entry - sell_price) * reduce_position.quantity;
1794
2460
  const ratio = pnl / loss;
1795
2461
  quantity = to_f(reduce_position.quantity * ratio, decimal_places);
2462
+ expected_loss = to_f(Math.abs(reduce_position.entry - sell_price) * quantity, "%.2f");
2463
+ full_loss = Math.abs(reduce_position.avg_price - sell_price) * reduce_position.avg_qty * full_ratio;
1796
2464
  }
1797
2465
  if (reverse_position) {
1798
2466
  expected_loss = Math.abs(reverse_position.avg_price - sell_price) * reverse_position.avg_qty;
@@ -1801,12 +2469,13 @@ function computeProfitDetail(payload) {
1801
2469
  return {
1802
2470
  pnl: new_pnl,
1803
2471
  loss: to_f(expected_loss, "%.2f"),
2472
+ full_loss: to_f(full_loss, "%.2f"),
1804
2473
  original_pnl: pnl,
1805
2474
  reward_factor,
1806
2475
  profit_percent,
1807
2476
  kind: focus_position.kind,
1808
2477
  sell_price: to_f(sell_price, price_places),
1809
- quantity,
2478
+ quantity: quantity * full_ratio,
1810
2479
  price_places,
1811
2480
  decimal_places
1812
2481
  };
@@ -1815,10 +2484,26 @@ function generateGapTp(payload) {
1815
2484
  const {
1816
2485
  long,
1817
2486
  short,
1818
- factor = 1,
2487
+ factor: factor_value = 0,
2488
+ risk: desired_risk,
2489
+ sell_factor = 1,
2490
+ kind,
1819
2491
  price_places = "%.1f",
1820
2492
  decimal_places = "%.3f"
1821
2493
  } = payload;
2494
+ if (!factor_value && !desired_risk) {
2495
+ throw new Error("Either factor or risk must be provided");
2496
+ }
2497
+ if (desired_risk && !kind) {
2498
+ throw new Error("Kind must be provided when risk is provided");
2499
+ }
2500
+ let factor = factor_value || calculate_factor({
2501
+ long,
2502
+ short,
2503
+ risk: desired_risk,
2504
+ kind,
2505
+ sell_factor
2506
+ });
1822
2507
  const gap = Math.abs(long.entry - short.entry);
1823
2508
  const max_quantity = Math.max(long.quantity, short.quantity);
1824
2509
  const gapLoss = gap * max_quantity;
@@ -1828,14 +2513,36 @@ function generateGapTp(payload) {
1828
2513
  const shortTp = to_f((1 + shortPercent) ** -1 * short.entry, price_places);
1829
2514
  const shortToReduce = to_f(Math.abs(longTp - long.entry) * long.quantity, "%.1f");
1830
2515
  const longToReduce = to_f(Math.abs(shortTp - short.entry) * short.quantity, "%.1f");
1831
- const short_quantity_to_sell = determine_amount_to_sell2(short.entry, short.quantity, longTp, shortToReduce, "short", decimal_places);
1832
- const long_quantity_to_sell = determine_amount_to_sell2(long.entry, long.quantity, shortTp, longToReduce, "long", decimal_places);
2516
+ const actualShortReduce = to_f(shortToReduce * sell_factor, "%.1f");
2517
+ const actualLongReduce = to_f(longToReduce * sell_factor, "%.1f");
2518
+ const short_quantity_to_sell = determine_amount_to_sell2(short.entry, short.quantity, longTp, actualShortReduce, "short", decimal_places);
2519
+ const long_quantity_to_sell = determine_amount_to_sell2(long.entry, long.quantity, shortTp, actualLongReduce, "long", decimal_places);
2520
+ const risk_amount_short = to_f(shortToReduce - actualShortReduce, "%.2f");
2521
+ const risk_amount_long = to_f(longToReduce - actualLongReduce, "%.2f");
2522
+ const profit_percent_long = to_f(shortToReduce * 100 / (long.entry * long.quantity), "%.4f");
2523
+ const profit_percent_short = to_f(longToReduce * 100 / (short.entry * short.quantity), "%.4f");
1833
2524
  return {
1834
- long_tp: longTp,
1835
- short_tp: shortTp,
1836
- short_to_reduce: shortToReduce,
1837
- long_to_reduce: longToReduce,
1838
- to_sell: {
2525
+ profit_percent: {
2526
+ long: profit_percent_long,
2527
+ short: profit_percent_short
2528
+ },
2529
+ risk: {
2530
+ short: risk_amount_short,
2531
+ long: risk_amount_long
2532
+ },
2533
+ take_profit: {
2534
+ long: longTp,
2535
+ short: shortTp
2536
+ },
2537
+ to_reduce: {
2538
+ short: actualShortReduce,
2539
+ long: actualLongReduce
2540
+ },
2541
+ full_reduce: {
2542
+ short: shortToReduce,
2543
+ long: longToReduce
2544
+ },
2545
+ sell_quantity: {
1839
2546
  short: short_quantity_to_sell,
1840
2547
  long: long_quantity_to_sell
1841
2548
  },
@@ -1843,6 +2550,370 @@ function generateGapTp(payload) {
1843
2550
  gap_loss: to_f(gapLoss, "%.2f")
1844
2551
  };
1845
2552
  }
2553
+ function calculate_factor(payload) {
2554
+ const {
2555
+ long,
2556
+ short,
2557
+ risk: desired_risk,
2558
+ kind,
2559
+ sell_factor,
2560
+ places = "%.4f"
2561
+ } = payload;
2562
+ const gap = Math.abs(long.entry - short.entry);
2563
+ const max_quantity = Math.max(long.quantity, short.quantity);
2564
+ const gapLoss = gap * max_quantity;
2565
+ let calculated_factor;
2566
+ const long_notional = long.entry * long.quantity;
2567
+ const short_notional = short.entry * short.quantity;
2568
+ const target_to_reduce = desired_risk / (1 - sell_factor);
2569
+ if (kind === "short") {
2570
+ const calculate_longPercent = target_to_reduce / long_notional;
2571
+ calculated_factor = calculate_longPercent * short_notional / gapLoss;
2572
+ } else {
2573
+ const calculated_shortPercent = target_to_reduce / (short_notional - target_to_reduce);
2574
+ calculated_factor = calculated_shortPercent * long_notional / gapLoss;
2575
+ }
2576
+ calculated_factor = to_f(calculated_factor, places);
2577
+ return calculated_factor;
2578
+ }
2579
+ function calculateFactorFromTakeProfit(payload) {
2580
+ const { long, short, knownTp, tpType, price_places = "%.4f" } = payload;
2581
+ const gap = Math.abs(long.entry - short.entry);
2582
+ const max_quantity = Math.max(long.quantity, short.quantity);
2583
+ const gapLoss = gap * max_quantity;
2584
+ if (gapLoss === 0) {
2585
+ return 0;
2586
+ }
2587
+ let factor;
2588
+ if (tpType === "long") {
2589
+ const longPercent = knownTp / long.entry - 1;
2590
+ factor = longPercent * short.entry * short.quantity / gapLoss;
2591
+ } else {
2592
+ const shortPercent = short.entry / knownTp - 1;
2593
+ factor = shortPercent * long.entry * long.quantity / gapLoss;
2594
+ }
2595
+ return to_f(factor, price_places);
2596
+ }
2597
+ function calculateFactorFromSellQuantity(payload) {
2598
+ const {
2599
+ long,
2600
+ short,
2601
+ knownSellQuantity,
2602
+ sellType,
2603
+ sell_factor = 1,
2604
+ price_places = "%.4f"
2605
+ } = payload;
2606
+ if (knownSellQuantity < 0.00001) {
2607
+ return 0;
2608
+ }
2609
+ const gap = Math.abs(long.entry - short.entry);
2610
+ const max_quantity = Math.max(long.quantity, short.quantity);
2611
+ const gapLoss = gap * max_quantity;
2612
+ if (gapLoss === 0) {
2613
+ return 0;
2614
+ }
2615
+ let low_factor = 0.001;
2616
+ let high_factor = 1;
2617
+ let best_factor = 0;
2618
+ let best_diff = Infinity;
2619
+ const tolerance = 0.00001;
2620
+ const max_iterations = 150;
2621
+ let expansions = 0;
2622
+ const max_expansions = 15;
2623
+ let iterations = 0;
2624
+ while (iterations < max_iterations) {
2625
+ iterations++;
2626
+ const mid_factor = (low_factor + high_factor) / 2;
2627
+ const testResult = generateGapTp({
2628
+ long,
2629
+ short,
2630
+ factor: mid_factor,
2631
+ sell_factor,
2632
+ price_places: "%.8f",
2633
+ decimal_places: "%.8f"
2634
+ });
2635
+ const testSellQty = sellType === "long" ? testResult.sell_quantity.long : testResult.sell_quantity.short;
2636
+ const diff = Math.abs(testSellQty - knownSellQuantity);
2637
+ if (diff < best_diff) {
2638
+ best_diff = diff;
2639
+ best_factor = mid_factor;
2640
+ }
2641
+ if (diff < tolerance) {
2642
+ return to_f(mid_factor, price_places);
2643
+ }
2644
+ if (testSellQty < knownSellQuantity) {
2645
+ low_factor = mid_factor;
2646
+ if (mid_factor > high_factor * 0.9 && expansions < max_expansions) {
2647
+ const ratio = knownSellQuantity / testSellQty;
2648
+ if (ratio > 2) {
2649
+ high_factor = high_factor * Math.min(ratio, 10);
2650
+ } else {
2651
+ high_factor = high_factor * 2;
2652
+ }
2653
+ expansions++;
2654
+ continue;
2655
+ }
2656
+ } else {
2657
+ high_factor = mid_factor;
2658
+ }
2659
+ if (Math.abs(high_factor - low_factor) < 0.00001 && diff > tolerance) {
2660
+ if (testSellQty < knownSellQuantity && expansions < max_expansions) {
2661
+ high_factor = high_factor * 1.1;
2662
+ expansions++;
2663
+ continue;
2664
+ } else {
2665
+ break;
2666
+ }
2667
+ }
2668
+ }
2669
+ return to_f(best_factor, price_places);
2670
+ }
2671
+ function determineRewardFactor(payload) {
2672
+ const { quantity, avg_qty, minimum_pnl, risk } = payload;
2673
+ const reward_factor = minimum_pnl / risk;
2674
+ const quantity_ratio = quantity / avg_qty;
2675
+ return to_f(reward_factor / quantity_ratio, "%.4f");
2676
+ }
2677
+ function getHedgeZone(payload) {
2678
+ const {
2679
+ reward_factor: _reward_factor,
2680
+ symbol_config,
2681
+ risk,
2682
+ position: position2,
2683
+ risk_factor = 1,
2684
+ support
2685
+ } = payload;
2686
+ const kind = position2.kind;
2687
+ let reward_factor = _reward_factor;
2688
+ if (support) {
2689
+ const _result = getOptimumHedgeFactor({
2690
+ target_support: support,
2691
+ symbol_config,
2692
+ risk,
2693
+ position: position2
2694
+ });
2695
+ reward_factor = Number(_result.reward_factor);
2696
+ }
2697
+ const take_profit = position2.tp?.price;
2698
+ const tp_diff = Math.abs(take_profit - position2.entry);
2699
+ const quantity = position2.quantity;
2700
+ const diff = risk / quantity;
2701
+ let new_take_profit = kind === "long" ? to_f(position2.entry + diff, symbol_config.price_places) : to_f(position2.entry - diff, symbol_config.price_places);
2702
+ let base_factor = to_f(Math.max(tp_diff, diff) / (Math.min(tp_diff, diff) || 1), "%.3f");
2703
+ let factor = reward_factor || base_factor;
2704
+ const new_risk = risk * factor * risk_factor;
2705
+ const stop_loss_diff = new_risk / quantity;
2706
+ new_take_profit = kind === "long" ? to_f(position2.entry + stop_loss_diff, symbol_config.price_places) : to_f(position2.entry - stop_loss_diff, symbol_config.price_places);
2707
+ const stop_loss = kind === "long" ? to_f(position2.entry - stop_loss_diff, symbol_config.price_places) : to_f(position2.entry + stop_loss_diff, symbol_config.price_places);
2708
+ const profit_percent = new_risk * 100 / (position2.entry * position2.quantity);
2709
+ return {
2710
+ support: Math.min(new_take_profit, stop_loss),
2711
+ resistance: Math.max(new_take_profit, stop_loss),
2712
+ risk: to_f(new_risk, "%.2f"),
2713
+ profit_percent: to_f(profit_percent, "%.2f")
2714
+ };
2715
+ }
2716
+ function getOptimumHedgeFactor(payload) {
2717
+ const {
2718
+ target_support,
2719
+ max_iterations = 50,
2720
+ min_factor = 0.1,
2721
+ max_factor = 20,
2722
+ symbol_config,
2723
+ risk,
2724
+ position: position2
2725
+ } = payload;
2726
+ const current_price = position2.entry;
2727
+ const tolerance = current_price > 100 ? 0.5 : current_price > 1 ? 0.01 : 0.001;
2728
+ let low = min_factor;
2729
+ let high = max_factor;
2730
+ let best_factor = low;
2731
+ let best_diff = Infinity;
2732
+ for (let iteration = 0;iteration < max_iterations; iteration++) {
2733
+ const mid_factor = (low + high) / 2;
2734
+ const hedge_zone = getHedgeZone({
2735
+ reward_factor: mid_factor,
2736
+ symbol_config,
2737
+ risk,
2738
+ position: position2
2739
+ });
2740
+ const current_support = hedge_zone.support;
2741
+ const diff = Math.abs(current_support - target_support);
2742
+ if (diff < best_diff) {
2743
+ best_diff = diff;
2744
+ best_factor = mid_factor;
2745
+ }
2746
+ if (diff <= tolerance) {
2747
+ return {
2748
+ reward_factor: to_f(mid_factor, "%.4f"),
2749
+ achieved_support: to_f(current_support, symbol_config.price_places),
2750
+ target_support: to_f(target_support, symbol_config.price_places),
2751
+ difference: to_f(diff, symbol_config.price_places),
2752
+ iterations: iteration + 1
2753
+ };
2754
+ }
2755
+ if (current_support > target_support) {
2756
+ low = mid_factor;
2757
+ } else {
2758
+ high = mid_factor;
2759
+ }
2760
+ if (Math.abs(high - low) < 0.0001) {
2761
+ break;
2762
+ }
2763
+ }
2764
+ const final_hedge_zone = getHedgeZone({
2765
+ symbol_config,
2766
+ risk,
2767
+ position: position2,
2768
+ reward_factor: best_factor
2769
+ });
2770
+ return {
2771
+ reward_factor: to_f(best_factor, "%.4f"),
2772
+ achieved_support: to_f(final_hedge_zone.support, symbol_config.price_places),
2773
+ target_support: to_f(target_support, symbol_config.price_places),
2774
+ difference: to_f(best_diff, symbol_config.price_places),
2775
+ iterations: max_iterations,
2776
+ converged: best_diff <= tolerance
2777
+ };
2778
+ }
2779
+ function determineCompoundLongTrade(payload) {
2780
+ const {
2781
+ focus_short_position,
2782
+ focus_long_position,
2783
+ shortConfig,
2784
+ global_config,
2785
+ rr = 1
2786
+ } = payload;
2787
+ const short_app_config = buildAppConfig(global_config, {
2788
+ entry: shortConfig.entry,
2789
+ stop: shortConfig.stop,
2790
+ risk_reward: shortConfig.risk_reward,
2791
+ risk: shortConfig.risk,
2792
+ symbol: shortConfig.symbol
2793
+ });
2794
+ const short_max_size = short_app_config.last_value.avg_size;
2795
+ const start_risk = Math.abs(short_app_config.last_value.neg_pnl) * rr;
2796
+ const short_profit = short_app_config.last_value.avg_size * short_app_config.last_value.avg_entry * shortConfig.profit_percent / 100;
2797
+ const diff = short_profit * rr / short_app_config.last_value.avg_size;
2798
+ const support = Math.abs(short_app_config.last_value.avg_entry - diff);
2799
+ const resistance = focus_short_position.next_order || focus_long_position.take_profit;
2800
+ console.log({ support, resistance, short_profit });
2801
+ const result = getRiskReward({
2802
+ entry: resistance,
2803
+ stop: support,
2804
+ risk: start_risk,
2805
+ global_config,
2806
+ force_exact_risk: true
2807
+ });
2808
+ const long_app_config = buildAppConfig(global_config, {
2809
+ entry: resistance,
2810
+ stop: support,
2811
+ risk_reward: result.risk_reward,
2812
+ risk: result.risk,
2813
+ symbol: shortConfig.symbol
2814
+ });
2815
+ const long_profit_percent = start_risk * 2 * 100 / (long_app_config.last_value.avg_size * long_app_config.last_value.avg_entry);
2816
+ return {
2817
+ start_risk,
2818
+ short_profit,
2819
+ support: to_f(support, global_config.price_places),
2820
+ resistance: to_f(resistance, global_config.price_places),
2821
+ long_v: long_app_config.last_value,
2822
+ profit_percent: to_f(long_profit_percent, "%.3f"),
2823
+ result,
2824
+ short_max_size
2825
+ };
2826
+ }
2827
+ function generateOppositeTradeConfig(payload) {
2828
+ const {
2829
+ kind,
2830
+ entry,
2831
+ quantity,
2832
+ target_pnl,
2833
+ ratio = 0.5,
2834
+ global_config
2835
+ } = payload;
2836
+ const diff = target_pnl / quantity;
2837
+ const tp = kind === "long" ? entry + diff : entry - diff;
2838
+ const stop = kind === "long" ? entry - diff : entry + diff;
2839
+ const opposite_pnl = target_pnl * ratio;
2840
+ const app_config = constructAppConfig({
2841
+ account: {
2842
+ expand: {
2843
+ b_config: {
2844
+ entry,
2845
+ stop,
2846
+ symbol: global_config.symbol,
2847
+ risk: target_pnl
2848
+ }
2849
+ }
2850
+ },
2851
+ global_config,
2852
+ distribution_config: {}
2853
+ });
2854
+ const risk_reward = computeRiskReward({
2855
+ app_config,
2856
+ entry: stop,
2857
+ stop: tp,
2858
+ risk_per_trade: opposite_pnl,
2859
+ target_loss: opposite_pnl
2860
+ });
2861
+ return {
2862
+ entry: to_f(stop, global_config.price_places),
2863
+ stop: to_f(tp, global_config.price_places),
2864
+ risk: to_f(opposite_pnl, "%.2f"),
2865
+ risk_reward
2866
+ };
2867
+ }
2868
+ function constructAppConfig(payload) {
2869
+ const { account, global_config, kelly_config, distribution_config } = payload;
2870
+ const config = account.expand?.b_config;
2871
+ if (!config) {
2872
+ return null;
2873
+ }
2874
+ const kelly = config.kelly;
2875
+ const options = {
2876
+ entry: config?.entry,
2877
+ stop: config?.stop,
2878
+ risk_reward: config?.risk_reward,
2879
+ risk: config?.risk,
2880
+ symbol: account.symbol,
2881
+ use_kelly: kelly_config?.use_kelly ?? kelly?.use_kelly,
2882
+ kelly_confidence_factor: kelly_config?.kelly_confidence_factor ?? kelly?.kelly_confidence_factor,
2883
+ kelly_minimum_risk: kelly_config?.kelly_minimum_risk ?? kelly?.kelly_minimum_risk,
2884
+ kelly_prediction_model: kelly_config?.kelly_prediction_model ?? kelly?.kelly_prediction_model,
2885
+ distribution: distribution_config?.distribution ?? config?.distribution,
2886
+ distribution_params: distribution_config?.distribution_params ?? config?.distribution_params
2887
+ };
2888
+ const { entries: _entries, ...appConfig } = buildAppConfig(global_config, options);
2889
+ return appConfig;
2890
+ }
2891
+ function generateDangerousConfig(payload) {
2892
+ const { account, global_config, config } = payload;
2893
+ const app_config = constructAppConfig({
2894
+ account,
2895
+ global_config,
2896
+ kelly_config: {},
2897
+ distribution_config: {}
2898
+ });
2899
+ const { optimal_risk, optimal_stop } = getOptimumStopAndRisk(app_config, {
2900
+ max_size: config.quantity,
2901
+ target_stop: config.stop
2902
+ });
2903
+ const optimumRiskReward = computeRiskReward({
2904
+ app_config,
2905
+ entry: config.entry,
2906
+ stop: optimal_stop,
2907
+ risk_per_trade: optimal_risk,
2908
+ target_loss: optimal_risk
2909
+ });
2910
+ return {
2911
+ entry: config.entry,
2912
+ risk: optimal_risk,
2913
+ stop: optimal_stop,
2914
+ risk_reward: optimumRiskReward
2915
+ };
2916
+ }
1846
2917
  // src/helpers/strategy.ts
1847
2918
  class Strategy {
1848
2919
  position;
@@ -2245,11 +3316,14 @@ class Strategy {
2245
3316
  return { ...app_config, avg, loss: -expected_loss, profit_percent };
2246
3317
  }
2247
3318
  identifyGapConfig(payload) {
2248
- const { factor = 1 } = payload;
3319
+ const { factor, sell_factor = 1, kind, risk } = payload;
2249
3320
  return generateGapTp({
2250
3321
  long: this.position.long,
2251
3322
  short: this.position.short,
2252
3323
  factor,
3324
+ sell_factor,
3325
+ kind,
3326
+ risk,
2253
3327
  decimal_places: this.config.global_config.decimal_places,
2254
3328
  price_places: this.config.global_config.price_places
2255
3329
  });
@@ -2265,6 +3339,7 @@ class Strategy {
2265
3339
  avg_price: focus_position.avg_price,
2266
3340
  avg_qty: focus_position.avg_qty
2267
3341
  },
3342
+ pnl: this.pnl(kind),
2268
3343
  strategy: {
2269
3344
  reward_factor,
2270
3345
  max_reward_factor,
@@ -2273,7 +3348,535 @@ class Strategy {
2273
3348
  });
2274
3349
  return result;
2275
3350
  }
3351
+ simulateGapReduction(payload) {
3352
+ const {
3353
+ factor,
3354
+ direction,
3355
+ sell_factor = 1,
3356
+ iterations = 10,
3357
+ risk: desired_risk,
3358
+ kind
3359
+ } = payload;
3360
+ const results = [];
3361
+ let params = {
3362
+ long: this.position.long,
3363
+ short: this.position.short,
3364
+ config: this.config
3365
+ };
3366
+ for (let i = 0;i < iterations; i++) {
3367
+ const instance = new Strategy(params);
3368
+ const { profit_percent, risk, take_profit, sell_quantity, gap_loss } = instance.identifyGapConfig({
3369
+ factor,
3370
+ sell_factor,
3371
+ kind,
3372
+ risk: desired_risk
3373
+ });
3374
+ const to_add = {
3375
+ profit_percent,
3376
+ risk,
3377
+ take_profit,
3378
+ sell_quantity,
3379
+ gap_loss,
3380
+ position: {
3381
+ long: {
3382
+ entry: this.to_f(params.long.entry),
3383
+ quantity: this.to_df(params.long.quantity)
3384
+ },
3385
+ short: {
3386
+ entry: this.to_f(params.short.entry),
3387
+ quantity: this.to_df(params.short.quantity)
3388
+ }
3389
+ }
3390
+ };
3391
+ const sell_kind = direction == "long" ? "short" : "long";
3392
+ if (sell_quantity[sell_kind] <= 0) {
3393
+ break;
3394
+ }
3395
+ results.push(to_add);
3396
+ const remaining = this.to_df(params[sell_kind].quantity - sell_quantity[sell_kind]);
3397
+ if (remaining <= 0) {
3398
+ break;
3399
+ }
3400
+ params[sell_kind].quantity = remaining;
3401
+ params[direction] = {
3402
+ entry: take_profit[direction],
3403
+ quantity: remaining
3404
+ };
3405
+ }
3406
+ const last_gap_loss = results.at(-1)?.gap_loss;
3407
+ const last_tp = results.at(-1)?.take_profit[direction];
3408
+ const entry = this.position[direction].entry;
3409
+ const quantity = this.to_df(Math.abs(last_tp - entry) / last_gap_loss);
3410
+ return {
3411
+ results,
3412
+ quantity
3413
+ };
3414
+ }
3415
+ }
3416
+ // src/helpers/compound.ts
3417
+ function buildTrades(payload) {
3418
+ const { appConfig, settings, kind } = payload;
3419
+ const kelly_config = settings.kelly;
3420
+ const distribution_params = settings.distribution_params;
3421
+ const current_app_config = { ...appConfig[kind] };
3422
+ const entryNum = parseFloat(settings.entry);
3423
+ const stopNum = parseFloat(settings.stop);
3424
+ current_app_config.entry = entryNum;
3425
+ current_app_config.stop = stopNum;
3426
+ current_app_config.risk_per_trade = parseFloat(settings.risk);
3427
+ current_app_config.risk_reward = parseFloat(settings.risk_reward);
3428
+ current_app_config.kind = kind;
3429
+ current_app_config.kelly = kelly_config;
3430
+ current_app_config.distribution_params = distribution_params;
3431
+ const options = {
3432
+ take_profit: null,
3433
+ entry: current_app_config.entry,
3434
+ stop: current_app_config.stop,
3435
+ raw_instance: null,
3436
+ risk: current_app_config.risk_per_trade,
3437
+ no_of_trades: undefined,
3438
+ risk_reward: current_app_config.risk_reward,
3439
+ kind: current_app_config.kind,
3440
+ increase: true,
3441
+ gap: current_app_config.gap,
3442
+ rr: current_app_config.rr,
3443
+ price_places: current_app_config.price_places,
3444
+ decimal_places: current_app_config.decimal_places,
3445
+ use_kelly: kelly_config?.use_kelly,
3446
+ kelly_confidence_factor: kelly_config?.kelly_confidence_factor,
3447
+ kelly_minimum_risk: kelly_config?.kelly_minimum_risk,
3448
+ kelly_prediction_model: kelly_config?.kelly_prediction_model,
3449
+ kelly_func: kelly_config?.kelly_func,
3450
+ distribution: settings.distribution,
3451
+ distribution_params: settings.distribution_params
3452
+ };
3453
+ if (kind === "long" && entryNum <= stopNum) {
3454
+ return [];
3455
+ }
3456
+ if (kind === "short" && entryNum >= stopNum) {
3457
+ return [];
3458
+ }
3459
+ try {
3460
+ const generatedTrades = sortedBuildConfig(current_app_config, options);
3461
+ return generatedTrades ?? [];
3462
+ } catch (error) {
3463
+ console.error("Error generating orders:", error);
3464
+ return [];
3465
+ }
3466
+ }
3467
+ function generateSummary({
3468
+ trades,
3469
+ fee_percent = 0.05,
3470
+ anchor
3471
+ }) {
3472
+ const avg_entry = trades[0].avg_entry;
3473
+ const avg_size = trades[0].avg_size;
3474
+ const expected_fee = avg_entry * avg_size * fee_percent / 100;
3475
+ return {
3476
+ first_entry: trades.at(-1).entry,
3477
+ last_entry: trades[0].entry,
3478
+ quantity: avg_size,
3479
+ entry: avg_entry,
3480
+ loss: trades[0].neg_pnl,
3481
+ number_of_trades: trades.length,
3482
+ fee: to_f(expected_fee, "%.2f"),
3483
+ anchor_pnl: anchor?.target_pnl
3484
+ };
3485
+ }
3486
+ function helperFuncToBuildTrades({
3487
+ custom_b_config,
3488
+ symbol_config,
3489
+ app_config_kind,
3490
+ appConfig,
3491
+ force_exact_risk = true
3492
+ }) {
3493
+ const risk = custom_b_config.risk * (custom_b_config.risk_factor || 1);
3494
+ let result = getRiskReward({
3495
+ entry: custom_b_config.entry,
3496
+ stop: custom_b_config.stop,
3497
+ risk,
3498
+ global_config: symbol_config,
3499
+ force_exact_risk,
3500
+ target_loss: custom_b_config.risk * (custom_b_config.risk_factor || 1),
3501
+ distribution: custom_b_config.distribution,
3502
+ distribution_params: custom_b_config.distribution_params
3503
+ });
3504
+ if (!force_exact_risk) {
3505
+ result = {
3506
+ risk_reward: result,
3507
+ risk
3508
+ };
3509
+ }
3510
+ const trades = result.risk_reward ? buildTrades({
3511
+ appConfig: { [app_config_kind]: appConfig },
3512
+ kind: app_config_kind,
3513
+ settings: {
3514
+ entry: custom_b_config.entry,
3515
+ stop: custom_b_config.stop,
3516
+ risk: result.risk || custom_b_config.risk,
3517
+ risk_reward: result.risk_reward,
3518
+ distribution: custom_b_config.distribution,
3519
+ distribution_params: custom_b_config.distribution_params
3520
+ }
3521
+ }) : [];
3522
+ const summary = trades.length > 0 ? generateSummary({ trades }) : {};
3523
+ return { trades, result, summary };
3524
+ }
3525
+ function constructAppConfig2({
3526
+ config,
3527
+ global_config
3528
+ }) {
3529
+ const options = {
3530
+ entry: config?.entry,
3531
+ stop: config?.stop,
3532
+ risk_reward: config?.risk_reward,
3533
+ risk: config?.risk,
3534
+ symbol: config.symbol
3535
+ };
3536
+ const { entries: _entries, ...appConfig } = buildAppConfig(global_config, options);
3537
+ return appConfig;
3538
+ }
3539
+ function buildWithOptimumReward({
3540
+ config,
3541
+ settings,
3542
+ global_config,
3543
+ force_exact
3544
+ }) {
3545
+ const kind = config.entry > config.stop ? "long" : "short";
3546
+ let stop = settings.stop;
3547
+ let entry = settings.entry;
3548
+ const risk = settings.risk;
3549
+ const stop_ratio = settings.stop_ratio || 1;
3550
+ const distribution = settings.distribution || config?.distribution;
3551
+ const distribution_params = settings.distribution_params || config?.distribution_params;
3552
+ const custom_b_config = {
3553
+ entry,
3554
+ stop,
3555
+ risk,
3556
+ distribution,
3557
+ distribution_params
3558
+ };
3559
+ const appConfig = constructAppConfig2({
3560
+ config,
3561
+ global_config
3562
+ });
3563
+ const { trades, summary, result } = helperFuncToBuildTrades({
3564
+ custom_b_config,
3565
+ app_config_kind: kind,
3566
+ appConfig,
3567
+ symbol_config: global_config,
3568
+ force_exact_risk: force_exact
3569
+ });
3570
+ const adjusted_size = summary.quantity;
3571
+ const symbol_config = global_config;
3572
+ const entryDetails = {
3573
+ entry: to_f(custom_b_config.entry, symbol_config.price_places),
3574
+ stop: to_f(custom_b_config.stop, symbol_config.price_places),
3575
+ risk: to_f(result.risk, "%.2f"),
3576
+ risk_reward: result.risk_reward,
3577
+ avg_entry: to_f(summary.entry, symbol_config.price_places),
3578
+ avg_size: to_f(adjusted_size, symbol_config.decimal_places),
3579
+ first_entry: to_f(summary.first_entry, symbol_config.price_places),
3580
+ pnl: to_f(custom_b_config.risk, "%.2f"),
3581
+ fee: to_f(summary.fee, "%.2f"),
3582
+ loss: to_f(summary.loss, "%.2f"),
3583
+ last_entry: to_f(summary.last_entry, symbol_config.price_places),
3584
+ margin: to_f(summary.entry * adjusted_size / symbol_config.leverage, "%.2f")
3585
+ };
3586
+ return {
3587
+ trades,
3588
+ summary: entryDetails,
3589
+ config: {
3590
+ ...custom_b_config,
3591
+ ...result,
3592
+ stop_ratio
3593
+ },
3594
+ stop_order: {
3595
+ quantity: entryDetails.avg_size * stop_ratio,
3596
+ price: entryDetails.stop
3597
+ },
3598
+ kind
3599
+ };
3600
+ }
3601
+ function generateOppositeOptimum({
3602
+ config,
3603
+ global_config,
3604
+ settings,
3605
+ ratio = 1,
3606
+ distribution,
3607
+ distribution_params,
3608
+ risk_factor = 1
3609
+ }) {
3610
+ const configKind = config.entry > config.stop ? "long" : "short";
3611
+ if (configKind === "long" && config.entry > config.stop) {
3612
+ if (settings.stop <= settings.entry) {
3613
+ throw new Error("Invalid input: For long config positions, opposite settings must have stop > entry");
3614
+ }
3615
+ } else if (configKind === "short" && config.entry < config.stop) {
3616
+ if (settings.stop >= settings.entry) {
3617
+ throw new Error("Invalid input: For short config positions, opposite settings must have stop < entry");
3618
+ }
3619
+ }
3620
+ const kind = config.entry > config.stop ? "long" : "short";
3621
+ const app_config_kind = kind === "long" ? "short" : "long";
3622
+ let risk = settings.risk;
3623
+ const custom_b_config = {
3624
+ entry: settings.entry,
3625
+ stop: settings.stop,
3626
+ risk: risk * ratio,
3627
+ distribution: distribution || "inverse-exponential",
3628
+ distribution_params: distribution_params || config?.distribution_params,
3629
+ risk_factor
3630
+ };
3631
+ const appConfig = constructAppConfig2({
3632
+ config: {
3633
+ ...config,
3634
+ ...custom_b_config
3635
+ },
3636
+ global_config
3637
+ });
3638
+ const { result, trades, summary } = helperFuncToBuildTrades({
3639
+ custom_b_config,
3640
+ symbol_config: global_config,
3641
+ app_config_kind,
3642
+ appConfig
3643
+ });
3644
+ if (Object.keys(summary).length === 0) {
3645
+ return {
3646
+ trades,
3647
+ summary,
3648
+ config: custom_b_config,
3649
+ kind: app_config_kind
3650
+ };
3651
+ }
3652
+ const symbol_config = global_config;
3653
+ const entryDetails = {
3654
+ entry: to_f(custom_b_config.entry, symbol_config.price_places),
3655
+ stop: to_f(custom_b_config.stop, symbol_config.price_places),
3656
+ risk: to_f(result.risk, "%.2f"),
3657
+ risk_reward: result.risk_reward,
3658
+ avg_entry: to_f(summary.entry, symbol_config.price_places),
3659
+ avg_size: to_f(summary.quantity, symbol_config.decimal_places),
3660
+ first_entry: to_f(summary.first_entry, symbol_config.price_places),
3661
+ pnl: to_f(custom_b_config.risk, "%.2f"),
3662
+ fee: to_f(summary.fee, "%.2f"),
3663
+ loss: to_f(summary.loss, "%.2f"),
3664
+ last_entry: to_f(summary.last_entry, symbol_config.price_places),
3665
+ defaultEntry: settings.entry ? to_f(settings.entry, symbol_config.price_places) : null
3666
+ };
3667
+ return {
3668
+ trades,
3669
+ summary: entryDetails,
3670
+ config: {
3671
+ ...custom_b_config,
3672
+ ...result
3673
+ },
3674
+ kind: app_config_kind
3675
+ };
3676
+ }
3677
+ function defaultTradeFromCurrentState({
3678
+ config,
3679
+ global_config
3680
+ }) {
3681
+ const kind = config.entry > config.stop ? "long" : "short";
3682
+ const settings = {
3683
+ entry: config?.entry,
3684
+ stop: config?.stop,
3685
+ risk: config?.risk,
3686
+ distribution: config?.distribution,
3687
+ risk_reward: config?.risk_reward
3688
+ };
3689
+ const appConfig = constructAppConfig2({
3690
+ config,
3691
+ global_config
3692
+ });
3693
+ const trades = buildTrades({
3694
+ appConfig: { [kind]: appConfig },
3695
+ kind,
3696
+ settings
3697
+ });
3698
+ return {
3699
+ trades,
3700
+ summary: generateSummary({
3701
+ trades,
3702
+ fee_percent: global_config.fee_percent
3703
+ })
3704
+ };
3705
+ }
3706
+ function increaseTradeHelper({
3707
+ increase_qty,
3708
+ stop,
3709
+ config,
3710
+ global_config,
3711
+ style,
3712
+ entry,
3713
+ position: position2,
3714
+ stop_ratio = 1,
3715
+ distribution: default_distribution,
3716
+ distribution_params: default_distribution_params
3717
+ }) {
3718
+ const symbol_config = global_config;
3719
+ const kind = config.entry > config.stop ? "long" : "short";
3720
+ const distribution = default_distribution || config.distribution || "inverse-exponential";
3721
+ const distribution_params = default_distribution_params || config.distribution_params;
3722
+ const appConfig = constructAppConfig2({
3723
+ config,
3724
+ global_config
3725
+ });
3726
+ const currentState = defaultTradeFromCurrentState({
3727
+ config,
3728
+ global_config
3729
+ });
3730
+ const { optimal_risk, neg_pnl } = getOptimumStopAndRisk(appConfig, {
3731
+ max_size: increase_qty,
3732
+ target_stop: stop,
3733
+ distribution
3734
+ });
3735
+ if (neg_pnl === 0) {
3736
+ return {
3737
+ trades: [],
3738
+ summary: {},
3739
+ config: {},
3740
+ kind,
3741
+ current: currentState
3742
+ };
3743
+ }
3744
+ const custom_b_config = {
3745
+ entry,
3746
+ stop,
3747
+ risk: style === "minimum" ? Math.abs(neg_pnl) : optimal_risk,
3748
+ distribution,
3749
+ distribution_params
3750
+ };
3751
+ const { result, trades, summary } = helperFuncToBuildTrades({
3752
+ custom_b_config,
3753
+ symbol_config,
3754
+ appConfig,
3755
+ app_config_kind: kind
3756
+ });
3757
+ if (Object.keys(summary).length === 0) {
3758
+ return {
3759
+ trades,
3760
+ summary,
3761
+ config: {
3762
+ ...custom_b_config,
3763
+ ...result
3764
+ },
3765
+ kind,
3766
+ current: currentState
3767
+ };
3768
+ }
3769
+ const new_avg_values = determine_average_entry_and_size([
3770
+ {
3771
+ price: position2.entry,
3772
+ quantity: position2.quantity
3773
+ },
3774
+ {
3775
+ price: summary?.entry,
3776
+ quantity: summary?.quantity
3777
+ }
3778
+ ], symbol_config.decimal_places, symbol_config.price_places);
3779
+ summary.entry = new_avg_values.entry;
3780
+ summary.quantity = new_avg_values.quantity;
3781
+ const loss = Math.abs(summary.entry - custom_b_config.stop) * summary.quantity;
3782
+ const entryDetails = {
3783
+ entry: to_f(custom_b_config.entry, symbol_config.price_places),
3784
+ stop: to_f(custom_b_config.stop, symbol_config.price_places),
3785
+ risk: to_f(result.risk, symbol_config.price_places),
3786
+ risk_reward: result.risk_reward,
3787
+ avg_entry: to_f(summary.entry, symbol_config.price_places),
3788
+ avg_size: to_f(summary.quantity, symbol_config.decimal_places),
3789
+ first_entry: to_f(summary.first_entry, symbol_config.price_places),
3790
+ pnl: to_f(custom_b_config.risk, "%.2f"),
3791
+ fee: to_f(summary.fee, "%.2f"),
3792
+ loss: to_f(loss, "%.2f"),
3793
+ last_entry: to_f(summary.last_entry, symbol_config.price_places),
3794
+ margin: to_f(summary.entry * summary.quantity / global_config.leverage, "%.2f")
3795
+ };
3796
+ return {
3797
+ trades,
3798
+ summary: entryDetails,
3799
+ stop_order: {
3800
+ quantity: entryDetails.avg_size * stop_ratio,
3801
+ price: entryDetails.stop
3802
+ },
3803
+ config: {
3804
+ ...custom_b_config,
3805
+ ...result
3806
+ },
3807
+ kind,
3808
+ current: currentState
3809
+ };
2276
3810
  }
3811
+ function generatePositionIncreaseTrade({
3812
+ account,
3813
+ zoneAccount,
3814
+ ratio = 0.1,
3815
+ config,
3816
+ global_config,
3817
+ style = "minimum",
3818
+ distribution = "inverse-exponential",
3819
+ distribution_params
3820
+ }) {
3821
+ const kind = config.entry > config.stop ? "long" : "short";
3822
+ const target_max_quantity = kind === "long" ? account.short.quantity : account.long.quantity;
3823
+ const increase_qty = target_max_quantity * ratio;
3824
+ const entry = zoneAccount.entry;
3825
+ const stop = zoneAccount.stop;
3826
+ return increaseTradeHelper({
3827
+ config,
3828
+ position: account[kind],
3829
+ global_config,
3830
+ entry,
3831
+ stop,
3832
+ style,
3833
+ increase_qty,
3834
+ distribution,
3835
+ distribution_params
3836
+ });
3837
+ }
3838
+ function determineHedgeTradeToPlace({
3839
+ position: position2,
3840
+ config,
3841
+ global_config,
3842
+ profit_risk = 200,
3843
+ allowable_loss = 1000
3844
+ }) {
3845
+ const diff = profit_risk / position2.quantity;
3846
+ const kind = position2.kind === "long" ? "short" : "long";
3847
+ const tp_price = position2.kind === "long" ? diff + position2.entry : position2.entry - diff;
3848
+ const loss_diff = allowable_loss / position2.quantity;
3849
+ const loss_price = position2.kind === "long" ? position2.entry - loss_diff : position2.entry + loss_diff;
3850
+ const entry = kind === "short" ? loss_price : tp_price;
3851
+ const stop = kind === "short" ? tp_price : loss_price;
3852
+ const result = buildWithOptimumReward({
3853
+ config: {
3854
+ ...config,
3855
+ entry,
3856
+ stop
3857
+ },
3858
+ global_config,
3859
+ force_exact: true,
3860
+ settings: {
3861
+ entry,
3862
+ stop,
3863
+ risk: profit_risk,
3864
+ distribution: config.distribution
3865
+ }
3866
+ });
3867
+ return {
3868
+ opposite: result,
3869
+ take_profit: to_f(tp_price, global_config.price_places)
3870
+ };
3871
+ }
3872
+ var compoundAPI = {
3873
+ determineHedgeTradeToPlace,
3874
+ buildWithOptimumReward,
3875
+ constructAppConfig: constructAppConfig2,
3876
+ generateOppositeOptimum,
3877
+ increaseTradeHelper,
3878
+ generatePositionIncreaseTrade
3879
+ };
2277
3880
  export {
2278
3881
  to_f,
2279
3882
  sortedBuildConfig,
@@ -2288,10 +3891,14 @@ export {
2288
3891
  getRiskReward,
2289
3892
  getParamForField,
2290
3893
  getOptimumStopAndRisk,
3894
+ getOptimumHedgeFactor,
3895
+ getHedgeZone,
2291
3896
  getDecimalPlaces,
2292
3897
  generate_config_params,
2293
3898
  generateOptimumAppConfig,
3899
+ generateOppositeTradeConfig,
2294
3900
  generateGapTp,
3901
+ generateDangerousConfig,
2295
3902
  formatPrice,
2296
3903
  fibonacci_analysis,
2297
3904
  extractValue,
@@ -2302,13 +3909,21 @@ export {
2302
3909
  determine_average_entry_and_size,
2303
3910
  determine_amount_to_sell2 as determine_amount_to_sell,
2304
3911
  determine_amount_to_buy,
3912
+ determineTPSl,
3913
+ determineRewardFactor,
3914
+ determineOptimumRisk,
2305
3915
  determineOptimumReward,
3916
+ determineCompoundLongTrade,
2306
3917
  createGapPairs,
2307
3918
  createArray,
3919
+ constructAppConfig,
2308
3920
  computeTotalAverageForEachTrade,
2309
3921
  computeSellZones,
2310
3922
  computeRiskReward,
2311
3923
  computeProfitDetail,
3924
+ compoundAPI,
3925
+ calculateFactorFromTakeProfit,
3926
+ calculateFactorFromSellQuantity,
2312
3927
  buildConfig,
2313
3928
  buildAvg,
2314
3929
  buildAppConfig,