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.
Files changed (4) hide show
  1. package/build/index.cjs +1625 -542
  2. package/build/index.mjs +1624 -543
  3. package/package.json +2 -1
  4. 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: 50000,
794
- * // ... other signal fields
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
- * 51000 // close price
815
+ * 105 // final close at +5% for remaining 30%
797
816
  * );
798
- * console.log(pnl.pnlPercentage); // e.g., 1.8% (after slippage and fees)
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.risk.checkSignal({
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
- if (self.params.callbacks?.onActive) {
2211
- const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
2212
- self.params.callbacks.onActive(self.params.execution.context.symbol, pendingSignal, currentPrice, self.params.execution.context.backtest);
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
- if (self.params.callbacks?.onSchedule) {
2227
- const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
2228
- self.params.callbacks.onSchedule(self.params.execution.context.symbol, scheduledSignal, currentPrice, self.params.execution.context.backtest);
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
- if (self.params.callbacks?.onCancel) {
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
- if (self.params.callbacks?.onTick) {
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
- if (self.params.callbacks?.onCancel) {
2306
- self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, currentPrice, self.params.execution.context.backtest);
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: self.params.execution.context.when.getTime(),
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
- if (self.params.callbacks?.onTick) {
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.risk.checkSignal({
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.risk.addSignal(self.params.execution.context.symbol, {
2374
- strategyName: self.params.method.context.strategyName,
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
- if (self.params.callbacks?.onTick) {
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
- // Call system onPing callback first (emits to pingSubject)
2399
- 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);
2400
- // Call user onPing callback only if signal is still active (not cancelled, not activated)
2401
- if (self.params.callbacks?.onPing) {
2402
- await self.params.callbacks.onPing(self.params.execution.context.symbol, scheduled, new Date(timestamp), self.params.execution.context.backtest);
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
- await CALL_PING_CALLBACKS_FN(self, scheduled, self.params.execution.context.when.getTime());
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
- if (self.params.callbacks?.onTick) {
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
- if (self.params.callbacks?.onSchedule) {
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
- if (self.params.callbacks?.onTick) {
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
- if (await functoolsKit.not(self.params.risk.checkSignal({
2464
- pendingSignal: signal,
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.risk.addSignal(self.params.execution.context.symbol, {
2475
- strategyName: self.params.method.context.strategyName,
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
- if (self.params.callbacks?.onTick) {
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
- if (self.params.callbacks?.onClose) {
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 self.params.partial.clear(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
2541
- await self.params.risk.removeSignal(self.params.execution.context.symbol, {
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: self.params.execution.context.when.getTime(),
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
- if (self.params.callbacks?.onTick) {
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 self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest, self.params.execution.context.when);
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 self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest, self.params.execution.context.when);
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 self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest, self.params.execution.context.when);
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 self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest, self.params.execution.context.when);
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
- if (self.params.callbacks?.onTick) {
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
- if (self.params.callbacks?.onIdle) {
2639
- self.params.callbacks.onIdle(self.params.execution.context.symbol, currentPrice, self.params.execution.context.backtest);
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
- if (self.params.callbacks?.onTick) {
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
- if (self.params.callbacks?.onCancel) {
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
- if (self.params.callbacks?.onTick) {
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.risk.checkSignal({
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.risk.addSignal(self.params.execution.context.symbol, {
2732
- strategyName: self.params.method.context.strategyName,
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
- if (self.params.callbacks?.onClose) {
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 self.params.partial.clear(self.params.execution.context.symbol, signal, averagePrice, self.params.execution.context.backtest);
2763
- await self.params.risk.removeSignal(self.params.execution.context.symbol, {
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
- if (self.params.callbacks?.onTick) {
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 self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
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 self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
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 self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
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 self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
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
- if (this.params.callbacks?.onCancel) {
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
- if (this.params.callbacks?.onCancel) {
3276
- this.params.callbacks.onCancel(this.params.execution.context.symbol, cancelledSignal, currentPrice, this.params.execution.context.backtest);
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: this.params.execution.context.when.getTime(),
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 (this.params.execution.context.backtest) {
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 (this.params.execution.context.backtest) {
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: { context: { strategyName, exchangeName, frameName } },
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
- * Delegates to ClientStrategy.cancel() which clears the scheduled signal
4069
- * without stopping the strategy or affecting pending signals.
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
- * Note: Cancelled event will be emitted on next tick() call when strategy
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 ctx - Context with strategyName, exchangeName, frameName
4077
- * @param cancelId - Optional cancellation ID for user-initiated cancellations
4078
- * @returns Promise that resolves when scheduled signal is cancelled
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.cancel = async (backtest, symbol, context, cancelId) => {
4081
- this.loggerService.log("strategyConnectionService cancel", {
4686
+ this.partialLoss = async (backtest, symbol, percentToClose, currentPrice, context) => {
4687
+ this.loggerService.log("strategyConnectionService partialLoss", {
4082
4688
  symbol,
4083
4689
  context,
4084
- cancelId,
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.cancel(symbol, cancelId);
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 pair.
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 strategyName - Name of the strategy to validate
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, strategyName]) => `${symbol}:${strategyName}`, async (symbol, strategyName) => {
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
- strategyName,
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.strategyName);
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.strategyName);
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.strategyName);
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.strategyName);
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.strategyName);
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.strategyName);
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.strategyName);
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, payload.strategyName);
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 instance.
5978
+ * Memoized to avoid redundant validations for the same risk-exchange-frame combination.
5291
5979
  * Logs validation activity.
5292
- * @param riskName - Name of the risk instance to validate
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(([riskName]) => `${riskName}`, async (riskName) => {
5983
+ this.validate = functoolsKit.memoize(([payload]) => `${payload.riskName}:${payload.exchangeName}:${payload.frameName}`, async (payload) => {
5296
5984
  this.loggerService.log("riskGlobalService validate", {
5297
- riskName,
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.riskName);
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.riskName);
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.riskName);
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.riskName);
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, "%+.2f%%") : "N/A",
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, "%.2f") : "N/A",
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, "%.2f") : "N/A",
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, "%+.2f%%") : "N/A",
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, "%.1f%%") : "N/A",
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, "%+.2f%%") : "N/A",
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, "%+.2f%%") : "N/A",
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, "%.2f%%") : "N/A",
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, "%+.2f%%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? functoolsKit.str(data.portfolioSharpeRatio, "%.2f") : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades}`,
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("${escapedSymbol}", results.walkerName);`,
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, event.strategyName);`,
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 += "\\n";`,
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 strategyName - Name of the strategy to validate
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(([strategyName]) => `${strategyName}`, (strategyName, methodName) => {
14083
+ this.validate = functoolsKit.memoize(([context]) => `${context.strategyName}:${context.exchangeName}:${context.frameName}`, (context, methodName) => {
13379
14084
  this.loggerService.log("partialGlobalService validate", {
13380
- strategyName,
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(data.strategyName, "partialGlobalService profit");
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(data.strategyName, "partialGlobalService loss");
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(data.strategyName, "partialGlobalService clear");
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
- * @example
16490
- * ```typescript
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
- async function getDate() {
16496
- backtest$1.loggerService.info(GET_DATE_METHOD_NAME);
16497
- if (!ExecutionContextService.hasContext()) {
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
- * Gets the current execution mode.
17292
+ * Subscribes to filtered ping events with one-time execution.
16505
17293
  *
16506
- * @returns Promise resolving to "backtest" or "live"
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
- * const mode = await getMode();
16511
- * if (mode === "backtest") {
16512
- * console.log("Running in backtest mode");
16513
- * } else {
16514
- * console.log("Running in live mode");
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
- async function getMode() {
16519
- backtest$1.loggerService.info(GET_MODE_METHOD_NAME);
16520
- if (!ExecutionContextService.hasContext()) {
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.strategyValidationService.validate(context.strategyName, "LiveUtils.getData");
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 walkerName - Walker name to stop
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, walkerName) => {
18396
- backtest$1.walkerValidationService.validate(walkerName, WALKER_METHOD_NAME_STOP);
18397
- const walkerSchema = backtest$1.walkerSchemaService.get(walkerName);
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 walkerName - Walker name to get data for
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, walkerName) => {
18429
- backtest$1.walkerValidationService.validate(walkerName, WALKER_METHOD_NAME_GET_DATA);
18430
- const walkerSchema = backtest$1.walkerSchemaService.get(walkerName);
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 walkerName - Walker name to generate report for
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, walkerName, strategyColumns, pnlColumns) => {
18460
- backtest$1.walkerValidationService.validate(walkerName, WALKER_METHOD_NAME_GET_REPORT);
18461
- const walkerSchema = backtest$1.walkerSchemaService.get(walkerName);
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 walkerName - Walker name to save report for
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, walkerName, path, strategyColumns, pnlColumns) => {
18494
- backtest$1.walkerValidationService.validate(walkerName, WALKER_METHOD_NAME_DUMP);
18495
- const walkerSchema = backtest$1.walkerSchemaService.get(walkerName);
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("my-strategy");
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("my-strategy");
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("my-strategy", "./reports");
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 strategyName - Strategy name to get heatmap data for
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("my-strategy", {
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 (strategyName, context, backtest = false) => {
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 strategyName - Strategy name to generate heatmap report for
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("my-strategy", {
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 (strategyName, context, backtest = false, columns) => {
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 strategyName - Strategy name to save heatmap report for
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("my-strategy", {
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("my-strategy", {
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 (strategyName, context, backtest = false, path, columns) => {
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("my-strategy");
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("my-strategy", "./reports");
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;