backtest-kit 1.7.2 → 1.8.1
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/build/index.cjs +1537 -489
- package/build/index.mjs +1536 -490
- package/package.json +2 -1
- package/types.d.ts +1033 -388
package/build/index.cjs
CHANGED
|
@@ -772,6 +772,11 @@ class ExchangeConnectionService {
|
|
|
772
772
|
/**
|
|
773
773
|
* Calculates profit/loss for a closed signal with slippage and fees.
|
|
774
774
|
*
|
|
775
|
+
* For signals with partial closes:
|
|
776
|
+
* - Calculates weighted PNL: Σ(percent_i × pnl_i) for each partial + (remaining% × final_pnl)
|
|
777
|
+
* - Each partial close has its own fees and slippage
|
|
778
|
+
* - Total fees = 2 × (number of partial closes + 1 final close) × CC_PERCENT_FEE
|
|
779
|
+
*
|
|
775
780
|
* Formula breakdown:
|
|
776
781
|
* 1. Apply slippage to open/close prices (worse execution)
|
|
777
782
|
* - LONG: buy higher (+slippage), sell lower (-slippage)
|
|
@@ -779,27 +784,113 @@ class ExchangeConnectionService {
|
|
|
779
784
|
* 2. Calculate raw PNL percentage
|
|
780
785
|
* - LONG: ((closePrice - openPrice) / openPrice) * 100
|
|
781
786
|
* - SHORT: ((openPrice - closePrice) / openPrice) * 100
|
|
782
|
-
* 3. Subtract total fees (0.1% * 2 = 0.2%)
|
|
787
|
+
* 3. Subtract total fees (0.1% * 2 = 0.2% per transaction)
|
|
783
788
|
*
|
|
784
|
-
* @param signal - Closed signal with position details
|
|
785
|
-
* @param priceClose - Actual close price at exit
|
|
789
|
+
* @param signal - Closed signal with position details and optional partial history
|
|
790
|
+
* @param priceClose - Actual close price at final exit
|
|
786
791
|
* @returns PNL data with percentage and prices
|
|
787
792
|
*
|
|
788
793
|
* @example
|
|
789
794
|
* ```typescript
|
|
795
|
+
* // Signal without partial closes
|
|
790
796
|
* const pnl = toProfitLossDto(
|
|
791
797
|
* {
|
|
792
798
|
* position: "long",
|
|
793
|
-
* priceOpen:
|
|
794
|
-
*
|
|
799
|
+
* priceOpen: 100,
|
|
800
|
+
* },
|
|
801
|
+
* 110 // close at +10%
|
|
802
|
+
* );
|
|
803
|
+
* console.log(pnl.pnlPercentage); // ~9.6% (after slippage and fees)
|
|
804
|
+
*
|
|
805
|
+
* // Signal with partial closes
|
|
806
|
+
* const pnlPartial = toProfitLossDto(
|
|
807
|
+
* {
|
|
808
|
+
* position: "long",
|
|
809
|
+
* priceOpen: 100,
|
|
810
|
+
* _partial: [
|
|
811
|
+
* { type: "profit", percent: 30, price: 120 }, // +20% on 30%
|
|
812
|
+
* { type: "profit", percent: 40, price: 115 }, // +15% on 40%
|
|
813
|
+
* ],
|
|
795
814
|
* },
|
|
796
|
-
*
|
|
815
|
+
* 105 // final close at +5% for remaining 30%
|
|
797
816
|
* );
|
|
798
|
-
*
|
|
817
|
+
* // Weighted PNL = 30% × 20% + 40% × 15% + 30% × 5% = 6% + 6% + 1.5% = 13.5% (before fees)
|
|
799
818
|
* ```
|
|
800
819
|
*/
|
|
801
820
|
const toProfitLossDto = (signal, priceClose) => {
|
|
802
821
|
const priceOpen = signal.priceOpen;
|
|
822
|
+
// Calculate weighted PNL with partial closes
|
|
823
|
+
if (signal._partial && signal._partial.length > 0) {
|
|
824
|
+
let totalWeightedPnl = 0;
|
|
825
|
+
let totalFees = 0;
|
|
826
|
+
// Calculate PNL for each partial close
|
|
827
|
+
for (const partial of signal._partial) {
|
|
828
|
+
const partialPercent = partial.percent;
|
|
829
|
+
const partialPrice = partial.price;
|
|
830
|
+
// Apply slippage to prices
|
|
831
|
+
let priceOpenWithSlippage;
|
|
832
|
+
let priceCloseWithSlippage;
|
|
833
|
+
if (signal.position === "long") {
|
|
834
|
+
priceOpenWithSlippage = priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
835
|
+
priceCloseWithSlippage = partialPrice * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
839
|
+
priceCloseWithSlippage = partialPrice * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
840
|
+
}
|
|
841
|
+
// Calculate PNL for this partial
|
|
842
|
+
let partialPnl;
|
|
843
|
+
if (signal.position === "long") {
|
|
844
|
+
partialPnl = ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100;
|
|
845
|
+
}
|
|
846
|
+
else {
|
|
847
|
+
partialPnl = ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
|
|
848
|
+
}
|
|
849
|
+
// Weight by percentage of position closed
|
|
850
|
+
const weightedPnl = (partialPercent / 100) * partialPnl;
|
|
851
|
+
totalWeightedPnl += weightedPnl;
|
|
852
|
+
// Each partial has fees for open + close (2 transactions)
|
|
853
|
+
totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * 2;
|
|
854
|
+
}
|
|
855
|
+
// Calculate PNL for remaining position (if any)
|
|
856
|
+
// Compute totalClosed from _partial array
|
|
857
|
+
const totalClosed = signal._partial.reduce((sum, p) => sum + p.percent, 0);
|
|
858
|
+
const remainingPercent = 100 - totalClosed;
|
|
859
|
+
if (remainingPercent > 0) {
|
|
860
|
+
// Apply slippage
|
|
861
|
+
let priceOpenWithSlippage;
|
|
862
|
+
let priceCloseWithSlippage;
|
|
863
|
+
if (signal.position === "long") {
|
|
864
|
+
priceOpenWithSlippage = priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
865
|
+
priceCloseWithSlippage = priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
869
|
+
priceCloseWithSlippage = priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
870
|
+
}
|
|
871
|
+
// Calculate PNL for remaining
|
|
872
|
+
let remainingPnl;
|
|
873
|
+
if (signal.position === "long") {
|
|
874
|
+
remainingPnl = ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100;
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
remainingPnl = ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
|
|
878
|
+
}
|
|
879
|
+
// Weight by remaining percentage
|
|
880
|
+
const weightedRemainingPnl = (remainingPercent / 100) * remainingPnl;
|
|
881
|
+
totalWeightedPnl += weightedRemainingPnl;
|
|
882
|
+
// Final close also has fees
|
|
883
|
+
totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * 2;
|
|
884
|
+
}
|
|
885
|
+
// Subtract total fees from weighted PNL
|
|
886
|
+
const pnlPercentage = totalWeightedPnl - totalFees;
|
|
887
|
+
return {
|
|
888
|
+
pnlPercentage,
|
|
889
|
+
priceOpen,
|
|
890
|
+
priceClose,
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
// Original logic for signals without partial closes
|
|
803
894
|
let priceOpenWithSlippage;
|
|
804
895
|
let priceCloseWithSlippage;
|
|
805
896
|
if (signal.position === "long") {
|
|
@@ -2090,15 +2181,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
2090
2181
|
if (self._isStopped) {
|
|
2091
2182
|
return null;
|
|
2092
2183
|
}
|
|
2093
|
-
if (await functoolsKit.not(self.params.
|
|
2094
|
-
pendingSignal: signal,
|
|
2095
|
-
symbol: self.params.execution.context.symbol,
|
|
2096
|
-
strategyName: self.params.method.context.strategyName,
|
|
2097
|
-
exchangeName: self.params.method.context.exchangeName,
|
|
2098
|
-
frameName: self.params.method.context.frameName,
|
|
2099
|
-
currentPrice,
|
|
2100
|
-
timestamp: currentTime,
|
|
2101
|
-
}))) {
|
|
2184
|
+
if (await functoolsKit.not(CALL_RISK_CHECK_SIGNAL_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest))) {
|
|
2102
2185
|
return null;
|
|
2103
2186
|
}
|
|
2104
2187
|
// Если priceOpen указан - проверяем нужно ли ждать активации или открыть сразу
|
|
@@ -2207,10 +2290,9 @@ const WAIT_FOR_INIT_FN$2 = async (self) => {
|
|
|
2207
2290
|
}
|
|
2208
2291
|
self._pendingSignal = pendingSignal;
|
|
2209
2292
|
// Call onActive callback for restored signal
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
}
|
|
2293
|
+
const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
|
|
2294
|
+
const currentTime = self.params.execution.context.when.getTime();
|
|
2295
|
+
await CALL_ACTIVE_CALLBACKS_FN(self, self.params.execution.context.symbol, pendingSignal, currentPrice, currentTime, self.params.execution.context.backtest);
|
|
2214
2296
|
}
|
|
2215
2297
|
// Restore scheduled signal
|
|
2216
2298
|
const scheduledSignal = await PersistScheduleAdapter.readScheduleData(self.params.execution.context.symbol, self.params.strategyName);
|
|
@@ -2223,11 +2305,84 @@ const WAIT_FOR_INIT_FN$2 = async (self) => {
|
|
|
2223
2305
|
}
|
|
2224
2306
|
self._scheduledSignal = scheduledSignal;
|
|
2225
2307
|
// Call onSchedule callback for restored scheduled signal
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2308
|
+
const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
|
|
2309
|
+
const currentTime = self.params.execution.context.when.getTime();
|
|
2310
|
+
await CALL_SCHEDULE_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduledSignal, currentPrice, currentTime, self.params.execution.context.backtest);
|
|
2311
|
+
}
|
|
2312
|
+
};
|
|
2313
|
+
const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
|
|
2314
|
+
// Initialize partial array if not present
|
|
2315
|
+
if (!signal._partial)
|
|
2316
|
+
signal._partial = [];
|
|
2317
|
+
// Calculate current totals (computed values)
|
|
2318
|
+
const tpClosed = signal._partial
|
|
2319
|
+
.filter((p) => p.type === "profit")
|
|
2320
|
+
.reduce((sum, p) => sum + p.percent, 0);
|
|
2321
|
+
const slClosed = signal._partial
|
|
2322
|
+
.filter((p) => p.type === "loss")
|
|
2323
|
+
.reduce((sum, p) => sum + p.percent, 0);
|
|
2324
|
+
const totalClosed = tpClosed + slClosed;
|
|
2325
|
+
// Check if would exceed 100% total closed
|
|
2326
|
+
const newTotalClosed = totalClosed + percentToClose;
|
|
2327
|
+
if (newTotalClosed > 100) {
|
|
2328
|
+
self.params.logger.warn("PARTIAL_PROFIT_FN: would exceed 100% closed, skipping", {
|
|
2329
|
+
signalId: signal.id,
|
|
2330
|
+
currentTotalClosed: totalClosed,
|
|
2331
|
+
percentToClose,
|
|
2332
|
+
newTotalClosed,
|
|
2333
|
+
});
|
|
2334
|
+
return;
|
|
2230
2335
|
}
|
|
2336
|
+
// Add new partial close entry
|
|
2337
|
+
signal._partial.push({
|
|
2338
|
+
type: "profit",
|
|
2339
|
+
percent: percentToClose,
|
|
2340
|
+
price: currentPrice,
|
|
2341
|
+
});
|
|
2342
|
+
self.params.logger.info("PARTIAL_PROFIT_FN executed", {
|
|
2343
|
+
signalId: signal.id,
|
|
2344
|
+
percentClosed: percentToClose,
|
|
2345
|
+
totalClosed: newTotalClosed,
|
|
2346
|
+
currentPrice,
|
|
2347
|
+
tpClosed: tpClosed + percentToClose,
|
|
2348
|
+
});
|
|
2349
|
+
};
|
|
2350
|
+
const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
|
|
2351
|
+
// Initialize partial array if not present
|
|
2352
|
+
if (!signal._partial)
|
|
2353
|
+
signal._partial = [];
|
|
2354
|
+
// Calculate current totals (computed values)
|
|
2355
|
+
const tpClosed = signal._partial
|
|
2356
|
+
.filter((p) => p.type === "profit")
|
|
2357
|
+
.reduce((sum, p) => sum + p.percent, 0);
|
|
2358
|
+
const slClosed = signal._partial
|
|
2359
|
+
.filter((p) => p.type === "loss")
|
|
2360
|
+
.reduce((sum, p) => sum + p.percent, 0);
|
|
2361
|
+
const totalClosed = tpClosed + slClosed;
|
|
2362
|
+
// Check if would exceed 100% total closed
|
|
2363
|
+
const newTotalClosed = totalClosed + percentToClose;
|
|
2364
|
+
if (newTotalClosed > 100) {
|
|
2365
|
+
self.params.logger.warn("PARTIAL_LOSS_FN: would exceed 100% closed, skipping", {
|
|
2366
|
+
signalId: signal.id,
|
|
2367
|
+
currentTotalClosed: totalClosed,
|
|
2368
|
+
percentToClose,
|
|
2369
|
+
newTotalClosed,
|
|
2370
|
+
});
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
// Add new partial close entry
|
|
2374
|
+
signal._partial.push({
|
|
2375
|
+
type: "loss",
|
|
2376
|
+
percent: percentToClose,
|
|
2377
|
+
price: currentPrice,
|
|
2378
|
+
});
|
|
2379
|
+
self.params.logger.warn("PARTIAL_LOSS_FN executed", {
|
|
2380
|
+
signalId: signal.id,
|
|
2381
|
+
percentClosed: percentToClose,
|
|
2382
|
+
totalClosed: newTotalClosed,
|
|
2383
|
+
currentPrice,
|
|
2384
|
+
slClosed: slClosed + percentToClose,
|
|
2385
|
+
});
|
|
2231
2386
|
};
|
|
2232
2387
|
const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice) => {
|
|
2233
2388
|
const currentTime = self.params.execution.context.when.getTime();
|
|
@@ -2244,9 +2399,7 @@ const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice)
|
|
|
2244
2399
|
maxMinutes: GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES,
|
|
2245
2400
|
});
|
|
2246
2401
|
await self.setScheduledSignal(null);
|
|
2247
|
-
|
|
2248
|
-
self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, currentPrice, self.params.execution.context.backtest);
|
|
2249
|
-
}
|
|
2402
|
+
await CALL_CANCEL_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, currentPrice, currentTime, self.params.execution.context.backtest);
|
|
2250
2403
|
const result = {
|
|
2251
2404
|
action: "cancelled",
|
|
2252
2405
|
signal: scheduled,
|
|
@@ -2259,9 +2412,7 @@ const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice)
|
|
|
2259
2412
|
backtest: self.params.execution.context.backtest,
|
|
2260
2413
|
reason: "timeout",
|
|
2261
2414
|
};
|
|
2262
|
-
|
|
2263
|
-
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
2264
|
-
}
|
|
2415
|
+
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
|
|
2265
2416
|
return result;
|
|
2266
2417
|
};
|
|
2267
2418
|
const CHECK_SCHEDULED_SIGNAL_PRICE_ACTIVATION_FN = (scheduled, currentPrice) => {
|
|
@@ -2302,14 +2453,13 @@ const CANCEL_SCHEDULED_SIGNAL_BY_STOPLOSS_FN = async (self, scheduled, currentPr
|
|
|
2302
2453
|
priceStopLoss: scheduled.priceStopLoss,
|
|
2303
2454
|
});
|
|
2304
2455
|
await self.setScheduledSignal(null);
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
}
|
|
2456
|
+
const currentTime = self.params.execution.context.when.getTime();
|
|
2457
|
+
await CALL_CANCEL_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, currentPrice, currentTime, self.params.execution.context.backtest);
|
|
2308
2458
|
const result = {
|
|
2309
2459
|
action: "cancelled",
|
|
2310
2460
|
signal: scheduled,
|
|
2311
2461
|
currentPrice: currentPrice,
|
|
2312
|
-
closeTimestamp:
|
|
2462
|
+
closeTimestamp: currentTime,
|
|
2313
2463
|
strategyName: self.params.method.context.strategyName,
|
|
2314
2464
|
exchangeName: self.params.method.context.exchangeName,
|
|
2315
2465
|
frameName: self.params.method.context.frameName,
|
|
@@ -2317,9 +2467,7 @@ const CANCEL_SCHEDULED_SIGNAL_BY_STOPLOSS_FN = async (self, scheduled, currentPr
|
|
|
2317
2467
|
backtest: self.params.execution.context.backtest,
|
|
2318
2468
|
reason: "price_reject",
|
|
2319
2469
|
};
|
|
2320
|
-
|
|
2321
|
-
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
2322
|
-
}
|
|
2470
|
+
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
|
|
2323
2471
|
return result;
|
|
2324
2472
|
};
|
|
2325
2473
|
const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp) => {
|
|
@@ -2346,15 +2494,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
|
|
|
2346
2494
|
scheduledAt: scheduled.scheduledAt,
|
|
2347
2495
|
pendingAt: activationTime,
|
|
2348
2496
|
});
|
|
2349
|
-
if (await functoolsKit.not(self.params.
|
|
2350
|
-
symbol: self.params.execution.context.symbol,
|
|
2351
|
-
pendingSignal: scheduled,
|
|
2352
|
-
strategyName: self.params.method.context.strategyName,
|
|
2353
|
-
exchangeName: self.params.method.context.exchangeName,
|
|
2354
|
-
frameName: self.params.method.context.frameName,
|
|
2355
|
-
currentPrice: scheduled.priceOpen,
|
|
2356
|
-
timestamp: activationTime,
|
|
2357
|
-
}))) {
|
|
2497
|
+
if (await functoolsKit.not(CALL_RISK_CHECK_SIGNAL_FN(self, self.params.execution.context.symbol, scheduled, scheduled.priceOpen, activationTime, self.params.execution.context.backtest))) {
|
|
2358
2498
|
self.params.logger.info("ClientStrategy scheduled signal rejected by risk", {
|
|
2359
2499
|
symbol: self.params.execution.context.symbol,
|
|
2360
2500
|
signalId: scheduled.id,
|
|
@@ -2370,15 +2510,8 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
|
|
|
2370
2510
|
_isScheduled: false,
|
|
2371
2511
|
};
|
|
2372
2512
|
await self.setPendingSignal(activatedSignal);
|
|
2373
|
-
await self.params.
|
|
2374
|
-
|
|
2375
|
-
riskName: self.params.riskName,
|
|
2376
|
-
exchangeName: self.params.method.context.exchangeName,
|
|
2377
|
-
frameName: self.params.method.context.frameName,
|
|
2378
|
-
});
|
|
2379
|
-
if (self.params.callbacks?.onOpen) {
|
|
2380
|
-
self.params.callbacks.onOpen(self.params.execution.context.symbol, self._pendingSignal, self._pendingSignal.priceOpen, self.params.execution.context.backtest);
|
|
2381
|
-
}
|
|
2513
|
+
await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, activationTime, self.params.execution.context.backtest);
|
|
2514
|
+
await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, self._pendingSignal, self._pendingSignal.priceOpen, activationTime, self.params.execution.context.backtest);
|
|
2382
2515
|
const result = {
|
|
2383
2516
|
action: "opened",
|
|
2384
2517
|
signal: self._pendingSignal,
|
|
@@ -2389,18 +2522,22 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
|
|
|
2389
2522
|
currentPrice: self._pendingSignal.priceOpen,
|
|
2390
2523
|
backtest: self.params.execution.context.backtest,
|
|
2391
2524
|
};
|
|
2392
|
-
|
|
2393
|
-
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
2394
|
-
}
|
|
2525
|
+
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, activationTime, self.params.execution.context.backtest);
|
|
2395
2526
|
return result;
|
|
2396
2527
|
};
|
|
2397
|
-
const CALL_PING_CALLBACKS_FN = functoolsKit.trycatch(async (self, scheduled, timestamp) => {
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2528
|
+
const CALL_PING_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, scheduled, timestamp, backtest) => {
|
|
2529
|
+
await ExecutionContextService.runInContext(async () => {
|
|
2530
|
+
// Call system onPing callback first (emits to pingSubject)
|
|
2531
|
+
await self.params.onPing(self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, scheduled, self.params.execution.context.backtest, timestamp);
|
|
2532
|
+
// Call user onPing callback only if signal is still active (not cancelled, not activated)
|
|
2533
|
+
if (self.params.callbacks?.onPing) {
|
|
2534
|
+
await self.params.callbacks.onPing(self.params.execution.context.symbol, scheduled, new Date(timestamp), self.params.execution.context.backtest);
|
|
2535
|
+
}
|
|
2536
|
+
}, {
|
|
2537
|
+
when: new Date(timestamp),
|
|
2538
|
+
symbol: symbol,
|
|
2539
|
+
backtest: backtest,
|
|
2540
|
+
});
|
|
2404
2541
|
}, {
|
|
2405
2542
|
fallback: (error) => {
|
|
2406
2543
|
const message = "ClientStrategy CALL_PING_CALLBACKS_FN thrown";
|
|
@@ -2413,8 +2550,308 @@ const CALL_PING_CALLBACKS_FN = functoolsKit.trycatch(async (self, scheduled, tim
|
|
|
2413
2550
|
errorEmitter.next(error);
|
|
2414
2551
|
},
|
|
2415
2552
|
});
|
|
2553
|
+
const CALL_ACTIVE_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
|
|
2554
|
+
await ExecutionContextService.runInContext(async () => {
|
|
2555
|
+
if (self.params.callbacks?.onActive) {
|
|
2556
|
+
self.params.callbacks.onActive(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
|
|
2557
|
+
}
|
|
2558
|
+
}, {
|
|
2559
|
+
when: new Date(timestamp),
|
|
2560
|
+
symbol: symbol,
|
|
2561
|
+
backtest: backtest,
|
|
2562
|
+
});
|
|
2563
|
+
}, {
|
|
2564
|
+
fallback: (error) => {
|
|
2565
|
+
const message = "ClientStrategy CALL_ACTIVE_CALLBACKS_FN thrown";
|
|
2566
|
+
const payload = {
|
|
2567
|
+
error: functoolsKit.errorData(error),
|
|
2568
|
+
message: functoolsKit.getErrorMessage(error),
|
|
2569
|
+
};
|
|
2570
|
+
backtest$1.loggerService.warn(message, payload);
|
|
2571
|
+
console.warn(message, payload);
|
|
2572
|
+
errorEmitter.next(error);
|
|
2573
|
+
},
|
|
2574
|
+
});
|
|
2575
|
+
const CALL_SCHEDULE_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
|
|
2576
|
+
await ExecutionContextService.runInContext(async () => {
|
|
2577
|
+
if (self.params.callbacks?.onSchedule) {
|
|
2578
|
+
self.params.callbacks.onSchedule(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
|
|
2579
|
+
}
|
|
2580
|
+
}, {
|
|
2581
|
+
when: new Date(timestamp),
|
|
2582
|
+
symbol: symbol,
|
|
2583
|
+
backtest: backtest,
|
|
2584
|
+
});
|
|
2585
|
+
}, {
|
|
2586
|
+
fallback: (error) => {
|
|
2587
|
+
const message = "ClientStrategy CALL_SCHEDULE_CALLBACKS_FN thrown";
|
|
2588
|
+
const payload = {
|
|
2589
|
+
error: functoolsKit.errorData(error),
|
|
2590
|
+
message: functoolsKit.getErrorMessage(error),
|
|
2591
|
+
};
|
|
2592
|
+
backtest$1.loggerService.warn(message, payload);
|
|
2593
|
+
console.warn(message, payload);
|
|
2594
|
+
errorEmitter.next(error);
|
|
2595
|
+
},
|
|
2596
|
+
});
|
|
2597
|
+
const CALL_CANCEL_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
|
|
2598
|
+
await ExecutionContextService.runInContext(async () => {
|
|
2599
|
+
if (self.params.callbacks?.onCancel) {
|
|
2600
|
+
self.params.callbacks.onCancel(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
|
|
2601
|
+
}
|
|
2602
|
+
}, {
|
|
2603
|
+
when: new Date(timestamp),
|
|
2604
|
+
symbol: symbol,
|
|
2605
|
+
backtest: backtest,
|
|
2606
|
+
});
|
|
2607
|
+
}, {
|
|
2608
|
+
fallback: (error) => {
|
|
2609
|
+
const message = "ClientStrategy CALL_CANCEL_CALLBACKS_FN thrown";
|
|
2610
|
+
const payload = {
|
|
2611
|
+
error: functoolsKit.errorData(error),
|
|
2612
|
+
message: functoolsKit.getErrorMessage(error),
|
|
2613
|
+
};
|
|
2614
|
+
backtest$1.loggerService.warn(message, payload);
|
|
2615
|
+
console.warn(message, payload);
|
|
2616
|
+
errorEmitter.next(error);
|
|
2617
|
+
},
|
|
2618
|
+
});
|
|
2619
|
+
const CALL_OPEN_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, priceOpen, timestamp, backtest) => {
|
|
2620
|
+
await ExecutionContextService.runInContext(async () => {
|
|
2621
|
+
if (self.params.callbacks?.onOpen) {
|
|
2622
|
+
self.params.callbacks.onOpen(self.params.execution.context.symbol, signal, priceOpen, self.params.execution.context.backtest);
|
|
2623
|
+
}
|
|
2624
|
+
}, {
|
|
2625
|
+
when: new Date(timestamp),
|
|
2626
|
+
symbol: symbol,
|
|
2627
|
+
backtest: backtest,
|
|
2628
|
+
});
|
|
2629
|
+
}, {
|
|
2630
|
+
fallback: (error) => {
|
|
2631
|
+
const message = "ClientStrategy CALL_OPEN_CALLBACKS_FN thrown";
|
|
2632
|
+
const payload = {
|
|
2633
|
+
error: functoolsKit.errorData(error),
|
|
2634
|
+
message: functoolsKit.getErrorMessage(error),
|
|
2635
|
+
};
|
|
2636
|
+
backtest$1.loggerService.warn(message, payload);
|
|
2637
|
+
console.warn(message, payload);
|
|
2638
|
+
errorEmitter.next(error);
|
|
2639
|
+
},
|
|
2640
|
+
});
|
|
2641
|
+
const CALL_CLOSE_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
|
|
2642
|
+
await ExecutionContextService.runInContext(async () => {
|
|
2643
|
+
if (self.params.callbacks?.onClose) {
|
|
2644
|
+
self.params.callbacks.onClose(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
|
|
2645
|
+
}
|
|
2646
|
+
}, {
|
|
2647
|
+
when: new Date(timestamp),
|
|
2648
|
+
symbol: symbol,
|
|
2649
|
+
backtest: backtest,
|
|
2650
|
+
});
|
|
2651
|
+
}, {
|
|
2652
|
+
fallback: (error) => {
|
|
2653
|
+
const message = "ClientStrategy CALL_CLOSE_CALLBACKS_FN thrown";
|
|
2654
|
+
const payload = {
|
|
2655
|
+
error: functoolsKit.errorData(error),
|
|
2656
|
+
message: functoolsKit.getErrorMessage(error),
|
|
2657
|
+
};
|
|
2658
|
+
backtest$1.loggerService.warn(message, payload);
|
|
2659
|
+
console.warn(message, payload);
|
|
2660
|
+
errorEmitter.next(error);
|
|
2661
|
+
},
|
|
2662
|
+
});
|
|
2663
|
+
const CALL_TICK_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, result, timestamp, backtest) => {
|
|
2664
|
+
await ExecutionContextService.runInContext(async () => {
|
|
2665
|
+
if (self.params.callbacks?.onTick) {
|
|
2666
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
2667
|
+
}
|
|
2668
|
+
}, {
|
|
2669
|
+
when: new Date(timestamp),
|
|
2670
|
+
symbol: symbol,
|
|
2671
|
+
backtest: backtest,
|
|
2672
|
+
});
|
|
2673
|
+
}, {
|
|
2674
|
+
fallback: (error) => {
|
|
2675
|
+
const message = "ClientStrategy CALL_TICK_CALLBACKS_FN thrown";
|
|
2676
|
+
const payload = {
|
|
2677
|
+
error: functoolsKit.errorData(error),
|
|
2678
|
+
message: functoolsKit.getErrorMessage(error),
|
|
2679
|
+
};
|
|
2680
|
+
backtest$1.loggerService.warn(message, payload);
|
|
2681
|
+
console.warn(message, payload);
|
|
2682
|
+
errorEmitter.next(error);
|
|
2683
|
+
},
|
|
2684
|
+
});
|
|
2685
|
+
const CALL_IDLE_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, currentPrice, timestamp, backtest) => {
|
|
2686
|
+
await ExecutionContextService.runInContext(async () => {
|
|
2687
|
+
if (self.params.callbacks?.onIdle) {
|
|
2688
|
+
self.params.callbacks.onIdle(self.params.execution.context.symbol, currentPrice, self.params.execution.context.backtest);
|
|
2689
|
+
}
|
|
2690
|
+
}, {
|
|
2691
|
+
when: new Date(timestamp),
|
|
2692
|
+
symbol: symbol,
|
|
2693
|
+
backtest: backtest,
|
|
2694
|
+
});
|
|
2695
|
+
}, {
|
|
2696
|
+
fallback: (error) => {
|
|
2697
|
+
const message = "ClientStrategy CALL_IDLE_CALLBACKS_FN thrown";
|
|
2698
|
+
const payload = {
|
|
2699
|
+
error: functoolsKit.errorData(error),
|
|
2700
|
+
message: functoolsKit.getErrorMessage(error),
|
|
2701
|
+
};
|
|
2702
|
+
backtest$1.loggerService.warn(message, payload);
|
|
2703
|
+
console.warn(message, payload);
|
|
2704
|
+
errorEmitter.next(error);
|
|
2705
|
+
},
|
|
2706
|
+
});
|
|
2707
|
+
const CALL_RISK_ADD_SIGNAL_FN = functoolsKit.trycatch(async (self, symbol, timestamp, backtest) => {
|
|
2708
|
+
await ExecutionContextService.runInContext(async () => {
|
|
2709
|
+
await self.params.risk.addSignal(symbol, {
|
|
2710
|
+
strategyName: self.params.method.context.strategyName,
|
|
2711
|
+
riskName: self.params.riskName,
|
|
2712
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
2713
|
+
frameName: self.params.method.context.frameName,
|
|
2714
|
+
});
|
|
2715
|
+
}, {
|
|
2716
|
+
when: new Date(timestamp),
|
|
2717
|
+
symbol: symbol,
|
|
2718
|
+
backtest: backtest,
|
|
2719
|
+
});
|
|
2720
|
+
}, {
|
|
2721
|
+
fallback: (error) => {
|
|
2722
|
+
const message = "ClientStrategy CALL_RISK_ADD_SIGNAL_FN thrown";
|
|
2723
|
+
const payload = {
|
|
2724
|
+
error: functoolsKit.errorData(error),
|
|
2725
|
+
message: functoolsKit.getErrorMessage(error),
|
|
2726
|
+
};
|
|
2727
|
+
backtest$1.loggerService.warn(message, payload);
|
|
2728
|
+
console.warn(message, payload);
|
|
2729
|
+
errorEmitter.next(error);
|
|
2730
|
+
},
|
|
2731
|
+
});
|
|
2732
|
+
const CALL_RISK_REMOVE_SIGNAL_FN = functoolsKit.trycatch(async (self, symbol, timestamp, backtest) => {
|
|
2733
|
+
await ExecutionContextService.runInContext(async () => {
|
|
2734
|
+
await self.params.risk.removeSignal(symbol, {
|
|
2735
|
+
strategyName: self.params.method.context.strategyName,
|
|
2736
|
+
riskName: self.params.riskName,
|
|
2737
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
2738
|
+
frameName: self.params.method.context.frameName,
|
|
2739
|
+
});
|
|
2740
|
+
}, {
|
|
2741
|
+
when: new Date(timestamp),
|
|
2742
|
+
symbol: symbol,
|
|
2743
|
+
backtest: backtest,
|
|
2744
|
+
});
|
|
2745
|
+
}, {
|
|
2746
|
+
fallback: (error) => {
|
|
2747
|
+
const message = "ClientStrategy CALL_RISK_REMOVE_SIGNAL_FN thrown";
|
|
2748
|
+
const payload = {
|
|
2749
|
+
error: functoolsKit.errorData(error),
|
|
2750
|
+
message: functoolsKit.getErrorMessage(error),
|
|
2751
|
+
};
|
|
2752
|
+
backtest$1.loggerService.warn(message, payload);
|
|
2753
|
+
console.warn(message, payload);
|
|
2754
|
+
errorEmitter.next(error);
|
|
2755
|
+
},
|
|
2756
|
+
});
|
|
2757
|
+
const CALL_PARTIAL_CLEAR_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
|
|
2758
|
+
await ExecutionContextService.runInContext(async () => {
|
|
2759
|
+
await self.params.partial.clear(symbol, signal, currentPrice, backtest);
|
|
2760
|
+
}, {
|
|
2761
|
+
when: new Date(timestamp),
|
|
2762
|
+
symbol: symbol,
|
|
2763
|
+
backtest: backtest,
|
|
2764
|
+
});
|
|
2765
|
+
}, {
|
|
2766
|
+
fallback: (error) => {
|
|
2767
|
+
const message = "ClientStrategy CALL_PARTIAL_CLEAR_FN thrown";
|
|
2768
|
+
const payload = {
|
|
2769
|
+
error: functoolsKit.errorData(error),
|
|
2770
|
+
message: functoolsKit.getErrorMessage(error),
|
|
2771
|
+
};
|
|
2772
|
+
backtest$1.loggerService.warn(message, payload);
|
|
2773
|
+
console.warn(message, payload);
|
|
2774
|
+
errorEmitter.next(error);
|
|
2775
|
+
},
|
|
2776
|
+
});
|
|
2777
|
+
const CALL_RISK_CHECK_SIGNAL_FN = functoolsKit.trycatch(async (self, symbol, pendingSignal, currentPrice, timestamp, backtest) => {
|
|
2778
|
+
return await ExecutionContextService.runInContext(async () => {
|
|
2779
|
+
return await self.params.risk.checkSignal({
|
|
2780
|
+
pendingSignal,
|
|
2781
|
+
symbol: symbol,
|
|
2782
|
+
strategyName: self.params.method.context.strategyName,
|
|
2783
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
2784
|
+
frameName: self.params.method.context.frameName,
|
|
2785
|
+
currentPrice,
|
|
2786
|
+
timestamp,
|
|
2787
|
+
});
|
|
2788
|
+
}, {
|
|
2789
|
+
when: new Date(timestamp),
|
|
2790
|
+
symbol: symbol,
|
|
2791
|
+
backtest: backtest,
|
|
2792
|
+
});
|
|
2793
|
+
}, {
|
|
2794
|
+
defaultValue: false,
|
|
2795
|
+
fallback: (error) => {
|
|
2796
|
+
const message = "ClientStrategy CALL_RISK_CHECK_SIGNAL_FN thrown";
|
|
2797
|
+
const payload = {
|
|
2798
|
+
error: functoolsKit.errorData(error),
|
|
2799
|
+
message: functoolsKit.getErrorMessage(error),
|
|
2800
|
+
};
|
|
2801
|
+
backtest$1.loggerService.warn(message, payload);
|
|
2802
|
+
console.warn(message, payload);
|
|
2803
|
+
errorEmitter.next(error);
|
|
2804
|
+
},
|
|
2805
|
+
});
|
|
2806
|
+
const CALL_PARTIAL_PROFIT_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, percentTp, timestamp, backtest) => {
|
|
2807
|
+
await ExecutionContextService.runInContext(async () => {
|
|
2808
|
+
await self.params.partial.profit(symbol, signal, currentPrice, percentTp, backtest, new Date(timestamp));
|
|
2809
|
+
if (self.params.callbacks?.onPartialProfit) {
|
|
2810
|
+
self.params.callbacks.onPartialProfit(symbol, signal, currentPrice, percentTp, backtest);
|
|
2811
|
+
}
|
|
2812
|
+
}, {
|
|
2813
|
+
when: new Date(timestamp),
|
|
2814
|
+
symbol: symbol,
|
|
2815
|
+
backtest: backtest,
|
|
2816
|
+
});
|
|
2817
|
+
}, {
|
|
2818
|
+
fallback: (error) => {
|
|
2819
|
+
const message = "ClientStrategy CALL_PARTIAL_PROFIT_CALLBACKS_FN thrown";
|
|
2820
|
+
const payload = {
|
|
2821
|
+
error: functoolsKit.errorData(error),
|
|
2822
|
+
message: functoolsKit.getErrorMessage(error),
|
|
2823
|
+
};
|
|
2824
|
+
backtest$1.loggerService.warn(message, payload);
|
|
2825
|
+
console.warn(message, payload);
|
|
2826
|
+
errorEmitter.next(error);
|
|
2827
|
+
},
|
|
2828
|
+
});
|
|
2829
|
+
const CALL_PARTIAL_LOSS_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, percentSl, timestamp, backtest) => {
|
|
2830
|
+
await ExecutionContextService.runInContext(async () => {
|
|
2831
|
+
await self.params.partial.loss(symbol, signal, currentPrice, percentSl, backtest, new Date(timestamp));
|
|
2832
|
+
if (self.params.callbacks?.onPartialLoss) {
|
|
2833
|
+
self.params.callbacks.onPartialLoss(symbol, signal, currentPrice, percentSl, backtest);
|
|
2834
|
+
}
|
|
2835
|
+
}, {
|
|
2836
|
+
when: new Date(timestamp),
|
|
2837
|
+
symbol: symbol,
|
|
2838
|
+
backtest: backtest,
|
|
2839
|
+
});
|
|
2840
|
+
}, {
|
|
2841
|
+
fallback: (error) => {
|
|
2842
|
+
const message = "ClientStrategy CALL_PARTIAL_LOSS_CALLBACKS_FN thrown";
|
|
2843
|
+
const payload = {
|
|
2844
|
+
error: functoolsKit.errorData(error),
|
|
2845
|
+
message: functoolsKit.getErrorMessage(error),
|
|
2846
|
+
};
|
|
2847
|
+
backtest$1.loggerService.warn(message, payload);
|
|
2848
|
+
console.warn(message, payload);
|
|
2849
|
+
errorEmitter.next(error);
|
|
2850
|
+
},
|
|
2851
|
+
});
|
|
2416
2852
|
const RETURN_SCHEDULED_SIGNAL_ACTIVE_FN = async (self, scheduled, currentPrice) => {
|
|
2417
|
-
|
|
2853
|
+
const currentTime = self.params.execution.context.when.getTime();
|
|
2854
|
+
await CALL_PING_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, currentTime, self.params.execution.context.backtest);
|
|
2418
2855
|
const result = {
|
|
2419
2856
|
action: "active",
|
|
2420
2857
|
signal: scheduled,
|
|
@@ -2427,13 +2864,12 @@ const RETURN_SCHEDULED_SIGNAL_ACTIVE_FN = async (self, scheduled, currentPrice)
|
|
|
2427
2864
|
percentSl: 0,
|
|
2428
2865
|
backtest: self.params.execution.context.backtest,
|
|
2429
2866
|
};
|
|
2430
|
-
|
|
2431
|
-
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
2432
|
-
}
|
|
2867
|
+
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
|
|
2433
2868
|
return result;
|
|
2434
2869
|
};
|
|
2435
2870
|
const OPEN_NEW_SCHEDULED_SIGNAL_FN = async (self, signal) => {
|
|
2436
2871
|
const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
|
|
2872
|
+
const currentTime = self.params.execution.context.when.getTime();
|
|
2437
2873
|
self.params.logger.info("ClientStrategy scheduled signal created", {
|
|
2438
2874
|
symbol: self.params.execution.context.symbol,
|
|
2439
2875
|
signalId: signal.id,
|
|
@@ -2441,9 +2877,7 @@ const OPEN_NEW_SCHEDULED_SIGNAL_FN = async (self, signal) => {
|
|
|
2441
2877
|
priceOpen: signal.priceOpen,
|
|
2442
2878
|
currentPrice: currentPrice,
|
|
2443
2879
|
});
|
|
2444
|
-
|
|
2445
|
-
self.params.callbacks.onSchedule(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
|
|
2446
|
-
}
|
|
2880
|
+
await CALL_SCHEDULE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
|
|
2447
2881
|
const result = {
|
|
2448
2882
|
action: "scheduled",
|
|
2449
2883
|
signal: signal,
|
|
@@ -2454,32 +2888,16 @@ const OPEN_NEW_SCHEDULED_SIGNAL_FN = async (self, signal) => {
|
|
|
2454
2888
|
currentPrice: currentPrice,
|
|
2455
2889
|
backtest: self.params.execution.context.backtest,
|
|
2456
2890
|
};
|
|
2457
|
-
|
|
2458
|
-
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
2459
|
-
}
|
|
2891
|
+
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
|
|
2460
2892
|
return result;
|
|
2461
2893
|
};
|
|
2462
2894
|
const OPEN_NEW_PENDING_SIGNAL_FN = async (self, signal) => {
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
symbol: self.params.execution.context.symbol,
|
|
2466
|
-
strategyName: self.params.method.context.strategyName,
|
|
2467
|
-
exchangeName: self.params.method.context.exchangeName,
|
|
2468
|
-
frameName: self.params.method.context.frameName,
|
|
2469
|
-
currentPrice: signal.priceOpen,
|
|
2470
|
-
timestamp: self.params.execution.context.when.getTime(),
|
|
2471
|
-
}))) {
|
|
2895
|
+
const currentTime = self.params.execution.context.when.getTime();
|
|
2896
|
+
if (await functoolsKit.not(CALL_RISK_CHECK_SIGNAL_FN(self, self.params.execution.context.symbol, signal, signal.priceOpen, currentTime, self.params.execution.context.backtest))) {
|
|
2472
2897
|
return null;
|
|
2473
2898
|
}
|
|
2474
|
-
await self.params.
|
|
2475
|
-
|
|
2476
|
-
riskName: self.params.riskName,
|
|
2477
|
-
exchangeName: self.params.method.context.exchangeName,
|
|
2478
|
-
frameName: self.params.method.context.frameName,
|
|
2479
|
-
});
|
|
2480
|
-
if (self.params.callbacks?.onOpen) {
|
|
2481
|
-
self.params.callbacks.onOpen(self.params.execution.context.symbol, signal, signal.priceOpen, self.params.execution.context.backtest);
|
|
2482
|
-
}
|
|
2899
|
+
await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, currentTime, self.params.execution.context.backtest);
|
|
2900
|
+
await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, signal.priceOpen, currentTime, self.params.execution.context.backtest);
|
|
2483
2901
|
const result = {
|
|
2484
2902
|
action: "opened",
|
|
2485
2903
|
signal: signal,
|
|
@@ -2490,9 +2908,7 @@ const OPEN_NEW_PENDING_SIGNAL_FN = async (self, signal) => {
|
|
|
2490
2908
|
currentPrice: signal.priceOpen,
|
|
2491
2909
|
backtest: self.params.execution.context.backtest,
|
|
2492
2910
|
};
|
|
2493
|
-
|
|
2494
|
-
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
2495
|
-
}
|
|
2911
|
+
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
|
|
2496
2912
|
return result;
|
|
2497
2913
|
};
|
|
2498
2914
|
const CHECK_PENDING_SIGNAL_COMPLETION_FN = async (self, signal, averagePrice) => {
|
|
@@ -2526,6 +2942,7 @@ const CHECK_PENDING_SIGNAL_COMPLETION_FN = async (self, signal, averagePrice) =>
|
|
|
2526
2942
|
};
|
|
2527
2943
|
const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason) => {
|
|
2528
2944
|
const pnl = toProfitLossDto(signal, currentPrice);
|
|
2945
|
+
const currentTime = self.params.execution.context.when.getTime();
|
|
2529
2946
|
self.params.logger.info(`ClientStrategy signal ${closeReason}`, {
|
|
2530
2947
|
symbol: self.params.execution.context.symbol,
|
|
2531
2948
|
signalId: signal.id,
|
|
@@ -2533,24 +2950,17 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
|
|
|
2533
2950
|
priceClose: currentPrice,
|
|
2534
2951
|
pnlPercentage: pnl.pnlPercentage,
|
|
2535
2952
|
});
|
|
2536
|
-
|
|
2537
|
-
self.params.callbacks.onClose(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
|
|
2538
|
-
}
|
|
2953
|
+
await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
|
|
2539
2954
|
// КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
|
|
2540
|
-
await
|
|
2541
|
-
await self.params.
|
|
2542
|
-
strategyName: self.params.method.context.strategyName,
|
|
2543
|
-
riskName: self.params.riskName,
|
|
2544
|
-
exchangeName: self.params.method.context.exchangeName,
|
|
2545
|
-
frameName: self.params.method.context.frameName,
|
|
2546
|
-
});
|
|
2955
|
+
await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
|
|
2956
|
+
await CALL_RISK_REMOVE_SIGNAL_FN(self, self.params.execution.context.symbol, currentTime, self.params.execution.context.backtest);
|
|
2547
2957
|
await self.setPendingSignal(null);
|
|
2548
2958
|
const result = {
|
|
2549
2959
|
action: "closed",
|
|
2550
2960
|
signal: signal,
|
|
2551
2961
|
currentPrice: currentPrice,
|
|
2552
2962
|
closeReason: closeReason,
|
|
2553
|
-
closeTimestamp:
|
|
2963
|
+
closeTimestamp: currentTime,
|
|
2554
2964
|
pnl: pnl,
|
|
2555
2965
|
strategyName: self.params.method.context.strategyName,
|
|
2556
2966
|
exchangeName: self.params.method.context.exchangeName,
|
|
@@ -2558,14 +2968,13 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
|
|
|
2558
2968
|
symbol: self.params.execution.context.symbol,
|
|
2559
2969
|
backtest: self.params.execution.context.backtest,
|
|
2560
2970
|
};
|
|
2561
|
-
|
|
2562
|
-
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
2563
|
-
}
|
|
2971
|
+
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
|
|
2564
2972
|
return result;
|
|
2565
2973
|
};
|
|
2566
2974
|
const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
2567
2975
|
let percentTp = 0;
|
|
2568
2976
|
let percentSl = 0;
|
|
2977
|
+
const currentTime = self.params.execution.context.when.getTime();
|
|
2569
2978
|
// Calculate percentage of path to TP/SL for partial fill/loss callbacks
|
|
2570
2979
|
{
|
|
2571
2980
|
if (signal.position === "long") {
|
|
@@ -2576,20 +2985,14 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
|
2576
2985
|
const tpDistance = signal.priceTakeProfit - signal.priceOpen;
|
|
2577
2986
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
2578
2987
|
percentTp = Math.min(progressPercent, 100);
|
|
2579
|
-
await
|
|
2580
|
-
if (self.params.callbacks?.onPartialProfit) {
|
|
2581
|
-
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest);
|
|
2582
|
-
}
|
|
2988
|
+
await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentTp, currentTime, self.params.execution.context.backtest);
|
|
2583
2989
|
}
|
|
2584
2990
|
else if (currentDistance < 0) {
|
|
2585
2991
|
// Moving towards SL
|
|
2586
2992
|
const slDistance = signal.priceOpen - signal.priceStopLoss;
|
|
2587
2993
|
const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
|
|
2588
2994
|
percentSl = Math.min(progressPercent, 100);
|
|
2589
|
-
await
|
|
2590
|
-
if (self.params.callbacks?.onPartialLoss) {
|
|
2591
|
-
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest);
|
|
2592
|
-
}
|
|
2995
|
+
await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentSl, currentTime, self.params.execution.context.backtest);
|
|
2593
2996
|
}
|
|
2594
2997
|
}
|
|
2595
2998
|
else if (signal.position === "short") {
|
|
@@ -2600,20 +3003,14 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
|
2600
3003
|
const tpDistance = signal.priceOpen - signal.priceTakeProfit;
|
|
2601
3004
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
2602
3005
|
percentTp = Math.min(progressPercent, 100);
|
|
2603
|
-
await
|
|
2604
|
-
if (self.params.callbacks?.onPartialProfit) {
|
|
2605
|
-
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest);
|
|
2606
|
-
}
|
|
3006
|
+
await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentTp, currentTime, self.params.execution.context.backtest);
|
|
2607
3007
|
}
|
|
2608
3008
|
if (currentDistance < 0) {
|
|
2609
3009
|
// Moving towards SL
|
|
2610
3010
|
const slDistance = signal.priceStopLoss - signal.priceOpen;
|
|
2611
3011
|
const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
|
|
2612
3012
|
percentSl = Math.min(progressPercent, 100);
|
|
2613
|
-
await
|
|
2614
|
-
if (self.params.callbacks?.onPartialLoss) {
|
|
2615
|
-
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest);
|
|
2616
|
-
}
|
|
3013
|
+
await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentSl, currentTime, self.params.execution.context.backtest);
|
|
2617
3014
|
}
|
|
2618
3015
|
}
|
|
2619
3016
|
}
|
|
@@ -2629,15 +3026,12 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
|
2629
3026
|
percentSl,
|
|
2630
3027
|
backtest: self.params.execution.context.backtest,
|
|
2631
3028
|
};
|
|
2632
|
-
|
|
2633
|
-
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
2634
|
-
}
|
|
3029
|
+
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
|
|
2635
3030
|
return result;
|
|
2636
3031
|
};
|
|
2637
3032
|
const RETURN_IDLE_FN = async (self, currentPrice) => {
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
}
|
|
3033
|
+
const currentTime = self.params.execution.context.when.getTime();
|
|
3034
|
+
await CALL_IDLE_CALLBACKS_FN(self, self.params.execution.context.symbol, currentPrice, currentTime, self.params.execution.context.backtest);
|
|
2641
3035
|
const result = {
|
|
2642
3036
|
action: "idle",
|
|
2643
3037
|
signal: null,
|
|
@@ -2648,9 +3042,7 @@ const RETURN_IDLE_FN = async (self, currentPrice) => {
|
|
|
2648
3042
|
currentPrice: currentPrice,
|
|
2649
3043
|
backtest: self.params.execution.context.backtest,
|
|
2650
3044
|
};
|
|
2651
|
-
|
|
2652
|
-
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
2653
|
-
}
|
|
3045
|
+
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
|
|
2654
3046
|
return result;
|
|
2655
3047
|
};
|
|
2656
3048
|
const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePrice, closeTimestamp, reason) => {
|
|
@@ -2663,9 +3055,7 @@ const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePr
|
|
|
2663
3055
|
reason,
|
|
2664
3056
|
});
|
|
2665
3057
|
await self.setScheduledSignal(null);
|
|
2666
|
-
|
|
2667
|
-
self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, averagePrice, self.params.execution.context.backtest);
|
|
2668
|
-
}
|
|
3058
|
+
await CALL_CANCEL_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, averagePrice, closeTimestamp, self.params.execution.context.backtest);
|
|
2669
3059
|
const result = {
|
|
2670
3060
|
action: "cancelled",
|
|
2671
3061
|
signal: scheduled,
|
|
@@ -2678,9 +3068,7 @@ const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePr
|
|
|
2678
3068
|
backtest: self.params.execution.context.backtest,
|
|
2679
3069
|
reason,
|
|
2680
3070
|
};
|
|
2681
|
-
|
|
2682
|
-
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
2683
|
-
}
|
|
3071
|
+
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
|
|
2684
3072
|
return result;
|
|
2685
3073
|
};
|
|
2686
3074
|
const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activationTimestamp) => {
|
|
@@ -2704,15 +3092,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
|
|
|
2704
3092
|
scheduledAt: scheduled.scheduledAt,
|
|
2705
3093
|
pendingAt: activationTime,
|
|
2706
3094
|
});
|
|
2707
|
-
if (await functoolsKit.not(self.params.
|
|
2708
|
-
pendingSignal: scheduled,
|
|
2709
|
-
symbol: self.params.execution.context.symbol,
|
|
2710
|
-
strategyName: self.params.method.context.strategyName,
|
|
2711
|
-
exchangeName: self.params.method.context.exchangeName,
|
|
2712
|
-
frameName: self.params.method.context.frameName,
|
|
2713
|
-
currentPrice: scheduled.priceOpen,
|
|
2714
|
-
timestamp: activationTime,
|
|
2715
|
-
}))) {
|
|
3095
|
+
if (await functoolsKit.not(CALL_RISK_CHECK_SIGNAL_FN(self, self.params.execution.context.symbol, scheduled, scheduled.priceOpen, activationTime, self.params.execution.context.backtest))) {
|
|
2716
3096
|
self.params.logger.info("ClientStrategy backtest scheduled signal rejected by risk", {
|
|
2717
3097
|
symbol: self.params.execution.context.symbol,
|
|
2718
3098
|
signalId: scheduled.id,
|
|
@@ -2728,15 +3108,8 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
|
|
|
2728
3108
|
_isScheduled: false,
|
|
2729
3109
|
};
|
|
2730
3110
|
await self.setPendingSignal(activatedSignal);
|
|
2731
|
-
await self.params.
|
|
2732
|
-
|
|
2733
|
-
riskName: self.params.riskName,
|
|
2734
|
-
exchangeName: self.params.method.context.exchangeName,
|
|
2735
|
-
frameName: self.params.method.context.frameName,
|
|
2736
|
-
});
|
|
2737
|
-
if (self.params.callbacks?.onOpen) {
|
|
2738
|
-
self.params.callbacks.onOpen(self.params.execution.context.symbol, activatedSignal, activatedSignal.priceOpen, self.params.execution.context.backtest);
|
|
2739
|
-
}
|
|
3111
|
+
await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, activationTime, self.params.execution.context.backtest);
|
|
3112
|
+
await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, activatedSignal, activatedSignal.priceOpen, activationTime, self.params.execution.context.backtest);
|
|
2740
3113
|
return true;
|
|
2741
3114
|
};
|
|
2742
3115
|
const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, closeReason, closeTimestamp) => {
|
|
@@ -2755,17 +3128,10 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
|
|
|
2755
3128
|
if (closeReason === "time_expired" && pnl.pnlPercentage < 0) {
|
|
2756
3129
|
self.params.logger.warn(`ClientStrategy backtest: Signal closed with loss (time_expired), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
|
|
2757
3130
|
}
|
|
2758
|
-
|
|
2759
|
-
self.params.callbacks.onClose(self.params.execution.context.symbol, signal, averagePrice, self.params.execution.context.backtest);
|
|
2760
|
-
}
|
|
3131
|
+
await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
|
|
2761
3132
|
// КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
|
|
2762
|
-
await
|
|
2763
|
-
await self.params.
|
|
2764
|
-
strategyName: self.params.method.context.strategyName,
|
|
2765
|
-
riskName: self.params.riskName,
|
|
2766
|
-
exchangeName: self.params.method.context.exchangeName,
|
|
2767
|
-
frameName: self.params.method.context.frameName,
|
|
2768
|
-
});
|
|
3133
|
+
await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, signal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
|
|
3134
|
+
await CALL_RISK_REMOVE_SIGNAL_FN(self, self.params.execution.context.symbol, closeTimestamp, self.params.execution.context.backtest);
|
|
2769
3135
|
await self.setPendingSignal(null);
|
|
2770
3136
|
const result = {
|
|
2771
3137
|
action: "closed",
|
|
@@ -2780,9 +3146,7 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
|
|
|
2780
3146
|
symbol: self.params.execution.context.symbol,
|
|
2781
3147
|
backtest: self.params.execution.context.backtest,
|
|
2782
3148
|
};
|
|
2783
|
-
|
|
2784
|
-
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
2785
|
-
}
|
|
3149
|
+
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
|
|
2786
3150
|
return result;
|
|
2787
3151
|
};
|
|
2788
3152
|
const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) => {
|
|
@@ -2857,7 +3221,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
2857
3221
|
result: null,
|
|
2858
3222
|
};
|
|
2859
3223
|
}
|
|
2860
|
-
await CALL_PING_CALLBACKS_FN(self, scheduled, candle.timestamp);
|
|
3224
|
+
await CALL_PING_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, candle.timestamp, true);
|
|
2861
3225
|
}
|
|
2862
3226
|
return {
|
|
2863
3227
|
activated: false,
|
|
@@ -2938,19 +3302,13 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
2938
3302
|
// Moving towards TP
|
|
2939
3303
|
const tpDistance = signal.priceTakeProfit - signal.priceOpen;
|
|
2940
3304
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
2941
|
-
await
|
|
2942
|
-
if (self.params.callbacks?.onPartialProfit) {
|
|
2943
|
-
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
|
|
2944
|
-
}
|
|
3305
|
+
await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
|
|
2945
3306
|
}
|
|
2946
3307
|
else if (currentDistance < 0) {
|
|
2947
3308
|
// Moving towards SL
|
|
2948
3309
|
const slDistance = signal.priceOpen - signal.priceStopLoss;
|
|
2949
3310
|
const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
|
|
2950
|
-
await
|
|
2951
|
-
if (self.params.callbacks?.onPartialLoss) {
|
|
2952
|
-
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
|
|
2953
|
-
}
|
|
3311
|
+
await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
|
|
2954
3312
|
}
|
|
2955
3313
|
}
|
|
2956
3314
|
else if (signal.position === "short") {
|
|
@@ -2960,19 +3318,13 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
2960
3318
|
// Moving towards TP
|
|
2961
3319
|
const tpDistance = signal.priceOpen - signal.priceTakeProfit;
|
|
2962
3320
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
2963
|
-
await
|
|
2964
|
-
if (self.params.callbacks?.onPartialProfit) {
|
|
2965
|
-
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
|
|
2966
|
-
}
|
|
3321
|
+
await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
|
|
2967
3322
|
}
|
|
2968
3323
|
if (currentDistance < 0) {
|
|
2969
3324
|
// Moving towards SL
|
|
2970
3325
|
const slDistance = signal.priceStopLoss - signal.priceOpen;
|
|
2971
3326
|
const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
|
|
2972
|
-
await
|
|
2973
|
-
if (self.params.callbacks?.onPartialLoss) {
|
|
2974
|
-
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
|
|
2975
|
-
}
|
|
3327
|
+
await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
|
|
2976
3328
|
}
|
|
2977
3329
|
}
|
|
2978
3330
|
}
|
|
@@ -3158,9 +3510,7 @@ class ClientStrategy {
|
|
|
3158
3510
|
signalId: cancelledSignal.id,
|
|
3159
3511
|
});
|
|
3160
3512
|
// Call onCancel callback
|
|
3161
|
-
|
|
3162
|
-
this.params.callbacks.onCancel(this.params.execution.context.symbol, cancelledSignal, currentPrice, this.params.execution.context.backtest);
|
|
3163
|
-
}
|
|
3513
|
+
await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, currentTime, this.params.execution.context.backtest);
|
|
3164
3514
|
const result = {
|
|
3165
3515
|
action: "cancelled",
|
|
3166
3516
|
signal: cancelledSignal,
|
|
@@ -3272,14 +3622,13 @@ class ClientStrategy {
|
|
|
3272
3622
|
const currentPrice = await this.params.exchange.getAveragePrice(symbol);
|
|
3273
3623
|
const cancelledSignal = this._cancelledSignal;
|
|
3274
3624
|
this._cancelledSignal = null; // Clear after using
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
}
|
|
3625
|
+
const closeTimestamp = this.params.execution.context.when.getTime();
|
|
3626
|
+
await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
|
|
3278
3627
|
const cancelledResult = {
|
|
3279
3628
|
action: "cancelled",
|
|
3280
3629
|
signal: cancelledSignal,
|
|
3281
3630
|
currentPrice,
|
|
3282
|
-
closeTimestamp:
|
|
3631
|
+
closeTimestamp: closeTimestamp,
|
|
3283
3632
|
strategyName: this.params.method.context.strategyName,
|
|
3284
3633
|
exchangeName: this.params.method.context.exchangeName,
|
|
3285
3634
|
frameName: this.params.method.context.frameName,
|
|
@@ -3458,6 +3807,190 @@ class ClientStrategy {
|
|
|
3458
3807
|
}
|
|
3459
3808
|
await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.method.context.strategyName);
|
|
3460
3809
|
}
|
|
3810
|
+
/**
|
|
3811
|
+
* Executes partial close at profit level (moving toward TP).
|
|
3812
|
+
*
|
|
3813
|
+
* Closes a percentage of the pending position at the current price, recording it as a "profit" type partial.
|
|
3814
|
+
* The partial close is tracked in `_partial` array for weighted PNL calculation when position fully closes.
|
|
3815
|
+
*
|
|
3816
|
+
* Behavior:
|
|
3817
|
+
* - Adds entry to signal's `_partial` array with type "profit"
|
|
3818
|
+
* - Validates percentToClose is in range (0, 100]
|
|
3819
|
+
* - Silently skips if total closed would exceed 100%
|
|
3820
|
+
* - Persists updated signal state (backtest and live modes)
|
|
3821
|
+
* - Calls onWrite callback for persistence testing
|
|
3822
|
+
*
|
|
3823
|
+
* Validation:
|
|
3824
|
+
* - Throws if no pending signal exists
|
|
3825
|
+
* - Throws if percentToClose is not a finite number
|
|
3826
|
+
* - Throws if percentToClose <= 0 or > 100
|
|
3827
|
+
* - Throws if currentPrice is not a positive finite number
|
|
3828
|
+
* - Throws if currentPrice is not moving toward TP:
|
|
3829
|
+
* - LONG: currentPrice must be > priceOpen
|
|
3830
|
+
* - SHORT: currentPrice must be < priceOpen
|
|
3831
|
+
*
|
|
3832
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
3833
|
+
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
3834
|
+
* @param currentPrice - Current market price for this partial close (must be in profit direction)
|
|
3835
|
+
* @param backtest - Whether running in backtest mode (controls persistence)
|
|
3836
|
+
* @returns Promise that resolves when state is updated and persisted
|
|
3837
|
+
*
|
|
3838
|
+
* @example
|
|
3839
|
+
* ```typescript
|
|
3840
|
+
* // Close 30% of position at profit (moving toward TP)
|
|
3841
|
+
* await strategy.partialProfit("BTCUSDT", 30, 45000, false);
|
|
3842
|
+
*
|
|
3843
|
+
* // Later close another 20%
|
|
3844
|
+
* await strategy.partialProfit("BTCUSDT", 20, 46000, false);
|
|
3845
|
+
*
|
|
3846
|
+
* // Final close will calculate weighted PNL from all partials
|
|
3847
|
+
* ```
|
|
3848
|
+
*/
|
|
3849
|
+
async partialProfit(symbol, percentToClose, currentPrice, backtest) {
|
|
3850
|
+
this.params.logger.debug("ClientStrategy partialProfit", {
|
|
3851
|
+
symbol,
|
|
3852
|
+
percentToClose,
|
|
3853
|
+
currentPrice,
|
|
3854
|
+
hasPendingSignal: this._pendingSignal !== null,
|
|
3855
|
+
});
|
|
3856
|
+
// Validation: must have pending signal
|
|
3857
|
+
if (!this._pendingSignal) {
|
|
3858
|
+
throw new Error(`ClientStrategy partialProfit: No pending signal exists for symbol=${symbol}`);
|
|
3859
|
+
}
|
|
3860
|
+
// Validation: percentToClose must be valid
|
|
3861
|
+
if (typeof percentToClose !== "number" || !isFinite(percentToClose)) {
|
|
3862
|
+
throw new Error(`ClientStrategy partialProfit: percentToClose must be a finite number, got ${percentToClose} (${typeof percentToClose})`);
|
|
3863
|
+
}
|
|
3864
|
+
if (percentToClose <= 0) {
|
|
3865
|
+
throw new Error(`ClientStrategy partialProfit: percentToClose must be > 0, got ${percentToClose}`);
|
|
3866
|
+
}
|
|
3867
|
+
if (percentToClose > 100) {
|
|
3868
|
+
throw new Error(`ClientStrategy partialProfit: percentToClose must be <= 100, got ${percentToClose}`);
|
|
3869
|
+
}
|
|
3870
|
+
// Validation: currentPrice must be valid
|
|
3871
|
+
if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0) {
|
|
3872
|
+
throw new Error(`ClientStrategy partialProfit: currentPrice must be a positive finite number, got ${currentPrice}`);
|
|
3873
|
+
}
|
|
3874
|
+
// Validation: currentPrice must be moving toward TP (profit direction)
|
|
3875
|
+
if (this._pendingSignal.position === "long") {
|
|
3876
|
+
// For LONG: currentPrice must be higher than priceOpen (moving toward TP)
|
|
3877
|
+
if (currentPrice <= this._pendingSignal.priceOpen) {
|
|
3878
|
+
throw new Error(`ClientStrategy partialProfit: For LONG position, currentPrice (${currentPrice}) must be > priceOpen (${this._pendingSignal.priceOpen})`);
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
else {
|
|
3882
|
+
// For SHORT: currentPrice must be lower than priceOpen (moving toward TP)
|
|
3883
|
+
if (currentPrice >= this._pendingSignal.priceOpen) {
|
|
3884
|
+
throw new Error(`ClientStrategy partialProfit: For SHORT position, currentPrice (${currentPrice}) must be < priceOpen (${this._pendingSignal.priceOpen})`);
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
// Execute partial close logic
|
|
3888
|
+
PARTIAL_PROFIT_FN(this, this._pendingSignal, percentToClose, currentPrice);
|
|
3889
|
+
// Persist updated signal state (inline setPendingSignal content)
|
|
3890
|
+
// Note: this._pendingSignal already mutated by PARTIAL_PROFIT_FN, no reassignment needed
|
|
3891
|
+
this.params.logger.debug("ClientStrategy setPendingSignal (inline)", {
|
|
3892
|
+
pendingSignal: this._pendingSignal,
|
|
3893
|
+
});
|
|
3894
|
+
// Call onWrite callback for testing persist storage
|
|
3895
|
+
if (this.params.callbacks?.onWrite) {
|
|
3896
|
+
this.params.callbacks.onWrite(this.params.execution.context.symbol, this._pendingSignal, backtest);
|
|
3897
|
+
}
|
|
3898
|
+
if (!backtest) {
|
|
3899
|
+
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName);
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
/**
|
|
3903
|
+
* Executes partial close at loss level (moving toward SL).
|
|
3904
|
+
*
|
|
3905
|
+
* Closes a percentage of the pending position at the current price, recording it as a "loss" type partial.
|
|
3906
|
+
* The partial close is tracked in `_partial` array for weighted PNL calculation when position fully closes.
|
|
3907
|
+
*
|
|
3908
|
+
* Behavior:
|
|
3909
|
+
* - Adds entry to signal's `_partial` array with type "loss"
|
|
3910
|
+
* - Validates percentToClose is in range (0, 100]
|
|
3911
|
+
* - Silently skips if total closed would exceed 100%
|
|
3912
|
+
* - Persists updated signal state (backtest and live modes)
|
|
3913
|
+
* - Calls onWrite callback for persistence testing
|
|
3914
|
+
*
|
|
3915
|
+
* Validation:
|
|
3916
|
+
* - Throws if no pending signal exists
|
|
3917
|
+
* - Throws if percentToClose is not a finite number
|
|
3918
|
+
* - Throws if percentToClose <= 0 or > 100
|
|
3919
|
+
* - Throws if currentPrice is not a positive finite number
|
|
3920
|
+
* - Throws if currentPrice is not moving toward SL:
|
|
3921
|
+
* - LONG: currentPrice must be < priceOpen
|
|
3922
|
+
* - SHORT: currentPrice must be > priceOpen
|
|
3923
|
+
*
|
|
3924
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
3925
|
+
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
3926
|
+
* @param currentPrice - Current market price for this partial close (must be in loss direction)
|
|
3927
|
+
* @param backtest - Whether running in backtest mode (controls persistence)
|
|
3928
|
+
* @returns Promise that resolves when state is updated and persisted
|
|
3929
|
+
*
|
|
3930
|
+
* @example
|
|
3931
|
+
* ```typescript
|
|
3932
|
+
* // Close 40% of position at loss (moving toward SL)
|
|
3933
|
+
* await strategy.partialLoss("BTCUSDT", 40, 38000, false);
|
|
3934
|
+
*
|
|
3935
|
+
* // Later close another 30%
|
|
3936
|
+
* await strategy.partialLoss("BTCUSDT", 30, 37000, false);
|
|
3937
|
+
*
|
|
3938
|
+
* // Final close will calculate weighted PNL from all partials
|
|
3939
|
+
* ```
|
|
3940
|
+
*/
|
|
3941
|
+
async partialLoss(symbol, percentToClose, currentPrice, backtest) {
|
|
3942
|
+
this.params.logger.debug("ClientStrategy partialLoss", {
|
|
3943
|
+
symbol,
|
|
3944
|
+
percentToClose,
|
|
3945
|
+
currentPrice,
|
|
3946
|
+
hasPendingSignal: this._pendingSignal !== null,
|
|
3947
|
+
});
|
|
3948
|
+
// Validation: must have pending signal
|
|
3949
|
+
if (!this._pendingSignal) {
|
|
3950
|
+
throw new Error(`ClientStrategy partialLoss: No pending signal exists for symbol=${symbol}`);
|
|
3951
|
+
}
|
|
3952
|
+
// Validation: percentToClose must be valid
|
|
3953
|
+
if (typeof percentToClose !== "number" || !isFinite(percentToClose)) {
|
|
3954
|
+
throw new Error(`ClientStrategy partialLoss: percentToClose must be a finite number, got ${percentToClose} (${typeof percentToClose})`);
|
|
3955
|
+
}
|
|
3956
|
+
if (percentToClose <= 0) {
|
|
3957
|
+
throw new Error(`ClientStrategy partialLoss: percentToClose must be > 0, got ${percentToClose}`);
|
|
3958
|
+
}
|
|
3959
|
+
if (percentToClose > 100) {
|
|
3960
|
+
throw new Error(`ClientStrategy partialLoss: percentToClose must be <= 100, got ${percentToClose}`);
|
|
3961
|
+
}
|
|
3962
|
+
// Validation: currentPrice must be valid
|
|
3963
|
+
if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0) {
|
|
3964
|
+
throw new Error(`ClientStrategy partialLoss: currentPrice must be a positive finite number, got ${currentPrice}`);
|
|
3965
|
+
}
|
|
3966
|
+
// Validation: currentPrice must be moving toward SL (loss direction)
|
|
3967
|
+
if (this._pendingSignal.position === "long") {
|
|
3968
|
+
// For LONG: currentPrice must be lower than priceOpen (moving toward SL)
|
|
3969
|
+
if (currentPrice >= this._pendingSignal.priceOpen) {
|
|
3970
|
+
throw new Error(`ClientStrategy partialLoss: For LONG position, currentPrice (${currentPrice}) must be < priceOpen (${this._pendingSignal.priceOpen})`);
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
else {
|
|
3974
|
+
// For SHORT: currentPrice must be higher than priceOpen (moving toward SL)
|
|
3975
|
+
if (currentPrice <= this._pendingSignal.priceOpen) {
|
|
3976
|
+
throw new Error(`ClientStrategy partialLoss: For SHORT position, currentPrice (${currentPrice}) must be > priceOpen (${this._pendingSignal.priceOpen})`);
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
// Execute partial close logic
|
|
3980
|
+
PARTIAL_LOSS_FN(this, this._pendingSignal, percentToClose, currentPrice);
|
|
3981
|
+
// Persist updated signal state (inline setPendingSignal content)
|
|
3982
|
+
// Note: this._pendingSignal already mutated by PARTIAL_LOSS_FN, no reassignment needed
|
|
3983
|
+
this.params.logger.debug("ClientStrategy setPendingSignal (inline)", {
|
|
3984
|
+
pendingSignal: this._pendingSignal,
|
|
3985
|
+
});
|
|
3986
|
+
// Call onWrite callback for testing persist storage
|
|
3987
|
+
if (this.params.callbacks?.onWrite) {
|
|
3988
|
+
this.params.callbacks.onWrite(this.params.execution.context.symbol, this._pendingSignal, backtest);
|
|
3989
|
+
}
|
|
3990
|
+
if (!backtest) {
|
|
3991
|
+
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName);
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3461
3994
|
}
|
|
3462
3995
|
|
|
3463
3996
|
const RISK_METHOD_NAME_GET_DATA = "RiskUtils.getData";
|
|
@@ -3859,6 +4392,7 @@ class StrategyConnectionService {
|
|
|
3859
4392
|
constructor() {
|
|
3860
4393
|
this.loggerService = inject(TYPES.loggerService);
|
|
3861
4394
|
this.executionContextService = inject(TYPES.executionContextService);
|
|
4395
|
+
this.methodContextService = inject(TYPES.methodContextService);
|
|
3862
4396
|
this.strategySchemaService = inject(TYPES.strategySchemaService);
|
|
3863
4397
|
this.riskConnectionService = inject(TYPES.riskConnectionService);
|
|
3864
4398
|
this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
|
|
@@ -3882,7 +4416,7 @@ class StrategyConnectionService {
|
|
|
3882
4416
|
symbol,
|
|
3883
4417
|
interval,
|
|
3884
4418
|
execution: this.executionContextService,
|
|
3885
|
-
method:
|
|
4419
|
+
method: this.methodContextService,
|
|
3886
4420
|
logger: this.loggerService,
|
|
3887
4421
|
partial: this.partialConnectionService,
|
|
3888
4422
|
exchange: this.exchangeConnectionService,
|
|
@@ -4061,28 +4595,104 @@ class StrategyConnectionService {
|
|
|
4061
4595
|
}
|
|
4062
4596
|
};
|
|
4063
4597
|
/**
|
|
4064
|
-
* Cancels the scheduled signal for the specified strategy.
|
|
4598
|
+
* Cancels the scheduled signal for the specified strategy.
|
|
4599
|
+
*
|
|
4600
|
+
* Delegates to ClientStrategy.cancel() which clears the scheduled signal
|
|
4601
|
+
* without stopping the strategy or affecting pending signals.
|
|
4602
|
+
*
|
|
4603
|
+
* Note: Cancelled event will be emitted on next tick() call when strategy
|
|
4604
|
+
* detects the scheduled signal was cancelled.
|
|
4605
|
+
*
|
|
4606
|
+
* @param backtest - Whether running in backtest mode
|
|
4607
|
+
* @param symbol - Trading pair symbol
|
|
4608
|
+
* @param ctx - Context with strategyName, exchangeName, frameName
|
|
4609
|
+
* @param cancelId - Optional cancellation ID for user-initiated cancellations
|
|
4610
|
+
* @returns Promise that resolves when scheduled signal is cancelled
|
|
4611
|
+
*/
|
|
4612
|
+
this.cancel = async (backtest, symbol, context, cancelId) => {
|
|
4613
|
+
this.loggerService.log("strategyConnectionService cancel", {
|
|
4614
|
+
symbol,
|
|
4615
|
+
context,
|
|
4616
|
+
cancelId,
|
|
4617
|
+
});
|
|
4618
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
4619
|
+
await strategy.cancel(symbol, backtest, cancelId);
|
|
4620
|
+
};
|
|
4621
|
+
/**
|
|
4622
|
+
* Executes partial close at profit level (moving toward TP).
|
|
4623
|
+
*
|
|
4624
|
+
* Closes a percentage of the pending position at the current price, recording it as a "profit" type partial.
|
|
4625
|
+
* The partial close is tracked in `_partial` array for weighted PNL calculation when position fully closes.
|
|
4626
|
+
*
|
|
4627
|
+
* Delegates to ClientStrategy.partialProfit() with current execution context.
|
|
4628
|
+
*
|
|
4629
|
+
* @param backtest - Whether running in backtest mode
|
|
4630
|
+
* @param symbol - Trading pair symbol
|
|
4631
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
4632
|
+
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
4633
|
+
* @param currentPrice - Current market price for this partial close
|
|
4634
|
+
* @returns Promise that resolves when state is updated and persisted
|
|
4635
|
+
*
|
|
4636
|
+
* @example
|
|
4637
|
+
* ```typescript
|
|
4638
|
+
* // Close 30% of position at profit
|
|
4639
|
+
* await strategyConnectionService.partialProfit(
|
|
4640
|
+
* false,
|
|
4641
|
+
* "BTCUSDT",
|
|
4642
|
+
* { strategyName: "my-strategy", exchangeName: "binance", frameName: "" },
|
|
4643
|
+
* 30,
|
|
4644
|
+
* 45000
|
|
4645
|
+
* );
|
|
4646
|
+
* ```
|
|
4647
|
+
*/
|
|
4648
|
+
this.partialProfit = async (backtest, symbol, percentToClose, currentPrice, context) => {
|
|
4649
|
+
this.loggerService.log("strategyConnectionService partialProfit", {
|
|
4650
|
+
symbol,
|
|
4651
|
+
context,
|
|
4652
|
+
percentToClose,
|
|
4653
|
+
currentPrice,
|
|
4654
|
+
backtest,
|
|
4655
|
+
});
|
|
4656
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
4657
|
+
await strategy.partialProfit(symbol, percentToClose, currentPrice, backtest);
|
|
4658
|
+
};
|
|
4659
|
+
/**
|
|
4660
|
+
* Executes partial close at loss level (moving toward SL).
|
|
4065
4661
|
*
|
|
4066
|
-
*
|
|
4067
|
-
*
|
|
4662
|
+
* Closes a percentage of the pending position at the current price, recording it as a "loss" type partial.
|
|
4663
|
+
* The partial close is tracked in `_partial` array for weighted PNL calculation when position fully closes.
|
|
4068
4664
|
*
|
|
4069
|
-
*
|
|
4070
|
-
* detects the scheduled signal was cancelled.
|
|
4665
|
+
* Delegates to ClientStrategy.partialLoss() with current execution context.
|
|
4071
4666
|
*
|
|
4072
4667
|
* @param backtest - Whether running in backtest mode
|
|
4073
4668
|
* @param symbol - Trading pair symbol
|
|
4074
|
-
* @param
|
|
4075
|
-
* @param
|
|
4076
|
-
* @
|
|
4669
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
4670
|
+
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
4671
|
+
* @param currentPrice - Current market price for this partial close
|
|
4672
|
+
* @returns Promise that resolves when state is updated and persisted
|
|
4673
|
+
*
|
|
4674
|
+
* @example
|
|
4675
|
+
* ```typescript
|
|
4676
|
+
* // Close 40% of position at loss
|
|
4677
|
+
* await strategyConnectionService.partialLoss(
|
|
4678
|
+
* false,
|
|
4679
|
+
* "BTCUSDT",
|
|
4680
|
+
* { strategyName: "my-strategy", exchangeName: "binance", frameName: "" },
|
|
4681
|
+
* 40,
|
|
4682
|
+
* 38000
|
|
4683
|
+
* );
|
|
4684
|
+
* ```
|
|
4077
4685
|
*/
|
|
4078
|
-
this.
|
|
4079
|
-
this.loggerService.log("strategyConnectionService
|
|
4686
|
+
this.partialLoss = async (backtest, symbol, percentToClose, currentPrice, context) => {
|
|
4687
|
+
this.loggerService.log("strategyConnectionService partialLoss", {
|
|
4080
4688
|
symbol,
|
|
4081
4689
|
context,
|
|
4082
|
-
|
|
4690
|
+
percentToClose,
|
|
4691
|
+
currentPrice,
|
|
4692
|
+
backtest,
|
|
4083
4693
|
});
|
|
4084
4694
|
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
4085
|
-
await strategy.
|
|
4695
|
+
await strategy.partialLoss(symbol, percentToClose, currentPrice, backtest);
|
|
4086
4696
|
};
|
|
4087
4697
|
}
|
|
4088
4698
|
}
|
|
@@ -5210,6 +5820,82 @@ class StrategyCoreService {
|
|
|
5210
5820
|
}
|
|
5211
5821
|
return await this.strategyConnectionService.clear(payload);
|
|
5212
5822
|
};
|
|
5823
|
+
/**
|
|
5824
|
+
* Executes partial close at profit level (moving toward TP).
|
|
5825
|
+
*
|
|
5826
|
+
* Validates strategy existence and delegates to connection service
|
|
5827
|
+
* to close a percentage of the pending position at profit.
|
|
5828
|
+
*
|
|
5829
|
+
* Does not require execution context as this is a direct state mutation.
|
|
5830
|
+
*
|
|
5831
|
+
* @param backtest - Whether running in backtest mode
|
|
5832
|
+
* @param symbol - Trading pair symbol
|
|
5833
|
+
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
5834
|
+
* @param currentPrice - Current market price for this partial close (must be in profit direction)
|
|
5835
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
5836
|
+
* @returns Promise that resolves when state is updated and persisted
|
|
5837
|
+
*
|
|
5838
|
+
* @example
|
|
5839
|
+
* ```typescript
|
|
5840
|
+
* // Close 30% of position at profit
|
|
5841
|
+
* await strategyCoreService.partialProfit(
|
|
5842
|
+
* false,
|
|
5843
|
+
* "BTCUSDT",
|
|
5844
|
+
* 30,
|
|
5845
|
+
* 45000,
|
|
5846
|
+
* { strategyName: "my-strategy", exchangeName: "binance", frameName: "" }
|
|
5847
|
+
* );
|
|
5848
|
+
* ```
|
|
5849
|
+
*/
|
|
5850
|
+
this.partialProfit = async (backtest, symbol, percentToClose, currentPrice, context) => {
|
|
5851
|
+
this.loggerService.log("strategyCoreService partialProfit", {
|
|
5852
|
+
symbol,
|
|
5853
|
+
percentToClose,
|
|
5854
|
+
currentPrice,
|
|
5855
|
+
context,
|
|
5856
|
+
backtest,
|
|
5857
|
+
});
|
|
5858
|
+
await this.validate(symbol, context);
|
|
5859
|
+
return await this.strategyConnectionService.partialProfit(backtest, symbol, percentToClose, currentPrice, context);
|
|
5860
|
+
};
|
|
5861
|
+
/**
|
|
5862
|
+
* Executes partial close at loss level (moving toward SL).
|
|
5863
|
+
*
|
|
5864
|
+
* Validates strategy existence and delegates to connection service
|
|
5865
|
+
* to close a percentage of the pending position at loss.
|
|
5866
|
+
*
|
|
5867
|
+
* Does not require execution context as this is a direct state mutation.
|
|
5868
|
+
*
|
|
5869
|
+
* @param backtest - Whether running in backtest mode
|
|
5870
|
+
* @param symbol - Trading pair symbol
|
|
5871
|
+
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
5872
|
+
* @param currentPrice - Current market price for this partial close (must be in loss direction)
|
|
5873
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
5874
|
+
* @returns Promise that resolves when state is updated and persisted
|
|
5875
|
+
*
|
|
5876
|
+
* @example
|
|
5877
|
+
* ```typescript
|
|
5878
|
+
* // Close 40% of position at loss
|
|
5879
|
+
* await strategyCoreService.partialLoss(
|
|
5880
|
+
* false,
|
|
5881
|
+
* "BTCUSDT",
|
|
5882
|
+
* 40,
|
|
5883
|
+
* 38000,
|
|
5884
|
+
* { strategyName: "my-strategy", exchangeName: "binance", frameName: "" }
|
|
5885
|
+
* );
|
|
5886
|
+
* ```
|
|
5887
|
+
*/
|
|
5888
|
+
this.partialLoss = async (backtest, symbol, percentToClose, currentPrice, context) => {
|
|
5889
|
+
this.loggerService.log("strategyCoreService partialLoss", {
|
|
5890
|
+
symbol,
|
|
5891
|
+
percentToClose,
|
|
5892
|
+
currentPrice,
|
|
5893
|
+
context,
|
|
5894
|
+
backtest,
|
|
5895
|
+
});
|
|
5896
|
+
await this.validate(symbol, context);
|
|
5897
|
+
return await this.strategyConnectionService.partialLoss(backtest, symbol, percentToClose, currentPrice, context);
|
|
5898
|
+
};
|
|
5213
5899
|
}
|
|
5214
5900
|
}
|
|
5215
5901
|
|
|
@@ -6932,6 +7618,21 @@ const backtest_columns = [
|
|
|
6932
7618
|
},
|
|
6933
7619
|
isVisible: () => true,
|
|
6934
7620
|
},
|
|
7621
|
+
{
|
|
7622
|
+
key: "partialCloses",
|
|
7623
|
+
label: "Partial Closes",
|
|
7624
|
+
format: (data) => {
|
|
7625
|
+
const partial = data.signal._partial;
|
|
7626
|
+
if (!partial || partial.length === 0)
|
|
7627
|
+
return "N/A";
|
|
7628
|
+
const profitCount = partial.filter(p => p.type === "profit").length;
|
|
7629
|
+
const lossCount = partial.filter(p => p.type === "loss").length;
|
|
7630
|
+
const profitPercent = partial.filter(p => p.type === "profit").reduce((sum, p) => sum + p.percent, 0);
|
|
7631
|
+
const lossPercent = partial.filter(p => p.type === "loss").reduce((sum, p) => sum + p.percent, 0);
|
|
7632
|
+
return `${partial.length} (↑${profitCount}: ${profitPercent.toFixed(1)}%, ↓${lossCount}: ${lossPercent.toFixed(1)}%)`;
|
|
7633
|
+
},
|
|
7634
|
+
isVisible: () => true,
|
|
7635
|
+
},
|
|
6935
7636
|
{
|
|
6936
7637
|
key: "closeReason",
|
|
6937
7638
|
label: "Close Reason",
|
|
@@ -7007,49 +7708,49 @@ const heat_columns = [
|
|
|
7007
7708
|
{
|
|
7008
7709
|
key: "totalPnl",
|
|
7009
7710
|
label: "Total PNL",
|
|
7010
|
-
format: (data) => data.totalPnl !== null ? functoolsKit.str(data.totalPnl, "
|
|
7711
|
+
format: (data) => data.totalPnl !== null ? functoolsKit.str(data.totalPnl, "%") : "N/A",
|
|
7011
7712
|
isVisible: () => true,
|
|
7012
7713
|
},
|
|
7013
7714
|
{
|
|
7014
7715
|
key: "sharpeRatio",
|
|
7015
7716
|
label: "Sharpe",
|
|
7016
|
-
format: (data) => data.sharpeRatio !== null ? functoolsKit.str(data.sharpeRatio
|
|
7717
|
+
format: (data) => data.sharpeRatio !== null ? functoolsKit.str(data.sharpeRatio) : "N/A",
|
|
7017
7718
|
isVisible: () => true,
|
|
7018
7719
|
},
|
|
7019
7720
|
{
|
|
7020
7721
|
key: "profitFactor",
|
|
7021
7722
|
label: "PF",
|
|
7022
|
-
format: (data) => data.profitFactor !== null ? functoolsKit.str(data.profitFactor
|
|
7723
|
+
format: (data) => data.profitFactor !== null ? functoolsKit.str(data.profitFactor) : "N/A",
|
|
7023
7724
|
isVisible: () => true,
|
|
7024
7725
|
},
|
|
7025
7726
|
{
|
|
7026
7727
|
key: "expectancy",
|
|
7027
7728
|
label: "Expect",
|
|
7028
|
-
format: (data) => data.expectancy !== null ? functoolsKit.str(data.expectancy, "
|
|
7729
|
+
format: (data) => data.expectancy !== null ? functoolsKit.str(data.expectancy, "%") : "N/A",
|
|
7029
7730
|
isVisible: () => true,
|
|
7030
7731
|
},
|
|
7031
7732
|
{
|
|
7032
7733
|
key: "winRate",
|
|
7033
7734
|
label: "WR",
|
|
7034
|
-
format: (data) => data.winRate !== null ? functoolsKit.str(data.winRate, "
|
|
7735
|
+
format: (data) => data.winRate !== null ? functoolsKit.str(data.winRate, "%") : "N/A",
|
|
7035
7736
|
isVisible: () => true,
|
|
7036
7737
|
},
|
|
7037
7738
|
{
|
|
7038
7739
|
key: "avgWin",
|
|
7039
7740
|
label: "Avg Win",
|
|
7040
|
-
format: (data) => data.avgWin !== null ? functoolsKit.str(data.avgWin, "
|
|
7741
|
+
format: (data) => data.avgWin !== null ? functoolsKit.str(data.avgWin, "%") : "N/A",
|
|
7041
7742
|
isVisible: () => true,
|
|
7042
7743
|
},
|
|
7043
7744
|
{
|
|
7044
7745
|
key: "avgLoss",
|
|
7045
7746
|
label: "Avg Loss",
|
|
7046
|
-
format: (data) => data.avgLoss !== null ? functoolsKit.str(data.avgLoss, "
|
|
7747
|
+
format: (data) => data.avgLoss !== null ? functoolsKit.str(data.avgLoss, "%") : "N/A",
|
|
7047
7748
|
isVisible: () => true,
|
|
7048
7749
|
},
|
|
7049
7750
|
{
|
|
7050
7751
|
key: "maxDrawdown",
|
|
7051
7752
|
label: "Max DD",
|
|
7052
|
-
format: (data) => data.maxDrawdown !== null ? functoolsKit.str(-data.maxDrawdown, "
|
|
7753
|
+
format: (data) => data.maxDrawdown !== null ? functoolsKit.str(-data.maxDrawdown, "%") : "N/A",
|
|
7053
7754
|
isVisible: () => true,
|
|
7054
7755
|
},
|
|
7055
7756
|
{
|
|
@@ -10324,7 +11025,7 @@ class HeatmapStorage {
|
|
|
10324
11025
|
return [
|
|
10325
11026
|
`# Portfolio Heatmap: ${strategyName}`,
|
|
10326
11027
|
"",
|
|
10327
|
-
`**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? functoolsKit.str(data.portfolioTotalPnl, "
|
|
11028
|
+
`**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? functoolsKit.str(data.portfolioTotalPnl, "%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? functoolsKit.str(data.portfolioSharpeRatio) : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades}`,
|
|
10328
11029
|
"",
|
|
10329
11030
|
table
|
|
10330
11031
|
].join("\n");
|
|
@@ -11428,12 +12129,16 @@ class OptimizerTemplateService {
|
|
|
11428
12129
|
``,
|
|
11429
12130
|
`listenWalkerComplete((results) => {`,
|
|
11430
12131
|
` console.log("Walker completed:", results.bestStrategy);`,
|
|
11431
|
-
` Walker.dump(
|
|
12132
|
+
` Walker.dump(results.symbol, { walkerName: results.walkerName });`,
|
|
11432
12133
|
`});`,
|
|
11433
12134
|
``,
|
|
11434
12135
|
`listenDoneBacktest((event) => {`,
|
|
11435
12136
|
` console.log("Backtest completed:", event.symbol);`,
|
|
11436
|
-
` Backtest.dump(event.symbol,
|
|
12137
|
+
` Backtest.dump(event.symbol, {`,
|
|
12138
|
+
` strategyName: event.strategyName,`,
|
|
12139
|
+
` exchangeName: event.exchangeName,`,
|
|
12140
|
+
` frameName: event.frameName`,
|
|
12141
|
+
` });`,
|
|
11437
12142
|
`});`,
|
|
11438
12143
|
``,
|
|
11439
12144
|
`listenError((error) => {`,
|
|
@@ -11467,12 +12172,10 @@ class OptimizerTemplateService {
|
|
|
11467
12172
|
` }`,
|
|
11468
12173
|
``,
|
|
11469
12174
|
` {`,
|
|
11470
|
-
` let summary = "# Outline Result Summary\\n";`,
|
|
12175
|
+
` let summary = "# Outline Result Summary\\n\\n";`,
|
|
11471
12176
|
``,
|
|
11472
12177
|
` {`,
|
|
11473
|
-
` summary +=
|
|
11474
|
-
` summary += \`**ResultId**: \${resultId}\\n\`;`,
|
|
11475
|
-
` summary += "\\n";`,
|
|
12178
|
+
` summary += \`**ResultId**: \${resultId}\\n\\n\`;`,
|
|
11476
12179
|
` }`,
|
|
11477
12180
|
``,
|
|
11478
12181
|
` if (result) {`,
|
|
@@ -11488,7 +12191,7 @@ class OptimizerTemplateService {
|
|
|
11488
12191
|
` systemMessages.forEach((msg, idx) => {`,
|
|
11489
12192
|
` summary += \`### System Message \${idx + 1}\\n\\n\`;`,
|
|
11490
12193
|
` summary += msg.content;`,
|
|
11491
|
-
` summary += "\\n";`,
|
|
12194
|
+
` summary += "\\n\\n";`,
|
|
11492
12195
|
` });`,
|
|
11493
12196
|
` }`,
|
|
11494
12197
|
``,
|
|
@@ -14519,13 +15222,203 @@ const validateInternal = async (args) => {
|
|
|
14519
15222
|
* });
|
|
14520
15223
|
* ```
|
|
14521
15224
|
*/
|
|
14522
|
-
async function validate(args = {}) {
|
|
14523
|
-
backtest$1.loggerService.log(METHOD_NAME);
|
|
14524
|
-
return await validateInternal(args);
|
|
15225
|
+
async function validate(args = {}) {
|
|
15226
|
+
backtest$1.loggerService.log(METHOD_NAME);
|
|
15227
|
+
return await validateInternal(args);
|
|
15228
|
+
}
|
|
15229
|
+
|
|
15230
|
+
const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
|
|
15231
|
+
const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
|
|
15232
|
+
const FORMAT_PRICE_METHOD_NAME = "exchange.formatPrice";
|
|
15233
|
+
const FORMAT_QUANTITY_METHOD_NAME = "exchange.formatQuantity";
|
|
15234
|
+
const GET_DATE_METHOD_NAME = "exchange.getDate";
|
|
15235
|
+
const GET_MODE_METHOD_NAME = "exchange.getMode";
|
|
15236
|
+
const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
|
|
15237
|
+
/**
|
|
15238
|
+
* Checks if trade context is active (execution and method contexts).
|
|
15239
|
+
*
|
|
15240
|
+
* Returns true when both contexts are active, which is required for calling
|
|
15241
|
+
* exchange functions like getCandles, getAveragePrice, formatPrice, formatQuantity,
|
|
15242
|
+
* getDate, and getMode.
|
|
15243
|
+
*
|
|
15244
|
+
* @returns true if trade context is active, false otherwise
|
|
15245
|
+
*
|
|
15246
|
+
* @example
|
|
15247
|
+
* ```typescript
|
|
15248
|
+
* import { hasTradeContext, getCandles } from "backtest-kit";
|
|
15249
|
+
*
|
|
15250
|
+
* if (hasTradeContext()) {
|
|
15251
|
+
* const candles = await getCandles("BTCUSDT", "1m", 100);
|
|
15252
|
+
* } else {
|
|
15253
|
+
* console.log("Trade context not active");
|
|
15254
|
+
* }
|
|
15255
|
+
* ```
|
|
15256
|
+
*/
|
|
15257
|
+
function hasTradeContext() {
|
|
15258
|
+
backtest$1.loggerService.info(HAS_TRADE_CONTEXT_METHOD_NAME);
|
|
15259
|
+
return ExecutionContextService.hasContext() && MethodContextService.hasContext();
|
|
15260
|
+
}
|
|
15261
|
+
/**
|
|
15262
|
+
* Fetches historical candle data from the registered exchange.
|
|
15263
|
+
*
|
|
15264
|
+
* Candles are fetched backwards from the current execution context time.
|
|
15265
|
+
* Uses the exchange's getCandles implementation.
|
|
15266
|
+
*
|
|
15267
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
15268
|
+
* @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
|
|
15269
|
+
* @param limit - Number of candles to fetch
|
|
15270
|
+
* @returns Promise resolving to array of candle data
|
|
15271
|
+
*
|
|
15272
|
+
* @example
|
|
15273
|
+
* ```typescript
|
|
15274
|
+
* const candles = await getCandles("BTCUSDT", "1m", 100);
|
|
15275
|
+
* console.log(candles[0]); // { timestamp, open, high, low, close, volume }
|
|
15276
|
+
* ```
|
|
15277
|
+
*/
|
|
15278
|
+
async function getCandles(symbol, interval, limit) {
|
|
15279
|
+
backtest$1.loggerService.info(GET_CANDLES_METHOD_NAME, {
|
|
15280
|
+
symbol,
|
|
15281
|
+
interval,
|
|
15282
|
+
limit,
|
|
15283
|
+
});
|
|
15284
|
+
if (!ExecutionContextService.hasContext()) {
|
|
15285
|
+
throw new Error("getCandles requires an execution context");
|
|
15286
|
+
}
|
|
15287
|
+
if (!MethodContextService.hasContext()) {
|
|
15288
|
+
throw new Error("getCandles requires a method context");
|
|
15289
|
+
}
|
|
15290
|
+
return await backtest$1.exchangeConnectionService.getCandles(symbol, interval, limit);
|
|
15291
|
+
}
|
|
15292
|
+
/**
|
|
15293
|
+
* Calculates VWAP (Volume Weighted Average Price) for a symbol.
|
|
15294
|
+
*
|
|
15295
|
+
* Uses the last 5 1-minute candles to calculate:
|
|
15296
|
+
* - Typical Price = (high + low + close) / 3
|
|
15297
|
+
* - VWAP = sum(typical_price * volume) / sum(volume)
|
|
15298
|
+
*
|
|
15299
|
+
* If volume is zero, returns simple average of close prices.
|
|
15300
|
+
*
|
|
15301
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
15302
|
+
* @returns Promise resolving to VWAP price
|
|
15303
|
+
*
|
|
15304
|
+
* @example
|
|
15305
|
+
* ```typescript
|
|
15306
|
+
* const vwap = await getAveragePrice("BTCUSDT");
|
|
15307
|
+
* console.log(vwap); // 50125.43
|
|
15308
|
+
* ```
|
|
15309
|
+
*/
|
|
15310
|
+
async function getAveragePrice(symbol) {
|
|
15311
|
+
backtest$1.loggerService.info(GET_AVERAGE_PRICE_METHOD_NAME, {
|
|
15312
|
+
symbol,
|
|
15313
|
+
});
|
|
15314
|
+
if (!ExecutionContextService.hasContext()) {
|
|
15315
|
+
throw new Error("getAveragePrice requires an execution context");
|
|
15316
|
+
}
|
|
15317
|
+
if (!MethodContextService.hasContext()) {
|
|
15318
|
+
throw new Error("getAveragePrice requires a method context");
|
|
15319
|
+
}
|
|
15320
|
+
return await backtest$1.exchangeConnectionService.getAveragePrice(symbol);
|
|
15321
|
+
}
|
|
15322
|
+
/**
|
|
15323
|
+
* Formats a price value according to exchange rules.
|
|
15324
|
+
*
|
|
15325
|
+
* Uses the exchange's formatPrice implementation for proper decimal places.
|
|
15326
|
+
*
|
|
15327
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
15328
|
+
* @param price - Raw price value
|
|
15329
|
+
* @returns Promise resolving to formatted price string
|
|
15330
|
+
*
|
|
15331
|
+
* @example
|
|
15332
|
+
* ```typescript
|
|
15333
|
+
* const formatted = await formatPrice("BTCUSDT", 50000.123456);
|
|
15334
|
+
* console.log(formatted); // "50000.12"
|
|
15335
|
+
* ```
|
|
15336
|
+
*/
|
|
15337
|
+
async function formatPrice(symbol, price) {
|
|
15338
|
+
backtest$1.loggerService.info(FORMAT_PRICE_METHOD_NAME, {
|
|
15339
|
+
symbol,
|
|
15340
|
+
price,
|
|
15341
|
+
});
|
|
15342
|
+
if (!MethodContextService.hasContext()) {
|
|
15343
|
+
throw new Error("formatPrice requires a method context");
|
|
15344
|
+
}
|
|
15345
|
+
return await backtest$1.exchangeConnectionService.formatPrice(symbol, price);
|
|
15346
|
+
}
|
|
15347
|
+
/**
|
|
15348
|
+
* Formats a quantity value according to exchange rules.
|
|
15349
|
+
*
|
|
15350
|
+
* Uses the exchange's formatQuantity implementation for proper decimal places.
|
|
15351
|
+
*
|
|
15352
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
15353
|
+
* @param quantity - Raw quantity value
|
|
15354
|
+
* @returns Promise resolving to formatted quantity string
|
|
15355
|
+
*
|
|
15356
|
+
* @example
|
|
15357
|
+
* ```typescript
|
|
15358
|
+
* const formatted = await formatQuantity("BTCUSDT", 0.123456789);
|
|
15359
|
+
* console.log(formatted); // "0.12345678"
|
|
15360
|
+
* ```
|
|
15361
|
+
*/
|
|
15362
|
+
async function formatQuantity(symbol, quantity) {
|
|
15363
|
+
backtest$1.loggerService.info(FORMAT_QUANTITY_METHOD_NAME, {
|
|
15364
|
+
symbol,
|
|
15365
|
+
quantity,
|
|
15366
|
+
});
|
|
15367
|
+
if (!MethodContextService.hasContext()) {
|
|
15368
|
+
throw new Error("formatQuantity requires a method context");
|
|
15369
|
+
}
|
|
15370
|
+
return await backtest$1.exchangeConnectionService.formatQuantity(symbol, quantity);
|
|
15371
|
+
}
|
|
15372
|
+
/**
|
|
15373
|
+
* Gets the current date from execution context.
|
|
15374
|
+
*
|
|
15375
|
+
* In backtest mode: returns the current timeframe date being processed
|
|
15376
|
+
* In live mode: returns current real-time date
|
|
15377
|
+
*
|
|
15378
|
+
* @returns Promise resolving to current execution context date
|
|
15379
|
+
*
|
|
15380
|
+
* @example
|
|
15381
|
+
* ```typescript
|
|
15382
|
+
* const date = await getDate();
|
|
15383
|
+
* console.log(date); // 2024-01-01T12:00:00.000Z
|
|
15384
|
+
* ```
|
|
15385
|
+
*/
|
|
15386
|
+
async function getDate() {
|
|
15387
|
+
backtest$1.loggerService.info(GET_DATE_METHOD_NAME);
|
|
15388
|
+
if (!ExecutionContextService.hasContext()) {
|
|
15389
|
+
throw new Error("getDate requires an execution context");
|
|
15390
|
+
}
|
|
15391
|
+
const { when } = backtest$1.executionContextService.context;
|
|
15392
|
+
return new Date(when.getTime());
|
|
15393
|
+
}
|
|
15394
|
+
/**
|
|
15395
|
+
* Gets the current execution mode.
|
|
15396
|
+
*
|
|
15397
|
+
* @returns Promise resolving to "backtest" or "live"
|
|
15398
|
+
*
|
|
15399
|
+
* @example
|
|
15400
|
+
* ```typescript
|
|
15401
|
+
* const mode = await getMode();
|
|
15402
|
+
* if (mode === "backtest") {
|
|
15403
|
+
* console.log("Running in backtest mode");
|
|
15404
|
+
* } else {
|
|
15405
|
+
* console.log("Running in live mode");
|
|
15406
|
+
* }
|
|
15407
|
+
* ```
|
|
15408
|
+
*/
|
|
15409
|
+
async function getMode() {
|
|
15410
|
+
backtest$1.loggerService.info(GET_MODE_METHOD_NAME);
|
|
15411
|
+
if (!ExecutionContextService.hasContext()) {
|
|
15412
|
+
throw new Error("getMode requires an execution context");
|
|
15413
|
+
}
|
|
15414
|
+
const { backtest: bt } = backtest$1.executionContextService.context;
|
|
15415
|
+
return bt ? "backtest" : "live";
|
|
14525
15416
|
}
|
|
14526
15417
|
|
|
14527
15418
|
const STOP_METHOD_NAME = "strategy.stop";
|
|
14528
15419
|
const CANCEL_METHOD_NAME = "strategy.cancel";
|
|
15420
|
+
const PARTIAL_PROFIT_METHOD_NAME = "strategy.partialProfit";
|
|
15421
|
+
const PARTIAL_LOSS_METHOD_NAME = "strategy.partialLoss";
|
|
14529
15422
|
/**
|
|
14530
15423
|
* Stops the strategy from generating new signals.
|
|
14531
15424
|
*
|
|
@@ -14602,6 +15495,86 @@ async function cancel(symbol, cancelId) {
|
|
|
14602
15495
|
const { exchangeName, frameName, strategyName } = backtest$1.methodContextService.context;
|
|
14603
15496
|
await backtest$1.strategyCoreService.cancel(isBacktest, symbol, { exchangeName, frameName, strategyName }, cancelId);
|
|
14604
15497
|
}
|
|
15498
|
+
/**
|
|
15499
|
+
* Executes partial close at profit level (moving toward TP).
|
|
15500
|
+
*
|
|
15501
|
+
* Closes a percentage of the active pending position at profit.
|
|
15502
|
+
* Price must be moving toward take profit (in profit direction).
|
|
15503
|
+
*
|
|
15504
|
+
* Automatically detects backtest/live mode from execution context.
|
|
15505
|
+
*
|
|
15506
|
+
* @param symbol - Trading pair symbol
|
|
15507
|
+
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
15508
|
+
* @returns Promise that resolves when state is updated
|
|
15509
|
+
*
|
|
15510
|
+
* @throws Error if currentPrice is not in profit direction:
|
|
15511
|
+
* - LONG: currentPrice must be > priceOpen
|
|
15512
|
+
* - SHORT: currentPrice must be < priceOpen
|
|
15513
|
+
*
|
|
15514
|
+
* @example
|
|
15515
|
+
* ```typescript
|
|
15516
|
+
* import { partialProfit } from "backtest-kit";
|
|
15517
|
+
*
|
|
15518
|
+
* // Close 30% of LONG position at profit
|
|
15519
|
+
* await partialProfit("BTCUSDT", 30, 45000);
|
|
15520
|
+
* ```
|
|
15521
|
+
*/
|
|
15522
|
+
async function partialProfit(symbol, percentToClose) {
|
|
15523
|
+
backtest$1.loggerService.info(PARTIAL_PROFIT_METHOD_NAME, {
|
|
15524
|
+
symbol,
|
|
15525
|
+
percentToClose,
|
|
15526
|
+
});
|
|
15527
|
+
if (!ExecutionContextService.hasContext()) {
|
|
15528
|
+
throw new Error("partialProfit requires an execution context");
|
|
15529
|
+
}
|
|
15530
|
+
if (!MethodContextService.hasContext()) {
|
|
15531
|
+
throw new Error("partialProfit requires a method context");
|
|
15532
|
+
}
|
|
15533
|
+
const currentPrice = await getAveragePrice(symbol);
|
|
15534
|
+
const { backtest: isBacktest } = backtest$1.executionContextService.context;
|
|
15535
|
+
const { exchangeName, frameName, strategyName } = backtest$1.methodContextService.context;
|
|
15536
|
+
await backtest$1.strategyCoreService.partialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
|
|
15537
|
+
}
|
|
15538
|
+
/**
|
|
15539
|
+
* Executes partial close at loss level (moving toward SL).
|
|
15540
|
+
*
|
|
15541
|
+
* Closes a percentage of the active pending position at loss.
|
|
15542
|
+
* Price must be moving toward stop loss (in loss direction).
|
|
15543
|
+
*
|
|
15544
|
+
* Automatically detects backtest/live mode from execution context.
|
|
15545
|
+
*
|
|
15546
|
+
* @param symbol - Trading pair symbol
|
|
15547
|
+
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
15548
|
+
* @returns Promise that resolves when state is updated
|
|
15549
|
+
*
|
|
15550
|
+
* @throws Error if currentPrice is not in loss direction:
|
|
15551
|
+
* - LONG: currentPrice must be < priceOpen
|
|
15552
|
+
* - SHORT: currentPrice must be > priceOpen
|
|
15553
|
+
*
|
|
15554
|
+
* @example
|
|
15555
|
+
* ```typescript
|
|
15556
|
+
* import { partialLoss } from "backtest-kit";
|
|
15557
|
+
*
|
|
15558
|
+
* // Close 40% of LONG position at loss
|
|
15559
|
+
* await partialLoss("BTCUSDT", 40, 38000);
|
|
15560
|
+
* ```
|
|
15561
|
+
*/
|
|
15562
|
+
async function partialLoss(symbol, percentToClose) {
|
|
15563
|
+
backtest$1.loggerService.info(PARTIAL_LOSS_METHOD_NAME, {
|
|
15564
|
+
symbol,
|
|
15565
|
+
percentToClose,
|
|
15566
|
+
});
|
|
15567
|
+
if (!ExecutionContextService.hasContext()) {
|
|
15568
|
+
throw new Error("partialLoss requires an execution context");
|
|
15569
|
+
}
|
|
15570
|
+
if (!MethodContextService.hasContext()) {
|
|
15571
|
+
throw new Error("partialLoss requires a method context");
|
|
15572
|
+
}
|
|
15573
|
+
const currentPrice = await getAveragePrice(symbol);
|
|
15574
|
+
const { backtest: isBacktest } = backtest$1.executionContextService.context;
|
|
15575
|
+
const { exchangeName, frameName, strategyName } = backtest$1.methodContextService.context;
|
|
15576
|
+
await backtest$1.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
|
|
15577
|
+
}
|
|
14605
15578
|
|
|
14606
15579
|
/**
|
|
14607
15580
|
* Sets custom logger implementation for the framework.
|
|
@@ -16284,258 +17257,70 @@ function listenRisk(fn) {
|
|
|
16284
17257
|
* cancel();
|
|
16285
17258
|
* ```
|
|
16286
17259
|
*/
|
|
16287
|
-
function listenRiskOnce(filterFn, fn) {
|
|
16288
|
-
backtest$1.loggerService.log(LISTEN_RISK_ONCE_METHOD_NAME);
|
|
16289
|
-
return riskSubject.filter(filterFn).once(fn);
|
|
16290
|
-
}
|
|
16291
|
-
/**
|
|
16292
|
-
* Subscribes to ping events during scheduled signal monitoring with queued async processing.
|
|
16293
|
-
*
|
|
16294
|
-
* Events are emitted every minute when a scheduled signal is being monitored (waiting for activation).
|
|
16295
|
-
* Allows tracking of scheduled signal lifecycle and custom monitoring logic.
|
|
16296
|
-
*
|
|
16297
|
-
* @param fn - Callback function to handle ping events
|
|
16298
|
-
* @returns Unsubscribe function to stop listening
|
|
16299
|
-
*
|
|
16300
|
-
* @example
|
|
16301
|
-
* ```typescript
|
|
16302
|
-
* import { listenPing } from "./function/event";
|
|
16303
|
-
*
|
|
16304
|
-
* const unsubscribe = listenPing((event) => {
|
|
16305
|
-
* console.log(`Ping for ${event.symbol} at ${new Date(event.timestamp).toISOString()}`);
|
|
16306
|
-
* console.log(`Strategy: ${event.strategyName}, Exchange: ${event.exchangeName}`);
|
|
16307
|
-
* console.log(`Mode: ${event.backtest ? "Backtest" : "Live"}`);
|
|
16308
|
-
* });
|
|
16309
|
-
*
|
|
16310
|
-
* // Later: stop listening
|
|
16311
|
-
* unsubscribe();
|
|
16312
|
-
* ```
|
|
16313
|
-
*/
|
|
16314
|
-
function listenPing(fn) {
|
|
16315
|
-
backtest$1.loggerService.log(LISTEN_PING_METHOD_NAME);
|
|
16316
|
-
return pingSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
|
|
16317
|
-
}
|
|
16318
|
-
/**
|
|
16319
|
-
* Subscribes to filtered ping events with one-time execution.
|
|
16320
|
-
*
|
|
16321
|
-
* Listens for events matching the filter predicate, then executes callback once
|
|
16322
|
-
* and automatically unsubscribes. Useful for waiting for specific ping conditions.
|
|
16323
|
-
*
|
|
16324
|
-
* @param filterFn - Predicate to filter which events trigger the callback
|
|
16325
|
-
* @param fn - Callback function to handle the filtered event (called only once)
|
|
16326
|
-
* @returns Unsubscribe function to cancel the listener before it fires
|
|
16327
|
-
*
|
|
16328
|
-
* @example
|
|
16329
|
-
* ```typescript
|
|
16330
|
-
* import { listenPingOnce } from "./function/event";
|
|
16331
|
-
*
|
|
16332
|
-
* // Wait for first ping on BTCUSDT
|
|
16333
|
-
* listenPingOnce(
|
|
16334
|
-
* (event) => event.symbol === "BTCUSDT",
|
|
16335
|
-
* (event) => console.log("First BTCUSDT ping received")
|
|
16336
|
-
* );
|
|
16337
|
-
*
|
|
16338
|
-
* // Wait for ping in backtest mode
|
|
16339
|
-
* const cancel = listenPingOnce(
|
|
16340
|
-
* (event) => event.backtest === true,
|
|
16341
|
-
* (event) => console.log("Backtest ping received at", new Date(event.timestamp))
|
|
16342
|
-
* );
|
|
16343
|
-
*
|
|
16344
|
-
* // Cancel if needed before event fires
|
|
16345
|
-
* cancel();
|
|
16346
|
-
* ```
|
|
16347
|
-
*/
|
|
16348
|
-
function listenPingOnce(filterFn, fn) {
|
|
16349
|
-
backtest$1.loggerService.log(LISTEN_PING_ONCE_METHOD_NAME);
|
|
16350
|
-
return pingSubject.filter(filterFn).once(fn);
|
|
16351
|
-
}
|
|
16352
|
-
|
|
16353
|
-
const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
|
|
16354
|
-
const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
|
|
16355
|
-
const FORMAT_PRICE_METHOD_NAME = "exchange.formatPrice";
|
|
16356
|
-
const FORMAT_QUANTITY_METHOD_NAME = "exchange.formatQuantity";
|
|
16357
|
-
const GET_DATE_METHOD_NAME = "exchange.getDate";
|
|
16358
|
-
const GET_MODE_METHOD_NAME = "exchange.getMode";
|
|
16359
|
-
const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
|
|
16360
|
-
/**
|
|
16361
|
-
* Checks if trade context is active (execution and method contexts).
|
|
16362
|
-
*
|
|
16363
|
-
* Returns true when both contexts are active, which is required for calling
|
|
16364
|
-
* exchange functions like getCandles, getAveragePrice, formatPrice, formatQuantity,
|
|
16365
|
-
* getDate, and getMode.
|
|
16366
|
-
*
|
|
16367
|
-
* @returns true if trade context is active, false otherwise
|
|
16368
|
-
*
|
|
16369
|
-
* @example
|
|
16370
|
-
* ```typescript
|
|
16371
|
-
* import { hasTradeContext, getCandles } from "backtest-kit";
|
|
16372
|
-
*
|
|
16373
|
-
* if (hasTradeContext()) {
|
|
16374
|
-
* const candles = await getCandles("BTCUSDT", "1m", 100);
|
|
16375
|
-
* } else {
|
|
16376
|
-
* console.log("Trade context not active");
|
|
16377
|
-
* }
|
|
16378
|
-
* ```
|
|
16379
|
-
*/
|
|
16380
|
-
function hasTradeContext() {
|
|
16381
|
-
backtest$1.loggerService.info(HAS_TRADE_CONTEXT_METHOD_NAME);
|
|
16382
|
-
return ExecutionContextService.hasContext() && MethodContextService.hasContext();
|
|
16383
|
-
}
|
|
16384
|
-
/**
|
|
16385
|
-
* Fetches historical candle data from the registered exchange.
|
|
16386
|
-
*
|
|
16387
|
-
* Candles are fetched backwards from the current execution context time.
|
|
16388
|
-
* Uses the exchange's getCandles implementation.
|
|
16389
|
-
*
|
|
16390
|
-
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
16391
|
-
* @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
|
|
16392
|
-
* @param limit - Number of candles to fetch
|
|
16393
|
-
* @returns Promise resolving to array of candle data
|
|
16394
|
-
*
|
|
16395
|
-
* @example
|
|
16396
|
-
* ```typescript
|
|
16397
|
-
* const candles = await getCandles("BTCUSDT", "1m", 100);
|
|
16398
|
-
* console.log(candles[0]); // { timestamp, open, high, low, close, volume }
|
|
16399
|
-
* ```
|
|
16400
|
-
*/
|
|
16401
|
-
async function getCandles(symbol, interval, limit) {
|
|
16402
|
-
backtest$1.loggerService.info(GET_CANDLES_METHOD_NAME, {
|
|
16403
|
-
symbol,
|
|
16404
|
-
interval,
|
|
16405
|
-
limit,
|
|
16406
|
-
});
|
|
16407
|
-
if (!ExecutionContextService.hasContext()) {
|
|
16408
|
-
throw new Error("getCandles requires an execution context");
|
|
16409
|
-
}
|
|
16410
|
-
if (!MethodContextService.hasContext()) {
|
|
16411
|
-
throw new Error("getCandles requires a method context");
|
|
16412
|
-
}
|
|
16413
|
-
return await backtest$1.exchangeConnectionService.getCandles(symbol, interval, limit);
|
|
16414
|
-
}
|
|
16415
|
-
/**
|
|
16416
|
-
* Calculates VWAP (Volume Weighted Average Price) for a symbol.
|
|
16417
|
-
*
|
|
16418
|
-
* Uses the last 5 1-minute candles to calculate:
|
|
16419
|
-
* - Typical Price = (high + low + close) / 3
|
|
16420
|
-
* - VWAP = sum(typical_price * volume) / sum(volume)
|
|
16421
|
-
*
|
|
16422
|
-
* If volume is zero, returns simple average of close prices.
|
|
16423
|
-
*
|
|
16424
|
-
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
16425
|
-
* @returns Promise resolving to VWAP price
|
|
16426
|
-
*
|
|
16427
|
-
* @example
|
|
16428
|
-
* ```typescript
|
|
16429
|
-
* const vwap = await getAveragePrice("BTCUSDT");
|
|
16430
|
-
* console.log(vwap); // 50125.43
|
|
16431
|
-
* ```
|
|
16432
|
-
*/
|
|
16433
|
-
async function getAveragePrice(symbol) {
|
|
16434
|
-
backtest$1.loggerService.info(GET_AVERAGE_PRICE_METHOD_NAME, {
|
|
16435
|
-
symbol,
|
|
16436
|
-
});
|
|
16437
|
-
if (!ExecutionContextService.hasContext()) {
|
|
16438
|
-
throw new Error("getAveragePrice requires an execution context");
|
|
16439
|
-
}
|
|
16440
|
-
if (!MethodContextService.hasContext()) {
|
|
16441
|
-
throw new Error("getAveragePrice requires a method context");
|
|
16442
|
-
}
|
|
16443
|
-
return await backtest$1.exchangeConnectionService.getAveragePrice(symbol);
|
|
16444
|
-
}
|
|
16445
|
-
/**
|
|
16446
|
-
* Formats a price value according to exchange rules.
|
|
16447
|
-
*
|
|
16448
|
-
* Uses the exchange's formatPrice implementation for proper decimal places.
|
|
16449
|
-
*
|
|
16450
|
-
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
16451
|
-
* @param price - Raw price value
|
|
16452
|
-
* @returns Promise resolving to formatted price string
|
|
16453
|
-
*
|
|
16454
|
-
* @example
|
|
16455
|
-
* ```typescript
|
|
16456
|
-
* const formatted = await formatPrice("BTCUSDT", 50000.123456);
|
|
16457
|
-
* console.log(formatted); // "50000.12"
|
|
16458
|
-
* ```
|
|
16459
|
-
*/
|
|
16460
|
-
async function formatPrice(symbol, price) {
|
|
16461
|
-
backtest$1.loggerService.info(FORMAT_PRICE_METHOD_NAME, {
|
|
16462
|
-
symbol,
|
|
16463
|
-
price,
|
|
16464
|
-
});
|
|
16465
|
-
if (!MethodContextService.hasContext()) {
|
|
16466
|
-
throw new Error("formatPrice requires a method context");
|
|
16467
|
-
}
|
|
16468
|
-
return await backtest$1.exchangeConnectionService.formatPrice(symbol, price);
|
|
16469
|
-
}
|
|
16470
|
-
/**
|
|
16471
|
-
* Formats a quantity value according to exchange rules.
|
|
16472
|
-
*
|
|
16473
|
-
* Uses the exchange's formatQuantity implementation for proper decimal places.
|
|
16474
|
-
*
|
|
16475
|
-
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
16476
|
-
* @param quantity - Raw quantity value
|
|
16477
|
-
* @returns Promise resolving to formatted quantity string
|
|
16478
|
-
*
|
|
16479
|
-
* @example
|
|
16480
|
-
* ```typescript
|
|
16481
|
-
* const formatted = await formatQuantity("BTCUSDT", 0.123456789);
|
|
16482
|
-
* console.log(formatted); // "0.12345678"
|
|
16483
|
-
* ```
|
|
16484
|
-
*/
|
|
16485
|
-
async function formatQuantity(symbol, quantity) {
|
|
16486
|
-
backtest$1.loggerService.info(FORMAT_QUANTITY_METHOD_NAME, {
|
|
16487
|
-
symbol,
|
|
16488
|
-
quantity,
|
|
16489
|
-
});
|
|
16490
|
-
if (!MethodContextService.hasContext()) {
|
|
16491
|
-
throw new Error("formatQuantity requires a method context");
|
|
16492
|
-
}
|
|
16493
|
-
return await backtest$1.exchangeConnectionService.formatQuantity(symbol, quantity);
|
|
17260
|
+
function listenRiskOnce(filterFn, fn) {
|
|
17261
|
+
backtest$1.loggerService.log(LISTEN_RISK_ONCE_METHOD_NAME);
|
|
17262
|
+
return riskSubject.filter(filterFn).once(fn);
|
|
16494
17263
|
}
|
|
16495
17264
|
/**
|
|
16496
|
-
*
|
|
17265
|
+
* Subscribes to ping events during scheduled signal monitoring with queued async processing.
|
|
16497
17266
|
*
|
|
16498
|
-
*
|
|
16499
|
-
*
|
|
17267
|
+
* Events are emitted every minute when a scheduled signal is being monitored (waiting for activation).
|
|
17268
|
+
* Allows tracking of scheduled signal lifecycle and custom monitoring logic.
|
|
16500
17269
|
*
|
|
16501
|
-
* @
|
|
17270
|
+
* @param fn - Callback function to handle ping events
|
|
17271
|
+
* @returns Unsubscribe function to stop listening
|
|
16502
17272
|
*
|
|
16503
17273
|
* @example
|
|
16504
17274
|
* ```typescript
|
|
16505
|
-
*
|
|
16506
|
-
*
|
|
17275
|
+
* import { listenPing } from "./function/event";
|
|
17276
|
+
*
|
|
17277
|
+
* const unsubscribe = listenPing((event) => {
|
|
17278
|
+
* console.log(`Ping for ${event.symbol} at ${new Date(event.timestamp).toISOString()}`);
|
|
17279
|
+
* console.log(`Strategy: ${event.strategyName}, Exchange: ${event.exchangeName}`);
|
|
17280
|
+
* console.log(`Mode: ${event.backtest ? "Backtest" : "Live"}`);
|
|
17281
|
+
* });
|
|
17282
|
+
*
|
|
17283
|
+
* // Later: stop listening
|
|
17284
|
+
* unsubscribe();
|
|
16507
17285
|
* ```
|
|
16508
17286
|
*/
|
|
16509
|
-
|
|
16510
|
-
backtest$1.loggerService.
|
|
16511
|
-
|
|
16512
|
-
throw new Error("getDate requires an execution context");
|
|
16513
|
-
}
|
|
16514
|
-
const { when } = backtest$1.executionContextService.context;
|
|
16515
|
-
return new Date(when.getTime());
|
|
17287
|
+
function listenPing(fn) {
|
|
17288
|
+
backtest$1.loggerService.log(LISTEN_PING_METHOD_NAME);
|
|
17289
|
+
return pingSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
|
|
16516
17290
|
}
|
|
16517
17291
|
/**
|
|
16518
|
-
*
|
|
17292
|
+
* Subscribes to filtered ping events with one-time execution.
|
|
16519
17293
|
*
|
|
16520
|
-
*
|
|
17294
|
+
* Listens for events matching the filter predicate, then executes callback once
|
|
17295
|
+
* and automatically unsubscribes. Useful for waiting for specific ping conditions.
|
|
17296
|
+
*
|
|
17297
|
+
* @param filterFn - Predicate to filter which events trigger the callback
|
|
17298
|
+
* @param fn - Callback function to handle the filtered event (called only once)
|
|
17299
|
+
* @returns Unsubscribe function to cancel the listener before it fires
|
|
16521
17300
|
*
|
|
16522
17301
|
* @example
|
|
16523
17302
|
* ```typescript
|
|
16524
|
-
*
|
|
16525
|
-
*
|
|
16526
|
-
*
|
|
16527
|
-
*
|
|
16528
|
-
*
|
|
16529
|
-
*
|
|
17303
|
+
* import { listenPingOnce } from "./function/event";
|
|
17304
|
+
*
|
|
17305
|
+
* // Wait for first ping on BTCUSDT
|
|
17306
|
+
* listenPingOnce(
|
|
17307
|
+
* (event) => event.symbol === "BTCUSDT",
|
|
17308
|
+
* (event) => console.log("First BTCUSDT ping received")
|
|
17309
|
+
* );
|
|
17310
|
+
*
|
|
17311
|
+
* // Wait for ping in backtest mode
|
|
17312
|
+
* const cancel = listenPingOnce(
|
|
17313
|
+
* (event) => event.backtest === true,
|
|
17314
|
+
* (event) => console.log("Backtest ping received at", new Date(event.timestamp))
|
|
17315
|
+
* );
|
|
17316
|
+
*
|
|
17317
|
+
* // Cancel if needed before event fires
|
|
17318
|
+
* cancel();
|
|
16530
17319
|
* ```
|
|
16531
17320
|
*/
|
|
16532
|
-
|
|
16533
|
-
backtest$1.loggerService.
|
|
16534
|
-
|
|
16535
|
-
throw new Error("getMode requires an execution context");
|
|
16536
|
-
}
|
|
16537
|
-
const { backtest: bt } = backtest$1.executionContextService.context;
|
|
16538
|
-
return bt ? "backtest" : "live";
|
|
17321
|
+
function listenPingOnce(filterFn, fn) {
|
|
17322
|
+
backtest$1.loggerService.log(LISTEN_PING_ONCE_METHOD_NAME);
|
|
17323
|
+
return pingSubject.filter(filterFn).once(fn);
|
|
16539
17324
|
}
|
|
16540
17325
|
|
|
16541
17326
|
const DUMP_SIGNAL_METHOD_NAME = "dump.dumpSignal";
|
|
@@ -16625,6 +17410,8 @@ const BACKTEST_METHOD_NAME_GET_STATUS = "BacktestUtils.getStatus";
|
|
|
16625
17410
|
const BACKTEST_METHOD_NAME_GET_PENDING_SIGNAL = "BacktestUtils.getPendingSignal";
|
|
16626
17411
|
const BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL = "BacktestUtils.getScheduledSignal";
|
|
16627
17412
|
const BACKTEST_METHOD_NAME_CANCEL = "BacktestUtils.cancel";
|
|
17413
|
+
const BACKTEST_METHOD_NAME_PARTIAL_PROFIT = "BacktestUtils.partialProfit";
|
|
17414
|
+
const BACKTEST_METHOD_NAME_PARTIAL_LOSS = "BacktestUtils.partialLoss";
|
|
16628
17415
|
const BACKTEST_METHOD_NAME_GET_DATA = "BacktestUtils.getData";
|
|
16629
17416
|
/**
|
|
16630
17417
|
* Internal task function that runs backtest and handles completion.
|
|
@@ -16651,6 +17438,7 @@ const INSTANCE_TASK_FN$2 = async (symbol, context, self) => {
|
|
|
16651
17438
|
await doneBacktestSubject.next({
|
|
16652
17439
|
exchangeName: context.exchangeName,
|
|
16653
17440
|
strategyName: context.strategyName,
|
|
17441
|
+
frameName: context.frameName,
|
|
16654
17442
|
backtest: true,
|
|
16655
17443
|
symbol,
|
|
16656
17444
|
});
|
|
@@ -16872,6 +17660,7 @@ class BacktestInstance {
|
|
|
16872
17660
|
await doneBacktestSubject.next({
|
|
16873
17661
|
exchangeName: context.exchangeName,
|
|
16874
17662
|
strategyName: context.strategyName,
|
|
17663
|
+
frameName: context.frameName,
|
|
16875
17664
|
backtest: true,
|
|
16876
17665
|
symbol,
|
|
16877
17666
|
});
|
|
@@ -16984,6 +17773,10 @@ class BacktestUtils {
|
|
|
16984
17773
|
* ```
|
|
16985
17774
|
*/
|
|
16986
17775
|
this.getPendingSignal = async (symbol, context) => {
|
|
17776
|
+
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_GET_PENDING_SIGNAL, {
|
|
17777
|
+
symbol,
|
|
17778
|
+
context,
|
|
17779
|
+
});
|
|
16987
17780
|
backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_PENDING_SIGNAL);
|
|
16988
17781
|
{
|
|
16989
17782
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17011,6 +17804,10 @@ class BacktestUtils {
|
|
|
17011
17804
|
* ```
|
|
17012
17805
|
*/
|
|
17013
17806
|
this.getScheduledSignal = async (symbol, context) => {
|
|
17807
|
+
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL, {
|
|
17808
|
+
symbol,
|
|
17809
|
+
context,
|
|
17810
|
+
});
|
|
17014
17811
|
backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL);
|
|
17015
17812
|
{
|
|
17016
17813
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17044,6 +17841,10 @@ class BacktestUtils {
|
|
|
17044
17841
|
* ```
|
|
17045
17842
|
*/
|
|
17046
17843
|
this.stop = async (symbol, context) => {
|
|
17844
|
+
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_STOP, {
|
|
17845
|
+
symbol,
|
|
17846
|
+
context,
|
|
17847
|
+
});
|
|
17047
17848
|
backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_STOP);
|
|
17048
17849
|
{
|
|
17049
17850
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17078,6 +17879,11 @@ class BacktestUtils {
|
|
|
17078
17879
|
* ```
|
|
17079
17880
|
*/
|
|
17080
17881
|
this.cancel = async (symbol, context, cancelId) => {
|
|
17882
|
+
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_CANCEL, {
|
|
17883
|
+
symbol,
|
|
17884
|
+
context,
|
|
17885
|
+
cancelId,
|
|
17886
|
+
});
|
|
17081
17887
|
backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_CANCEL);
|
|
17082
17888
|
{
|
|
17083
17889
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17088,6 +17894,92 @@ class BacktestUtils {
|
|
|
17088
17894
|
}
|
|
17089
17895
|
await backtest$1.strategyCoreService.cancel(true, symbol, context, cancelId);
|
|
17090
17896
|
};
|
|
17897
|
+
/**
|
|
17898
|
+
* Executes partial close at profit level (moving toward TP).
|
|
17899
|
+
*
|
|
17900
|
+
* Closes a percentage of the active pending position at profit.
|
|
17901
|
+
* Price must be moving toward take profit (in profit direction).
|
|
17902
|
+
*
|
|
17903
|
+
* @param symbol - Trading pair symbol
|
|
17904
|
+
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
17905
|
+
* @param currentPrice - Current market price for this partial close
|
|
17906
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
17907
|
+
* @returns Promise that resolves when state is updated
|
|
17908
|
+
*
|
|
17909
|
+
* @throws Error if currentPrice is not in profit direction:
|
|
17910
|
+
* - LONG: currentPrice must be > priceOpen
|
|
17911
|
+
* - SHORT: currentPrice must be < priceOpen
|
|
17912
|
+
*
|
|
17913
|
+
* @example
|
|
17914
|
+
* ```typescript
|
|
17915
|
+
* // Close 30% of LONG position at profit
|
|
17916
|
+
* await Backtest.partialProfit("BTCUSDT", 30, 45000, {
|
|
17917
|
+
* exchangeName: "binance",
|
|
17918
|
+
* frameName: "frame1",
|
|
17919
|
+
* strategyName: "my-strategy"
|
|
17920
|
+
* });
|
|
17921
|
+
* ```
|
|
17922
|
+
*/
|
|
17923
|
+
this.partialProfit = async (symbol, percentToClose, currentPrice, context) => {
|
|
17924
|
+
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_PARTIAL_PROFIT, {
|
|
17925
|
+
symbol,
|
|
17926
|
+
percentToClose,
|
|
17927
|
+
currentPrice,
|
|
17928
|
+
context,
|
|
17929
|
+
});
|
|
17930
|
+
backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT);
|
|
17931
|
+
{
|
|
17932
|
+
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
17933
|
+
riskName &&
|
|
17934
|
+
backtest$1.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT);
|
|
17935
|
+
riskList &&
|
|
17936
|
+
riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT));
|
|
17937
|
+
}
|
|
17938
|
+
await backtest$1.strategyCoreService.partialProfit(true, symbol, percentToClose, currentPrice, context);
|
|
17939
|
+
};
|
|
17940
|
+
/**
|
|
17941
|
+
* Executes partial close at loss level (moving toward SL).
|
|
17942
|
+
*
|
|
17943
|
+
* Closes a percentage of the active pending position at loss.
|
|
17944
|
+
* Price must be moving toward stop loss (in loss direction).
|
|
17945
|
+
*
|
|
17946
|
+
* @param symbol - Trading pair symbol
|
|
17947
|
+
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
17948
|
+
* @param currentPrice - Current market price for this partial close
|
|
17949
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
17950
|
+
* @returns Promise that resolves when state is updated
|
|
17951
|
+
*
|
|
17952
|
+
* @throws Error if currentPrice is not in loss direction:
|
|
17953
|
+
* - LONG: currentPrice must be < priceOpen
|
|
17954
|
+
* - SHORT: currentPrice must be > priceOpen
|
|
17955
|
+
*
|
|
17956
|
+
* @example
|
|
17957
|
+
* ```typescript
|
|
17958
|
+
* // Close 40% of LONG position at loss
|
|
17959
|
+
* await Backtest.partialLoss("BTCUSDT", 40, 38000, {
|
|
17960
|
+
* exchangeName: "binance",
|
|
17961
|
+
* frameName: "frame1",
|
|
17962
|
+
* strategyName: "my-strategy"
|
|
17963
|
+
* });
|
|
17964
|
+
* ```
|
|
17965
|
+
*/
|
|
17966
|
+
this.partialLoss = async (symbol, percentToClose, currentPrice, context) => {
|
|
17967
|
+
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_PARTIAL_LOSS, {
|
|
17968
|
+
symbol,
|
|
17969
|
+
percentToClose,
|
|
17970
|
+
currentPrice,
|
|
17971
|
+
context,
|
|
17972
|
+
});
|
|
17973
|
+
backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_PARTIAL_LOSS);
|
|
17974
|
+
{
|
|
17975
|
+
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
17976
|
+
riskName &&
|
|
17977
|
+
backtest$1.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_LOSS);
|
|
17978
|
+
riskList &&
|
|
17979
|
+
riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_LOSS));
|
|
17980
|
+
}
|
|
17981
|
+
await backtest$1.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
|
|
17982
|
+
};
|
|
17091
17983
|
/**
|
|
17092
17984
|
* Gets statistical data from all closed signals for a symbol-strategy pair.
|
|
17093
17985
|
*
|
|
@@ -17107,6 +17999,10 @@ class BacktestUtils {
|
|
|
17107
17999
|
* ```
|
|
17108
18000
|
*/
|
|
17109
18001
|
this.getData = async (symbol, context) => {
|
|
18002
|
+
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_GET_DATA, {
|
|
18003
|
+
symbol,
|
|
18004
|
+
context,
|
|
18005
|
+
});
|
|
17110
18006
|
backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_DATA);
|
|
17111
18007
|
{
|
|
17112
18008
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17137,6 +18033,10 @@ class BacktestUtils {
|
|
|
17137
18033
|
* ```
|
|
17138
18034
|
*/
|
|
17139
18035
|
this.getReport = async (symbol, context, columns) => {
|
|
18036
|
+
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_GET_REPORT, {
|
|
18037
|
+
symbol,
|
|
18038
|
+
context,
|
|
18039
|
+
});
|
|
17140
18040
|
backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_REPORT);
|
|
17141
18041
|
{
|
|
17142
18042
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17174,6 +18074,11 @@ class BacktestUtils {
|
|
|
17174
18074
|
* ```
|
|
17175
18075
|
*/
|
|
17176
18076
|
this.dump = async (symbol, context, path, columns) => {
|
|
18077
|
+
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_DUMP, {
|
|
18078
|
+
symbol,
|
|
18079
|
+
context,
|
|
18080
|
+
path,
|
|
18081
|
+
});
|
|
17177
18082
|
backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_DUMP);
|
|
17178
18083
|
{
|
|
17179
18084
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17234,6 +18139,8 @@ const LIVE_METHOD_NAME_GET_STATUS = "LiveUtils.getStatus";
|
|
|
17234
18139
|
const LIVE_METHOD_NAME_GET_PENDING_SIGNAL = "LiveUtils.getPendingSignal";
|
|
17235
18140
|
const LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL = "LiveUtils.getScheduledSignal";
|
|
17236
18141
|
const LIVE_METHOD_NAME_CANCEL = "LiveUtils.cancel";
|
|
18142
|
+
const LIVE_METHOD_NAME_PARTIAL_PROFIT = "LiveUtils.partialProfit";
|
|
18143
|
+
const LIVE_METHOD_NAME_PARTIAL_LOSS = "LiveUtils.partialLoss";
|
|
17237
18144
|
/**
|
|
17238
18145
|
* Internal task function that runs live trading and handles completion.
|
|
17239
18146
|
* Consumes live trading results and updates instance state flags.
|
|
@@ -17259,6 +18166,7 @@ const INSTANCE_TASK_FN$1 = async (symbol, context, self) => {
|
|
|
17259
18166
|
await doneLiveSubject.next({
|
|
17260
18167
|
exchangeName: context.exchangeName,
|
|
17261
18168
|
strategyName: context.strategyName,
|
|
18169
|
+
frameName: "",
|
|
17262
18170
|
backtest: false,
|
|
17263
18171
|
symbol,
|
|
17264
18172
|
});
|
|
@@ -17445,6 +18353,7 @@ class LiveInstance {
|
|
|
17445
18353
|
await doneLiveSubject.next({
|
|
17446
18354
|
exchangeName: context.exchangeName,
|
|
17447
18355
|
strategyName: context.strategyName,
|
|
18356
|
+
frameName: "",
|
|
17448
18357
|
backtest: false,
|
|
17449
18358
|
symbol,
|
|
17450
18359
|
});
|
|
@@ -17564,6 +18473,10 @@ class LiveUtils {
|
|
|
17564
18473
|
* ```
|
|
17565
18474
|
*/
|
|
17566
18475
|
this.getPendingSignal = async (symbol, context) => {
|
|
18476
|
+
backtest$1.loggerService.info(LIVE_METHOD_NAME_GET_PENDING_SIGNAL, {
|
|
18477
|
+
symbol,
|
|
18478
|
+
context,
|
|
18479
|
+
});
|
|
17567
18480
|
backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_PENDING_SIGNAL);
|
|
17568
18481
|
{
|
|
17569
18482
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17593,6 +18506,10 @@ class LiveUtils {
|
|
|
17593
18506
|
* ```
|
|
17594
18507
|
*/
|
|
17595
18508
|
this.getScheduledSignal = async (symbol, context) => {
|
|
18509
|
+
backtest$1.loggerService.info(LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL, {
|
|
18510
|
+
symbol,
|
|
18511
|
+
context,
|
|
18512
|
+
});
|
|
17596
18513
|
backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL);
|
|
17597
18514
|
{
|
|
17598
18515
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17623,6 +18540,10 @@ class LiveUtils {
|
|
|
17623
18540
|
* ```
|
|
17624
18541
|
*/
|
|
17625
18542
|
this.stop = async (symbol, context) => {
|
|
18543
|
+
backtest$1.loggerService.info(LIVE_METHOD_NAME_STOP, {
|
|
18544
|
+
symbol,
|
|
18545
|
+
context,
|
|
18546
|
+
});
|
|
17626
18547
|
backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_STOP);
|
|
17627
18548
|
{
|
|
17628
18549
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17659,6 +18580,11 @@ class LiveUtils {
|
|
|
17659
18580
|
* ```
|
|
17660
18581
|
*/
|
|
17661
18582
|
this.cancel = async (symbol, context, cancelId) => {
|
|
18583
|
+
backtest$1.loggerService.info(LIVE_METHOD_NAME_CANCEL, {
|
|
18584
|
+
symbol,
|
|
18585
|
+
context,
|
|
18586
|
+
cancelId,
|
|
18587
|
+
});
|
|
17662
18588
|
backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_CANCEL);
|
|
17663
18589
|
{
|
|
17664
18590
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17671,6 +18597,94 @@ class LiveUtils {
|
|
|
17671
18597
|
frameName: "",
|
|
17672
18598
|
}, cancelId);
|
|
17673
18599
|
};
|
|
18600
|
+
/**
|
|
18601
|
+
* Executes partial close at profit level (moving toward TP).
|
|
18602
|
+
*
|
|
18603
|
+
* Closes a percentage of the active pending position at profit.
|
|
18604
|
+
* Price must be moving toward take profit (in profit direction).
|
|
18605
|
+
*
|
|
18606
|
+
* @param symbol - Trading pair symbol
|
|
18607
|
+
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
18608
|
+
* @param currentPrice - Current market price for this partial close
|
|
18609
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
18610
|
+
* @returns Promise that resolves when state is updated
|
|
18611
|
+
*
|
|
18612
|
+
* @throws Error if currentPrice is not in profit direction:
|
|
18613
|
+
* - LONG: currentPrice must be > priceOpen
|
|
18614
|
+
* - SHORT: currentPrice must be < priceOpen
|
|
18615
|
+
*
|
|
18616
|
+
* @example
|
|
18617
|
+
* ```typescript
|
|
18618
|
+
* // Close 30% of LONG position at profit
|
|
18619
|
+
* await Live.partialProfit("BTCUSDT", 30, 45000, {
|
|
18620
|
+
* exchangeName: "binance",
|
|
18621
|
+
* strategyName: "my-strategy"
|
|
18622
|
+
* });
|
|
18623
|
+
* ```
|
|
18624
|
+
*/
|
|
18625
|
+
this.partialProfit = async (symbol, percentToClose, currentPrice, context) => {
|
|
18626
|
+
backtest$1.loggerService.info(LIVE_METHOD_NAME_PARTIAL_PROFIT, {
|
|
18627
|
+
symbol,
|
|
18628
|
+
percentToClose,
|
|
18629
|
+
currentPrice,
|
|
18630
|
+
context,
|
|
18631
|
+
});
|
|
18632
|
+
backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_PARTIAL_PROFIT);
|
|
18633
|
+
{
|
|
18634
|
+
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
18635
|
+
riskName && backtest$1.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT);
|
|
18636
|
+
riskList && riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT));
|
|
18637
|
+
}
|
|
18638
|
+
await backtest$1.strategyCoreService.partialProfit(false, symbol, percentToClose, currentPrice, {
|
|
18639
|
+
strategyName: context.strategyName,
|
|
18640
|
+
exchangeName: context.exchangeName,
|
|
18641
|
+
frameName: "",
|
|
18642
|
+
});
|
|
18643
|
+
};
|
|
18644
|
+
/**
|
|
18645
|
+
* Executes partial close at loss level (moving toward SL).
|
|
18646
|
+
*
|
|
18647
|
+
* Closes a percentage of the active pending position at loss.
|
|
18648
|
+
* Price must be moving toward stop loss (in loss direction).
|
|
18649
|
+
*
|
|
18650
|
+
* @param symbol - Trading pair symbol
|
|
18651
|
+
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
18652
|
+
* @param currentPrice - Current market price for this partial close
|
|
18653
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
18654
|
+
* @returns Promise that resolves when state is updated
|
|
18655
|
+
*
|
|
18656
|
+
* @throws Error if currentPrice is not in loss direction:
|
|
18657
|
+
* - LONG: currentPrice must be < priceOpen
|
|
18658
|
+
* - SHORT: currentPrice must be > priceOpen
|
|
18659
|
+
*
|
|
18660
|
+
* @example
|
|
18661
|
+
* ```typescript
|
|
18662
|
+
* // Close 40% of LONG position at loss
|
|
18663
|
+
* await Live.partialLoss("BTCUSDT", 40, 38000, {
|
|
18664
|
+
* exchangeName: "binance",
|
|
18665
|
+
* strategyName: "my-strategy"
|
|
18666
|
+
* });
|
|
18667
|
+
* ```
|
|
18668
|
+
*/
|
|
18669
|
+
this.partialLoss = async (symbol, percentToClose, currentPrice, context) => {
|
|
18670
|
+
backtest$1.loggerService.info(LIVE_METHOD_NAME_PARTIAL_LOSS, {
|
|
18671
|
+
symbol,
|
|
18672
|
+
percentToClose,
|
|
18673
|
+
currentPrice,
|
|
18674
|
+
context,
|
|
18675
|
+
});
|
|
18676
|
+
backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_PARTIAL_LOSS);
|
|
18677
|
+
{
|
|
18678
|
+
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
18679
|
+
riskName && backtest$1.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS);
|
|
18680
|
+
riskList && riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS));
|
|
18681
|
+
}
|
|
18682
|
+
await backtest$1.strategyCoreService.partialLoss(false, symbol, percentToClose, currentPrice, {
|
|
18683
|
+
strategyName: context.strategyName,
|
|
18684
|
+
exchangeName: context.exchangeName,
|
|
18685
|
+
frameName: "",
|
|
18686
|
+
});
|
|
18687
|
+
};
|
|
17674
18688
|
/**
|
|
17675
18689
|
* Gets statistical data from all live trading events for a symbol-strategy pair.
|
|
17676
18690
|
*
|
|
@@ -17690,7 +18704,11 @@ class LiveUtils {
|
|
|
17690
18704
|
* ```
|
|
17691
18705
|
*/
|
|
17692
18706
|
this.getData = async (symbol, context) => {
|
|
17693
|
-
backtest$1.
|
|
18707
|
+
backtest$1.loggerService.info(LIVE_METHOD_NAME_GET_DATA, {
|
|
18708
|
+
symbol,
|
|
18709
|
+
context,
|
|
18710
|
+
});
|
|
18711
|
+
backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_DATA);
|
|
17694
18712
|
{
|
|
17695
18713
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
17696
18714
|
riskName && backtest$1.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_DATA);
|
|
@@ -17718,6 +18736,10 @@ class LiveUtils {
|
|
|
17718
18736
|
* ```
|
|
17719
18737
|
*/
|
|
17720
18738
|
this.getReport = async (symbol, context, columns) => {
|
|
18739
|
+
backtest$1.loggerService.info(LIVE_METHOD_NAME_GET_REPORT, {
|
|
18740
|
+
symbol,
|
|
18741
|
+
context,
|
|
18742
|
+
});
|
|
17721
18743
|
backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_REPORT);
|
|
17722
18744
|
{
|
|
17723
18745
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17753,6 +18775,11 @@ class LiveUtils {
|
|
|
17753
18775
|
* ```
|
|
17754
18776
|
*/
|
|
17755
18777
|
this.dump = async (symbol, context, path, columns) => {
|
|
18778
|
+
backtest$1.loggerService.info(LIVE_METHOD_NAME_DUMP, {
|
|
18779
|
+
symbol,
|
|
18780
|
+
context,
|
|
18781
|
+
path,
|
|
18782
|
+
});
|
|
17756
18783
|
backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_DUMP);
|
|
17757
18784
|
{
|
|
17758
18785
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -18099,6 +19126,7 @@ const INSTANCE_TASK_FN = async (symbol, context, self) => {
|
|
|
18099
19126
|
await doneWalkerSubject.next({
|
|
18100
19127
|
exchangeName: walkerSchema.exchangeName,
|
|
18101
19128
|
strategyName: context.walkerName,
|
|
19129
|
+
frameName: walkerSchema.frameName,
|
|
18102
19130
|
backtest: true,
|
|
18103
19131
|
symbol,
|
|
18104
19132
|
});
|
|
@@ -18289,6 +19317,7 @@ class WalkerInstance {
|
|
|
18289
19317
|
doneWalkerSubject.next({
|
|
18290
19318
|
exchangeName: walkerSchema.exchangeName,
|
|
18291
19319
|
strategyName: context.walkerName,
|
|
19320
|
+
frameName: walkerSchema.frameName,
|
|
18292
19321
|
backtest: true,
|
|
18293
19322
|
symbol,
|
|
18294
19323
|
});
|
|
@@ -18397,18 +19426,22 @@ class WalkerUtils {
|
|
|
18397
19426
|
* Stop signal is filtered by walkerName to prevent interference.
|
|
18398
19427
|
*
|
|
18399
19428
|
* @param symbol - Trading pair symbol
|
|
18400
|
-
* @param
|
|
19429
|
+
* @param context - Execution context with walker name
|
|
18401
19430
|
* @returns Promise that resolves when all stop flags are set
|
|
18402
19431
|
*
|
|
18403
19432
|
* @example
|
|
18404
19433
|
* ```typescript
|
|
18405
19434
|
* // Stop walker and all its strategies
|
|
18406
|
-
* await Walker.stop("BTCUSDT", "my-walker");
|
|
19435
|
+
* await Walker.stop("BTCUSDT", { walkerName: "my-walker" });
|
|
18407
19436
|
* ```
|
|
18408
19437
|
*/
|
|
18409
|
-
this.stop = async (symbol,
|
|
18410
|
-
backtest$1.
|
|
18411
|
-
|
|
19438
|
+
this.stop = async (symbol, context) => {
|
|
19439
|
+
backtest$1.loggerService.info(WALKER_METHOD_NAME_STOP, {
|
|
19440
|
+
symbol,
|
|
19441
|
+
context,
|
|
19442
|
+
});
|
|
19443
|
+
backtest$1.walkerValidationService.validate(context.walkerName, WALKER_METHOD_NAME_STOP);
|
|
19444
|
+
const walkerSchema = backtest$1.walkerSchemaService.get(context.walkerName);
|
|
18412
19445
|
for (const strategyName of walkerSchema.strategies) {
|
|
18413
19446
|
backtest$1.strategyValidationService.validate(strategyName, WALKER_METHOD_NAME_STOP);
|
|
18414
19447
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(strategyName);
|
|
@@ -18418,7 +19451,7 @@ class WalkerUtils {
|
|
|
18418
19451
|
riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, WALKER_METHOD_NAME_STOP));
|
|
18419
19452
|
}
|
|
18420
19453
|
for (const strategyName of walkerSchema.strategies) {
|
|
18421
|
-
await walkerStopSubject.next({ symbol, strategyName, walkerName });
|
|
19454
|
+
await walkerStopSubject.next({ symbol, strategyName, walkerName: context.walkerName });
|
|
18422
19455
|
await backtest$1.strategyCoreService.stop(true, symbol, {
|
|
18423
19456
|
strategyName,
|
|
18424
19457
|
exchangeName: walkerSchema.exchangeName,
|
|
@@ -18430,18 +19463,22 @@ class WalkerUtils {
|
|
|
18430
19463
|
* Gets walker results data from all strategy comparisons.
|
|
18431
19464
|
*
|
|
18432
19465
|
* @param symbol - Trading symbol
|
|
18433
|
-
* @param
|
|
19466
|
+
* @param context - Execution context with walker name
|
|
18434
19467
|
* @returns Promise resolving to walker results data object
|
|
18435
19468
|
*
|
|
18436
19469
|
* @example
|
|
18437
19470
|
* ```typescript
|
|
18438
|
-
* const results = await Walker.getData("BTCUSDT", "my-walker");
|
|
19471
|
+
* const results = await Walker.getData("BTCUSDT", { walkerName: "my-walker" });
|
|
18439
19472
|
* console.log(results.bestStrategy, results.bestMetric);
|
|
18440
19473
|
* ```
|
|
18441
19474
|
*/
|
|
18442
|
-
this.getData = async (symbol,
|
|
18443
|
-
backtest$1.
|
|
18444
|
-
|
|
19475
|
+
this.getData = async (symbol, context) => {
|
|
19476
|
+
backtest$1.loggerService.info(WALKER_METHOD_NAME_GET_DATA, {
|
|
19477
|
+
symbol,
|
|
19478
|
+
context,
|
|
19479
|
+
});
|
|
19480
|
+
backtest$1.walkerValidationService.validate(context.walkerName, WALKER_METHOD_NAME_GET_DATA);
|
|
19481
|
+
const walkerSchema = backtest$1.walkerSchemaService.get(context.walkerName);
|
|
18445
19482
|
for (const strategyName of walkerSchema.strategies) {
|
|
18446
19483
|
backtest$1.strategyValidationService.validate(strategyName, WALKER_METHOD_NAME_GET_DATA);
|
|
18447
19484
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(strategyName);
|
|
@@ -18450,7 +19487,7 @@ class WalkerUtils {
|
|
|
18450
19487
|
riskList &&
|
|
18451
19488
|
riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, WALKER_METHOD_NAME_GET_DATA));
|
|
18452
19489
|
}
|
|
18453
|
-
return await backtest$1.walkerMarkdownService.getData(walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
|
|
19490
|
+
return await backtest$1.walkerMarkdownService.getData(context.walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
|
|
18454
19491
|
exchangeName: walkerSchema.exchangeName,
|
|
18455
19492
|
frameName: walkerSchema.frameName,
|
|
18456
19493
|
});
|
|
@@ -18459,20 +19496,24 @@ class WalkerUtils {
|
|
|
18459
19496
|
* Generates markdown report with all strategy comparisons for a walker.
|
|
18460
19497
|
*
|
|
18461
19498
|
* @param symbol - Trading symbol
|
|
18462
|
-
* @param
|
|
19499
|
+
* @param context - Execution context with walker name
|
|
18463
19500
|
* @param strategyColumns - Optional strategy columns configuration
|
|
18464
19501
|
* @param pnlColumns - Optional PNL columns configuration
|
|
18465
19502
|
* @returns Promise resolving to markdown formatted report string
|
|
18466
19503
|
*
|
|
18467
19504
|
* @example
|
|
18468
19505
|
* ```typescript
|
|
18469
|
-
* const markdown = await Walker.getReport("BTCUSDT", "my-walker");
|
|
19506
|
+
* const markdown = await Walker.getReport("BTCUSDT", { walkerName: "my-walker" });
|
|
18470
19507
|
* console.log(markdown);
|
|
18471
19508
|
* ```
|
|
18472
19509
|
*/
|
|
18473
|
-
this.getReport = async (symbol,
|
|
18474
|
-
backtest$1.
|
|
18475
|
-
|
|
19510
|
+
this.getReport = async (symbol, context, strategyColumns, pnlColumns) => {
|
|
19511
|
+
backtest$1.loggerService.info(WALKER_METHOD_NAME_GET_REPORT, {
|
|
19512
|
+
symbol,
|
|
19513
|
+
context,
|
|
19514
|
+
});
|
|
19515
|
+
backtest$1.walkerValidationService.validate(context.walkerName, WALKER_METHOD_NAME_GET_REPORT);
|
|
19516
|
+
const walkerSchema = backtest$1.walkerSchemaService.get(context.walkerName);
|
|
18476
19517
|
for (const strategyName of walkerSchema.strategies) {
|
|
18477
19518
|
backtest$1.strategyValidationService.validate(strategyName, WALKER_METHOD_NAME_GET_REPORT);
|
|
18478
19519
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(strategyName);
|
|
@@ -18481,7 +19522,7 @@ class WalkerUtils {
|
|
|
18481
19522
|
riskList &&
|
|
18482
19523
|
riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, WALKER_METHOD_NAME_GET_REPORT));
|
|
18483
19524
|
}
|
|
18484
|
-
return await backtest$1.walkerMarkdownService.getReport(walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
|
|
19525
|
+
return await backtest$1.walkerMarkdownService.getReport(context.walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
|
|
18485
19526
|
exchangeName: walkerSchema.exchangeName,
|
|
18486
19527
|
frameName: walkerSchema.frameName,
|
|
18487
19528
|
}, strategyColumns, pnlColumns);
|
|
@@ -18490,7 +19531,7 @@ class WalkerUtils {
|
|
|
18490
19531
|
* Saves walker report to disk.
|
|
18491
19532
|
*
|
|
18492
19533
|
* @param symbol - Trading symbol
|
|
18493
|
-
* @param
|
|
19534
|
+
* @param context - Execution context with walker name
|
|
18494
19535
|
* @param path - Optional directory path to save report (default: "./dump/walker")
|
|
18495
19536
|
* @param strategyColumns - Optional strategy columns configuration
|
|
18496
19537
|
* @param pnlColumns - Optional PNL columns configuration
|
|
@@ -18498,15 +19539,20 @@ class WalkerUtils {
|
|
|
18498
19539
|
* @example
|
|
18499
19540
|
* ```typescript
|
|
18500
19541
|
* // Save to default path: ./dump/walker/my-walker.md
|
|
18501
|
-
* await Walker.dump("BTCUSDT", "my-walker");
|
|
19542
|
+
* await Walker.dump("BTCUSDT", { walkerName: "my-walker" });
|
|
18502
19543
|
*
|
|
18503
19544
|
* // Save to custom path: ./custom/path/my-walker.md
|
|
18504
|
-
* await Walker.dump("BTCUSDT", "my-walker", "./custom/path");
|
|
19545
|
+
* await Walker.dump("BTCUSDT", { walkerName: "my-walker" }, "./custom/path");
|
|
18505
19546
|
* ```
|
|
18506
19547
|
*/
|
|
18507
|
-
this.dump = async (symbol,
|
|
18508
|
-
backtest$1.
|
|
18509
|
-
|
|
19548
|
+
this.dump = async (symbol, context, path, strategyColumns, pnlColumns) => {
|
|
19549
|
+
backtest$1.loggerService.info(WALKER_METHOD_NAME_DUMP, {
|
|
19550
|
+
symbol,
|
|
19551
|
+
context,
|
|
19552
|
+
path,
|
|
19553
|
+
});
|
|
19554
|
+
backtest$1.walkerValidationService.validate(context.walkerName, WALKER_METHOD_NAME_DUMP);
|
|
19555
|
+
const walkerSchema = backtest$1.walkerSchemaService.get(context.walkerName);
|
|
18510
19556
|
for (const strategyName of walkerSchema.strategies) {
|
|
18511
19557
|
backtest$1.strategyValidationService.validate(strategyName, WALKER_METHOD_NAME_DUMP);
|
|
18512
19558
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(strategyName);
|
|
@@ -18515,7 +19561,7 @@ class WalkerUtils {
|
|
|
18515
19561
|
riskList &&
|
|
18516
19562
|
riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, WALKER_METHOD_NAME_DUMP));
|
|
18517
19563
|
}
|
|
18518
|
-
await backtest$1.walkerMarkdownService.dump(walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
|
|
19564
|
+
await backtest$1.walkerMarkdownService.dump(context.walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
|
|
18519
19565
|
exchangeName: walkerSchema.exchangeName,
|
|
18520
19566
|
frameName: walkerSchema.frameName,
|
|
18521
19567
|
}, path, strategyColumns, pnlColumns);
|
|
@@ -20335,6 +21381,8 @@ exports.listenWalker = listenWalker;
|
|
|
20335
21381
|
exports.listenWalkerComplete = listenWalkerComplete;
|
|
20336
21382
|
exports.listenWalkerOnce = listenWalkerOnce;
|
|
20337
21383
|
exports.listenWalkerProgress = listenWalkerProgress;
|
|
21384
|
+
exports.partialLoss = partialLoss;
|
|
21385
|
+
exports.partialProfit = partialProfit;
|
|
20338
21386
|
exports.setColumns = setColumns;
|
|
20339
21387
|
exports.setConfig = setConfig;
|
|
20340
21388
|
exports.setLogger = setLogger;
|