@gbozee/ultimate 0.0.2-99 → 0.0.2-next.0
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.
- package/dist/frontend-index.d.ts +1085 -24
- package/dist/frontend-index.js +1645 -78
- package/dist/index.cjs +8691 -2993
- package/dist/index.d.ts +1858 -303
- package/dist/index.js +8695 -3001
- package/dist/mcp-client.cjs +25 -50
- package/dist/mcp-client.js +25 -50
- package/dist/mcp-server.cjs +8632 -2698
- package/dist/mcp-server.js +8636 -2706
- package/package.json +7 -4
package/dist/frontend-index.js
CHANGED
|
@@ -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,20 @@ 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
|
+
max_quantity = 0.03;
|
|
83
407
|
constructor({
|
|
84
408
|
focus,
|
|
409
|
+
symbol,
|
|
85
410
|
budget,
|
|
86
411
|
percent_change = 0.02,
|
|
87
412
|
price_places = "%.5f",
|
|
@@ -100,8 +425,21 @@ class Signal {
|
|
|
100
425
|
minimum_size = 0,
|
|
101
426
|
first_order_size = 0,
|
|
102
427
|
gap = 10,
|
|
103
|
-
max_size = 0
|
|
428
|
+
max_size = 0,
|
|
429
|
+
use_kelly = false,
|
|
430
|
+
kelly_prediction_model = "exponential",
|
|
431
|
+
kelly_confidence_factor = 0.6,
|
|
432
|
+
kelly_minimum_risk = 0.2,
|
|
433
|
+
kelly_func = "theoretical",
|
|
434
|
+
full_distribution,
|
|
435
|
+
max_quantity = 0.03,
|
|
436
|
+
distribution_params = {}
|
|
104
437
|
}) {
|
|
438
|
+
if (full_distribution) {
|
|
439
|
+
this.distribution = full_distribution;
|
|
440
|
+
}
|
|
441
|
+
this.distribution_params = distribution_params;
|
|
442
|
+
this.symbol = symbol;
|
|
105
443
|
this.minimum_size = minimum_size;
|
|
106
444
|
this.first_order_size = first_order_size;
|
|
107
445
|
this.focus = focus;
|
|
@@ -122,6 +460,12 @@ class Signal {
|
|
|
122
460
|
this.increase_position = increase_position;
|
|
123
461
|
this.gap = gap;
|
|
124
462
|
this.max_size = max_size;
|
|
463
|
+
this.use_kelly = use_kelly;
|
|
464
|
+
this.kelly_prediction_model = kelly_prediction_model;
|
|
465
|
+
this.kelly_confidence_factor = kelly_confidence_factor;
|
|
466
|
+
this.kelly_minimum_risk = kelly_minimum_risk;
|
|
467
|
+
this.kelly_func = kelly_func;
|
|
468
|
+
this.max_quantity = max_quantity;
|
|
125
469
|
}
|
|
126
470
|
build_entry({
|
|
127
471
|
current_price,
|
|
@@ -131,7 +475,9 @@ class Signal {
|
|
|
131
475
|
kind = "long",
|
|
132
476
|
risk,
|
|
133
477
|
no_of_trades = 1,
|
|
134
|
-
take_profit
|
|
478
|
+
take_profit,
|
|
479
|
+
distribution,
|
|
480
|
+
distribution_params = {}
|
|
135
481
|
}) {
|
|
136
482
|
let _stop_loss = stop_loss;
|
|
137
483
|
if (!_stop_loss && stop_percent) {
|
|
@@ -140,20 +486,26 @@ class Signal {
|
|
|
140
486
|
const percent_change = _stop_loss ? Math.max(current_price, _stop_loss) / Math.min(current_price, _stop_loss) - 1 : this.percent_change;
|
|
141
487
|
const _no_of_trades = no_of_trades || this.risk_reward;
|
|
142
488
|
let _resistance = current_price * Math.pow(1 + percent_change, 1);
|
|
489
|
+
const simple_support = Math.min(current_price, stop_loss);
|
|
490
|
+
const simple_resistance = Math.max(current_price, stop_loss);
|
|
143
491
|
const derivedConfig = {
|
|
144
492
|
...this,
|
|
145
493
|
percent_change,
|
|
146
494
|
focus: current_price,
|
|
147
|
-
resistance: _resistance,
|
|
495
|
+
resistance: distribution ? simple_resistance : _resistance,
|
|
148
496
|
risk_per_trade: risk / this.risk_reward,
|
|
149
497
|
minimum_pnl: pnl,
|
|
150
498
|
risk_reward: _no_of_trades,
|
|
151
499
|
take_profit: take_profit || this.take_profit,
|
|
152
|
-
support: kind === "long" ? _stop_loss : this.support
|
|
500
|
+
support: distribution ? simple_support : kind === "long" ? _stop_loss : this.support,
|
|
501
|
+
full_distribution: distribution ? {
|
|
502
|
+
...this.distribution,
|
|
503
|
+
[kind]: distribution
|
|
504
|
+
} : undefined,
|
|
505
|
+
distribution_params
|
|
153
506
|
};
|
|
154
507
|
const instance = new Signal(derivedConfig);
|
|
155
|
-
if (kind === "short") {
|
|
156
|
-
}
|
|
508
|
+
if (kind === "short") {}
|
|
157
509
|
let result = instance.get_bulk_trade_zones({ current_price, kind });
|
|
158
510
|
return result;
|
|
159
511
|
return result?.filter((x) => {
|
|
@@ -309,7 +661,6 @@ class Signal {
|
|
|
309
661
|
let potentials = new_result.filter((x) => condition(x["entry"], i["risk_sell"])).map((x) => x["entry"]);
|
|
310
662
|
if (potentials.length && max_index) {
|
|
311
663
|
if (kind === "long") {
|
|
312
|
-
console.log("slice: ", potentials.slice(0, max_index));
|
|
313
664
|
i["risk_sell"] = Math.max(...potentials.slice(0, max_index));
|
|
314
665
|
} else {
|
|
315
666
|
i["risk_sell"] = Math.min(...potentials.slice(0, max_index));
|
|
@@ -366,13 +717,31 @@ class Signal {
|
|
|
366
717
|
}
|
|
367
718
|
this.zone_risk = original;
|
|
368
719
|
}
|
|
720
|
+
get_future_zones_simple({
|
|
721
|
+
current_price,
|
|
722
|
+
kind = "long",
|
|
723
|
+
raw
|
|
724
|
+
}) {
|
|
725
|
+
const margin_zones = [this.support, this.resistance];
|
|
726
|
+
const distribution = this.distribution ? this.distribution[kind] : "geometric";
|
|
727
|
+
let _kind = distribution === "inverse-exponential" ? kind === "long" ? "short" : "long" : kind;
|
|
728
|
+
const entries = distributions_default({
|
|
729
|
+
margin_range: margin_zones,
|
|
730
|
+
kind: _kind,
|
|
731
|
+
distribution,
|
|
732
|
+
risk_reward: this.risk_reward,
|
|
733
|
+
price_places: this.price_places,
|
|
734
|
+
distribution_params: this.distribution_params
|
|
735
|
+
});
|
|
736
|
+
return entries.sort((a, b) => a - b);
|
|
737
|
+
}
|
|
369
738
|
get_future_zones({
|
|
370
739
|
current_price,
|
|
371
740
|
kind = "long",
|
|
372
741
|
raw
|
|
373
742
|
}) {
|
|
374
|
-
if (raw) {
|
|
375
|
-
}
|
|
743
|
+
if (raw) {}
|
|
744
|
+
return this.get_future_zones_simple({ current_price, raw, kind });
|
|
376
745
|
const margin_range = this.get_margin_range(current_price, kind);
|
|
377
746
|
let margin_zones = this.get_margin_zones({ current_price });
|
|
378
747
|
let remaining_zones = margin_zones.filter((x) => JSON.stringify(x) != JSON.stringify(margin_range));
|
|
@@ -560,10 +929,25 @@ class Signal {
|
|
|
560
929
|
}
|
|
561
930
|
const defaultStopLoss = i === 0 ? stop_loss : _base;
|
|
562
931
|
const new_stop = kind === "long" ? this.support : stop_loss;
|
|
932
|
+
let risk_to_use = risk_per_trade;
|
|
933
|
+
if (this.use_kelly) {
|
|
934
|
+
const func = this.kelly_func === "theoretical" ? calculateTheoreticalKelly : this.kelly_func === "position_based" ? calculatePositionBasedKelly : calculateTheoreticalKellyFixed;
|
|
935
|
+
const theoretical_kelly = func({
|
|
936
|
+
current_entry: x,
|
|
937
|
+
zone_prices: limit_orders,
|
|
938
|
+
kind,
|
|
939
|
+
config: {
|
|
940
|
+
price_prediction_model: this.kelly_prediction_model,
|
|
941
|
+
confidence_factor: this.kelly_confidence_factor,
|
|
942
|
+
volatility_adjustment: true
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
risk_to_use = theoretical_kelly * risk_per_trade / this.kelly_minimum_risk;
|
|
946
|
+
}
|
|
563
947
|
const y = this.build_trade_dict({
|
|
564
948
|
entry: x,
|
|
565
949
|
stop: (this.increase_position ? new_stop : defaultStopLoss) || defaultStopLoss,
|
|
566
|
-
risk:
|
|
950
|
+
risk: risk_to_use,
|
|
567
951
|
arr: limit_orders,
|
|
568
952
|
index: i,
|
|
569
953
|
new_fees: total_incurred_market_fees,
|
|
@@ -577,14 +961,13 @@ class Signal {
|
|
|
577
961
|
return y !== null ? y : undefined;
|
|
578
962
|
}) || []).filter((y) => y !== undefined).filter((y) => {
|
|
579
963
|
const min_options = [0.001, 0.002, 0.003];
|
|
580
|
-
if (min_options.includes(this.minimum_size)) {
|
|
581
|
-
return y.quantity <=
|
|
964
|
+
if (min_options.includes(this.minimum_size) && this.symbol.toUpperCase().startsWith("BTC")) {
|
|
965
|
+
return y.quantity <= this.max_quantity;
|
|
582
966
|
}
|
|
583
967
|
return true;
|
|
584
968
|
});
|
|
585
969
|
let total_orders = limit_trades.concat(market_trades);
|
|
586
|
-
if (kind === "short") {
|
|
587
|
-
}
|
|
970
|
+
if (kind === "short") {}
|
|
588
971
|
if (this.minimum_size && total_orders.length > 0) {
|
|
589
972
|
let payload = total_orders;
|
|
590
973
|
let greater_than_min_size = total_orders.filter((o) => o ? o.quantity >= this.minimum_size : true);
|
|
@@ -670,8 +1053,7 @@ class Signal {
|
|
|
670
1053
|
});
|
|
671
1054
|
const multiplier = start - index;
|
|
672
1055
|
const incurred_fees = fees.reduce((a, b) => a + b, 0) + previous_risks.reduce((a, b) => a + b, 0);
|
|
673
|
-
if (index === 0) {
|
|
674
|
-
}
|
|
1056
|
+
if (index === 0) {}
|
|
675
1057
|
let quantity = determine_position_size({
|
|
676
1058
|
entry,
|
|
677
1059
|
stop,
|
|
@@ -837,8 +1219,7 @@ function extractValue(_param, condition) {
|
|
|
837
1219
|
try {
|
|
838
1220
|
let value2 = JSON.parse(_param || "[]");
|
|
839
1221
|
param = value2.map((o) => parseFloat(o));
|
|
840
|
-
} catch (error) {
|
|
841
|
-
}
|
|
1222
|
+
} catch (error) {}
|
|
842
1223
|
} else {
|
|
843
1224
|
param = parseFloat(_param);
|
|
844
1225
|
}
|
|
@@ -853,8 +1234,7 @@ function asCoins(symbol) {
|
|
|
853
1234
|
if (symbol.toLowerCase().includes("-")) {
|
|
854
1235
|
result = result.split("-")[0];
|
|
855
1236
|
}
|
|
856
|
-
if (symbol.toLowerCase() == "usdt-usd") {
|
|
857
|
-
}
|
|
1237
|
+
if (symbol.toLowerCase() == "usdt-usd") {}
|
|
858
1238
|
let result2 = _type == "usdt" ? symbol.split(result)[0] : result;
|
|
859
1239
|
if (result.includes("-")) {
|
|
860
1240
|
result2 = result;
|
|
@@ -1153,7 +1533,64 @@ function computeSellZones(payload) {
|
|
|
1153
1533
|
const spread = factor - 1;
|
|
1154
1534
|
return Array.from({ length: zones }, (_, i) => entry * Math.pow(1 + spread, i));
|
|
1155
1535
|
}
|
|
1536
|
+
function determineTPSl(payload) {
|
|
1537
|
+
const { sell_ratio = 1, kind, positions, configs, decimal_places } = payload;
|
|
1538
|
+
const long_settings = configs.long;
|
|
1539
|
+
const short_settings = configs.short;
|
|
1540
|
+
const settings = kind === "long" ? long_settings : short_settings;
|
|
1541
|
+
const reverse_kind = kind === "long" ? "short" : "long";
|
|
1542
|
+
const _position = positions[kind];
|
|
1543
|
+
const reverse_position = positions[reverse_kind];
|
|
1544
|
+
const reduce_ratio = settings.reduce_ratio;
|
|
1545
|
+
const notion = _position.entry * _position.quantity;
|
|
1546
|
+
const profit_percent = settings.profit_percent;
|
|
1547
|
+
const places = decimal_places || reverse_position.decimal_places;
|
|
1548
|
+
if (profit_percent) {
|
|
1549
|
+
let quantity = to_f(_position.quantity * sell_ratio, places);
|
|
1550
|
+
const as_float = parseFloat(profit_percent) * sell_ratio;
|
|
1551
|
+
const pnl = to_f(as_float * notion / 100, "%.2f");
|
|
1552
|
+
const diff = pnl / quantity;
|
|
1553
|
+
const tp_price = to_f(kind === "long" ? _position.entry + diff : _position.entry - diff, _position.price_places);
|
|
1554
|
+
const expected_loss = to_f(parseFloat(reduce_ratio) * pnl, "%.2f");
|
|
1555
|
+
let reduce_quantity = 0;
|
|
1556
|
+
if (reverse_position.quantity > 0) {
|
|
1557
|
+
const total_loss = Math.abs(tp_price - reverse_position.entry) * reverse_position.quantity;
|
|
1558
|
+
const ratio = expected_loss / total_loss;
|
|
1559
|
+
reduce_quantity = to_f(reverse_position.quantity * ratio, places);
|
|
1560
|
+
}
|
|
1561
|
+
return {
|
|
1562
|
+
tp_price,
|
|
1563
|
+
pnl,
|
|
1564
|
+
quantity,
|
|
1565
|
+
reduce_quantity,
|
|
1566
|
+
expected_loss
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
return {
|
|
1570
|
+
tp_price: 0,
|
|
1571
|
+
pnl: 0,
|
|
1572
|
+
quantity: 0,
|
|
1573
|
+
reduce_quantity: 0,
|
|
1574
|
+
expected_loss: 0
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1156
1577
|
// src/helpers/shared.ts
|
|
1578
|
+
function getMaxQuantity(x, app_config) {
|
|
1579
|
+
let max_quantity = app_config.max_quantity;
|
|
1580
|
+
if (max_quantity) {
|
|
1581
|
+
return x <= max_quantity;
|
|
1582
|
+
}
|
|
1583
|
+
if (app_config.symbol === "BTCUSDT") {
|
|
1584
|
+
max_quantity = 0.03;
|
|
1585
|
+
}
|
|
1586
|
+
if (app_config.symbol?.toLowerCase().startsWith("sol")) {
|
|
1587
|
+
max_quantity = 2;
|
|
1588
|
+
}
|
|
1589
|
+
if (!max_quantity) {
|
|
1590
|
+
return true;
|
|
1591
|
+
}
|
|
1592
|
+
return x <= max_quantity;
|
|
1593
|
+
}
|
|
1157
1594
|
function buildConfig(app_config, {
|
|
1158
1595
|
take_profit,
|
|
1159
1596
|
entry,
|
|
@@ -1168,7 +1605,15 @@ function buildConfig(app_config, {
|
|
|
1168
1605
|
gap,
|
|
1169
1606
|
rr = 1,
|
|
1170
1607
|
price_places = "%.1f",
|
|
1171
|
-
decimal_places = "%.3f"
|
|
1608
|
+
decimal_places = "%.3f",
|
|
1609
|
+
use_kelly = false,
|
|
1610
|
+
kelly_confidence_factor = 0.95,
|
|
1611
|
+
kelly_minimum_risk = 0.2,
|
|
1612
|
+
kelly_prediction_model = "exponential",
|
|
1613
|
+
kelly_func = "theoretical",
|
|
1614
|
+
min_avg_size = 0,
|
|
1615
|
+
distribution,
|
|
1616
|
+
distribution_params
|
|
1172
1617
|
}) {
|
|
1173
1618
|
let fee = app_config.fee / 100;
|
|
1174
1619
|
let working_risk = risk || app_config.risk_per_trade;
|
|
@@ -1192,7 +1637,15 @@ function buildConfig(app_config, {
|
|
|
1192
1637
|
kind: app_config.kind,
|
|
1193
1638
|
gap,
|
|
1194
1639
|
min_profit: min_profit || app_config.min_profit,
|
|
1195
|
-
rr: rr || 1
|
|
1640
|
+
rr: rr || 1,
|
|
1641
|
+
use_kelly: use_kelly || app_config.kelly?.use_kelly,
|
|
1642
|
+
kelly_confidence_factor: kelly_confidence_factor || app_config.kelly?.kelly_confidence_factor,
|
|
1643
|
+
kelly_minimum_risk: kelly_minimum_risk || app_config.kelly?.kelly_minimum_risk,
|
|
1644
|
+
kelly_prediction_model: kelly_prediction_model || app_config.kelly?.kelly_prediction_model,
|
|
1645
|
+
kelly_func: kelly_func || app_config.kelly?.kelly_func,
|
|
1646
|
+
symbol: app_config.symbol,
|
|
1647
|
+
max_quantity: app_config.max_quantity,
|
|
1648
|
+
distribution_params: distribution_params || app_config.distribution_params
|
|
1196
1649
|
};
|
|
1197
1650
|
const instance = new Signal(config);
|
|
1198
1651
|
if (raw_instance) {
|
|
@@ -1202,16 +1655,19 @@ function buildConfig(app_config, {
|
|
|
1202
1655
|
return [];
|
|
1203
1656
|
}
|
|
1204
1657
|
const condition = (kind === "long" ? entry > app_config.support : entry >= app_config.support) && stop >= app_config.support * 0.999;
|
|
1205
|
-
if (kind === "short") {
|
|
1206
|
-
}
|
|
1658
|
+
if (kind === "short") {}
|
|
1207
1659
|
const result = entry === stop ? [] : condition ? instance.build_entry({
|
|
1208
1660
|
current_price: entry,
|
|
1209
1661
|
stop_loss: stop,
|
|
1210
1662
|
risk: working_risk,
|
|
1211
1663
|
kind: kind || app_config.kind,
|
|
1212
|
-
no_of_trades: trade_no
|
|
1664
|
+
no_of_trades: trade_no,
|
|
1665
|
+
distribution,
|
|
1666
|
+
distribution_params
|
|
1213
1667
|
}) || [] : [];
|
|
1214
|
-
|
|
1668
|
+
const new_trades = computeTotalAverageForEachTrade(result, config);
|
|
1669
|
+
let filtered = new_trades.filter((o) => o.avg_size > min_avg_size);
|
|
1670
|
+
return computeTotalAverageForEachTrade(filtered, config);
|
|
1215
1671
|
}
|
|
1216
1672
|
function buildAvg({
|
|
1217
1673
|
_trades,
|
|
@@ -1228,13 +1684,7 @@ function buildAvg({
|
|
|
1228
1684
|
}
|
|
1229
1685
|
function sortedBuildConfig(app_config, options) {
|
|
1230
1686
|
const sorted = buildConfig(app_config, options).sort((a, b) => app_config.kind === "long" ? a.entry - b.entry : b.entry - b.entry).filter((x) => {
|
|
1231
|
-
|
|
1232
|
-
return x.quantity <= 0.03;
|
|
1233
|
-
}
|
|
1234
|
-
if (app_config.symbol?.toLowerCase().startsWith("sol")) {
|
|
1235
|
-
return x.quantity <= 2;
|
|
1236
|
-
}
|
|
1237
|
-
return true;
|
|
1687
|
+
return getMaxQuantity(x.quantity, app_config);
|
|
1238
1688
|
});
|
|
1239
1689
|
return sorted.map((k, i) => {
|
|
1240
1690
|
const arrSet = sorted.slice(0, i + 1);
|
|
@@ -1265,7 +1715,8 @@ function get_app_config_and_max_size(config, payload) {
|
|
|
1265
1715
|
price_places: config.price_places,
|
|
1266
1716
|
decimal_places: config.decimal_places,
|
|
1267
1717
|
min_profit: config.profit_percent * config.profit / 100,
|
|
1268
|
-
symbol: config.symbol
|
|
1718
|
+
symbol: config.symbol,
|
|
1719
|
+
max_quantity: config.max_quantity
|
|
1269
1720
|
};
|
|
1270
1721
|
const initialResult = sortedBuildConfig(app_config, {
|
|
1271
1722
|
entry: app_config.entry,
|
|
@@ -1276,7 +1727,14 @@ function get_app_config_and_max_size(config, payload) {
|
|
|
1276
1727
|
increase: true,
|
|
1277
1728
|
gap: app_config.gap,
|
|
1278
1729
|
price_places: app_config.price_places,
|
|
1279
|
-
decimal_places: app_config.decimal_places
|
|
1730
|
+
decimal_places: app_config.decimal_places,
|
|
1731
|
+
use_kelly: payload.use_kelly,
|
|
1732
|
+
kelly_confidence_factor: payload.kelly_confidence_factor,
|
|
1733
|
+
kelly_minimum_risk: payload.kelly_minimum_risk,
|
|
1734
|
+
kelly_prediction_model: payload.kelly_prediction_model,
|
|
1735
|
+
kelly_func: payload.kelly_func,
|
|
1736
|
+
distribution: payload.distribution,
|
|
1737
|
+
distribution_params: payload.distribution_params
|
|
1280
1738
|
});
|
|
1281
1739
|
const max_size = initialResult[0]?.avg_size;
|
|
1282
1740
|
const last_value = initialResult[0];
|
|
@@ -1305,21 +1763,37 @@ function buildAppConfig(config, payload) {
|
|
|
1305
1763
|
reverse_factor: 1,
|
|
1306
1764
|
profit_percent: 0,
|
|
1307
1765
|
kind: payload.entry > payload.stop ? "long" : "short",
|
|
1308
|
-
symbol: payload.symbol
|
|
1766
|
+
symbol: payload.symbol || config.symbol
|
|
1309
1767
|
}, {
|
|
1310
1768
|
entry: payload.entry,
|
|
1311
1769
|
stop: payload.stop,
|
|
1312
|
-
kind: payload.entry > payload.stop ? "long" : "short"
|
|
1770
|
+
kind: payload.entry > payload.stop ? "long" : "short",
|
|
1771
|
+
use_kelly: payload.use_kelly,
|
|
1772
|
+
kelly_confidence_factor: payload.kelly_confidence_factor,
|
|
1773
|
+
kelly_minimum_risk: payload.kelly_minimum_risk,
|
|
1774
|
+
kelly_prediction_model: payload.kelly_prediction_model,
|
|
1775
|
+
kelly_func: payload.kelly_func,
|
|
1776
|
+
distribution: payload.distribution,
|
|
1777
|
+
distribution_params: payload.distribution_params
|
|
1313
1778
|
});
|
|
1314
1779
|
app_config.max_size = max_size;
|
|
1315
1780
|
app_config.entry = payload.entry || app_config.entry;
|
|
1316
1781
|
app_config.stop = payload.stop || app_config.stop;
|
|
1317
1782
|
app_config.last_value = last_value;
|
|
1318
1783
|
app_config.entries = entries;
|
|
1784
|
+
app_config.kelly = {
|
|
1785
|
+
use_kelly: payload.use_kelly,
|
|
1786
|
+
kelly_confidence_factor: payload.kelly_confidence_factor,
|
|
1787
|
+
kelly_minimum_risk: payload.kelly_minimum_risk,
|
|
1788
|
+
kelly_prediction_model: payload.kelly_prediction_model,
|
|
1789
|
+
kelly_func: payload.kelly_func
|
|
1790
|
+
};
|
|
1791
|
+
app_config.distribution = payload.distribution;
|
|
1792
|
+
app_config.distribution_params = payload.distribution_params;
|
|
1319
1793
|
return app_config;
|
|
1320
1794
|
}
|
|
1321
1795
|
function getOptimumStopAndRisk(app_config, params) {
|
|
1322
|
-
const { max_size, target_stop } = params;
|
|
1796
|
+
const { max_size, target_stop, distribution, distribution_params: _distribution_params } = params;
|
|
1323
1797
|
const isLong = app_config.kind === "long";
|
|
1324
1798
|
const stopRange = Math.abs(app_config.entry - target_stop) * 0.5;
|
|
1325
1799
|
let low_stop = isLong ? target_stop - stopRange : Math.max(target_stop - stopRange, app_config.entry);
|
|
@@ -1342,7 +1816,9 @@ function getOptimumStopAndRisk(app_config, params) {
|
|
|
1342
1816
|
increase: true,
|
|
1343
1817
|
gap: app_config.gap,
|
|
1344
1818
|
price_places: app_config.price_places,
|
|
1345
|
-
decimal_places: app_config.decimal_places
|
|
1819
|
+
decimal_places: app_config.decimal_places,
|
|
1820
|
+
distribution,
|
|
1821
|
+
distribution_params: _distribution_params
|
|
1346
1822
|
});
|
|
1347
1823
|
if (result.length === 0) {
|
|
1348
1824
|
if (isLong) {
|
|
@@ -1396,7 +1872,9 @@ function getOptimumStopAndRisk(app_config, params) {
|
|
|
1396
1872
|
increase: true,
|
|
1397
1873
|
gap: app_config.gap,
|
|
1398
1874
|
price_places: app_config.price_places,
|
|
1399
|
-
decimal_places: app_config.decimal_places
|
|
1875
|
+
decimal_places: app_config.decimal_places,
|
|
1876
|
+
distribution,
|
|
1877
|
+
distribution_params: _distribution_params
|
|
1400
1878
|
});
|
|
1401
1879
|
if (result.length === 0) {
|
|
1402
1880
|
high_risk = mid_risk;
|
|
@@ -1441,7 +1919,9 @@ function getOptimumStopAndRisk(app_config, params) {
|
|
|
1441
1919
|
increase: true,
|
|
1442
1920
|
gap: app_config.gap,
|
|
1443
1921
|
price_places: app_config.price_places,
|
|
1444
|
-
decimal_places: app_config.decimal_places
|
|
1922
|
+
decimal_places: app_config.decimal_places,
|
|
1923
|
+
distribution,
|
|
1924
|
+
distribution_params: _distribution_params
|
|
1445
1925
|
});
|
|
1446
1926
|
if (result.length === 0)
|
|
1447
1927
|
continue;
|
|
@@ -1564,7 +2044,8 @@ function generateOptimumAppConfig(config, payload, position2) {
|
|
|
1564
2044
|
}, {
|
|
1565
2045
|
entry: payload.entry,
|
|
1566
2046
|
stop: payload.stop,
|
|
1567
|
-
kind: position2.kind
|
|
2047
|
+
kind: position2.kind,
|
|
2048
|
+
distribution: payload.distribution
|
|
1568
2049
|
});
|
|
1569
2050
|
current_app_config.max_size = max_size;
|
|
1570
2051
|
current_app_config.last_value = last_value;
|
|
@@ -1579,7 +2060,8 @@ function generateOptimumAppConfig(config, payload, position2) {
|
|
|
1579
2060
|
increase: true,
|
|
1580
2061
|
gap: current_app_config.gap,
|
|
1581
2062
|
price_places: current_app_config.price_places,
|
|
1582
|
-
decimal_places: current_app_config.decimal_places
|
|
2063
|
+
decimal_places: current_app_config.decimal_places,
|
|
2064
|
+
distribution: payload.distribution
|
|
1583
2065
|
});
|
|
1584
2066
|
if (full_trades.length === 0) {
|
|
1585
2067
|
high_risk = mid_risk;
|
|
@@ -1637,7 +2119,16 @@ function generateOptimumAppConfig(config, payload, position2) {
|
|
|
1637
2119
|
});
|
|
1638
2120
|
return best_app_config;
|
|
1639
2121
|
}
|
|
1640
|
-
function determineOptimumReward(
|
|
2122
|
+
function determineOptimumReward(payload) {
|
|
2123
|
+
const {
|
|
2124
|
+
app_config,
|
|
2125
|
+
increase = true,
|
|
2126
|
+
low_range = 1,
|
|
2127
|
+
high_range = 199,
|
|
2128
|
+
target_loss,
|
|
2129
|
+
distribution,
|
|
2130
|
+
max_size
|
|
2131
|
+
} = payload;
|
|
1641
2132
|
const criterion = app_config.strategy || "quantity";
|
|
1642
2133
|
const risk_rewards = createArray(low_range, high_range, 1);
|
|
1643
2134
|
let func = risk_rewards.map((trade_no) => {
|
|
@@ -1650,13 +2141,16 @@ function determineOptimumReward(app_config, increase = true, low_range = 30, hig
|
|
|
1650
2141
|
increase,
|
|
1651
2142
|
kind: app_config.kind,
|
|
1652
2143
|
gap: app_config.gap,
|
|
1653
|
-
decimal_places: app_config.decimal_places
|
|
2144
|
+
decimal_places: app_config.decimal_places,
|
|
2145
|
+
distribution,
|
|
2146
|
+
distribution_params: payload.distribution_params
|
|
1654
2147
|
});
|
|
1655
2148
|
let total = 0;
|
|
1656
2149
|
let max = -Infinity;
|
|
1657
2150
|
let min = Infinity;
|
|
1658
2151
|
let neg_pnl = trades[0]?.neg_pnl || 0;
|
|
1659
2152
|
let entry = trades.at(-1)?.entry;
|
|
2153
|
+
let avg_size = trades[0]?.avg_size || 0;
|
|
1660
2154
|
if (!entry) {
|
|
1661
2155
|
return null;
|
|
1662
2156
|
}
|
|
@@ -1673,14 +2167,41 @@ function determineOptimumReward(app_config, increase = true, low_range = 30, hig
|
|
|
1673
2167
|
risk_per_trade: app_config.risk_per_trade,
|
|
1674
2168
|
max,
|
|
1675
2169
|
min,
|
|
2170
|
+
avg_size,
|
|
1676
2171
|
neg_pnl,
|
|
1677
2172
|
entry
|
|
1678
2173
|
};
|
|
1679
2174
|
});
|
|
1680
|
-
func = func.filter((r) => Boolean(r))
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
}
|
|
2175
|
+
func = func.filter((r) => Boolean(r));
|
|
2176
|
+
if (max_size !== undefined && max_size > 0) {
|
|
2177
|
+
func = func.filter((r) => r.avg_size <= max_size);
|
|
2178
|
+
}
|
|
2179
|
+
if (target_loss === undefined) {
|
|
2180
|
+
func = func.filter((r) => {
|
|
2181
|
+
let foundIndex = r?.result.findIndex((e) => e.quantity === r.max);
|
|
2182
|
+
return criterion === "quantity" ? foundIndex === 0 : true;
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2185
|
+
if (target_loss !== undefined) {
|
|
2186
|
+
const validResults = func.filter((r) => Math.abs(r.neg_pnl) <= target_loss);
|
|
2187
|
+
if (validResults.length > 0) {
|
|
2188
|
+
validResults.sort((a, b) => {
|
|
2189
|
+
const diffA = target_loss - Math.abs(a.neg_pnl);
|
|
2190
|
+
const diffB = target_loss - Math.abs(b.neg_pnl);
|
|
2191
|
+
return diffA - diffB;
|
|
2192
|
+
});
|
|
2193
|
+
if (app_config.raw) {
|
|
2194
|
+
return validResults[0];
|
|
2195
|
+
}
|
|
2196
|
+
return validResults[0]?.value;
|
|
2197
|
+
} else {
|
|
2198
|
+
func.sort((a, b) => Math.abs(a.neg_pnl) - Math.abs(b.neg_pnl));
|
|
2199
|
+
if (app_config.raw) {
|
|
2200
|
+
return func[0];
|
|
2201
|
+
}
|
|
2202
|
+
return func[0]?.value;
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
1684
2205
|
const highest = criterion === "quantity" ? Math.max(...func.map((o) => o.max)) : Math.min(...func.map((o) => o.entry));
|
|
1685
2206
|
const key = criterion === "quantity" ? "max" : "entry";
|
|
1686
2207
|
const index = findIndexByCondition(func, app_config.kind, (x) => x[key] == highest, criterion);
|
|
@@ -1714,6 +2235,7 @@ function findIndexByCondition(lst, kind, condition, defaultKey = "neg_pnl") {
|
|
|
1714
2235
|
return b.net_diff - a.net_diff;
|
|
1715
2236
|
}
|
|
1716
2237
|
});
|
|
2238
|
+
console.log("found", sortedFound);
|
|
1717
2239
|
if (defaultKey === "quantity") {
|
|
1718
2240
|
return sortedFound[0].index;
|
|
1719
2241
|
}
|
|
@@ -1734,58 +2256,154 @@ function findIndexByCondition(lst, kind, condition, defaultKey = "neg_pnl") {
|
|
|
1734
2256
|
}, found[0]);
|
|
1735
2257
|
return maximum;
|
|
1736
2258
|
}
|
|
2259
|
+
function determineOptimumRisk(config, payload, params) {
|
|
2260
|
+
const { highest_risk, tolerance = 0.01, max_iterations = 200 } = params;
|
|
2261
|
+
let low_risk = 1;
|
|
2262
|
+
let high_risk = Math.max(highest_risk * 5, payload.risk * 10);
|
|
2263
|
+
let best_risk = payload.risk;
|
|
2264
|
+
let best_neg_pnl = 0;
|
|
2265
|
+
let best_diff = Infinity;
|
|
2266
|
+
console.log(`Finding optimal risk for target neg_pnl: ${highest_risk}`);
|
|
2267
|
+
let iterations = 0;
|
|
2268
|
+
while (iterations < max_iterations && high_risk - low_risk > tolerance) {
|
|
2269
|
+
iterations++;
|
|
2270
|
+
const mid_risk = (low_risk + high_risk) / 2;
|
|
2271
|
+
const test_payload = {
|
|
2272
|
+
...payload,
|
|
2273
|
+
risk: mid_risk
|
|
2274
|
+
};
|
|
2275
|
+
const { last_value } = buildAppConfig(config, test_payload);
|
|
2276
|
+
if (!last_value || !last_value.neg_pnl) {
|
|
2277
|
+
high_risk = mid_risk;
|
|
2278
|
+
continue;
|
|
2279
|
+
}
|
|
2280
|
+
const current_neg_pnl = Math.abs(last_value.neg_pnl);
|
|
2281
|
+
const diff = Math.abs(current_neg_pnl - highest_risk);
|
|
2282
|
+
console.log(`Iteration ${iterations}: Risk=${mid_risk.toFixed(2)}, neg_pnl=${current_neg_pnl.toFixed(2)}, diff=${diff.toFixed(2)}`);
|
|
2283
|
+
if (diff < best_diff) {
|
|
2284
|
+
best_diff = diff;
|
|
2285
|
+
best_risk = mid_risk;
|
|
2286
|
+
best_neg_pnl = current_neg_pnl;
|
|
2287
|
+
}
|
|
2288
|
+
if (diff <= tolerance) {
|
|
2289
|
+
console.log(`Converged! Optimal risk: ${mid_risk.toFixed(2)}`);
|
|
2290
|
+
break;
|
|
2291
|
+
}
|
|
2292
|
+
if (current_neg_pnl < highest_risk) {
|
|
2293
|
+
low_risk = mid_risk;
|
|
2294
|
+
} else {
|
|
2295
|
+
high_risk = mid_risk;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
const final_payload = {
|
|
2299
|
+
...payload,
|
|
2300
|
+
risk: best_risk
|
|
2301
|
+
};
|
|
2302
|
+
const final_result = buildAppConfig(config, final_payload);
|
|
2303
|
+
return {
|
|
2304
|
+
optimal_risk: to_f(best_risk, "%.2f"),
|
|
2305
|
+
achieved_neg_pnl: to_f(best_neg_pnl, "%.2f"),
|
|
2306
|
+
target_neg_pnl: to_f(highest_risk, "%.2f"),
|
|
2307
|
+
difference: to_f(best_diff, "%.2f"),
|
|
2308
|
+
iterations,
|
|
2309
|
+
converged: best_diff <= tolerance,
|
|
2310
|
+
last_value: final_result.last_value,
|
|
2311
|
+
entries: final_result.entries,
|
|
2312
|
+
app_config: final_result
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
1737
2315
|
function computeRiskReward(payload) {
|
|
1738
|
-
const {
|
|
2316
|
+
const {
|
|
2317
|
+
app_config,
|
|
2318
|
+
entry,
|
|
2319
|
+
stop,
|
|
2320
|
+
risk_per_trade,
|
|
2321
|
+
target_loss,
|
|
2322
|
+
distribution,
|
|
2323
|
+
high_range,
|
|
2324
|
+
max_size
|
|
2325
|
+
} = payload;
|
|
1739
2326
|
const kind = entry > stop ? "long" : "short";
|
|
1740
2327
|
app_config.kind = kind;
|
|
1741
2328
|
app_config.entry = entry;
|
|
1742
2329
|
app_config.stop = stop;
|
|
1743
2330
|
app_config.risk_per_trade = risk_per_trade;
|
|
1744
|
-
const result = determineOptimumReward(
|
|
2331
|
+
const result = determineOptimumReward({
|
|
2332
|
+
app_config,
|
|
2333
|
+
target_loss,
|
|
2334
|
+
distribution,
|
|
2335
|
+
distribution_params: payload.distribution_params,
|
|
2336
|
+
high_range,
|
|
2337
|
+
max_size
|
|
2338
|
+
});
|
|
1745
2339
|
return result;
|
|
1746
2340
|
}
|
|
1747
2341
|
function getRiskReward(payload) {
|
|
1748
|
-
const {
|
|
2342
|
+
const {
|
|
2343
|
+
high_range,
|
|
2344
|
+
max_size,
|
|
2345
|
+
entry,
|
|
2346
|
+
stop,
|
|
2347
|
+
risk,
|
|
2348
|
+
global_config,
|
|
2349
|
+
force_exact_risk = false,
|
|
2350
|
+
target_loss,
|
|
2351
|
+
distribution,
|
|
2352
|
+
risk_factor = 1
|
|
2353
|
+
} = payload;
|
|
1749
2354
|
const { entries, last_value, ...app_config } = buildAppConfig(global_config, {
|
|
1750
2355
|
entry,
|
|
1751
2356
|
stop,
|
|
1752
2357
|
risk_reward: 30,
|
|
1753
2358
|
risk,
|
|
1754
|
-
symbol: global_config.symbol
|
|
2359
|
+
symbol: global_config.symbol,
|
|
2360
|
+
distribution,
|
|
2361
|
+
distribution_params: payload.distribution_params
|
|
1755
2362
|
});
|
|
1756
2363
|
const risk_reward = computeRiskReward({
|
|
1757
2364
|
app_config,
|
|
1758
2365
|
entry,
|
|
1759
2366
|
stop,
|
|
1760
|
-
risk_per_trade: risk
|
|
2367
|
+
risk_per_trade: risk,
|
|
2368
|
+
high_range,
|
|
2369
|
+
target_loss,
|
|
2370
|
+
distribution,
|
|
2371
|
+
distribution_params: payload.distribution_params,
|
|
2372
|
+
max_size
|
|
1761
2373
|
});
|
|
2374
|
+
if (force_exact_risk) {
|
|
2375
|
+
const new_risk_per_trade = determineOptimumRisk(global_config, {
|
|
2376
|
+
entry,
|
|
2377
|
+
stop,
|
|
2378
|
+
risk_reward,
|
|
2379
|
+
risk,
|
|
2380
|
+
symbol: global_config.symbol,
|
|
2381
|
+
distribution,
|
|
2382
|
+
distribution_params: payload.distribution_params
|
|
2383
|
+
}, {
|
|
2384
|
+
highest_risk: risk * risk_factor
|
|
2385
|
+
}).optimal_risk;
|
|
2386
|
+
return { risk: new_risk_per_trade, risk_reward };
|
|
2387
|
+
}
|
|
1762
2388
|
return risk_reward;
|
|
1763
2389
|
}
|
|
1764
2390
|
function computeProfitDetail(payload) {
|
|
1765
2391
|
const {
|
|
1766
2392
|
focus_position,
|
|
1767
2393
|
strategy,
|
|
2394
|
+
pnl,
|
|
1768
2395
|
price_places = "%.1f",
|
|
1769
2396
|
reduce_position,
|
|
1770
2397
|
decimal_places,
|
|
1771
|
-
reverse_position
|
|
2398
|
+
reverse_position,
|
|
2399
|
+
full_ratio = 1
|
|
1772
2400
|
} = payload;
|
|
1773
|
-
let reward_factor = strategy
|
|
1774
|
-
|
|
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");
|
|
2401
|
+
let reward_factor = strategy?.reward_factor || 1;
|
|
2402
|
+
const profit_percent = to_f(pnl * 100 / (focus_position.avg_price * focus_position.avg_qty), "%.4f");
|
|
1786
2403
|
const diff = pnl / focus_position.quantity;
|
|
1787
2404
|
const sell_price = to_f(focus_position.kind === "long" ? focus_position.entry + diff : focus_position.entry - diff, price_places);
|
|
1788
2405
|
let loss = 0;
|
|
2406
|
+
let full_loss = 0;
|
|
1789
2407
|
let expected_loss = 0;
|
|
1790
2408
|
let quantity = 0;
|
|
1791
2409
|
let new_pnl = pnl;
|
|
@@ -1793,6 +2411,8 @@ function computeProfitDetail(payload) {
|
|
|
1793
2411
|
loss = Math.abs(reduce_position.entry - sell_price) * reduce_position.quantity;
|
|
1794
2412
|
const ratio = pnl / loss;
|
|
1795
2413
|
quantity = to_f(reduce_position.quantity * ratio, decimal_places);
|
|
2414
|
+
expected_loss = to_f(Math.abs(reduce_position.entry - sell_price) * quantity, "%.2f");
|
|
2415
|
+
full_loss = Math.abs(reduce_position.avg_price - sell_price) * reduce_position.avg_qty * full_ratio;
|
|
1796
2416
|
}
|
|
1797
2417
|
if (reverse_position) {
|
|
1798
2418
|
expected_loss = Math.abs(reverse_position.avg_price - sell_price) * reverse_position.avg_qty;
|
|
@@ -1801,12 +2421,13 @@ function computeProfitDetail(payload) {
|
|
|
1801
2421
|
return {
|
|
1802
2422
|
pnl: new_pnl,
|
|
1803
2423
|
loss: to_f(expected_loss, "%.2f"),
|
|
2424
|
+
full_loss: to_f(full_loss, "%.2f"),
|
|
1804
2425
|
original_pnl: pnl,
|
|
1805
2426
|
reward_factor,
|
|
1806
2427
|
profit_percent,
|
|
1807
2428
|
kind: focus_position.kind,
|
|
1808
2429
|
sell_price: to_f(sell_price, price_places),
|
|
1809
|
-
quantity,
|
|
2430
|
+
quantity: quantity * full_ratio,
|
|
1810
2431
|
price_places,
|
|
1811
2432
|
decimal_places
|
|
1812
2433
|
};
|
|
@@ -1815,10 +2436,26 @@ function generateGapTp(payload) {
|
|
|
1815
2436
|
const {
|
|
1816
2437
|
long,
|
|
1817
2438
|
short,
|
|
1818
|
-
factor =
|
|
2439
|
+
factor: factor_value = 0,
|
|
2440
|
+
risk: desired_risk,
|
|
2441
|
+
sell_factor = 1,
|
|
2442
|
+
kind,
|
|
1819
2443
|
price_places = "%.1f",
|
|
1820
2444
|
decimal_places = "%.3f"
|
|
1821
2445
|
} = payload;
|
|
2446
|
+
if (!factor_value && !desired_risk) {
|
|
2447
|
+
throw new Error("Either factor or risk must be provided");
|
|
2448
|
+
}
|
|
2449
|
+
if (desired_risk && !kind) {
|
|
2450
|
+
throw new Error("Kind must be provided when risk is provided");
|
|
2451
|
+
}
|
|
2452
|
+
let factor = factor_value || calculate_factor({
|
|
2453
|
+
long,
|
|
2454
|
+
short,
|
|
2455
|
+
risk: desired_risk,
|
|
2456
|
+
kind,
|
|
2457
|
+
sell_factor
|
|
2458
|
+
});
|
|
1822
2459
|
const gap = Math.abs(long.entry - short.entry);
|
|
1823
2460
|
const max_quantity = Math.max(long.quantity, short.quantity);
|
|
1824
2461
|
const gapLoss = gap * max_quantity;
|
|
@@ -1828,14 +2465,36 @@ function generateGapTp(payload) {
|
|
|
1828
2465
|
const shortTp = to_f((1 + shortPercent) ** -1 * short.entry, price_places);
|
|
1829
2466
|
const shortToReduce = to_f(Math.abs(longTp - long.entry) * long.quantity, "%.1f");
|
|
1830
2467
|
const longToReduce = to_f(Math.abs(shortTp - short.entry) * short.quantity, "%.1f");
|
|
1831
|
-
const
|
|
1832
|
-
const
|
|
2468
|
+
const actualShortReduce = to_f(shortToReduce * sell_factor, "%.1f");
|
|
2469
|
+
const actualLongReduce = to_f(longToReduce * sell_factor, "%.1f");
|
|
2470
|
+
const short_quantity_to_sell = determine_amount_to_sell2(short.entry, short.quantity, longTp, actualShortReduce, "short", decimal_places);
|
|
2471
|
+
const long_quantity_to_sell = determine_amount_to_sell2(long.entry, long.quantity, shortTp, actualLongReduce, "long", decimal_places);
|
|
2472
|
+
const risk_amount_short = to_f(shortToReduce - actualShortReduce, "%.2f");
|
|
2473
|
+
const risk_amount_long = to_f(longToReduce - actualLongReduce, "%.2f");
|
|
2474
|
+
const profit_percent_long = to_f(shortToReduce * 100 / (long.entry * long.quantity), "%.4f");
|
|
2475
|
+
const profit_percent_short = to_f(longToReduce * 100 / (short.entry * short.quantity), "%.4f");
|
|
1833
2476
|
return {
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
2477
|
+
profit_percent: {
|
|
2478
|
+
long: profit_percent_long,
|
|
2479
|
+
short: profit_percent_short
|
|
2480
|
+
},
|
|
2481
|
+
risk: {
|
|
2482
|
+
short: risk_amount_short,
|
|
2483
|
+
long: risk_amount_long
|
|
2484
|
+
},
|
|
2485
|
+
take_profit: {
|
|
2486
|
+
long: longTp,
|
|
2487
|
+
short: shortTp
|
|
2488
|
+
},
|
|
2489
|
+
to_reduce: {
|
|
2490
|
+
short: actualShortReduce,
|
|
2491
|
+
long: actualLongReduce
|
|
2492
|
+
},
|
|
2493
|
+
full_reduce: {
|
|
2494
|
+
short: shortToReduce,
|
|
2495
|
+
long: longToReduce
|
|
2496
|
+
},
|
|
2497
|
+
sell_quantity: {
|
|
1839
2498
|
short: short_quantity_to_sell,
|
|
1840
2499
|
long: long_quantity_to_sell
|
|
1841
2500
|
},
|
|
@@ -1843,6 +2502,370 @@ function generateGapTp(payload) {
|
|
|
1843
2502
|
gap_loss: to_f(gapLoss, "%.2f")
|
|
1844
2503
|
};
|
|
1845
2504
|
}
|
|
2505
|
+
function calculate_factor(payload) {
|
|
2506
|
+
const {
|
|
2507
|
+
long,
|
|
2508
|
+
short,
|
|
2509
|
+
risk: desired_risk,
|
|
2510
|
+
kind,
|
|
2511
|
+
sell_factor,
|
|
2512
|
+
places = "%.4f"
|
|
2513
|
+
} = payload;
|
|
2514
|
+
const gap = Math.abs(long.entry - short.entry);
|
|
2515
|
+
const max_quantity = Math.max(long.quantity, short.quantity);
|
|
2516
|
+
const gapLoss = gap * max_quantity;
|
|
2517
|
+
let calculated_factor;
|
|
2518
|
+
const long_notional = long.entry * long.quantity;
|
|
2519
|
+
const short_notional = short.entry * short.quantity;
|
|
2520
|
+
const target_to_reduce = desired_risk / (1 - sell_factor);
|
|
2521
|
+
if (kind === "short") {
|
|
2522
|
+
const calculate_longPercent = target_to_reduce / long_notional;
|
|
2523
|
+
calculated_factor = calculate_longPercent * short_notional / gapLoss;
|
|
2524
|
+
} else {
|
|
2525
|
+
const calculated_shortPercent = target_to_reduce / (short_notional - target_to_reduce);
|
|
2526
|
+
calculated_factor = calculated_shortPercent * long_notional / gapLoss;
|
|
2527
|
+
}
|
|
2528
|
+
calculated_factor = to_f(calculated_factor, places);
|
|
2529
|
+
return calculated_factor;
|
|
2530
|
+
}
|
|
2531
|
+
function calculateFactorFromTakeProfit(payload) {
|
|
2532
|
+
const { long, short, knownTp, tpType, price_places = "%.4f" } = payload;
|
|
2533
|
+
const gap = Math.abs(long.entry - short.entry);
|
|
2534
|
+
const max_quantity = Math.max(long.quantity, short.quantity);
|
|
2535
|
+
const gapLoss = gap * max_quantity;
|
|
2536
|
+
if (gapLoss === 0) {
|
|
2537
|
+
return 0;
|
|
2538
|
+
}
|
|
2539
|
+
let factor;
|
|
2540
|
+
if (tpType === "long") {
|
|
2541
|
+
const longPercent = knownTp / long.entry - 1;
|
|
2542
|
+
factor = longPercent * short.entry * short.quantity / gapLoss;
|
|
2543
|
+
} else {
|
|
2544
|
+
const shortPercent = short.entry / knownTp - 1;
|
|
2545
|
+
factor = shortPercent * long.entry * long.quantity / gapLoss;
|
|
2546
|
+
}
|
|
2547
|
+
return to_f(factor, price_places);
|
|
2548
|
+
}
|
|
2549
|
+
function calculateFactorFromSellQuantity(payload) {
|
|
2550
|
+
const {
|
|
2551
|
+
long,
|
|
2552
|
+
short,
|
|
2553
|
+
knownSellQuantity,
|
|
2554
|
+
sellType,
|
|
2555
|
+
sell_factor = 1,
|
|
2556
|
+
price_places = "%.4f"
|
|
2557
|
+
} = payload;
|
|
2558
|
+
if (knownSellQuantity < 0.00001) {
|
|
2559
|
+
return 0;
|
|
2560
|
+
}
|
|
2561
|
+
const gap = Math.abs(long.entry - short.entry);
|
|
2562
|
+
const max_quantity = Math.max(long.quantity, short.quantity);
|
|
2563
|
+
const gapLoss = gap * max_quantity;
|
|
2564
|
+
if (gapLoss === 0) {
|
|
2565
|
+
return 0;
|
|
2566
|
+
}
|
|
2567
|
+
let low_factor = 0.001;
|
|
2568
|
+
let high_factor = 1;
|
|
2569
|
+
let best_factor = 0;
|
|
2570
|
+
let best_diff = Infinity;
|
|
2571
|
+
const tolerance = 0.00001;
|
|
2572
|
+
const max_iterations = 150;
|
|
2573
|
+
let expansions = 0;
|
|
2574
|
+
const max_expansions = 15;
|
|
2575
|
+
let iterations = 0;
|
|
2576
|
+
while (iterations < max_iterations) {
|
|
2577
|
+
iterations++;
|
|
2578
|
+
const mid_factor = (low_factor + high_factor) / 2;
|
|
2579
|
+
const testResult = generateGapTp({
|
|
2580
|
+
long,
|
|
2581
|
+
short,
|
|
2582
|
+
factor: mid_factor,
|
|
2583
|
+
sell_factor,
|
|
2584
|
+
price_places: "%.8f",
|
|
2585
|
+
decimal_places: "%.8f"
|
|
2586
|
+
});
|
|
2587
|
+
const testSellQty = sellType === "long" ? testResult.sell_quantity.long : testResult.sell_quantity.short;
|
|
2588
|
+
const diff = Math.abs(testSellQty - knownSellQuantity);
|
|
2589
|
+
if (diff < best_diff) {
|
|
2590
|
+
best_diff = diff;
|
|
2591
|
+
best_factor = mid_factor;
|
|
2592
|
+
}
|
|
2593
|
+
if (diff < tolerance) {
|
|
2594
|
+
return to_f(mid_factor, price_places);
|
|
2595
|
+
}
|
|
2596
|
+
if (testSellQty < knownSellQuantity) {
|
|
2597
|
+
low_factor = mid_factor;
|
|
2598
|
+
if (mid_factor > high_factor * 0.9 && expansions < max_expansions) {
|
|
2599
|
+
const ratio = knownSellQuantity / testSellQty;
|
|
2600
|
+
if (ratio > 2) {
|
|
2601
|
+
high_factor = high_factor * Math.min(ratio, 10);
|
|
2602
|
+
} else {
|
|
2603
|
+
high_factor = high_factor * 2;
|
|
2604
|
+
}
|
|
2605
|
+
expansions++;
|
|
2606
|
+
continue;
|
|
2607
|
+
}
|
|
2608
|
+
} else {
|
|
2609
|
+
high_factor = mid_factor;
|
|
2610
|
+
}
|
|
2611
|
+
if (Math.abs(high_factor - low_factor) < 0.00001 && diff > tolerance) {
|
|
2612
|
+
if (testSellQty < knownSellQuantity && expansions < max_expansions) {
|
|
2613
|
+
high_factor = high_factor * 1.1;
|
|
2614
|
+
expansions++;
|
|
2615
|
+
continue;
|
|
2616
|
+
} else {
|
|
2617
|
+
break;
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
return to_f(best_factor, price_places);
|
|
2622
|
+
}
|
|
2623
|
+
function determineRewardFactor(payload) {
|
|
2624
|
+
const { quantity, avg_qty, minimum_pnl, risk } = payload;
|
|
2625
|
+
const reward_factor = minimum_pnl / risk;
|
|
2626
|
+
const quantity_ratio = quantity / avg_qty;
|
|
2627
|
+
return to_f(reward_factor / quantity_ratio, "%.4f");
|
|
2628
|
+
}
|
|
2629
|
+
function getHedgeZone(payload) {
|
|
2630
|
+
const {
|
|
2631
|
+
reward_factor: _reward_factor,
|
|
2632
|
+
symbol_config,
|
|
2633
|
+
risk,
|
|
2634
|
+
position: position2,
|
|
2635
|
+
risk_factor = 1,
|
|
2636
|
+
support
|
|
2637
|
+
} = payload;
|
|
2638
|
+
const kind = position2.kind;
|
|
2639
|
+
let reward_factor = _reward_factor;
|
|
2640
|
+
if (support) {
|
|
2641
|
+
const _result = getOptimumHedgeFactor({
|
|
2642
|
+
target_support: support,
|
|
2643
|
+
symbol_config,
|
|
2644
|
+
risk,
|
|
2645
|
+
position: position2
|
|
2646
|
+
});
|
|
2647
|
+
reward_factor = Number(_result.reward_factor);
|
|
2648
|
+
}
|
|
2649
|
+
const take_profit = position2.tp?.price;
|
|
2650
|
+
const tp_diff = Math.abs(take_profit - position2.entry);
|
|
2651
|
+
const quantity = position2.quantity;
|
|
2652
|
+
const diff = risk / quantity;
|
|
2653
|
+
let new_take_profit = kind === "long" ? to_f(position2.entry + diff, symbol_config.price_places) : to_f(position2.entry - diff, symbol_config.price_places);
|
|
2654
|
+
let base_factor = to_f(Math.max(tp_diff, diff) / (Math.min(tp_diff, diff) || 1), "%.3f");
|
|
2655
|
+
let factor = reward_factor || base_factor;
|
|
2656
|
+
const new_risk = risk * factor * risk_factor;
|
|
2657
|
+
const stop_loss_diff = new_risk / quantity;
|
|
2658
|
+
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);
|
|
2659
|
+
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);
|
|
2660
|
+
const profit_percent = new_risk * 100 / (position2.entry * position2.quantity);
|
|
2661
|
+
return {
|
|
2662
|
+
support: Math.min(new_take_profit, stop_loss),
|
|
2663
|
+
resistance: Math.max(new_take_profit, stop_loss),
|
|
2664
|
+
risk: to_f(new_risk, "%.2f"),
|
|
2665
|
+
profit_percent: to_f(profit_percent, "%.2f")
|
|
2666
|
+
};
|
|
2667
|
+
}
|
|
2668
|
+
function getOptimumHedgeFactor(payload) {
|
|
2669
|
+
const {
|
|
2670
|
+
target_support,
|
|
2671
|
+
max_iterations = 50,
|
|
2672
|
+
min_factor = 0.1,
|
|
2673
|
+
max_factor = 20,
|
|
2674
|
+
symbol_config,
|
|
2675
|
+
risk,
|
|
2676
|
+
position: position2
|
|
2677
|
+
} = payload;
|
|
2678
|
+
const current_price = position2.entry;
|
|
2679
|
+
const tolerance = current_price > 100 ? 0.5 : current_price > 1 ? 0.01 : 0.001;
|
|
2680
|
+
let low = min_factor;
|
|
2681
|
+
let high = max_factor;
|
|
2682
|
+
let best_factor = low;
|
|
2683
|
+
let best_diff = Infinity;
|
|
2684
|
+
for (let iteration = 0;iteration < max_iterations; iteration++) {
|
|
2685
|
+
const mid_factor = (low + high) / 2;
|
|
2686
|
+
const hedge_zone = getHedgeZone({
|
|
2687
|
+
reward_factor: mid_factor,
|
|
2688
|
+
symbol_config,
|
|
2689
|
+
risk,
|
|
2690
|
+
position: position2
|
|
2691
|
+
});
|
|
2692
|
+
const current_support = hedge_zone.support;
|
|
2693
|
+
const diff = Math.abs(current_support - target_support);
|
|
2694
|
+
if (diff < best_diff) {
|
|
2695
|
+
best_diff = diff;
|
|
2696
|
+
best_factor = mid_factor;
|
|
2697
|
+
}
|
|
2698
|
+
if (diff <= tolerance) {
|
|
2699
|
+
return {
|
|
2700
|
+
reward_factor: to_f(mid_factor, "%.4f"),
|
|
2701
|
+
achieved_support: to_f(current_support, symbol_config.price_places),
|
|
2702
|
+
target_support: to_f(target_support, symbol_config.price_places),
|
|
2703
|
+
difference: to_f(diff, symbol_config.price_places),
|
|
2704
|
+
iterations: iteration + 1
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
if (current_support > target_support) {
|
|
2708
|
+
low = mid_factor;
|
|
2709
|
+
} else {
|
|
2710
|
+
high = mid_factor;
|
|
2711
|
+
}
|
|
2712
|
+
if (Math.abs(high - low) < 0.0001) {
|
|
2713
|
+
break;
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
const final_hedge_zone = getHedgeZone({
|
|
2717
|
+
symbol_config,
|
|
2718
|
+
risk,
|
|
2719
|
+
position: position2,
|
|
2720
|
+
reward_factor: best_factor
|
|
2721
|
+
});
|
|
2722
|
+
return {
|
|
2723
|
+
reward_factor: to_f(best_factor, "%.4f"),
|
|
2724
|
+
achieved_support: to_f(final_hedge_zone.support, symbol_config.price_places),
|
|
2725
|
+
target_support: to_f(target_support, symbol_config.price_places),
|
|
2726
|
+
difference: to_f(best_diff, symbol_config.price_places),
|
|
2727
|
+
iterations: max_iterations,
|
|
2728
|
+
converged: best_diff <= tolerance
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
function determineCompoundLongTrade(payload) {
|
|
2732
|
+
const {
|
|
2733
|
+
focus_short_position,
|
|
2734
|
+
focus_long_position,
|
|
2735
|
+
shortConfig,
|
|
2736
|
+
global_config,
|
|
2737
|
+
rr = 1
|
|
2738
|
+
} = payload;
|
|
2739
|
+
const short_app_config = buildAppConfig(global_config, {
|
|
2740
|
+
entry: shortConfig.entry,
|
|
2741
|
+
stop: shortConfig.stop,
|
|
2742
|
+
risk_reward: shortConfig.risk_reward,
|
|
2743
|
+
risk: shortConfig.risk,
|
|
2744
|
+
symbol: shortConfig.symbol
|
|
2745
|
+
});
|
|
2746
|
+
const short_max_size = short_app_config.last_value.avg_size;
|
|
2747
|
+
const start_risk = Math.abs(short_app_config.last_value.neg_pnl) * rr;
|
|
2748
|
+
const short_profit = short_app_config.last_value.avg_size * short_app_config.last_value.avg_entry * shortConfig.profit_percent / 100;
|
|
2749
|
+
const diff = short_profit * rr / short_app_config.last_value.avg_size;
|
|
2750
|
+
const support = Math.abs(short_app_config.last_value.avg_entry - diff);
|
|
2751
|
+
const resistance = focus_short_position.next_order || focus_long_position.take_profit;
|
|
2752
|
+
console.log({ support, resistance, short_profit });
|
|
2753
|
+
const result = getRiskReward({
|
|
2754
|
+
entry: resistance,
|
|
2755
|
+
stop: support,
|
|
2756
|
+
risk: start_risk,
|
|
2757
|
+
global_config,
|
|
2758
|
+
force_exact_risk: true
|
|
2759
|
+
});
|
|
2760
|
+
const long_app_config = buildAppConfig(global_config, {
|
|
2761
|
+
entry: resistance,
|
|
2762
|
+
stop: support,
|
|
2763
|
+
risk_reward: result.risk_reward,
|
|
2764
|
+
risk: result.risk,
|
|
2765
|
+
symbol: shortConfig.symbol
|
|
2766
|
+
});
|
|
2767
|
+
const long_profit_percent = start_risk * 2 * 100 / (long_app_config.last_value.avg_size * long_app_config.last_value.avg_entry);
|
|
2768
|
+
return {
|
|
2769
|
+
start_risk,
|
|
2770
|
+
short_profit,
|
|
2771
|
+
support: to_f(support, global_config.price_places),
|
|
2772
|
+
resistance: to_f(resistance, global_config.price_places),
|
|
2773
|
+
long_v: long_app_config.last_value,
|
|
2774
|
+
profit_percent: to_f(long_profit_percent, "%.3f"),
|
|
2775
|
+
result,
|
|
2776
|
+
short_max_size
|
|
2777
|
+
};
|
|
2778
|
+
}
|
|
2779
|
+
function generateOppositeTradeConfig(payload) {
|
|
2780
|
+
const {
|
|
2781
|
+
kind,
|
|
2782
|
+
entry,
|
|
2783
|
+
quantity,
|
|
2784
|
+
target_pnl,
|
|
2785
|
+
ratio = 0.5,
|
|
2786
|
+
global_config
|
|
2787
|
+
} = payload;
|
|
2788
|
+
const diff = target_pnl / quantity;
|
|
2789
|
+
const tp = kind === "long" ? entry + diff : entry - diff;
|
|
2790
|
+
const stop = kind === "long" ? entry - diff : entry + diff;
|
|
2791
|
+
const opposite_pnl = target_pnl * ratio;
|
|
2792
|
+
const app_config = constructAppConfig({
|
|
2793
|
+
account: {
|
|
2794
|
+
expand: {
|
|
2795
|
+
b_config: {
|
|
2796
|
+
entry,
|
|
2797
|
+
stop,
|
|
2798
|
+
symbol: global_config.symbol,
|
|
2799
|
+
risk: target_pnl
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
},
|
|
2803
|
+
global_config,
|
|
2804
|
+
distribution_config: {}
|
|
2805
|
+
});
|
|
2806
|
+
const risk_reward = computeRiskReward({
|
|
2807
|
+
app_config,
|
|
2808
|
+
entry: stop,
|
|
2809
|
+
stop: tp,
|
|
2810
|
+
risk_per_trade: opposite_pnl,
|
|
2811
|
+
target_loss: opposite_pnl
|
|
2812
|
+
});
|
|
2813
|
+
return {
|
|
2814
|
+
entry: to_f(stop, global_config.price_places),
|
|
2815
|
+
stop: to_f(tp, global_config.price_places),
|
|
2816
|
+
risk: to_f(opposite_pnl, "%.2f"),
|
|
2817
|
+
risk_reward
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
function constructAppConfig(payload) {
|
|
2821
|
+
const { account, global_config, kelly_config, distribution_config } = payload;
|
|
2822
|
+
const config = account.expand?.b_config;
|
|
2823
|
+
if (!config) {
|
|
2824
|
+
return null;
|
|
2825
|
+
}
|
|
2826
|
+
const kelly = config.kelly;
|
|
2827
|
+
const options = {
|
|
2828
|
+
entry: config?.entry,
|
|
2829
|
+
stop: config?.stop,
|
|
2830
|
+
risk_reward: config?.risk_reward,
|
|
2831
|
+
risk: config?.risk,
|
|
2832
|
+
symbol: account.symbol,
|
|
2833
|
+
use_kelly: kelly_config?.use_kelly ?? kelly?.use_kelly,
|
|
2834
|
+
kelly_confidence_factor: kelly_config?.kelly_confidence_factor ?? kelly?.kelly_confidence_factor,
|
|
2835
|
+
kelly_minimum_risk: kelly_config?.kelly_minimum_risk ?? kelly?.kelly_minimum_risk,
|
|
2836
|
+
kelly_prediction_model: kelly_config?.kelly_prediction_model ?? kelly?.kelly_prediction_model,
|
|
2837
|
+
distribution: distribution_config?.distribution ?? config?.distribution,
|
|
2838
|
+
distribution_params: distribution_config?.distribution_params ?? config?.distribution_params
|
|
2839
|
+
};
|
|
2840
|
+
const { entries: _entries, ...appConfig } = buildAppConfig(global_config, options);
|
|
2841
|
+
return appConfig;
|
|
2842
|
+
}
|
|
2843
|
+
function generateDangerousConfig(payload) {
|
|
2844
|
+
const { account, global_config, config } = payload;
|
|
2845
|
+
const app_config = constructAppConfig({
|
|
2846
|
+
account,
|
|
2847
|
+
global_config,
|
|
2848
|
+
kelly_config: {},
|
|
2849
|
+
distribution_config: {}
|
|
2850
|
+
});
|
|
2851
|
+
const { optimal_risk, optimal_stop } = getOptimumStopAndRisk(app_config, {
|
|
2852
|
+
max_size: config.quantity,
|
|
2853
|
+
target_stop: config.stop
|
|
2854
|
+
});
|
|
2855
|
+
const optimumRiskReward = computeRiskReward({
|
|
2856
|
+
app_config,
|
|
2857
|
+
entry: config.entry,
|
|
2858
|
+
stop: optimal_stop,
|
|
2859
|
+
risk_per_trade: optimal_risk,
|
|
2860
|
+
target_loss: optimal_risk
|
|
2861
|
+
});
|
|
2862
|
+
return {
|
|
2863
|
+
entry: config.entry,
|
|
2864
|
+
risk: optimal_risk,
|
|
2865
|
+
stop: optimal_stop,
|
|
2866
|
+
risk_reward: optimumRiskReward
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
1846
2869
|
// src/helpers/strategy.ts
|
|
1847
2870
|
class Strategy {
|
|
1848
2871
|
position;
|
|
@@ -2245,11 +3268,14 @@ class Strategy {
|
|
|
2245
3268
|
return { ...app_config, avg, loss: -expected_loss, profit_percent };
|
|
2246
3269
|
}
|
|
2247
3270
|
identifyGapConfig(payload) {
|
|
2248
|
-
const { factor = 1 } = payload;
|
|
3271
|
+
const { factor, sell_factor = 1, kind, risk } = payload;
|
|
2249
3272
|
return generateGapTp({
|
|
2250
3273
|
long: this.position.long,
|
|
2251
3274
|
short: this.position.short,
|
|
2252
3275
|
factor,
|
|
3276
|
+
sell_factor,
|
|
3277
|
+
kind,
|
|
3278
|
+
risk,
|
|
2253
3279
|
decimal_places: this.config.global_config.decimal_places,
|
|
2254
3280
|
price_places: this.config.global_config.price_places
|
|
2255
3281
|
});
|
|
@@ -2265,6 +3291,7 @@ class Strategy {
|
|
|
2265
3291
|
avg_price: focus_position.avg_price,
|
|
2266
3292
|
avg_qty: focus_position.avg_qty
|
|
2267
3293
|
},
|
|
3294
|
+
pnl: this.pnl(kind),
|
|
2268
3295
|
strategy: {
|
|
2269
3296
|
reward_factor,
|
|
2270
3297
|
max_reward_factor,
|
|
@@ -2273,7 +3300,535 @@ class Strategy {
|
|
|
2273
3300
|
});
|
|
2274
3301
|
return result;
|
|
2275
3302
|
}
|
|
3303
|
+
simulateGapReduction(payload) {
|
|
3304
|
+
const {
|
|
3305
|
+
factor,
|
|
3306
|
+
direction,
|
|
3307
|
+
sell_factor = 1,
|
|
3308
|
+
iterations = 10,
|
|
3309
|
+
risk: desired_risk,
|
|
3310
|
+
kind
|
|
3311
|
+
} = payload;
|
|
3312
|
+
const results = [];
|
|
3313
|
+
let params = {
|
|
3314
|
+
long: this.position.long,
|
|
3315
|
+
short: this.position.short,
|
|
3316
|
+
config: this.config
|
|
3317
|
+
};
|
|
3318
|
+
for (let i = 0;i < iterations; i++) {
|
|
3319
|
+
const instance = new Strategy(params);
|
|
3320
|
+
const { profit_percent, risk, take_profit, sell_quantity, gap_loss } = instance.identifyGapConfig({
|
|
3321
|
+
factor,
|
|
3322
|
+
sell_factor,
|
|
3323
|
+
kind,
|
|
3324
|
+
risk: desired_risk
|
|
3325
|
+
});
|
|
3326
|
+
const to_add = {
|
|
3327
|
+
profit_percent,
|
|
3328
|
+
risk,
|
|
3329
|
+
take_profit,
|
|
3330
|
+
sell_quantity,
|
|
3331
|
+
gap_loss,
|
|
3332
|
+
position: {
|
|
3333
|
+
long: {
|
|
3334
|
+
entry: this.to_f(params.long.entry),
|
|
3335
|
+
quantity: this.to_df(params.long.quantity)
|
|
3336
|
+
},
|
|
3337
|
+
short: {
|
|
3338
|
+
entry: this.to_f(params.short.entry),
|
|
3339
|
+
quantity: this.to_df(params.short.quantity)
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
};
|
|
3343
|
+
const sell_kind = direction == "long" ? "short" : "long";
|
|
3344
|
+
if (sell_quantity[sell_kind] <= 0) {
|
|
3345
|
+
break;
|
|
3346
|
+
}
|
|
3347
|
+
results.push(to_add);
|
|
3348
|
+
const remaining = this.to_df(params[sell_kind].quantity - sell_quantity[sell_kind]);
|
|
3349
|
+
if (remaining <= 0) {
|
|
3350
|
+
break;
|
|
3351
|
+
}
|
|
3352
|
+
params[sell_kind].quantity = remaining;
|
|
3353
|
+
params[direction] = {
|
|
3354
|
+
entry: take_profit[direction],
|
|
3355
|
+
quantity: remaining
|
|
3356
|
+
};
|
|
3357
|
+
}
|
|
3358
|
+
const last_gap_loss = results.at(-1)?.gap_loss;
|
|
3359
|
+
const last_tp = results.at(-1)?.take_profit[direction];
|
|
3360
|
+
const entry = this.position[direction].entry;
|
|
3361
|
+
const quantity = this.to_df(Math.abs(last_tp - entry) / last_gap_loss);
|
|
3362
|
+
return {
|
|
3363
|
+
results,
|
|
3364
|
+
quantity
|
|
3365
|
+
};
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
// src/helpers/compound.ts
|
|
3369
|
+
function buildTrades(payload) {
|
|
3370
|
+
const { appConfig, settings, kind } = payload;
|
|
3371
|
+
const kelly_config = settings.kelly;
|
|
3372
|
+
const distribution_params = settings.distribution_params;
|
|
3373
|
+
const current_app_config = { ...appConfig[kind] };
|
|
3374
|
+
const entryNum = parseFloat(settings.entry);
|
|
3375
|
+
const stopNum = parseFloat(settings.stop);
|
|
3376
|
+
current_app_config.entry = entryNum;
|
|
3377
|
+
current_app_config.stop = stopNum;
|
|
3378
|
+
current_app_config.risk_per_trade = parseFloat(settings.risk);
|
|
3379
|
+
current_app_config.risk_reward = parseFloat(settings.risk_reward);
|
|
3380
|
+
current_app_config.kind = kind;
|
|
3381
|
+
current_app_config.kelly = kelly_config;
|
|
3382
|
+
current_app_config.distribution_params = distribution_params;
|
|
3383
|
+
const options = {
|
|
3384
|
+
take_profit: null,
|
|
3385
|
+
entry: current_app_config.entry,
|
|
3386
|
+
stop: current_app_config.stop,
|
|
3387
|
+
raw_instance: null,
|
|
3388
|
+
risk: current_app_config.risk_per_trade,
|
|
3389
|
+
no_of_trades: undefined,
|
|
3390
|
+
risk_reward: current_app_config.risk_reward,
|
|
3391
|
+
kind: current_app_config.kind,
|
|
3392
|
+
increase: true,
|
|
3393
|
+
gap: current_app_config.gap,
|
|
3394
|
+
rr: current_app_config.rr,
|
|
3395
|
+
price_places: current_app_config.price_places,
|
|
3396
|
+
decimal_places: current_app_config.decimal_places,
|
|
3397
|
+
use_kelly: kelly_config?.use_kelly,
|
|
3398
|
+
kelly_confidence_factor: kelly_config?.kelly_confidence_factor,
|
|
3399
|
+
kelly_minimum_risk: kelly_config?.kelly_minimum_risk,
|
|
3400
|
+
kelly_prediction_model: kelly_config?.kelly_prediction_model,
|
|
3401
|
+
kelly_func: kelly_config?.kelly_func,
|
|
3402
|
+
distribution: settings.distribution,
|
|
3403
|
+
distribution_params: settings.distribution_params
|
|
3404
|
+
};
|
|
3405
|
+
if (kind === "long" && entryNum <= stopNum) {
|
|
3406
|
+
return [];
|
|
3407
|
+
}
|
|
3408
|
+
if (kind === "short" && entryNum >= stopNum) {
|
|
3409
|
+
return [];
|
|
3410
|
+
}
|
|
3411
|
+
try {
|
|
3412
|
+
const generatedTrades = sortedBuildConfig(current_app_config, options);
|
|
3413
|
+
return generatedTrades ?? [];
|
|
3414
|
+
} catch (error) {
|
|
3415
|
+
console.error("Error generating orders:", error);
|
|
3416
|
+
return [];
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
function generateSummary({
|
|
3420
|
+
trades,
|
|
3421
|
+
fee_percent = 0.05,
|
|
3422
|
+
anchor
|
|
3423
|
+
}) {
|
|
3424
|
+
const avg_entry = trades[0].avg_entry;
|
|
3425
|
+
const avg_size = trades[0].avg_size;
|
|
3426
|
+
const expected_fee = avg_entry * avg_size * fee_percent / 100;
|
|
3427
|
+
return {
|
|
3428
|
+
first_entry: trades.at(-1).entry,
|
|
3429
|
+
last_entry: trades[0].entry,
|
|
3430
|
+
quantity: avg_size,
|
|
3431
|
+
entry: avg_entry,
|
|
3432
|
+
loss: trades[0].neg_pnl,
|
|
3433
|
+
number_of_trades: trades.length,
|
|
3434
|
+
fee: to_f(expected_fee, "%.2f"),
|
|
3435
|
+
anchor_pnl: anchor?.target_pnl
|
|
3436
|
+
};
|
|
3437
|
+
}
|
|
3438
|
+
function helperFuncToBuildTrades({
|
|
3439
|
+
custom_b_config,
|
|
3440
|
+
symbol_config,
|
|
3441
|
+
app_config_kind,
|
|
3442
|
+
appConfig,
|
|
3443
|
+
force_exact_risk = true
|
|
3444
|
+
}) {
|
|
3445
|
+
const risk = custom_b_config.risk * (custom_b_config.risk_factor || 1);
|
|
3446
|
+
let result = getRiskReward({
|
|
3447
|
+
entry: custom_b_config.entry,
|
|
3448
|
+
stop: custom_b_config.stop,
|
|
3449
|
+
risk,
|
|
3450
|
+
global_config: symbol_config,
|
|
3451
|
+
force_exact_risk,
|
|
3452
|
+
target_loss: custom_b_config.risk * (custom_b_config.risk_factor || 1),
|
|
3453
|
+
distribution: custom_b_config.distribution,
|
|
3454
|
+
distribution_params: custom_b_config.distribution_params
|
|
3455
|
+
});
|
|
3456
|
+
if (!force_exact_risk) {
|
|
3457
|
+
result = {
|
|
3458
|
+
risk_reward: result,
|
|
3459
|
+
risk
|
|
3460
|
+
};
|
|
3461
|
+
}
|
|
3462
|
+
const trades = result.risk_reward ? buildTrades({
|
|
3463
|
+
appConfig: { [app_config_kind]: appConfig },
|
|
3464
|
+
kind: app_config_kind,
|
|
3465
|
+
settings: {
|
|
3466
|
+
entry: custom_b_config.entry,
|
|
3467
|
+
stop: custom_b_config.stop,
|
|
3468
|
+
risk: result.risk || custom_b_config.risk,
|
|
3469
|
+
risk_reward: result.risk_reward,
|
|
3470
|
+
distribution: custom_b_config.distribution,
|
|
3471
|
+
distribution_params: custom_b_config.distribution_params
|
|
3472
|
+
}
|
|
3473
|
+
}) : [];
|
|
3474
|
+
const summary = trades.length > 0 ? generateSummary({ trades }) : {};
|
|
3475
|
+
return { trades, result, summary };
|
|
3476
|
+
}
|
|
3477
|
+
function constructAppConfig2({
|
|
3478
|
+
config,
|
|
3479
|
+
global_config
|
|
3480
|
+
}) {
|
|
3481
|
+
const options = {
|
|
3482
|
+
entry: config?.entry,
|
|
3483
|
+
stop: config?.stop,
|
|
3484
|
+
risk_reward: config?.risk_reward,
|
|
3485
|
+
risk: config?.risk,
|
|
3486
|
+
symbol: config.symbol
|
|
3487
|
+
};
|
|
3488
|
+
const { entries: _entries, ...appConfig } = buildAppConfig(global_config, options);
|
|
3489
|
+
return appConfig;
|
|
3490
|
+
}
|
|
3491
|
+
function buildWithOptimumReward({
|
|
3492
|
+
config,
|
|
3493
|
+
settings,
|
|
3494
|
+
global_config,
|
|
3495
|
+
force_exact
|
|
3496
|
+
}) {
|
|
3497
|
+
const kind = config.entry > config.stop ? "long" : "short";
|
|
3498
|
+
let stop = settings.stop;
|
|
3499
|
+
let entry = settings.entry;
|
|
3500
|
+
const risk = settings.risk;
|
|
3501
|
+
const stop_ratio = settings.stop_ratio || 1;
|
|
3502
|
+
const distribution = settings.distribution || config?.distribution;
|
|
3503
|
+
const distribution_params = settings.distribution_params || config?.distribution_params;
|
|
3504
|
+
const custom_b_config = {
|
|
3505
|
+
entry,
|
|
3506
|
+
stop,
|
|
3507
|
+
risk,
|
|
3508
|
+
distribution,
|
|
3509
|
+
distribution_params
|
|
3510
|
+
};
|
|
3511
|
+
const appConfig = constructAppConfig2({
|
|
3512
|
+
config,
|
|
3513
|
+
global_config
|
|
3514
|
+
});
|
|
3515
|
+
const { trades, summary, result } = helperFuncToBuildTrades({
|
|
3516
|
+
custom_b_config,
|
|
3517
|
+
app_config_kind: kind,
|
|
3518
|
+
appConfig,
|
|
3519
|
+
symbol_config: global_config,
|
|
3520
|
+
force_exact_risk: force_exact
|
|
3521
|
+
});
|
|
3522
|
+
const adjusted_size = summary.quantity;
|
|
3523
|
+
const symbol_config = global_config;
|
|
3524
|
+
const entryDetails = {
|
|
3525
|
+
entry: to_f(custom_b_config.entry, symbol_config.price_places),
|
|
3526
|
+
stop: to_f(custom_b_config.stop, symbol_config.price_places),
|
|
3527
|
+
risk: to_f(result.risk, "%.2f"),
|
|
3528
|
+
risk_reward: result.risk_reward,
|
|
3529
|
+
avg_entry: to_f(summary.entry, symbol_config.price_places),
|
|
3530
|
+
avg_size: to_f(adjusted_size, symbol_config.decimal_places),
|
|
3531
|
+
first_entry: to_f(summary.first_entry, symbol_config.price_places),
|
|
3532
|
+
pnl: to_f(custom_b_config.risk, "%.2f"),
|
|
3533
|
+
fee: to_f(summary.fee, "%.2f"),
|
|
3534
|
+
loss: to_f(summary.loss, "%.2f"),
|
|
3535
|
+
last_entry: to_f(summary.last_entry, symbol_config.price_places),
|
|
3536
|
+
margin: to_f(summary.entry * adjusted_size / symbol_config.leverage, "%.2f")
|
|
3537
|
+
};
|
|
3538
|
+
return {
|
|
3539
|
+
trades,
|
|
3540
|
+
summary: entryDetails,
|
|
3541
|
+
config: {
|
|
3542
|
+
...custom_b_config,
|
|
3543
|
+
...result,
|
|
3544
|
+
stop_ratio
|
|
3545
|
+
},
|
|
3546
|
+
stop_order: {
|
|
3547
|
+
quantity: entryDetails.avg_size * stop_ratio,
|
|
3548
|
+
price: entryDetails.stop
|
|
3549
|
+
},
|
|
3550
|
+
kind
|
|
3551
|
+
};
|
|
2276
3552
|
}
|
|
3553
|
+
function generateOppositeOptimum({
|
|
3554
|
+
config,
|
|
3555
|
+
global_config,
|
|
3556
|
+
settings,
|
|
3557
|
+
ratio = 1,
|
|
3558
|
+
distribution,
|
|
3559
|
+
distribution_params,
|
|
3560
|
+
risk_factor = 1
|
|
3561
|
+
}) {
|
|
3562
|
+
const configKind = config.entry > config.stop ? "long" : "short";
|
|
3563
|
+
if (configKind === "long" && config.entry > config.stop) {
|
|
3564
|
+
if (settings.stop <= settings.entry) {
|
|
3565
|
+
throw new Error("Invalid input: For long config positions, opposite settings must have stop > entry");
|
|
3566
|
+
}
|
|
3567
|
+
} else if (configKind === "short" && config.entry < config.stop) {
|
|
3568
|
+
if (settings.stop >= settings.entry) {
|
|
3569
|
+
throw new Error("Invalid input: For short config positions, opposite settings must have stop < entry");
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
const kind = config.entry > config.stop ? "long" : "short";
|
|
3573
|
+
const app_config_kind = kind === "long" ? "short" : "long";
|
|
3574
|
+
let risk = settings.risk;
|
|
3575
|
+
const custom_b_config = {
|
|
3576
|
+
entry: settings.entry,
|
|
3577
|
+
stop: settings.stop,
|
|
3578
|
+
risk: risk * ratio,
|
|
3579
|
+
distribution: distribution || "inverse-exponential",
|
|
3580
|
+
distribution_params: distribution_params || config?.distribution_params,
|
|
3581
|
+
risk_factor
|
|
3582
|
+
};
|
|
3583
|
+
const appConfig = constructAppConfig2({
|
|
3584
|
+
config: {
|
|
3585
|
+
...config,
|
|
3586
|
+
...custom_b_config
|
|
3587
|
+
},
|
|
3588
|
+
global_config
|
|
3589
|
+
});
|
|
3590
|
+
const { result, trades, summary } = helperFuncToBuildTrades({
|
|
3591
|
+
custom_b_config,
|
|
3592
|
+
symbol_config: global_config,
|
|
3593
|
+
app_config_kind,
|
|
3594
|
+
appConfig
|
|
3595
|
+
});
|
|
3596
|
+
if (Object.keys(summary).length === 0) {
|
|
3597
|
+
return {
|
|
3598
|
+
trades,
|
|
3599
|
+
summary,
|
|
3600
|
+
config: custom_b_config,
|
|
3601
|
+
kind: app_config_kind
|
|
3602
|
+
};
|
|
3603
|
+
}
|
|
3604
|
+
const symbol_config = global_config;
|
|
3605
|
+
const entryDetails = {
|
|
3606
|
+
entry: to_f(custom_b_config.entry, symbol_config.price_places),
|
|
3607
|
+
stop: to_f(custom_b_config.stop, symbol_config.price_places),
|
|
3608
|
+
risk: to_f(result.risk, "%.2f"),
|
|
3609
|
+
risk_reward: result.risk_reward,
|
|
3610
|
+
avg_entry: to_f(summary.entry, symbol_config.price_places),
|
|
3611
|
+
avg_size: to_f(summary.quantity, symbol_config.decimal_places),
|
|
3612
|
+
first_entry: to_f(summary.first_entry, symbol_config.price_places),
|
|
3613
|
+
pnl: to_f(custom_b_config.risk, "%.2f"),
|
|
3614
|
+
fee: to_f(summary.fee, "%.2f"),
|
|
3615
|
+
loss: to_f(summary.loss, "%.2f"),
|
|
3616
|
+
last_entry: to_f(summary.last_entry, symbol_config.price_places),
|
|
3617
|
+
defaultEntry: settings.entry ? to_f(settings.entry, symbol_config.price_places) : null
|
|
3618
|
+
};
|
|
3619
|
+
return {
|
|
3620
|
+
trades,
|
|
3621
|
+
summary: entryDetails,
|
|
3622
|
+
config: {
|
|
3623
|
+
...custom_b_config,
|
|
3624
|
+
...result
|
|
3625
|
+
},
|
|
3626
|
+
kind: app_config_kind
|
|
3627
|
+
};
|
|
3628
|
+
}
|
|
3629
|
+
function defaultTradeFromCurrentState({
|
|
3630
|
+
config,
|
|
3631
|
+
global_config
|
|
3632
|
+
}) {
|
|
3633
|
+
const kind = config.entry > config.stop ? "long" : "short";
|
|
3634
|
+
const settings = {
|
|
3635
|
+
entry: config?.entry,
|
|
3636
|
+
stop: config?.stop,
|
|
3637
|
+
risk: config?.risk,
|
|
3638
|
+
distribution: config?.distribution,
|
|
3639
|
+
risk_reward: config?.risk_reward
|
|
3640
|
+
};
|
|
3641
|
+
const appConfig = constructAppConfig2({
|
|
3642
|
+
config,
|
|
3643
|
+
global_config
|
|
3644
|
+
});
|
|
3645
|
+
const trades = buildTrades({
|
|
3646
|
+
appConfig: { [kind]: appConfig },
|
|
3647
|
+
kind,
|
|
3648
|
+
settings
|
|
3649
|
+
});
|
|
3650
|
+
return {
|
|
3651
|
+
trades,
|
|
3652
|
+
summary: generateSummary({
|
|
3653
|
+
trades,
|
|
3654
|
+
fee_percent: global_config.fee_percent
|
|
3655
|
+
})
|
|
3656
|
+
};
|
|
3657
|
+
}
|
|
3658
|
+
function increaseTradeHelper({
|
|
3659
|
+
increase_qty,
|
|
3660
|
+
stop,
|
|
3661
|
+
config,
|
|
3662
|
+
global_config,
|
|
3663
|
+
style,
|
|
3664
|
+
entry,
|
|
3665
|
+
position: position2,
|
|
3666
|
+
stop_ratio = 1,
|
|
3667
|
+
distribution: default_distribution,
|
|
3668
|
+
distribution_params: default_distribution_params
|
|
3669
|
+
}) {
|
|
3670
|
+
const symbol_config = global_config;
|
|
3671
|
+
const kind = config.entry > config.stop ? "long" : "short";
|
|
3672
|
+
const distribution = default_distribution || config.distribution || "inverse-exponential";
|
|
3673
|
+
const distribution_params = default_distribution_params || config.distribution_params;
|
|
3674
|
+
const appConfig = constructAppConfig2({
|
|
3675
|
+
config,
|
|
3676
|
+
global_config
|
|
3677
|
+
});
|
|
3678
|
+
const currentState = defaultTradeFromCurrentState({
|
|
3679
|
+
config,
|
|
3680
|
+
global_config
|
|
3681
|
+
});
|
|
3682
|
+
const { optimal_risk, neg_pnl } = getOptimumStopAndRisk(appConfig, {
|
|
3683
|
+
max_size: increase_qty,
|
|
3684
|
+
target_stop: stop,
|
|
3685
|
+
distribution
|
|
3686
|
+
});
|
|
3687
|
+
if (neg_pnl === 0) {
|
|
3688
|
+
return {
|
|
3689
|
+
trades: [],
|
|
3690
|
+
summary: {},
|
|
3691
|
+
config: {},
|
|
3692
|
+
kind,
|
|
3693
|
+
current: currentState
|
|
3694
|
+
};
|
|
3695
|
+
}
|
|
3696
|
+
const custom_b_config = {
|
|
3697
|
+
entry,
|
|
3698
|
+
stop,
|
|
3699
|
+
risk: style === "minimum" ? Math.abs(neg_pnl) : optimal_risk,
|
|
3700
|
+
distribution,
|
|
3701
|
+
distribution_params
|
|
3702
|
+
};
|
|
3703
|
+
const { result, trades, summary } = helperFuncToBuildTrades({
|
|
3704
|
+
custom_b_config,
|
|
3705
|
+
symbol_config,
|
|
3706
|
+
appConfig,
|
|
3707
|
+
app_config_kind: kind
|
|
3708
|
+
});
|
|
3709
|
+
if (Object.keys(summary).length === 0) {
|
|
3710
|
+
return {
|
|
3711
|
+
trades,
|
|
3712
|
+
summary,
|
|
3713
|
+
config: {
|
|
3714
|
+
...custom_b_config,
|
|
3715
|
+
...result
|
|
3716
|
+
},
|
|
3717
|
+
kind,
|
|
3718
|
+
current: currentState
|
|
3719
|
+
};
|
|
3720
|
+
}
|
|
3721
|
+
const new_avg_values = determine_average_entry_and_size([
|
|
3722
|
+
{
|
|
3723
|
+
price: position2.entry,
|
|
3724
|
+
quantity: position2.quantity
|
|
3725
|
+
},
|
|
3726
|
+
{
|
|
3727
|
+
price: summary?.entry,
|
|
3728
|
+
quantity: summary?.quantity
|
|
3729
|
+
}
|
|
3730
|
+
], symbol_config.decimal_places, symbol_config.price_places);
|
|
3731
|
+
summary.entry = new_avg_values.entry;
|
|
3732
|
+
summary.quantity = new_avg_values.quantity;
|
|
3733
|
+
const loss = Math.abs(summary.entry - custom_b_config.stop) * summary.quantity;
|
|
3734
|
+
const entryDetails = {
|
|
3735
|
+
entry: to_f(custom_b_config.entry, symbol_config.price_places),
|
|
3736
|
+
stop: to_f(custom_b_config.stop, symbol_config.price_places),
|
|
3737
|
+
risk: to_f(result.risk, symbol_config.price_places),
|
|
3738
|
+
risk_reward: result.risk_reward,
|
|
3739
|
+
avg_entry: to_f(summary.entry, symbol_config.price_places),
|
|
3740
|
+
avg_size: to_f(summary.quantity, symbol_config.decimal_places),
|
|
3741
|
+
first_entry: to_f(summary.first_entry, symbol_config.price_places),
|
|
3742
|
+
pnl: to_f(custom_b_config.risk, "%.2f"),
|
|
3743
|
+
fee: to_f(summary.fee, "%.2f"),
|
|
3744
|
+
loss: to_f(loss, "%.2f"),
|
|
3745
|
+
last_entry: to_f(summary.last_entry, symbol_config.price_places),
|
|
3746
|
+
margin: to_f(summary.entry * summary.quantity / global_config.leverage, "%.2f")
|
|
3747
|
+
};
|
|
3748
|
+
return {
|
|
3749
|
+
trades,
|
|
3750
|
+
summary: entryDetails,
|
|
3751
|
+
stop_order: {
|
|
3752
|
+
quantity: entryDetails.avg_size * stop_ratio,
|
|
3753
|
+
price: entryDetails.stop
|
|
3754
|
+
},
|
|
3755
|
+
config: {
|
|
3756
|
+
...custom_b_config,
|
|
3757
|
+
...result
|
|
3758
|
+
},
|
|
3759
|
+
kind,
|
|
3760
|
+
current: currentState
|
|
3761
|
+
};
|
|
3762
|
+
}
|
|
3763
|
+
function generatePositionIncreaseTrade({
|
|
3764
|
+
account,
|
|
3765
|
+
zoneAccount,
|
|
3766
|
+
ratio = 0.1,
|
|
3767
|
+
config,
|
|
3768
|
+
global_config,
|
|
3769
|
+
style = "minimum",
|
|
3770
|
+
distribution = "inverse-exponential",
|
|
3771
|
+
distribution_params
|
|
3772
|
+
}) {
|
|
3773
|
+
const kind = config.entry > config.stop ? "long" : "short";
|
|
3774
|
+
const target_max_quantity = kind === "long" ? account.short.quantity : account.long.quantity;
|
|
3775
|
+
const increase_qty = target_max_quantity * ratio;
|
|
3776
|
+
const entry = zoneAccount.entry;
|
|
3777
|
+
const stop = zoneAccount.stop;
|
|
3778
|
+
return increaseTradeHelper({
|
|
3779
|
+
config,
|
|
3780
|
+
position: account[kind],
|
|
3781
|
+
global_config,
|
|
3782
|
+
entry,
|
|
3783
|
+
stop,
|
|
3784
|
+
style,
|
|
3785
|
+
increase_qty,
|
|
3786
|
+
distribution,
|
|
3787
|
+
distribution_params
|
|
3788
|
+
});
|
|
3789
|
+
}
|
|
3790
|
+
function determineHedgeTradeToPlace({
|
|
3791
|
+
position: position2,
|
|
3792
|
+
config,
|
|
3793
|
+
global_config,
|
|
3794
|
+
profit_risk = 200,
|
|
3795
|
+
allowable_loss = 1000
|
|
3796
|
+
}) {
|
|
3797
|
+
const diff = profit_risk / position2.quantity;
|
|
3798
|
+
const kind = position2.kind === "long" ? "short" : "long";
|
|
3799
|
+
const tp_price = position2.kind === "long" ? diff + position2.entry : position2.entry - diff;
|
|
3800
|
+
const loss_diff = allowable_loss / position2.quantity;
|
|
3801
|
+
const loss_price = position2.kind === "long" ? position2.entry - loss_diff : position2.entry + loss_diff;
|
|
3802
|
+
const entry = kind === "short" ? loss_price : tp_price;
|
|
3803
|
+
const stop = kind === "short" ? tp_price : loss_price;
|
|
3804
|
+
const result = buildWithOptimumReward({
|
|
3805
|
+
config: {
|
|
3806
|
+
...config,
|
|
3807
|
+
entry,
|
|
3808
|
+
stop
|
|
3809
|
+
},
|
|
3810
|
+
global_config,
|
|
3811
|
+
force_exact: true,
|
|
3812
|
+
settings: {
|
|
3813
|
+
entry,
|
|
3814
|
+
stop,
|
|
3815
|
+
risk: profit_risk,
|
|
3816
|
+
distribution: config.distribution
|
|
3817
|
+
}
|
|
3818
|
+
});
|
|
3819
|
+
return {
|
|
3820
|
+
opposite: result,
|
|
3821
|
+
take_profit: to_f(tp_price, global_config.price_places)
|
|
3822
|
+
};
|
|
3823
|
+
}
|
|
3824
|
+
var compoundAPI = {
|
|
3825
|
+
determineHedgeTradeToPlace,
|
|
3826
|
+
buildWithOptimumReward,
|
|
3827
|
+
constructAppConfig: constructAppConfig2,
|
|
3828
|
+
generateOppositeOptimum,
|
|
3829
|
+
increaseTradeHelper,
|
|
3830
|
+
generatePositionIncreaseTrade
|
|
3831
|
+
};
|
|
2277
3832
|
export {
|
|
2278
3833
|
to_f,
|
|
2279
3834
|
sortedBuildConfig,
|
|
@@ -2288,10 +3843,14 @@ export {
|
|
|
2288
3843
|
getRiskReward,
|
|
2289
3844
|
getParamForField,
|
|
2290
3845
|
getOptimumStopAndRisk,
|
|
3846
|
+
getOptimumHedgeFactor,
|
|
3847
|
+
getHedgeZone,
|
|
2291
3848
|
getDecimalPlaces,
|
|
2292
3849
|
generate_config_params,
|
|
2293
3850
|
generateOptimumAppConfig,
|
|
3851
|
+
generateOppositeTradeConfig,
|
|
2294
3852
|
generateGapTp,
|
|
3853
|
+
generateDangerousConfig,
|
|
2295
3854
|
formatPrice,
|
|
2296
3855
|
fibonacci_analysis,
|
|
2297
3856
|
extractValue,
|
|
@@ -2302,13 +3861,21 @@ export {
|
|
|
2302
3861
|
determine_average_entry_and_size,
|
|
2303
3862
|
determine_amount_to_sell2 as determine_amount_to_sell,
|
|
2304
3863
|
determine_amount_to_buy,
|
|
3864
|
+
determineTPSl,
|
|
3865
|
+
determineRewardFactor,
|
|
3866
|
+
determineOptimumRisk,
|
|
2305
3867
|
determineOptimumReward,
|
|
3868
|
+
determineCompoundLongTrade,
|
|
2306
3869
|
createGapPairs,
|
|
2307
3870
|
createArray,
|
|
3871
|
+
constructAppConfig,
|
|
2308
3872
|
computeTotalAverageForEachTrade,
|
|
2309
3873
|
computeSellZones,
|
|
2310
3874
|
computeRiskReward,
|
|
2311
3875
|
computeProfitDetail,
|
|
3876
|
+
compoundAPI,
|
|
3877
|
+
calculateFactorFromTakeProfit,
|
|
3878
|
+
calculateFactorFromSellQuantity,
|
|
2312
3879
|
buildConfig,
|
|
2313
3880
|
buildAvg,
|
|
2314
3881
|
buildAppConfig,
|