backtest-kit 1.7.1 → 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 +1625 -542
- package/build/index.mjs +1624 -543
- package/package.json +2 -1
- package/types.d.ts +1061 -392
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,
|
|
@@ -3402,7 +3751,7 @@ class ClientStrategy {
|
|
|
3402
3751
|
* // Existing signal will continue until natural close
|
|
3403
3752
|
* ```
|
|
3404
3753
|
*/
|
|
3405
|
-
async stop(symbol) {
|
|
3754
|
+
async stop(symbol, backtest) {
|
|
3406
3755
|
this.params.logger.debug("ClientStrategy stop", {
|
|
3407
3756
|
symbol,
|
|
3408
3757
|
hasPendingSignal: this._pendingSignal !== null,
|
|
@@ -3414,7 +3763,7 @@ class ClientStrategy {
|
|
|
3414
3763
|
return;
|
|
3415
3764
|
}
|
|
3416
3765
|
this._scheduledSignal = null;
|
|
3417
|
-
if (
|
|
3766
|
+
if (backtest) {
|
|
3418
3767
|
return;
|
|
3419
3768
|
}
|
|
3420
3769
|
await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.method.context.strategyName);
|
|
@@ -3440,12 +3789,10 @@ class ClientStrategy {
|
|
|
3440
3789
|
* // Strategy continues, can generate new signals
|
|
3441
3790
|
* ```
|
|
3442
3791
|
*/
|
|
3443
|
-
async cancel(symbol, cancelId) {
|
|
3792
|
+
async cancel(symbol, backtest, cancelId) {
|
|
3444
3793
|
this.params.logger.debug("ClientStrategy cancel", {
|
|
3445
3794
|
symbol,
|
|
3446
|
-
strategyName: this.params.method.context.strategyName,
|
|
3447
3795
|
hasScheduledSignal: this._scheduledSignal !== null,
|
|
3448
|
-
backtest: this.params.execution.context.backtest,
|
|
3449
3796
|
cancelId,
|
|
3450
3797
|
});
|
|
3451
3798
|
// Save cancelled signal for next tick to emit cancelled event
|
|
@@ -3455,11 +3802,195 @@ class ClientStrategy {
|
|
|
3455
3802
|
});
|
|
3456
3803
|
this._scheduledSignal = null;
|
|
3457
3804
|
}
|
|
3458
|
-
if (
|
|
3805
|
+
if (backtest) {
|
|
3459
3806
|
return;
|
|
3460
3807
|
}
|
|
3461
3808
|
await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.method.context.strategyName);
|
|
3462
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
|
+
}
|
|
3463
3994
|
}
|
|
3464
3995
|
|
|
3465
3996
|
const RISK_METHOD_NAME_GET_DATA = "RiskUtils.getData";
|
|
@@ -3861,6 +4392,7 @@ class StrategyConnectionService {
|
|
|
3861
4392
|
constructor() {
|
|
3862
4393
|
this.loggerService = inject(TYPES.loggerService);
|
|
3863
4394
|
this.executionContextService = inject(TYPES.executionContextService);
|
|
4395
|
+
this.methodContextService = inject(TYPES.methodContextService);
|
|
3864
4396
|
this.strategySchemaService = inject(TYPES.strategySchemaService);
|
|
3865
4397
|
this.riskConnectionService = inject(TYPES.riskConnectionService);
|
|
3866
4398
|
this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
|
|
@@ -3884,7 +4416,7 @@ class StrategyConnectionService {
|
|
|
3884
4416
|
symbol,
|
|
3885
4417
|
interval,
|
|
3886
4418
|
execution: this.executionContextService,
|
|
3887
|
-
method:
|
|
4419
|
+
method: this.methodContextService,
|
|
3888
4420
|
logger: this.loggerService,
|
|
3889
4421
|
partial: this.partialConnectionService,
|
|
3890
4422
|
exchange: this.exchangeConnectionService,
|
|
@@ -4040,7 +4572,7 @@ class StrategyConnectionService {
|
|
|
4040
4572
|
context,
|
|
4041
4573
|
});
|
|
4042
4574
|
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
4043
|
-
await strategy.stop(symbol);
|
|
4575
|
+
await strategy.stop(symbol, backtest);
|
|
4044
4576
|
};
|
|
4045
4577
|
/**
|
|
4046
4578
|
* Clears the memoized ClientStrategy instance from cache.
|
|
@@ -4063,28 +4595,104 @@ class StrategyConnectionService {
|
|
|
4063
4595
|
}
|
|
4064
4596
|
};
|
|
4065
4597
|
/**
|
|
4066
|
-
* 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).
|
|
4067
4661
|
*
|
|
4068
|
-
*
|
|
4069
|
-
*
|
|
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.
|
|
4070
4664
|
*
|
|
4071
|
-
*
|
|
4072
|
-
* detects the scheduled signal was cancelled.
|
|
4665
|
+
* Delegates to ClientStrategy.partialLoss() with current execution context.
|
|
4073
4666
|
*
|
|
4074
4667
|
* @param backtest - Whether running in backtest mode
|
|
4075
4668
|
* @param symbol - Trading pair symbol
|
|
4076
|
-
* @param
|
|
4077
|
-
* @param
|
|
4078
|
-
* @
|
|
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
|
+
* ```
|
|
4079
4685
|
*/
|
|
4080
|
-
this.
|
|
4081
|
-
this.loggerService.log("strategyConnectionService
|
|
4686
|
+
this.partialLoss = async (backtest, symbol, percentToClose, currentPrice, context) => {
|
|
4687
|
+
this.loggerService.log("strategyConnectionService partialLoss", {
|
|
4082
4688
|
symbol,
|
|
4083
4689
|
context,
|
|
4084
|
-
|
|
4690
|
+
percentToClose,
|
|
4691
|
+
currentPrice,
|
|
4692
|
+
backtest,
|
|
4085
4693
|
});
|
|
4086
4694
|
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
4087
|
-
await strategy.
|
|
4695
|
+
await strategy.partialLoss(symbol, percentToClose, currentPrice, backtest);
|
|
4088
4696
|
};
|
|
4089
4697
|
}
|
|
4090
4698
|
}
|
|
@@ -5018,19 +5626,19 @@ class StrategyCoreService {
|
|
|
5018
5626
|
/**
|
|
5019
5627
|
* Validates strategy and associated risk configuration.
|
|
5020
5628
|
*
|
|
5021
|
-
* Memoized to avoid redundant validations for the same symbol-strategy
|
|
5629
|
+
* Memoized to avoid redundant validations for the same symbol-strategy-exchange-frame combination.
|
|
5022
5630
|
* Logs validation activity.
|
|
5023
5631
|
* @param symbol - Trading pair symbol
|
|
5024
|
-
* @param
|
|
5632
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
5025
5633
|
* @returns Promise that resolves when validation is complete
|
|
5026
5634
|
*/
|
|
5027
|
-
this.validate = functoolsKit.memoize(([symbol,
|
|
5635
|
+
this.validate = functoolsKit.memoize(([symbol, context]) => `${symbol}:${context.strategyName}:${context.exchangeName}:${context.frameName}`, async (symbol, context) => {
|
|
5028
5636
|
this.loggerService.log(METHOD_NAME_VALIDATE, {
|
|
5029
5637
|
symbol,
|
|
5030
|
-
|
|
5638
|
+
context,
|
|
5031
5639
|
});
|
|
5032
|
-
const { riskName, riskList } = this.strategySchemaService.get(strategyName);
|
|
5033
|
-
this.strategyValidationService.validate(strategyName, METHOD_NAME_VALIDATE);
|
|
5640
|
+
const { riskName, riskList } = this.strategySchemaService.get(context.strategyName);
|
|
5641
|
+
this.strategyValidationService.validate(context.strategyName, METHOD_NAME_VALIDATE);
|
|
5034
5642
|
riskName && this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE);
|
|
5035
5643
|
riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE));
|
|
5036
5644
|
});
|
|
@@ -5049,7 +5657,7 @@ class StrategyCoreService {
|
|
|
5049
5657
|
symbol,
|
|
5050
5658
|
context,
|
|
5051
5659
|
});
|
|
5052
|
-
await this.validate(symbol, context
|
|
5660
|
+
await this.validate(symbol, context);
|
|
5053
5661
|
return await this.strategyConnectionService.getPendingSignal(backtest, symbol, context);
|
|
5054
5662
|
};
|
|
5055
5663
|
/**
|
|
@@ -5067,7 +5675,7 @@ class StrategyCoreService {
|
|
|
5067
5675
|
symbol,
|
|
5068
5676
|
context,
|
|
5069
5677
|
});
|
|
5070
|
-
await this.validate(symbol, context
|
|
5678
|
+
await this.validate(symbol, context);
|
|
5071
5679
|
return await this.strategyConnectionService.getScheduledSignal(backtest, symbol, context);
|
|
5072
5680
|
};
|
|
5073
5681
|
/**
|
|
@@ -5087,7 +5695,7 @@ class StrategyCoreService {
|
|
|
5087
5695
|
context,
|
|
5088
5696
|
backtest,
|
|
5089
5697
|
});
|
|
5090
|
-
await this.validate(symbol, context
|
|
5698
|
+
await this.validate(symbol, context);
|
|
5091
5699
|
return await this.strategyConnectionService.getStopped(backtest, symbol, context);
|
|
5092
5700
|
};
|
|
5093
5701
|
/**
|
|
@@ -5109,7 +5717,7 @@ class StrategyCoreService {
|
|
|
5109
5717
|
backtest,
|
|
5110
5718
|
context,
|
|
5111
5719
|
});
|
|
5112
|
-
await this.validate(symbol, context
|
|
5720
|
+
await this.validate(symbol, context);
|
|
5113
5721
|
return await ExecutionContextService.runInContext(async () => {
|
|
5114
5722
|
return await this.strategyConnectionService.tick(symbol, context);
|
|
5115
5723
|
}, {
|
|
@@ -5139,7 +5747,7 @@ class StrategyCoreService {
|
|
|
5139
5747
|
backtest,
|
|
5140
5748
|
context,
|
|
5141
5749
|
});
|
|
5142
|
-
await this.validate(symbol, context
|
|
5750
|
+
await this.validate(symbol, context);
|
|
5143
5751
|
return await ExecutionContextService.runInContext(async () => {
|
|
5144
5752
|
return await this.strategyConnectionService.backtest(symbol, context, candles);
|
|
5145
5753
|
}, {
|
|
@@ -5165,7 +5773,7 @@ class StrategyCoreService {
|
|
|
5165
5773
|
context,
|
|
5166
5774
|
backtest,
|
|
5167
5775
|
});
|
|
5168
|
-
await this.validate(symbol, context
|
|
5776
|
+
await this.validate(symbol, context);
|
|
5169
5777
|
return await this.strategyConnectionService.stop(backtest, symbol, context);
|
|
5170
5778
|
};
|
|
5171
5779
|
/**
|
|
@@ -5188,7 +5796,7 @@ class StrategyCoreService {
|
|
|
5188
5796
|
backtest,
|
|
5189
5797
|
cancelId,
|
|
5190
5798
|
});
|
|
5191
|
-
await this.validate(symbol, context
|
|
5799
|
+
await this.validate(symbol, context);
|
|
5192
5800
|
return await this.strategyConnectionService.cancel(backtest, symbol, context, cancelId);
|
|
5193
5801
|
};
|
|
5194
5802
|
/**
|
|
@@ -5204,10 +5812,90 @@ class StrategyCoreService {
|
|
|
5204
5812
|
payload,
|
|
5205
5813
|
});
|
|
5206
5814
|
if (payload) {
|
|
5207
|
-
await this.validate(payload.symbol,
|
|
5815
|
+
await this.validate(payload.symbol, {
|
|
5816
|
+
strategyName: payload.strategyName,
|
|
5817
|
+
exchangeName: payload.exchangeName,
|
|
5818
|
+
frameName: payload.frameName
|
|
5819
|
+
});
|
|
5208
5820
|
}
|
|
5209
5821
|
return await this.strategyConnectionService.clear(payload);
|
|
5210
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
|
+
};
|
|
5211
5899
|
}
|
|
5212
5900
|
}
|
|
5213
5901
|
|
|
@@ -5287,16 +5975,16 @@ class RiskGlobalService {
|
|
|
5287
5975
|
this.riskValidationService = inject(TYPES.riskValidationService);
|
|
5288
5976
|
/**
|
|
5289
5977
|
* Validates risk configuration.
|
|
5290
|
-
* Memoized to avoid redundant validations for the same risk
|
|
5978
|
+
* Memoized to avoid redundant validations for the same risk-exchange-frame combination.
|
|
5291
5979
|
* Logs validation activity.
|
|
5292
|
-
* @param
|
|
5980
|
+
* @param payload - Payload with riskName, exchangeName and frameName
|
|
5293
5981
|
* @returns Promise that resolves when validation is complete
|
|
5294
5982
|
*/
|
|
5295
|
-
this.validate = functoolsKit.memoize(([
|
|
5983
|
+
this.validate = functoolsKit.memoize(([payload]) => `${payload.riskName}:${payload.exchangeName}:${payload.frameName}`, async (payload) => {
|
|
5296
5984
|
this.loggerService.log("riskGlobalService validate", {
|
|
5297
|
-
|
|
5985
|
+
payload,
|
|
5298
5986
|
});
|
|
5299
|
-
this.riskValidationService.validate(riskName, "riskGlobalService validate");
|
|
5987
|
+
this.riskValidationService.validate(payload.riskName, "riskGlobalService validate");
|
|
5300
5988
|
});
|
|
5301
5989
|
/**
|
|
5302
5990
|
* Checks if a signal should be allowed based on risk limits.
|
|
@@ -5310,7 +5998,7 @@ class RiskGlobalService {
|
|
|
5310
5998
|
symbol: params.symbol,
|
|
5311
5999
|
payload,
|
|
5312
6000
|
});
|
|
5313
|
-
await this.validate(payload
|
|
6001
|
+
await this.validate(payload);
|
|
5314
6002
|
return await this.riskConnectionService.checkSignal(params, payload);
|
|
5315
6003
|
};
|
|
5316
6004
|
/**
|
|
@@ -5324,7 +6012,7 @@ class RiskGlobalService {
|
|
|
5324
6012
|
symbol,
|
|
5325
6013
|
payload,
|
|
5326
6014
|
});
|
|
5327
|
-
await this.validate(payload
|
|
6015
|
+
await this.validate(payload);
|
|
5328
6016
|
await this.riskConnectionService.addSignal(symbol, payload);
|
|
5329
6017
|
};
|
|
5330
6018
|
/**
|
|
@@ -5338,7 +6026,7 @@ class RiskGlobalService {
|
|
|
5338
6026
|
symbol,
|
|
5339
6027
|
payload,
|
|
5340
6028
|
});
|
|
5341
|
-
await this.validate(payload
|
|
6029
|
+
await this.validate(payload);
|
|
5342
6030
|
await this.riskConnectionService.removeSignal(symbol, payload);
|
|
5343
6031
|
};
|
|
5344
6032
|
/**
|
|
@@ -5352,7 +6040,7 @@ class RiskGlobalService {
|
|
|
5352
6040
|
payload,
|
|
5353
6041
|
});
|
|
5354
6042
|
if (payload) {
|
|
5355
|
-
await this.validate(payload
|
|
6043
|
+
await this.validate(payload);
|
|
5356
6044
|
}
|
|
5357
6045
|
return await this.riskConnectionService.clear(payload);
|
|
5358
6046
|
};
|
|
@@ -6930,6 +7618,21 @@ const backtest_columns = [
|
|
|
6930
7618
|
},
|
|
6931
7619
|
isVisible: () => true,
|
|
6932
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
|
+
},
|
|
6933
7636
|
{
|
|
6934
7637
|
key: "closeReason",
|
|
6935
7638
|
label: "Close Reason",
|
|
@@ -7005,49 +7708,49 @@ const heat_columns = [
|
|
|
7005
7708
|
{
|
|
7006
7709
|
key: "totalPnl",
|
|
7007
7710
|
label: "Total PNL",
|
|
7008
|
-
format: (data) => data.totalPnl !== null ? functoolsKit.str(data.totalPnl, "
|
|
7711
|
+
format: (data) => data.totalPnl !== null ? functoolsKit.str(data.totalPnl, "%") : "N/A",
|
|
7009
7712
|
isVisible: () => true,
|
|
7010
7713
|
},
|
|
7011
7714
|
{
|
|
7012
7715
|
key: "sharpeRatio",
|
|
7013
7716
|
label: "Sharpe",
|
|
7014
|
-
format: (data) => data.sharpeRatio !== null ? functoolsKit.str(data.sharpeRatio
|
|
7717
|
+
format: (data) => data.sharpeRatio !== null ? functoolsKit.str(data.sharpeRatio) : "N/A",
|
|
7015
7718
|
isVisible: () => true,
|
|
7016
7719
|
},
|
|
7017
7720
|
{
|
|
7018
7721
|
key: "profitFactor",
|
|
7019
7722
|
label: "PF",
|
|
7020
|
-
format: (data) => data.profitFactor !== null ? functoolsKit.str(data.profitFactor
|
|
7723
|
+
format: (data) => data.profitFactor !== null ? functoolsKit.str(data.profitFactor) : "N/A",
|
|
7021
7724
|
isVisible: () => true,
|
|
7022
7725
|
},
|
|
7023
7726
|
{
|
|
7024
7727
|
key: "expectancy",
|
|
7025
7728
|
label: "Expect",
|
|
7026
|
-
format: (data) => data.expectancy !== null ? functoolsKit.str(data.expectancy, "
|
|
7729
|
+
format: (data) => data.expectancy !== null ? functoolsKit.str(data.expectancy, "%") : "N/A",
|
|
7027
7730
|
isVisible: () => true,
|
|
7028
7731
|
},
|
|
7029
7732
|
{
|
|
7030
7733
|
key: "winRate",
|
|
7031
7734
|
label: "WR",
|
|
7032
|
-
format: (data) => data.winRate !== null ? functoolsKit.str(data.winRate, "
|
|
7735
|
+
format: (data) => data.winRate !== null ? functoolsKit.str(data.winRate, "%") : "N/A",
|
|
7033
7736
|
isVisible: () => true,
|
|
7034
7737
|
},
|
|
7035
7738
|
{
|
|
7036
7739
|
key: "avgWin",
|
|
7037
7740
|
label: "Avg Win",
|
|
7038
|
-
format: (data) => data.avgWin !== null ? functoolsKit.str(data.avgWin, "
|
|
7741
|
+
format: (data) => data.avgWin !== null ? functoolsKit.str(data.avgWin, "%") : "N/A",
|
|
7039
7742
|
isVisible: () => true,
|
|
7040
7743
|
},
|
|
7041
7744
|
{
|
|
7042
7745
|
key: "avgLoss",
|
|
7043
7746
|
label: "Avg Loss",
|
|
7044
|
-
format: (data) => data.avgLoss !== null ? functoolsKit.str(data.avgLoss, "
|
|
7747
|
+
format: (data) => data.avgLoss !== null ? functoolsKit.str(data.avgLoss, "%") : "N/A",
|
|
7045
7748
|
isVisible: () => true,
|
|
7046
7749
|
},
|
|
7047
7750
|
{
|
|
7048
7751
|
key: "maxDrawdown",
|
|
7049
7752
|
label: "Max DD",
|
|
7050
|
-
format: (data) => data.maxDrawdown !== null ? functoolsKit.str(-data.maxDrawdown, "
|
|
7753
|
+
format: (data) => data.maxDrawdown !== null ? functoolsKit.str(-data.maxDrawdown, "%") : "N/A",
|
|
7051
7754
|
isVisible: () => true,
|
|
7052
7755
|
},
|
|
7053
7756
|
{
|
|
@@ -10322,7 +11025,7 @@ class HeatmapStorage {
|
|
|
10322
11025
|
return [
|
|
10323
11026
|
`# Portfolio Heatmap: ${strategyName}`,
|
|
10324
11027
|
"",
|
|
10325
|
-
`**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}`,
|
|
10326
11029
|
"",
|
|
10327
11030
|
table
|
|
10328
11031
|
].join("\n");
|
|
@@ -11426,12 +12129,16 @@ class OptimizerTemplateService {
|
|
|
11426
12129
|
``,
|
|
11427
12130
|
`listenWalkerComplete((results) => {`,
|
|
11428
12131
|
` console.log("Walker completed:", results.bestStrategy);`,
|
|
11429
|
-
` Walker.dump(
|
|
12132
|
+
` Walker.dump(results.symbol, { walkerName: results.walkerName });`,
|
|
11430
12133
|
`});`,
|
|
11431
12134
|
``,
|
|
11432
12135
|
`listenDoneBacktest((event) => {`,
|
|
11433
12136
|
` console.log("Backtest completed:", event.symbol);`,
|
|
11434
|
-
` Backtest.dump(event.symbol,
|
|
12137
|
+
` Backtest.dump(event.symbol, {`,
|
|
12138
|
+
` strategyName: event.strategyName,`,
|
|
12139
|
+
` exchangeName: event.exchangeName,`,
|
|
12140
|
+
` frameName: event.frameName`,
|
|
12141
|
+
` });`,
|
|
11435
12142
|
`});`,
|
|
11436
12143
|
``,
|
|
11437
12144
|
`listenError((error) => {`,
|
|
@@ -11465,12 +12172,10 @@ class OptimizerTemplateService {
|
|
|
11465
12172
|
` }`,
|
|
11466
12173
|
``,
|
|
11467
12174
|
` {`,
|
|
11468
|
-
` let summary = "# Outline Result Summary\\n";`,
|
|
12175
|
+
` let summary = "# Outline Result Summary\\n\\n";`,
|
|
11469
12176
|
``,
|
|
11470
12177
|
` {`,
|
|
11471
|
-
` summary +=
|
|
11472
|
-
` summary += \`**ResultId**: \${resultId}\\n\`;`,
|
|
11473
|
-
` summary += "\\n";`,
|
|
12178
|
+
` summary += \`**ResultId**: \${resultId}\\n\\n\`;`,
|
|
11474
12179
|
` }`,
|
|
11475
12180
|
``,
|
|
11476
12181
|
` if (result) {`,
|
|
@@ -11486,7 +12191,7 @@ class OptimizerTemplateService {
|
|
|
11486
12191
|
` systemMessages.forEach((msg, idx) => {`,
|
|
11487
12192
|
` summary += \`### System Message \${idx + 1}\\n\\n\`;`,
|
|
11488
12193
|
` summary += msg.content;`,
|
|
11489
|
-
` summary += "\\n";`,
|
|
12194
|
+
` summary += "\\n\\n";`,
|
|
11490
12195
|
` });`,
|
|
11491
12196
|
` }`,
|
|
11492
12197
|
``,
|
|
@@ -13370,18 +14075,18 @@ class PartialGlobalService {
|
|
|
13370
14075
|
this.riskValidationService = inject(TYPES.riskValidationService);
|
|
13371
14076
|
/**
|
|
13372
14077
|
* Validates strategy and associated risk configuration.
|
|
13373
|
-
* Memoized to avoid redundant validations for the same strategy.
|
|
14078
|
+
* Memoized to avoid redundant validations for the same strategy-exchange-frame combination.
|
|
13374
14079
|
*
|
|
13375
|
-
* @param
|
|
14080
|
+
* @param context - Context with strategyName, exchangeName and frameName
|
|
13376
14081
|
* @param methodName - Name of the calling method for error tracking
|
|
13377
14082
|
*/
|
|
13378
|
-
this.validate = functoolsKit.memoize(([
|
|
14083
|
+
this.validate = functoolsKit.memoize(([context]) => `${context.strategyName}:${context.exchangeName}:${context.frameName}`, (context, methodName) => {
|
|
13379
14084
|
this.loggerService.log("partialGlobalService validate", {
|
|
13380
|
-
|
|
14085
|
+
context,
|
|
13381
14086
|
methodName,
|
|
13382
14087
|
});
|
|
13383
|
-
this.strategyValidationService.validate(strategyName, methodName);
|
|
13384
|
-
const { riskName, riskList } = this.strategySchemaService.get(strategyName);
|
|
14088
|
+
this.strategyValidationService.validate(context.strategyName, methodName);
|
|
14089
|
+
const { riskName, riskList } = this.strategySchemaService.get(context.strategyName);
|
|
13385
14090
|
riskName && this.riskValidationService.validate(riskName, methodName);
|
|
13386
14091
|
riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, methodName));
|
|
13387
14092
|
});
|
|
@@ -13407,7 +14112,11 @@ class PartialGlobalService {
|
|
|
13407
14112
|
backtest,
|
|
13408
14113
|
when,
|
|
13409
14114
|
});
|
|
13410
|
-
this.validate(
|
|
14115
|
+
this.validate({
|
|
14116
|
+
strategyName: data.strategyName,
|
|
14117
|
+
exchangeName: data.exchangeName,
|
|
14118
|
+
frameName: data.frameName
|
|
14119
|
+
}, "partialGlobalService profit");
|
|
13411
14120
|
return await this.partialConnectionService.profit(symbol, data, currentPrice, revenuePercent, backtest, when);
|
|
13412
14121
|
};
|
|
13413
14122
|
/**
|
|
@@ -13432,7 +14141,11 @@ class PartialGlobalService {
|
|
|
13432
14141
|
backtest,
|
|
13433
14142
|
when,
|
|
13434
14143
|
});
|
|
13435
|
-
this.validate(
|
|
14144
|
+
this.validate({
|
|
14145
|
+
strategyName: data.strategyName,
|
|
14146
|
+
exchangeName: data.exchangeName,
|
|
14147
|
+
frameName: data.frameName
|
|
14148
|
+
}, "partialGlobalService loss");
|
|
13436
14149
|
return await this.partialConnectionService.loss(symbol, data, currentPrice, lossPercent, backtest, when);
|
|
13437
14150
|
};
|
|
13438
14151
|
/**
|
|
@@ -13452,7 +14165,11 @@ class PartialGlobalService {
|
|
|
13452
14165
|
priceClose,
|
|
13453
14166
|
backtest,
|
|
13454
14167
|
});
|
|
13455
|
-
this.validate(
|
|
14168
|
+
this.validate({
|
|
14169
|
+
strategyName: data.strategyName,
|
|
14170
|
+
exchangeName: data.exchangeName,
|
|
14171
|
+
frameName: data.frameName
|
|
14172
|
+
}, "partialGlobalService clear");
|
|
13456
14173
|
return await this.partialConnectionService.clear(symbol, data, priceClose, backtest);
|
|
13457
14174
|
};
|
|
13458
14175
|
}
|
|
@@ -14505,13 +15222,203 @@ const validateInternal = async (args) => {
|
|
|
14505
15222
|
* });
|
|
14506
15223
|
* ```
|
|
14507
15224
|
*/
|
|
14508
|
-
async function validate(args = {}) {
|
|
14509
|
-
backtest$1.loggerService.log(METHOD_NAME);
|
|
14510
|
-
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";
|
|
14511
15416
|
}
|
|
14512
15417
|
|
|
14513
15418
|
const STOP_METHOD_NAME = "strategy.stop";
|
|
14514
15419
|
const CANCEL_METHOD_NAME = "strategy.cancel";
|
|
15420
|
+
const PARTIAL_PROFIT_METHOD_NAME = "strategy.partialProfit";
|
|
15421
|
+
const PARTIAL_LOSS_METHOD_NAME = "strategy.partialLoss";
|
|
14515
15422
|
/**
|
|
14516
15423
|
* Stops the strategy from generating new signals.
|
|
14517
15424
|
*
|
|
@@ -14588,6 +15495,86 @@ async function cancel(symbol, cancelId) {
|
|
|
14588
15495
|
const { exchangeName, frameName, strategyName } = backtest$1.methodContextService.context;
|
|
14589
15496
|
await backtest$1.strategyCoreService.cancel(isBacktest, symbol, { exchangeName, frameName, strategyName }, cancelId);
|
|
14590
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
|
+
}
|
|
14591
15578
|
|
|
14592
15579
|
/**
|
|
14593
15580
|
* Sets custom logger implementation for the framework.
|
|
@@ -16291,237 +17278,49 @@ function listenRiskOnce(filterFn, fn) {
|
|
|
16291
17278
|
* console.log(`Ping for ${event.symbol} at ${new Date(event.timestamp).toISOString()}`);
|
|
16292
17279
|
* console.log(`Strategy: ${event.strategyName}, Exchange: ${event.exchangeName}`);
|
|
16293
17280
|
* console.log(`Mode: ${event.backtest ? "Backtest" : "Live"}`);
|
|
16294
|
-
* });
|
|
16295
|
-
*
|
|
16296
|
-
* // Later: stop listening
|
|
16297
|
-
* unsubscribe();
|
|
16298
|
-
* ```
|
|
16299
|
-
*/
|
|
16300
|
-
function listenPing(fn) {
|
|
16301
|
-
backtest$1.loggerService.log(LISTEN_PING_METHOD_NAME);
|
|
16302
|
-
return pingSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
|
|
16303
|
-
}
|
|
16304
|
-
/**
|
|
16305
|
-
* Subscribes to filtered ping events with one-time execution.
|
|
16306
|
-
*
|
|
16307
|
-
* Listens for events matching the filter predicate, then executes callback once
|
|
16308
|
-
* and automatically unsubscribes. Useful for waiting for specific ping conditions.
|
|
16309
|
-
*
|
|
16310
|
-
* @param filterFn - Predicate to filter which events trigger the callback
|
|
16311
|
-
* @param fn - Callback function to handle the filtered event (called only once)
|
|
16312
|
-
* @returns Unsubscribe function to cancel the listener before it fires
|
|
16313
|
-
*
|
|
16314
|
-
* @example
|
|
16315
|
-
* ```typescript
|
|
16316
|
-
* import { listenPingOnce } from "./function/event";
|
|
16317
|
-
*
|
|
16318
|
-
* // Wait for first ping on BTCUSDT
|
|
16319
|
-
* listenPingOnce(
|
|
16320
|
-
* (event) => event.symbol === "BTCUSDT",
|
|
16321
|
-
* (event) => console.log("First BTCUSDT ping received")
|
|
16322
|
-
* );
|
|
16323
|
-
*
|
|
16324
|
-
* // Wait for ping in backtest mode
|
|
16325
|
-
* const cancel = listenPingOnce(
|
|
16326
|
-
* (event) => event.backtest === true,
|
|
16327
|
-
* (event) => console.log("Backtest ping received at", new Date(event.timestamp))
|
|
16328
|
-
* );
|
|
16329
|
-
*
|
|
16330
|
-
* // Cancel if needed before event fires
|
|
16331
|
-
* cancel();
|
|
16332
|
-
* ```
|
|
16333
|
-
*/
|
|
16334
|
-
function listenPingOnce(filterFn, fn) {
|
|
16335
|
-
backtest$1.loggerService.log(LISTEN_PING_ONCE_METHOD_NAME);
|
|
16336
|
-
return pingSubject.filter(filterFn).once(fn);
|
|
16337
|
-
}
|
|
16338
|
-
|
|
16339
|
-
const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
|
|
16340
|
-
const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
|
|
16341
|
-
const FORMAT_PRICE_METHOD_NAME = "exchange.formatPrice";
|
|
16342
|
-
const FORMAT_QUANTITY_METHOD_NAME = "exchange.formatQuantity";
|
|
16343
|
-
const GET_DATE_METHOD_NAME = "exchange.getDate";
|
|
16344
|
-
const GET_MODE_METHOD_NAME = "exchange.getMode";
|
|
16345
|
-
const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
|
|
16346
|
-
/**
|
|
16347
|
-
* Checks if trade context is active (execution and method contexts).
|
|
16348
|
-
*
|
|
16349
|
-
* Returns true when both contexts are active, which is required for calling
|
|
16350
|
-
* exchange functions like getCandles, getAveragePrice, formatPrice, formatQuantity,
|
|
16351
|
-
* getDate, and getMode.
|
|
16352
|
-
*
|
|
16353
|
-
* @returns true if trade context is active, false otherwise
|
|
16354
|
-
*
|
|
16355
|
-
* @example
|
|
16356
|
-
* ```typescript
|
|
16357
|
-
* import { hasTradeContext, getCandles } from "backtest-kit";
|
|
16358
|
-
*
|
|
16359
|
-
* if (hasTradeContext()) {
|
|
16360
|
-
* const candles = await getCandles("BTCUSDT", "1m", 100);
|
|
16361
|
-
* } else {
|
|
16362
|
-
* console.log("Trade context not active");
|
|
16363
|
-
* }
|
|
16364
|
-
* ```
|
|
16365
|
-
*/
|
|
16366
|
-
function hasTradeContext() {
|
|
16367
|
-
backtest$1.loggerService.info(HAS_TRADE_CONTEXT_METHOD_NAME);
|
|
16368
|
-
return ExecutionContextService.hasContext() && MethodContextService.hasContext();
|
|
16369
|
-
}
|
|
16370
|
-
/**
|
|
16371
|
-
* Fetches historical candle data from the registered exchange.
|
|
16372
|
-
*
|
|
16373
|
-
* Candles are fetched backwards from the current execution context time.
|
|
16374
|
-
* Uses the exchange's getCandles implementation.
|
|
16375
|
-
*
|
|
16376
|
-
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
16377
|
-
* @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
|
|
16378
|
-
* @param limit - Number of candles to fetch
|
|
16379
|
-
* @returns Promise resolving to array of candle data
|
|
16380
|
-
*
|
|
16381
|
-
* @example
|
|
16382
|
-
* ```typescript
|
|
16383
|
-
* const candles = await getCandles("BTCUSDT", "1m", 100);
|
|
16384
|
-
* console.log(candles[0]); // { timestamp, open, high, low, close, volume }
|
|
16385
|
-
* ```
|
|
16386
|
-
*/
|
|
16387
|
-
async function getCandles(symbol, interval, limit) {
|
|
16388
|
-
backtest$1.loggerService.info(GET_CANDLES_METHOD_NAME, {
|
|
16389
|
-
symbol,
|
|
16390
|
-
interval,
|
|
16391
|
-
limit,
|
|
16392
|
-
});
|
|
16393
|
-
if (!ExecutionContextService.hasContext()) {
|
|
16394
|
-
throw new Error("getCandles requires an execution context");
|
|
16395
|
-
}
|
|
16396
|
-
if (!MethodContextService.hasContext()) {
|
|
16397
|
-
throw new Error("getCandles requires a method context");
|
|
16398
|
-
}
|
|
16399
|
-
return await backtest$1.exchangeConnectionService.getCandles(symbol, interval, limit);
|
|
16400
|
-
}
|
|
16401
|
-
/**
|
|
16402
|
-
* Calculates VWAP (Volume Weighted Average Price) for a symbol.
|
|
16403
|
-
*
|
|
16404
|
-
* Uses the last 5 1-minute candles to calculate:
|
|
16405
|
-
* - Typical Price = (high + low + close) / 3
|
|
16406
|
-
* - VWAP = sum(typical_price * volume) / sum(volume)
|
|
16407
|
-
*
|
|
16408
|
-
* If volume is zero, returns simple average of close prices.
|
|
16409
|
-
*
|
|
16410
|
-
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
16411
|
-
* @returns Promise resolving to VWAP price
|
|
16412
|
-
*
|
|
16413
|
-
* @example
|
|
16414
|
-
* ```typescript
|
|
16415
|
-
* const vwap = await getAveragePrice("BTCUSDT");
|
|
16416
|
-
* console.log(vwap); // 50125.43
|
|
16417
|
-
* ```
|
|
16418
|
-
*/
|
|
16419
|
-
async function getAveragePrice(symbol) {
|
|
16420
|
-
backtest$1.loggerService.info(GET_AVERAGE_PRICE_METHOD_NAME, {
|
|
16421
|
-
symbol,
|
|
16422
|
-
});
|
|
16423
|
-
if (!ExecutionContextService.hasContext()) {
|
|
16424
|
-
throw new Error("getAveragePrice requires an execution context");
|
|
16425
|
-
}
|
|
16426
|
-
if (!MethodContextService.hasContext()) {
|
|
16427
|
-
throw new Error("getAveragePrice requires a method context");
|
|
16428
|
-
}
|
|
16429
|
-
return await backtest$1.exchangeConnectionService.getAveragePrice(symbol);
|
|
16430
|
-
}
|
|
16431
|
-
/**
|
|
16432
|
-
* Formats a price value according to exchange rules.
|
|
16433
|
-
*
|
|
16434
|
-
* Uses the exchange's formatPrice implementation for proper decimal places.
|
|
16435
|
-
*
|
|
16436
|
-
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
16437
|
-
* @param price - Raw price value
|
|
16438
|
-
* @returns Promise resolving to formatted price string
|
|
16439
|
-
*
|
|
16440
|
-
* @example
|
|
16441
|
-
* ```typescript
|
|
16442
|
-
* const formatted = await formatPrice("BTCUSDT", 50000.123456);
|
|
16443
|
-
* console.log(formatted); // "50000.12"
|
|
16444
|
-
* ```
|
|
16445
|
-
*/
|
|
16446
|
-
async function formatPrice(symbol, price) {
|
|
16447
|
-
backtest$1.loggerService.info(FORMAT_PRICE_METHOD_NAME, {
|
|
16448
|
-
symbol,
|
|
16449
|
-
price,
|
|
16450
|
-
});
|
|
16451
|
-
if (!MethodContextService.hasContext()) {
|
|
16452
|
-
throw new Error("formatPrice requires a method context");
|
|
16453
|
-
}
|
|
16454
|
-
return await backtest$1.exchangeConnectionService.formatPrice(symbol, price);
|
|
16455
|
-
}
|
|
16456
|
-
/**
|
|
16457
|
-
* Formats a quantity value according to exchange rules.
|
|
16458
|
-
*
|
|
16459
|
-
* Uses the exchange's formatQuantity implementation for proper decimal places.
|
|
16460
|
-
*
|
|
16461
|
-
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
16462
|
-
* @param quantity - Raw quantity value
|
|
16463
|
-
* @returns Promise resolving to formatted quantity string
|
|
16464
|
-
*
|
|
16465
|
-
* @example
|
|
16466
|
-
* ```typescript
|
|
16467
|
-
* const formatted = await formatQuantity("BTCUSDT", 0.123456789);
|
|
16468
|
-
* console.log(formatted); // "0.12345678"
|
|
16469
|
-
* ```
|
|
16470
|
-
*/
|
|
16471
|
-
async function formatQuantity(symbol, quantity) {
|
|
16472
|
-
backtest$1.loggerService.info(FORMAT_QUANTITY_METHOD_NAME, {
|
|
16473
|
-
symbol,
|
|
16474
|
-
quantity,
|
|
16475
|
-
});
|
|
16476
|
-
if (!MethodContextService.hasContext()) {
|
|
16477
|
-
throw new Error("formatQuantity requires a method context");
|
|
16478
|
-
}
|
|
16479
|
-
return await backtest$1.exchangeConnectionService.formatQuantity(symbol, quantity);
|
|
16480
|
-
}
|
|
16481
|
-
/**
|
|
16482
|
-
* Gets the current date from execution context.
|
|
16483
|
-
*
|
|
16484
|
-
* In backtest mode: returns the current timeframe date being processed
|
|
16485
|
-
* In live mode: returns current real-time date
|
|
16486
|
-
*
|
|
16487
|
-
* @returns Promise resolving to current execution context date
|
|
17281
|
+
* });
|
|
16488
17282
|
*
|
|
16489
|
-
*
|
|
16490
|
-
*
|
|
16491
|
-
* const date = await getDate();
|
|
16492
|
-
* console.log(date); // 2024-01-01T12:00:00.000Z
|
|
17283
|
+
* // Later: stop listening
|
|
17284
|
+
* unsubscribe();
|
|
16493
17285
|
* ```
|
|
16494
17286
|
*/
|
|
16495
|
-
|
|
16496
|
-
backtest$1.loggerService.
|
|
16497
|
-
|
|
16498
|
-
throw new Error("getDate requires an execution context");
|
|
16499
|
-
}
|
|
16500
|
-
const { when } = backtest$1.executionContextService.context;
|
|
16501
|
-
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)));
|
|
16502
17290
|
}
|
|
16503
17291
|
/**
|
|
16504
|
-
*
|
|
17292
|
+
* Subscribes to filtered ping events with one-time execution.
|
|
16505
17293
|
*
|
|
16506
|
-
*
|
|
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
|
|
16507
17300
|
*
|
|
16508
17301
|
* @example
|
|
16509
17302
|
* ```typescript
|
|
16510
|
-
*
|
|
16511
|
-
*
|
|
16512
|
-
*
|
|
16513
|
-
*
|
|
16514
|
-
*
|
|
16515
|
-
*
|
|
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();
|
|
16516
17319
|
* ```
|
|
16517
17320
|
*/
|
|
16518
|
-
|
|
16519
|
-
backtest$1.loggerService.
|
|
16520
|
-
|
|
16521
|
-
throw new Error("getMode requires an execution context");
|
|
16522
|
-
}
|
|
16523
|
-
const { backtest: bt } = backtest$1.executionContextService.context;
|
|
16524
|
-
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);
|
|
16525
17324
|
}
|
|
16526
17325
|
|
|
16527
17326
|
const DUMP_SIGNAL_METHOD_NAME = "dump.dumpSignal";
|
|
@@ -16611,6 +17410,8 @@ const BACKTEST_METHOD_NAME_GET_STATUS = "BacktestUtils.getStatus";
|
|
|
16611
17410
|
const BACKTEST_METHOD_NAME_GET_PENDING_SIGNAL = "BacktestUtils.getPendingSignal";
|
|
16612
17411
|
const BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL = "BacktestUtils.getScheduledSignal";
|
|
16613
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";
|
|
16614
17415
|
const BACKTEST_METHOD_NAME_GET_DATA = "BacktestUtils.getData";
|
|
16615
17416
|
/**
|
|
16616
17417
|
* Internal task function that runs backtest and handles completion.
|
|
@@ -16637,6 +17438,7 @@ const INSTANCE_TASK_FN$2 = async (symbol, context, self) => {
|
|
|
16637
17438
|
await doneBacktestSubject.next({
|
|
16638
17439
|
exchangeName: context.exchangeName,
|
|
16639
17440
|
strategyName: context.strategyName,
|
|
17441
|
+
frameName: context.frameName,
|
|
16640
17442
|
backtest: true,
|
|
16641
17443
|
symbol,
|
|
16642
17444
|
});
|
|
@@ -16858,6 +17660,7 @@ class BacktestInstance {
|
|
|
16858
17660
|
await doneBacktestSubject.next({
|
|
16859
17661
|
exchangeName: context.exchangeName,
|
|
16860
17662
|
strategyName: context.strategyName,
|
|
17663
|
+
frameName: context.frameName,
|
|
16861
17664
|
backtest: true,
|
|
16862
17665
|
symbol,
|
|
16863
17666
|
});
|
|
@@ -16970,6 +17773,10 @@ class BacktestUtils {
|
|
|
16970
17773
|
* ```
|
|
16971
17774
|
*/
|
|
16972
17775
|
this.getPendingSignal = async (symbol, context) => {
|
|
17776
|
+
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_GET_PENDING_SIGNAL, {
|
|
17777
|
+
symbol,
|
|
17778
|
+
context,
|
|
17779
|
+
});
|
|
16973
17780
|
backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_PENDING_SIGNAL);
|
|
16974
17781
|
{
|
|
16975
17782
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -16997,6 +17804,10 @@ class BacktestUtils {
|
|
|
16997
17804
|
* ```
|
|
16998
17805
|
*/
|
|
16999
17806
|
this.getScheduledSignal = async (symbol, context) => {
|
|
17807
|
+
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL, {
|
|
17808
|
+
symbol,
|
|
17809
|
+
context,
|
|
17810
|
+
});
|
|
17000
17811
|
backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL);
|
|
17001
17812
|
{
|
|
17002
17813
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17030,6 +17841,10 @@ class BacktestUtils {
|
|
|
17030
17841
|
* ```
|
|
17031
17842
|
*/
|
|
17032
17843
|
this.stop = async (symbol, context) => {
|
|
17844
|
+
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_STOP, {
|
|
17845
|
+
symbol,
|
|
17846
|
+
context,
|
|
17847
|
+
});
|
|
17033
17848
|
backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_STOP);
|
|
17034
17849
|
{
|
|
17035
17850
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17064,6 +17879,11 @@ class BacktestUtils {
|
|
|
17064
17879
|
* ```
|
|
17065
17880
|
*/
|
|
17066
17881
|
this.cancel = async (symbol, context, cancelId) => {
|
|
17882
|
+
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_CANCEL, {
|
|
17883
|
+
symbol,
|
|
17884
|
+
context,
|
|
17885
|
+
cancelId,
|
|
17886
|
+
});
|
|
17067
17887
|
backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_CANCEL);
|
|
17068
17888
|
{
|
|
17069
17889
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17074,6 +17894,92 @@ class BacktestUtils {
|
|
|
17074
17894
|
}
|
|
17075
17895
|
await backtest$1.strategyCoreService.cancel(true, symbol, context, cancelId);
|
|
17076
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
|
+
};
|
|
17077
17983
|
/**
|
|
17078
17984
|
* Gets statistical data from all closed signals for a symbol-strategy pair.
|
|
17079
17985
|
*
|
|
@@ -17093,6 +17999,10 @@ class BacktestUtils {
|
|
|
17093
17999
|
* ```
|
|
17094
18000
|
*/
|
|
17095
18001
|
this.getData = async (symbol, context) => {
|
|
18002
|
+
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_GET_DATA, {
|
|
18003
|
+
symbol,
|
|
18004
|
+
context,
|
|
18005
|
+
});
|
|
17096
18006
|
backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_DATA);
|
|
17097
18007
|
{
|
|
17098
18008
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17123,6 +18033,10 @@ class BacktestUtils {
|
|
|
17123
18033
|
* ```
|
|
17124
18034
|
*/
|
|
17125
18035
|
this.getReport = async (symbol, context, columns) => {
|
|
18036
|
+
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_GET_REPORT, {
|
|
18037
|
+
symbol,
|
|
18038
|
+
context,
|
|
18039
|
+
});
|
|
17126
18040
|
backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_REPORT);
|
|
17127
18041
|
{
|
|
17128
18042
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17160,6 +18074,11 @@ class BacktestUtils {
|
|
|
17160
18074
|
* ```
|
|
17161
18075
|
*/
|
|
17162
18076
|
this.dump = async (symbol, context, path, columns) => {
|
|
18077
|
+
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_DUMP, {
|
|
18078
|
+
symbol,
|
|
18079
|
+
context,
|
|
18080
|
+
path,
|
|
18081
|
+
});
|
|
17163
18082
|
backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_DUMP);
|
|
17164
18083
|
{
|
|
17165
18084
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17220,6 +18139,8 @@ const LIVE_METHOD_NAME_GET_STATUS = "LiveUtils.getStatus";
|
|
|
17220
18139
|
const LIVE_METHOD_NAME_GET_PENDING_SIGNAL = "LiveUtils.getPendingSignal";
|
|
17221
18140
|
const LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL = "LiveUtils.getScheduledSignal";
|
|
17222
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";
|
|
17223
18144
|
/**
|
|
17224
18145
|
* Internal task function that runs live trading and handles completion.
|
|
17225
18146
|
* Consumes live trading results and updates instance state flags.
|
|
@@ -17245,6 +18166,7 @@ const INSTANCE_TASK_FN$1 = async (symbol, context, self) => {
|
|
|
17245
18166
|
await doneLiveSubject.next({
|
|
17246
18167
|
exchangeName: context.exchangeName,
|
|
17247
18168
|
strategyName: context.strategyName,
|
|
18169
|
+
frameName: "",
|
|
17248
18170
|
backtest: false,
|
|
17249
18171
|
symbol,
|
|
17250
18172
|
});
|
|
@@ -17431,6 +18353,7 @@ class LiveInstance {
|
|
|
17431
18353
|
await doneLiveSubject.next({
|
|
17432
18354
|
exchangeName: context.exchangeName,
|
|
17433
18355
|
strategyName: context.strategyName,
|
|
18356
|
+
frameName: "",
|
|
17434
18357
|
backtest: false,
|
|
17435
18358
|
symbol,
|
|
17436
18359
|
});
|
|
@@ -17550,6 +18473,10 @@ class LiveUtils {
|
|
|
17550
18473
|
* ```
|
|
17551
18474
|
*/
|
|
17552
18475
|
this.getPendingSignal = async (symbol, context) => {
|
|
18476
|
+
backtest$1.loggerService.info(LIVE_METHOD_NAME_GET_PENDING_SIGNAL, {
|
|
18477
|
+
symbol,
|
|
18478
|
+
context,
|
|
18479
|
+
});
|
|
17553
18480
|
backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_PENDING_SIGNAL);
|
|
17554
18481
|
{
|
|
17555
18482
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17579,6 +18506,10 @@ class LiveUtils {
|
|
|
17579
18506
|
* ```
|
|
17580
18507
|
*/
|
|
17581
18508
|
this.getScheduledSignal = async (symbol, context) => {
|
|
18509
|
+
backtest$1.loggerService.info(LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL, {
|
|
18510
|
+
symbol,
|
|
18511
|
+
context,
|
|
18512
|
+
});
|
|
17582
18513
|
backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL);
|
|
17583
18514
|
{
|
|
17584
18515
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17609,6 +18540,10 @@ class LiveUtils {
|
|
|
17609
18540
|
* ```
|
|
17610
18541
|
*/
|
|
17611
18542
|
this.stop = async (symbol, context) => {
|
|
18543
|
+
backtest$1.loggerService.info(LIVE_METHOD_NAME_STOP, {
|
|
18544
|
+
symbol,
|
|
18545
|
+
context,
|
|
18546
|
+
});
|
|
17612
18547
|
backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_STOP);
|
|
17613
18548
|
{
|
|
17614
18549
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17645,6 +18580,11 @@ class LiveUtils {
|
|
|
17645
18580
|
* ```
|
|
17646
18581
|
*/
|
|
17647
18582
|
this.cancel = async (symbol, context, cancelId) => {
|
|
18583
|
+
backtest$1.loggerService.info(LIVE_METHOD_NAME_CANCEL, {
|
|
18584
|
+
symbol,
|
|
18585
|
+
context,
|
|
18586
|
+
cancelId,
|
|
18587
|
+
});
|
|
17648
18588
|
backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_CANCEL);
|
|
17649
18589
|
{
|
|
17650
18590
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17657,6 +18597,94 @@ class LiveUtils {
|
|
|
17657
18597
|
frameName: "",
|
|
17658
18598
|
}, cancelId);
|
|
17659
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
|
+
};
|
|
17660
18688
|
/**
|
|
17661
18689
|
* Gets statistical data from all live trading events for a symbol-strategy pair.
|
|
17662
18690
|
*
|
|
@@ -17676,7 +18704,11 @@ class LiveUtils {
|
|
|
17676
18704
|
* ```
|
|
17677
18705
|
*/
|
|
17678
18706
|
this.getData = async (symbol, context) => {
|
|
17679
|
-
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);
|
|
17680
18712
|
{
|
|
17681
18713
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
17682
18714
|
riskName && backtest$1.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_DATA);
|
|
@@ -17704,6 +18736,10 @@ class LiveUtils {
|
|
|
17704
18736
|
* ```
|
|
17705
18737
|
*/
|
|
17706
18738
|
this.getReport = async (symbol, context, columns) => {
|
|
18739
|
+
backtest$1.loggerService.info(LIVE_METHOD_NAME_GET_REPORT, {
|
|
18740
|
+
symbol,
|
|
18741
|
+
context,
|
|
18742
|
+
});
|
|
17707
18743
|
backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_REPORT);
|
|
17708
18744
|
{
|
|
17709
18745
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -17739,6 +18775,11 @@ class LiveUtils {
|
|
|
17739
18775
|
* ```
|
|
17740
18776
|
*/
|
|
17741
18777
|
this.dump = async (symbol, context, path, columns) => {
|
|
18778
|
+
backtest$1.loggerService.info(LIVE_METHOD_NAME_DUMP, {
|
|
18779
|
+
symbol,
|
|
18780
|
+
context,
|
|
18781
|
+
path,
|
|
18782
|
+
});
|
|
17742
18783
|
backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_DUMP);
|
|
17743
18784
|
{
|
|
17744
18785
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
@@ -18085,6 +19126,7 @@ const INSTANCE_TASK_FN = async (symbol, context, self) => {
|
|
|
18085
19126
|
await doneWalkerSubject.next({
|
|
18086
19127
|
exchangeName: walkerSchema.exchangeName,
|
|
18087
19128
|
strategyName: context.walkerName,
|
|
19129
|
+
frameName: walkerSchema.frameName,
|
|
18088
19130
|
backtest: true,
|
|
18089
19131
|
symbol,
|
|
18090
19132
|
});
|
|
@@ -18275,6 +19317,7 @@ class WalkerInstance {
|
|
|
18275
19317
|
doneWalkerSubject.next({
|
|
18276
19318
|
exchangeName: walkerSchema.exchangeName,
|
|
18277
19319
|
strategyName: context.walkerName,
|
|
19320
|
+
frameName: walkerSchema.frameName,
|
|
18278
19321
|
backtest: true,
|
|
18279
19322
|
symbol,
|
|
18280
19323
|
});
|
|
@@ -18383,18 +19426,22 @@ class WalkerUtils {
|
|
|
18383
19426
|
* Stop signal is filtered by walkerName to prevent interference.
|
|
18384
19427
|
*
|
|
18385
19428
|
* @param symbol - Trading pair symbol
|
|
18386
|
-
* @param
|
|
19429
|
+
* @param context - Execution context with walker name
|
|
18387
19430
|
* @returns Promise that resolves when all stop flags are set
|
|
18388
19431
|
*
|
|
18389
19432
|
* @example
|
|
18390
19433
|
* ```typescript
|
|
18391
19434
|
* // Stop walker and all its strategies
|
|
18392
|
-
* await Walker.stop("BTCUSDT", "my-walker");
|
|
19435
|
+
* await Walker.stop("BTCUSDT", { walkerName: "my-walker" });
|
|
18393
19436
|
* ```
|
|
18394
19437
|
*/
|
|
18395
|
-
this.stop = async (symbol,
|
|
18396
|
-
backtest$1.
|
|
18397
|
-
|
|
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);
|
|
18398
19445
|
for (const strategyName of walkerSchema.strategies) {
|
|
18399
19446
|
backtest$1.strategyValidationService.validate(strategyName, WALKER_METHOD_NAME_STOP);
|
|
18400
19447
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(strategyName);
|
|
@@ -18404,7 +19451,7 @@ class WalkerUtils {
|
|
|
18404
19451
|
riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, WALKER_METHOD_NAME_STOP));
|
|
18405
19452
|
}
|
|
18406
19453
|
for (const strategyName of walkerSchema.strategies) {
|
|
18407
|
-
await walkerStopSubject.next({ symbol, strategyName, walkerName });
|
|
19454
|
+
await walkerStopSubject.next({ symbol, strategyName, walkerName: context.walkerName });
|
|
18408
19455
|
await backtest$1.strategyCoreService.stop(true, symbol, {
|
|
18409
19456
|
strategyName,
|
|
18410
19457
|
exchangeName: walkerSchema.exchangeName,
|
|
@@ -18416,18 +19463,22 @@ class WalkerUtils {
|
|
|
18416
19463
|
* Gets walker results data from all strategy comparisons.
|
|
18417
19464
|
*
|
|
18418
19465
|
* @param symbol - Trading symbol
|
|
18419
|
-
* @param
|
|
19466
|
+
* @param context - Execution context with walker name
|
|
18420
19467
|
* @returns Promise resolving to walker results data object
|
|
18421
19468
|
*
|
|
18422
19469
|
* @example
|
|
18423
19470
|
* ```typescript
|
|
18424
|
-
* const results = await Walker.getData("BTCUSDT", "my-walker");
|
|
19471
|
+
* const results = await Walker.getData("BTCUSDT", { walkerName: "my-walker" });
|
|
18425
19472
|
* console.log(results.bestStrategy, results.bestMetric);
|
|
18426
19473
|
* ```
|
|
18427
19474
|
*/
|
|
18428
|
-
this.getData = async (symbol,
|
|
18429
|
-
backtest$1.
|
|
18430
|
-
|
|
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);
|
|
18431
19482
|
for (const strategyName of walkerSchema.strategies) {
|
|
18432
19483
|
backtest$1.strategyValidationService.validate(strategyName, WALKER_METHOD_NAME_GET_DATA);
|
|
18433
19484
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(strategyName);
|
|
@@ -18436,7 +19487,7 @@ class WalkerUtils {
|
|
|
18436
19487
|
riskList &&
|
|
18437
19488
|
riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, WALKER_METHOD_NAME_GET_DATA));
|
|
18438
19489
|
}
|
|
18439
|
-
return await backtest$1.walkerMarkdownService.getData(walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
|
|
19490
|
+
return await backtest$1.walkerMarkdownService.getData(context.walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
|
|
18440
19491
|
exchangeName: walkerSchema.exchangeName,
|
|
18441
19492
|
frameName: walkerSchema.frameName,
|
|
18442
19493
|
});
|
|
@@ -18445,20 +19496,24 @@ class WalkerUtils {
|
|
|
18445
19496
|
* Generates markdown report with all strategy comparisons for a walker.
|
|
18446
19497
|
*
|
|
18447
19498
|
* @param symbol - Trading symbol
|
|
18448
|
-
* @param
|
|
19499
|
+
* @param context - Execution context with walker name
|
|
18449
19500
|
* @param strategyColumns - Optional strategy columns configuration
|
|
18450
19501
|
* @param pnlColumns - Optional PNL columns configuration
|
|
18451
19502
|
* @returns Promise resolving to markdown formatted report string
|
|
18452
19503
|
*
|
|
18453
19504
|
* @example
|
|
18454
19505
|
* ```typescript
|
|
18455
|
-
* const markdown = await Walker.getReport("BTCUSDT", "my-walker");
|
|
19506
|
+
* const markdown = await Walker.getReport("BTCUSDT", { walkerName: "my-walker" });
|
|
18456
19507
|
* console.log(markdown);
|
|
18457
19508
|
* ```
|
|
18458
19509
|
*/
|
|
18459
|
-
this.getReport = async (symbol,
|
|
18460
|
-
backtest$1.
|
|
18461
|
-
|
|
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);
|
|
18462
19517
|
for (const strategyName of walkerSchema.strategies) {
|
|
18463
19518
|
backtest$1.strategyValidationService.validate(strategyName, WALKER_METHOD_NAME_GET_REPORT);
|
|
18464
19519
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(strategyName);
|
|
@@ -18467,7 +19522,7 @@ class WalkerUtils {
|
|
|
18467
19522
|
riskList &&
|
|
18468
19523
|
riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, WALKER_METHOD_NAME_GET_REPORT));
|
|
18469
19524
|
}
|
|
18470
|
-
return await backtest$1.walkerMarkdownService.getReport(walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
|
|
19525
|
+
return await backtest$1.walkerMarkdownService.getReport(context.walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
|
|
18471
19526
|
exchangeName: walkerSchema.exchangeName,
|
|
18472
19527
|
frameName: walkerSchema.frameName,
|
|
18473
19528
|
}, strategyColumns, pnlColumns);
|
|
@@ -18476,7 +19531,7 @@ class WalkerUtils {
|
|
|
18476
19531
|
* Saves walker report to disk.
|
|
18477
19532
|
*
|
|
18478
19533
|
* @param symbol - Trading symbol
|
|
18479
|
-
* @param
|
|
19534
|
+
* @param context - Execution context with walker name
|
|
18480
19535
|
* @param path - Optional directory path to save report (default: "./dump/walker")
|
|
18481
19536
|
* @param strategyColumns - Optional strategy columns configuration
|
|
18482
19537
|
* @param pnlColumns - Optional PNL columns configuration
|
|
@@ -18484,15 +19539,20 @@ class WalkerUtils {
|
|
|
18484
19539
|
* @example
|
|
18485
19540
|
* ```typescript
|
|
18486
19541
|
* // Save to default path: ./dump/walker/my-walker.md
|
|
18487
|
-
* await Walker.dump("BTCUSDT", "my-walker");
|
|
19542
|
+
* await Walker.dump("BTCUSDT", { walkerName: "my-walker" });
|
|
18488
19543
|
*
|
|
18489
19544
|
* // Save to custom path: ./custom/path/my-walker.md
|
|
18490
|
-
* await Walker.dump("BTCUSDT", "my-walker", "./custom/path");
|
|
19545
|
+
* await Walker.dump("BTCUSDT", { walkerName: "my-walker" }, "./custom/path");
|
|
18491
19546
|
* ```
|
|
18492
19547
|
*/
|
|
18493
|
-
this.dump = async (symbol,
|
|
18494
|
-
backtest$1.
|
|
18495
|
-
|
|
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);
|
|
18496
19556
|
for (const strategyName of walkerSchema.strategies) {
|
|
18497
19557
|
backtest$1.strategyValidationService.validate(strategyName, WALKER_METHOD_NAME_DUMP);
|
|
18498
19558
|
const { riskName, riskList } = backtest$1.strategySchemaService.get(strategyName);
|
|
@@ -18501,7 +19561,7 @@ class WalkerUtils {
|
|
|
18501
19561
|
riskList &&
|
|
18502
19562
|
riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, WALKER_METHOD_NAME_DUMP));
|
|
18503
19563
|
}
|
|
18504
|
-
await backtest$1.walkerMarkdownService.dump(walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
|
|
19564
|
+
await backtest$1.walkerMarkdownService.dump(context.walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
|
|
18505
19565
|
exchangeName: walkerSchema.exchangeName,
|
|
18506
19566
|
frameName: walkerSchema.frameName,
|
|
18507
19567
|
}, path, strategyColumns, pnlColumns);
|
|
@@ -18557,15 +19617,27 @@ const HEAT_METHOD_NAME_DUMP = "HeatUtils.dump";
|
|
|
18557
19617
|
* import { Heat } from "backtest-kit";
|
|
18558
19618
|
*
|
|
18559
19619
|
* // Get raw heatmap data for a strategy
|
|
18560
|
-
* const stats = await Heat.getData(
|
|
19620
|
+
* const stats = await Heat.getData({
|
|
19621
|
+
* strategyName: "my-strategy",
|
|
19622
|
+
* exchangeName: "binance",
|
|
19623
|
+
* frameName: "frame1"
|
|
19624
|
+
* });
|
|
18561
19625
|
* console.log(`Portfolio PNL: ${stats.portfolioTotalPnl}%`);
|
|
18562
19626
|
*
|
|
18563
19627
|
* // Generate markdown report
|
|
18564
|
-
* const markdown = await Heat.getReport(
|
|
19628
|
+
* const markdown = await Heat.getReport({
|
|
19629
|
+
* strategyName: "my-strategy",
|
|
19630
|
+
* exchangeName: "binance",
|
|
19631
|
+
* frameName: "frame1"
|
|
19632
|
+
* });
|
|
18565
19633
|
* console.log(markdown);
|
|
18566
19634
|
*
|
|
18567
19635
|
* // Save to disk
|
|
18568
|
-
* await Heat.dump(
|
|
19636
|
+
* await Heat.dump({
|
|
19637
|
+
* strategyName: "my-strategy",
|
|
19638
|
+
* exchangeName: "binance",
|
|
19639
|
+
* frameName: "frame1"
|
|
19640
|
+
* }, false, "./reports");
|
|
18569
19641
|
* ```
|
|
18570
19642
|
*/
|
|
18571
19643
|
class HeatUtils {
|
|
@@ -18576,14 +19648,14 @@ class HeatUtils {
|
|
|
18576
19648
|
* Returns per-symbol breakdown and portfolio-wide metrics.
|
|
18577
19649
|
* Data is automatically collected from all closed signals for the strategy.
|
|
18578
19650
|
*
|
|
18579
|
-
* @param
|
|
18580
|
-
* @param context - Execution context with exchangeName and frameName
|
|
19651
|
+
* @param context - Execution context with strategyName, exchangeName and frameName
|
|
18581
19652
|
* @param backtest - True if backtest mode, false if live mode (default: false)
|
|
18582
19653
|
* @returns Promise resolving to heatmap statistics object
|
|
18583
19654
|
*
|
|
18584
19655
|
* @example
|
|
18585
19656
|
* ```typescript
|
|
18586
|
-
* const stats = await Heat.getData(
|
|
19657
|
+
* const stats = await Heat.getData({
|
|
19658
|
+
* strategyName: "my-strategy",
|
|
18587
19659
|
* exchangeName: "binance",
|
|
18588
19660
|
* frameName: "frame1"
|
|
18589
19661
|
* });
|
|
@@ -18598,11 +19670,11 @@ class HeatUtils {
|
|
|
18598
19670
|
* });
|
|
18599
19671
|
* ```
|
|
18600
19672
|
*/
|
|
18601
|
-
this.getData = async (
|
|
18602
|
-
backtest$1.loggerService.info(HEAT_METHOD_NAME_GET_DATA, { strategyName });
|
|
18603
|
-
backtest$1.strategyValidationService.validate(strategyName, HEAT_METHOD_NAME_GET_DATA);
|
|
19673
|
+
this.getData = async (context, backtest = false) => {
|
|
19674
|
+
backtest$1.loggerService.info(HEAT_METHOD_NAME_GET_DATA, { strategyName: context.strategyName });
|
|
19675
|
+
backtest$1.strategyValidationService.validate(context.strategyName, HEAT_METHOD_NAME_GET_DATA);
|
|
18604
19676
|
{
|
|
18605
|
-
const { riskName, riskList } = backtest$1.strategySchemaService.get(strategyName);
|
|
19677
|
+
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
18606
19678
|
riskName && backtest$1.riskValidationService.validate(riskName, HEAT_METHOD_NAME_GET_DATA);
|
|
18607
19679
|
riskList && riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, HEAT_METHOD_NAME_GET_DATA));
|
|
18608
19680
|
}
|
|
@@ -18614,15 +19686,15 @@ class HeatUtils {
|
|
|
18614
19686
|
* Table includes: Symbol, Total PNL, Sharpe Ratio, Max Drawdown, Trades.
|
|
18615
19687
|
* Symbols are sorted by Total PNL descending.
|
|
18616
19688
|
*
|
|
18617
|
-
* @param
|
|
18618
|
-
* @param context - Execution context with exchangeName and frameName
|
|
19689
|
+
* @param context - Execution context with strategyName, exchangeName and frameName
|
|
18619
19690
|
* @param backtest - True if backtest mode, false if live mode (default: false)
|
|
18620
19691
|
* @param columns - Optional columns configuration for the report
|
|
18621
19692
|
* @returns Promise resolving to markdown formatted report string
|
|
18622
19693
|
*
|
|
18623
19694
|
* @example
|
|
18624
19695
|
* ```typescript
|
|
18625
|
-
* const markdown = await Heat.getReport(
|
|
19696
|
+
* const markdown = await Heat.getReport({
|
|
19697
|
+
* strategyName: "my-strategy",
|
|
18626
19698
|
* exchangeName: "binance",
|
|
18627
19699
|
* frameName: "frame1"
|
|
18628
19700
|
* });
|
|
@@ -18639,15 +19711,15 @@ class HeatUtils {
|
|
|
18639
19711
|
* // ...
|
|
18640
19712
|
* ```
|
|
18641
19713
|
*/
|
|
18642
|
-
this.getReport = async (
|
|
18643
|
-
backtest$1.loggerService.info(HEAT_METHOD_NAME_GET_REPORT, { strategyName });
|
|
18644
|
-
backtest$1.strategyValidationService.validate(strategyName, HEAT_METHOD_NAME_GET_REPORT);
|
|
19714
|
+
this.getReport = async (context, backtest = false, columns) => {
|
|
19715
|
+
backtest$1.loggerService.info(HEAT_METHOD_NAME_GET_REPORT, { strategyName: context.strategyName });
|
|
19716
|
+
backtest$1.strategyValidationService.validate(context.strategyName, HEAT_METHOD_NAME_GET_REPORT);
|
|
18645
19717
|
{
|
|
18646
|
-
const { riskName, riskList } = backtest$1.strategySchemaService.get(strategyName);
|
|
19718
|
+
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
18647
19719
|
riskName && backtest$1.riskValidationService.validate(riskName, HEAT_METHOD_NAME_GET_REPORT);
|
|
18648
19720
|
riskList && riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, HEAT_METHOD_NAME_GET_REPORT));
|
|
18649
19721
|
}
|
|
18650
|
-
return await backtest$1.heatMarkdownService.getReport(strategyName, context.exchangeName, context.frameName, backtest, columns);
|
|
19722
|
+
return await backtest$1.heatMarkdownService.getReport(context.strategyName, context.exchangeName, context.frameName, backtest, columns);
|
|
18651
19723
|
};
|
|
18652
19724
|
/**
|
|
18653
19725
|
* Saves heatmap report to disk for a strategy.
|
|
@@ -18655,8 +19727,7 @@ class HeatUtils {
|
|
|
18655
19727
|
* Creates directory if it doesn't exist.
|
|
18656
19728
|
* Default filename: {strategyName}.md
|
|
18657
19729
|
*
|
|
18658
|
-
* @param
|
|
18659
|
-
* @param context - Execution context with exchangeName and frameName
|
|
19730
|
+
* @param context - Execution context with strategyName, exchangeName and frameName
|
|
18660
19731
|
* @param backtest - True if backtest mode, false if live mode (default: false)
|
|
18661
19732
|
* @param path - Optional directory path to save report (default: "./dump/heatmap")
|
|
18662
19733
|
* @param columns - Optional columns configuration for the report
|
|
@@ -18664,27 +19735,29 @@ class HeatUtils {
|
|
|
18664
19735
|
* @example
|
|
18665
19736
|
* ```typescript
|
|
18666
19737
|
* // Save to default path: ./dump/heatmap/my-strategy.md
|
|
18667
|
-
* await Heat.dump(
|
|
19738
|
+
* await Heat.dump({
|
|
19739
|
+
* strategyName: "my-strategy",
|
|
18668
19740
|
* exchangeName: "binance",
|
|
18669
19741
|
* frameName: "frame1"
|
|
18670
19742
|
* });
|
|
18671
19743
|
*
|
|
18672
19744
|
* // Save to custom path: ./reports/my-strategy.md
|
|
18673
|
-
* await Heat.dump(
|
|
19745
|
+
* await Heat.dump({
|
|
19746
|
+
* strategyName: "my-strategy",
|
|
18674
19747
|
* exchangeName: "binance",
|
|
18675
19748
|
* frameName: "frame1"
|
|
18676
19749
|
* }, false, "./reports");
|
|
18677
19750
|
* ```
|
|
18678
19751
|
*/
|
|
18679
|
-
this.dump = async (
|
|
18680
|
-
backtest$1.loggerService.info(HEAT_METHOD_NAME_DUMP, { strategyName, path });
|
|
18681
|
-
backtest$1.strategyValidationService.validate(strategyName, HEAT_METHOD_NAME_DUMP);
|
|
19752
|
+
this.dump = async (context, backtest = false, path, columns) => {
|
|
19753
|
+
backtest$1.loggerService.info(HEAT_METHOD_NAME_DUMP, { strategyName: context.strategyName, path });
|
|
19754
|
+
backtest$1.strategyValidationService.validate(context.strategyName, HEAT_METHOD_NAME_DUMP);
|
|
18682
19755
|
{
|
|
18683
|
-
const { riskName, riskList } = backtest$1.strategySchemaService.get(strategyName);
|
|
19756
|
+
const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
18684
19757
|
riskName && backtest$1.riskValidationService.validate(riskName, HEAT_METHOD_NAME_DUMP);
|
|
18685
19758
|
riskList && riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, HEAT_METHOD_NAME_DUMP));
|
|
18686
19759
|
}
|
|
18687
|
-
await backtest$1.heatMarkdownService.dump(strategyName, context.exchangeName, context.frameName, backtest, path, columns);
|
|
19760
|
+
await backtest$1.heatMarkdownService.dump(context.strategyName, context.exchangeName, context.frameName, backtest, path, columns);
|
|
18688
19761
|
};
|
|
18689
19762
|
}
|
|
18690
19763
|
}
|
|
@@ -18696,7 +19769,11 @@ class HeatUtils {
|
|
|
18696
19769
|
* import { Heat } from "backtest-kit";
|
|
18697
19770
|
*
|
|
18698
19771
|
* // Strategy-specific heatmap
|
|
18699
|
-
* const stats = await Heat.getData(
|
|
19772
|
+
* const stats = await Heat.getData({
|
|
19773
|
+
* strategyName: "my-strategy",
|
|
19774
|
+
* exchangeName: "binance",
|
|
19775
|
+
* frameName: "frame1"
|
|
19776
|
+
* });
|
|
18700
19777
|
* console.log(`Portfolio PNL: ${stats.portfolioTotalPnl}%`);
|
|
18701
19778
|
* console.log(`Total Symbols: ${stats.totalSymbols}`);
|
|
18702
19779
|
*
|
|
@@ -18710,7 +19787,11 @@ class HeatUtils {
|
|
|
18710
19787
|
* });
|
|
18711
19788
|
*
|
|
18712
19789
|
* // Generate and save report
|
|
18713
|
-
* await Heat.dump(
|
|
19790
|
+
* await Heat.dump({
|
|
19791
|
+
* strategyName: "my-strategy",
|
|
19792
|
+
* exchangeName: "binance",
|
|
19793
|
+
* frameName: "frame1"
|
|
19794
|
+
* }, false, "./reports");
|
|
18714
19795
|
* ```
|
|
18715
19796
|
*/
|
|
18716
19797
|
const Heat = new HeatUtils();
|
|
@@ -20300,6 +21381,8 @@ exports.listenWalker = listenWalker;
|
|
|
20300
21381
|
exports.listenWalkerComplete = listenWalkerComplete;
|
|
20301
21382
|
exports.listenWalkerOnce = listenWalkerOnce;
|
|
20302
21383
|
exports.listenWalkerProgress = listenWalkerProgress;
|
|
21384
|
+
exports.partialLoss = partialLoss;
|
|
21385
|
+
exports.partialProfit = partialProfit;
|
|
20303
21386
|
exports.setColumns = setColumns;
|
|
20304
21387
|
exports.setConfig = setConfig;
|
|
20305
21388
|
exports.setLogger = setLogger;
|