@backtest-kit/ui 5.0.0 → 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 +1 -28
- package/build/index.mjs +3 -30
- package/build/modules/frontend/build/assets/{Background-hRKtQSrv.js → Background-BmAP0u-v.js} +1 -1
- package/build/modules/frontend/build/assets/{IconPhoto-DQlKNGdb.js → IconPhoto-CTW1vRuU.js} +1 -1
- package/build/modules/frontend/build/assets/{KeyboardArrowLeft-wYBUTbRV.js → KeyboardArrowLeft-D-bGIipj.js} +1 -1
- package/build/modules/frontend/build/assets/{Refresh-_yMWRw_x.js → Refresh-Dx7WNRNQ.js} +1 -1
- package/build/modules/frontend/build/assets/{index-3gnGrSPi.js → index-B6tX14ok.js} +11 -11
- package/build/modules/frontend/build/assets/{index-BPJ87g-R.js → index-BHg7O3a-.js} +1 -1
- package/build/modules/frontend/build/assets/{index-BUme2lKb.js → index-Bf8C3fbL.js} +1 -1
- package/build/modules/frontend/build/assets/{index-Bay1AtTY.js → index-BfcHCLoD.js} +1 -1
- package/build/modules/frontend/build/assets/{index-TU650r8r.js → index-Bo1e6VXK.js} +1 -1
- package/build/modules/frontend/build/assets/{index-Kwt_oEAl.js → index-C3pFAoIV.js} +1 -1
- package/build/modules/frontend/build/assets/{index-BFSXu2bZ.js → index-DKubDtVK.js} +1 -1
- package/build/modules/frontend/build/index.html +1 -1
- package/package.json +3 -3
- package/types.d.ts +0 -1
- package/build/modules/frontend/build/assets/markdownit-CZc4lN8k.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
|
@@ -906,22 +906,6 @@ class SymbolMetaService {
|
|
|
906
906
|
}
|
|
907
907
|
}
|
|
908
908
|
|
|
909
|
-
const PRICE_TIMEOUT = 120000;
|
|
910
|
-
const CREATE_KEY_FN = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
911
|
-
const parts = [symbol, strategyName, exchangeName];
|
|
912
|
-
if (frameName)
|
|
913
|
-
parts.push(frameName);
|
|
914
|
-
parts.push(backtest ? "backtest" : "live");
|
|
915
|
-
return parts.join(":");
|
|
916
|
-
};
|
|
917
|
-
const GET_SUBJECT_FN = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN(symbol, strategyName, exchangeName, frameName, backtest), () => new functoolsKit.BehaviorSubject());
|
|
918
|
-
const GET_PRICE_FN = async (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
919
|
-
const priceSubject = GET_SUBJECT_FN(symbol, strategyName, exchangeName, frameName, backtest);
|
|
920
|
-
if (priceSubject.data) {
|
|
921
|
-
return priceSubject.data;
|
|
922
|
-
}
|
|
923
|
-
return await functoolsKit.waitForNext(priceSubject, (data) => !!data, PRICE_TIMEOUT);
|
|
924
|
-
};
|
|
925
909
|
class PriceConnectionService {
|
|
926
910
|
constructor() {
|
|
927
911
|
this.loggerService = inject(TYPES.loggerService);
|
|
@@ -933,19 +917,8 @@ class PriceConnectionService {
|
|
|
933
917
|
frameName,
|
|
934
918
|
backtest,
|
|
935
919
|
});
|
|
936
|
-
|
|
937
|
-
if (typeof currentPrice === "symbol") {
|
|
938
|
-
throw new Error(`Price for ${CREATE_KEY_FN(symbol, strategyName, exchangeName, frameName, backtest)} not received within timeout`);
|
|
939
|
-
}
|
|
940
|
-
return currentPrice;
|
|
920
|
+
return await backtestKit.lib.priceMetaService.getCurrentPrice(symbol, { strategyName, exchangeName, frameName }, backtest);
|
|
941
921
|
};
|
|
942
|
-
this.init = functoolsKit.singleshot(async () => {
|
|
943
|
-
this.loggerService.log("priceConnectionService init");
|
|
944
|
-
backtestKit.listenSignal((event) => {
|
|
945
|
-
const priceSubject = GET_SUBJECT_FN(event.symbol, event.strategyName, event.exchangeName, event.frameName, event.backtest);
|
|
946
|
-
event.currentPrice && priceSubject.next(event.currentPrice);
|
|
947
|
-
});
|
|
948
|
-
});
|
|
949
922
|
}
|
|
950
923
|
}
|
|
951
924
|
|
package/build/index.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
|
-
import { isObject, singleshot, pickDocuments, str, memoize,
|
|
2
|
+
import { isObject, singleshot, pickDocuments, str, memoize, errorData, getErrorMessage } from 'functools-kit';
|
|
3
3
|
import micro from 'micro';
|
|
4
4
|
import Router from 'router';
|
|
5
5
|
import finalhandler from 'finalhandler';
|
|
6
6
|
import serveHandler from 'serve-handler';
|
|
7
7
|
import os from 'os';
|
|
8
8
|
import { createActivator } from 'di-kit';
|
|
9
|
-
import { Exchange, Notification, Storage, Log, Live,
|
|
9
|
+
import { Exchange, Notification, Storage, Log, Live, lib } from 'backtest-kit';
|
|
10
10
|
import fs, { readdir, readFile } from 'fs/promises';
|
|
11
11
|
import path, { join, dirname } from 'path';
|
|
12
12
|
import { createRequire } from 'module';
|
|
@@ -903,22 +903,6 @@ class SymbolMetaService {
|
|
|
903
903
|
}
|
|
904
904
|
}
|
|
905
905
|
|
|
906
|
-
const PRICE_TIMEOUT = 120000;
|
|
907
|
-
const CREATE_KEY_FN = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
908
|
-
const parts = [symbol, strategyName, exchangeName];
|
|
909
|
-
if (frameName)
|
|
910
|
-
parts.push(frameName);
|
|
911
|
-
parts.push(backtest ? "backtest" : "live");
|
|
912
|
-
return parts.join(":");
|
|
913
|
-
};
|
|
914
|
-
const GET_SUBJECT_FN = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN(symbol, strategyName, exchangeName, frameName, backtest), () => new BehaviorSubject());
|
|
915
|
-
const GET_PRICE_FN = async (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
916
|
-
const priceSubject = GET_SUBJECT_FN(symbol, strategyName, exchangeName, frameName, backtest);
|
|
917
|
-
if (priceSubject.data) {
|
|
918
|
-
return priceSubject.data;
|
|
919
|
-
}
|
|
920
|
-
return await waitForNext(priceSubject, (data) => !!data, PRICE_TIMEOUT);
|
|
921
|
-
};
|
|
922
906
|
class PriceConnectionService {
|
|
923
907
|
constructor() {
|
|
924
908
|
this.loggerService = inject(TYPES.loggerService);
|
|
@@ -930,19 +914,8 @@ class PriceConnectionService {
|
|
|
930
914
|
frameName,
|
|
931
915
|
backtest,
|
|
932
916
|
});
|
|
933
|
-
|
|
934
|
-
if (typeof currentPrice === "symbol") {
|
|
935
|
-
throw new Error(`Price for ${CREATE_KEY_FN(symbol, strategyName, exchangeName, frameName, backtest)} not received within timeout`);
|
|
936
|
-
}
|
|
937
|
-
return currentPrice;
|
|
917
|
+
return await lib.priceMetaService.getCurrentPrice(symbol, { strategyName, exchangeName, frameName }, backtest);
|
|
938
918
|
};
|
|
939
|
-
this.init = singleshot(async () => {
|
|
940
|
-
this.loggerService.log("priceConnectionService init");
|
|
941
|
-
listenSignal((event) => {
|
|
942
|
-
const priceSubject = GET_SUBJECT_FN(event.symbol, event.strategyName, event.exchangeName, event.frameName, event.backtest);
|
|
943
|
-
event.currentPrice && priceSubject.next(event.currentPrice);
|
|
944
|
-
});
|
|
945
|
-
});
|
|
946
919
|
}
|
|
947
920
|
}
|
|
948
921
|
|
package/build/modules/frontend/build/assets/{Background-hRKtQSrv.js → Background-BmAP0u-v.js}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{f as n,az as o}from"./index-
|
|
1
|
+
import{f as n,az as o}from"./index-B6tX14ok.js";const d=()=>n(o,{children:"\n body {\n background-color: #ddd !important;\n }\n "});export{d as B};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{f as e,i as t,B as r,J as s}from"./index-
|
|
1
|
+
import{f as e,i as t,B as r,J as s}from"./index-B6tX14ok.js";const a=({className:a,symbol:i,style:o,sx:n})=>e(s,{children:async()=>{try{const s=(await t.symbolGlobalService.getSymbolMap())[i],l=null==s?void 0:s.icon,c=(null==s?void 0:s.color)||"#ccc";return e(r,{className:a,sx:{position:"relative",width:24,height:24,borderRadius:"50%",display:"flex",alignItems:"center",justifyContent:"center",background:l?"transparent":c,...n},style:o,children:l?e("img",{loading:"lazy",crossOrigin:"anonymous",src:l,alt:i,style:{width:"100%",height:"100%",borderRadius:"50%",objectFit:"contain"},onError:e=>{const t=e.target,r=t.parentElement;r&&(r.style.background=c,t.style.display="none")}}):e(r,{sx:{width:"60%",height:"60%",borderRadius:"50%",backgroundColor:"rgba(255, 255, 255, 0.2)"}})})}catch(s){return e(r,{className:a,sx:{position:"relative",width:24,height:24,borderRadius:"50%",display:"flex",alignItems:"center",justifyContent:"center",background:"#ccc",...n},style:o,children:e(r,{sx:{width:"60%",height:"60%",borderRadius:"50%",backgroundColor:"rgba(255, 255, 255, 0.2)"}})})}}});export{a as I};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{c as o,f as r}from"./index-
|
|
1
|
+
import{c as o,f as r}from"./index-B6tX14ok.js";const a=o(r("path",{d:"M15.41 16.59 10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41z"}),"KeyboardArrowLeft");export{a as K};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{aA as e,aB as t,r as a,aC as s,f as i,aD as n,aE as o,aF as r,aG as d,aH as m,aI as u,aJ as p,aK as x,c}from"./index-
|
|
1
|
+
import{aA as e,aB as t,r as a,aC as s,f as i,aD as n,aE as o,aF as r,aG as d,aH as m,aI as u,aJ as p,aK as x,c}from"./index-B6tX14ok.js";const h=e(),l=["className","component","disableGutters","fixed","maxWidth","classes"],b=m(),f=h("div",{name:"MuiContainer",slot:"Root",overridesResolver:(e,t)=>{const{ownerState:a}=e;return[t.root,t[`maxWidth${o(String(a.maxWidth))}`],a.fixed&&t.fixed,a.disableGutters&&t.disableGutters]}}),g=e=>r({props:e,name:"MuiContainer",defaultTheme:b});const W=function(e={}){const{createStyledComponent:r=f,useThemeProps:m=g,componentName:p="MuiContainer"}=e,x=r(({theme:e,ownerState:a})=>t({width:"100%",marginLeft:"auto",boxSizing:"border-box",marginRight:"auto",display:"block"},!a.disableGutters&&{paddingLeft:e.spacing(2),paddingRight:e.spacing(2),[e.breakpoints.up("sm")]:{paddingLeft:e.spacing(3),paddingRight:e.spacing(3)}}),({theme:e,ownerState:t})=>t.fixed&&Object.keys(e.breakpoints.values).reduce((t,a)=>{const s=a,i=e.breakpoints.values[s];return 0!==i&&(t[e.breakpoints.up(s)]={maxWidth:`${i}${e.breakpoints.unit}`}),t},{}),({theme:e,ownerState:a})=>t({},"xs"===a.maxWidth&&{[e.breakpoints.up("xs")]:{maxWidth:Math.max(e.breakpoints.values.xs,444)}},a.maxWidth&&"xs"!==a.maxWidth&&{[e.breakpoints.up(a.maxWidth)]:{maxWidth:`${e.breakpoints.values[a.maxWidth]}${e.breakpoints.unit}`}}));return a.forwardRef(function(e,a){const r=m(e),{className:c,component:h="div",disableGutters:b=!1,fixed:f=!1,maxWidth:g="lg"}=r,W=s(r,l),k=t({},r,{component:h,disableGutters:b,fixed:f,maxWidth:g}),S=((e,t)=>{const{classes:a,fixed:s,disableGutters:i,maxWidth:n}=e,r={root:["root",n&&`maxWidth${o(String(n))}`,s&&"fixed",i&&"disableGutters"]};return d(r,e=>u(t,e),a)})(k,p);return i(x,t({as:h,ownerState:k,className:n(S.root,c),ref:a},W))})}({createStyledComponent:x("div",{name:"MuiContainer",slot:"Root",overridesResolver:(e,t)=>{const{ownerState:a}=e;return[t.root,t[`maxWidth${o(String(a.maxWidth))}`],a.fixed&&t.fixed,a.disableGutters&&t.disableGutters]}}),useThemeProps:e=>p({props:e,name:"MuiContainer"})}),k=c(i("path",{d:"M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"}),"Refresh");export{W as C,k as R};
|