@backtest-kit/cli 5.11.0 → 5.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,153 @@
1
+ # Async Risk Validation with resolve()
2
+
3
+ ## Overview
4
+
5
+ Risk validators can be async and call `resolve()` on any graph node. This lets you reuse the same source nodes (garch, trend, fear & greed) in both signal generation and risk filtering — without duplicate fetches.
6
+
7
+ ---
8
+
9
+ ## Sync vs async validation
10
+
11
+ `backtest_strategy_structure.md` shows the basic sync pattern:
12
+
13
+ ```ts
14
+ // Sync — receives pre-computed payload fields
15
+ validations: [
16
+ {
17
+ validate: ({ currentSignal, currentPrice }) => {
18
+ if (currentSignal.position === "short" && currentPrice > 50000) {
19
+ throw new Error("Price too high for short");
20
+ }
21
+ },
22
+ },
23
+ ],
24
+ ```
25
+
26
+ Async pattern — resolve a graph node:
27
+
28
+ ```ts
29
+ // Async — fetch fresh data at validation time
30
+ validations: [
31
+ async () => {
32
+ const garch = await resolve(garchSource);
33
+ if (!garch.reliable || garch.movePercent < 1.0) {
34
+ throw new Error(`GARCH volatility too low: ${garch.movePercent.toFixed(2)}%`);
35
+ }
36
+ },
37
+ ],
38
+ ```
39
+
40
+ Both forms can coexist in the same `validations` array.
41
+
42
+ ---
43
+
44
+ ## GARCH volatility gate
45
+
46
+ The canonical use case: block signals when predicted volatility is too low to cover transaction costs and generate profit.
47
+
48
+ ```ts
49
+ import { predict } from "garch";
50
+ import { sourceNode, resolve } from "@backtest-kit/graph";
51
+ import { Cache, getCandles, addRiskSchema } from "backtest-kit";
52
+
53
+ const CANDLES_FOR_GARCH = 1_000;
54
+ const GARCH_CONFIDENCE = 0.95;
55
+ const MIN_MOVE_PERCENT = 1.0;
56
+
57
+ const garchSource = sourceNode(
58
+ Cache.fn(
59
+ async (symbol: string) => {
60
+ const candles = await getCandles(symbol, "8h", CANDLES_FOR_GARCH);
61
+ return predict(candles, "8h", null, GARCH_CONFIDENCE);
62
+ },
63
+ { interval: "8h", key: ([symbol]: [string]) => symbol },
64
+ ),
65
+ );
66
+
67
+ addRiskSchema({
68
+ riskName: "garch_volatility_risk",
69
+ validations: [
70
+ async () => {
71
+ const garch = await resolve(garchSource);
72
+ if (!garch.reliable || garch.movePercent < MIN_MOVE_PERCENT) {
73
+ throw new Error(
74
+ `GARCH volatility too low: ${garch.movePercent.toFixed(2)}% (need ≥${MIN_MOVE_PERCENT}%, confidence=${GARCH_CONFIDENCE})`,
75
+ );
76
+ }
77
+ },
78
+ ],
79
+ });
80
+ ```
81
+
82
+ **`garch.reliable`** — false when the model didn't converge or has insufficient data. Always check this before using `movePercent`.
83
+
84
+ **`garch.movePercent`** — predicted price move as percentage of current price over one candle (one 8h bar here). At confidence=0.95 (±1.96σ), a value of 1.0 means the model predicts ≥1% move with 95% probability.
85
+
86
+ ---
87
+
88
+ ## Fear & Greed gate (directional)
89
+
90
+ From `feb_2024.strategy.ts` — block longs in fear and shorts in greed:
91
+
92
+ ```ts
93
+ addRiskSchema({
94
+ riskName: "fear_greed_directional",
95
+ validations: [
96
+ async ({ currentSignal }) => {
97
+ const fearGreed = await resolve(fearGreedSource);
98
+ if (currentSignal.position === "short" && fearGreed > 50) {
99
+ throw new Error(`Still greed (${fearGreed}): short not allowed`);
100
+ }
101
+ },
102
+ async ({ currentSignal }) => {
103
+ const fearGreed = await resolve(fearGreedSource);
104
+ if (currentSignal.position === "long" && fearGreed < 50) {
105
+ throw new Error(`Still fear (${fearGreed}): long not allowed`);
106
+ }
107
+ },
108
+ ],
109
+ });
110
+ ```
111
+
112
+ `fearGreedSource` uses `Cache.file` with `interval: "8h"` — fetched once per 8h from `api.alternative.me`.
113
+
114
+ ---
115
+
116
+ ## Node deduplication in risk
117
+
118
+ When `garchSource` is a dependency of both `enterSignal` (output node) and the risk validator, the graph deduplicates resolution within the same tick. If `getSignal()` already resolved `garchSource`, the risk validator gets the cached result — no second fetch.
119
+
120
+ This only holds if the validator uses `resolve(garchSource)` with the **same node reference**. Do not create a new `sourceNode` inside the validator — that would be a different node with its own cache.
121
+
122
+ ---
123
+
124
+ ## `validate` vs bare async function
125
+
126
+ Both forms are accepted in `validations`:
127
+
128
+ ```ts
129
+ // Object form — supports `note` field
130
+ validations: [
131
+ {
132
+ note: "GARCH volatility must be at least 1%",
133
+ validate: async () => {
134
+ const garch = await resolve(garchSource);
135
+ if (!garch.reliable || garch.movePercent < 1.0) {
136
+ throw new Error("...");
137
+ }
138
+ },
139
+ },
140
+ ]
141
+
142
+ // Bare async function — more concise
143
+ validations: [
144
+ async () => {
145
+ const garch = await resolve(garchSource);
146
+ if (!garch.reliable || garch.movePercent < 1.0) {
147
+ throw new Error("...");
148
+ }
149
+ },
150
+ ]
151
+ ```
152
+
153
+ Throw `Error` to reject the signal. Return normally (or return `void`) to allow it.
@@ -0,0 +1,250 @@
1
+ # Strategy Structure Guide
2
+
3
+ ## Overview
4
+
5
+ A backtest-kit strategy file registers four schemas and wires them together. The CLI reads all `.ts` / `.mjs` files in the target directory and calls the registered schemas at runtime.
6
+
7
+ ```
8
+ addExchangeSchema — data source (candles, trades, order book)
9
+ addFrameSchema — backtest time window
10
+ addStrategySchema — signal generation logic
11
+ addRiskSchema — optional position filters
12
+ ```
13
+
14
+ ---
15
+
16
+ ## Minimal Strategy File
17
+
18
+ ```ts
19
+ import { addExchangeSchema, addFrameSchema, addStrategySchema, getCandles, Log } from "backtest-kit";
20
+ import { randomString } from "functools-kit";
21
+
22
+ // 1. Data source
23
+ addExchangeSchema({
24
+ exchangeName: "ccxt-exchange",
25
+ getCandles: async (symbol, interval, since, limit) => { ... },
26
+ });
27
+
28
+ // 2. Backtest window
29
+ addFrameSchema({
30
+ frameName: "feb_2024_frame",
31
+ interval: "1m", // tick granularity
32
+ startDate: new Date("2024-02-01T00:00:00Z"),
33
+ endDate: new Date("2024-02-29T23:59:59Z"),
34
+ });
35
+
36
+ // 3. Signal logic
37
+ addStrategySchema({
38
+ strategyName: "my_strategy",
39
+ interval: "15m", // getSignal call frequency
40
+ getSignal: async (symbol, when) => {
41
+ // ... compute signal
42
+ return {
43
+ id: randomString(),
44
+ position: "long",
45
+ priceTakeProfit: 50000,
46
+ priceStopLoss: 48000,
47
+ minuteEstimatedTime: 480,
48
+ };
49
+ // return null → no signal this tick
50
+ },
51
+ });
52
+ ```
53
+
54
+ ---
55
+
56
+ ## ISignalDto — Signal Fields
57
+
58
+ ```ts
59
+ interface ISignalDto {
60
+ id?: string; // auto-generated UUID if omitted
61
+ position: "long" | "short";
62
+ priceTakeProfit: number; // for long: > priceOpen; for short: < priceOpen
63
+ priceStopLoss: number; // for long: < priceOpen; for short: > priceOpen
64
+ minuteEstimatedTime: number; // expected hold duration in minutes
65
+ priceOpen?: number; // omit → opens immediately at current price
66
+ // provide → creates a scheduled signal
67
+ note?: string; // human-readable description
68
+ }
69
+ ```
70
+
71
+ **Immediate vs scheduled signal:**
72
+ - `priceOpen` omitted → position opens at current VWAP immediately
73
+ - `priceOpen` set → signal waits until price reaches that level, then opens
74
+
75
+ ---
76
+
77
+ ## Signal Lifecycle
78
+
79
+ ```
80
+ idle
81
+ │ getSignal() returns ISignalDto (no priceOpen)
82
+
83
+ opened → active → closed (TP or SL hit, or time expired)
84
+
85
+ pnlPercentage in result
86
+
87
+ │ getSignal() returns ISignalDto (with priceOpen)
88
+
89
+ scheduled → waiting → opened → active → closed
90
+
91
+ cancelled (if price never reached)
92
+ ```
93
+
94
+ Lifecycle callbacks (all optional):
95
+
96
+ ```ts
97
+ addStrategySchema({
98
+ strategyName: "...",
99
+ interval: "15m",
100
+ getSignal: ...,
101
+ callbacks: {
102
+ onOpen: (symbol, signal, price, backtest) => { ... },
103
+ onClose: (symbol, signal, closePrice, backtest) => { ... },
104
+ onActive: (symbol, signal, price, backtest) => { ... },
105
+ onIdle: (symbol, price, backtest) => { ... },
106
+ onCancel: (symbol, signal, price, backtest) => { ... },
107
+ onSchedule:(symbol, signal, price, backtest) => { ... },
108
+ onPartialProfit: (symbol, signal, price, revenuePercent, backtest) => { ... },
109
+ onPartialLoss: (symbol, signal, price, lossPercent, backtest) => { ... },
110
+ onBreakeven: (symbol, signal, price, backtest) => { ... },
111
+ },
112
+ });
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Interval Types
118
+
119
+ | Schema | Parameter | Valid values |
120
+ |---|---|---|
121
+ | `addFrameSchema` | `interval` | `"1m"` `"3m"` `"5m"` `"15m"` `"30m"` `"1h"` `"2h"` `"4h"` `"6h"` `"8h"` `"12h"` `"1d"` `"3d"` |
122
+ | `addStrategySchema` | `interval` | `"1m"` `"3m"` `"5m"` `"15m"` `"30m"` `"1h"` |
123
+ | `getCandles` / `addExchangeSchema` | `interval` | `"1m"` `"3m"` `"5m"` `"15m"` `"30m"` `"1h"` `"2h"` `"4h"` `"6h"` `"8h"` |
124
+
125
+ **Rule:** `frameSchema.interval` controls tick granularity; `strategySchema.interval` throttles how often `getSignal` is called. Strategy interval must be ≥ frame interval.
126
+
127
+ ---
128
+
129
+ ## Exchange Schema — Binance via CCXT
130
+
131
+ Full boilerplate with candles + aggregated trades:
132
+
133
+ ```ts
134
+ import { singleshot } from "functools-kit";
135
+ import ccxt from "ccxt";
136
+
137
+ const getExchange = singleshot(async () => {
138
+ const exchange = new ccxt.binance({
139
+ options: {
140
+ defaultType: "spot",
141
+ adjustForTimeDifference: true,
142
+ recvWindow: 60000,
143
+ },
144
+ enableRateLimit: true,
145
+ });
146
+ await exchange.loadMarkets();
147
+ return exchange;
148
+ });
149
+
150
+ addExchangeSchema({
151
+ exchangeName: "ccxt-exchange",
152
+ getCandles: async (symbol, interval, since, limit) => {
153
+ const exchange = await getExchange();
154
+ const candles = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);
155
+ return candles.map(([timestamp, open, high, low, close, volume]) => ({
156
+ timestamp, open, high, low, close, volume,
157
+ }));
158
+ },
159
+ getAggregatedTrades: async (symbol, from, to) => {
160
+ const exchange = await getExchange();
161
+ const response = await exchange.publicGetAggTrades({
162
+ symbol,
163
+ startTime: from.getTime(),
164
+ endTime: to.getTime(),
165
+ });
166
+ return response.map((t: any) => ({
167
+ id: String(t.a),
168
+ price: parseFloat(t.p),
169
+ qty: parseFloat(t.q),
170
+ timestamp: t.T,
171
+ isBuyerMaker: t.m,
172
+ }));
173
+ },
174
+ });
175
+ ```
176
+
177
+ Notes:
178
+ - `singleshot` from `functools-kit` memoizes the exchange init — called once, reused everywhere
179
+ - `getAggregatedTrades` is optional; only needed if strategy uses `volume-anomaly`
180
+ - In backtest mode the `backtest: boolean` flag is passed as 4th arg — use it to switch between live API and cached data
181
+
182
+ ---
183
+
184
+ ## Risk Schema
185
+
186
+ Filters that run before a signal is accepted. If any validation returns a rejection string, the signal is blocked.
187
+
188
+ ```ts
189
+ import { addRiskSchema } from "backtest-kit";
190
+
191
+ addRiskSchema({
192
+ riskName: "max_positions",
193
+ validations: [
194
+ {
195
+ note: "Allow max 1 active position",
196
+ validate: ({ activePositionCount }) => {
197
+ if (activePositionCount >= 1) return "Too many open positions";
198
+ return null; // null = allowed
199
+ },
200
+ },
201
+ ],
202
+ });
203
+
204
+ // Attach to strategy:
205
+ addStrategySchema({
206
+ strategyName: "my_strategy",
207
+ interval: "15m",
208
+ riskName: "max_positions", // single risk profile
209
+ getSignal: ...,
210
+ });
211
+ // or multiple:
212
+ // riskList: ["max_positions", "funding_filter"]
213
+ ```
214
+
215
+ `IRiskValidationPayload` fields available in `validate`:
216
+
217
+ | Field | Type | Description |
218
+ |---|---|---|
219
+ | `currentSignal` | `IRiskSignalRow` | Signal about to open |
220
+ | `activePositionCount` | `number` | Currently open positions |
221
+ | `activePositions` | `IRiskActivePosition[]` | Details of each open position |
222
+ | `currentPrice` | `number` | Current market price |
223
+ | `symbol` | `string` | Trading pair |
224
+ | `backtest` | `boolean` | Whether running in backtest mode |
225
+
226
+ ---
227
+
228
+ ## PNL Calculation
229
+
230
+ Transaction costs are automatically applied:
231
+ - Slippage: **0.1%** per side
232
+ - Fee: **0.1%** per side
233
+ - Round-trip cost: **0.4%**
234
+
235
+ `priceOpen` and `priceClose` in `IStrategyPnL` are **adjusted** (slippage + fee already subtracted). `pnlPercentage` is the net result.
236
+
237
+ Minimum `movePercent` to cover costs: `> 0.4%`. Recommended filter: `> 0.7%` to leave margin.
238
+
239
+ ---
240
+
241
+ ## File Naming Convention
242
+
243
+ Strategy files must be importable by the CLI. Use either:
244
+ - `content/my_strategy.ts` (TypeScript, requires compilation)
245
+ - `strategies/my_strategy/index.mjs` (compiled ESM)
246
+
247
+ The CLI entry point from `package.json`:
248
+ ```
249
+ npx @backtest-kit/cli ./strategies/feb_2024/index.mjs --backtest --ui --noCache
250
+ ```
@@ -0,0 +1,174 @@
1
+ # Pine Script Debugging Guide
2
+
3
+ ## Context
4
+
5
+ This project runs Pine Script indicators via `@backtest-kit/pinets` against real exchange data fetched through `ccxt`. Use `@backtest-kit/cli --pine` to execute any `.pine` file and dump results to a JSONL file for inspection.
6
+
7
+ Key files:
8
+ - `math/*.pine` — Pine Script indicators
9
+
10
+ ---
11
+
12
+ ## Debug Workflow
13
+
14
+ ### Step 1 — Add debug plots to Pine
15
+
16
+ Add named `plot()` calls for internal variables you want to inspect. Use `display=display.data_window` to mark bot-facing outputs (convention only — the CLI captures all named plots regardless):
17
+
18
+ ```pine
19
+ // === OUTPUTS FOR BOT ===
20
+ plot(close, "Close", display=display.data_window)
21
+ plot(active_signal, "Signal", display=display.data_window)
22
+
23
+ // === DEBUG ===
24
+ plot(ema_fast, "EmaFast", display=display.data_window)
25
+ plot(ema_slow, "EmaSlow", display=display.data_window)
26
+ plot(ema_fast - ema_slow, "EmaGap", display=display.data_window)
27
+ plot(bars_since_signal, "BarsSinceSignal", display=display.data_window)
28
+ plot(last_signal, "LastSignal", display=display.data_window)
29
+ ```
30
+
31
+ ### Step 2 — Run and dump to JSONL
32
+
33
+ Use `--jsonl` to write output to a file instead of stdout. JSONL is preferred over Markdown for large outputs — each row is a self-contained JSON object, so an AI agent can read only the rows it needs without loading the full table into context.
34
+
35
+ Output is written to `<pine-dir>/dump/<output>.jsonl` — the directory is created automatically. By default `<output>` equals the `.pine` file name (without extension). Override with `--output`.
36
+
37
+ ```bash
38
+ npx @backtest-kit/cli --pine ./math/my_indicator.pine \
39
+ --symbol BTCUSDT \
40
+ --timeframe 15m \
41
+ --limit 180 \
42
+ --when "2025-09-24T12:00:00.000Z" \
43
+ --jsonl
44
+ # → ./math/dump/my_indicator.jsonl
45
+ ```
46
+
47
+ Override the output name:
48
+
49
+ ```bash
50
+ npx @backtest-kit/cli --pine ./math/my_indicator.pine \
51
+ --jsonl \
52
+ --output debug
53
+ # → ./math/dump/debug.jsonl
54
+ ```
55
+
56
+ Or add to `package.json`:
57
+
58
+ ```json
59
+ {
60
+ "scripts": {
61
+ "pine:debug": "npx @backtest-kit/cli --pine ./math/my_indicator.pine --symbol BTCUSDT --timeframe 15m --limit 180 --jsonl"
62
+ }
63
+ }
64
+ ```
65
+
66
+ ```bash
67
+ npm run pine:debug
68
+ ```
69
+
70
+ ### Step 3 — Read the JSONL file
71
+
72
+ Each line is one bar:
73
+
74
+ ```jsonl
75
+ {"Close":112871.28,"EmaFast":112500.10,"EmaGap":123.45,"BarsSinceSignal":0,"LastSignal":1,"Signal":1,"timestamp":"2025-09-22T15:00:00.000Z"}
76
+ {"Close":112666.69,"EmaFast":112480.55,"EmaGap":98.12,"BarsSinceSignal":1,"LastSignal":1,"Signal":1,"timestamp":"2025-09-22T15:15:00.000Z"}
77
+ ```
78
+
79
+ Scan rows where `BarsSinceSignal == 0` to find signal transitions — that's where the crossover fired.
80
+
81
+ ---
82
+
83
+ ## CLI Flags
84
+
85
+ | Flag | Type | Description |
86
+ |------|------|-------------|
87
+ | `--pine` | boolean | Enable PineScript execution mode |
88
+ | `--symbol` | string | Trading pair (default: `"BTCUSDT"`) |
89
+ | `--timeframe` | string | Candle interval (default: `"15m"`) |
90
+ | `--limit` | string | Number of candles to fetch (default: `250`) |
91
+ | `--when` | string | End date for candle window — ISO 8601 or Unix ms (default: now) |
92
+ | `--exchange` | string | Exchange name (default: first registered, falls back to CCXT Binance) |
93
+ | `--output` | string | Output file base name without extension (default: `.pine` file name) |
94
+ | `--jsonl` | boolean | Write plots as JSONL (one row per line) to `<pine-dir>/dump/{output}.jsonl` — **preferred for debug** |
95
+ | `--json` | boolean | Write plots as a JSON array to `<pine-dir>/dump/{output}.json` |
96
+ | `--markdown` | boolean | Write Markdown table to `<pine-dir>/dump/{output}.md` |
97
+
98
+ **Important:** `limit` must cover indicator warmup bars — rows before warmup completes will show `N/A`.
99
+
100
+ ---
101
+
102
+ ## Exchange Configuration (pine.module)
103
+
104
+ By default the CLI registers CCXT Binance automatically — no setup needed for Binance spot.
105
+
106
+ To use a different exchange, create `modules/pine.module.ts` next to the `.pine` file (or at project root as fallback):
107
+
108
+ ```
109
+ math/
110
+ ├── my_indicator.pine
111
+ └── modules/
112
+ └── pine.module.ts ← loaded automatically before running
113
+ ```
114
+
115
+ ```typescript
116
+ // modules/pine.module.ts
117
+ import { addExchangeSchema } from "backtest-kit";
118
+ import ccxt from "ccxt";
119
+
120
+ addExchangeSchema({
121
+ exchangeName: "my-exchange",
122
+ getCandles: async (symbol, interval, since, limit) => {
123
+ const exchange = new ccxt.bybit({ enableRateLimit: true });
124
+ const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);
125
+ return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({
126
+ timestamp, open, high, low, close, volume,
127
+ }));
128
+ },
129
+ });
130
+ ```
131
+
132
+ Then pass `--exchange my-exchange` to the CLI.
133
+
134
+ ---
135
+
136
+ ## Common Patterns
137
+
138
+ ### N/A values
139
+ EMA of length N returns `N/A` for the first `N-1` bars — this is expected warmup behavior. `EmaFast` (len=8) starts at bar 8, `EmaSlow` (len=21) starts at bar 21.
140
+
141
+ ### Diagnosing whipsaw
142
+ Look at `EmaGap` at the moment `LastSignal` changes (i.e. `BarsSinceSignal == 0`). If `|EmaGap|` is small (e.g. < 15), the crossover happened in a flat/noisy zone — likely a false signal.
143
+
144
+ ### Diagnosing stale signals
145
+ `active_signal` goes to 0 when `bars_since_signal > signal_valid_bars`. If `Signal` is 0 but `LastSignal` is non-zero, the signal expired. Increase `signal_valid_bars` or check why the crossover didn't sustain.
146
+
147
+ ---
148
+
149
+ ## Adding Filters
150
+
151
+ Filters go into the entry condition expressions:
152
+
153
+ ```pine
154
+ min_gap = input.float(15.0, "Min EMA Gap Filter", minval=0.0)
155
+
156
+ long_cond = ta.crossover(ema_fast, ema_slow) and math.abs(ema_gap) >= min_gap
157
+ short_cond = ta.crossunder(ema_fast, ema_slow) and math.abs(ema_gap) >= min_gap
158
+ ```
159
+
160
+ Add the filter threshold as a debug plot, then check in JSONL: when `BarsSinceSignal` resets to 0, does `EmaGap` meet the threshold?
161
+
162
+ ---
163
+
164
+ ## Pine Variables Worth Plotting for Debug
165
+
166
+ | Variable | Purpose |
167
+ |---|---|
168
+ | `ema_fast - ema_slow` | Gap magnitude — key for noise filtering |
169
+ | `bars_since_signal` | How many bars since last crossover |
170
+ | `last_signal` | Raw last direction (1/-1/0), ignores expiry |
171
+ | `active_signal` | Final output after expiry window |
172
+ | `ta.rsi(close, 14)` | Momentum context |
173
+ | `ta.atr(14)` | Volatility context for dynamic thresholds |
174
+ | `volume` | Volume spike confirmation |
@@ -0,0 +1,88 @@
1
+ # Pine Script Warmup & Limit Calculation
2
+
3
+ ## The Problem
4
+
5
+ Pine functions that look back over N bars (`ta.supertrend`, `ta.ema`, `ta.highest`, etc.) return `na` / `null` until they have enough bars to compute. If `limit` is too small, the entire output is N/A.
6
+
7
+ ---
8
+
9
+ ## Warmup Formula
10
+
11
+ ```
12
+ warmup_bars = max_lookback_period + any_secondary_lookback
13
+ ```
14
+
15
+ Examples:
16
+
17
+ ```
18
+ // EMA golden cross
19
+ warmup = max(ema_slow_len=21) = 21 bars
20
+
21
+ // MasterTrend
22
+ warmup = atrPeriod(15) + confirmBars(15) = 30 bars
23
+ ```
24
+
25
+ Rule: `limit` must be at least `warmup + desired_output_bars`.
26
+
27
+ For a meaningful output of ~150 bars:
28
+ ```
29
+ limit = warmup + 150
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Timeframe → Limit Reference
35
+
36
+ Warmup for `master_trend_15m.pine` (atrPeriod=15, confirmBars=15 → warmup=30):
37
+
38
+ | Timeframe | Min limit (warmup only) | Limit for ~150 output bars | Real time covered |
39
+ |---|---|---|---|
40
+ | 5m | 30 | 180 | ~15h |
41
+ | **15m** | 30 | **180** | **~45h** |
42
+ | 1h | 30 | 180 | ~7.5 days |
43
+
44
+ ---
45
+
46
+ ## Diagnosing N/A Output
47
+
48
+ Symptom: all columns except `Close` show `N/A` from bar 0.
49
+
50
+ Check: count warmup bars in output — the index of the first non-null value.
51
+
52
+ ```js
53
+ const posData = plots["Position"].data;
54
+ const firstValid = posData.findIndex((d) => d.value !== null && !isNaN(d.value));
55
+ console.log("Warmup bars:", firstValid);
56
+ // if firstValid === posData.length → limit too small, zero valid output
57
+ ```
58
+
59
+ If `firstValid === posData.length` → increase `limit` by at least `warmup - limit + desired_output`.
60
+
61
+ ---
62
+
63
+ ## Stacked Lookbacks
64
+
65
+ When multiple lookback functions are chained, warmup is additive:
66
+
67
+ ```pine
68
+ [supertrend, direction] = ta.supertrend(factor, atrPeriod) // needs atrPeriod bars
69
+ // confirmBars counter needs confirmBars more bars after supertrend is valid
70
+ // total warmup = atrPeriod + confirmBars = 15 + 15 = 30
71
+ ```
72
+
73
+ `ta.supertrend` itself uses ATR internally, so warmup = atrPeriod. The `confirmBars` confirmation window adds another `confirmBars` bars before `trend` stabilizes.
74
+
75
+ ---
76
+
77
+ ## Pine inputs cannot be set via run()
78
+
79
+ The `inputs` parameter in `run()` is silently ignored — Pine `input.int()` / `input.float()` defaults are always used.
80
+
81
+ To test different period values, change the defaults in the `.pine` file directly:
82
+
83
+ ```pine
84
+ // change this line in the .pine file
85
+ atrPeriod = input(15, "ATR Period") // → input(20, "ATR Period")
86
+ ```
87
+
88
+ This means warmup changes when tuning params — recalculate `limit` after any period change.
@@ -0,0 +1,30 @@
1
+ # Dependencies
2
+ node_modules
3
+
4
+ # Environment variables
5
+ .env
6
+ .env.local
7
+ .env.*.local
8
+
9
+ # Logs
10
+ logs
11
+ *.log
12
+ npm-debug.log*
13
+ error.txt
14
+
15
+ # Runtime data
16
+ tmp
17
+ generated
18
+ dump
19
+ wwwroot
20
+
21
+ # OS files
22
+ .DS_Store
23
+ Thumbs.db
24
+
25
+ # IDE
26
+ .vscode
27
+ .idea
28
+ *.swp
29
+ *.swo
30
+ *~