@backtest-kit/cli 7.4.0 โ 7.5.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 +105 -0
- package/build/index.cjs +170 -26
- package/build/index.mjs +171 -27
- package/package.json +13 -13
- package/template/project/package.mustache +5 -5
- package/template/signal-info.mustache +20 -20
package/README.md
CHANGED
|
@@ -44,6 +44,7 @@ Point the CLI at your strategy file, choose a mode, and it handles exchange conn
|
|
|
44
44
|
| **PineScript** | `--pine` | Run a local `.pine` indicator against exchange data |
|
|
45
45
|
| **Pine Editor** | `--editor` | Open the visual Pine Script editor in the browser |
|
|
46
46
|
| **Candle Dump** | `--dump` | Fetch and save raw OHLCV candles to a file |
|
|
47
|
+
| **PnL Debug** | `--pnldebug` | Simulate per-minute PnL for a given entry price and direction |
|
|
47
48
|
| **Flush** | `--flush` | Delete report/log/markdown/agent folders from strategy dump dir |
|
|
48
49
|
| **Init Project** | `--init` | Scaffold a new backtest-kit project |
|
|
49
50
|
|
|
@@ -1065,6 +1066,110 @@ Or add it to `package.json`:
|
|
|
1065
1066
|
npx @backtest-kit/cli --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
|
|
1066
1067
|
```
|
|
1067
1068
|
|
|
1069
|
+
## ๐ PnL Debug (`--pnldebug`)
|
|
1070
|
+
|
|
1071
|
+
`@backtest-kit/cli` can simulate a hypothetical position minute by minute and print running PnL, peak profit, and maximum drawdown for each candle โ without placing any trades or loading a strategy file.
|
|
1072
|
+
|
|
1073
|
+
### CLI Flags
|
|
1074
|
+
|
|
1075
|
+
| Flag | Type | Description |
|
|
1076
|
+
|------|------|-------------|
|
|
1077
|
+
| `--pnldebug` | boolean | Enable PnL debug mode |
|
|
1078
|
+
| `--priceopen` | number | Entry price (required) |
|
|
1079
|
+
| `--direction` | string | `long` or `short` (default: `long`) |
|
|
1080
|
+
| `--when` | string | Start timestamp โ ISO 8601 or Unix ms (default: now) |
|
|
1081
|
+
| `--minutes` | string | Number of 1m candles to simulate (default: `60`) |
|
|
1082
|
+
| `--symbol` | string | Trading pair (default: `"BTCUSDT"`) |
|
|
1083
|
+
| `--exchange` | string | Exchange name (default: first registered, falls back to CCXT Binance) |
|
|
1084
|
+
| `--output` | string | Output file base name (default: `{SYMBOL}_{DIRECTION}_{PRICEOPEN}_{TIMESTAMP}`) |
|
|
1085
|
+
| `--json` | boolean | Save results as JSON array to `./dump/<output>.json` |
|
|
1086
|
+
| `--jsonl` | boolean | Save results as JSONL to `./dump/<output>.jsonl` |
|
|
1087
|
+
| `--markdown` | boolean | Save results as Markdown table to `./dump/<output>.md` |
|
|
1088
|
+
|
|
1089
|
+
### Output columns
|
|
1090
|
+
|
|
1091
|
+
| Column | Description |
|
|
1092
|
+
|--------|-------------|
|
|
1093
|
+
| `min` | Minute offset from start (1-based) |
|
|
1094
|
+
| `timestamp` | Candle timestamp (ISO 8601) |
|
|
1095
|
+
| `close` | Candle close price |
|
|
1096
|
+
| `pnl%` | Running PnL vs entry price (signed %) |
|
|
1097
|
+
| `peak%` | Highest PnL reached so far (always โฅ 0) |
|
|
1098
|
+
| `drawdown%` | Lowest PnL reached so far (always โค 0) |
|
|
1099
|
+
|
|
1100
|
+
### Exchange via `pnldebug.module`
|
|
1101
|
+
|
|
1102
|
+
By default the CLI registers CCXT Binance automatically. To use a different exchange, create a `modules/pnldebug.module.ts` file in the current working directory โ the CLI loads it automatically before fetching candles.
|
|
1103
|
+
|
|
1104
|
+
```typescript
|
|
1105
|
+
// modules/pnldebug.module.ts
|
|
1106
|
+
import { addExchangeSchema } from "backtest-kit";
|
|
1107
|
+
import ccxt from "ccxt";
|
|
1108
|
+
|
|
1109
|
+
addExchangeSchema({
|
|
1110
|
+
exchangeName: "my-exchange",
|
|
1111
|
+
getCandles: async (symbol, interval, since, limit) => {
|
|
1112
|
+
const exchange = new ccxt.bybit({ enableRateLimit: true });
|
|
1113
|
+
const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);
|
|
1114
|
+
return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({
|
|
1115
|
+
timestamp, open, high, low, close, volume,
|
|
1116
|
+
}));
|
|
1117
|
+
},
|
|
1118
|
+
formatPrice: (symbol, price) => price.toFixed(2),
|
|
1119
|
+
formatQuantity: (symbol, quantity) => quantity.toFixed(8),
|
|
1120
|
+
});
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
### Usage
|
|
1124
|
+
|
|
1125
|
+
Print to stdout (default table format):
|
|
1126
|
+
|
|
1127
|
+
```bash
|
|
1128
|
+
npx @backtest-kit/cli --pnldebug --symbol BTCUSDT --priceopen 64069.50 --direction short --when "2025-02-25" --minutes 120
|
|
1129
|
+
```
|
|
1130
|
+
|
|
1131
|
+
Save as Markdown:
|
|
1132
|
+
|
|
1133
|
+
```bash
|
|
1134
|
+
npx @backtest-kit/cli --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
|
|
1135
|
+
# โ ./dump/BTCUSDT_long_67956.73_{timestamp}.md
|
|
1136
|
+
```
|
|
1137
|
+
|
|
1138
|
+
Override the output file name with `--output`:
|
|
1139
|
+
|
|
1140
|
+
```bash
|
|
1141
|
+
npx @backtest-kit/cli --pnldebug --priceopen 64069.50 --direction short --when "2025-02-25" --minutes 120 \
|
|
1142
|
+
--jsonl --output feb25_short_debug
|
|
1143
|
+
# โ ./dump/feb25_short_debug.jsonl
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
Or add it to `package.json`:
|
|
1147
|
+
|
|
1148
|
+
```json
|
|
1149
|
+
{
|
|
1150
|
+
"scripts": {
|
|
1151
|
+
"pnldebug": "npx @backtest-kit/cli --pnldebug --symbol BTCUSDT --priceopen 64069.50 --direction short --when \"2025-02-25\" --minutes 120"
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
```bash
|
|
1157
|
+
npm run pnldebug
|
|
1158
|
+
```
|
|
1159
|
+
|
|
1160
|
+
### Example stdout output
|
|
1161
|
+
|
|
1162
|
+
```
|
|
1163
|
+
Symbol: BTCUSDT | Direction: short | PriceOpen: 64069.50 | From: 2025-02-25T00:00:00.000Z | Minutes: 120
|
|
1164
|
+
|
|
1165
|
+
min | timestamp | close | pnl% | peak% | drawdown%
|
|
1166
|
+
-----------------------------------------------------------------------------------
|
|
1167
|
+
1 | 2025-02-25T00:01:00.000Z | 64020.10 | +0.08% | +0.08% | 0.00%
|
|
1168
|
+
2 | 2025-02-25T00:02:00.000Z | 64105.30 | -0.06% | +0.08% | -0.06%
|
|
1169
|
+
...
|
|
1170
|
+
120 | 2025-02-25T02:00:00.000Z | 63200.00 | +1.36% | +1.36% | -0.06%
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1068
1173
|
## ๐๏ธ Flushing Strategy Output (`--flush`)
|
|
1069
1174
|
|
|
1070
1175
|
`@backtest-kit/cli` can delete generated output folders from one or more strategy dump directories without touching cached candle data.
|
package/build/index.cjs
CHANGED
|
@@ -570,6 +570,22 @@ const getArgs = functoolsKit.singleshot(() => {
|
|
|
570
570
|
type: "boolean",
|
|
571
571
|
default: false,
|
|
572
572
|
},
|
|
573
|
+
pnldebug: {
|
|
574
|
+
type: "boolean",
|
|
575
|
+
default: false,
|
|
576
|
+
},
|
|
577
|
+
priceopen: {
|
|
578
|
+
type: "string",
|
|
579
|
+
default: "",
|
|
580
|
+
},
|
|
581
|
+
direction: {
|
|
582
|
+
type: "string",
|
|
583
|
+
default: "",
|
|
584
|
+
},
|
|
585
|
+
minutes: {
|
|
586
|
+
type: "string",
|
|
587
|
+
default: "",
|
|
588
|
+
},
|
|
573
589
|
init: {
|
|
574
590
|
type: "boolean",
|
|
575
591
|
default: false,
|
|
@@ -716,11 +732,11 @@ class SetupUtils {
|
|
|
716
732
|
BacktestKit.Markdown.enable();
|
|
717
733
|
BacktestKit.Report.enable();
|
|
718
734
|
BacktestKit.Dump.enable();
|
|
735
|
+
BacktestKit.State.enable();
|
|
719
736
|
BacktestKit.Memory.enable();
|
|
720
737
|
}
|
|
721
738
|
{
|
|
722
739
|
BacktestKit.Dump.useMarkdown();
|
|
723
|
-
BacktestKit.Memory.usePersist();
|
|
724
740
|
}
|
|
725
741
|
{
|
|
726
742
|
BacktestKit.StorageLive.usePersist();
|
|
@@ -734,6 +750,18 @@ class SetupUtils {
|
|
|
734
750
|
BacktestKit.NotificationLive.usePersist();
|
|
735
751
|
BacktestKit.NotificationBacktest.useMemory();
|
|
736
752
|
}
|
|
753
|
+
{
|
|
754
|
+
BacktestKit.RecentLive.usePersist();
|
|
755
|
+
BacktestKit.RecentBacktest.useMemory();
|
|
756
|
+
}
|
|
757
|
+
{
|
|
758
|
+
BacktestKit.MemoryLive.usePersist();
|
|
759
|
+
BacktestKit.MemoryBacktest.useLocal();
|
|
760
|
+
}
|
|
761
|
+
{
|
|
762
|
+
BacktestKit.StateLive.usePersist();
|
|
763
|
+
BacktestKit.StateBacktest.useLocal();
|
|
764
|
+
}
|
|
737
765
|
{
|
|
738
766
|
BacktestKit.Markdown.useDummy();
|
|
739
767
|
BacktestKit.Log.useJsonl();
|
|
@@ -776,6 +804,7 @@ class SetupUtils {
|
|
|
776
804
|
BacktestKit.PersistIntervalAdapter.clear();
|
|
777
805
|
BacktestKit.PersistMemoryAdapter.clear();
|
|
778
806
|
BacktestKit.PersistRecentAdapter.clear();
|
|
807
|
+
BacktestKit.PersistStateAdapter.clear();
|
|
779
808
|
}
|
|
780
809
|
{
|
|
781
810
|
BacktestKit.Dump.clear();
|
|
@@ -2965,14 +2994,14 @@ const cli = {
|
|
|
2965
2994
|
};
|
|
2966
2995
|
init();
|
|
2967
2996
|
|
|
2968
|
-
const MODES = ["backtest", "walker", "paper", "live", "pine", "editor", "dump", "flush", "init", "help", "version"];
|
|
2997
|
+
const MODES = ["backtest", "walker", "paper", "live", "pine", "editor", "dump", "pnldebug", "flush", "init", "help", "version"];
|
|
2969
2998
|
const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
|
|
2970
2999
|
const HELP_TEXT$1 = `
|
|
2971
3000
|
Example:
|
|
2972
3001
|
|
|
2973
3002
|
node ${ENTRY_PATH$1} --help
|
|
2974
3003
|
`.trimStart();
|
|
2975
|
-
const main$
|
|
3004
|
+
const main$e = async () => {
|
|
2976
3005
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
2977
3006
|
return;
|
|
2978
3007
|
}
|
|
@@ -2980,14 +3009,14 @@ const main$d = async () => {
|
|
|
2980
3009
|
if (MODES.some((mode) => values[mode])) {
|
|
2981
3010
|
return;
|
|
2982
3011
|
}
|
|
2983
|
-
process.stdout.write(`@backtest-kit/cli ${"7.
|
|
3012
|
+
process.stdout.write(`@backtest-kit/cli ${"7.5.0"}\n`);
|
|
2984
3013
|
process.stdout.write("\n");
|
|
2985
3014
|
process.stdout.write(`Run with --help to see available commands.\n`);
|
|
2986
3015
|
process.stdout.write("\n");
|
|
2987
3016
|
process.stdout.write(HELP_TEXT$1);
|
|
2988
3017
|
process.exit(0);
|
|
2989
3018
|
};
|
|
2990
|
-
main$
|
|
3019
|
+
main$e();
|
|
2991
3020
|
|
|
2992
3021
|
const notifyShutdown = functoolsKit.singleshot(async () => {
|
|
2993
3022
|
console.log("Graceful shutdown initiated. Press Ctrl+C again to force quit.");
|
|
@@ -3003,7 +3032,7 @@ const flush = async (entryPoint) => {
|
|
|
3003
3032
|
console.log(`Removed: ${target}`);
|
|
3004
3033
|
}
|
|
3005
3034
|
};
|
|
3006
|
-
const main$
|
|
3035
|
+
const main$d = async () => {
|
|
3007
3036
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
3008
3037
|
return;
|
|
3009
3038
|
}
|
|
@@ -3020,7 +3049,7 @@ const main$c = async () => {
|
|
|
3020
3049
|
}
|
|
3021
3050
|
process.exit(0);
|
|
3022
3051
|
};
|
|
3023
|
-
main$
|
|
3052
|
+
main$d();
|
|
3024
3053
|
|
|
3025
3054
|
const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
|
|
3026
3055
|
process.off("SIGINT", BEFORE_EXIT_FN$5);
|
|
@@ -3042,7 +3071,7 @@ const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
|
|
|
3042
3071
|
const listenGracefulShutdown$5 = functoolsKit.singleshot(() => {
|
|
3043
3072
|
process.on("SIGINT", BEFORE_EXIT_FN$5);
|
|
3044
3073
|
});
|
|
3045
|
-
const main$
|
|
3074
|
+
const main$c = async () => {
|
|
3046
3075
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
3047
3076
|
return;
|
|
3048
3077
|
}
|
|
@@ -3057,7 +3086,7 @@ const main$b = async () => {
|
|
|
3057
3086
|
await cli.backtestMainService.connect();
|
|
3058
3087
|
listenGracefulShutdown$5();
|
|
3059
3088
|
};
|
|
3060
|
-
main$
|
|
3089
|
+
main$c();
|
|
3061
3090
|
|
|
3062
3091
|
const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
|
|
3063
3092
|
process.off("SIGINT", BEFORE_EXIT_FN$4);
|
|
@@ -3075,7 +3104,7 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
|
|
|
3075
3104
|
const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
|
|
3076
3105
|
process.on("SIGINT", BEFORE_EXIT_FN$4);
|
|
3077
3106
|
});
|
|
3078
|
-
const main$
|
|
3107
|
+
const main$b = async () => {
|
|
3079
3108
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
3080
3109
|
return;
|
|
3081
3110
|
}
|
|
@@ -3091,7 +3120,7 @@ const main$a = async () => {
|
|
|
3091
3120
|
listenGracefulShutdown$4();
|
|
3092
3121
|
await cli.walkerMainService.connect();
|
|
3093
3122
|
};
|
|
3094
|
-
main$
|
|
3123
|
+
main$b();
|
|
3095
3124
|
|
|
3096
3125
|
const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
|
|
3097
3126
|
process.off("SIGINT", BEFORE_EXIT_FN$3);
|
|
@@ -3112,7 +3141,7 @@ const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
|
|
|
3112
3141
|
const listenGracefulShutdown$3 = functoolsKit.singleshot(() => {
|
|
3113
3142
|
process.on("SIGINT", BEFORE_EXIT_FN$3);
|
|
3114
3143
|
});
|
|
3115
|
-
const main$
|
|
3144
|
+
const main$a = async () => {
|
|
3116
3145
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
3117
3146
|
return;
|
|
3118
3147
|
}
|
|
@@ -3123,7 +3152,7 @@ const main$9 = async () => {
|
|
|
3123
3152
|
cli.paperMainService.connect();
|
|
3124
3153
|
listenGracefulShutdown$3();
|
|
3125
3154
|
};
|
|
3126
|
-
main$
|
|
3155
|
+
main$a();
|
|
3127
3156
|
|
|
3128
3157
|
const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
|
|
3129
3158
|
process.off("SIGINT", BEFORE_EXIT_FN$2);
|
|
@@ -3144,7 +3173,7 @@ const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
|
|
|
3144
3173
|
const listenGracefulShutdown$2 = functoolsKit.singleshot(() => {
|
|
3145
3174
|
process.on("SIGINT", BEFORE_EXIT_FN$2);
|
|
3146
3175
|
});
|
|
3147
|
-
const main$
|
|
3176
|
+
const main$9 = async () => {
|
|
3148
3177
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
3149
3178
|
return;
|
|
3150
3179
|
}
|
|
@@ -3155,7 +3184,7 @@ const main$8 = async () => {
|
|
|
3155
3184
|
await cli.liveMainService.connect();
|
|
3156
3185
|
listenGracefulShutdown$2();
|
|
3157
3186
|
};
|
|
3158
|
-
main$
|
|
3187
|
+
main$9();
|
|
3159
3188
|
|
|
3160
3189
|
const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
|
|
3161
3190
|
process.off("SIGINT", BEFORE_EXIT_FN$1);
|
|
@@ -3165,7 +3194,7 @@ const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
|
|
|
3165
3194
|
const listenGracefulShutdown$1 = functoolsKit.singleshot(() => {
|
|
3166
3195
|
process.on("SIGINT", BEFORE_EXIT_FN$1);
|
|
3167
3196
|
});
|
|
3168
|
-
const main$
|
|
3197
|
+
const main$8 = async () => {
|
|
3169
3198
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
3170
3199
|
return;
|
|
3171
3200
|
}
|
|
@@ -3175,7 +3204,7 @@ const main$7 = async () => {
|
|
|
3175
3204
|
}
|
|
3176
3205
|
listenGracefulShutdown$1();
|
|
3177
3206
|
};
|
|
3178
|
-
main$
|
|
3207
|
+
main$8();
|
|
3179
3208
|
|
|
3180
3209
|
const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
|
|
3181
3210
|
process.off("SIGINT", BEFORE_EXIT_FN);
|
|
@@ -3185,7 +3214,7 @@ const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
|
|
|
3185
3214
|
const listenGracefulShutdown = functoolsKit.singleshot(() => {
|
|
3186
3215
|
process.on("SIGINT", BEFORE_EXIT_FN);
|
|
3187
3216
|
});
|
|
3188
|
-
const main$
|
|
3217
|
+
const main$7 = async () => {
|
|
3189
3218
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
3190
3219
|
return;
|
|
3191
3220
|
}
|
|
@@ -3195,7 +3224,7 @@ const main$6 = async () => {
|
|
|
3195
3224
|
}
|
|
3196
3225
|
listenGracefulShutdown();
|
|
3197
3226
|
};
|
|
3198
|
-
main$
|
|
3227
|
+
main$7();
|
|
3199
3228
|
|
|
3200
3229
|
const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
3201
3230
|
const keys = Object.keys(schema);
|
|
@@ -3217,7 +3246,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
|
3217
3246
|
}
|
|
3218
3247
|
return rows;
|
|
3219
3248
|
};
|
|
3220
|
-
const main$
|
|
3249
|
+
const main$6 = async () => {
|
|
3221
3250
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
3222
3251
|
return;
|
|
3223
3252
|
}
|
|
@@ -3297,9 +3326,9 @@ const main$5 = async () => {
|
|
|
3297
3326
|
console.log(await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema));
|
|
3298
3327
|
process.exit(0);
|
|
3299
3328
|
};
|
|
3300
|
-
main$
|
|
3329
|
+
main$6();
|
|
3301
3330
|
|
|
3302
|
-
const main$
|
|
3331
|
+
const main$5 = async () => {
|
|
3303
3332
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
3304
3333
|
return;
|
|
3305
3334
|
}
|
|
@@ -3334,9 +3363,9 @@ const main$4 = async () => {
|
|
|
3334
3363
|
};
|
|
3335
3364
|
process.on("SIGINT", beforeExit);
|
|
3336
3365
|
};
|
|
3337
|
-
main$
|
|
3366
|
+
main$5();
|
|
3338
3367
|
|
|
3339
|
-
const main$
|
|
3368
|
+
const main$4 = async () => {
|
|
3340
3369
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
3341
3370
|
return;
|
|
3342
3371
|
}
|
|
@@ -3397,6 +3426,102 @@ const main$3 = async () => {
|
|
|
3397
3426
|
console.log(JSON.stringify(candles, null, 2));
|
|
3398
3427
|
process.exit(0);
|
|
3399
3428
|
};
|
|
3429
|
+
main$4();
|
|
3430
|
+
|
|
3431
|
+
const main$3 = async () => {
|
|
3432
|
+
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
3433
|
+
return;
|
|
3434
|
+
}
|
|
3435
|
+
const { values } = getArgs();
|
|
3436
|
+
if (!values.pnldebug) {
|
|
3437
|
+
return;
|
|
3438
|
+
}
|
|
3439
|
+
await cli.moduleConnectionService.loadModule("./pnldebug.module");
|
|
3440
|
+
{
|
|
3441
|
+
await cli.exchangeSchemaService.addSchema();
|
|
3442
|
+
await cli.symbolSchemaService.addSchema();
|
|
3443
|
+
}
|
|
3444
|
+
const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
|
|
3445
|
+
const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
|
|
3446
|
+
const symbol = values.symbol || "BTCUSDT";
|
|
3447
|
+
const priceOpenStr = values.priceopen;
|
|
3448
|
+
if (!priceOpenStr) {
|
|
3449
|
+
console.error("Error: --priceopen is required");
|
|
3450
|
+
process.exit(1);
|
|
3451
|
+
}
|
|
3452
|
+
const priceOpen = parseFloat(priceOpenStr);
|
|
3453
|
+
if (isNaN(priceOpen)) {
|
|
3454
|
+
console.error(`Error: --priceopen must be a number, got: ${priceOpenStr}`);
|
|
3455
|
+
process.exit(1);
|
|
3456
|
+
}
|
|
3457
|
+
const direction = (values.direction || "long").toLowerCase();
|
|
3458
|
+
if (direction !== "long" && direction !== "short") {
|
|
3459
|
+
console.error(`Error: --direction must be 'long' or 'short', got: ${direction}`);
|
|
3460
|
+
process.exit(1);
|
|
3461
|
+
}
|
|
3462
|
+
const whenStr = values.when || Date.now().toString();
|
|
3463
|
+
const whenStamp = Date.parse(whenStr);
|
|
3464
|
+
const when = isNaN(whenStamp) ? new Date() : new Date(whenStamp);
|
|
3465
|
+
const timestamp = BacktestKit.alignToInterval(when, "1m").getTime();
|
|
3466
|
+
const minutesStr = values.minutes || "60";
|
|
3467
|
+
const minutesNum = parseInt(minutesStr);
|
|
3468
|
+
const minutes = isNaN(minutesNum) ? 60 : minutesNum;
|
|
3469
|
+
const candles = await BacktestKit.Exchange.getRawCandles(symbol, "1m", { exchangeName }, minutes, undefined, timestamp);
|
|
3470
|
+
if (candles.length === 0) {
|
|
3471
|
+
console.error("Error: no candles returned for the given parameters");
|
|
3472
|
+
process.exit(1);
|
|
3473
|
+
}
|
|
3474
|
+
let peak = 0;
|
|
3475
|
+
let drawdown = 0;
|
|
3476
|
+
const rows = candles.map((c, i) => {
|
|
3477
|
+
const pnl = direction === "short"
|
|
3478
|
+
? (priceOpen - c.close) / priceOpen * 100
|
|
3479
|
+
: (c.close - priceOpen) / priceOpen * 100;
|
|
3480
|
+
if (pnl > peak)
|
|
3481
|
+
peak = pnl;
|
|
3482
|
+
if (pnl < drawdown)
|
|
3483
|
+
drawdown = pnl;
|
|
3484
|
+
return { min: i + 1, timestamp: c.timestamp, close: c.close, pnl, peak, drawdown };
|
|
3485
|
+
});
|
|
3486
|
+
const dumpName = values.output || `${symbol}_${direction}_${priceOpen}_${timestamp}`;
|
|
3487
|
+
const dumpDir = path.join(process.cwd(), "dump");
|
|
3488
|
+
if (values.json) {
|
|
3489
|
+
const filePath = path.resolve(dumpDir, `${dumpName}.json`);
|
|
3490
|
+
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
3491
|
+
await fs$1.writeFile(filePath, JSON.stringify(rows, null, 2), "utf-8");
|
|
3492
|
+
console.log(`Saved: ${filePath}`);
|
|
3493
|
+
process.exit(0);
|
|
3494
|
+
}
|
|
3495
|
+
if (values.jsonl) {
|
|
3496
|
+
const filePath = path.resolve(dumpDir, `${dumpName}.jsonl`);
|
|
3497
|
+
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
3498
|
+
await fs$1.writeFile(filePath, rows.map((r) => JSON.stringify(r)).join("\n"), "utf-8");
|
|
3499
|
+
console.log(`Saved: ${filePath}`);
|
|
3500
|
+
process.exit(0);
|
|
3501
|
+
}
|
|
3502
|
+
if (values.markdown) {
|
|
3503
|
+
const header = `| min | timestamp | close | pnl% | peak% | drawdown% |\n| --- | --- | --- | --- | --- | --- |`;
|
|
3504
|
+
const mdRows = rows.map((r) => `| ${r.min} | ${new Date(r.timestamp).toISOString()} | ${r.close.toFixed(2)} | ${(r.pnl >= 0 ? "+" : "") + r.pnl.toFixed(2)}% | +${r.peak.toFixed(2)}% | ${r.drawdown.toFixed(2)}% |`);
|
|
3505
|
+
const filePath = path.resolve(dumpDir, `${dumpName}.md`);
|
|
3506
|
+
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
3507
|
+
await fs$1.writeFile(filePath, [header, ...mdRows].join("\n"), "utf-8");
|
|
3508
|
+
console.log(`Saved: ${filePath}`);
|
|
3509
|
+
process.exit(0);
|
|
3510
|
+
}
|
|
3511
|
+
console.log(`Symbol: ${symbol} | Direction: ${direction} | PriceOpen: ${priceOpen} | From: ${new Date(timestamp).toISOString()} | Minutes: ${minutes}\n`);
|
|
3512
|
+
console.log(`${"min".padStart(5)} | ${"timestamp".padEnd(24)} | ${"close".padStart(12)} | ${"pnl%".padStart(8)} | ${"peak%".padStart(8)} | ${"drawdown%".padStart(10)}`);
|
|
3513
|
+
console.log("-".repeat(83));
|
|
3514
|
+
for (const r of rows) {
|
|
3515
|
+
const min = String(r.min).padStart(5);
|
|
3516
|
+
const ts = new Date(r.timestamp).toISOString().padEnd(24);
|
|
3517
|
+
const close = r.close.toFixed(2).padStart(12);
|
|
3518
|
+
const pnlStr = (r.pnl >= 0 ? "+" : "") + r.pnl.toFixed(2) + "%";
|
|
3519
|
+
const peakStr = "+" + r.peak.toFixed(2) + "%";
|
|
3520
|
+
const drawdownStr = r.drawdown.toFixed(2) + "%";
|
|
3521
|
+
console.log(`${min} | ${ts} | ${close} | ${pnlStr.padStart(8)} | ${peakStr.padStart(8)} | ${drawdownStr.padStart(10)}`);
|
|
3522
|
+
}
|
|
3523
|
+
process.exit(0);
|
|
3524
|
+
};
|
|
3400
3525
|
main$3();
|
|
3401
3526
|
|
|
3402
3527
|
const __filename$1 = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
@@ -3515,6 +3640,7 @@ Modes:
|
|
|
3515
3640
|
--pine <entry> Execute a local .pine indicator file
|
|
3516
3641
|
--editor Open the Pine Script visual editor in the browser
|
|
3517
3642
|
--dump Fetch and save raw OHLCV candles
|
|
3643
|
+
--pnldebug Simulate PnL per minute for a given entry price and direction
|
|
3518
3644
|
--flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
|
|
3519
3645
|
--init Scaffold a new project in the current directory
|
|
3520
3646
|
--help Print this help message
|
|
@@ -3587,6 +3713,21 @@ Candle dump flags (--dump):
|
|
|
3587
3713
|
|
|
3588
3714
|
Module file ./modules/dump.module is loaded automatically if it exists.
|
|
3589
3715
|
|
|
3716
|
+
PnL debug flags (--pnldebug):
|
|
3717
|
+
|
|
3718
|
+
--symbol <string> Trading pair (default: BTCUSDT)
|
|
3719
|
+
--priceopen <number> Entry price (required)
|
|
3720
|
+
--direction <string> Position direction: long or short (default: long)
|
|
3721
|
+
--when <string> Start timestamp โ ISO 8601 or Unix ms (default: now)
|
|
3722
|
+
--minutes <string> Number of 1m candles to simulate (default: 60)
|
|
3723
|
+
--exchange <string> Exchange name (default: first registered)
|
|
3724
|
+
--output <string> Output file base name (default: {SYMBOL}_{DIRECTION}_{PRICEOPEN}_{TIMESTAMP})
|
|
3725
|
+
--json Save as JSON array to ./dump/<output>.json
|
|
3726
|
+
--jsonl Save as JSONL to ./dump/<output>.jsonl
|
|
3727
|
+
--markdown Save as Markdown table to ./dump/<output>.md
|
|
3728
|
+
|
|
3729
|
+
Module file ./modules/pnldebug.module is loaded automatically if it exists.
|
|
3730
|
+
|
|
3590
3731
|
Flush flags (--flush):
|
|
3591
3732
|
|
|
3592
3733
|
One or more positional entry points. For each entry point the following
|
|
@@ -3609,6 +3750,7 @@ Module hooks (loaded automatically by each mode):
|
|
|
3609
3750
|
modules/pine.module --pine Exchange schema for PineScript runs
|
|
3610
3751
|
modules/editor.module --editor Exchange schema for the visual Pine editor
|
|
3611
3752
|
modules/dump.module --dump Exchange schema for candle dumps
|
|
3753
|
+
modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
|
|
3612
3754
|
|
|
3613
3755
|
--flush has no associated module. It only removes dump subdirectories.
|
|
3614
3756
|
|
|
@@ -3632,6 +3774,8 @@ Examples:
|
|
|
3632
3774
|
node ${ENTRY_PATH} --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --jsonl
|
|
3633
3775
|
node ${ENTRY_PATH} --editor
|
|
3634
3776
|
node ${ENTRY_PATH} --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
|
|
3777
|
+
node ${ENTRY_PATH} --pnldebug --symbol BTCUSDT --priceopen 64069.50 --direction short --when "2025-02-25" --minutes 120
|
|
3778
|
+
node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
|
|
3635
3779
|
node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
|
|
3636
3780
|
node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
|
|
3637
3781
|
node ${ENTRY_PATH} --init --output my-trading-bot
|
|
@@ -3644,7 +3788,7 @@ const main$1 = async () => {
|
|
|
3644
3788
|
if (!values.help) {
|
|
3645
3789
|
return;
|
|
3646
3790
|
}
|
|
3647
|
-
process.stdout.write(`@backtest-kit/cli ${"7.
|
|
3791
|
+
process.stdout.write(`@backtest-kit/cli ${"7.5.0"}\n\n`);
|
|
3648
3792
|
process.stdout.write(HELP_TEXT);
|
|
3649
3793
|
process.exit(0);
|
|
3650
3794
|
};
|
|
@@ -3658,7 +3802,7 @@ const main = async () => {
|
|
|
3658
3802
|
if (!values.version) {
|
|
3659
3803
|
return;
|
|
3660
3804
|
}
|
|
3661
|
-
process.stdout.write(`@backtest-kit/cli ${"7.
|
|
3805
|
+
process.stdout.write(`@backtest-kit/cli ${"7.5.0"}\n`);
|
|
3662
3806
|
process.exit(0);
|
|
3663
3807
|
};
|
|
3664
3808
|
main();
|
package/build/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as BacktestKit from 'backtest-kit';
|
|
3
|
-
import { setConfig, Log, listExchangeSchema, addExchangeSchema, roundTicks, listFrameSchema, addFrameSchema, listenDoneLive, listenDoneBacktest, shutdown, listenSignal, Notification, Recent, Storage, Markdown, Report, Dump, Memory, StorageLive, StorageBacktest, RecentLive, RecentBacktest, NotificationLive, NotificationBacktest, MarkdownWriter, ReportWriter, PersistSignalAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistPartialAdapter, PersistBreakevenAdapter, PersistCandleAdapter, PersistStorageAdapter, PersistNotificationAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistIntervalAdapter, PersistMemoryAdapter, PersistRecentAdapter, listStrategySchema, overrideExchangeSchema, Backtest, Session, Cache, Interval, alignToInterval, addWalkerSchema, overrideWalkerSchema, Walker, listenDoneWalker, Live, getCandles, checkCandles, warmCandles, listenRisk, listenStrategyCommit, listenSync, listenSignalNotify, Exchange } from 'backtest-kit';
|
|
3
|
+
import { setConfig, Log, listExchangeSchema, addExchangeSchema, roundTicks, listFrameSchema, addFrameSchema, listenDoneLive, listenDoneBacktest, shutdown, listenSignal, Notification, Recent, Storage, Markdown, Report, Dump, State, Memory, StorageLive, StorageBacktest, RecentLive, RecentBacktest, NotificationLive, NotificationBacktest, MemoryLive, MemoryBacktest, StateLive, StateBacktest, MarkdownWriter, ReportWriter, PersistSignalAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistPartialAdapter, PersistBreakevenAdapter, PersistCandleAdapter, PersistStorageAdapter, PersistNotificationAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistIntervalAdapter, PersistMemoryAdapter, PersistRecentAdapter, PersistStateAdapter, listStrategySchema, overrideExchangeSchema, Backtest, Session, Cache, Interval, alignToInterval, addWalkerSchema, overrideWalkerSchema, Walker, listenDoneWalker, Live, getCandles, checkCandles, warmCandles, listenRisk, listenStrategyCommit, listenSync, listenSignalNotify, Exchange } from 'backtest-kit';
|
|
4
4
|
import { getErrorMessage, errorData, singleshot, str, BehaviorSubject, compose, createAwaiter, execpool, queued, sleep, randomString, TIMEOUT_SYMBOL, typo, retry, trycatch, memoize, isObject } from 'functools-kit';
|
|
5
5
|
import fs, { constants } from 'fs';
|
|
6
6
|
import * as stackTrace from 'stack-trace';
|
|
@@ -545,6 +545,22 @@ const getArgs = singleshot(() => {
|
|
|
545
545
|
type: "boolean",
|
|
546
546
|
default: false,
|
|
547
547
|
},
|
|
548
|
+
pnldebug: {
|
|
549
|
+
type: "boolean",
|
|
550
|
+
default: false,
|
|
551
|
+
},
|
|
552
|
+
priceopen: {
|
|
553
|
+
type: "string",
|
|
554
|
+
default: "",
|
|
555
|
+
},
|
|
556
|
+
direction: {
|
|
557
|
+
type: "string",
|
|
558
|
+
default: "",
|
|
559
|
+
},
|
|
560
|
+
minutes: {
|
|
561
|
+
type: "string",
|
|
562
|
+
default: "",
|
|
563
|
+
},
|
|
548
564
|
init: {
|
|
549
565
|
type: "boolean",
|
|
550
566
|
default: false,
|
|
@@ -691,11 +707,11 @@ class SetupUtils {
|
|
|
691
707
|
Markdown.enable();
|
|
692
708
|
Report.enable();
|
|
693
709
|
Dump.enable();
|
|
710
|
+
State.enable();
|
|
694
711
|
Memory.enable();
|
|
695
712
|
}
|
|
696
713
|
{
|
|
697
714
|
Dump.useMarkdown();
|
|
698
|
-
Memory.usePersist();
|
|
699
715
|
}
|
|
700
716
|
{
|
|
701
717
|
StorageLive.usePersist();
|
|
@@ -709,6 +725,18 @@ class SetupUtils {
|
|
|
709
725
|
NotificationLive.usePersist();
|
|
710
726
|
NotificationBacktest.useMemory();
|
|
711
727
|
}
|
|
728
|
+
{
|
|
729
|
+
RecentLive.usePersist();
|
|
730
|
+
RecentBacktest.useMemory();
|
|
731
|
+
}
|
|
732
|
+
{
|
|
733
|
+
MemoryLive.usePersist();
|
|
734
|
+
MemoryBacktest.useLocal();
|
|
735
|
+
}
|
|
736
|
+
{
|
|
737
|
+
StateLive.usePersist();
|
|
738
|
+
StateBacktest.useLocal();
|
|
739
|
+
}
|
|
712
740
|
{
|
|
713
741
|
Markdown.useDummy();
|
|
714
742
|
Log.useJsonl();
|
|
@@ -751,6 +779,7 @@ class SetupUtils {
|
|
|
751
779
|
PersistIntervalAdapter.clear();
|
|
752
780
|
PersistMemoryAdapter.clear();
|
|
753
781
|
PersistRecentAdapter.clear();
|
|
782
|
+
PersistStateAdapter.clear();
|
|
754
783
|
}
|
|
755
784
|
{
|
|
756
785
|
Dump.clear();
|
|
@@ -2936,14 +2965,14 @@ const cli = {
|
|
|
2936
2965
|
};
|
|
2937
2966
|
init();
|
|
2938
2967
|
|
|
2939
|
-
const MODES = ["backtest", "walker", "paper", "live", "pine", "editor", "dump", "flush", "init", "help", "version"];
|
|
2968
|
+
const MODES = ["backtest", "walker", "paper", "live", "pine", "editor", "dump", "pnldebug", "flush", "init", "help", "version"];
|
|
2940
2969
|
const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
|
|
2941
2970
|
const HELP_TEXT$1 = `
|
|
2942
2971
|
Example:
|
|
2943
2972
|
|
|
2944
2973
|
node ${ENTRY_PATH$1} --help
|
|
2945
2974
|
`.trimStart();
|
|
2946
|
-
const main$
|
|
2975
|
+
const main$e = async () => {
|
|
2947
2976
|
if (!getEntry(import.meta.url)) {
|
|
2948
2977
|
return;
|
|
2949
2978
|
}
|
|
@@ -2951,14 +2980,14 @@ const main$d = async () => {
|
|
|
2951
2980
|
if (MODES.some((mode) => values[mode])) {
|
|
2952
2981
|
return;
|
|
2953
2982
|
}
|
|
2954
|
-
process.stdout.write(`@backtest-kit/cli ${"7.
|
|
2983
|
+
process.stdout.write(`@backtest-kit/cli ${"7.5.0"}\n`);
|
|
2955
2984
|
process.stdout.write("\n");
|
|
2956
2985
|
process.stdout.write(`Run with --help to see available commands.\n`);
|
|
2957
2986
|
process.stdout.write("\n");
|
|
2958
2987
|
process.stdout.write(HELP_TEXT$1);
|
|
2959
2988
|
process.exit(0);
|
|
2960
2989
|
};
|
|
2961
|
-
main$
|
|
2990
|
+
main$e();
|
|
2962
2991
|
|
|
2963
2992
|
const notifyShutdown = singleshot(async () => {
|
|
2964
2993
|
console.log("Graceful shutdown initiated. Press Ctrl+C again to force quit.");
|
|
@@ -2974,7 +3003,7 @@ const flush = async (entryPoint) => {
|
|
|
2974
3003
|
console.log(`Removed: ${target}`);
|
|
2975
3004
|
}
|
|
2976
3005
|
};
|
|
2977
|
-
const main$
|
|
3006
|
+
const main$d = async () => {
|
|
2978
3007
|
if (!getEntry(import.meta.url)) {
|
|
2979
3008
|
return;
|
|
2980
3009
|
}
|
|
@@ -2991,7 +3020,7 @@ const main$c = async () => {
|
|
|
2991
3020
|
}
|
|
2992
3021
|
process.exit(0);
|
|
2993
3022
|
};
|
|
2994
|
-
main$
|
|
3023
|
+
main$d();
|
|
2995
3024
|
|
|
2996
3025
|
const BEFORE_EXIT_FN$5 = singleshot(async () => {
|
|
2997
3026
|
process.off("SIGINT", BEFORE_EXIT_FN$5);
|
|
@@ -3013,7 +3042,7 @@ const BEFORE_EXIT_FN$5 = singleshot(async () => {
|
|
|
3013
3042
|
const listenGracefulShutdown$5 = singleshot(() => {
|
|
3014
3043
|
process.on("SIGINT", BEFORE_EXIT_FN$5);
|
|
3015
3044
|
});
|
|
3016
|
-
const main$
|
|
3045
|
+
const main$c = async () => {
|
|
3017
3046
|
if (!getEntry(import.meta.url)) {
|
|
3018
3047
|
return;
|
|
3019
3048
|
}
|
|
@@ -3028,7 +3057,7 @@ const main$b = async () => {
|
|
|
3028
3057
|
await cli.backtestMainService.connect();
|
|
3029
3058
|
listenGracefulShutdown$5();
|
|
3030
3059
|
};
|
|
3031
|
-
main$
|
|
3060
|
+
main$c();
|
|
3032
3061
|
|
|
3033
3062
|
const BEFORE_EXIT_FN$4 = singleshot(async () => {
|
|
3034
3063
|
process.off("SIGINT", BEFORE_EXIT_FN$4);
|
|
@@ -3046,7 +3075,7 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
|
|
|
3046
3075
|
const listenGracefulShutdown$4 = singleshot(() => {
|
|
3047
3076
|
process.on("SIGINT", BEFORE_EXIT_FN$4);
|
|
3048
3077
|
});
|
|
3049
|
-
const main$
|
|
3078
|
+
const main$b = async () => {
|
|
3050
3079
|
if (!getEntry(import.meta.url)) {
|
|
3051
3080
|
return;
|
|
3052
3081
|
}
|
|
@@ -3062,7 +3091,7 @@ const main$a = async () => {
|
|
|
3062
3091
|
listenGracefulShutdown$4();
|
|
3063
3092
|
await cli.walkerMainService.connect();
|
|
3064
3093
|
};
|
|
3065
|
-
main$
|
|
3094
|
+
main$b();
|
|
3066
3095
|
|
|
3067
3096
|
const BEFORE_EXIT_FN$3 = singleshot(async () => {
|
|
3068
3097
|
process.off("SIGINT", BEFORE_EXIT_FN$3);
|
|
@@ -3083,7 +3112,7 @@ const BEFORE_EXIT_FN$3 = singleshot(async () => {
|
|
|
3083
3112
|
const listenGracefulShutdown$3 = singleshot(() => {
|
|
3084
3113
|
process.on("SIGINT", BEFORE_EXIT_FN$3);
|
|
3085
3114
|
});
|
|
3086
|
-
const main$
|
|
3115
|
+
const main$a = async () => {
|
|
3087
3116
|
if (!getEntry(import.meta.url)) {
|
|
3088
3117
|
return;
|
|
3089
3118
|
}
|
|
@@ -3094,7 +3123,7 @@ const main$9 = async () => {
|
|
|
3094
3123
|
cli.paperMainService.connect();
|
|
3095
3124
|
listenGracefulShutdown$3();
|
|
3096
3125
|
};
|
|
3097
|
-
main$
|
|
3126
|
+
main$a();
|
|
3098
3127
|
|
|
3099
3128
|
const BEFORE_EXIT_FN$2 = singleshot(async () => {
|
|
3100
3129
|
process.off("SIGINT", BEFORE_EXIT_FN$2);
|
|
@@ -3115,7 +3144,7 @@ const BEFORE_EXIT_FN$2 = singleshot(async () => {
|
|
|
3115
3144
|
const listenGracefulShutdown$2 = singleshot(() => {
|
|
3116
3145
|
process.on("SIGINT", BEFORE_EXIT_FN$2);
|
|
3117
3146
|
});
|
|
3118
|
-
const main$
|
|
3147
|
+
const main$9 = async () => {
|
|
3119
3148
|
if (!getEntry(import.meta.url)) {
|
|
3120
3149
|
return;
|
|
3121
3150
|
}
|
|
@@ -3126,7 +3155,7 @@ const main$8 = async () => {
|
|
|
3126
3155
|
await cli.liveMainService.connect();
|
|
3127
3156
|
listenGracefulShutdown$2();
|
|
3128
3157
|
};
|
|
3129
|
-
main$
|
|
3158
|
+
main$9();
|
|
3130
3159
|
|
|
3131
3160
|
const BEFORE_EXIT_FN$1 = singleshot(async () => {
|
|
3132
3161
|
process.off("SIGINT", BEFORE_EXIT_FN$1);
|
|
@@ -3136,7 +3165,7 @@ const BEFORE_EXIT_FN$1 = singleshot(async () => {
|
|
|
3136
3165
|
const listenGracefulShutdown$1 = singleshot(() => {
|
|
3137
3166
|
process.on("SIGINT", BEFORE_EXIT_FN$1);
|
|
3138
3167
|
});
|
|
3139
|
-
const main$
|
|
3168
|
+
const main$8 = async () => {
|
|
3140
3169
|
if (!getEntry(import.meta.url)) {
|
|
3141
3170
|
return;
|
|
3142
3171
|
}
|
|
@@ -3146,7 +3175,7 @@ const main$7 = async () => {
|
|
|
3146
3175
|
}
|
|
3147
3176
|
listenGracefulShutdown$1();
|
|
3148
3177
|
};
|
|
3149
|
-
main$
|
|
3178
|
+
main$8();
|
|
3150
3179
|
|
|
3151
3180
|
const BEFORE_EXIT_FN = singleshot(async () => {
|
|
3152
3181
|
process.off("SIGINT", BEFORE_EXIT_FN);
|
|
@@ -3156,7 +3185,7 @@ const BEFORE_EXIT_FN = singleshot(async () => {
|
|
|
3156
3185
|
const listenGracefulShutdown = singleshot(() => {
|
|
3157
3186
|
process.on("SIGINT", BEFORE_EXIT_FN);
|
|
3158
3187
|
});
|
|
3159
|
-
const main$
|
|
3188
|
+
const main$7 = async () => {
|
|
3160
3189
|
if (!getEntry(import.meta.url)) {
|
|
3161
3190
|
return;
|
|
3162
3191
|
}
|
|
@@ -3166,7 +3195,7 @@ const main$6 = async () => {
|
|
|
3166
3195
|
}
|
|
3167
3196
|
listenGracefulShutdown();
|
|
3168
3197
|
};
|
|
3169
|
-
main$
|
|
3198
|
+
main$7();
|
|
3170
3199
|
|
|
3171
3200
|
const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
3172
3201
|
const keys = Object.keys(schema);
|
|
@@ -3188,7 +3217,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
|
3188
3217
|
}
|
|
3189
3218
|
return rows;
|
|
3190
3219
|
};
|
|
3191
|
-
const main$
|
|
3220
|
+
const main$6 = async () => {
|
|
3192
3221
|
if (!getEntry(import.meta.url)) {
|
|
3193
3222
|
return;
|
|
3194
3223
|
}
|
|
@@ -3268,9 +3297,9 @@ const main$5 = async () => {
|
|
|
3268
3297
|
console.log(await toMarkdown(signalId, plots, signalSchema));
|
|
3269
3298
|
process.exit(0);
|
|
3270
3299
|
};
|
|
3271
|
-
main$
|
|
3300
|
+
main$6();
|
|
3272
3301
|
|
|
3273
|
-
const main$
|
|
3302
|
+
const main$5 = async () => {
|
|
3274
3303
|
if (!getEntry(import.meta.url)) {
|
|
3275
3304
|
return;
|
|
3276
3305
|
}
|
|
@@ -3305,9 +3334,9 @@ const main$4 = async () => {
|
|
|
3305
3334
|
};
|
|
3306
3335
|
process.on("SIGINT", beforeExit);
|
|
3307
3336
|
};
|
|
3308
|
-
main$
|
|
3337
|
+
main$5();
|
|
3309
3338
|
|
|
3310
|
-
const main$
|
|
3339
|
+
const main$4 = async () => {
|
|
3311
3340
|
if (!getEntry(import.meta.url)) {
|
|
3312
3341
|
return;
|
|
3313
3342
|
}
|
|
@@ -3368,6 +3397,102 @@ const main$3 = async () => {
|
|
|
3368
3397
|
console.log(JSON.stringify(candles, null, 2));
|
|
3369
3398
|
process.exit(0);
|
|
3370
3399
|
};
|
|
3400
|
+
main$4();
|
|
3401
|
+
|
|
3402
|
+
const main$3 = async () => {
|
|
3403
|
+
if (!getEntry(import.meta.url)) {
|
|
3404
|
+
return;
|
|
3405
|
+
}
|
|
3406
|
+
const { values } = getArgs();
|
|
3407
|
+
if (!values.pnldebug) {
|
|
3408
|
+
return;
|
|
3409
|
+
}
|
|
3410
|
+
await cli.moduleConnectionService.loadModule("./pnldebug.module");
|
|
3411
|
+
{
|
|
3412
|
+
await cli.exchangeSchemaService.addSchema();
|
|
3413
|
+
await cli.symbolSchemaService.addSchema();
|
|
3414
|
+
}
|
|
3415
|
+
const [defaultExchangeName = null] = await listExchangeSchema();
|
|
3416
|
+
const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
|
|
3417
|
+
const symbol = values.symbol || "BTCUSDT";
|
|
3418
|
+
const priceOpenStr = values.priceopen;
|
|
3419
|
+
if (!priceOpenStr) {
|
|
3420
|
+
console.error("Error: --priceopen is required");
|
|
3421
|
+
process.exit(1);
|
|
3422
|
+
}
|
|
3423
|
+
const priceOpen = parseFloat(priceOpenStr);
|
|
3424
|
+
if (isNaN(priceOpen)) {
|
|
3425
|
+
console.error(`Error: --priceopen must be a number, got: ${priceOpenStr}`);
|
|
3426
|
+
process.exit(1);
|
|
3427
|
+
}
|
|
3428
|
+
const direction = (values.direction || "long").toLowerCase();
|
|
3429
|
+
if (direction !== "long" && direction !== "short") {
|
|
3430
|
+
console.error(`Error: --direction must be 'long' or 'short', got: ${direction}`);
|
|
3431
|
+
process.exit(1);
|
|
3432
|
+
}
|
|
3433
|
+
const whenStr = values.when || Date.now().toString();
|
|
3434
|
+
const whenStamp = Date.parse(whenStr);
|
|
3435
|
+
const when = isNaN(whenStamp) ? new Date() : new Date(whenStamp);
|
|
3436
|
+
const timestamp = alignToInterval(when, "1m").getTime();
|
|
3437
|
+
const minutesStr = values.minutes || "60";
|
|
3438
|
+
const minutesNum = parseInt(minutesStr);
|
|
3439
|
+
const minutes = isNaN(minutesNum) ? 60 : minutesNum;
|
|
3440
|
+
const candles = await Exchange.getRawCandles(symbol, "1m", { exchangeName }, minutes, undefined, timestamp);
|
|
3441
|
+
if (candles.length === 0) {
|
|
3442
|
+
console.error("Error: no candles returned for the given parameters");
|
|
3443
|
+
process.exit(1);
|
|
3444
|
+
}
|
|
3445
|
+
let peak = 0;
|
|
3446
|
+
let drawdown = 0;
|
|
3447
|
+
const rows = candles.map((c, i) => {
|
|
3448
|
+
const pnl = direction === "short"
|
|
3449
|
+
? (priceOpen - c.close) / priceOpen * 100
|
|
3450
|
+
: (c.close - priceOpen) / priceOpen * 100;
|
|
3451
|
+
if (pnl > peak)
|
|
3452
|
+
peak = pnl;
|
|
3453
|
+
if (pnl < drawdown)
|
|
3454
|
+
drawdown = pnl;
|
|
3455
|
+
return { min: i + 1, timestamp: c.timestamp, close: c.close, pnl, peak, drawdown };
|
|
3456
|
+
});
|
|
3457
|
+
const dumpName = values.output || `${symbol}_${direction}_${priceOpen}_${timestamp}`;
|
|
3458
|
+
const dumpDir = join(process.cwd(), "dump");
|
|
3459
|
+
if (values.json) {
|
|
3460
|
+
const filePath = resolve(dumpDir, `${dumpName}.json`);
|
|
3461
|
+
await mkdir(dumpDir, { recursive: true });
|
|
3462
|
+
await writeFile(filePath, JSON.stringify(rows, null, 2), "utf-8");
|
|
3463
|
+
console.log(`Saved: ${filePath}`);
|
|
3464
|
+
process.exit(0);
|
|
3465
|
+
}
|
|
3466
|
+
if (values.jsonl) {
|
|
3467
|
+
const filePath = resolve(dumpDir, `${dumpName}.jsonl`);
|
|
3468
|
+
await mkdir(dumpDir, { recursive: true });
|
|
3469
|
+
await writeFile(filePath, rows.map((r) => JSON.stringify(r)).join("\n"), "utf-8");
|
|
3470
|
+
console.log(`Saved: ${filePath}`);
|
|
3471
|
+
process.exit(0);
|
|
3472
|
+
}
|
|
3473
|
+
if (values.markdown) {
|
|
3474
|
+
const header = `| min | timestamp | close | pnl% | peak% | drawdown% |\n| --- | --- | --- | --- | --- | --- |`;
|
|
3475
|
+
const mdRows = rows.map((r) => `| ${r.min} | ${new Date(r.timestamp).toISOString()} | ${r.close.toFixed(2)} | ${(r.pnl >= 0 ? "+" : "") + r.pnl.toFixed(2)}% | +${r.peak.toFixed(2)}% | ${r.drawdown.toFixed(2)}% |`);
|
|
3476
|
+
const filePath = resolve(dumpDir, `${dumpName}.md`);
|
|
3477
|
+
await mkdir(dumpDir, { recursive: true });
|
|
3478
|
+
await writeFile(filePath, [header, ...mdRows].join("\n"), "utf-8");
|
|
3479
|
+
console.log(`Saved: ${filePath}`);
|
|
3480
|
+
process.exit(0);
|
|
3481
|
+
}
|
|
3482
|
+
console.log(`Symbol: ${symbol} | Direction: ${direction} | PriceOpen: ${priceOpen} | From: ${new Date(timestamp).toISOString()} | Minutes: ${minutes}\n`);
|
|
3483
|
+
console.log(`${"min".padStart(5)} | ${"timestamp".padEnd(24)} | ${"close".padStart(12)} | ${"pnl%".padStart(8)} | ${"peak%".padStart(8)} | ${"drawdown%".padStart(10)}`);
|
|
3484
|
+
console.log("-".repeat(83));
|
|
3485
|
+
for (const r of rows) {
|
|
3486
|
+
const min = String(r.min).padStart(5);
|
|
3487
|
+
const ts = new Date(r.timestamp).toISOString().padEnd(24);
|
|
3488
|
+
const close = r.close.toFixed(2).padStart(12);
|
|
3489
|
+
const pnlStr = (r.pnl >= 0 ? "+" : "") + r.pnl.toFixed(2) + "%";
|
|
3490
|
+
const peakStr = "+" + r.peak.toFixed(2) + "%";
|
|
3491
|
+
const drawdownStr = r.drawdown.toFixed(2) + "%";
|
|
3492
|
+
console.log(`${min} | ${ts} | ${close} | ${pnlStr.padStart(8)} | ${peakStr.padStart(8)} | ${drawdownStr.padStart(10)}`);
|
|
3493
|
+
}
|
|
3494
|
+
process.exit(0);
|
|
3495
|
+
};
|
|
3371
3496
|
main$3();
|
|
3372
3497
|
|
|
3373
3498
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -3486,6 +3611,7 @@ Modes:
|
|
|
3486
3611
|
--pine <entry> Execute a local .pine indicator file
|
|
3487
3612
|
--editor Open the Pine Script visual editor in the browser
|
|
3488
3613
|
--dump Fetch and save raw OHLCV candles
|
|
3614
|
+
--pnldebug Simulate PnL per minute for a given entry price and direction
|
|
3489
3615
|
--flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
|
|
3490
3616
|
--init Scaffold a new project in the current directory
|
|
3491
3617
|
--help Print this help message
|
|
@@ -3558,6 +3684,21 @@ Candle dump flags (--dump):
|
|
|
3558
3684
|
|
|
3559
3685
|
Module file ./modules/dump.module is loaded automatically if it exists.
|
|
3560
3686
|
|
|
3687
|
+
PnL debug flags (--pnldebug):
|
|
3688
|
+
|
|
3689
|
+
--symbol <string> Trading pair (default: BTCUSDT)
|
|
3690
|
+
--priceopen <number> Entry price (required)
|
|
3691
|
+
--direction <string> Position direction: long or short (default: long)
|
|
3692
|
+
--when <string> Start timestamp โ ISO 8601 or Unix ms (default: now)
|
|
3693
|
+
--minutes <string> Number of 1m candles to simulate (default: 60)
|
|
3694
|
+
--exchange <string> Exchange name (default: first registered)
|
|
3695
|
+
--output <string> Output file base name (default: {SYMBOL}_{DIRECTION}_{PRICEOPEN}_{TIMESTAMP})
|
|
3696
|
+
--json Save as JSON array to ./dump/<output>.json
|
|
3697
|
+
--jsonl Save as JSONL to ./dump/<output>.jsonl
|
|
3698
|
+
--markdown Save as Markdown table to ./dump/<output>.md
|
|
3699
|
+
|
|
3700
|
+
Module file ./modules/pnldebug.module is loaded automatically if it exists.
|
|
3701
|
+
|
|
3561
3702
|
Flush flags (--flush):
|
|
3562
3703
|
|
|
3563
3704
|
One or more positional entry points. For each entry point the following
|
|
@@ -3580,6 +3721,7 @@ Module hooks (loaded automatically by each mode):
|
|
|
3580
3721
|
modules/pine.module --pine Exchange schema for PineScript runs
|
|
3581
3722
|
modules/editor.module --editor Exchange schema for the visual Pine editor
|
|
3582
3723
|
modules/dump.module --dump Exchange schema for candle dumps
|
|
3724
|
+
modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
|
|
3583
3725
|
|
|
3584
3726
|
--flush has no associated module. It only removes dump subdirectories.
|
|
3585
3727
|
|
|
@@ -3603,6 +3745,8 @@ Examples:
|
|
|
3603
3745
|
node ${ENTRY_PATH} --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --jsonl
|
|
3604
3746
|
node ${ENTRY_PATH} --editor
|
|
3605
3747
|
node ${ENTRY_PATH} --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
|
|
3748
|
+
node ${ENTRY_PATH} --pnldebug --symbol BTCUSDT --priceopen 64069.50 --direction short --when "2025-02-25" --minutes 120
|
|
3749
|
+
node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
|
|
3606
3750
|
node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
|
|
3607
3751
|
node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
|
|
3608
3752
|
node ${ENTRY_PATH} --init --output my-trading-bot
|
|
@@ -3615,7 +3759,7 @@ const main$1 = async () => {
|
|
|
3615
3759
|
if (!values.help) {
|
|
3616
3760
|
return;
|
|
3617
3761
|
}
|
|
3618
|
-
process.stdout.write(`@backtest-kit/cli ${"7.
|
|
3762
|
+
process.stdout.write(`@backtest-kit/cli ${"7.5.0"}\n\n`);
|
|
3619
3763
|
process.stdout.write(HELP_TEXT);
|
|
3620
3764
|
process.exit(0);
|
|
3621
3765
|
};
|
|
@@ -3629,7 +3773,7 @@ const main = async () => {
|
|
|
3629
3773
|
if (!values.version) {
|
|
3630
3774
|
return;
|
|
3631
3775
|
}
|
|
3632
|
-
process.stdout.write(`@backtest-kit/cli ${"7.
|
|
3776
|
+
process.stdout.write(`@backtest-kit/cli ${"7.5.0"}\n`);
|
|
3633
3777
|
process.exit(0);
|
|
3634
3778
|
};
|
|
3635
3779
|
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backtest-kit/cli",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.5.0",
|
|
4
4
|
"description": "Zero-boilerplate CLI runner for backtest-kit strategies. Run backtests, paper trading, and live bots with candle cache warming, web dashboard, and Telegram notifications โ no setup code required.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Petr Tripolsky",
|
|
@@ -62,11 +62,11 @@
|
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@babel/plugin-transform-modules-umd": "7.27.1",
|
|
64
64
|
"@babel/standalone": "7.29.1",
|
|
65
|
-
"@backtest-kit/graph": "7.
|
|
66
|
-
"@backtest-kit/ollama": "7.
|
|
67
|
-
"@backtest-kit/pinets": "7.
|
|
68
|
-
"@backtest-kit/signals": "7.
|
|
69
|
-
"@backtest-kit/ui": "7.
|
|
65
|
+
"@backtest-kit/graph": "7.5.0",
|
|
66
|
+
"@backtest-kit/ollama": "7.5.0",
|
|
67
|
+
"@backtest-kit/pinets": "7.5.0",
|
|
68
|
+
"@backtest-kit/signals": "7.5.0",
|
|
69
|
+
"@backtest-kit/ui": "7.5.0",
|
|
70
70
|
"@rollup/plugin-replace": "6.0.3",
|
|
71
71
|
"@rollup/plugin-typescript": "11.1.6",
|
|
72
72
|
"@types/image-size": "0.7.0",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"@types/mustache": "4.2.6",
|
|
75
75
|
"@types/node": "22.9.0",
|
|
76
76
|
"@types/stack-trace": "0.0.33",
|
|
77
|
-
"backtest-kit": "7.
|
|
77
|
+
"backtest-kit": "7.5.0",
|
|
78
78
|
"glob": "11.0.1",
|
|
79
79
|
"markdown-it": "14.1.1",
|
|
80
80
|
"rimraf": "6.0.1",
|
|
@@ -89,12 +89,12 @@
|
|
|
89
89
|
"peerDependencies": {
|
|
90
90
|
"@babel/plugin-transform-modules-umd": "^7.27.1",
|
|
91
91
|
"@babel/standalone": "^7.29.1",
|
|
92
|
-
"@backtest-kit/graph": "^7.
|
|
93
|
-
"@backtest-kit/ollama": "^7.
|
|
94
|
-
"@backtest-kit/pinets": "^7.
|
|
95
|
-
"@backtest-kit/signals": "^7.
|
|
96
|
-
"@backtest-kit/ui": "^7.
|
|
97
|
-
"backtest-kit": "^7.
|
|
92
|
+
"@backtest-kit/graph": "^7.5.0",
|
|
93
|
+
"@backtest-kit/ollama": "^7.5.0",
|
|
94
|
+
"@backtest-kit/pinets": "^7.5.0",
|
|
95
|
+
"@backtest-kit/signals": "^7.5.0",
|
|
96
|
+
"@backtest-kit/ui": "^7.5.0",
|
|
97
|
+
"backtest-kit": "^7.5.0",
|
|
98
98
|
"markdown-it": "^14.1.1",
|
|
99
99
|
"typescript": "^5.0.0"
|
|
100
100
|
},
|
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
"license": "ISC",
|
|
14
14
|
"type": "commonjs",
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@backtest-kit/cli": "^7.
|
|
17
|
-
"@backtest-kit/graph": "^7.
|
|
18
|
-
"@backtest-kit/pinets": "^7.
|
|
19
|
-
"@backtest-kit/ui": "^7.
|
|
16
|
+
"@backtest-kit/cli": "^7.5.0",
|
|
17
|
+
"@backtest-kit/graph": "^7.5.0",
|
|
18
|
+
"@backtest-kit/pinets": "^7.5.0",
|
|
19
|
+
"@backtest-kit/ui": "^7.5.0",
|
|
20
20
|
"agent-swarm-kit": "^2.6.0",
|
|
21
|
-
"backtest-kit": "^7.
|
|
21
|
+
"backtest-kit": "^7.5.0",
|
|
22
22
|
"functools-kit": "^2.3.0",
|
|
23
23
|
"garch": "^1.2.3",
|
|
24
24
|
"get-moment-stamp": "^1.1.2",
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
**โน๏ธ Signal Info**
|
|
2
|
-
|
|
3
|
-
**Symbol:** `{{symbol}}` ({{data.position}})
|
|
4
|
-
**Current:** `{{currentPrice}}`
|
|
5
|
-
**Entry:** `{{data.priceOpen}}`
|
|
6
|
-
**Orig Entry:** `{{data.originalPriceOpen}}`
|
|
7
|
-
**DCA Entries:** `{{data.totalEntries}}`
|
|
8
|
-
**Partials Done:** `{{data.totalPartials}}`
|
|
9
|
-
**Take Profit:** `{{data.priceTakeProfit}}`
|
|
10
|
-
**Stop Loss:** `{{data.priceStopLoss}}`
|
|
11
|
-
**Orig TP:** `{{data.originalPriceTakeProfit}}`
|
|
12
|
-
**Orig SL:** `{{data.originalPriceStopLoss}}`
|
|
13
|
-
**PnL:** {{data.pnl.pnlPercentage}}% ({{data.pnl.pnlCost}} / {{data.pnl.pnlEntries}})
|
|
14
|
-
**Peak Profit:** {{data.peakProfit.pnlPercentage}}% ({{data.peakProfit.pnlCost}} / {{data.peakProfit.pnlEntries}})
|
|
15
|
-
**Max Drawdown:** {{data.maxDrawdown.pnlPercentage}}% ({{data.maxDrawdown.pnlCost}} / {{data.maxDrawdown.pnlEntries}})
|
|
16
|
-
**Signal ID:** `{{data.id}}`
|
|
17
|
-
**Time:** `{{timestamp}}`
|
|
18
|
-
|
|
19
|
-
{{#backtest}}
|
|
20
|
-
_๐งช Backtest_
|
|
1
|
+
**โน๏ธ Signal Info**
|
|
2
|
+
|
|
3
|
+
**Symbol:** `{{symbol}}` ({{data.position}})
|
|
4
|
+
**Current:** `{{currentPrice}}`
|
|
5
|
+
**Entry:** `{{data.priceOpen}}`
|
|
6
|
+
**Orig Entry:** `{{data.originalPriceOpen}}`
|
|
7
|
+
**DCA Entries:** `{{data.totalEntries}}`
|
|
8
|
+
**Partials Done:** `{{data.totalPartials}}`
|
|
9
|
+
**Take Profit:** `{{data.priceTakeProfit}}`
|
|
10
|
+
**Stop Loss:** `{{data.priceStopLoss}}`
|
|
11
|
+
**Orig TP:** `{{data.originalPriceTakeProfit}}`
|
|
12
|
+
**Orig SL:** `{{data.originalPriceStopLoss}}`
|
|
13
|
+
**PnL:** {{data.pnl.pnlPercentage}}% ({{data.pnl.pnlCost}} / {{data.pnl.pnlEntries}})
|
|
14
|
+
**Peak Profit:** {{data.peakProfit.pnlPercentage}}% ({{data.peakProfit.pnlCost}} / {{data.peakProfit.pnlEntries}})
|
|
15
|
+
**Max Drawdown:** {{data.maxDrawdown.pnlPercentage}}% ({{data.maxDrawdown.pnlCost}} / {{data.maxDrawdown.pnlEntries}})
|
|
16
|
+
**Signal ID:** `{{data.id}}`
|
|
17
|
+
**Time:** `{{timestamp}}`
|
|
18
|
+
|
|
19
|
+
{{#backtest}}
|
|
20
|
+
_๐งช Backtest_
|
|
21
21
|
{{/backtest}}
|