@gbozee/ultimate 0.0.2-21 → 0.0.2-210
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/frontend-index.js +1318 -0
- package/dist/frontend-index.d.ts +1747 -0
- package/dist/frontend-index.js +3884 -0
- package/dist/index.cjs +65374 -0
- package/dist/index.d.ts +3147 -271
- package/dist/index.js +48714 -21177
- package/dist/mcp-client.cjs +8845 -0
- package/dist/mcp-client.d.ts +5 -0
- package/dist/mcp-client.js +8819 -0
- package/dist/mcp-server.cjs +72447 -0
- package/dist/mcp-server.d.ts +5 -0
- package/dist/mcp-server.js +72423 -0
- package/dist/mcp.d.ts +5 -0
- package/package.json +33 -7
|
@@ -0,0 +1,3884 @@
|
|
|
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
|
+
|
|
313
|
+
// src/helpers/trade_signal.ts
|
|
314
|
+
function determine_close_price({
|
|
315
|
+
entry,
|
|
316
|
+
pnl,
|
|
317
|
+
quantity,
|
|
318
|
+
leverage = 1,
|
|
319
|
+
kind = "long"
|
|
320
|
+
}) {
|
|
321
|
+
const dollar_value = entry / leverage;
|
|
322
|
+
const position = dollar_value * quantity;
|
|
323
|
+
if (position) {
|
|
324
|
+
const percent = pnl / position;
|
|
325
|
+
const difference = position * percent / quantity;
|
|
326
|
+
const result = kind === "long" ? difference + entry : entry - difference;
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
return 0;
|
|
330
|
+
}
|
|
331
|
+
function determine_pnl(entry, close_price, quantity, kind = "long", contract_size) {
|
|
332
|
+
if (contract_size) {
|
|
333
|
+
const direction = kind === "long" ? 1 : -1;
|
|
334
|
+
return quantity * contract_size * direction * (1 / entry - 1 / close_price);
|
|
335
|
+
}
|
|
336
|
+
const difference = kind === "long" ? close_price - entry : entry - close_price;
|
|
337
|
+
return difference * quantity;
|
|
338
|
+
}
|
|
339
|
+
function* _get_zones({
|
|
340
|
+
current_price,
|
|
341
|
+
focus,
|
|
342
|
+
percent_change,
|
|
343
|
+
places = "%.5f"
|
|
344
|
+
}) {
|
|
345
|
+
let last = focus;
|
|
346
|
+
let focus_high = last * (1 + percent_change);
|
|
347
|
+
let focus_low = last * Math.pow(1 + percent_change, -1);
|
|
348
|
+
if (focus_high > current_price) {
|
|
349
|
+
while (focus_high > current_price) {
|
|
350
|
+
yield to_f(last, places);
|
|
351
|
+
focus_high = last;
|
|
352
|
+
last = focus_high * Math.pow(1 + percent_change, -1);
|
|
353
|
+
focus_low = last * Math.pow(1 + percent_change, -1);
|
|
354
|
+
}
|
|
355
|
+
} else {
|
|
356
|
+
if (focus_high <= current_price) {
|
|
357
|
+
while (focus_high <= current_price) {
|
|
358
|
+
yield to_f(focus_high, places);
|
|
359
|
+
focus_low = focus_high;
|
|
360
|
+
last = focus_low * (1 + percent_change);
|
|
361
|
+
focus_high = last * (1 + percent_change);
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
while (focus_low <= current_price) {
|
|
365
|
+
yield to_f(focus_high, places);
|
|
366
|
+
focus_low = focus_high;
|
|
367
|
+
last = focus_low * (1 + percent_change);
|
|
368
|
+
focus_high = last * (1 + percent_change);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
class Signal {
|
|
375
|
+
focus;
|
|
376
|
+
budget;
|
|
377
|
+
percent_change = 0.02;
|
|
378
|
+
price_places = "%.5f";
|
|
379
|
+
distribution_params = {};
|
|
380
|
+
decimal_places = "%.0f";
|
|
381
|
+
zone_risk = 1;
|
|
382
|
+
fee = 0.08 / 100;
|
|
383
|
+
support;
|
|
384
|
+
risk_reward = 4;
|
|
385
|
+
resistance;
|
|
386
|
+
risk_per_trade;
|
|
387
|
+
increase_size = false;
|
|
388
|
+
additional_increase = 0;
|
|
389
|
+
minimum_pnl = 0;
|
|
390
|
+
take_profit;
|
|
391
|
+
increase_position = false;
|
|
392
|
+
minimum_size;
|
|
393
|
+
first_order_size;
|
|
394
|
+
gap = 10;
|
|
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;
|
|
407
|
+
constructor({
|
|
408
|
+
focus,
|
|
409
|
+
symbol,
|
|
410
|
+
budget,
|
|
411
|
+
percent_change = 0.02,
|
|
412
|
+
price_places = "%.5f",
|
|
413
|
+
decimal_places = "%.0f",
|
|
414
|
+
zone_risk = 1,
|
|
415
|
+
fee = 0.06 / 100,
|
|
416
|
+
support,
|
|
417
|
+
risk_reward = 4,
|
|
418
|
+
resistance,
|
|
419
|
+
risk_per_trade,
|
|
420
|
+
increase_size = false,
|
|
421
|
+
additional_increase = 0,
|
|
422
|
+
minimum_pnl = 0,
|
|
423
|
+
take_profit,
|
|
424
|
+
increase_position = false,
|
|
425
|
+
minimum_size = 0,
|
|
426
|
+
first_order_size = 0,
|
|
427
|
+
gap = 10,
|
|
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 = {}
|
|
437
|
+
}) {
|
|
438
|
+
if (full_distribution) {
|
|
439
|
+
this.distribution = full_distribution;
|
|
440
|
+
}
|
|
441
|
+
this.distribution_params = distribution_params;
|
|
442
|
+
this.symbol = symbol;
|
|
443
|
+
this.minimum_size = minimum_size;
|
|
444
|
+
this.first_order_size = first_order_size;
|
|
445
|
+
this.focus = focus;
|
|
446
|
+
this.budget = budget;
|
|
447
|
+
this.percent_change = percent_change;
|
|
448
|
+
this.price_places = price_places;
|
|
449
|
+
this.decimal_places = decimal_places;
|
|
450
|
+
this.zone_risk = zone_risk;
|
|
451
|
+
this.fee = fee;
|
|
452
|
+
this.support = support;
|
|
453
|
+
this.risk_reward = risk_reward;
|
|
454
|
+
this.resistance = resistance;
|
|
455
|
+
this.risk_per_trade = risk_per_trade;
|
|
456
|
+
this.increase_size = increase_size;
|
|
457
|
+
this.additional_increase = additional_increase;
|
|
458
|
+
this.minimum_pnl = minimum_pnl;
|
|
459
|
+
this.take_profit = take_profit;
|
|
460
|
+
this.increase_position = increase_position;
|
|
461
|
+
this.gap = gap;
|
|
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;
|
|
469
|
+
}
|
|
470
|
+
build_entry({
|
|
471
|
+
current_price,
|
|
472
|
+
stop_loss,
|
|
473
|
+
pnl,
|
|
474
|
+
stop_percent,
|
|
475
|
+
kind = "long",
|
|
476
|
+
risk,
|
|
477
|
+
no_of_trades = 1,
|
|
478
|
+
take_profit,
|
|
479
|
+
distribution,
|
|
480
|
+
distribution_params = {}
|
|
481
|
+
}) {
|
|
482
|
+
console.log("distribution", distribution_params);
|
|
483
|
+
let _stop_loss = stop_loss;
|
|
484
|
+
if (!_stop_loss && stop_percent) {
|
|
485
|
+
_stop_loss = kind === "long" ? current_price * Math.pow(1 + stop_percent, -1) : current_price * Math.pow(1 + stop_percent, 1);
|
|
486
|
+
}
|
|
487
|
+
const percent_change = _stop_loss ? Math.max(current_price, _stop_loss) / Math.min(current_price, _stop_loss) - 1 : this.percent_change;
|
|
488
|
+
const _no_of_trades = no_of_trades || this.risk_reward;
|
|
489
|
+
let _resistance = current_price * Math.pow(1 + percent_change, 1);
|
|
490
|
+
const simple_support = Math.min(current_price, stop_loss);
|
|
491
|
+
const simple_resistance = Math.max(current_price, stop_loss);
|
|
492
|
+
const derivedConfig = {
|
|
493
|
+
...this,
|
|
494
|
+
percent_change,
|
|
495
|
+
focus: current_price,
|
|
496
|
+
resistance: distribution ? simple_resistance : _resistance,
|
|
497
|
+
risk_per_trade: risk / this.risk_reward,
|
|
498
|
+
minimum_pnl: pnl,
|
|
499
|
+
risk_reward: _no_of_trades,
|
|
500
|
+
take_profit: take_profit || this.take_profit,
|
|
501
|
+
support: distribution ? simple_support : kind === "long" ? _stop_loss : this.support,
|
|
502
|
+
full_distribution: distribution ? {
|
|
503
|
+
...this.distribution,
|
|
504
|
+
[kind]: distribution
|
|
505
|
+
} : undefined,
|
|
506
|
+
distribution_params
|
|
507
|
+
};
|
|
508
|
+
const instance = new Signal(derivedConfig);
|
|
509
|
+
if (kind === "short") {}
|
|
510
|
+
let result = instance.get_bulk_trade_zones({ current_price, kind });
|
|
511
|
+
return result;
|
|
512
|
+
return result?.filter((x) => {
|
|
513
|
+
let pp = parseFloat(this.decimal_places.replace("%.", "").replace("f", ""));
|
|
514
|
+
if (pp < 3) {
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
if (kind === "long") {
|
|
518
|
+
return x.entry > x.stop + 0.5;
|
|
519
|
+
}
|
|
520
|
+
return x.entry + 0.5 < x.stop;
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
get risk() {
|
|
524
|
+
return this.budget * this.percent_change;
|
|
525
|
+
}
|
|
526
|
+
get min_trades() {
|
|
527
|
+
return parseInt(this.risk.toString());
|
|
528
|
+
}
|
|
529
|
+
get min_price() {
|
|
530
|
+
const number = this.price_places.replace("%.", "").replace("f", "");
|
|
531
|
+
return 1 * Math.pow(10, -parseInt(number));
|
|
532
|
+
}
|
|
533
|
+
build_opposite_order({
|
|
534
|
+
current_price,
|
|
535
|
+
kind = "long"
|
|
536
|
+
}) {
|
|
537
|
+
let _current_price = current_price;
|
|
538
|
+
if (kind === "long") {
|
|
539
|
+
_current_price = current_price * Math.pow(1 + this.percent_change, -1);
|
|
540
|
+
}
|
|
541
|
+
const result = this.special_build_orders({
|
|
542
|
+
current_price: _current_price,
|
|
543
|
+
kind
|
|
544
|
+
});
|
|
545
|
+
const first_price = result[result.length - 1].entry;
|
|
546
|
+
const stop = result[0].stop;
|
|
547
|
+
const instance = new Signal({ ...this, take_profit: stop });
|
|
548
|
+
const new_kind = kind === "long" ? "short" : "long";
|
|
549
|
+
return instance.build_orders({
|
|
550
|
+
current_price: first_price,
|
|
551
|
+
kind: new_kind
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
special_build_orders({
|
|
555
|
+
current_price,
|
|
556
|
+
kind = "long"
|
|
557
|
+
}) {
|
|
558
|
+
let orders = this.build_orders({ current_price, kind });
|
|
559
|
+
if (orders?.length > 1) {
|
|
560
|
+
orders = this.build_orders({ current_price: orders[1].entry, kind });
|
|
561
|
+
}
|
|
562
|
+
if (orders.length > 0) {
|
|
563
|
+
const new_kind = kind === "long" ? "short" : "long";
|
|
564
|
+
let opposite_order = this.build_orders({
|
|
565
|
+
current_price: orders[orders.length - 1].entry,
|
|
566
|
+
kind: new_kind
|
|
567
|
+
});
|
|
568
|
+
this.take_profit = opposite_order[0].stop;
|
|
569
|
+
orders = this.build_orders({
|
|
570
|
+
current_price: orders[orders.length - 1].entry,
|
|
571
|
+
kind
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
return orders;
|
|
575
|
+
}
|
|
576
|
+
build_orders({
|
|
577
|
+
current_price,
|
|
578
|
+
kind = "long",
|
|
579
|
+
limit = false,
|
|
580
|
+
replace_focus = false,
|
|
581
|
+
max_index = 0,
|
|
582
|
+
min_index = 2
|
|
583
|
+
}) {
|
|
584
|
+
const focus = this.focus;
|
|
585
|
+
if (replace_focus) {
|
|
586
|
+
this.focus = current_price;
|
|
587
|
+
}
|
|
588
|
+
const new_kind = kind === "long" ? "short" : "long";
|
|
589
|
+
const take_profit = this.take_profit;
|
|
590
|
+
this.take_profit = undefined;
|
|
591
|
+
let result = this.get_bulk_trade_zones({
|
|
592
|
+
current_price,
|
|
593
|
+
kind: new_kind,
|
|
594
|
+
limit
|
|
595
|
+
});
|
|
596
|
+
if (result?.length) {
|
|
597
|
+
let oppositeStop = result[0]["sell_price"];
|
|
598
|
+
let oppositeEntry = result[result.length - 1]["entry"];
|
|
599
|
+
let tradeLength = this.risk_reward + 1;
|
|
600
|
+
let percentChange = Math.abs(1 - Math.max(oppositeEntry, oppositeStop) / Math.min(oppositeEntry, oppositeStop)) / tradeLength;
|
|
601
|
+
let newTrades = [];
|
|
602
|
+
for (let x = 0;x < tradeLength; x++) {
|
|
603
|
+
newTrades.push(oppositeStop * Math.pow(1 + percentChange, x));
|
|
604
|
+
}
|
|
605
|
+
if (kind === "short") {
|
|
606
|
+
newTrades = [];
|
|
607
|
+
for (let x = 0;x < tradeLength; x++) {
|
|
608
|
+
newTrades.push(oppositeStop * Math.pow(1 + percentChange, x * -1));
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
this.take_profit = take_profit;
|
|
612
|
+
newTrades = newTrades.map((r) => this.to_f(r));
|
|
613
|
+
if (kind === "long") {
|
|
614
|
+
if (newTrades[1] > current_price) {
|
|
615
|
+
const start = newTrades[0];
|
|
616
|
+
newTrades = [];
|
|
617
|
+
for (let x = 0;x < tradeLength; x++) {
|
|
618
|
+
newTrades.push(start * Math.pow(1 + percentChange, x * -1));
|
|
619
|
+
}
|
|
620
|
+
newTrades.sort();
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const newR = this.process_orders({
|
|
624
|
+
current_price,
|
|
625
|
+
stop_loss: newTrades[0],
|
|
626
|
+
trade_zones: newTrades,
|
|
627
|
+
kind
|
|
628
|
+
});
|
|
629
|
+
return newR;
|
|
630
|
+
}
|
|
631
|
+
this.focus = focus;
|
|
632
|
+
return result;
|
|
633
|
+
}
|
|
634
|
+
build_orders_old({
|
|
635
|
+
current_price,
|
|
636
|
+
kind = "long",
|
|
637
|
+
limit = false,
|
|
638
|
+
replace_focus = false,
|
|
639
|
+
max_index = 0,
|
|
640
|
+
min_index = 2
|
|
641
|
+
}) {
|
|
642
|
+
const focus = this.focus;
|
|
643
|
+
if (replace_focus) {
|
|
644
|
+
this.focus = current_price;
|
|
645
|
+
}
|
|
646
|
+
const result = this.get_bulk_trade_zones({ current_price, kind, limit });
|
|
647
|
+
if (result?.length) {
|
|
648
|
+
let next_focus;
|
|
649
|
+
if (kind == "long") {
|
|
650
|
+
next_focus = current_price * (1 + this.percent_change);
|
|
651
|
+
} else {
|
|
652
|
+
next_focus = current_price * Math.pow(1 + this.percent_change, -1);
|
|
653
|
+
}
|
|
654
|
+
let new_result = this.get_bulk_trade_zones({
|
|
655
|
+
current_price: next_focus,
|
|
656
|
+
kind,
|
|
657
|
+
limit
|
|
658
|
+
});
|
|
659
|
+
if (new_result?.length) {
|
|
660
|
+
for (let i of result) {
|
|
661
|
+
let condition = kind === "long" ? (a, b) => a >= b : (a, b) => a <= b;
|
|
662
|
+
let potentials = new_result.filter((x) => condition(x["entry"], i["risk_sell"])).map((x) => x["entry"]);
|
|
663
|
+
if (potentials.length && max_index) {
|
|
664
|
+
if (kind === "long") {
|
|
665
|
+
i["risk_sell"] = Math.max(...potentials.slice(0, max_index));
|
|
666
|
+
} else {
|
|
667
|
+
i["risk_sell"] = Math.min(...potentials.slice(0, max_index));
|
|
668
|
+
}
|
|
669
|
+
i["pnl"] = this.to_df(determine_pnl(i["entry"], i["risk_sell"], i["quantity"], kind));
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
this.focus = focus;
|
|
675
|
+
return result;
|
|
676
|
+
}
|
|
677
|
+
get_bulk_trade_zones({
|
|
678
|
+
current_price,
|
|
679
|
+
kind = "long",
|
|
680
|
+
limit = false
|
|
681
|
+
}) {
|
|
682
|
+
const futures = this.get_future_zones({ current_price, kind });
|
|
683
|
+
const original = this.zone_risk;
|
|
684
|
+
if (futures) {
|
|
685
|
+
const values = futures;
|
|
686
|
+
if (values) {
|
|
687
|
+
let trade_zones = values.sort();
|
|
688
|
+
if (this.resistance) {
|
|
689
|
+
trade_zones = trade_zones.filter((x) => this.resistance ? x <= this.resistance : true);
|
|
690
|
+
if (kind === "short") {
|
|
691
|
+
trade_zones = trade_zones.sort((a, b) => b - a);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (trade_zones.length > 0) {
|
|
695
|
+
const stop_loss = trade_zones[0];
|
|
696
|
+
const result = this.process_orders({
|
|
697
|
+
current_price,
|
|
698
|
+
stop_loss,
|
|
699
|
+
trade_zones,
|
|
700
|
+
kind
|
|
701
|
+
});
|
|
702
|
+
if (!result.length) {
|
|
703
|
+
if (kind === "long") {
|
|
704
|
+
let m_z = this.get_margin_range(futures[0]);
|
|
705
|
+
if (m_z && m_z[0] < current_price && current_price !== m_z[1]) {
|
|
706
|
+
return this.get_bulk_trade_zones({
|
|
707
|
+
current_price: m_z[1],
|
|
708
|
+
kind,
|
|
709
|
+
limit
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
this.zone_risk = original;
|
|
715
|
+
return result;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
this.zone_risk = original;
|
|
720
|
+
}
|
|
721
|
+
get_future_zones_simple({
|
|
722
|
+
current_price,
|
|
723
|
+
kind = "long",
|
|
724
|
+
raw
|
|
725
|
+
}) {
|
|
726
|
+
const margin_zones = [this.support, this.resistance];
|
|
727
|
+
const distribution = this.distribution ? this.distribution[kind] : "geometric";
|
|
728
|
+
let _kind = distribution === "inverse-exponential" ? kind === "long" ? "short" : "long" : kind;
|
|
729
|
+
const entries = distributions_default({
|
|
730
|
+
margin_range: margin_zones,
|
|
731
|
+
kind: _kind,
|
|
732
|
+
distribution,
|
|
733
|
+
risk_reward: this.risk_reward,
|
|
734
|
+
price_places: this.price_places,
|
|
735
|
+
distribution_params: this.distribution_params
|
|
736
|
+
});
|
|
737
|
+
return entries.sort((a, b) => a - b);
|
|
738
|
+
}
|
|
739
|
+
get_future_zones({
|
|
740
|
+
current_price,
|
|
741
|
+
kind = "long",
|
|
742
|
+
raw
|
|
743
|
+
}) {
|
|
744
|
+
if (raw) {}
|
|
745
|
+
return this.get_future_zones_simple({ current_price, raw, kind });
|
|
746
|
+
const margin_range = this.get_margin_range(current_price, kind);
|
|
747
|
+
let margin_zones = this.get_margin_zones({ current_price });
|
|
748
|
+
let remaining_zones = margin_zones.filter((x) => JSON.stringify(x) != JSON.stringify(margin_range));
|
|
749
|
+
if (margin_range) {
|
|
750
|
+
const difference = Math.abs(margin_range[0] - margin_range[1]);
|
|
751
|
+
const spread = to_f(difference / this.risk_reward, this.price_places);
|
|
752
|
+
let entries;
|
|
753
|
+
const percent_change = this.percent_change / this.risk_reward;
|
|
754
|
+
if (kind === "long") {
|
|
755
|
+
entries = Array.from({ length: Math.floor(this.risk_reward) + 1 }, (_, x) => to_f(margin_range[1] - spread * x, this.price_places));
|
|
756
|
+
} else {
|
|
757
|
+
entries = Array.from({ length: Math.floor(this.risk_reward) + 1 }, (_, x) => to_f(margin_range[1] * Math.pow(1 + percent_change, x), this.price_places));
|
|
758
|
+
}
|
|
759
|
+
if (Math.min(...entries) < this.to_f(current_price) && this.to_f(current_price) < Math.max(...entries)) {
|
|
760
|
+
return entries.sort((a, b) => a - b);
|
|
761
|
+
}
|
|
762
|
+
if (remaining_zones.length > 0) {
|
|
763
|
+
let new_range = remaining_zones[0];
|
|
764
|
+
let entries2 = [];
|
|
765
|
+
let x = 0;
|
|
766
|
+
if (new_range) {
|
|
767
|
+
while (entries2.length < this.risk_reward + 1) {
|
|
768
|
+
if (kind === "long") {
|
|
769
|
+
let value = this.to_f(new_range[1] - spread * x);
|
|
770
|
+
if (value <= current_price) {
|
|
771
|
+
entries2.push(value);
|
|
772
|
+
}
|
|
773
|
+
} else {
|
|
774
|
+
let value = this.to_f(new_range[1] * Math.pow(1 + percent_change, x));
|
|
775
|
+
if (value >= current_price) {
|
|
776
|
+
entries2.push(value);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
x += 1;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return entries2.sort((a, b) => a - b);
|
|
783
|
+
}
|
|
784
|
+
if (remaining_zones.length === 0 && this.to_f(current_price) <= Math.min(...entries)) {
|
|
785
|
+
const next_focus = margin_range[0] * Math.pow(1 + this.percent_change, -1);
|
|
786
|
+
let entries2 = [];
|
|
787
|
+
let x = 0;
|
|
788
|
+
while (entries2.length < this.risk_reward + 1) {
|
|
789
|
+
if (kind === "long") {
|
|
790
|
+
let value = this.to_f(next_focus - spread * x);
|
|
791
|
+
if (value <= this.to_f(current_price)) {
|
|
792
|
+
entries2.push(value);
|
|
793
|
+
}
|
|
794
|
+
} else {
|
|
795
|
+
let value = this.to_f(next_focus * Math.pow(1 + percent_change, x));
|
|
796
|
+
if (value >= this.to_f(current_price)) {
|
|
797
|
+
entries2.push(value);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
x += 1;
|
|
801
|
+
}
|
|
802
|
+
return entries2.sort((a, b) => a - b);
|
|
803
|
+
}
|
|
804
|
+
return entries.sort((a, b) => a - b);
|
|
805
|
+
}
|
|
806
|
+
return [];
|
|
807
|
+
}
|
|
808
|
+
to_f(value, places) {
|
|
809
|
+
return to_f(value, places || this.price_places);
|
|
810
|
+
}
|
|
811
|
+
get_margin_zones({
|
|
812
|
+
current_price,
|
|
813
|
+
kind = "long"
|
|
814
|
+
}) {
|
|
815
|
+
if (this.support && kind === "long") {
|
|
816
|
+
let result = [];
|
|
817
|
+
let start = current_price;
|
|
818
|
+
let counter = 0;
|
|
819
|
+
while (start > this.support) {
|
|
820
|
+
let v = this.get_margin_range(start);
|
|
821
|
+
if (v) {
|
|
822
|
+
result.push(v);
|
|
823
|
+
start = v[0] - this.min_price;
|
|
824
|
+
counter += 1;
|
|
825
|
+
}
|
|
826
|
+
if (counter > 10) {
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return result;
|
|
831
|
+
}
|
|
832
|
+
if (this.resistance) {
|
|
833
|
+
let result = [];
|
|
834
|
+
let start = current_price;
|
|
835
|
+
let counter = 0;
|
|
836
|
+
while (start < this.resistance) {
|
|
837
|
+
let v = this.get_margin_range(start);
|
|
838
|
+
if (v) {
|
|
839
|
+
result.push(v);
|
|
840
|
+
start = v[1] + this.min_price;
|
|
841
|
+
}
|
|
842
|
+
if (counter > 10) {
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return result;
|
|
847
|
+
}
|
|
848
|
+
return [this.get_margin_range(current_price)];
|
|
849
|
+
}
|
|
850
|
+
get_margin_range(current_price, kind = "long") {
|
|
851
|
+
const diff = -this.min_price;
|
|
852
|
+
const zones = _get_zones({
|
|
853
|
+
current_price: current_price + diff,
|
|
854
|
+
focus: this.focus,
|
|
855
|
+
percent_change: this.percent_change,
|
|
856
|
+
places: this.price_places
|
|
857
|
+
}) || [];
|
|
858
|
+
const top_zones = [];
|
|
859
|
+
for (const i of zones) {
|
|
860
|
+
if (i < 0.00000001) {
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
top_zones.push(this.to_f(i));
|
|
864
|
+
}
|
|
865
|
+
if (top_zones.length > 0) {
|
|
866
|
+
const result = top_zones[top_zones.length - 1];
|
|
867
|
+
return [this.to_f(result), this.to_f(result * (1 + this.percent_change))];
|
|
868
|
+
}
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
process_orders({
|
|
872
|
+
current_price,
|
|
873
|
+
stop_loss,
|
|
874
|
+
trade_zones,
|
|
875
|
+
kind = "long"
|
|
876
|
+
}) {
|
|
877
|
+
const number_of_orders = trade_zones.slice(1).length;
|
|
878
|
+
let take_profit = stop_loss * (1 + 2 * this.percent_change);
|
|
879
|
+
if (kind === "short") {
|
|
880
|
+
take_profit = stop_loss * Math.pow(1 + 2 * this.percent_change, -1);
|
|
881
|
+
}
|
|
882
|
+
if (this.take_profit) {
|
|
883
|
+
take_profit = this.take_profit;
|
|
884
|
+
}
|
|
885
|
+
if (number_of_orders > 0) {
|
|
886
|
+
const risk_per_trade = this.get_risk_per_trade(number_of_orders);
|
|
887
|
+
let limit_orders = trade_zones.slice(1).filter((x) => x <= this.to_f(current_price));
|
|
888
|
+
let market_orders = trade_zones.slice(1).filter((x) => x > this.to_f(current_price));
|
|
889
|
+
if (kind === "short") {
|
|
890
|
+
limit_orders = trade_zones.slice(1).filter((x) => x >= this.to_f(current_price));
|
|
891
|
+
market_orders = trade_zones.slice(1).filter((x) => x < this.to_f(current_price));
|
|
892
|
+
}
|
|
893
|
+
if (market_orders.length === 1) {
|
|
894
|
+
limit_orders = limit_orders.concat(market_orders);
|
|
895
|
+
market_orders = [];
|
|
896
|
+
}
|
|
897
|
+
const increase_position = Boolean(this.support) && this.increase_position;
|
|
898
|
+
const market_trades = limit_orders.length > 0 ? market_orders.map((x, i) => {
|
|
899
|
+
const defaultStopLoss = i === 0 ? limit_orders[limit_orders.length - 1] : market_orders[i - 1];
|
|
900
|
+
const y = this.build_trade_dict({
|
|
901
|
+
entry: x,
|
|
902
|
+
stop: increase_position ? this.support : defaultStopLoss,
|
|
903
|
+
risk: risk_per_trade,
|
|
904
|
+
arr: market_orders,
|
|
905
|
+
index: i,
|
|
906
|
+
kind,
|
|
907
|
+
start: market_orders.length + limit_orders.length,
|
|
908
|
+
take_profit
|
|
909
|
+
});
|
|
910
|
+
return y;
|
|
911
|
+
}).filter((y) => y) : [];
|
|
912
|
+
let total_incurred_market_fees = 0;
|
|
913
|
+
if (market_trades.length > 0) {
|
|
914
|
+
let first = market_trades[0];
|
|
915
|
+
if (first) {
|
|
916
|
+
total_incurred_market_fees += first.incurred;
|
|
917
|
+
total_incurred_market_fees += first.fee;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
const default_gap = this.gap;
|
|
921
|
+
const gap_pairs = createGapPairs(limit_orders, default_gap);
|
|
922
|
+
const limit_trades = (limit_orders.map((x, i) => {
|
|
923
|
+
let _base = limit_orders[i - 1];
|
|
924
|
+
let _stops = gap_pairs.find((o) => o[0] === x);
|
|
925
|
+
if (!_stops) {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (_stops) {
|
|
929
|
+
_base = _stops[1];
|
|
930
|
+
}
|
|
931
|
+
const defaultStopLoss = i === 0 ? stop_loss : _base;
|
|
932
|
+
const new_stop = kind === "long" ? this.support : stop_loss;
|
|
933
|
+
let risk_to_use = risk_per_trade;
|
|
934
|
+
if (this.use_kelly) {
|
|
935
|
+
const func = this.kelly_func === "theoretical" ? calculateTheoreticalKelly : this.kelly_func === "position_based" ? calculatePositionBasedKelly : calculateTheoreticalKellyFixed;
|
|
936
|
+
const theoretical_kelly = func({
|
|
937
|
+
current_entry: x,
|
|
938
|
+
zone_prices: limit_orders,
|
|
939
|
+
kind,
|
|
940
|
+
config: {
|
|
941
|
+
price_prediction_model: this.kelly_prediction_model,
|
|
942
|
+
confidence_factor: this.kelly_confidence_factor,
|
|
943
|
+
volatility_adjustment: true
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
risk_to_use = theoretical_kelly * risk_per_trade / this.kelly_minimum_risk;
|
|
947
|
+
}
|
|
948
|
+
const y = this.build_trade_dict({
|
|
949
|
+
entry: x,
|
|
950
|
+
stop: (this.increase_position ? new_stop : defaultStopLoss) || defaultStopLoss,
|
|
951
|
+
risk: risk_to_use,
|
|
952
|
+
arr: limit_orders,
|
|
953
|
+
index: i,
|
|
954
|
+
new_fees: total_incurred_market_fees,
|
|
955
|
+
kind,
|
|
956
|
+
start: market_orders.length + limit_orders.length,
|
|
957
|
+
take_profit
|
|
958
|
+
});
|
|
959
|
+
if (y) {
|
|
960
|
+
y.new_stop = defaultStopLoss;
|
|
961
|
+
}
|
|
962
|
+
return y !== null ? y : undefined;
|
|
963
|
+
}) || []).filter((y) => y !== undefined).filter((y) => {
|
|
964
|
+
const min_options = [0.001, 0.002, 0.003];
|
|
965
|
+
if (min_options.includes(this.minimum_size) && this.symbol.toUpperCase().startsWith("BTC")) {
|
|
966
|
+
return y.quantity <= this.max_quantity;
|
|
967
|
+
}
|
|
968
|
+
return true;
|
|
969
|
+
});
|
|
970
|
+
let total_orders = limit_trades.concat(market_trades);
|
|
971
|
+
if (kind === "short") {}
|
|
972
|
+
if (this.minimum_size && total_orders.length > 0) {
|
|
973
|
+
let payload = total_orders;
|
|
974
|
+
let greater_than_min_size = total_orders.filter((o) => o ? o.quantity >= this.minimum_size : true);
|
|
975
|
+
let less_than_min_size = total_orders.filter((o) => o ? o.quantity < this.minimum_size : true) || total_orders;
|
|
976
|
+
less_than_min_size = groupIntoPairsWithSumLessThan(less_than_min_size, this.minimum_size, "quantity", this.first_order_size);
|
|
977
|
+
less_than_min_size = less_than_min_size.map((q, i) => {
|
|
978
|
+
let avg_entry = determine_average_entry_and_size(q.map((o) => ({
|
|
979
|
+
price: o.entry,
|
|
980
|
+
quantity: o.quantity
|
|
981
|
+
})), this.decimal_places, this.price_places);
|
|
982
|
+
let candidate = q[0];
|
|
983
|
+
candidate.entry = avg_entry.price;
|
|
984
|
+
candidate.quantity = avg_entry.quantity;
|
|
985
|
+
return candidate;
|
|
986
|
+
});
|
|
987
|
+
less_than_min_size = less_than_min_size.map((q, i) => {
|
|
988
|
+
let new_stop = q.new_stop;
|
|
989
|
+
if (i > 0) {
|
|
990
|
+
new_stop = less_than_min_size[i - 1].entry;
|
|
991
|
+
}
|
|
992
|
+
return {
|
|
993
|
+
...q,
|
|
994
|
+
new_stop
|
|
995
|
+
};
|
|
996
|
+
});
|
|
997
|
+
if (greater_than_min_size.length !== total_orders.length) {
|
|
998
|
+
payload = greater_than_min_size.concat(less_than_min_size);
|
|
999
|
+
}
|
|
1000
|
+
return payload;
|
|
1001
|
+
}
|
|
1002
|
+
return total_orders;
|
|
1003
|
+
}
|
|
1004
|
+
return [];
|
|
1005
|
+
}
|
|
1006
|
+
get_risk_per_trade(number_of_orders) {
|
|
1007
|
+
if (this.risk_per_trade) {
|
|
1008
|
+
return this.risk_per_trade;
|
|
1009
|
+
}
|
|
1010
|
+
return this.zone_risk / number_of_orders;
|
|
1011
|
+
}
|
|
1012
|
+
build_trade_dict({
|
|
1013
|
+
entry,
|
|
1014
|
+
stop,
|
|
1015
|
+
risk,
|
|
1016
|
+
arr,
|
|
1017
|
+
index,
|
|
1018
|
+
new_fees = 0,
|
|
1019
|
+
kind = "long",
|
|
1020
|
+
start = 0,
|
|
1021
|
+
take_profit
|
|
1022
|
+
}) {
|
|
1023
|
+
const considered = arr.map((x, i) => i).filter((i) => i > index);
|
|
1024
|
+
const with_quantity = considered.map((x) => {
|
|
1025
|
+
const q = determine_position_size({
|
|
1026
|
+
entry: arr[x],
|
|
1027
|
+
stop: arr[x - 1],
|
|
1028
|
+
budget: risk,
|
|
1029
|
+
places: this.decimal_places
|
|
1030
|
+
});
|
|
1031
|
+
if (!q) {
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
if (this.minimum_size) {
|
|
1035
|
+
if (q < this.minimum_size) {
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return { quantity: q, entry: arr[x] };
|
|
1040
|
+
}).filter((x) => x);
|
|
1041
|
+
if (this.increase_size) {
|
|
1042
|
+
const arr_length = with_quantity.length;
|
|
1043
|
+
with_quantity.forEach((x, i) => {
|
|
1044
|
+
if (x) {
|
|
1045
|
+
x.quantity = x.quantity * (arr_length - i);
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
const fees = with_quantity.map((x) => {
|
|
1050
|
+
return this.to_df(this.fee * x.quantity * x.entry);
|
|
1051
|
+
});
|
|
1052
|
+
const previous_risks = with_quantity.map((x) => {
|
|
1053
|
+
return this.to_df(risk);
|
|
1054
|
+
});
|
|
1055
|
+
const multiplier = start - index;
|
|
1056
|
+
const incurred_fees = fees.reduce((a, b) => a + b, 0) + previous_risks.reduce((a, b) => a + b, 0);
|
|
1057
|
+
if (index === 0) {}
|
|
1058
|
+
let quantity = determine_position_size({
|
|
1059
|
+
entry,
|
|
1060
|
+
stop,
|
|
1061
|
+
budget: risk,
|
|
1062
|
+
places: this.decimal_places,
|
|
1063
|
+
min_size: this.minimum_size
|
|
1064
|
+
});
|
|
1065
|
+
if (!quantity) {
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
if (this.increase_size) {
|
|
1069
|
+
quantity = quantity * multiplier;
|
|
1070
|
+
const new_risk = determine_pnl(entry, stop, quantity, kind);
|
|
1071
|
+
risk = Math.abs(new_risk);
|
|
1072
|
+
}
|
|
1073
|
+
const fee = this.to_df(this.fee * quantity * entry);
|
|
1074
|
+
const increment = Math.abs(arr.length - (index + 1));
|
|
1075
|
+
let pnl = this.to_df(risk) * (this.risk_reward + increment);
|
|
1076
|
+
if (this.minimum_pnl) {
|
|
1077
|
+
pnl = this.minimum_pnl + fee;
|
|
1078
|
+
}
|
|
1079
|
+
let sell_price = determine_close_price({ entry, pnl, quantity, kind });
|
|
1080
|
+
if (take_profit && !this.minimum_pnl) {
|
|
1081
|
+
sell_price = take_profit;
|
|
1082
|
+
pnl = this.to_df(determine_pnl(entry, sell_price, quantity, kind));
|
|
1083
|
+
pnl = pnl + fee;
|
|
1084
|
+
sell_price = determine_close_price({ entry, pnl, quantity, kind });
|
|
1085
|
+
}
|
|
1086
|
+
let risk_sell = sell_price;
|
|
1087
|
+
return {
|
|
1088
|
+
entry,
|
|
1089
|
+
risk: this.to_df(risk),
|
|
1090
|
+
quantity,
|
|
1091
|
+
sell_price: this.to_f(sell_price),
|
|
1092
|
+
risk_sell: this.to_f(risk_sell),
|
|
1093
|
+
stop,
|
|
1094
|
+
pnl,
|
|
1095
|
+
fee,
|
|
1096
|
+
net: this.to_df(pnl - fee),
|
|
1097
|
+
incurred: this.to_df(incurred_fees + new_fees),
|
|
1098
|
+
stop_percent: this.to_df(Math.abs(entry - stop) / entry)
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
to_df(currentPrice, places = "%.3f") {
|
|
1102
|
+
return to_f(currentPrice, places);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// src/helpers/pnl.ts
|
|
1107
|
+
function determine_position_size2(entry, stop, budget) {
|
|
1108
|
+
let stop_percent = Math.abs(entry - stop) / entry;
|
|
1109
|
+
let size = budget / stop_percent / entry;
|
|
1110
|
+
return size;
|
|
1111
|
+
}
|
|
1112
|
+
function determine_risk(entry, stop, quantity) {
|
|
1113
|
+
let stop_percent = Math.abs(entry - stop) / entry;
|
|
1114
|
+
let risk = quantity * stop_percent * entry;
|
|
1115
|
+
return risk;
|
|
1116
|
+
}
|
|
1117
|
+
function determine_close_price2(entry, pnl, quantity, kind, single = false, leverage = 1) {
|
|
1118
|
+
const dollar_value = entry / leverage;
|
|
1119
|
+
const position = dollar_value * quantity;
|
|
1120
|
+
if (position) {
|
|
1121
|
+
let percent = pnl / position;
|
|
1122
|
+
let difference = position * percent / quantity;
|
|
1123
|
+
let result;
|
|
1124
|
+
if (kind === "long") {
|
|
1125
|
+
result = difference + entry;
|
|
1126
|
+
} else {
|
|
1127
|
+
result = entry - difference;
|
|
1128
|
+
}
|
|
1129
|
+
if (single) {
|
|
1130
|
+
return result;
|
|
1131
|
+
}
|
|
1132
|
+
return result;
|
|
1133
|
+
}
|
|
1134
|
+
return 0;
|
|
1135
|
+
}
|
|
1136
|
+
function determine_amount_to_sell(entry, quantity, sell_price, pnl, kind, places = "%.3f") {
|
|
1137
|
+
const _pnl = determine_pnl2(entry, sell_price, quantity, kind);
|
|
1138
|
+
const ratio = pnl / to_f2(Math.abs(_pnl), places);
|
|
1139
|
+
quantity = quantity * ratio;
|
|
1140
|
+
return to_f2(quantity, places);
|
|
1141
|
+
}
|
|
1142
|
+
function determine_pnl2(entry, close_price, quantity, kind, contract_size, places = "%.2f") {
|
|
1143
|
+
if (contract_size) {
|
|
1144
|
+
const direction = kind === "long" ? 1 : -1;
|
|
1145
|
+
return quantity * contract_size * direction * (1 / entry - 1 / close_price);
|
|
1146
|
+
}
|
|
1147
|
+
let difference = entry - close_price;
|
|
1148
|
+
if (kind === "long") {
|
|
1149
|
+
difference = close_price - entry;
|
|
1150
|
+
}
|
|
1151
|
+
return to_f2(difference * quantity, places);
|
|
1152
|
+
}
|
|
1153
|
+
function position(entry, quantity, kind, leverage = 1) {
|
|
1154
|
+
const direction = { long: 1, short: -1 };
|
|
1155
|
+
return parseFloat((direction[kind] * quantity * (entry / leverage)).toFixed(3));
|
|
1156
|
+
}
|
|
1157
|
+
function to_f2(value, places) {
|
|
1158
|
+
if (value) {
|
|
1159
|
+
let pp = parseInt(places.replace("%.", "").replace("f", ""));
|
|
1160
|
+
return parseFloat(value.toFixed(pp));
|
|
1161
|
+
}
|
|
1162
|
+
return value;
|
|
1163
|
+
}
|
|
1164
|
+
var value = {
|
|
1165
|
+
determine_risk,
|
|
1166
|
+
determine_position_size: determine_position_size2,
|
|
1167
|
+
determine_close_price: determine_close_price2,
|
|
1168
|
+
determine_pnl: determine_pnl2,
|
|
1169
|
+
position,
|
|
1170
|
+
determine_amount_to_sell,
|
|
1171
|
+
to_f: to_f2
|
|
1172
|
+
};
|
|
1173
|
+
var pnl_default = value;
|
|
1174
|
+
|
|
1175
|
+
// src/helpers/trade_utils.ts
|
|
1176
|
+
function profitHelper(longPosition, shortPosition, config, contract_size, balance = 0) {
|
|
1177
|
+
let long = { takeProfit: 0, quantity: 0, pnl: 0 };
|
|
1178
|
+
let short = { takeProfit: 0, quantity: 0, pnl: 0 };
|
|
1179
|
+
if (longPosition) {
|
|
1180
|
+
long = config?.getSize2(longPosition, contract_size, balance) || null;
|
|
1181
|
+
}
|
|
1182
|
+
if (shortPosition) {
|
|
1183
|
+
short = config?.getSize2(shortPosition, contract_size, balance) || null;
|
|
1184
|
+
}
|
|
1185
|
+
return { long, short };
|
|
1186
|
+
}
|
|
1187
|
+
function getParamForField(self, configs, field, isGroup) {
|
|
1188
|
+
if (isGroup === "group" && field === "checkbox") {
|
|
1189
|
+
return configs.filter((o) => o.kind === field && o.group === true).map((o) => {
|
|
1190
|
+
let _self = self;
|
|
1191
|
+
let value2 = _self[o.name];
|
|
1192
|
+
return { ...o, value: value2 };
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
let r = configs.find((o) => o.name == field);
|
|
1196
|
+
if (r) {
|
|
1197
|
+
let oo = self;
|
|
1198
|
+
let tt = oo[r.name] || "";
|
|
1199
|
+
r.value = tt;
|
|
1200
|
+
}
|
|
1201
|
+
return r;
|
|
1202
|
+
}
|
|
1203
|
+
function getTradeEntries(entry, min_size, kind, size, spread = 0) {
|
|
1204
|
+
let result = [];
|
|
1205
|
+
let index = 0;
|
|
1206
|
+
let no_of_trades = size > min_size ? Math.round(size / min_size) : 1;
|
|
1207
|
+
while (index < no_of_trades) {
|
|
1208
|
+
if (kind === "long") {
|
|
1209
|
+
result.push({ entry: entry - index * spread, size: min_size });
|
|
1210
|
+
} else {
|
|
1211
|
+
result.push({ entry: entry + index * spread, size: min_size });
|
|
1212
|
+
}
|
|
1213
|
+
index = index + 1;
|
|
1214
|
+
}
|
|
1215
|
+
return result;
|
|
1216
|
+
}
|
|
1217
|
+
function extractValue(_param, condition) {
|
|
1218
|
+
let param;
|
|
1219
|
+
if (condition) {
|
|
1220
|
+
try {
|
|
1221
|
+
let value2 = JSON.parse(_param || "[]");
|
|
1222
|
+
param = value2.map((o) => parseFloat(o));
|
|
1223
|
+
} catch (error) {}
|
|
1224
|
+
} else {
|
|
1225
|
+
param = parseFloat(_param);
|
|
1226
|
+
}
|
|
1227
|
+
return param;
|
|
1228
|
+
}
|
|
1229
|
+
function asCoins(symbol) {
|
|
1230
|
+
let _type = symbol.toLowerCase().includes("usdt") ? "usdt" : "coin";
|
|
1231
|
+
if (symbol.toLowerCase() == "btcusdt") {
|
|
1232
|
+
_type = "usdt";
|
|
1233
|
+
}
|
|
1234
|
+
let result = _type === "usdt" ? symbol.toLowerCase().includes("usdt") ? "USDT" : "BUSD" : symbol.toUpperCase().split("USD_")[0];
|
|
1235
|
+
if (symbol.toLowerCase().includes("-")) {
|
|
1236
|
+
result = result.split("-")[0];
|
|
1237
|
+
}
|
|
1238
|
+
if (symbol.toLowerCase() == "usdt-usd") {}
|
|
1239
|
+
let result2 = _type == "usdt" ? symbol.split(result)[0] : result;
|
|
1240
|
+
if (result.includes("-")) {
|
|
1241
|
+
result2 = result;
|
|
1242
|
+
}
|
|
1243
|
+
return result2;
|
|
1244
|
+
}
|
|
1245
|
+
var SpecialCoins = ["NGN", "USDT", "BUSD", "PAX", "USDC", "EUR"];
|
|
1246
|
+
function allCoins(symbols) {
|
|
1247
|
+
let r = symbols.map((o, i) => asCoins(o));
|
|
1248
|
+
return [...new Set(r), ...SpecialCoins];
|
|
1249
|
+
}
|
|
1250
|
+
function formatPrice(value2, opts = {}) {
|
|
1251
|
+
const { locale = "en-US", currency = "USD" } = opts;
|
|
1252
|
+
const formatter = new Intl.NumberFormat(locale, {
|
|
1253
|
+
currency,
|
|
1254
|
+
style: "currency",
|
|
1255
|
+
maximumFractionDigits: 2
|
|
1256
|
+
});
|
|
1257
|
+
return formatter.format(value2);
|
|
1258
|
+
}
|
|
1259
|
+
function to_f(value2, places = "%.1f") {
|
|
1260
|
+
let v = typeof value2 === "string" ? parseFloat(value2) : value2;
|
|
1261
|
+
const formattedValue = places.replace("%.", "").replace("f", "");
|
|
1262
|
+
return parseFloat(v.toFixed(parseInt(formattedValue)));
|
|
1263
|
+
}
|
|
1264
|
+
function determine_stop_and_size(entry, pnl, take_profit, kind = "long") {
|
|
1265
|
+
const difference = kind === "long" ? take_profit - entry : entry - take_profit;
|
|
1266
|
+
const quantity = pnl / difference;
|
|
1267
|
+
return Math.abs(quantity);
|
|
1268
|
+
}
|
|
1269
|
+
var range = (start, stop, step = 1) => Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + i * step);
|
|
1270
|
+
function determine_amount_to_sell2(entry, quantity, sell_price, pnl, kind, places = "%.3f") {
|
|
1271
|
+
const _pnl = determine_pnl(entry, sell_price, quantity, kind);
|
|
1272
|
+
const ratio = pnl / to_f(Math.abs(_pnl), places);
|
|
1273
|
+
quantity = quantity * ratio;
|
|
1274
|
+
return to_f(quantity, places);
|
|
1275
|
+
}
|
|
1276
|
+
function determine_position_size({
|
|
1277
|
+
entry,
|
|
1278
|
+
stop,
|
|
1279
|
+
budget,
|
|
1280
|
+
percent,
|
|
1281
|
+
min_size,
|
|
1282
|
+
notional_value,
|
|
1283
|
+
as_coin = true,
|
|
1284
|
+
places = "%.3f"
|
|
1285
|
+
}) {
|
|
1286
|
+
let stop_percent = stop ? Math.abs(entry - stop) / entry : percent;
|
|
1287
|
+
if (stop_percent && budget) {
|
|
1288
|
+
let size = budget / stop_percent;
|
|
1289
|
+
let notion_value = size * entry;
|
|
1290
|
+
if (notional_value && notional_value > notion_value) {
|
|
1291
|
+
size = notional_value / entry;
|
|
1292
|
+
}
|
|
1293
|
+
if (as_coin) {
|
|
1294
|
+
size = size / entry;
|
|
1295
|
+
if (min_size && min_size === 1) {
|
|
1296
|
+
return to_f(Math.round(size), places);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return to_f(size, places);
|
|
1300
|
+
}
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
function determine_remaining_entry({
|
|
1304
|
+
risk,
|
|
1305
|
+
max_size,
|
|
1306
|
+
stop_loss,
|
|
1307
|
+
kind,
|
|
1308
|
+
position: position2
|
|
1309
|
+
}) {
|
|
1310
|
+
const avg_entry = determine_avg_entry_based_on_max_size({
|
|
1311
|
+
risk,
|
|
1312
|
+
max_size,
|
|
1313
|
+
stop_loss,
|
|
1314
|
+
kind
|
|
1315
|
+
});
|
|
1316
|
+
const result = avg_entry * max_size - position2.quantity * position2.entry;
|
|
1317
|
+
return result / (max_size - position2.quantity);
|
|
1318
|
+
}
|
|
1319
|
+
function determine_avg_entry_based_on_max_size({
|
|
1320
|
+
risk,
|
|
1321
|
+
max_size,
|
|
1322
|
+
stop_loss,
|
|
1323
|
+
kind = "long"
|
|
1324
|
+
}) {
|
|
1325
|
+
const diff = max_size * stop_loss;
|
|
1326
|
+
if (kind === "long") {
|
|
1327
|
+
return (risk + diff) / max_size;
|
|
1328
|
+
}
|
|
1329
|
+
return (risk - diff) / max_size;
|
|
1330
|
+
}
|
|
1331
|
+
function determine_average_entry_and_size(orders, places = "%.3f", price_places = "%.1f") {
|
|
1332
|
+
const sum_values = orders.reduce((sum, order) => sum + order.price * order.quantity, 0);
|
|
1333
|
+
const total_quantity = orders.reduce((sum, order) => sum + order.quantity, 0);
|
|
1334
|
+
const avg_price = total_quantity ? to_f(sum_values / total_quantity, price_places) : 0;
|
|
1335
|
+
return {
|
|
1336
|
+
entry: avg_price,
|
|
1337
|
+
price: avg_price,
|
|
1338
|
+
quantity: to_f(total_quantity, places)
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
var createArray = (start, stop, step) => {
|
|
1342
|
+
const result = [];
|
|
1343
|
+
let current = start;
|
|
1344
|
+
while (current <= stop) {
|
|
1345
|
+
result.push(current);
|
|
1346
|
+
current += step;
|
|
1347
|
+
}
|
|
1348
|
+
return result;
|
|
1349
|
+
};
|
|
1350
|
+
var groupBy = (xs, key) => {
|
|
1351
|
+
return xs.reduce((rv, x) => {
|
|
1352
|
+
(rv[x[key]] = rv[x[key]] || []).push(x);
|
|
1353
|
+
return rv;
|
|
1354
|
+
}, {});
|
|
1355
|
+
};
|
|
1356
|
+
function fibonacci_analysis({
|
|
1357
|
+
support,
|
|
1358
|
+
resistance,
|
|
1359
|
+
kind = "long",
|
|
1360
|
+
trend = "long",
|
|
1361
|
+
places = "%.1f"
|
|
1362
|
+
}) {
|
|
1363
|
+
const swing_high = trend === "long" ? resistance : support;
|
|
1364
|
+
const swing_low = trend === "long" ? support : resistance;
|
|
1365
|
+
const ranges = [0, 0.236, 0.382, 0.5, 0.618, 0.789, 1, 1.272, 1.414, 1.618];
|
|
1366
|
+
const fib_calc = (p, h, l) => p * (h - l) + l;
|
|
1367
|
+
const fib_values = ranges.map((x) => fib_calc(x, swing_high, swing_low)).map((x) => to_f(x, places));
|
|
1368
|
+
if (kind === "short") {
|
|
1369
|
+
return trend === "long" ? fib_values.reverse() : fib_values;
|
|
1370
|
+
} else {
|
|
1371
|
+
return trend === "short" ? fib_values.reverse() : fib_values;
|
|
1372
|
+
}
|
|
1373
|
+
return fib_values;
|
|
1374
|
+
}
|
|
1375
|
+
var groupIntoPairs = (arr, size) => {
|
|
1376
|
+
const result = [];
|
|
1377
|
+
for (let i = 0;i < arr.length; i += size) {
|
|
1378
|
+
result.push(arr.slice(i, i + size));
|
|
1379
|
+
}
|
|
1380
|
+
return result;
|
|
1381
|
+
};
|
|
1382
|
+
var groupIntoPairsWithSumLessThan = (arr, targetSum, key = "quantity", firstSize = 0) => {
|
|
1383
|
+
if (firstSize) {
|
|
1384
|
+
const totalSize = arr.reduce((sum, order) => sum + order[key], 0);
|
|
1385
|
+
const remainingSize = totalSize - firstSize;
|
|
1386
|
+
let newSum = 0;
|
|
1387
|
+
let newArray = [];
|
|
1388
|
+
let lastIndex = 0;
|
|
1389
|
+
for (let i = 0;i < arr.length; i++) {
|
|
1390
|
+
if (newSum < remainingSize) {
|
|
1391
|
+
newSum += arr[i][key];
|
|
1392
|
+
newArray.push(arr[i]);
|
|
1393
|
+
lastIndex = i;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
const lastGroup = arr.slice(lastIndex + 1);
|
|
1397
|
+
const previousPair = groupInPairs(newArray, key, targetSum);
|
|
1398
|
+
if (lastGroup.length > 0) {
|
|
1399
|
+
previousPair.push(lastGroup);
|
|
1400
|
+
}
|
|
1401
|
+
return previousPair;
|
|
1402
|
+
}
|
|
1403
|
+
return groupInPairs(arr, key, targetSum);
|
|
1404
|
+
};
|
|
1405
|
+
function groupInPairs(_arr, key, targetSum) {
|
|
1406
|
+
const result = [];
|
|
1407
|
+
let currentSum = 0;
|
|
1408
|
+
let currentGroup = [];
|
|
1409
|
+
for (let i = 0;i < _arr.length; i++) {
|
|
1410
|
+
currentSum += _arr[i][key];
|
|
1411
|
+
currentGroup.push(_arr[i]);
|
|
1412
|
+
if (currentSum >= targetSum) {
|
|
1413
|
+
result.push(currentGroup);
|
|
1414
|
+
currentGroup = [];
|
|
1415
|
+
currentSum = 0;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
return result;
|
|
1419
|
+
}
|
|
1420
|
+
var computeTotalAverageForEachTrade = (trades, config) => {
|
|
1421
|
+
let _take_profit = config.take_profit;
|
|
1422
|
+
let kind = config.kind;
|
|
1423
|
+
let entryToUse = kind === "short" ? Math.min(config.entry, config.stop) : Math.max(config.entry, config.stop);
|
|
1424
|
+
let _currentEntry = config.currentEntry || entryToUse;
|
|
1425
|
+
let less = trades.filter((p) => kind === "long" ? p.entry <= _currentEntry : p.entry >= _currentEntry);
|
|
1426
|
+
let rrr = trades.map((r, i) => {
|
|
1427
|
+
let considered = [];
|
|
1428
|
+
if (kind === "long") {
|
|
1429
|
+
considered = trades.filter((p) => p.entry > _currentEntry);
|
|
1430
|
+
} else {
|
|
1431
|
+
considered = trades.filter((p) => p.entry < _currentEntry);
|
|
1432
|
+
}
|
|
1433
|
+
const x_pnl = 0;
|
|
1434
|
+
const remaining = less.filter((o) => {
|
|
1435
|
+
if (kind === "long") {
|
|
1436
|
+
return o.entry >= r.entry;
|
|
1437
|
+
}
|
|
1438
|
+
return o.entry <= r.entry;
|
|
1439
|
+
});
|
|
1440
|
+
if (remaining.length === 0) {
|
|
1441
|
+
return { ...r, pnl: x_pnl };
|
|
1442
|
+
}
|
|
1443
|
+
const start = kind === "long" ? Math.max(...remaining.map((o) => o.entry)) : Math.min(...remaining.map((o) => o.entry));
|
|
1444
|
+
considered = considered.map((o) => ({ ...o, entry: start }));
|
|
1445
|
+
considered = considered.concat(remaining);
|
|
1446
|
+
let avg_entry = determine_average_entry_and_size([
|
|
1447
|
+
...considered.map((o) => ({
|
|
1448
|
+
price: o.entry,
|
|
1449
|
+
quantity: o.quantity
|
|
1450
|
+
})),
|
|
1451
|
+
{
|
|
1452
|
+
price: _currentEntry,
|
|
1453
|
+
quantity: config.currentQty || 0
|
|
1454
|
+
}
|
|
1455
|
+
], config.decimal_places, config.price_places);
|
|
1456
|
+
let _pnl = r.pnl;
|
|
1457
|
+
let sell_price = r.sell_price;
|
|
1458
|
+
let entry_pnl = r.pnl;
|
|
1459
|
+
if (_take_profit) {
|
|
1460
|
+
_pnl = pnl_default.determine_pnl(avg_entry.price, _take_profit, avg_entry.quantity, kind);
|
|
1461
|
+
sell_price = _take_profit;
|
|
1462
|
+
entry_pnl = pnl_default.determine_pnl(r.entry, _take_profit, avg_entry.quantity, kind);
|
|
1463
|
+
}
|
|
1464
|
+
const loss = pnl_default.determine_pnl(avg_entry.price, r.stop, avg_entry.quantity, kind);
|
|
1465
|
+
let new_stop = r.new_stop;
|
|
1466
|
+
const entry_loss = pnl_default.determine_pnl(r.entry, new_stop, avg_entry.quantity, kind);
|
|
1467
|
+
let min_profit = 0;
|
|
1468
|
+
let min_entry_profit = 0;
|
|
1469
|
+
if (config.min_profit) {
|
|
1470
|
+
min_profit = pnl_default.determine_close_price(avg_entry.price, config.min_profit, avg_entry.quantity, kind);
|
|
1471
|
+
min_entry_profit = pnl_default.determine_close_price(r.entry, config.min_profit, avg_entry.quantity, kind);
|
|
1472
|
+
}
|
|
1473
|
+
let x_fee = r.fee;
|
|
1474
|
+
if (config.fee) {
|
|
1475
|
+
x_fee = config.fee * r.stop * avg_entry.quantity;
|
|
1476
|
+
}
|
|
1477
|
+
let tp_close = pnl_default.determine_close_price(r.entry, Math.abs(entry_loss) * (config.rr || 1) + x_fee, avg_entry.quantity, kind);
|
|
1478
|
+
return {
|
|
1479
|
+
...r,
|
|
1480
|
+
x_fee: to_f(x_fee, "%.2f"),
|
|
1481
|
+
avg_entry: avg_entry.price,
|
|
1482
|
+
avg_size: avg_entry.quantity,
|
|
1483
|
+
entry_pnl: to_f(entry_pnl, "%.2f"),
|
|
1484
|
+
entry_loss: to_f(entry_loss, "%.2f"),
|
|
1485
|
+
min_entry_pnl: to_f(min_entry_profit, "%.2f"),
|
|
1486
|
+
pnl: _pnl,
|
|
1487
|
+
neg_pnl: to_f(loss, "%.2f"),
|
|
1488
|
+
sell_price,
|
|
1489
|
+
close_p: to_f(tp_close, "%.2f"),
|
|
1490
|
+
min_pnl: to_f(min_profit, "%.2f"),
|
|
1491
|
+
new_stop
|
|
1492
|
+
};
|
|
1493
|
+
});
|
|
1494
|
+
return rrr;
|
|
1495
|
+
};
|
|
1496
|
+
function getDecimalPlaces(numberString) {
|
|
1497
|
+
let parts = numberString.toString().split(".");
|
|
1498
|
+
if (parts.length == 2) {
|
|
1499
|
+
return parts[1].length;
|
|
1500
|
+
}
|
|
1501
|
+
return 0;
|
|
1502
|
+
}
|
|
1503
|
+
function createGapPairs(arr, gap, item) {
|
|
1504
|
+
if (arr.length === 0) {
|
|
1505
|
+
return [];
|
|
1506
|
+
}
|
|
1507
|
+
const result = [];
|
|
1508
|
+
const firstElement = arr[0];
|
|
1509
|
+
for (let i = arr.length - 1;i >= 0; i--) {
|
|
1510
|
+
const current = arr[i];
|
|
1511
|
+
const gapIndex = i - gap;
|
|
1512
|
+
const pairedElement = gapIndex < 0 ? firstElement : arr[gapIndex];
|
|
1513
|
+
if (current !== pairedElement) {
|
|
1514
|
+
result.push([current, pairedElement]);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
if (item) {
|
|
1518
|
+
let r = result.find((o) => o[0] === item);
|
|
1519
|
+
return r ? [r] : [];
|
|
1520
|
+
}
|
|
1521
|
+
return result;
|
|
1522
|
+
}
|
|
1523
|
+
function logWithLineNumber(...args) {
|
|
1524
|
+
const stack = new Error().stack;
|
|
1525
|
+
const lines = stack?.split(`
|
|
1526
|
+
`).slice(2).map((line) => line.trim());
|
|
1527
|
+
const lineNumber = lines?.[0]?.split(":").pop();
|
|
1528
|
+
console.log(`${lineNumber}:`, ...args);
|
|
1529
|
+
}
|
|
1530
|
+
function computeSellZones(payload) {
|
|
1531
|
+
const { entry, exit, zones = 10 } = payload;
|
|
1532
|
+
const gap = exit / entry;
|
|
1533
|
+
const factor = Math.pow(gap, 1 / zones);
|
|
1534
|
+
const spread = factor - 1;
|
|
1535
|
+
return Array.from({ length: zones }, (_, i) => entry * Math.pow(1 + spread, i));
|
|
1536
|
+
}
|
|
1537
|
+
function determineTPSl(payload) {
|
|
1538
|
+
const { sell_ratio = 1, kind, positions, configs, decimal_places } = payload;
|
|
1539
|
+
const long_settings = configs.long;
|
|
1540
|
+
const short_settings = configs.short;
|
|
1541
|
+
const settings = kind === "long" ? long_settings : short_settings;
|
|
1542
|
+
const reverse_kind = kind === "long" ? "short" : "long";
|
|
1543
|
+
const _position = positions[kind];
|
|
1544
|
+
const reverse_position = positions[reverse_kind];
|
|
1545
|
+
const reduce_ratio = settings.reduce_ratio;
|
|
1546
|
+
const notion = _position.entry * _position.quantity;
|
|
1547
|
+
const profit_percent = settings.profit_percent;
|
|
1548
|
+
const places = decimal_places || reverse_position.decimal_places;
|
|
1549
|
+
if (profit_percent) {
|
|
1550
|
+
let quantity = to_f(_position.quantity * sell_ratio, places);
|
|
1551
|
+
const as_float = parseFloat(profit_percent) * sell_ratio;
|
|
1552
|
+
const pnl = to_f(as_float * notion / 100, "%.2f");
|
|
1553
|
+
const diff = pnl / quantity;
|
|
1554
|
+
const tp_price = to_f(kind === "long" ? _position.entry + diff : _position.entry - diff, _position.price_places);
|
|
1555
|
+
const expected_loss = to_f(parseFloat(reduce_ratio) * pnl, "%.2f");
|
|
1556
|
+
let reduce_quantity = 0;
|
|
1557
|
+
if (reverse_position.quantity > 0) {
|
|
1558
|
+
const total_loss = Math.abs(tp_price - reverse_position.entry) * reverse_position.quantity;
|
|
1559
|
+
const ratio = expected_loss / total_loss;
|
|
1560
|
+
reduce_quantity = to_f(reverse_position.quantity * ratio, places);
|
|
1561
|
+
}
|
|
1562
|
+
return {
|
|
1563
|
+
tp_price,
|
|
1564
|
+
pnl,
|
|
1565
|
+
quantity,
|
|
1566
|
+
reduce_quantity,
|
|
1567
|
+
expected_loss
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
return {
|
|
1571
|
+
tp_price: 0,
|
|
1572
|
+
pnl: 0,
|
|
1573
|
+
quantity: 0,
|
|
1574
|
+
reduce_quantity: 0,
|
|
1575
|
+
expected_loss: 0
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
// src/helpers/shared.ts
|
|
1579
|
+
function getMaxQuantity(x, app_config) {
|
|
1580
|
+
let max_quantity = app_config.max_quantity;
|
|
1581
|
+
if (max_quantity) {
|
|
1582
|
+
return x <= max_quantity;
|
|
1583
|
+
}
|
|
1584
|
+
if (app_config.symbol === "BTCUSDT") {
|
|
1585
|
+
max_quantity = 0.03;
|
|
1586
|
+
}
|
|
1587
|
+
if (app_config.symbol?.toLowerCase().startsWith("sol")) {
|
|
1588
|
+
max_quantity = 2;
|
|
1589
|
+
}
|
|
1590
|
+
if (!max_quantity) {
|
|
1591
|
+
return true;
|
|
1592
|
+
}
|
|
1593
|
+
return x <= max_quantity;
|
|
1594
|
+
}
|
|
1595
|
+
function buildConfig(app_config, {
|
|
1596
|
+
take_profit,
|
|
1597
|
+
entry,
|
|
1598
|
+
stop,
|
|
1599
|
+
raw_instance,
|
|
1600
|
+
risk,
|
|
1601
|
+
no_of_trades,
|
|
1602
|
+
min_profit = 0,
|
|
1603
|
+
risk_reward,
|
|
1604
|
+
kind,
|
|
1605
|
+
increase,
|
|
1606
|
+
gap,
|
|
1607
|
+
rr = 1,
|
|
1608
|
+
price_places = "%.1f",
|
|
1609
|
+
decimal_places = "%.3f",
|
|
1610
|
+
use_kelly = false,
|
|
1611
|
+
kelly_confidence_factor = 0.95,
|
|
1612
|
+
kelly_minimum_risk = 0.2,
|
|
1613
|
+
kelly_prediction_model = "exponential",
|
|
1614
|
+
kelly_func = "theoretical",
|
|
1615
|
+
min_avg_size = 0,
|
|
1616
|
+
distribution,
|
|
1617
|
+
distribution_params
|
|
1618
|
+
}) {
|
|
1619
|
+
let fee = app_config.fee / 100;
|
|
1620
|
+
let working_risk = risk || app_config.risk_per_trade;
|
|
1621
|
+
let trade_no = no_of_trades || app_config.risk_reward;
|
|
1622
|
+
const config = {
|
|
1623
|
+
focus: app_config.focus,
|
|
1624
|
+
fee,
|
|
1625
|
+
budget: app_config.budget,
|
|
1626
|
+
risk_reward: risk_reward || trade_no,
|
|
1627
|
+
support: app_config.support,
|
|
1628
|
+
resistance: app_config.resistance,
|
|
1629
|
+
price_places: app_config.price_places || price_places,
|
|
1630
|
+
decimal_places,
|
|
1631
|
+
percent_change: app_config.percent_change / app_config.tradeSplit,
|
|
1632
|
+
risk_per_trade: working_risk,
|
|
1633
|
+
take_profit: take_profit || app_config.take_profit,
|
|
1634
|
+
increase_position: increase,
|
|
1635
|
+
minimum_size: app_config.min_size,
|
|
1636
|
+
entry,
|
|
1637
|
+
stop,
|
|
1638
|
+
kind: app_config.kind,
|
|
1639
|
+
gap,
|
|
1640
|
+
min_profit: min_profit || app_config.min_profit,
|
|
1641
|
+
rr: rr || 1,
|
|
1642
|
+
use_kelly: use_kelly || app_config.kelly?.use_kelly,
|
|
1643
|
+
kelly_confidence_factor: kelly_confidence_factor || app_config.kelly?.kelly_confidence_factor,
|
|
1644
|
+
kelly_minimum_risk: kelly_minimum_risk || app_config.kelly?.kelly_minimum_risk,
|
|
1645
|
+
kelly_prediction_model: kelly_prediction_model || app_config.kelly?.kelly_prediction_model,
|
|
1646
|
+
kelly_func: kelly_func || app_config.kelly?.kelly_func,
|
|
1647
|
+
symbol: app_config.symbol,
|
|
1648
|
+
max_quantity: app_config.max_quantity,
|
|
1649
|
+
distribution_params: distribution_params || app_config.distribution_params
|
|
1650
|
+
};
|
|
1651
|
+
const instance = new Signal(config);
|
|
1652
|
+
if (raw_instance) {
|
|
1653
|
+
return instance;
|
|
1654
|
+
}
|
|
1655
|
+
if (!stop) {
|
|
1656
|
+
return [];
|
|
1657
|
+
}
|
|
1658
|
+
const condition = (kind === "long" ? entry > app_config.support : entry >= app_config.support) && stop >= app_config.support * 0.999;
|
|
1659
|
+
if (kind === "short") {}
|
|
1660
|
+
const result = entry === stop ? [] : condition ? instance.build_entry({
|
|
1661
|
+
current_price: entry,
|
|
1662
|
+
stop_loss: stop,
|
|
1663
|
+
risk: working_risk,
|
|
1664
|
+
kind: kind || app_config.kind,
|
|
1665
|
+
no_of_trades: trade_no,
|
|
1666
|
+
distribution,
|
|
1667
|
+
distribution_params
|
|
1668
|
+
}) || [] : [];
|
|
1669
|
+
const new_trades = computeTotalAverageForEachTrade(result, config);
|
|
1670
|
+
let filtered = new_trades.filter((o) => o.avg_size > min_avg_size);
|
|
1671
|
+
return computeTotalAverageForEachTrade(filtered, config);
|
|
1672
|
+
}
|
|
1673
|
+
function buildAvg({
|
|
1674
|
+
_trades,
|
|
1675
|
+
kind
|
|
1676
|
+
}) {
|
|
1677
|
+
let avg = determine_average_entry_and_size(_trades?.map((r) => ({
|
|
1678
|
+
price: r.entry,
|
|
1679
|
+
quantity: r.quantity
|
|
1680
|
+
})) || []);
|
|
1681
|
+
const stop_prices = _trades.map((o) => o.stop);
|
|
1682
|
+
const stop_loss = kind === "long" ? Math.min(...stop_prices) : Math.max(...stop_prices);
|
|
1683
|
+
avg.pnl = pnl_default.determine_pnl(avg.price, stop_loss, avg.quantity, kind);
|
|
1684
|
+
return avg;
|
|
1685
|
+
}
|
|
1686
|
+
function sortedBuildConfig(app_config, options) {
|
|
1687
|
+
const sorted = buildConfig(app_config, options).sort((a, b) => app_config.kind === "long" ? a.entry - b.entry : b.entry - b.entry).filter((x) => {
|
|
1688
|
+
return getMaxQuantity(x.quantity, app_config);
|
|
1689
|
+
});
|
|
1690
|
+
return sorted.map((k, i) => {
|
|
1691
|
+
const arrSet = sorted.slice(0, i + 1);
|
|
1692
|
+
const avg_values = determine_average_entry_and_size(arrSet.map((u) => ({ price: u.entry, quantity: u.quantity })), app_config.decimal_places, app_config.price_places);
|
|
1693
|
+
return {
|
|
1694
|
+
...k,
|
|
1695
|
+
reverse_avg_entry: avg_values.price,
|
|
1696
|
+
reverse_avg_quantity: avg_values.quantity
|
|
1697
|
+
};
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
function get_app_config_and_max_size(config, payload) {
|
|
1701
|
+
const app_config = {
|
|
1702
|
+
kind: payload.kind,
|
|
1703
|
+
entry: payload.entry,
|
|
1704
|
+
stop: payload.stop,
|
|
1705
|
+
risk_per_trade: config.risk,
|
|
1706
|
+
risk_reward: config.risk_reward || 199,
|
|
1707
|
+
support: to_f(config.support, config.price_places),
|
|
1708
|
+
resistance: to_f(config.resistance, config.price_places),
|
|
1709
|
+
focus: payload.entry,
|
|
1710
|
+
fee: 0,
|
|
1711
|
+
percent_change: config.stop_percent / 100,
|
|
1712
|
+
tradeSplit: 1,
|
|
1713
|
+
gap: 1,
|
|
1714
|
+
min_size: config.min_size,
|
|
1715
|
+
budget: 0,
|
|
1716
|
+
price_places: config.price_places,
|
|
1717
|
+
decimal_places: config.decimal_places,
|
|
1718
|
+
min_profit: config.profit_percent * config.profit / 100,
|
|
1719
|
+
symbol: config.symbol,
|
|
1720
|
+
max_quantity: config.max_quantity
|
|
1721
|
+
};
|
|
1722
|
+
const initialResult = sortedBuildConfig(app_config, {
|
|
1723
|
+
entry: app_config.entry,
|
|
1724
|
+
stop: app_config.stop,
|
|
1725
|
+
kind: app_config.kind,
|
|
1726
|
+
risk: app_config.risk_per_trade,
|
|
1727
|
+
risk_reward: app_config.risk_reward,
|
|
1728
|
+
increase: true,
|
|
1729
|
+
gap: app_config.gap,
|
|
1730
|
+
price_places: app_config.price_places,
|
|
1731
|
+
decimal_places: app_config.decimal_places,
|
|
1732
|
+
use_kelly: payload.use_kelly,
|
|
1733
|
+
kelly_confidence_factor: payload.kelly_confidence_factor,
|
|
1734
|
+
kelly_minimum_risk: payload.kelly_minimum_risk,
|
|
1735
|
+
kelly_prediction_model: payload.kelly_prediction_model,
|
|
1736
|
+
kelly_func: payload.kelly_func,
|
|
1737
|
+
distribution: payload.distribution,
|
|
1738
|
+
distribution_params: payload.distribution_params
|
|
1739
|
+
});
|
|
1740
|
+
const max_size = initialResult[0]?.avg_size;
|
|
1741
|
+
const last_value = initialResult[0];
|
|
1742
|
+
const entries = initialResult.map((x) => ({
|
|
1743
|
+
entry: x.entry,
|
|
1744
|
+
avg_entry: x.avg_entry,
|
|
1745
|
+
avg_size: x.avg_size,
|
|
1746
|
+
neg_pnl: x.neg_pnl,
|
|
1747
|
+
quantity: x.quantity
|
|
1748
|
+
}));
|
|
1749
|
+
return {
|
|
1750
|
+
app_config,
|
|
1751
|
+
max_size,
|
|
1752
|
+
last_value,
|
|
1753
|
+
entries
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
function buildAppConfig(config, payload) {
|
|
1757
|
+
const { app_config, max_size, last_value, entries } = get_app_config_and_max_size({
|
|
1758
|
+
...config,
|
|
1759
|
+
risk: payload.risk,
|
|
1760
|
+
profit: payload.profit || 500,
|
|
1761
|
+
risk_reward: payload.risk_reward,
|
|
1762
|
+
accounts: [],
|
|
1763
|
+
reduce_percent: 90,
|
|
1764
|
+
reverse_factor: 1,
|
|
1765
|
+
profit_percent: 0,
|
|
1766
|
+
kind: payload.entry > payload.stop ? "long" : "short",
|
|
1767
|
+
symbol: payload.symbol || config.symbol
|
|
1768
|
+
}, {
|
|
1769
|
+
entry: payload.entry,
|
|
1770
|
+
stop: payload.stop,
|
|
1771
|
+
kind: payload.entry > payload.stop ? "long" : "short",
|
|
1772
|
+
use_kelly: payload.use_kelly,
|
|
1773
|
+
kelly_confidence_factor: payload.kelly_confidence_factor,
|
|
1774
|
+
kelly_minimum_risk: payload.kelly_minimum_risk,
|
|
1775
|
+
kelly_prediction_model: payload.kelly_prediction_model,
|
|
1776
|
+
kelly_func: payload.kelly_func,
|
|
1777
|
+
distribution: payload.distribution,
|
|
1778
|
+
distribution_params: payload.distribution_params
|
|
1779
|
+
});
|
|
1780
|
+
app_config.max_size = max_size;
|
|
1781
|
+
app_config.entry = payload.entry || app_config.entry;
|
|
1782
|
+
app_config.stop = payload.stop || app_config.stop;
|
|
1783
|
+
app_config.last_value = last_value;
|
|
1784
|
+
app_config.entries = entries;
|
|
1785
|
+
app_config.kelly = {
|
|
1786
|
+
use_kelly: payload.use_kelly,
|
|
1787
|
+
kelly_confidence_factor: payload.kelly_confidence_factor,
|
|
1788
|
+
kelly_minimum_risk: payload.kelly_minimum_risk,
|
|
1789
|
+
kelly_prediction_model: payload.kelly_prediction_model,
|
|
1790
|
+
kelly_func: payload.kelly_func
|
|
1791
|
+
};
|
|
1792
|
+
app_config.distribution = payload.distribution;
|
|
1793
|
+
app_config.distribution_params = payload.distribution_params;
|
|
1794
|
+
return app_config;
|
|
1795
|
+
}
|
|
1796
|
+
function getOptimumStopAndRisk(app_config, params) {
|
|
1797
|
+
const { max_size, target_stop, distribution, distribution_params: _distribution_params } = params;
|
|
1798
|
+
const isLong = app_config.kind === "long";
|
|
1799
|
+
const stopRange = Math.abs(app_config.entry - target_stop) * 0.5;
|
|
1800
|
+
let low_stop = isLong ? target_stop - stopRange : Math.max(target_stop - stopRange, app_config.entry);
|
|
1801
|
+
let high_stop = isLong ? Math.min(target_stop + stopRange, app_config.entry) : target_stop + stopRange;
|
|
1802
|
+
let optimal_stop = target_stop;
|
|
1803
|
+
let best_stop_result = null;
|
|
1804
|
+
let best_entry_diff = Infinity;
|
|
1805
|
+
console.log(`Finding optimal stop for ${isLong ? "LONG" : "SHORT"} position. Target: ${target_stop}, Search range: ${low_stop} to ${high_stop}`);
|
|
1806
|
+
let iterations = 0;
|
|
1807
|
+
const MAX_ITERATIONS = 50;
|
|
1808
|
+
while (Math.abs(high_stop - low_stop) > 0.1 && iterations < MAX_ITERATIONS) {
|
|
1809
|
+
iterations++;
|
|
1810
|
+
const mid_stop = (low_stop + high_stop) / 2;
|
|
1811
|
+
const result = sortedBuildConfig(app_config, {
|
|
1812
|
+
entry: app_config.entry,
|
|
1813
|
+
stop: mid_stop,
|
|
1814
|
+
kind: app_config.kind,
|
|
1815
|
+
risk: app_config.risk_per_trade,
|
|
1816
|
+
risk_reward: app_config.risk_reward,
|
|
1817
|
+
increase: true,
|
|
1818
|
+
gap: app_config.gap,
|
|
1819
|
+
price_places: app_config.price_places,
|
|
1820
|
+
decimal_places: app_config.decimal_places,
|
|
1821
|
+
distribution,
|
|
1822
|
+
distribution_params: _distribution_params
|
|
1823
|
+
});
|
|
1824
|
+
if (result.length === 0) {
|
|
1825
|
+
if (isLong) {
|
|
1826
|
+
low_stop = mid_stop;
|
|
1827
|
+
} else {
|
|
1828
|
+
high_stop = mid_stop;
|
|
1829
|
+
}
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1832
|
+
const first_entry = result[0].entry;
|
|
1833
|
+
const entry_diff = Math.abs(first_entry - target_stop);
|
|
1834
|
+
console.log(`Stop: ${mid_stop.toFixed(2)}, First Entry: ${first_entry.toFixed(2)}, Diff: ${entry_diff.toFixed(2)}`);
|
|
1835
|
+
if (entry_diff < best_entry_diff) {
|
|
1836
|
+
best_entry_diff = entry_diff;
|
|
1837
|
+
optimal_stop = mid_stop;
|
|
1838
|
+
best_stop_result = result;
|
|
1839
|
+
}
|
|
1840
|
+
if (first_entry < target_stop) {
|
|
1841
|
+
if (isLong) {
|
|
1842
|
+
low_stop = mid_stop;
|
|
1843
|
+
} else {
|
|
1844
|
+
low_stop = mid_stop;
|
|
1845
|
+
}
|
|
1846
|
+
} else {
|
|
1847
|
+
if (isLong) {
|
|
1848
|
+
high_stop = mid_stop;
|
|
1849
|
+
} else {
|
|
1850
|
+
high_stop = mid_stop;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
if (entry_diff < 1)
|
|
1854
|
+
break;
|
|
1855
|
+
}
|
|
1856
|
+
console.log(`Found optimal stop: ${optimal_stop.toFixed(2)} that gives first_entry: ${best_stop_result?.[0]?.entry?.toFixed(2)}, difference: ${best_entry_diff.toFixed(2)} after ${iterations} iterations`);
|
|
1857
|
+
let low_risk = 10;
|
|
1858
|
+
let high_risk = params.highest_risk || 2000;
|
|
1859
|
+
let optimal_risk = app_config.risk_per_trade;
|
|
1860
|
+
let best_size_result = best_stop_result;
|
|
1861
|
+
let best_size_diff = Infinity;
|
|
1862
|
+
console.log(`Finding optimal risk_per_trade for size target: ${max_size} (never exceeding it)`);
|
|
1863
|
+
iterations = 0;
|
|
1864
|
+
while (Math.abs(high_risk - low_risk) > 0.1 && iterations < MAX_ITERATIONS) {
|
|
1865
|
+
iterations++;
|
|
1866
|
+
const mid_risk = (low_risk + high_risk) / 2;
|
|
1867
|
+
const result = sortedBuildConfig(app_config, {
|
|
1868
|
+
entry: app_config.entry,
|
|
1869
|
+
stop: optimal_stop,
|
|
1870
|
+
kind: app_config.kind,
|
|
1871
|
+
risk: mid_risk,
|
|
1872
|
+
risk_reward: app_config.risk_reward,
|
|
1873
|
+
increase: true,
|
|
1874
|
+
gap: app_config.gap,
|
|
1875
|
+
price_places: app_config.price_places,
|
|
1876
|
+
decimal_places: app_config.decimal_places,
|
|
1877
|
+
distribution,
|
|
1878
|
+
distribution_params: _distribution_params
|
|
1879
|
+
});
|
|
1880
|
+
if (result.length === 0) {
|
|
1881
|
+
high_risk = mid_risk;
|
|
1882
|
+
continue;
|
|
1883
|
+
}
|
|
1884
|
+
const first_entry = result[0];
|
|
1885
|
+
const avg_size = first_entry.avg_size;
|
|
1886
|
+
console.log(`Risk: ${mid_risk.toFixed(2)}, Avg Size: ${avg_size.toFixed(4)}, Target: ${max_size}`);
|
|
1887
|
+
if (avg_size <= max_size) {
|
|
1888
|
+
const size_diff = max_size - avg_size;
|
|
1889
|
+
if (size_diff < best_size_diff) {
|
|
1890
|
+
best_size_diff = size_diff;
|
|
1891
|
+
optimal_risk = mid_risk;
|
|
1892
|
+
best_size_result = result;
|
|
1893
|
+
}
|
|
1894
|
+
low_risk = mid_risk;
|
|
1895
|
+
} else {
|
|
1896
|
+
high_risk = mid_risk;
|
|
1897
|
+
}
|
|
1898
|
+
if (best_size_diff < 0.001)
|
|
1899
|
+
break;
|
|
1900
|
+
}
|
|
1901
|
+
console.log(`Found optimal risk_per_trade: ${optimal_risk.toFixed(2)} that gives avg_size: ${best_size_result?.[0]?.avg_size?.toFixed(4)}, difference: ${best_size_diff.toFixed(4)} after ${iterations} iterations`);
|
|
1902
|
+
let final_risk = optimal_risk;
|
|
1903
|
+
let final_result = best_size_result;
|
|
1904
|
+
if (best_size_result?.[0]?.avg_size < max_size) {
|
|
1905
|
+
console.log(`Current avg_size (${best_size_result?.[0].avg_size.toFixed(4)}) is less than target (${max_size}). Increasing risk to reach target...`);
|
|
1906
|
+
let current_risk = optimal_risk;
|
|
1907
|
+
const risk_increment = 5;
|
|
1908
|
+
iterations = 0;
|
|
1909
|
+
while (iterations < MAX_ITERATIONS) {
|
|
1910
|
+
iterations++;
|
|
1911
|
+
current_risk += risk_increment;
|
|
1912
|
+
if (current_risk > 5000)
|
|
1913
|
+
break;
|
|
1914
|
+
const result = sortedBuildConfig(app_config, {
|
|
1915
|
+
entry: app_config.entry,
|
|
1916
|
+
stop: optimal_stop,
|
|
1917
|
+
kind: app_config.kind,
|
|
1918
|
+
risk: current_risk,
|
|
1919
|
+
risk_reward: app_config.risk_reward,
|
|
1920
|
+
increase: true,
|
|
1921
|
+
gap: app_config.gap,
|
|
1922
|
+
price_places: app_config.price_places,
|
|
1923
|
+
decimal_places: app_config.decimal_places,
|
|
1924
|
+
distribution,
|
|
1925
|
+
distribution_params: _distribution_params
|
|
1926
|
+
});
|
|
1927
|
+
if (result.length === 0)
|
|
1928
|
+
continue;
|
|
1929
|
+
const avg_size = result[0].avg_size;
|
|
1930
|
+
console.log(`Increased Risk: ${current_risk.toFixed(2)}, New Avg Size: ${avg_size.toFixed(4)}`);
|
|
1931
|
+
if (avg_size >= max_size) {
|
|
1932
|
+
final_risk = current_risk;
|
|
1933
|
+
final_result = result;
|
|
1934
|
+
console.log(`Target size reached! Final risk: ${final_risk.toFixed(2)}, Final avg_size: ${avg_size.toFixed(4)}`);
|
|
1935
|
+
break;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
return {
|
|
1940
|
+
optimal_stop: to_f(optimal_stop, app_config.price_places),
|
|
1941
|
+
optimal_risk: to_f(final_risk, app_config.price_places),
|
|
1942
|
+
avg_size: final_result?.[0]?.avg_size || 0,
|
|
1943
|
+
avg_entry: final_result?.[0]?.avg_entry || 0,
|
|
1944
|
+
result: final_result,
|
|
1945
|
+
first_entry: final_result?.[0]?.entry || 0,
|
|
1946
|
+
neg_pnl: final_result?.[0]?.neg_pnl || 0,
|
|
1947
|
+
risk_reward: app_config.risk_reward,
|
|
1948
|
+
size_diff: Math.abs((final_result?.[0]?.avg_size || 0) - max_size),
|
|
1949
|
+
entry_diff: best_entry_diff
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
function generate_config_params(app_config, payload) {
|
|
1953
|
+
const { result, ...optimum } = getOptimumStopAndRisk(app_config, {
|
|
1954
|
+
max_size: app_config.max_size,
|
|
1955
|
+
highest_risk: payload.risk * 2,
|
|
1956
|
+
target_stop: app_config.stop
|
|
1957
|
+
});
|
|
1958
|
+
return {
|
|
1959
|
+
entry: payload.entry,
|
|
1960
|
+
stop: optimum.optimal_stop,
|
|
1961
|
+
avg_size: optimum.avg_size,
|
|
1962
|
+
avg_entry: optimum.avg_entry,
|
|
1963
|
+
risk_reward: payload.risk_reward,
|
|
1964
|
+
neg_pnl: optimum.neg_pnl,
|
|
1965
|
+
risk: payload.risk
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
function determine_break_even_price(payload) {
|
|
1969
|
+
const { long_position, short_position, fee_percent = 0.05 / 100 } = payload;
|
|
1970
|
+
const long_notional = long_position.entry * long_position.quantity;
|
|
1971
|
+
const short_notional = short_position.entry * short_position.quantity;
|
|
1972
|
+
const net_quantity = long_position.quantity - short_position.quantity;
|
|
1973
|
+
const net_capital = Math.abs(long_notional - short_notional);
|
|
1974
|
+
const break_even_price = net_capital / (Math.abs(net_quantity) + fee_percent * Math.abs(net_quantity));
|
|
1975
|
+
return {
|
|
1976
|
+
price: break_even_price,
|
|
1977
|
+
direction: net_quantity > 0 ? "long" : "short"
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
function determine_amount_to_buy(payload) {
|
|
1981
|
+
const {
|
|
1982
|
+
orders,
|
|
1983
|
+
kind,
|
|
1984
|
+
decimal_places = "%.3f",
|
|
1985
|
+
position: position2,
|
|
1986
|
+
existingOrders
|
|
1987
|
+
} = payload;
|
|
1988
|
+
const totalQuantity = orders.reduce((sum, order) => sum + (order.quantity || 0), 0);
|
|
1989
|
+
let runningTotal = to_f(totalQuantity, decimal_places);
|
|
1990
|
+
let sortedOrders = [...orders].sort((a, b) => (a.entry || 0) - (b.entry || 0));
|
|
1991
|
+
if (kind === "short") {
|
|
1992
|
+
sortedOrders.reverse();
|
|
1993
|
+
}
|
|
1994
|
+
const withCumulative = [];
|
|
1995
|
+
for (const order of sortedOrders) {
|
|
1996
|
+
withCumulative.push({
|
|
1997
|
+
...order,
|
|
1998
|
+
cumulative_quantity: runningTotal
|
|
1999
|
+
});
|
|
2000
|
+
runningTotal -= order.quantity;
|
|
2001
|
+
runningTotal = to_f(runningTotal, decimal_places);
|
|
2002
|
+
}
|
|
2003
|
+
let filteredOrders = withCumulative.filter((order) => (order.cumulative_quantity || 0) > position2?.quantity).map((order) => ({
|
|
2004
|
+
...order,
|
|
2005
|
+
price: order.entry,
|
|
2006
|
+
kind,
|
|
2007
|
+
side: kind.toLowerCase() === "long" ? "buy" : "sell"
|
|
2008
|
+
}));
|
|
2009
|
+
filteredOrders = filteredOrders.filter((k) => !existingOrders.map((j) => j.price).includes(k.price));
|
|
2010
|
+
return filteredOrders;
|
|
2011
|
+
}
|
|
2012
|
+
function generateOptimumAppConfig(config, payload, position2) {
|
|
2013
|
+
let low_risk = payload.start_risk;
|
|
2014
|
+
let high_risk = payload.max_risk || 50000;
|
|
2015
|
+
let best_risk = null;
|
|
2016
|
+
let best_app_config = null;
|
|
2017
|
+
const tolerance = 0.1;
|
|
2018
|
+
const max_iterations = 150;
|
|
2019
|
+
let iterations = 0;
|
|
2020
|
+
while (high_risk - low_risk > tolerance && iterations < max_iterations) {
|
|
2021
|
+
iterations++;
|
|
2022
|
+
const mid_risk = (low_risk + high_risk) / 2;
|
|
2023
|
+
const {
|
|
2024
|
+
app_config: current_app_config,
|
|
2025
|
+
max_size,
|
|
2026
|
+
last_value,
|
|
2027
|
+
entries
|
|
2028
|
+
} = get_app_config_and_max_size({
|
|
2029
|
+
...config,
|
|
2030
|
+
risk: mid_risk,
|
|
2031
|
+
profit_percent: config.profit_percent || 0,
|
|
2032
|
+
profit: config.profit || 500,
|
|
2033
|
+
risk_reward: payload.risk_reward,
|
|
2034
|
+
kind: position2.kind,
|
|
2035
|
+
price_places: config.price_places,
|
|
2036
|
+
decimal_places: config.decimal_places,
|
|
2037
|
+
accounts: config.accounts || [],
|
|
2038
|
+
reduce_percent: config.reduce_percent || 90,
|
|
2039
|
+
reverse_factor: config.reverse_factor || 1,
|
|
2040
|
+
symbol: config.symbol || "",
|
|
2041
|
+
support: config.support || 0,
|
|
2042
|
+
resistance: config.resistance || 0,
|
|
2043
|
+
min_size: config.min_size || 0,
|
|
2044
|
+
stop_percent: config.stop_percent || 0
|
|
2045
|
+
}, {
|
|
2046
|
+
entry: payload.entry,
|
|
2047
|
+
stop: payload.stop,
|
|
2048
|
+
kind: position2.kind,
|
|
2049
|
+
distribution: payload.distribution
|
|
2050
|
+
});
|
|
2051
|
+
current_app_config.max_size = max_size;
|
|
2052
|
+
current_app_config.last_value = last_value;
|
|
2053
|
+
current_app_config.entries = entries;
|
|
2054
|
+
current_app_config.risk_reward = payload.risk_reward;
|
|
2055
|
+
const full_trades = sortedBuildConfig(current_app_config, {
|
|
2056
|
+
entry: current_app_config.entry,
|
|
2057
|
+
stop: current_app_config.stop,
|
|
2058
|
+
kind: current_app_config.kind,
|
|
2059
|
+
risk: current_app_config.risk_per_trade,
|
|
2060
|
+
risk_reward: current_app_config.risk_reward,
|
|
2061
|
+
increase: true,
|
|
2062
|
+
gap: current_app_config.gap,
|
|
2063
|
+
price_places: current_app_config.price_places,
|
|
2064
|
+
decimal_places: current_app_config.decimal_places,
|
|
2065
|
+
distribution: payload.distribution
|
|
2066
|
+
});
|
|
2067
|
+
if (full_trades.length === 0) {
|
|
2068
|
+
high_risk = mid_risk;
|
|
2069
|
+
continue;
|
|
2070
|
+
}
|
|
2071
|
+
const trades = determine_amount_to_buy({
|
|
2072
|
+
orders: full_trades,
|
|
2073
|
+
kind: position2.kind,
|
|
2074
|
+
decimal_places: current_app_config.decimal_places,
|
|
2075
|
+
price_places: current_app_config.price_places,
|
|
2076
|
+
position: position2,
|
|
2077
|
+
existingOrders: []
|
|
2078
|
+
});
|
|
2079
|
+
if (trades.length === 0) {
|
|
2080
|
+
low_risk = mid_risk;
|
|
2081
|
+
continue;
|
|
2082
|
+
}
|
|
2083
|
+
const last_trade = trades[trades.length - 1];
|
|
2084
|
+
const last_entry = last_trade.entry;
|
|
2085
|
+
if (position2.kind === "long") {
|
|
2086
|
+
if (last_entry > position2.entry) {
|
|
2087
|
+
high_risk = mid_risk;
|
|
2088
|
+
} else {
|
|
2089
|
+
best_risk = mid_risk;
|
|
2090
|
+
best_app_config = current_app_config;
|
|
2091
|
+
low_risk = mid_risk;
|
|
2092
|
+
}
|
|
2093
|
+
} else {
|
|
2094
|
+
if (last_entry < position2.entry) {
|
|
2095
|
+
high_risk = mid_risk;
|
|
2096
|
+
} else {
|
|
2097
|
+
best_risk = mid_risk;
|
|
2098
|
+
best_app_config = current_app_config;
|
|
2099
|
+
low_risk = mid_risk;
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
if (iterations >= max_iterations) {
|
|
2104
|
+
console.warn(`generateAppConfig: Reached max iterations (${max_iterations}) without converging. Returning best found result.`);
|
|
2105
|
+
} else if (best_app_config) {
|
|
2106
|
+
console.log(`Search finished. Best Risk: ${best_risk?.toFixed(2)}, Final Last Entry: ${best_app_config.last_value?.entry?.toFixed(4)}`);
|
|
2107
|
+
} else {
|
|
2108
|
+
console.warn(`generateAppConfig: Could not find a valid risk configuration.`);
|
|
2109
|
+
}
|
|
2110
|
+
if (!best_app_config) {
|
|
2111
|
+
return null;
|
|
2112
|
+
}
|
|
2113
|
+
best_app_config.entries = determine_amount_to_buy({
|
|
2114
|
+
orders: best_app_config.entries,
|
|
2115
|
+
kind: position2.kind,
|
|
2116
|
+
decimal_places: best_app_config.decimal_places,
|
|
2117
|
+
price_places: best_app_config.price_places,
|
|
2118
|
+
position: position2,
|
|
2119
|
+
existingOrders: []
|
|
2120
|
+
});
|
|
2121
|
+
return best_app_config;
|
|
2122
|
+
}
|
|
2123
|
+
function determineOptimumReward(payload) {
|
|
2124
|
+
const {
|
|
2125
|
+
app_config,
|
|
2126
|
+
increase = true,
|
|
2127
|
+
low_range = 1,
|
|
2128
|
+
high_range = 199,
|
|
2129
|
+
target_loss,
|
|
2130
|
+
distribution,
|
|
2131
|
+
max_size
|
|
2132
|
+
} = payload;
|
|
2133
|
+
const criterion = app_config.strategy || "quantity";
|
|
2134
|
+
const risk_rewards = createArray(low_range, high_range, 1);
|
|
2135
|
+
let func = risk_rewards.map((trade_no) => {
|
|
2136
|
+
let trades = sortedBuildConfig(app_config, {
|
|
2137
|
+
take_profit: app_config.take_profit,
|
|
2138
|
+
entry: app_config.entry,
|
|
2139
|
+
stop: app_config.stop,
|
|
2140
|
+
no_of_trades: trade_no,
|
|
2141
|
+
risk_reward: trade_no,
|
|
2142
|
+
increase,
|
|
2143
|
+
kind: app_config.kind,
|
|
2144
|
+
gap: app_config.gap,
|
|
2145
|
+
decimal_places: app_config.decimal_places,
|
|
2146
|
+
distribution,
|
|
2147
|
+
distribution_params: payload.distribution_params
|
|
2148
|
+
});
|
|
2149
|
+
let total = 0;
|
|
2150
|
+
let max = -Infinity;
|
|
2151
|
+
let min = Infinity;
|
|
2152
|
+
let neg_pnl = trades[0]?.neg_pnl || 0;
|
|
2153
|
+
let entry = trades.at(-1)?.entry;
|
|
2154
|
+
let avg_size = trades[0]?.avg_size || 0;
|
|
2155
|
+
if (!entry) {
|
|
2156
|
+
return null;
|
|
2157
|
+
}
|
|
2158
|
+
for (let trade of trades) {
|
|
2159
|
+
total += trade.quantity;
|
|
2160
|
+
max = Math.max(max, trade.quantity);
|
|
2161
|
+
min = Math.min(min, trade.quantity);
|
|
2162
|
+
entry = app_config.kind === "long" ? Math.max(entry, trade.entry) : Math.min(entry, trade.entry);
|
|
2163
|
+
}
|
|
2164
|
+
return {
|
|
2165
|
+
result: trades,
|
|
2166
|
+
value: trade_no,
|
|
2167
|
+
total,
|
|
2168
|
+
risk_per_trade: app_config.risk_per_trade,
|
|
2169
|
+
max,
|
|
2170
|
+
min,
|
|
2171
|
+
avg_size,
|
|
2172
|
+
neg_pnl,
|
|
2173
|
+
entry
|
|
2174
|
+
};
|
|
2175
|
+
});
|
|
2176
|
+
func = func.filter((r) => Boolean(r));
|
|
2177
|
+
if (max_size !== undefined && max_size > 0) {
|
|
2178
|
+
func = func.filter((r) => r.avg_size <= max_size);
|
|
2179
|
+
}
|
|
2180
|
+
if (target_loss === undefined) {
|
|
2181
|
+
func = func.filter((r) => {
|
|
2182
|
+
let foundIndex = r?.result.findIndex((e) => e.quantity === r.max);
|
|
2183
|
+
return criterion === "quantity" ? foundIndex === 0 : true;
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2186
|
+
if (target_loss !== undefined) {
|
|
2187
|
+
const validResults = func.filter((r) => Math.abs(r.neg_pnl) <= target_loss);
|
|
2188
|
+
if (validResults.length > 0) {
|
|
2189
|
+
validResults.sort((a, b) => {
|
|
2190
|
+
const diffA = target_loss - Math.abs(a.neg_pnl);
|
|
2191
|
+
const diffB = target_loss - Math.abs(b.neg_pnl);
|
|
2192
|
+
return diffA - diffB;
|
|
2193
|
+
});
|
|
2194
|
+
if (app_config.raw) {
|
|
2195
|
+
return validResults[0];
|
|
2196
|
+
}
|
|
2197
|
+
return validResults[0]?.value;
|
|
2198
|
+
} else {
|
|
2199
|
+
func.sort((a, b) => Math.abs(a.neg_pnl) - Math.abs(b.neg_pnl));
|
|
2200
|
+
if (app_config.raw) {
|
|
2201
|
+
return func[0];
|
|
2202
|
+
}
|
|
2203
|
+
return func[0]?.value;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
const highest = criterion === "quantity" ? Math.max(...func.map((o) => o.max)) : Math.min(...func.map((o) => o.entry));
|
|
2207
|
+
const key = criterion === "quantity" ? "max" : "entry";
|
|
2208
|
+
const index = findIndexByCondition(func, app_config.kind, (x) => x[key] == highest, criterion);
|
|
2209
|
+
if (app_config.raw) {
|
|
2210
|
+
return func[index];
|
|
2211
|
+
}
|
|
2212
|
+
return func[index]?.value;
|
|
2213
|
+
}
|
|
2214
|
+
function findIndexByCondition(lst, kind, condition, defaultKey = "neg_pnl") {
|
|
2215
|
+
const found = [];
|
|
2216
|
+
let max_new_diff = 0;
|
|
2217
|
+
let new_lst = lst.map((i, j) => ({
|
|
2218
|
+
...i,
|
|
2219
|
+
net_diff: lst[j].neg_pnl + lst[j].risk_per_trade
|
|
2220
|
+
}));
|
|
2221
|
+
new_lst.forEach((item, index) => {
|
|
2222
|
+
if (item.net_diff > 0) {
|
|
2223
|
+
found.push(index);
|
|
2224
|
+
}
|
|
2225
|
+
});
|
|
2226
|
+
if (found.length === 0) {
|
|
2227
|
+
max_new_diff = Math.max(...new_lst.map((e) => e.net_diff));
|
|
2228
|
+
found.push(new_lst.findIndex((e) => e.net_diff === max_new_diff));
|
|
2229
|
+
}
|
|
2230
|
+
const sortedFound = found.map((o, index) => ({ ...new_lst[o], index: o })).filter((j) => max_new_diff === 0 ? j.net_diff > 0 : j.net_diff >= max_new_diff).sort((a, b) => {
|
|
2231
|
+
if (a.total !== b.total) {
|
|
2232
|
+
return b.total - a.total;
|
|
2233
|
+
return b.net_diff - a.net_diff;
|
|
2234
|
+
return a.net_diff - b.net_diff;
|
|
2235
|
+
} else {
|
|
2236
|
+
return b.net_diff - a.net_diff;
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
console.log("found", sortedFound);
|
|
2240
|
+
if (defaultKey === "quantity") {
|
|
2241
|
+
return sortedFound[0].index;
|
|
2242
|
+
}
|
|
2243
|
+
if (found.length === 1) {
|
|
2244
|
+
return found[0];
|
|
2245
|
+
}
|
|
2246
|
+
if (found.length === 0) {
|
|
2247
|
+
return -1;
|
|
2248
|
+
}
|
|
2249
|
+
const entryCondition = (a, b) => {
|
|
2250
|
+
if (kind == "long") {
|
|
2251
|
+
return a.entry > b.entry;
|
|
2252
|
+
}
|
|
2253
|
+
return a.entry < b.entry;
|
|
2254
|
+
};
|
|
2255
|
+
const maximum = found.reduce((maxIndex, currentIndex) => {
|
|
2256
|
+
return new_lst[currentIndex]["net_diff"] < new_lst[maxIndex]["net_diff"] && entryCondition(new_lst[currentIndex], new_lst[maxIndex]) ? currentIndex : maxIndex;
|
|
2257
|
+
}, found[0]);
|
|
2258
|
+
return maximum;
|
|
2259
|
+
}
|
|
2260
|
+
function determineOptimumRisk(config, payload, params) {
|
|
2261
|
+
const { highest_risk, tolerance = 0.01, max_iterations = 200 } = params;
|
|
2262
|
+
let low_risk = 1;
|
|
2263
|
+
let high_risk = Math.max(highest_risk * 5, payload.risk * 10);
|
|
2264
|
+
let best_risk = payload.risk;
|
|
2265
|
+
let best_neg_pnl = 0;
|
|
2266
|
+
let best_diff = Infinity;
|
|
2267
|
+
console.log(`Finding optimal risk for target neg_pnl: ${highest_risk}`);
|
|
2268
|
+
let iterations = 0;
|
|
2269
|
+
while (iterations < max_iterations && high_risk - low_risk > tolerance) {
|
|
2270
|
+
iterations++;
|
|
2271
|
+
const mid_risk = (low_risk + high_risk) / 2;
|
|
2272
|
+
const test_payload = {
|
|
2273
|
+
...payload,
|
|
2274
|
+
risk: mid_risk
|
|
2275
|
+
};
|
|
2276
|
+
const { last_value } = buildAppConfig(config, test_payload);
|
|
2277
|
+
if (!last_value || !last_value.neg_pnl) {
|
|
2278
|
+
high_risk = mid_risk;
|
|
2279
|
+
continue;
|
|
2280
|
+
}
|
|
2281
|
+
const current_neg_pnl = Math.abs(last_value.neg_pnl);
|
|
2282
|
+
const diff = Math.abs(current_neg_pnl - highest_risk);
|
|
2283
|
+
console.log(`Iteration ${iterations}: Risk=${mid_risk.toFixed(2)}, neg_pnl=${current_neg_pnl.toFixed(2)}, diff=${diff.toFixed(2)}`);
|
|
2284
|
+
if (diff < best_diff) {
|
|
2285
|
+
best_diff = diff;
|
|
2286
|
+
best_risk = mid_risk;
|
|
2287
|
+
best_neg_pnl = current_neg_pnl;
|
|
2288
|
+
}
|
|
2289
|
+
if (diff <= tolerance) {
|
|
2290
|
+
console.log(`Converged! Optimal risk: ${mid_risk.toFixed(2)}`);
|
|
2291
|
+
break;
|
|
2292
|
+
}
|
|
2293
|
+
if (current_neg_pnl < highest_risk) {
|
|
2294
|
+
low_risk = mid_risk;
|
|
2295
|
+
} else {
|
|
2296
|
+
high_risk = mid_risk;
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
const final_payload = {
|
|
2300
|
+
...payload,
|
|
2301
|
+
risk: best_risk
|
|
2302
|
+
};
|
|
2303
|
+
const final_result = buildAppConfig(config, final_payload);
|
|
2304
|
+
return {
|
|
2305
|
+
optimal_risk: to_f(best_risk, "%.2f"),
|
|
2306
|
+
achieved_neg_pnl: to_f(best_neg_pnl, "%.2f"),
|
|
2307
|
+
target_neg_pnl: to_f(highest_risk, "%.2f"),
|
|
2308
|
+
difference: to_f(best_diff, "%.2f"),
|
|
2309
|
+
iterations,
|
|
2310
|
+
converged: best_diff <= tolerance,
|
|
2311
|
+
last_value: final_result.last_value,
|
|
2312
|
+
entries: final_result.entries,
|
|
2313
|
+
app_config: final_result
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2316
|
+
function computeRiskReward(payload) {
|
|
2317
|
+
const {
|
|
2318
|
+
app_config,
|
|
2319
|
+
entry,
|
|
2320
|
+
stop,
|
|
2321
|
+
risk_per_trade,
|
|
2322
|
+
target_loss,
|
|
2323
|
+
distribution,
|
|
2324
|
+
high_range,
|
|
2325
|
+
max_size
|
|
2326
|
+
} = payload;
|
|
2327
|
+
const kind = entry > stop ? "long" : "short";
|
|
2328
|
+
app_config.kind = kind;
|
|
2329
|
+
app_config.entry = entry;
|
|
2330
|
+
app_config.stop = stop;
|
|
2331
|
+
app_config.risk_per_trade = risk_per_trade;
|
|
2332
|
+
const result = determineOptimumReward({
|
|
2333
|
+
app_config,
|
|
2334
|
+
target_loss,
|
|
2335
|
+
distribution,
|
|
2336
|
+
distribution_params: payload.distribution_params,
|
|
2337
|
+
high_range,
|
|
2338
|
+
max_size
|
|
2339
|
+
});
|
|
2340
|
+
return result;
|
|
2341
|
+
}
|
|
2342
|
+
function getRiskReward(payload) {
|
|
2343
|
+
const {
|
|
2344
|
+
high_range,
|
|
2345
|
+
max_size,
|
|
2346
|
+
entry,
|
|
2347
|
+
stop,
|
|
2348
|
+
risk,
|
|
2349
|
+
global_config,
|
|
2350
|
+
force_exact_risk = false,
|
|
2351
|
+
target_loss,
|
|
2352
|
+
distribution,
|
|
2353
|
+
risk_factor = 1
|
|
2354
|
+
} = payload;
|
|
2355
|
+
const { entries, last_value, ...app_config } = buildAppConfig(global_config, {
|
|
2356
|
+
entry,
|
|
2357
|
+
stop,
|
|
2358
|
+
risk_reward: 30,
|
|
2359
|
+
risk,
|
|
2360
|
+
symbol: global_config.symbol,
|
|
2361
|
+
distribution,
|
|
2362
|
+
distribution_params: payload.distribution_params
|
|
2363
|
+
});
|
|
2364
|
+
const risk_reward = computeRiskReward({
|
|
2365
|
+
app_config,
|
|
2366
|
+
entry,
|
|
2367
|
+
stop,
|
|
2368
|
+
risk_per_trade: risk,
|
|
2369
|
+
high_range,
|
|
2370
|
+
target_loss,
|
|
2371
|
+
distribution,
|
|
2372
|
+
distribution_params: payload.distribution_params,
|
|
2373
|
+
max_size
|
|
2374
|
+
});
|
|
2375
|
+
if (force_exact_risk) {
|
|
2376
|
+
const new_risk_per_trade = determineOptimumRisk(global_config, {
|
|
2377
|
+
entry,
|
|
2378
|
+
stop,
|
|
2379
|
+
risk_reward,
|
|
2380
|
+
risk,
|
|
2381
|
+
symbol: global_config.symbol,
|
|
2382
|
+
distribution,
|
|
2383
|
+
distribution_params: payload.distribution_params
|
|
2384
|
+
}, {
|
|
2385
|
+
highest_risk: risk * risk_factor
|
|
2386
|
+
}).optimal_risk;
|
|
2387
|
+
return { risk: new_risk_per_trade, risk_reward };
|
|
2388
|
+
}
|
|
2389
|
+
return risk_reward;
|
|
2390
|
+
}
|
|
2391
|
+
function computeProfitDetail(payload) {
|
|
2392
|
+
const {
|
|
2393
|
+
focus_position,
|
|
2394
|
+
strategy,
|
|
2395
|
+
pnl,
|
|
2396
|
+
price_places = "%.1f",
|
|
2397
|
+
reduce_position,
|
|
2398
|
+
decimal_places,
|
|
2399
|
+
reverse_position,
|
|
2400
|
+
full_ratio = 1
|
|
2401
|
+
} = payload;
|
|
2402
|
+
let reward_factor = strategy?.reward_factor || 1;
|
|
2403
|
+
const profit_percent = to_f(pnl * 100 / (focus_position.avg_price * focus_position.avg_qty), "%.4f");
|
|
2404
|
+
const diff = pnl / focus_position.quantity;
|
|
2405
|
+
const sell_price = to_f(focus_position.kind === "long" ? focus_position.entry + diff : focus_position.entry - diff, price_places);
|
|
2406
|
+
let loss = 0;
|
|
2407
|
+
let full_loss = 0;
|
|
2408
|
+
let expected_loss = 0;
|
|
2409
|
+
let quantity = 0;
|
|
2410
|
+
let new_pnl = pnl;
|
|
2411
|
+
if (reduce_position) {
|
|
2412
|
+
loss = Math.abs(reduce_position.entry - sell_price) * reduce_position.quantity;
|
|
2413
|
+
const ratio = pnl / loss;
|
|
2414
|
+
quantity = to_f(reduce_position.quantity * ratio, decimal_places);
|
|
2415
|
+
expected_loss = to_f(Math.abs(reduce_position.entry - sell_price) * quantity, "%.2f");
|
|
2416
|
+
full_loss = Math.abs(reduce_position.avg_price - sell_price) * reduce_position.avg_qty * full_ratio;
|
|
2417
|
+
}
|
|
2418
|
+
if (reverse_position) {
|
|
2419
|
+
expected_loss = Math.abs(reverse_position.avg_price - sell_price) * reverse_position.avg_qty;
|
|
2420
|
+
new_pnl = to_f(pnl - expected_loss, "%.2f");
|
|
2421
|
+
}
|
|
2422
|
+
return {
|
|
2423
|
+
pnl: new_pnl,
|
|
2424
|
+
loss: to_f(expected_loss, "%.2f"),
|
|
2425
|
+
full_loss: to_f(full_loss, "%.2f"),
|
|
2426
|
+
original_pnl: pnl,
|
|
2427
|
+
reward_factor,
|
|
2428
|
+
profit_percent,
|
|
2429
|
+
kind: focus_position.kind,
|
|
2430
|
+
sell_price: to_f(sell_price, price_places),
|
|
2431
|
+
quantity: quantity * full_ratio,
|
|
2432
|
+
price_places,
|
|
2433
|
+
decimal_places
|
|
2434
|
+
};
|
|
2435
|
+
}
|
|
2436
|
+
function generateGapTp(payload) {
|
|
2437
|
+
const {
|
|
2438
|
+
long,
|
|
2439
|
+
short,
|
|
2440
|
+
factor: factor_value = 0,
|
|
2441
|
+
risk: desired_risk,
|
|
2442
|
+
sell_factor = 1,
|
|
2443
|
+
kind,
|
|
2444
|
+
price_places = "%.1f",
|
|
2445
|
+
decimal_places = "%.3f"
|
|
2446
|
+
} = payload;
|
|
2447
|
+
if (!factor_value && !desired_risk) {
|
|
2448
|
+
throw new Error("Either factor or risk must be provided");
|
|
2449
|
+
}
|
|
2450
|
+
if (desired_risk && !kind) {
|
|
2451
|
+
throw new Error("Kind must be provided when risk is provided");
|
|
2452
|
+
}
|
|
2453
|
+
let factor = factor_value || calculate_factor({
|
|
2454
|
+
long,
|
|
2455
|
+
short,
|
|
2456
|
+
risk: desired_risk,
|
|
2457
|
+
kind,
|
|
2458
|
+
sell_factor
|
|
2459
|
+
});
|
|
2460
|
+
const gap = Math.abs(long.entry - short.entry);
|
|
2461
|
+
const max_quantity = Math.max(long.quantity, short.quantity);
|
|
2462
|
+
const gapLoss = gap * max_quantity;
|
|
2463
|
+
const longPercent = gapLoss * factor / (short.entry * short.quantity);
|
|
2464
|
+
const shortPercent = gapLoss * factor / (long.entry * long.quantity);
|
|
2465
|
+
const longTp = to_f((1 + longPercent) * long.entry, price_places);
|
|
2466
|
+
const shortTp = to_f((1 + shortPercent) ** -1 * short.entry, price_places);
|
|
2467
|
+
const shortToReduce = to_f(Math.abs(longTp - long.entry) * long.quantity, "%.1f");
|
|
2468
|
+
const longToReduce = to_f(Math.abs(shortTp - short.entry) * short.quantity, "%.1f");
|
|
2469
|
+
const actualShortReduce = to_f(shortToReduce * sell_factor, "%.1f");
|
|
2470
|
+
const actualLongReduce = to_f(longToReduce * sell_factor, "%.1f");
|
|
2471
|
+
const short_quantity_to_sell = determine_amount_to_sell2(short.entry, short.quantity, longTp, actualShortReduce, "short", decimal_places);
|
|
2472
|
+
const long_quantity_to_sell = determine_amount_to_sell2(long.entry, long.quantity, shortTp, actualLongReduce, "long", decimal_places);
|
|
2473
|
+
const risk_amount_short = to_f(shortToReduce - actualShortReduce, "%.2f");
|
|
2474
|
+
const risk_amount_long = to_f(longToReduce - actualLongReduce, "%.2f");
|
|
2475
|
+
const profit_percent_long = to_f(shortToReduce * 100 / (long.entry * long.quantity), "%.4f");
|
|
2476
|
+
const profit_percent_short = to_f(longToReduce * 100 / (short.entry * short.quantity), "%.4f");
|
|
2477
|
+
return {
|
|
2478
|
+
profit_percent: {
|
|
2479
|
+
long: profit_percent_long,
|
|
2480
|
+
short: profit_percent_short
|
|
2481
|
+
},
|
|
2482
|
+
risk: {
|
|
2483
|
+
short: risk_amount_short,
|
|
2484
|
+
long: risk_amount_long
|
|
2485
|
+
},
|
|
2486
|
+
take_profit: {
|
|
2487
|
+
long: longTp,
|
|
2488
|
+
short: shortTp
|
|
2489
|
+
},
|
|
2490
|
+
to_reduce: {
|
|
2491
|
+
short: actualShortReduce,
|
|
2492
|
+
long: actualLongReduce
|
|
2493
|
+
},
|
|
2494
|
+
full_reduce: {
|
|
2495
|
+
short: shortToReduce,
|
|
2496
|
+
long: longToReduce
|
|
2497
|
+
},
|
|
2498
|
+
sell_quantity: {
|
|
2499
|
+
short: short_quantity_to_sell,
|
|
2500
|
+
long: long_quantity_to_sell
|
|
2501
|
+
},
|
|
2502
|
+
gap: to_f(gap, price_places),
|
|
2503
|
+
gap_loss: to_f(gapLoss, "%.2f")
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
function calculate_factor(payload) {
|
|
2507
|
+
const {
|
|
2508
|
+
long,
|
|
2509
|
+
short,
|
|
2510
|
+
risk: desired_risk,
|
|
2511
|
+
kind,
|
|
2512
|
+
sell_factor,
|
|
2513
|
+
places = "%.4f"
|
|
2514
|
+
} = payload;
|
|
2515
|
+
const gap = Math.abs(long.entry - short.entry);
|
|
2516
|
+
const max_quantity = Math.max(long.quantity, short.quantity);
|
|
2517
|
+
const gapLoss = gap * max_quantity;
|
|
2518
|
+
let calculated_factor;
|
|
2519
|
+
const long_notional = long.entry * long.quantity;
|
|
2520
|
+
const short_notional = short.entry * short.quantity;
|
|
2521
|
+
const target_to_reduce = desired_risk / (1 - sell_factor);
|
|
2522
|
+
if (kind === "short") {
|
|
2523
|
+
const calculate_longPercent = target_to_reduce / long_notional;
|
|
2524
|
+
calculated_factor = calculate_longPercent * short_notional / gapLoss;
|
|
2525
|
+
} else {
|
|
2526
|
+
const calculated_shortPercent = target_to_reduce / (short_notional - target_to_reduce);
|
|
2527
|
+
calculated_factor = calculated_shortPercent * long_notional / gapLoss;
|
|
2528
|
+
}
|
|
2529
|
+
calculated_factor = to_f(calculated_factor, places);
|
|
2530
|
+
return calculated_factor;
|
|
2531
|
+
}
|
|
2532
|
+
function calculateFactorFromTakeProfit(payload) {
|
|
2533
|
+
const { long, short, knownTp, tpType, price_places = "%.4f" } = payload;
|
|
2534
|
+
const gap = Math.abs(long.entry - short.entry);
|
|
2535
|
+
const max_quantity = Math.max(long.quantity, short.quantity);
|
|
2536
|
+
const gapLoss = gap * max_quantity;
|
|
2537
|
+
if (gapLoss === 0) {
|
|
2538
|
+
return 0;
|
|
2539
|
+
}
|
|
2540
|
+
let factor;
|
|
2541
|
+
if (tpType === "long") {
|
|
2542
|
+
const longPercent = knownTp / long.entry - 1;
|
|
2543
|
+
factor = longPercent * short.entry * short.quantity / gapLoss;
|
|
2544
|
+
} else {
|
|
2545
|
+
const shortPercent = short.entry / knownTp - 1;
|
|
2546
|
+
factor = shortPercent * long.entry * long.quantity / gapLoss;
|
|
2547
|
+
}
|
|
2548
|
+
return to_f(factor, price_places);
|
|
2549
|
+
}
|
|
2550
|
+
function calculateFactorFromSellQuantity(payload) {
|
|
2551
|
+
const {
|
|
2552
|
+
long,
|
|
2553
|
+
short,
|
|
2554
|
+
knownSellQuantity,
|
|
2555
|
+
sellType,
|
|
2556
|
+
sell_factor = 1,
|
|
2557
|
+
price_places = "%.4f"
|
|
2558
|
+
} = payload;
|
|
2559
|
+
if (knownSellQuantity < 0.00001) {
|
|
2560
|
+
return 0;
|
|
2561
|
+
}
|
|
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
|
+
if (gapLoss === 0) {
|
|
2566
|
+
return 0;
|
|
2567
|
+
}
|
|
2568
|
+
let low_factor = 0.001;
|
|
2569
|
+
let high_factor = 1;
|
|
2570
|
+
let best_factor = 0;
|
|
2571
|
+
let best_diff = Infinity;
|
|
2572
|
+
const tolerance = 0.00001;
|
|
2573
|
+
const max_iterations = 150;
|
|
2574
|
+
let expansions = 0;
|
|
2575
|
+
const max_expansions = 15;
|
|
2576
|
+
let iterations = 0;
|
|
2577
|
+
while (iterations < max_iterations) {
|
|
2578
|
+
iterations++;
|
|
2579
|
+
const mid_factor = (low_factor + high_factor) / 2;
|
|
2580
|
+
const testResult = generateGapTp({
|
|
2581
|
+
long,
|
|
2582
|
+
short,
|
|
2583
|
+
factor: mid_factor,
|
|
2584
|
+
sell_factor,
|
|
2585
|
+
price_places: "%.8f",
|
|
2586
|
+
decimal_places: "%.8f"
|
|
2587
|
+
});
|
|
2588
|
+
const testSellQty = sellType === "long" ? testResult.sell_quantity.long : testResult.sell_quantity.short;
|
|
2589
|
+
const diff = Math.abs(testSellQty - knownSellQuantity);
|
|
2590
|
+
if (diff < best_diff) {
|
|
2591
|
+
best_diff = diff;
|
|
2592
|
+
best_factor = mid_factor;
|
|
2593
|
+
}
|
|
2594
|
+
if (diff < tolerance) {
|
|
2595
|
+
return to_f(mid_factor, price_places);
|
|
2596
|
+
}
|
|
2597
|
+
if (testSellQty < knownSellQuantity) {
|
|
2598
|
+
low_factor = mid_factor;
|
|
2599
|
+
if (mid_factor > high_factor * 0.9 && expansions < max_expansions) {
|
|
2600
|
+
const ratio = knownSellQuantity / testSellQty;
|
|
2601
|
+
if (ratio > 2) {
|
|
2602
|
+
high_factor = high_factor * Math.min(ratio, 10);
|
|
2603
|
+
} else {
|
|
2604
|
+
high_factor = high_factor * 2;
|
|
2605
|
+
}
|
|
2606
|
+
expansions++;
|
|
2607
|
+
continue;
|
|
2608
|
+
}
|
|
2609
|
+
} else {
|
|
2610
|
+
high_factor = mid_factor;
|
|
2611
|
+
}
|
|
2612
|
+
if (Math.abs(high_factor - low_factor) < 0.00001 && diff > tolerance) {
|
|
2613
|
+
if (testSellQty < knownSellQuantity && expansions < max_expansions) {
|
|
2614
|
+
high_factor = high_factor * 1.1;
|
|
2615
|
+
expansions++;
|
|
2616
|
+
continue;
|
|
2617
|
+
} else {
|
|
2618
|
+
break;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
return to_f(best_factor, price_places);
|
|
2623
|
+
}
|
|
2624
|
+
function determineRewardFactor(payload) {
|
|
2625
|
+
const { quantity, avg_qty, minimum_pnl, risk } = payload;
|
|
2626
|
+
const reward_factor = minimum_pnl / risk;
|
|
2627
|
+
const quantity_ratio = quantity / avg_qty;
|
|
2628
|
+
return to_f(reward_factor / quantity_ratio, "%.4f");
|
|
2629
|
+
}
|
|
2630
|
+
function getHedgeZone(payload) {
|
|
2631
|
+
const {
|
|
2632
|
+
reward_factor: _reward_factor,
|
|
2633
|
+
symbol_config,
|
|
2634
|
+
risk,
|
|
2635
|
+
position: position2,
|
|
2636
|
+
risk_factor = 1,
|
|
2637
|
+
support
|
|
2638
|
+
} = payload;
|
|
2639
|
+
const kind = position2.kind;
|
|
2640
|
+
let reward_factor = _reward_factor;
|
|
2641
|
+
if (support) {
|
|
2642
|
+
const _result = getOptimumHedgeFactor({
|
|
2643
|
+
target_support: support,
|
|
2644
|
+
symbol_config,
|
|
2645
|
+
risk,
|
|
2646
|
+
position: position2
|
|
2647
|
+
});
|
|
2648
|
+
reward_factor = Number(_result.reward_factor);
|
|
2649
|
+
}
|
|
2650
|
+
const take_profit = position2.tp?.price;
|
|
2651
|
+
const tp_diff = Math.abs(take_profit - position2.entry);
|
|
2652
|
+
const quantity = position2.quantity;
|
|
2653
|
+
const diff = risk / quantity;
|
|
2654
|
+
let new_take_profit = kind === "long" ? to_f(position2.entry + diff, symbol_config.price_places) : to_f(position2.entry - diff, symbol_config.price_places);
|
|
2655
|
+
let base_factor = to_f(Math.max(tp_diff, diff) / (Math.min(tp_diff, diff) || 1), "%.3f");
|
|
2656
|
+
let factor = reward_factor || base_factor;
|
|
2657
|
+
const new_risk = risk * factor * risk_factor;
|
|
2658
|
+
const stop_loss_diff = new_risk / quantity;
|
|
2659
|
+
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);
|
|
2660
|
+
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);
|
|
2661
|
+
const profit_percent = new_risk * 100 / (position2.entry * position2.quantity);
|
|
2662
|
+
return {
|
|
2663
|
+
support: Math.min(new_take_profit, stop_loss),
|
|
2664
|
+
resistance: Math.max(new_take_profit, stop_loss),
|
|
2665
|
+
risk: to_f(new_risk, "%.2f"),
|
|
2666
|
+
profit_percent: to_f(profit_percent, "%.2f")
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
function getOptimumHedgeFactor(payload) {
|
|
2670
|
+
const {
|
|
2671
|
+
target_support,
|
|
2672
|
+
max_iterations = 50,
|
|
2673
|
+
min_factor = 0.1,
|
|
2674
|
+
max_factor = 20,
|
|
2675
|
+
symbol_config,
|
|
2676
|
+
risk,
|
|
2677
|
+
position: position2
|
|
2678
|
+
} = payload;
|
|
2679
|
+
const current_price = position2.entry;
|
|
2680
|
+
const tolerance = current_price > 100 ? 0.5 : current_price > 1 ? 0.01 : 0.001;
|
|
2681
|
+
let low = min_factor;
|
|
2682
|
+
let high = max_factor;
|
|
2683
|
+
let best_factor = low;
|
|
2684
|
+
let best_diff = Infinity;
|
|
2685
|
+
for (let iteration = 0;iteration < max_iterations; iteration++) {
|
|
2686
|
+
const mid_factor = (low + high) / 2;
|
|
2687
|
+
const hedge_zone = getHedgeZone({
|
|
2688
|
+
reward_factor: mid_factor,
|
|
2689
|
+
symbol_config,
|
|
2690
|
+
risk,
|
|
2691
|
+
position: position2
|
|
2692
|
+
});
|
|
2693
|
+
const current_support = hedge_zone.support;
|
|
2694
|
+
const diff = Math.abs(current_support - target_support);
|
|
2695
|
+
if (diff < best_diff) {
|
|
2696
|
+
best_diff = diff;
|
|
2697
|
+
best_factor = mid_factor;
|
|
2698
|
+
}
|
|
2699
|
+
if (diff <= tolerance) {
|
|
2700
|
+
return {
|
|
2701
|
+
reward_factor: to_f(mid_factor, "%.4f"),
|
|
2702
|
+
achieved_support: to_f(current_support, symbol_config.price_places),
|
|
2703
|
+
target_support: to_f(target_support, symbol_config.price_places),
|
|
2704
|
+
difference: to_f(diff, symbol_config.price_places),
|
|
2705
|
+
iterations: iteration + 1
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
if (current_support > target_support) {
|
|
2709
|
+
low = mid_factor;
|
|
2710
|
+
} else {
|
|
2711
|
+
high = mid_factor;
|
|
2712
|
+
}
|
|
2713
|
+
if (Math.abs(high - low) < 0.0001) {
|
|
2714
|
+
break;
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
const final_hedge_zone = getHedgeZone({
|
|
2718
|
+
symbol_config,
|
|
2719
|
+
risk,
|
|
2720
|
+
position: position2,
|
|
2721
|
+
reward_factor: best_factor
|
|
2722
|
+
});
|
|
2723
|
+
return {
|
|
2724
|
+
reward_factor: to_f(best_factor, "%.4f"),
|
|
2725
|
+
achieved_support: to_f(final_hedge_zone.support, symbol_config.price_places),
|
|
2726
|
+
target_support: to_f(target_support, symbol_config.price_places),
|
|
2727
|
+
difference: to_f(best_diff, symbol_config.price_places),
|
|
2728
|
+
iterations: max_iterations,
|
|
2729
|
+
converged: best_diff <= tolerance
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
function determineCompoundLongTrade(payload) {
|
|
2733
|
+
const {
|
|
2734
|
+
focus_short_position,
|
|
2735
|
+
focus_long_position,
|
|
2736
|
+
shortConfig,
|
|
2737
|
+
global_config,
|
|
2738
|
+
rr = 1
|
|
2739
|
+
} = payload;
|
|
2740
|
+
const short_app_config = buildAppConfig(global_config, {
|
|
2741
|
+
entry: shortConfig.entry,
|
|
2742
|
+
stop: shortConfig.stop,
|
|
2743
|
+
risk_reward: shortConfig.risk_reward,
|
|
2744
|
+
risk: shortConfig.risk,
|
|
2745
|
+
symbol: shortConfig.symbol
|
|
2746
|
+
});
|
|
2747
|
+
const short_max_size = short_app_config.last_value.avg_size;
|
|
2748
|
+
const start_risk = Math.abs(short_app_config.last_value.neg_pnl) * rr;
|
|
2749
|
+
const short_profit = short_app_config.last_value.avg_size * short_app_config.last_value.avg_entry * shortConfig.profit_percent / 100;
|
|
2750
|
+
const diff = short_profit * rr / short_app_config.last_value.avg_size;
|
|
2751
|
+
const support = Math.abs(short_app_config.last_value.avg_entry - diff);
|
|
2752
|
+
const resistance = focus_short_position.next_order || focus_long_position.take_profit;
|
|
2753
|
+
console.log({ support, resistance, short_profit });
|
|
2754
|
+
const result = getRiskReward({
|
|
2755
|
+
entry: resistance,
|
|
2756
|
+
stop: support,
|
|
2757
|
+
risk: start_risk,
|
|
2758
|
+
global_config,
|
|
2759
|
+
force_exact_risk: true
|
|
2760
|
+
});
|
|
2761
|
+
const long_app_config = buildAppConfig(global_config, {
|
|
2762
|
+
entry: resistance,
|
|
2763
|
+
stop: support,
|
|
2764
|
+
risk_reward: result.risk_reward,
|
|
2765
|
+
risk: result.risk,
|
|
2766
|
+
symbol: shortConfig.symbol
|
|
2767
|
+
});
|
|
2768
|
+
const long_profit_percent = start_risk * 2 * 100 / (long_app_config.last_value.avg_size * long_app_config.last_value.avg_entry);
|
|
2769
|
+
return {
|
|
2770
|
+
start_risk,
|
|
2771
|
+
short_profit,
|
|
2772
|
+
support: to_f(support, global_config.price_places),
|
|
2773
|
+
resistance: to_f(resistance, global_config.price_places),
|
|
2774
|
+
long_v: long_app_config.last_value,
|
|
2775
|
+
profit_percent: to_f(long_profit_percent, "%.3f"),
|
|
2776
|
+
result,
|
|
2777
|
+
short_max_size
|
|
2778
|
+
};
|
|
2779
|
+
}
|
|
2780
|
+
function generateOppositeTradeConfig(payload) {
|
|
2781
|
+
const {
|
|
2782
|
+
kind,
|
|
2783
|
+
entry,
|
|
2784
|
+
quantity,
|
|
2785
|
+
target_pnl,
|
|
2786
|
+
ratio = 0.5,
|
|
2787
|
+
global_config
|
|
2788
|
+
} = payload;
|
|
2789
|
+
const diff = target_pnl / quantity;
|
|
2790
|
+
const tp = kind === "long" ? entry + diff : entry - diff;
|
|
2791
|
+
const stop = kind === "long" ? entry - diff : entry + diff;
|
|
2792
|
+
const opposite_pnl = target_pnl * ratio;
|
|
2793
|
+
const app_config = constructAppConfig({
|
|
2794
|
+
account: {
|
|
2795
|
+
expand: {
|
|
2796
|
+
b_config: {
|
|
2797
|
+
entry,
|
|
2798
|
+
stop,
|
|
2799
|
+
symbol: global_config.symbol,
|
|
2800
|
+
risk: target_pnl
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
},
|
|
2804
|
+
global_config,
|
|
2805
|
+
distribution_config: {}
|
|
2806
|
+
});
|
|
2807
|
+
const risk_reward = computeRiskReward({
|
|
2808
|
+
app_config,
|
|
2809
|
+
entry: stop,
|
|
2810
|
+
stop: tp,
|
|
2811
|
+
risk_per_trade: opposite_pnl,
|
|
2812
|
+
target_loss: opposite_pnl
|
|
2813
|
+
});
|
|
2814
|
+
return {
|
|
2815
|
+
entry: to_f(stop, global_config.price_places),
|
|
2816
|
+
stop: to_f(tp, global_config.price_places),
|
|
2817
|
+
risk: to_f(opposite_pnl, "%.2f"),
|
|
2818
|
+
risk_reward
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2821
|
+
function constructAppConfig(payload) {
|
|
2822
|
+
const { account, global_config, kelly_config, distribution_config } = payload;
|
|
2823
|
+
const config = account.expand?.b_config;
|
|
2824
|
+
if (!config) {
|
|
2825
|
+
return null;
|
|
2826
|
+
}
|
|
2827
|
+
const kelly = config.kelly;
|
|
2828
|
+
const options = {
|
|
2829
|
+
entry: config?.entry,
|
|
2830
|
+
stop: config?.stop,
|
|
2831
|
+
risk_reward: config?.risk_reward,
|
|
2832
|
+
risk: config?.risk,
|
|
2833
|
+
symbol: account.symbol,
|
|
2834
|
+
use_kelly: kelly_config?.use_kelly ?? kelly?.use_kelly,
|
|
2835
|
+
kelly_confidence_factor: kelly_config?.kelly_confidence_factor ?? kelly?.kelly_confidence_factor,
|
|
2836
|
+
kelly_minimum_risk: kelly_config?.kelly_minimum_risk ?? kelly?.kelly_minimum_risk,
|
|
2837
|
+
kelly_prediction_model: kelly_config?.kelly_prediction_model ?? kelly?.kelly_prediction_model,
|
|
2838
|
+
distribution: distribution_config?.distribution ?? config?.distribution,
|
|
2839
|
+
distribution_params: distribution_config?.distribution_params ?? config?.distribution_params
|
|
2840
|
+
};
|
|
2841
|
+
const { entries: _entries, ...appConfig } = buildAppConfig(global_config, options);
|
|
2842
|
+
return appConfig;
|
|
2843
|
+
}
|
|
2844
|
+
function generateDangerousConfig(payload) {
|
|
2845
|
+
const { account, global_config, config } = payload;
|
|
2846
|
+
const app_config = constructAppConfig({
|
|
2847
|
+
account,
|
|
2848
|
+
global_config,
|
|
2849
|
+
kelly_config: {},
|
|
2850
|
+
distribution_config: {}
|
|
2851
|
+
});
|
|
2852
|
+
const { optimal_risk, optimal_stop } = getOptimumStopAndRisk(app_config, {
|
|
2853
|
+
max_size: config.quantity,
|
|
2854
|
+
target_stop: config.stop
|
|
2855
|
+
});
|
|
2856
|
+
const optimumRiskReward = computeRiskReward({
|
|
2857
|
+
app_config,
|
|
2858
|
+
entry: config.entry,
|
|
2859
|
+
stop: optimal_stop,
|
|
2860
|
+
risk_per_trade: optimal_risk,
|
|
2861
|
+
target_loss: optimal_risk
|
|
2862
|
+
});
|
|
2863
|
+
return {
|
|
2864
|
+
entry: config.entry,
|
|
2865
|
+
risk: optimal_risk,
|
|
2866
|
+
stop: optimal_stop,
|
|
2867
|
+
risk_reward: optimumRiskReward
|
|
2868
|
+
};
|
|
2869
|
+
}
|
|
2870
|
+
// src/helpers/strategy.ts
|
|
2871
|
+
class Strategy {
|
|
2872
|
+
position;
|
|
2873
|
+
dominant_position = "long";
|
|
2874
|
+
config;
|
|
2875
|
+
constructor(payload) {
|
|
2876
|
+
this.position = {
|
|
2877
|
+
long: payload.long,
|
|
2878
|
+
short: payload.short
|
|
2879
|
+
};
|
|
2880
|
+
this.dominant_position = payload.dominant_position || "long";
|
|
2881
|
+
this.config = payload.config;
|
|
2882
|
+
if (!this.config.fee_percent) {
|
|
2883
|
+
this.config.fee_percent = 0.05;
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
get price_places() {
|
|
2887
|
+
return this.config.global_config.price_places;
|
|
2888
|
+
}
|
|
2889
|
+
get decimal_places() {
|
|
2890
|
+
return this.config.global_config.decimal_places;
|
|
2891
|
+
}
|
|
2892
|
+
to_f(price) {
|
|
2893
|
+
return to_f(price, this.price_places);
|
|
2894
|
+
}
|
|
2895
|
+
to_df(quantity) {
|
|
2896
|
+
return to_f(quantity, this.decimal_places);
|
|
2897
|
+
}
|
|
2898
|
+
pnl(kind, _position) {
|
|
2899
|
+
const position2 = _position || this.position[kind];
|
|
2900
|
+
const { entry, quantity } = position2;
|
|
2901
|
+
const notional = entry * quantity;
|
|
2902
|
+
let tp_percent = this.config.tp_percent;
|
|
2903
|
+
if (kind == "short") {
|
|
2904
|
+
tp_percent = tp_percent * this.config.short_tp_factor;
|
|
2905
|
+
}
|
|
2906
|
+
const profit = notional * (tp_percent / 100);
|
|
2907
|
+
return this.to_f(profit);
|
|
2908
|
+
}
|
|
2909
|
+
tp(kind) {
|
|
2910
|
+
let position2 = this.position[kind];
|
|
2911
|
+
if (position2.quantity == 0) {
|
|
2912
|
+
const reverse_kind = kind == "long" ? "short" : "long";
|
|
2913
|
+
position2 = this.position[reverse_kind];
|
|
2914
|
+
}
|
|
2915
|
+
const { entry, quantity } = position2;
|
|
2916
|
+
const profit = this.pnl(kind, position2);
|
|
2917
|
+
const diff = profit / (quantity || 1);
|
|
2918
|
+
return this.to_f(kind == "long" ? entry + diff : entry - diff);
|
|
2919
|
+
}
|
|
2920
|
+
calculate_fee(position2) {
|
|
2921
|
+
const { price, quantity } = position2;
|
|
2922
|
+
const fee = price * quantity * this.config.fee_percent / 100;
|
|
2923
|
+
return this.to_f(fee);
|
|
2924
|
+
}
|
|
2925
|
+
get long_tp() {
|
|
2926
|
+
return this.tp("long");
|
|
2927
|
+
}
|
|
2928
|
+
get short_tp() {
|
|
2929
|
+
return this.tp("short");
|
|
2930
|
+
}
|
|
2931
|
+
generateGapClosingAlgorithm(payload) {
|
|
2932
|
+
const {
|
|
2933
|
+
kind,
|
|
2934
|
+
ignore_entries = false,
|
|
2935
|
+
reduce_ratio = 1,
|
|
2936
|
+
sell_factor = 1
|
|
2937
|
+
} = payload;
|
|
2938
|
+
const { entry, quantity } = this.position[kind];
|
|
2939
|
+
const focus_position = this.position[kind];
|
|
2940
|
+
const reverse_kind = kind == "long" ? "short" : "long";
|
|
2941
|
+
const reverse_position = this.position[reverse_kind];
|
|
2942
|
+
let _entry = this.tp(kind);
|
|
2943
|
+
let _stop = this.tp(reverse_kind);
|
|
2944
|
+
const second_payload = {
|
|
2945
|
+
entry: _entry,
|
|
2946
|
+
stop: _stop,
|
|
2947
|
+
risk_reward: this.config.risk_reward,
|
|
2948
|
+
start_risk: this.pnl(reverse_kind),
|
|
2949
|
+
max_risk: this.config.budget
|
|
2950
|
+
};
|
|
2951
|
+
const third_payload = {
|
|
2952
|
+
entry,
|
|
2953
|
+
quantity,
|
|
2954
|
+
kind
|
|
2955
|
+
};
|
|
2956
|
+
const app_config = generateOptimumAppConfig(this.config.global_config, second_payload, third_payload);
|
|
2957
|
+
let entries = [];
|
|
2958
|
+
let risk_per_trade = this.config.budget;
|
|
2959
|
+
let last_value = null;
|
|
2960
|
+
if (app_config) {
|
|
2961
|
+
let { entries: _entries, ...rest } = app_config;
|
|
2962
|
+
entries = _entries;
|
|
2963
|
+
risk_per_trade = rest.risk_per_trade;
|
|
2964
|
+
last_value = rest.last_value;
|
|
2965
|
+
if (ignore_entries) {
|
|
2966
|
+
entries = [];
|
|
2967
|
+
}
|
|
2968
|
+
console.log({ app_config });
|
|
2969
|
+
}
|
|
2970
|
+
const risk = this.to_f(risk_per_trade);
|
|
2971
|
+
let below_reverse_entries = kind === "long" ? entries.filter((u) => {
|
|
2972
|
+
return u.entry < (reverse_position.entry || focus_position.entry);
|
|
2973
|
+
}) : entries.filter((u) => {
|
|
2974
|
+
return u.entry > (reverse_position.entry || focus_position.entry);
|
|
2975
|
+
});
|
|
2976
|
+
const threshold = below_reverse_entries.at(-1);
|
|
2977
|
+
const result = this.gapCloserHelper({
|
|
2978
|
+
risk,
|
|
2979
|
+
entries,
|
|
2980
|
+
kind,
|
|
2981
|
+
sell_factor,
|
|
2982
|
+
reduce_ratio
|
|
2983
|
+
});
|
|
2984
|
+
return {
|
|
2985
|
+
...result,
|
|
2986
|
+
last_entry: last_value?.entry,
|
|
2987
|
+
first_entry: entries.at(-1)?.entry,
|
|
2988
|
+
threshold
|
|
2989
|
+
};
|
|
2990
|
+
}
|
|
2991
|
+
gapCloserHelper(payload) {
|
|
2992
|
+
const {
|
|
2993
|
+
risk,
|
|
2994
|
+
entries = [],
|
|
2995
|
+
kind,
|
|
2996
|
+
sell_factor = 1,
|
|
2997
|
+
reduce_ratio = 1
|
|
2998
|
+
} = payload;
|
|
2999
|
+
const { entry, quantity } = this.position[kind];
|
|
3000
|
+
const focus_position = this.position[kind];
|
|
3001
|
+
const reverse_kind = kind == "long" ? "short" : "long";
|
|
3002
|
+
const reverse_position = this.position[reverse_kind];
|
|
3003
|
+
let _entry = this.tp(kind);
|
|
3004
|
+
let _stop = this.tp(reverse_kind);
|
|
3005
|
+
const second_payload = {
|
|
3006
|
+
entry: _entry,
|
|
3007
|
+
stop: _stop,
|
|
3008
|
+
risk_reward: this.config.risk_reward,
|
|
3009
|
+
start_risk: this.pnl(reverse_kind),
|
|
3010
|
+
max_risk: this.config.budget
|
|
3011
|
+
};
|
|
3012
|
+
const adjusted_focus_entries = entries.map((entry2) => {
|
|
3013
|
+
let adjusted_price = entry2.price;
|
|
3014
|
+
if (focus_position.quantity > 0) {
|
|
3015
|
+
if (kind === "long" && entry2.price >= focus_position.entry) {
|
|
3016
|
+
adjusted_price = focus_position.entry;
|
|
3017
|
+
} else if (kind === "short" && entry2.price <= focus_position.entry) {
|
|
3018
|
+
adjusted_price = focus_position.entry;
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
return {
|
|
3022
|
+
price: adjusted_price,
|
|
3023
|
+
quantity: entry2.quantity
|
|
3024
|
+
};
|
|
3025
|
+
});
|
|
3026
|
+
const avg = determine_average_entry_and_size(adjusted_focus_entries.concat([
|
|
3027
|
+
{
|
|
3028
|
+
price: entry,
|
|
3029
|
+
quantity
|
|
3030
|
+
}
|
|
3031
|
+
]), this.config.global_config.decimal_places, this.config.global_config.price_places);
|
|
3032
|
+
const focus_loss = this.to_f(Math.abs(avg.price - second_payload.stop) * avg.quantity);
|
|
3033
|
+
let below_reverse_entries = kind === "long" ? entries.filter((u) => {
|
|
3034
|
+
return u.entry < (reverse_position.entry || focus_position.entry);
|
|
3035
|
+
}) : entries.filter((u) => {
|
|
3036
|
+
return u.entry > (reverse_position.entry || focus_position.entry);
|
|
3037
|
+
});
|
|
3038
|
+
const threshold = below_reverse_entries.at(-1);
|
|
3039
|
+
let adjusted_reverse_entries = entries.map((entry2) => {
|
|
3040
|
+
let adjusted_price = entry2.price;
|
|
3041
|
+
if (threshold) {
|
|
3042
|
+
if (reverse_kind === "short" && entry2.price > threshold.entry) {
|
|
3043
|
+
adjusted_price = threshold.entry;
|
|
3044
|
+
} else if (reverse_kind === "long" && entry2.price < threshold.entry) {
|
|
3045
|
+
adjusted_price = threshold.entry;
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
return {
|
|
3049
|
+
price: adjusted_price,
|
|
3050
|
+
quantity: entry2.quantity
|
|
3051
|
+
};
|
|
3052
|
+
});
|
|
3053
|
+
const reverse_avg = determine_average_entry_and_size(adjusted_reverse_entries.concat([
|
|
3054
|
+
{
|
|
3055
|
+
price: reverse_position.entry,
|
|
3056
|
+
quantity: reverse_position.quantity
|
|
3057
|
+
}
|
|
3058
|
+
]), this.config.global_config.decimal_places, this.config.global_config.price_places);
|
|
3059
|
+
const sell_quantity = this.to_df(reverse_avg.quantity * sell_factor);
|
|
3060
|
+
const reverse_pnl = this.to_f(Math.abs(reverse_avg.price - second_payload.stop) * sell_quantity);
|
|
3061
|
+
const fee_to_pay = this.calculate_fee({
|
|
3062
|
+
price: avg.entry,
|
|
3063
|
+
quantity: avg.quantity
|
|
3064
|
+
}) + this.calculate_fee({
|
|
3065
|
+
price: reverse_avg.entry,
|
|
3066
|
+
quantity: sell_quantity
|
|
3067
|
+
});
|
|
3068
|
+
const net_reverse_pnl = reverse_pnl - fee_to_pay;
|
|
3069
|
+
const ratio = net_reverse_pnl * reduce_ratio / focus_loss;
|
|
3070
|
+
const quantity_to_sell = this.to_df(ratio * avg.quantity);
|
|
3071
|
+
const remaining_quantity = this.to_df(avg.quantity - quantity_to_sell);
|
|
3072
|
+
const incurred_loss = this.to_f((avg.price - second_payload.stop) * quantity_to_sell);
|
|
3073
|
+
return {
|
|
3074
|
+
risk,
|
|
3075
|
+
risk_reward: this.config.risk_reward,
|
|
3076
|
+
[kind]: {
|
|
3077
|
+
avg_entry: avg.entry,
|
|
3078
|
+
avg_size: avg.quantity,
|
|
3079
|
+
loss: focus_loss,
|
|
3080
|
+
stop: second_payload.stop,
|
|
3081
|
+
stop_quantity: quantity_to_sell,
|
|
3082
|
+
re_entry_quantity: remaining_quantity,
|
|
3083
|
+
initial_pnl: this.pnl(kind),
|
|
3084
|
+
tp: second_payload.entry,
|
|
3085
|
+
incurred_loss
|
|
3086
|
+
},
|
|
3087
|
+
[reverse_kind]: {
|
|
3088
|
+
avg_entry: reverse_avg.entry,
|
|
3089
|
+
avg_size: reverse_avg.quantity,
|
|
3090
|
+
pnl: reverse_pnl,
|
|
3091
|
+
tp: second_payload.stop,
|
|
3092
|
+
re_entry_quantity: remaining_quantity,
|
|
3093
|
+
initial_pnl: this.pnl(reverse_kind),
|
|
3094
|
+
remaining_quantity: this.to_df(reverse_avg.quantity - sell_quantity)
|
|
3095
|
+
},
|
|
3096
|
+
spread: Math.abs(avg.entry - reverse_avg.entry),
|
|
3097
|
+
gap_loss: to_f(Math.abs(avg.entry - reverse_avg.entry) * reverse_avg.quantity, "%.2f"),
|
|
3098
|
+
net_profit: incurred_loss + reverse_pnl
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
runIterations(payload) {
|
|
3102
|
+
const {
|
|
3103
|
+
kind,
|
|
3104
|
+
iterations,
|
|
3105
|
+
ignore_entries = false,
|
|
3106
|
+
reduce_ratio = 1,
|
|
3107
|
+
sell_factor = 1
|
|
3108
|
+
} = payload;
|
|
3109
|
+
const reverse_kind = kind == "long" ? "short" : "long";
|
|
3110
|
+
const result = [];
|
|
3111
|
+
let position2 = {
|
|
3112
|
+
long: this.position.long,
|
|
3113
|
+
short: this.position.short
|
|
3114
|
+
};
|
|
3115
|
+
let tp_percent_multiplier = 1;
|
|
3116
|
+
let short_tp_factor_multiplier = 1;
|
|
3117
|
+
for (let i = 0;i < iterations; i++) {
|
|
3118
|
+
const instance = new Strategy({
|
|
3119
|
+
long: position2.long,
|
|
3120
|
+
short: position2.short,
|
|
3121
|
+
config: {
|
|
3122
|
+
...this.config,
|
|
3123
|
+
tp_percent: this.config.tp_percent * tp_percent_multiplier,
|
|
3124
|
+
short_tp_factor: this.config.short_tp_factor * short_tp_factor_multiplier
|
|
3125
|
+
}
|
|
3126
|
+
});
|
|
3127
|
+
const algorithm = instance.generateGapClosingAlgorithm({
|
|
3128
|
+
kind,
|
|
3129
|
+
ignore_entries,
|
|
3130
|
+
reduce_ratio,
|
|
3131
|
+
sell_factor
|
|
3132
|
+
});
|
|
3133
|
+
if (!algorithm) {
|
|
3134
|
+
console.log("No algorithm found");
|
|
3135
|
+
return result;
|
|
3136
|
+
break;
|
|
3137
|
+
}
|
|
3138
|
+
result.push(algorithm);
|
|
3139
|
+
position2[kind] = {
|
|
3140
|
+
entry: algorithm[kind].avg_entry,
|
|
3141
|
+
quantity: algorithm[kind].re_entry_quantity
|
|
3142
|
+
};
|
|
3143
|
+
let reverse_entry = algorithm[reverse_kind].tp;
|
|
3144
|
+
let reverse_quantity = algorithm[reverse_kind].re_entry_quantity;
|
|
3145
|
+
if (algorithm[reverse_kind].remaining_quantity > 0) {
|
|
3146
|
+
const purchase_to_occur = {
|
|
3147
|
+
price: reverse_entry,
|
|
3148
|
+
quantity: algorithm[reverse_kind].remaining_quantity
|
|
3149
|
+
};
|
|
3150
|
+
const avg = determine_average_entry_and_size([
|
|
3151
|
+
purchase_to_occur,
|
|
3152
|
+
{
|
|
3153
|
+
price: algorithm[reverse_kind].avg_entry,
|
|
3154
|
+
quantity: reverse_quantity - algorithm[reverse_kind].remaining_quantity
|
|
3155
|
+
}
|
|
3156
|
+
], this.config.global_config.decimal_places, this.config.global_config.price_places);
|
|
3157
|
+
reverse_entry = avg.entry;
|
|
3158
|
+
reverse_quantity = avg.quantity;
|
|
3159
|
+
if (reverse_kind === "short") {
|
|
3160
|
+
short_tp_factor_multiplier = 2;
|
|
3161
|
+
} else {
|
|
3162
|
+
tp_percent_multiplier = 2;
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
position2[reverse_kind] = {
|
|
3166
|
+
entry: reverse_entry,
|
|
3167
|
+
quantity: reverse_quantity
|
|
3168
|
+
};
|
|
3169
|
+
}
|
|
3170
|
+
return result;
|
|
3171
|
+
}
|
|
3172
|
+
getPositionAfterTp(payload) {
|
|
3173
|
+
const { kind, include_fees = false } = payload;
|
|
3174
|
+
const focus_position = this.position[kind];
|
|
3175
|
+
const reverse_kind = kind == "long" ? "short" : "long";
|
|
3176
|
+
const reverse_position = this.position[reverse_kind];
|
|
3177
|
+
const focus_tp = this.tp(kind);
|
|
3178
|
+
const focus_pnl = this.pnl(kind);
|
|
3179
|
+
const fees = include_fees ? this.calculate_fee({
|
|
3180
|
+
price: focus_tp,
|
|
3181
|
+
quantity: focus_position.quantity
|
|
3182
|
+
}) * 2 : 0;
|
|
3183
|
+
const expected_loss = (focus_pnl - fees) * this.config.reduce_ratio;
|
|
3184
|
+
const actual_loss = Math.abs(focus_tp - reverse_position.entry) * reverse_position.quantity;
|
|
3185
|
+
const ratio = expected_loss / actual_loss;
|
|
3186
|
+
const loss_quantity = this.to_df(ratio * reverse_position.quantity);
|
|
3187
|
+
const remaining_quantity = this.to_df(reverse_position.quantity - loss_quantity);
|
|
3188
|
+
const diff = focus_pnl - expected_loss;
|
|
3189
|
+
return {
|
|
3190
|
+
[kind]: {
|
|
3191
|
+
entry: focus_tp,
|
|
3192
|
+
quantity: remaining_quantity
|
|
3193
|
+
},
|
|
3194
|
+
[reverse_kind]: {
|
|
3195
|
+
entry: reverse_position.entry,
|
|
3196
|
+
quantity: remaining_quantity
|
|
3197
|
+
},
|
|
3198
|
+
pnl: {
|
|
3199
|
+
[kind]: focus_pnl,
|
|
3200
|
+
[reverse_kind]: -expected_loss,
|
|
3201
|
+
diff
|
|
3202
|
+
},
|
|
3203
|
+
spread: this.to_f(Math.abs(focus_tp - reverse_position.entry) * remaining_quantity)
|
|
3204
|
+
};
|
|
3205
|
+
}
|
|
3206
|
+
getPositionAfterIteration(payload) {
|
|
3207
|
+
const { kind, iterations, with_fees = false } = payload;
|
|
3208
|
+
let _result = this.getPositionAfterTp({
|
|
3209
|
+
kind,
|
|
3210
|
+
include_fees: with_fees
|
|
3211
|
+
});
|
|
3212
|
+
const result = [_result];
|
|
3213
|
+
for (let i = 0;i < iterations - 1; i++) {
|
|
3214
|
+
const instance = new Strategy({
|
|
3215
|
+
long: _result.long,
|
|
3216
|
+
short: _result.short,
|
|
3217
|
+
config: {
|
|
3218
|
+
...this.config,
|
|
3219
|
+
tp_percent: this.config.tp_percent + (i + 1) * 0.3
|
|
3220
|
+
}
|
|
3221
|
+
});
|
|
3222
|
+
_result = instance.getPositionAfterTp({
|
|
3223
|
+
kind,
|
|
3224
|
+
include_fees: with_fees
|
|
3225
|
+
});
|
|
3226
|
+
result.push(_result);
|
|
3227
|
+
}
|
|
3228
|
+
return result;
|
|
3229
|
+
}
|
|
3230
|
+
generateOppositeTrades(payload) {
|
|
3231
|
+
const { kind, risk_factor = 0.5, avg_entry } = payload;
|
|
3232
|
+
const entry = avg_entry || this.position[kind].entry;
|
|
3233
|
+
const stop = this.tp(kind);
|
|
3234
|
+
const risk = this.pnl(kind) * risk_factor;
|
|
3235
|
+
const risk_reward = getRiskReward({
|
|
3236
|
+
entry,
|
|
3237
|
+
stop,
|
|
3238
|
+
risk,
|
|
3239
|
+
global_config: this.config.global_config
|
|
3240
|
+
});
|
|
3241
|
+
const { entries, last_value, ...app_config } = buildAppConfig(this.config.global_config, {
|
|
3242
|
+
entry,
|
|
3243
|
+
stop,
|
|
3244
|
+
risk_reward,
|
|
3245
|
+
risk,
|
|
3246
|
+
symbol: this.config.global_config.symbol
|
|
3247
|
+
});
|
|
3248
|
+
const trades_to_place = determine_amount_to_buy({
|
|
3249
|
+
orders: entries,
|
|
3250
|
+
kind: app_config.kind,
|
|
3251
|
+
decimal_places: app_config.decimal_places,
|
|
3252
|
+
price_places: app_config.price_places,
|
|
3253
|
+
position: this.position[app_config.kind],
|
|
3254
|
+
existingOrders: []
|
|
3255
|
+
});
|
|
3256
|
+
const avg = determine_average_entry_and_size(trades_to_place.map((u) => ({
|
|
3257
|
+
price: u.entry,
|
|
3258
|
+
quantity: u.quantity
|
|
3259
|
+
})).concat([
|
|
3260
|
+
{
|
|
3261
|
+
price: this.position[app_config.kind].entry,
|
|
3262
|
+
quantity: this.position[app_config.kind].quantity
|
|
3263
|
+
}
|
|
3264
|
+
]), app_config.decimal_places, app_config.price_places);
|
|
3265
|
+
const expected_loss = to_f(Math.abs(avg.price - stop) * avg.quantity, "%.2f");
|
|
3266
|
+
const profit_percent = to_f(this.pnl(kind) * 100 / (avg.price * avg.quantity), "%.3f");
|
|
3267
|
+
app_config.entry = this.to_f(app_config.entry);
|
|
3268
|
+
app_config.stop = this.to_f(app_config.stop);
|
|
3269
|
+
return { ...app_config, avg, loss: -expected_loss, profit_percent };
|
|
3270
|
+
}
|
|
3271
|
+
identifyGapConfig(payload) {
|
|
3272
|
+
const { factor, sell_factor = 1, kind, risk } = payload;
|
|
3273
|
+
return generateGapTp({
|
|
3274
|
+
long: this.position.long,
|
|
3275
|
+
short: this.position.short,
|
|
3276
|
+
factor,
|
|
3277
|
+
sell_factor,
|
|
3278
|
+
kind,
|
|
3279
|
+
risk,
|
|
3280
|
+
decimal_places: this.config.global_config.decimal_places,
|
|
3281
|
+
price_places: this.config.global_config.price_places
|
|
3282
|
+
});
|
|
3283
|
+
}
|
|
3284
|
+
analyzeProfit(payload) {
|
|
3285
|
+
const { reward_factor = 1, max_reward_factor, risk, kind } = payload;
|
|
3286
|
+
const focus_position = this.position[kind];
|
|
3287
|
+
const result = computeProfitDetail({
|
|
3288
|
+
focus_position: {
|
|
3289
|
+
kind,
|
|
3290
|
+
entry: focus_position.entry,
|
|
3291
|
+
quantity: focus_position.quantity,
|
|
3292
|
+
avg_price: focus_position.avg_price,
|
|
3293
|
+
avg_qty: focus_position.avg_qty
|
|
3294
|
+
},
|
|
3295
|
+
pnl: this.pnl(kind),
|
|
3296
|
+
strategy: {
|
|
3297
|
+
reward_factor,
|
|
3298
|
+
max_reward_factor,
|
|
3299
|
+
risk
|
|
3300
|
+
}
|
|
3301
|
+
});
|
|
3302
|
+
return result;
|
|
3303
|
+
}
|
|
3304
|
+
simulateGapReduction(payload) {
|
|
3305
|
+
const {
|
|
3306
|
+
factor,
|
|
3307
|
+
direction,
|
|
3308
|
+
sell_factor = 1,
|
|
3309
|
+
iterations = 10,
|
|
3310
|
+
risk: desired_risk,
|
|
3311
|
+
kind
|
|
3312
|
+
} = payload;
|
|
3313
|
+
const results = [];
|
|
3314
|
+
let params = {
|
|
3315
|
+
long: this.position.long,
|
|
3316
|
+
short: this.position.short,
|
|
3317
|
+
config: this.config
|
|
3318
|
+
};
|
|
3319
|
+
for (let i = 0;i < iterations; i++) {
|
|
3320
|
+
const instance = new Strategy(params);
|
|
3321
|
+
const { profit_percent, risk, take_profit, sell_quantity, gap_loss } = instance.identifyGapConfig({
|
|
3322
|
+
factor,
|
|
3323
|
+
sell_factor,
|
|
3324
|
+
kind,
|
|
3325
|
+
risk: desired_risk
|
|
3326
|
+
});
|
|
3327
|
+
const to_add = {
|
|
3328
|
+
profit_percent,
|
|
3329
|
+
risk,
|
|
3330
|
+
take_profit,
|
|
3331
|
+
sell_quantity,
|
|
3332
|
+
gap_loss,
|
|
3333
|
+
position: {
|
|
3334
|
+
long: {
|
|
3335
|
+
entry: this.to_f(params.long.entry),
|
|
3336
|
+
quantity: this.to_df(params.long.quantity)
|
|
3337
|
+
},
|
|
3338
|
+
short: {
|
|
3339
|
+
entry: this.to_f(params.short.entry),
|
|
3340
|
+
quantity: this.to_df(params.short.quantity)
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
};
|
|
3344
|
+
const sell_kind = direction == "long" ? "short" : "long";
|
|
3345
|
+
if (sell_quantity[sell_kind] <= 0) {
|
|
3346
|
+
break;
|
|
3347
|
+
}
|
|
3348
|
+
results.push(to_add);
|
|
3349
|
+
const remaining = this.to_df(params[sell_kind].quantity - sell_quantity[sell_kind]);
|
|
3350
|
+
if (remaining <= 0) {
|
|
3351
|
+
break;
|
|
3352
|
+
}
|
|
3353
|
+
params[sell_kind].quantity = remaining;
|
|
3354
|
+
params[direction] = {
|
|
3355
|
+
entry: take_profit[direction],
|
|
3356
|
+
quantity: remaining
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
const last_gap_loss = results.at(-1)?.gap_loss;
|
|
3360
|
+
const last_tp = results.at(-1)?.take_profit[direction];
|
|
3361
|
+
const entry = this.position[direction].entry;
|
|
3362
|
+
const quantity = this.to_df(Math.abs(last_tp - entry) / last_gap_loss);
|
|
3363
|
+
return {
|
|
3364
|
+
results,
|
|
3365
|
+
quantity
|
|
3366
|
+
};
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
// src/helpers/compound.ts
|
|
3370
|
+
function buildTrades(payload) {
|
|
3371
|
+
const { appConfig, settings, kind } = payload;
|
|
3372
|
+
const kelly_config = settings.kelly;
|
|
3373
|
+
const distribution_params = settings.distribution_params;
|
|
3374
|
+
const current_app_config = { ...appConfig[kind] };
|
|
3375
|
+
const entryNum = parseFloat(settings.entry);
|
|
3376
|
+
const stopNum = parseFloat(settings.stop);
|
|
3377
|
+
current_app_config.entry = entryNum;
|
|
3378
|
+
current_app_config.stop = stopNum;
|
|
3379
|
+
current_app_config.risk_per_trade = parseFloat(settings.risk);
|
|
3380
|
+
current_app_config.risk_reward = parseFloat(settings.risk_reward);
|
|
3381
|
+
current_app_config.kind = kind;
|
|
3382
|
+
current_app_config.kelly = kelly_config;
|
|
3383
|
+
current_app_config.distribution_params = distribution_params;
|
|
3384
|
+
const options = {
|
|
3385
|
+
take_profit: null,
|
|
3386
|
+
entry: current_app_config.entry,
|
|
3387
|
+
stop: current_app_config.stop,
|
|
3388
|
+
raw_instance: null,
|
|
3389
|
+
risk: current_app_config.risk_per_trade,
|
|
3390
|
+
no_of_trades: undefined,
|
|
3391
|
+
risk_reward: current_app_config.risk_reward,
|
|
3392
|
+
kind: current_app_config.kind,
|
|
3393
|
+
increase: true,
|
|
3394
|
+
gap: current_app_config.gap,
|
|
3395
|
+
rr: current_app_config.rr,
|
|
3396
|
+
price_places: current_app_config.price_places,
|
|
3397
|
+
decimal_places: current_app_config.decimal_places,
|
|
3398
|
+
use_kelly: kelly_config?.use_kelly,
|
|
3399
|
+
kelly_confidence_factor: kelly_config?.kelly_confidence_factor,
|
|
3400
|
+
kelly_minimum_risk: kelly_config?.kelly_minimum_risk,
|
|
3401
|
+
kelly_prediction_model: kelly_config?.kelly_prediction_model,
|
|
3402
|
+
kelly_func: kelly_config?.kelly_func,
|
|
3403
|
+
distribution: settings.distribution
|
|
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
|
+
});
|
|
3455
|
+
if (!force_exact_risk) {
|
|
3456
|
+
result = {
|
|
3457
|
+
risk_reward: result,
|
|
3458
|
+
risk
|
|
3459
|
+
};
|
|
3460
|
+
}
|
|
3461
|
+
const trades = result.risk_reward ? buildTrades({
|
|
3462
|
+
appConfig: { [app_config_kind]: appConfig },
|
|
3463
|
+
kind: app_config_kind,
|
|
3464
|
+
settings: {
|
|
3465
|
+
entry: custom_b_config.entry,
|
|
3466
|
+
stop: custom_b_config.stop,
|
|
3467
|
+
risk: result.risk || custom_b_config.risk,
|
|
3468
|
+
risk_reward: result.risk_reward,
|
|
3469
|
+
distribution: custom_b_config.distribution
|
|
3470
|
+
}
|
|
3471
|
+
}) : [];
|
|
3472
|
+
const summary = trades.length > 0 ? generateSummary({ trades }) : {};
|
|
3473
|
+
return { trades, result, summary };
|
|
3474
|
+
}
|
|
3475
|
+
function constructAppConfig2({
|
|
3476
|
+
config,
|
|
3477
|
+
global_config
|
|
3478
|
+
}) {
|
|
3479
|
+
const options = {
|
|
3480
|
+
entry: config?.entry,
|
|
3481
|
+
stop: config?.stop,
|
|
3482
|
+
risk_reward: config?.risk_reward,
|
|
3483
|
+
risk: config?.risk,
|
|
3484
|
+
symbol: config.symbol
|
|
3485
|
+
};
|
|
3486
|
+
const { entries: _entries, ...appConfig } = buildAppConfig(global_config, options);
|
|
3487
|
+
return appConfig;
|
|
3488
|
+
}
|
|
3489
|
+
function buildWithOptimumReward({
|
|
3490
|
+
config,
|
|
3491
|
+
settings,
|
|
3492
|
+
global_config,
|
|
3493
|
+
force_exact
|
|
3494
|
+
}) {
|
|
3495
|
+
const kind = config.entry > config.stop ? "long" : "short";
|
|
3496
|
+
let stop = settings.stop;
|
|
3497
|
+
let entry = settings.entry;
|
|
3498
|
+
const risk = settings.risk;
|
|
3499
|
+
const stop_ratio = settings.stop_ratio || 1;
|
|
3500
|
+
const distribution = settings.distribution || config?.distribution;
|
|
3501
|
+
const distribution_params = settings.distribution_params || config?.distribution_params;
|
|
3502
|
+
const custom_b_config = {
|
|
3503
|
+
entry,
|
|
3504
|
+
stop,
|
|
3505
|
+
risk,
|
|
3506
|
+
distribution,
|
|
3507
|
+
distribution_params
|
|
3508
|
+
};
|
|
3509
|
+
const appConfig = constructAppConfig2({
|
|
3510
|
+
config,
|
|
3511
|
+
global_config
|
|
3512
|
+
});
|
|
3513
|
+
const { trades, summary, result } = helperFuncToBuildTrades({
|
|
3514
|
+
custom_b_config,
|
|
3515
|
+
app_config_kind: kind,
|
|
3516
|
+
appConfig,
|
|
3517
|
+
symbol_config: global_config,
|
|
3518
|
+
force_exact_risk: force_exact
|
|
3519
|
+
});
|
|
3520
|
+
const adjusted_size = summary.quantity;
|
|
3521
|
+
const symbol_config = global_config;
|
|
3522
|
+
const entryDetails = {
|
|
3523
|
+
entry: to_f(custom_b_config.entry, symbol_config.price_places),
|
|
3524
|
+
stop: to_f(custom_b_config.stop, symbol_config.price_places),
|
|
3525
|
+
risk: to_f(result.risk, "%.2f"),
|
|
3526
|
+
risk_reward: result.risk_reward,
|
|
3527
|
+
avg_entry: to_f(summary.entry, symbol_config.price_places),
|
|
3528
|
+
avg_size: to_f(adjusted_size, symbol_config.decimal_places),
|
|
3529
|
+
first_entry: to_f(summary.first_entry, symbol_config.price_places),
|
|
3530
|
+
pnl: to_f(custom_b_config.risk, "%.2f"),
|
|
3531
|
+
fee: to_f(summary.fee, "%.2f"),
|
|
3532
|
+
loss: to_f(summary.loss, "%.2f"),
|
|
3533
|
+
last_entry: to_f(summary.last_entry, symbol_config.price_places),
|
|
3534
|
+
margin: to_f(summary.entry * adjusted_size / symbol_config.leverage, "%.2f")
|
|
3535
|
+
};
|
|
3536
|
+
return {
|
|
3537
|
+
trades,
|
|
3538
|
+
summary: entryDetails,
|
|
3539
|
+
config: {
|
|
3540
|
+
...custom_b_config,
|
|
3541
|
+
...result,
|
|
3542
|
+
stop_ratio
|
|
3543
|
+
},
|
|
3544
|
+
stop_order: {
|
|
3545
|
+
quantity: entryDetails.avg_size * stop_ratio,
|
|
3546
|
+
price: entryDetails.stop
|
|
3547
|
+
},
|
|
3548
|
+
kind
|
|
3549
|
+
};
|
|
3550
|
+
}
|
|
3551
|
+
function generateOppositeOptimum({
|
|
3552
|
+
config,
|
|
3553
|
+
global_config,
|
|
3554
|
+
settings,
|
|
3555
|
+
ratio = 1,
|
|
3556
|
+
distribution,
|
|
3557
|
+
distribution_params,
|
|
3558
|
+
risk_factor = 1
|
|
3559
|
+
}) {
|
|
3560
|
+
const configKind = config.entry > config.stop ? "long" : "short";
|
|
3561
|
+
if (configKind === "long" && config.entry > config.stop) {
|
|
3562
|
+
if (settings.stop <= settings.entry) {
|
|
3563
|
+
throw new Error("Invalid input: For long config positions, opposite settings must have stop > entry");
|
|
3564
|
+
}
|
|
3565
|
+
} else if (configKind === "short" && config.entry < config.stop) {
|
|
3566
|
+
if (settings.stop >= settings.entry) {
|
|
3567
|
+
throw new Error("Invalid input: For short config positions, opposite settings must have stop < entry");
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
const kind = config.entry > config.stop ? "long" : "short";
|
|
3571
|
+
const app_config_kind = kind === "long" ? "short" : "long";
|
|
3572
|
+
let risk = settings.risk;
|
|
3573
|
+
const custom_b_config = {
|
|
3574
|
+
entry: settings.entry,
|
|
3575
|
+
stop: settings.stop,
|
|
3576
|
+
risk: risk * ratio,
|
|
3577
|
+
distribution: distribution || "inverse-exponential",
|
|
3578
|
+
distribution_params: distribution_params || config?.distribution_params,
|
|
3579
|
+
risk_factor
|
|
3580
|
+
};
|
|
3581
|
+
const appConfig = constructAppConfig2({
|
|
3582
|
+
config: {
|
|
3583
|
+
...config,
|
|
3584
|
+
...custom_b_config
|
|
3585
|
+
},
|
|
3586
|
+
global_config
|
|
3587
|
+
});
|
|
3588
|
+
const { result, trades, summary } = helperFuncToBuildTrades({
|
|
3589
|
+
custom_b_config,
|
|
3590
|
+
symbol_config: global_config,
|
|
3591
|
+
app_config_kind,
|
|
3592
|
+
appConfig
|
|
3593
|
+
});
|
|
3594
|
+
if (Object.keys(summary).length === 0) {
|
|
3595
|
+
return {
|
|
3596
|
+
trades,
|
|
3597
|
+
summary,
|
|
3598
|
+
config: custom_b_config,
|
|
3599
|
+
kind: app_config_kind
|
|
3600
|
+
};
|
|
3601
|
+
}
|
|
3602
|
+
const symbol_config = global_config;
|
|
3603
|
+
const entryDetails = {
|
|
3604
|
+
entry: to_f(custom_b_config.entry, symbol_config.price_places),
|
|
3605
|
+
stop: to_f(custom_b_config.stop, symbol_config.price_places),
|
|
3606
|
+
risk: to_f(result.risk, "%.2f"),
|
|
3607
|
+
risk_reward: result.risk_reward,
|
|
3608
|
+
avg_entry: to_f(summary.entry, symbol_config.price_places),
|
|
3609
|
+
avg_size: to_f(summary.quantity, symbol_config.decimal_places),
|
|
3610
|
+
first_entry: to_f(summary.first_entry, symbol_config.price_places),
|
|
3611
|
+
pnl: to_f(custom_b_config.risk, "%.2f"),
|
|
3612
|
+
fee: to_f(summary.fee, "%.2f"),
|
|
3613
|
+
loss: to_f(summary.loss, "%.2f"),
|
|
3614
|
+
last_entry: to_f(summary.last_entry, symbol_config.price_places),
|
|
3615
|
+
defaultEntry: settings.entry ? to_f(settings.entry, symbol_config.price_places) : null
|
|
3616
|
+
};
|
|
3617
|
+
return {
|
|
3618
|
+
trades,
|
|
3619
|
+
summary: entryDetails,
|
|
3620
|
+
config: {
|
|
3621
|
+
...custom_b_config,
|
|
3622
|
+
...result
|
|
3623
|
+
},
|
|
3624
|
+
kind: app_config_kind
|
|
3625
|
+
};
|
|
3626
|
+
}
|
|
3627
|
+
function defaultTradeFromCurrentState({
|
|
3628
|
+
config,
|
|
3629
|
+
global_config
|
|
3630
|
+
}) {
|
|
3631
|
+
const kind = config.entry > config.stop ? "long" : "short";
|
|
3632
|
+
const settings = {
|
|
3633
|
+
entry: config?.entry,
|
|
3634
|
+
stop: config?.stop,
|
|
3635
|
+
risk: config?.risk,
|
|
3636
|
+
distribution: config?.distribution,
|
|
3637
|
+
risk_reward: config?.risk_reward
|
|
3638
|
+
};
|
|
3639
|
+
const appConfig = constructAppConfig2({
|
|
3640
|
+
config,
|
|
3641
|
+
global_config
|
|
3642
|
+
});
|
|
3643
|
+
const trades = buildTrades({
|
|
3644
|
+
appConfig: { [kind]: appConfig },
|
|
3645
|
+
kind,
|
|
3646
|
+
settings
|
|
3647
|
+
});
|
|
3648
|
+
return {
|
|
3649
|
+
trades,
|
|
3650
|
+
summary: generateSummary({
|
|
3651
|
+
trades,
|
|
3652
|
+
fee_percent: global_config.fee_percent
|
|
3653
|
+
})
|
|
3654
|
+
};
|
|
3655
|
+
}
|
|
3656
|
+
function increaseTradeHelper({
|
|
3657
|
+
increase_qty,
|
|
3658
|
+
stop,
|
|
3659
|
+
config,
|
|
3660
|
+
global_config,
|
|
3661
|
+
style,
|
|
3662
|
+
entry,
|
|
3663
|
+
position: position2,
|
|
3664
|
+
stop_ratio = 1,
|
|
3665
|
+
distribution: default_distribution,
|
|
3666
|
+
distribution_params: default_distribution_params
|
|
3667
|
+
}) {
|
|
3668
|
+
const symbol_config = global_config;
|
|
3669
|
+
const kind = config.entry > config.stop ? "long" : "short";
|
|
3670
|
+
const distribution = default_distribution || config.distribution || "inverse-exponential";
|
|
3671
|
+
const distribution_params = default_distribution_params || config.distribution_params;
|
|
3672
|
+
const appConfig = constructAppConfig2({
|
|
3673
|
+
config,
|
|
3674
|
+
global_config
|
|
3675
|
+
});
|
|
3676
|
+
const currentState = defaultTradeFromCurrentState({
|
|
3677
|
+
config,
|
|
3678
|
+
global_config
|
|
3679
|
+
});
|
|
3680
|
+
const { optimal_risk, neg_pnl } = getOptimumStopAndRisk(appConfig, {
|
|
3681
|
+
max_size: increase_qty,
|
|
3682
|
+
target_stop: stop,
|
|
3683
|
+
distribution
|
|
3684
|
+
});
|
|
3685
|
+
if (neg_pnl === 0) {
|
|
3686
|
+
return {
|
|
3687
|
+
trades: [],
|
|
3688
|
+
summary: {},
|
|
3689
|
+
config: {},
|
|
3690
|
+
kind,
|
|
3691
|
+
current: currentState
|
|
3692
|
+
};
|
|
3693
|
+
}
|
|
3694
|
+
const custom_b_config = {
|
|
3695
|
+
entry,
|
|
3696
|
+
stop,
|
|
3697
|
+
risk: style === "minimum" ? Math.abs(neg_pnl) : optimal_risk,
|
|
3698
|
+
distribution,
|
|
3699
|
+
distribution_params
|
|
3700
|
+
};
|
|
3701
|
+
const { result, trades, summary } = helperFuncToBuildTrades({
|
|
3702
|
+
custom_b_config,
|
|
3703
|
+
symbol_config,
|
|
3704
|
+
appConfig,
|
|
3705
|
+
app_config_kind: kind
|
|
3706
|
+
});
|
|
3707
|
+
if (Object.keys(summary).length === 0) {
|
|
3708
|
+
return {
|
|
3709
|
+
trades,
|
|
3710
|
+
summary,
|
|
3711
|
+
config: {
|
|
3712
|
+
...custom_b_config,
|
|
3713
|
+
...result
|
|
3714
|
+
},
|
|
3715
|
+
kind,
|
|
3716
|
+
current: currentState
|
|
3717
|
+
};
|
|
3718
|
+
}
|
|
3719
|
+
const new_avg_values = determine_average_entry_and_size([
|
|
3720
|
+
{
|
|
3721
|
+
price: position2.entry,
|
|
3722
|
+
quantity: position2.quantity
|
|
3723
|
+
},
|
|
3724
|
+
{
|
|
3725
|
+
price: summary?.entry,
|
|
3726
|
+
quantity: summary?.quantity
|
|
3727
|
+
}
|
|
3728
|
+
], symbol_config.decimal_places, symbol_config.price_places);
|
|
3729
|
+
summary.entry = new_avg_values.entry;
|
|
3730
|
+
summary.quantity = new_avg_values.quantity;
|
|
3731
|
+
const loss = Math.abs(summary.entry - custom_b_config.stop) * summary.quantity;
|
|
3732
|
+
const entryDetails = {
|
|
3733
|
+
entry: to_f(custom_b_config.entry, symbol_config.price_places),
|
|
3734
|
+
stop: to_f(custom_b_config.stop, symbol_config.price_places),
|
|
3735
|
+
risk: to_f(result.risk, symbol_config.price_places),
|
|
3736
|
+
risk_reward: result.risk_reward,
|
|
3737
|
+
avg_entry: to_f(summary.entry, symbol_config.price_places),
|
|
3738
|
+
avg_size: to_f(summary.quantity, symbol_config.decimal_places),
|
|
3739
|
+
first_entry: to_f(summary.first_entry, symbol_config.price_places),
|
|
3740
|
+
pnl: to_f(custom_b_config.risk, "%.2f"),
|
|
3741
|
+
fee: to_f(summary.fee, "%.2f"),
|
|
3742
|
+
loss: to_f(loss, "%.2f"),
|
|
3743
|
+
last_entry: to_f(summary.last_entry, symbol_config.price_places),
|
|
3744
|
+
margin: to_f(summary.entry * summary.quantity / global_config.leverage, "%.2f")
|
|
3745
|
+
};
|
|
3746
|
+
return {
|
|
3747
|
+
trades,
|
|
3748
|
+
summary: entryDetails,
|
|
3749
|
+
stop_order: {
|
|
3750
|
+
quantity: entryDetails.avg_size * stop_ratio,
|
|
3751
|
+
price: entryDetails.stop
|
|
3752
|
+
},
|
|
3753
|
+
config: {
|
|
3754
|
+
...custom_b_config,
|
|
3755
|
+
...result
|
|
3756
|
+
},
|
|
3757
|
+
kind,
|
|
3758
|
+
current: currentState
|
|
3759
|
+
};
|
|
3760
|
+
}
|
|
3761
|
+
function generatePositionIncreaseTrade({
|
|
3762
|
+
account,
|
|
3763
|
+
zoneAccount,
|
|
3764
|
+
ratio = 0.1,
|
|
3765
|
+
config,
|
|
3766
|
+
global_config,
|
|
3767
|
+
style = "minimum",
|
|
3768
|
+
distribution = "inverse-exponential",
|
|
3769
|
+
distribution_params
|
|
3770
|
+
}) {
|
|
3771
|
+
const kind = config.entry > config.stop ? "long" : "short";
|
|
3772
|
+
const target_max_quantity = kind === "long" ? account.short.quantity : account.long.quantity;
|
|
3773
|
+
const increase_qty = target_max_quantity * ratio;
|
|
3774
|
+
const entry = zoneAccount.entry;
|
|
3775
|
+
const stop = zoneAccount.stop;
|
|
3776
|
+
return increaseTradeHelper({
|
|
3777
|
+
config,
|
|
3778
|
+
position: account[kind],
|
|
3779
|
+
global_config,
|
|
3780
|
+
entry,
|
|
3781
|
+
stop,
|
|
3782
|
+
style,
|
|
3783
|
+
increase_qty,
|
|
3784
|
+
distribution,
|
|
3785
|
+
distribution_params
|
|
3786
|
+
});
|
|
3787
|
+
}
|
|
3788
|
+
function determineHedgeTradeToPlace({
|
|
3789
|
+
position: position2,
|
|
3790
|
+
config,
|
|
3791
|
+
global_config,
|
|
3792
|
+
profit_risk = 200,
|
|
3793
|
+
allowable_loss = 1000
|
|
3794
|
+
}) {
|
|
3795
|
+
const diff = profit_risk / position2.quantity;
|
|
3796
|
+
const kind = position2.kind === "long" ? "short" : "long";
|
|
3797
|
+
const tp_price = position2.kind === "long" ? diff + position2.entry : position2.entry - diff;
|
|
3798
|
+
const loss_diff = allowable_loss / position2.quantity;
|
|
3799
|
+
const loss_price = position2.kind === "long" ? position2.entry - loss_diff : position2.entry + loss_diff;
|
|
3800
|
+
const entry = kind === "short" ? loss_price : tp_price;
|
|
3801
|
+
const stop = kind === "short" ? tp_price : loss_price;
|
|
3802
|
+
const result = buildWithOptimumReward({
|
|
3803
|
+
config: {
|
|
3804
|
+
...config,
|
|
3805
|
+
entry,
|
|
3806
|
+
stop
|
|
3807
|
+
},
|
|
3808
|
+
global_config,
|
|
3809
|
+
force_exact: true,
|
|
3810
|
+
settings: {
|
|
3811
|
+
entry,
|
|
3812
|
+
stop,
|
|
3813
|
+
risk: profit_risk,
|
|
3814
|
+
distribution: config.distribution
|
|
3815
|
+
}
|
|
3816
|
+
});
|
|
3817
|
+
return {
|
|
3818
|
+
opposite: result,
|
|
3819
|
+
take_profit: to_f(tp_price, global_config.price_places)
|
|
3820
|
+
};
|
|
3821
|
+
}
|
|
3822
|
+
var compoundAPI = {
|
|
3823
|
+
determineHedgeTradeToPlace,
|
|
3824
|
+
buildWithOptimumReward,
|
|
3825
|
+
constructAppConfig: constructAppConfig2,
|
|
3826
|
+
generateOppositeOptimum,
|
|
3827
|
+
increaseTradeHelper,
|
|
3828
|
+
generatePositionIncreaseTrade
|
|
3829
|
+
};
|
|
3830
|
+
export {
|
|
3831
|
+
to_f,
|
|
3832
|
+
sortedBuildConfig,
|
|
3833
|
+
range,
|
|
3834
|
+
profitHelper,
|
|
3835
|
+
logWithLineNumber,
|
|
3836
|
+
groupIntoPairsWithSumLessThan,
|
|
3837
|
+
groupIntoPairs,
|
|
3838
|
+
groupBy,
|
|
3839
|
+
get_app_config_and_max_size,
|
|
3840
|
+
getTradeEntries,
|
|
3841
|
+
getRiskReward,
|
|
3842
|
+
getParamForField,
|
|
3843
|
+
getOptimumStopAndRisk,
|
|
3844
|
+
getOptimumHedgeFactor,
|
|
3845
|
+
getHedgeZone,
|
|
3846
|
+
getDecimalPlaces,
|
|
3847
|
+
generate_config_params,
|
|
3848
|
+
generateOptimumAppConfig,
|
|
3849
|
+
generateOppositeTradeConfig,
|
|
3850
|
+
generateGapTp,
|
|
3851
|
+
generateDangerousConfig,
|
|
3852
|
+
formatPrice,
|
|
3853
|
+
fibonacci_analysis,
|
|
3854
|
+
extractValue,
|
|
3855
|
+
determine_stop_and_size,
|
|
3856
|
+
determine_remaining_entry,
|
|
3857
|
+
determine_position_size,
|
|
3858
|
+
determine_break_even_price,
|
|
3859
|
+
determine_average_entry_and_size,
|
|
3860
|
+
determine_amount_to_sell2 as determine_amount_to_sell,
|
|
3861
|
+
determine_amount_to_buy,
|
|
3862
|
+
determineTPSl,
|
|
3863
|
+
determineRewardFactor,
|
|
3864
|
+
determineOptimumRisk,
|
|
3865
|
+
determineOptimumReward,
|
|
3866
|
+
determineCompoundLongTrade,
|
|
3867
|
+
createGapPairs,
|
|
3868
|
+
createArray,
|
|
3869
|
+
constructAppConfig,
|
|
3870
|
+
computeTotalAverageForEachTrade,
|
|
3871
|
+
computeSellZones,
|
|
3872
|
+
computeRiskReward,
|
|
3873
|
+
computeProfitDetail,
|
|
3874
|
+
compoundAPI,
|
|
3875
|
+
calculateFactorFromTakeProfit,
|
|
3876
|
+
calculateFactorFromSellQuantity,
|
|
3877
|
+
buildConfig,
|
|
3878
|
+
buildAvg,
|
|
3879
|
+
buildAppConfig,
|
|
3880
|
+
asCoins,
|
|
3881
|
+
allCoins,
|
|
3882
|
+
Strategy,
|
|
3883
|
+
SpecialCoins
|
|
3884
|
+
};
|