@backtest-kit/ui 4.0.1 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -11
- package/build/index.cjs +384 -34
- package/build/index.mjs +386 -36
- package/build/modules/frontend/build/assets/Background-BmAP0u-v.js +1 -0
- package/build/modules/frontend/build/assets/{IconPhoto-DuToE4bm.js → IconPhoto-CTW1vRuU.js} +1 -1
- package/build/modules/frontend/build/assets/{KeyboardArrowLeft-D3jtXBuX.js → KeyboardArrowLeft-D-bGIipj.js} +1 -1
- package/build/modules/frontend/build/assets/{Refresh-Dtm02nVv.js → Refresh-Dx7WNRNQ.js} +1 -1
- package/build/modules/frontend/build/assets/{index-Bdi8Xt6Q.js → index-B6tX14ok.js} +12 -12
- package/build/modules/frontend/build/assets/{index-joLw_fAh.js → index-BHg7O3a-.js} +1 -1
- package/build/modules/frontend/build/assets/index-Bf8C3fbL.js +1 -0
- package/build/modules/frontend/build/assets/index-BfcHCLoD.js +1 -0
- package/build/modules/frontend/build/assets/index-Bo1e6VXK.js +1 -0
- package/build/modules/frontend/build/assets/index-C3pFAoIV.js +1 -0
- package/build/modules/frontend/build/assets/index-DKubDtVK.js +1 -0
- package/build/modules/frontend/build/index.html +1 -1
- package/package.json +3 -3
- package/types.d.ts +65 -1
- package/build/modules/frontend/build/assets/index-BjlWg4VP.js +0 -1
- package/build/modules/frontend/build/assets/index-DbhMxTEj.js +0 -1
- package/build/modules/frontend/build/assets/index-XQYcIYy3.js +0 -1
- package/build/modules/frontend/build/assets/index-vEmMqWZV.js +0 -1
- package/build/modules/frontend/build/assets/markdownit-WGWginA6.js +0 -1
package/README.md
CHANGED
|
@@ -67,25 +67,127 @@ setLogger({
|
|
|
67
67
|
|
|
68
68
|
## 📐 Dashboard Revenue Math
|
|
69
69
|
|
|
70
|
-
The **Revenue** metrics on the dashboard are calculated in **dollar terms
|
|
70
|
+
The **Revenue** metrics on the dashboard are calculated in **dollar terms** by summing the `pnlCost` field from all closed signals within each time window.
|
|
71
71
|
|
|
72
72
|
### Dollar PnL formula
|
|
73
73
|
|
|
74
74
|
```
|
|
75
|
-
|
|
75
|
+
revenue[window] = Σ signal.pnl.pnlCost (for all closed signals in that window)
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|:--------------:|-------------------:|-------------------------:|
|
|
80
|
-
| 1 | $100 | +$5.00 |
|
|
81
|
-
| 2 | $200 | +$10.00 |
|
|
82
|
-
| 3 | $300 | +$15.00 |
|
|
78
|
+
`pnlCost` is computed by the backend (`toProfitLossDto`) as:
|
|
83
79
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
```
|
|
81
|
+
pnlCost = (pnlPercentage / 100) × pnlEntries
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
| Field | Source | Description |
|
|
85
|
+
|-------|--------|-------------|
|
|
86
|
+
| `pnl.pnlCost` | `IStorageSignalRow` | Absolute P&L in USD — the only value summed for revenue |
|
|
87
|
+
| `pnl.pnlPercentage` | `IStorageSignalRow` | Percentage P&L (accounts for DCA-weighted entry price, slippage, and fees) |
|
|
88
|
+
| `pnl.pnlEntries` | `IStorageSignalRow` | Total invested capital in USD — sum of all entry costs (`Σ entry.cost`) |
|
|
89
|
+
|
|
90
|
+
**Example** (1 DCA entry at $100, position closed +5%):
|
|
91
|
+
|
|
92
|
+
| DCA entries | `pnlEntries` | `pnlPercentage` | `pnlCost` |
|
|
93
|
+
|:-----------:|-------------:|----------------:|----------:|
|
|
94
|
+
| 1 | $100 | 5 % | +$5.00 |
|
|
95
|
+
| 2 | $200 | 5 % | +$10.00 |
|
|
96
|
+
| 3 | $300 | 5 % | +$15.00 |
|
|
97
|
+
|
|
98
|
+
### Time windows
|
|
99
|
+
|
|
100
|
+
The anchor point depends on execution mode:
|
|
101
|
+
|
|
102
|
+
- **Backtest mode** — latest `updatedAt` across all closed signals (time windows are relative to the end of the run)
|
|
103
|
+
- **Live mode** — `Date.now()` (wall-clock time)
|
|
104
|
+
|
|
105
|
+
| Window | Range |
|
|
106
|
+
|--------|-------|
|
|
107
|
+
| Today | `>= startOf(anchorDay)` |
|
|
108
|
+
| Yesterday | `[anchorDay − 1d, anchorDay)` |
|
|
109
|
+
| 7 days | `>= anchorDay − 7d` |
|
|
110
|
+
| 31 days | `>= anchorDay − 31d` |
|
|
111
|
+
|
|
112
|
+
Revenue and signal count are tracked separately for each window and aggregated across all symbols on the Dashboard.
|
|
113
|
+
|
|
114
|
+
## 📐 Position PNL Math
|
|
115
|
+
|
|
116
|
+
### Effective entry price (DCA-weighted)
|
|
117
|
+
|
|
118
|
+
When multiple DCA entries exist, the effective open price is a **cost-weighted harmonic mean**:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
effectivePrice = Σcost / Σ(cost / price)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
This is the correct formula for fixed-dollar entries (not simple average), because buying $100 worth at different prices gives different coin quantities.
|
|
125
|
+
|
|
126
|
+
### Partial closes (PP/PL)
|
|
127
|
+
|
|
128
|
+
Each partial stores a `costBasisAtClose` snapshot — the running dollar cost-basis **before** that partial fired. This avoids replaying the full entry history on every call.
|
|
129
|
+
|
|
130
|
+
**Cost-basis replay:**
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
for each partial[i]:
|
|
134
|
+
closedDollar += (percent[i] / 100) × costBasisAtClose[i]
|
|
135
|
+
remainingCostBasis = costBasisAtClose[i] × (1 - percent[i] / 100)
|
|
136
|
+
|
|
137
|
+
# DCA entries added AFTER the last partial are appended:
|
|
138
|
+
remainingCostBasis += Σ entry.cost for entries[lastEntryCount..]
|
|
139
|
+
|
|
140
|
+
totalClosedPercent = closedDollar / totalInvested × 100
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Effective price through partials** is computed iteratively so that a partial sell does not change the entry price of the remaining coins:
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
# partial[0]:
|
|
147
|
+
effPrice = costBasisAtClose[0] / Σ(cost/price for entries[0..cnt[0]])
|
|
148
|
+
|
|
149
|
+
# partial[j]:
|
|
150
|
+
remainingCB = prev.costBasisAtClose × (1 - prev.percent / 100)
|
|
151
|
+
oldCoins = remainingCB / effPrice ← coins still held
|
|
152
|
+
newCoins = Σ(cost/price for DCA entries between j-1 and j)
|
|
153
|
+
effPrice = (remainingCB + newCost) / (oldCoins + newCoins)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### toProfitLossDto — weighted PNL with slippage & fees
|
|
157
|
+
|
|
158
|
+
**Without partials:**
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
priceOpenSlip = effectivePrice × (1 ± slippage)
|
|
162
|
+
priceCloseSlip = priceClose × (1 ∓ slippage)
|
|
163
|
+
|
|
164
|
+
pnlPercentage = (priceCloseSlip - priceOpenSlip) / priceOpenSlip × 100
|
|
165
|
+
fee = CC_PERCENT_FEE × (1 + priceCloseSlip / priceOpenSlip)
|
|
166
|
+
pnlPercentage -= fee
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**With partials — dollar-weighted sum:**
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
weight[i] = (percent[i] / 100 × costBasisAtClose[i]) / totalInvested
|
|
173
|
+
|
|
174
|
+
totalWeightedPnl = Σ weight[i] × pnl[i] # each partial at its own effectivePrice
|
|
175
|
+
+ remainingWeight × pnlRemaining # rest closed at final priceClose
|
|
176
|
+
|
|
177
|
+
fee = CC_PERCENT_FEE # open (once)
|
|
178
|
+
+ Σ CC_PERCENT_FEE × weight[i] × (closeSlip[i] / openSlip[i]) # per partial
|
|
179
|
+
+ CC_PERCENT_FEE × remainingWeight × (closeSlip / openSlip) # final close
|
|
180
|
+
|
|
181
|
+
pnlPercentage = totalWeightedPnl - fee
|
|
182
|
+
pnlCost = pnlPercentage / 100 × totalInvested
|
|
183
|
+
```
|
|
87
184
|
|
|
88
|
-
|
|
185
|
+
| Field | Description |
|
|
186
|
+
|-------|-------------|
|
|
187
|
+
| `totalInvested` | `Σ entry.cost` (or `CC_POSITION_ENTRY_COST` if no `_entry`) |
|
|
188
|
+
| `weight[i]` | Real dollar share of each partial relative to `totalInvested` |
|
|
189
|
+
| `effectivePrice` at partial `i` | Computed via iterative `costBasisAtClose` replay up to `partials[i]` |
|
|
190
|
+
| `priceOpen` in result | `getEffectivePriceOpen(signal)` — DCA-weighted harmonic mean across all entries |
|
|
89
191
|
|
|
90
192
|
## 🖥️ Dashboard Views
|
|
91
193
|
|
package/build/index.cjs
CHANGED
|
@@ -68,12 +68,14 @@ const mockServices$1 = {
|
|
|
68
68
|
storageMockService: Symbol("storageMockService"),
|
|
69
69
|
exchangeMockService: Symbol("exchangeMockService"),
|
|
70
70
|
logMockService: Symbol("logMockService"),
|
|
71
|
+
statusMockService: Symbol("statusMockService"),
|
|
71
72
|
};
|
|
72
73
|
const viewServices$1 = {
|
|
73
74
|
notificationViewService: Symbol("notificationViewService"),
|
|
74
75
|
storageViewService: Symbol("storageViewService"),
|
|
75
76
|
exchangeViewService: Symbol("exchangeViewService"),
|
|
76
77
|
logViewService: Symbol("logViewService"),
|
|
78
|
+
statusViewService: Symbol("statusViewService"),
|
|
77
79
|
};
|
|
78
80
|
const TYPES = {
|
|
79
81
|
...baseServices$1,
|
|
@@ -176,9 +178,9 @@ class LoggerService {
|
|
|
176
178
|
}
|
|
177
179
|
}
|
|
178
180
|
|
|
179
|
-
const MOCK_PATH$
|
|
181
|
+
const MOCK_PATH$3 = "./mock/notifications.json";
|
|
180
182
|
const READ_NOTIFICATION_LIST_FN = functoolsKit.singleshot(async () => {
|
|
181
|
-
const data = await fs.readFile(MOCK_PATH$
|
|
183
|
+
const data = await fs.readFile(MOCK_PATH$3, "utf-8");
|
|
182
184
|
return JSON.parse(data);
|
|
183
185
|
});
|
|
184
186
|
const DEFAULT_LIMIT$3 = 25;
|
|
@@ -238,9 +240,9 @@ class NotificationMockService {
|
|
|
238
240
|
}
|
|
239
241
|
}
|
|
240
242
|
|
|
241
|
-
const MOCK_PATH$
|
|
243
|
+
const MOCK_PATH$2 = "./mock/db";
|
|
242
244
|
const READ_BACKTEST_STORAGE_FN = functoolsKit.singleshot(async () => {
|
|
243
|
-
const dbPath = path.join(process.cwd(), MOCK_PATH$
|
|
245
|
+
const dbPath = path.join(process.cwd(), MOCK_PATH$2);
|
|
244
246
|
const files = await fs.readdir(dbPath);
|
|
245
247
|
const signals = [];
|
|
246
248
|
for (const file of files) {
|
|
@@ -275,6 +277,7 @@ class StorageMockService {
|
|
|
275
277
|
}
|
|
276
278
|
}
|
|
277
279
|
|
|
280
|
+
const MS_PER_MINUTE$1 = 60000;
|
|
278
281
|
class ExchangeMockService {
|
|
279
282
|
constructor() {
|
|
280
283
|
this.loggerService = inject(TYPES.loggerService);
|
|
@@ -298,12 +301,31 @@ class ExchangeMockService {
|
|
|
298
301
|
interval,
|
|
299
302
|
});
|
|
300
303
|
};
|
|
304
|
+
this.getLiveCandles = async (signalId, interval) => {
|
|
305
|
+
this.loggerService.log("exchangeMockService getLiveCandles", {
|
|
306
|
+
signalId,
|
|
307
|
+
interval,
|
|
308
|
+
});
|
|
309
|
+
const signal = await this.storageMockService.findSignalById(signalId);
|
|
310
|
+
if (!signal) {
|
|
311
|
+
throw new Error(`Signal with ID ${signalId} not found`);
|
|
312
|
+
}
|
|
313
|
+
const { pendingAt, scheduledAt, minuteEstimatedTime, } = signal;
|
|
314
|
+
const eventAt = pendingAt || scheduledAt;
|
|
315
|
+
return await this.exchangeService.getRangeCandles({
|
|
316
|
+
symbol: signal.symbol,
|
|
317
|
+
exchangeName: signal.exchangeName,
|
|
318
|
+
signalStartTime: eventAt,
|
|
319
|
+
signalStopTime: eventAt + minuteEstimatedTime * MS_PER_MINUTE$1,
|
|
320
|
+
interval,
|
|
321
|
+
});
|
|
322
|
+
};
|
|
301
323
|
}
|
|
302
324
|
}
|
|
303
325
|
|
|
304
|
-
const MOCK_PATH = "./mock/logs.json";
|
|
326
|
+
const MOCK_PATH$1 = "./mock/logs.json";
|
|
305
327
|
const READ_LOG_LIST_FN = functoolsKit.singleshot(async () => {
|
|
306
|
-
const data = await fs.readFile(MOCK_PATH, "utf-8");
|
|
328
|
+
const data = await fs.readFile(MOCK_PATH$1, "utf-8");
|
|
307
329
|
return JSON.parse(data);
|
|
308
330
|
});
|
|
309
331
|
const DEFAULT_LIMIT$2 = 25;
|
|
@@ -351,6 +373,68 @@ class LogMockService {
|
|
|
351
373
|
}
|
|
352
374
|
}
|
|
353
375
|
|
|
376
|
+
const MOCK_PATH = "./mock/status.json";
|
|
377
|
+
const READ_STATUS_LIST_FN = functoolsKit.singleshot(async () => {
|
|
378
|
+
const data = await fs.readFile(MOCK_PATH, "utf-8");
|
|
379
|
+
return JSON.parse(data);
|
|
380
|
+
});
|
|
381
|
+
class StatusMockService {
|
|
382
|
+
constructor() {
|
|
383
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
384
|
+
this.getStatusList = async () => {
|
|
385
|
+
this.loggerService.log("statusMockService getStatusList");
|
|
386
|
+
const list = await READ_STATUS_LIST_FN();
|
|
387
|
+
return list.map(({ id, symbol, strategyName, exchangeName }) => ({
|
|
388
|
+
id,
|
|
389
|
+
symbol,
|
|
390
|
+
strategyName,
|
|
391
|
+
exchangeName,
|
|
392
|
+
status: "pending",
|
|
393
|
+
}));
|
|
394
|
+
};
|
|
395
|
+
this.getStatusMap = async () => {
|
|
396
|
+
this.loggerService.log("statusMockService getStatusMap");
|
|
397
|
+
const list = await this.getStatusList();
|
|
398
|
+
return list.reduce((acm, cur) => ({ ...acm, [cur.id]: cur }), {});
|
|
399
|
+
};
|
|
400
|
+
this.getStatusOne = async (id) => {
|
|
401
|
+
this.loggerService.log("statusMockService getStatusOne", { id });
|
|
402
|
+
const list = await READ_STATUS_LIST_FN();
|
|
403
|
+
const signal = list.find((s) => s.id === id);
|
|
404
|
+
if (!signal) {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
const positionEntries = signal._entry ?? [];
|
|
408
|
+
const positionLevels = positionEntries.map((e) => e.price);
|
|
409
|
+
const positionPartials = signal._partial ?? [];
|
|
410
|
+
return {
|
|
411
|
+
signalId: signal.signalId,
|
|
412
|
+
position: signal.position,
|
|
413
|
+
symbol: signal.symbol,
|
|
414
|
+
exchangeName: signal.exchangeName,
|
|
415
|
+
strategyName: signal.strategyName,
|
|
416
|
+
totalEntries: signal.totalEntries,
|
|
417
|
+
totalPartials: signal.totalPartials,
|
|
418
|
+
originalPriceStopLoss: signal.originalPriceStopLoss,
|
|
419
|
+
originalPriceTakeProfit: signal.originalPriceTakeProfit,
|
|
420
|
+
originalPriceOpen: signal.originalPriceOpen,
|
|
421
|
+
priceOpen: signal.priceOpen,
|
|
422
|
+
priceTakeProfit: signal.priceTakeProfit,
|
|
423
|
+
priceStopLoss: signal.priceStopLoss,
|
|
424
|
+
pnlPercentage: signal.pnl.pnlPercentage,
|
|
425
|
+
pnlCost: signal.pnl.pnlCost,
|
|
426
|
+
pnlEntries: signal.pnl.pnlEntries,
|
|
427
|
+
partialExecuted: signal.partialExecuted,
|
|
428
|
+
minuteEstimatedTime: signal.minuteEstimatedTime,
|
|
429
|
+
pendingAt: signal.pendingAt,
|
|
430
|
+
positionLevels,
|
|
431
|
+
positionEntries,
|
|
432
|
+
positionPartials,
|
|
433
|
+
};
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
354
438
|
const DEFAULT_LIMIT$1 = 25;
|
|
355
439
|
const DEFAULT_OFFSET$1 = 0;
|
|
356
440
|
const CREATE_FILTER_LIST_FN$1 = (filterData) => Object.keys(filterData).map((key) => (row) => new RegExp(filterData[key], "i").test(row[key]));
|
|
@@ -495,6 +579,7 @@ class StorageViewService {
|
|
|
495
579
|
}
|
|
496
580
|
}
|
|
497
581
|
|
|
582
|
+
const MS_PER_MINUTE = 60000;
|
|
498
583
|
class ExchangeViewService {
|
|
499
584
|
constructor() {
|
|
500
585
|
this.loggerService = inject(TYPES.loggerService);
|
|
@@ -522,6 +607,28 @@ class ExchangeViewService {
|
|
|
522
607
|
interval,
|
|
523
608
|
});
|
|
524
609
|
};
|
|
610
|
+
this.getLiveCandles = async (signalId, interval) => {
|
|
611
|
+
this.loggerService.log("exchangeViewService getLiveCandles", {
|
|
612
|
+
signalId,
|
|
613
|
+
interval,
|
|
614
|
+
});
|
|
615
|
+
if (CC_ENABLE_MOCK) {
|
|
616
|
+
return await this.exchangeMockService.getLiveCandles(signalId, interval);
|
|
617
|
+
}
|
|
618
|
+
const signal = await this.storageViewService.findSignalById(signalId);
|
|
619
|
+
if (!signal) {
|
|
620
|
+
throw new Error(`Signal with ID ${signalId} not found`);
|
|
621
|
+
}
|
|
622
|
+
const { pendingAt, scheduledAt, minuteEstimatedTime, } = signal;
|
|
623
|
+
const eventAt = pendingAt || scheduledAt;
|
|
624
|
+
return await this.exchangeService.getRangeCandles({
|
|
625
|
+
symbol: signal.symbol,
|
|
626
|
+
exchangeName: signal.exchangeName,
|
|
627
|
+
signalStartTime: eventAt,
|
|
628
|
+
signalStopTime: eventAt + minuteEstimatedTime * MS_PER_MINUTE,
|
|
629
|
+
interval,
|
|
630
|
+
});
|
|
631
|
+
};
|
|
525
632
|
}
|
|
526
633
|
}
|
|
527
634
|
|
|
@@ -580,6 +687,100 @@ class LogViewService {
|
|
|
580
687
|
}
|
|
581
688
|
}
|
|
582
689
|
|
|
690
|
+
class StatusViewService {
|
|
691
|
+
constructor() {
|
|
692
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
693
|
+
this.statusMockService = inject(TYPES.statusMockService);
|
|
694
|
+
this.getStatusList = async () => {
|
|
695
|
+
this.loggerService.log("statusViewService getStatusList");
|
|
696
|
+
if (CC_ENABLE_MOCK) {
|
|
697
|
+
const liveList = await this.statusMockService.getStatusList();
|
|
698
|
+
return liveList.filter(({ status }) => status === "pending");
|
|
699
|
+
}
|
|
700
|
+
return await backtestKit.Live.list();
|
|
701
|
+
};
|
|
702
|
+
this.getStatusMap = async () => {
|
|
703
|
+
this.loggerService.log("statusViewService getStatusMap");
|
|
704
|
+
if (CC_ENABLE_MOCK) {
|
|
705
|
+
return await this.statusMockService.getStatusMap();
|
|
706
|
+
}
|
|
707
|
+
const liveList = await backtestKit.Live.list();
|
|
708
|
+
return liveList
|
|
709
|
+
.filter(({ status }) => status === "pending")
|
|
710
|
+
.reduce((acm, cur) => ({ ...acm, [cur.id]: cur }), {});
|
|
711
|
+
};
|
|
712
|
+
this.getStatusOne = async (id) => {
|
|
713
|
+
this.loggerService.log("statusViewService getStatusOne", {
|
|
714
|
+
id,
|
|
715
|
+
});
|
|
716
|
+
if (CC_ENABLE_MOCK) {
|
|
717
|
+
return await this.statusMockService.getStatusOne(id);
|
|
718
|
+
}
|
|
719
|
+
const liveList = await backtestKit.Live.list();
|
|
720
|
+
const liveOne = liveList.find((live) => live.id === id);
|
|
721
|
+
if (!liveOne) {
|
|
722
|
+
throw new Error(`Live with id ${id} not found`);
|
|
723
|
+
}
|
|
724
|
+
const { symbol, strategyName, exchangeName } = liveOne;
|
|
725
|
+
const currentPrice = await backtestKit.Exchange.getAveragePrice(symbol, {
|
|
726
|
+
exchangeName,
|
|
727
|
+
});
|
|
728
|
+
const pendingSignal = await backtestKit.Live.getPendingSignal(symbol, currentPrice, {
|
|
729
|
+
strategyName,
|
|
730
|
+
exchangeName,
|
|
731
|
+
});
|
|
732
|
+
if (!pendingSignal) {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
const positionLevels = await backtestKit.Live.getPositionLevels(symbol, {
|
|
736
|
+
strategyName,
|
|
737
|
+
exchangeName,
|
|
738
|
+
});
|
|
739
|
+
if (!positionLevels) {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
const positionEntries = await backtestKit.Live.getPositionEntries(symbol, {
|
|
743
|
+
strategyName,
|
|
744
|
+
exchangeName,
|
|
745
|
+
});
|
|
746
|
+
if (!positionEntries) {
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
const positionPartials = await backtestKit.Live.getPositionPartials(symbol, {
|
|
750
|
+
strategyName,
|
|
751
|
+
exchangeName,
|
|
752
|
+
});
|
|
753
|
+
if (!positionPartials) {
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
return {
|
|
757
|
+
signalId: pendingSignal.id,
|
|
758
|
+
position: pendingSignal.position,
|
|
759
|
+
symbol: pendingSignal.symbol,
|
|
760
|
+
exchangeName: pendingSignal.exchangeName,
|
|
761
|
+
strategyName: pendingSignal.strategyName,
|
|
762
|
+
totalEntries: pendingSignal.totalEntries,
|
|
763
|
+
totalPartials: pendingSignal.totalPartials,
|
|
764
|
+
originalPriceStopLoss: pendingSignal.originalPriceStopLoss,
|
|
765
|
+
originalPriceTakeProfit: pendingSignal.originalPriceTakeProfit,
|
|
766
|
+
originalPriceOpen: pendingSignal.originalPriceOpen,
|
|
767
|
+
priceOpen: pendingSignal.priceOpen,
|
|
768
|
+
priceTakeProfit: pendingSignal.priceTakeProfit,
|
|
769
|
+
priceStopLoss: pendingSignal.priceStopLoss,
|
|
770
|
+
pnlPercentage: pendingSignal.pnl.pnlPercentage,
|
|
771
|
+
pnlCost: pendingSignal.pnl.pnlCost,
|
|
772
|
+
pnlEntries: pendingSignal.pnl.pnlEntries,
|
|
773
|
+
partialExecuted: pendingSignal.partialExecuted,
|
|
774
|
+
pendingAt: pendingSignal.pendingAt,
|
|
775
|
+
minuteEstimatedTime: pendingSignal.minuteEstimatedTime,
|
|
776
|
+
positionEntries,
|
|
777
|
+
positionLevels,
|
|
778
|
+
positionPartials,
|
|
779
|
+
};
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
583
784
|
const symbol_list = [
|
|
584
785
|
{
|
|
585
786
|
icon: "/icon/btc.png",
|
|
@@ -705,22 +906,6 @@ class SymbolMetaService {
|
|
|
705
906
|
}
|
|
706
907
|
}
|
|
707
908
|
|
|
708
|
-
const PRICE_TIMEOUT = 120000;
|
|
709
|
-
const CREATE_KEY_FN = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
710
|
-
const parts = [symbol, strategyName, exchangeName];
|
|
711
|
-
if (frameName)
|
|
712
|
-
parts.push(frameName);
|
|
713
|
-
parts.push(backtest ? "backtest" : "live");
|
|
714
|
-
return parts.join(":");
|
|
715
|
-
};
|
|
716
|
-
const GET_SUBJECT_FN = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN(symbol, strategyName, exchangeName, frameName, backtest), () => new functoolsKit.BehaviorSubject());
|
|
717
|
-
const GET_PRICE_FN = async (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
718
|
-
const priceSubject = GET_SUBJECT_FN(symbol, strategyName, exchangeName, frameName, backtest);
|
|
719
|
-
if (priceSubject.data) {
|
|
720
|
-
return priceSubject.data;
|
|
721
|
-
}
|
|
722
|
-
return await functoolsKit.waitForNext(priceSubject, (data) => !!data, PRICE_TIMEOUT);
|
|
723
|
-
};
|
|
724
909
|
class PriceConnectionService {
|
|
725
910
|
constructor() {
|
|
726
911
|
this.loggerService = inject(TYPES.loggerService);
|
|
@@ -732,19 +917,8 @@ class PriceConnectionService {
|
|
|
732
917
|
frameName,
|
|
733
918
|
backtest,
|
|
734
919
|
});
|
|
735
|
-
|
|
736
|
-
if (typeof currentPrice === "symbol") {
|
|
737
|
-
throw new Error(`Price for ${CREATE_KEY_FN(symbol, strategyName, exchangeName, frameName, backtest)} not received within timeout`);
|
|
738
|
-
}
|
|
739
|
-
return currentPrice;
|
|
920
|
+
return await backtestKit.lib.priceMetaService.getCurrentPrice(symbol, { strategyName, exchangeName, frameName }, backtest);
|
|
740
921
|
};
|
|
741
|
-
this.init = functoolsKit.singleshot(async () => {
|
|
742
|
-
this.loggerService.log("priceConnectionService init");
|
|
743
|
-
backtestKit.listenSignal((event) => {
|
|
744
|
-
const priceSubject = GET_SUBJECT_FN(event.symbol, event.strategyName, event.exchangeName, event.frameName, event.backtest);
|
|
745
|
-
event.currentPrice && priceSubject.next(event.currentPrice);
|
|
746
|
-
});
|
|
747
|
-
});
|
|
748
922
|
}
|
|
749
923
|
}
|
|
750
924
|
|
|
@@ -764,12 +938,14 @@ class PriceConnectionService {
|
|
|
764
938
|
provide(TYPES.storageMockService, () => new StorageMockService());
|
|
765
939
|
provide(TYPES.exchangeMockService, () => new ExchangeMockService());
|
|
766
940
|
provide(TYPES.logMockService, () => new LogMockService());
|
|
941
|
+
provide(TYPES.statusMockService, () => new StatusMockService());
|
|
767
942
|
}
|
|
768
943
|
{
|
|
769
944
|
provide(TYPES.notificationViewService, () => new NotificationViewService());
|
|
770
945
|
provide(TYPES.storageViewService, () => new StorageViewService());
|
|
771
946
|
provide(TYPES.exchangeViewService, () => new ExchangeViewService());
|
|
772
947
|
provide(TYPES.logViewService, () => new LogViewService());
|
|
948
|
+
provide(TYPES.statusViewService, () => new StatusViewService());
|
|
773
949
|
}
|
|
774
950
|
|
|
775
951
|
const baseServices = {
|
|
@@ -788,12 +964,14 @@ const mockServices = {
|
|
|
788
964
|
storageMockService: inject(TYPES.storageMockService),
|
|
789
965
|
exchangeMockService: inject(TYPES.exchangeMockService),
|
|
790
966
|
logMockService: inject(TYPES.logMockService),
|
|
967
|
+
statusMockService: inject(TYPES.statusMockService),
|
|
791
968
|
};
|
|
792
969
|
const viewServices = {
|
|
793
970
|
notificationViewService: inject(TYPES.notificationViewService),
|
|
794
971
|
storageViewService: inject(TYPES.storageViewService),
|
|
795
972
|
exchangeViewService: inject(TYPES.exchangeViewService),
|
|
796
973
|
logViewService: inject(TYPES.logViewService),
|
|
974
|
+
statusViewService: inject(TYPES.statusViewService),
|
|
797
975
|
};
|
|
798
976
|
const ioc = {
|
|
799
977
|
...baseServices,
|
|
@@ -1127,6 +1305,92 @@ router$6.post("/api/v1/mock/log_filter", async (req, res) => {
|
|
|
1127
1305
|
});
|
|
1128
1306
|
}
|
|
1129
1307
|
});
|
|
1308
|
+
router$6.post("/api/v1/mock/candles_live", async (req, res) => {
|
|
1309
|
+
try {
|
|
1310
|
+
const request = await micro.json(req);
|
|
1311
|
+
const { signalId, interval, requestId, serviceName } = request;
|
|
1312
|
+
const data = await ioc.exchangeMockService.getLiveCandles(signalId, interval);
|
|
1313
|
+
const result = {
|
|
1314
|
+
data,
|
|
1315
|
+
status: "ok",
|
|
1316
|
+
error: "",
|
|
1317
|
+
requestId,
|
|
1318
|
+
serviceName,
|
|
1319
|
+
};
|
|
1320
|
+
ioc.loggerService.log("/api/v1/mock/candles_live ok", {
|
|
1321
|
+
request,
|
|
1322
|
+
result: omit(result, "data"),
|
|
1323
|
+
});
|
|
1324
|
+
return await micro.send(res, 200, result);
|
|
1325
|
+
}
|
|
1326
|
+
catch (error) {
|
|
1327
|
+
ioc.loggerService.log("/api/v1/mock/candles_live error", {
|
|
1328
|
+
error: functoolsKit.errorData(error),
|
|
1329
|
+
});
|
|
1330
|
+
return await micro.send(res, 200, {
|
|
1331
|
+
status: "error",
|
|
1332
|
+
error: functoolsKit.getErrorMessage(error),
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
// StatusMockService endpoints
|
|
1337
|
+
router$6.post("/api/v1/mock/status_list", async (req, res) => {
|
|
1338
|
+
try {
|
|
1339
|
+
const request = await micro.json(req);
|
|
1340
|
+
const { requestId, serviceName } = request;
|
|
1341
|
+
const data = await ioc.statusMockService.getStatusList();
|
|
1342
|
+
const result = {
|
|
1343
|
+
data,
|
|
1344
|
+
status: "ok",
|
|
1345
|
+
error: "",
|
|
1346
|
+
requestId,
|
|
1347
|
+
serviceName,
|
|
1348
|
+
};
|
|
1349
|
+
ioc.loggerService.log("/api/v1/mock/status_list ok", {
|
|
1350
|
+
request,
|
|
1351
|
+
result: omit(result, "data"),
|
|
1352
|
+
});
|
|
1353
|
+
return await micro.send(res, 200, result);
|
|
1354
|
+
}
|
|
1355
|
+
catch (error) {
|
|
1356
|
+
ioc.loggerService.log("/api/v1/mock/status_list error", {
|
|
1357
|
+
error: functoolsKit.errorData(error),
|
|
1358
|
+
});
|
|
1359
|
+
return await micro.send(res, 200, {
|
|
1360
|
+
status: "error",
|
|
1361
|
+
error: functoolsKit.getErrorMessage(error),
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
router$6.post("/api/v1/mock/status_one/:id", async (req, res) => {
|
|
1366
|
+
try {
|
|
1367
|
+
const request = await micro.json(req);
|
|
1368
|
+
const { requestId, serviceName } = request;
|
|
1369
|
+
const id = req.params.id;
|
|
1370
|
+
const data = await ioc.statusMockService.getStatusOne(id);
|
|
1371
|
+
const result = {
|
|
1372
|
+
data,
|
|
1373
|
+
status: "ok",
|
|
1374
|
+
error: "",
|
|
1375
|
+
requestId,
|
|
1376
|
+
serviceName,
|
|
1377
|
+
};
|
|
1378
|
+
ioc.loggerService.log("/api/v1/mock/status_one/:id ok", {
|
|
1379
|
+
request,
|
|
1380
|
+
result: omit(result, "data"),
|
|
1381
|
+
});
|
|
1382
|
+
return await micro.send(res, 200, result);
|
|
1383
|
+
}
|
|
1384
|
+
catch (error) {
|
|
1385
|
+
ioc.loggerService.log("/api/v1/mock/status_one/:id error", {
|
|
1386
|
+
error: functoolsKit.errorData(error),
|
|
1387
|
+
});
|
|
1388
|
+
return await micro.send(res, 200, {
|
|
1389
|
+
status: "error",
|
|
1390
|
+
error: functoolsKit.getErrorMessage(error),
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1130
1394
|
|
|
1131
1395
|
const router$5 = Router({
|
|
1132
1396
|
params: true,
|
|
@@ -1193,6 +1457,34 @@ router$5.post("/api/v1/view/candles_point", async (req, res) => {
|
|
|
1193
1457
|
});
|
|
1194
1458
|
}
|
|
1195
1459
|
});
|
|
1460
|
+
router$5.post("/api/v1/view/candles_live", async (req, res) => {
|
|
1461
|
+
try {
|
|
1462
|
+
const request = await micro.json(req);
|
|
1463
|
+
const { signalId, interval, requestId, serviceName } = request;
|
|
1464
|
+
const data = await ioc.exchangeViewService.getLiveCandles(signalId, interval);
|
|
1465
|
+
const result = {
|
|
1466
|
+
data,
|
|
1467
|
+
status: "ok",
|
|
1468
|
+
error: "",
|
|
1469
|
+
requestId,
|
|
1470
|
+
serviceName,
|
|
1471
|
+
};
|
|
1472
|
+
ioc.loggerService.log("/api/v1/view/candles_live ok", {
|
|
1473
|
+
request,
|
|
1474
|
+
result: omit(result, "data"),
|
|
1475
|
+
});
|
|
1476
|
+
return await micro.send(res, 200, result);
|
|
1477
|
+
}
|
|
1478
|
+
catch (error) {
|
|
1479
|
+
ioc.loggerService.log("/api/v1/view/candles_live error", {
|
|
1480
|
+
error: functoolsKit.errorData(error),
|
|
1481
|
+
});
|
|
1482
|
+
return await micro.send(res, 200, {
|
|
1483
|
+
status: "error",
|
|
1484
|
+
error: functoolsKit.getErrorMessage(error),
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1196
1488
|
// NotificationViewService endpoints
|
|
1197
1489
|
router$5.post("/api/v1/view/notification_list", async (req, res) => {
|
|
1198
1490
|
try {
|
|
@@ -1451,6 +1743,64 @@ router$5.post("/api/v1/view/log_filter", async (req, res) => {
|
|
|
1451
1743
|
});
|
|
1452
1744
|
}
|
|
1453
1745
|
});
|
|
1746
|
+
// StatusViewService endpoints
|
|
1747
|
+
router$5.post("/api/v1/view/status_list", async (req, res) => {
|
|
1748
|
+
try {
|
|
1749
|
+
const request = await micro.json(req);
|
|
1750
|
+
const { requestId, serviceName } = request;
|
|
1751
|
+
const data = await ioc.statusViewService.getStatusList();
|
|
1752
|
+
const result = {
|
|
1753
|
+
data,
|
|
1754
|
+
status: "ok",
|
|
1755
|
+
error: "",
|
|
1756
|
+
requestId,
|
|
1757
|
+
serviceName,
|
|
1758
|
+
};
|
|
1759
|
+
ioc.loggerService.log("/api/v1/view/status_list ok", {
|
|
1760
|
+
request,
|
|
1761
|
+
result: omit(result, "data"),
|
|
1762
|
+
});
|
|
1763
|
+
return await micro.send(res, 200, result);
|
|
1764
|
+
}
|
|
1765
|
+
catch (error) {
|
|
1766
|
+
ioc.loggerService.log("/api/v1/view/status_list error", {
|
|
1767
|
+
error: functoolsKit.errorData(error),
|
|
1768
|
+
});
|
|
1769
|
+
return await micro.send(res, 200, {
|
|
1770
|
+
status: "error",
|
|
1771
|
+
error: functoolsKit.getErrorMessage(error),
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
});
|
|
1775
|
+
router$5.post("/api/v1/view/status_one/:id", async (req, res) => {
|
|
1776
|
+
try {
|
|
1777
|
+
const request = await micro.json(req);
|
|
1778
|
+
const { requestId, serviceName } = request;
|
|
1779
|
+
const id = req.params.id;
|
|
1780
|
+
const data = await ioc.statusViewService.getStatusOne(id);
|
|
1781
|
+
const result = {
|
|
1782
|
+
data,
|
|
1783
|
+
status: "ok",
|
|
1784
|
+
error: "",
|
|
1785
|
+
requestId,
|
|
1786
|
+
serviceName,
|
|
1787
|
+
};
|
|
1788
|
+
ioc.loggerService.log("/api/v1/view/status_one/:id ok", {
|
|
1789
|
+
request,
|
|
1790
|
+
result: omit(result, "data"),
|
|
1791
|
+
});
|
|
1792
|
+
return await micro.send(res, 200, result);
|
|
1793
|
+
}
|
|
1794
|
+
catch (error) {
|
|
1795
|
+
ioc.loggerService.log("/api/v1/view/status_one/:id error", {
|
|
1796
|
+
error: functoolsKit.errorData(error),
|
|
1797
|
+
});
|
|
1798
|
+
return await micro.send(res, 200, {
|
|
1799
|
+
status: "error",
|
|
1800
|
+
error: functoolsKit.getErrorMessage(error),
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
});
|
|
1454
1804
|
|
|
1455
1805
|
const require$2 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
1456
1806
|
function getModulesPath() {
|