tradelab 0.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/LICENSE +21 -0
- package/README.md +230 -0
- package/examples/emaCross.js +108 -0
- package/examples/yahooEmaCross.js +88 -0
- package/package.json +42 -0
- package/scripts/import-csv.js +69 -0
- package/scripts/prefetch.js +52 -0
- package/src/data/csv.js +340 -0
- package/src/data/index.js +125 -0
- package/src/data/yahoo.js +245 -0
- package/src/engine/backtest.js +852 -0
- package/src/engine/execution.js +120 -0
- package/src/index.js +43 -0
- package/src/metrics/buildMetrics.js +306 -0
- package/src/reporting/exportBacktestArtifacts.js +53 -0
- package/src/reporting/exportTradesCsv.js +73 -0
- package/src/reporting/renderHtmlReport.js +310 -0
- package/src/utils/indicators.js +138 -0
- package/src/utils/positionSizing.js +26 -0
- package/src/utils/time.js +92 -0
- package/templates/report.css +213 -0
- package/templates/report.html +106 -0
- package/templates/report.js +120 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# tradelab
|
|
2
|
+
|
|
3
|
+
`tradelab` is a candle-based backtesting toolkit for Node.js. It is built for two use cases:
|
|
4
|
+
|
|
5
|
+
- you already have candles and want a solid execution/backtest engine
|
|
6
|
+
- you want to fetch Yahoo Finance data or import CSVs and backtest with minimal setup
|
|
7
|
+
|
|
8
|
+
The package stays focused on historical research. It does not try to be a broker adapter or a live trading framework.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- Backtest engine with pending entries, OCO exits, scale-outs, pyramiding, cooldowns, daily risk limits, and optional replay data
|
|
13
|
+
- Yahoo Finance historical downloader with local caching
|
|
14
|
+
- Flexible CSV import for common OHLCV layouts
|
|
15
|
+
- Metrics for positions and realized legs
|
|
16
|
+
- CSV trade export
|
|
17
|
+
- Self-contained HTML report export
|
|
18
|
+
- Utility indicators and session helpers for strategy code
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install tradelab
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Node `18+` is required.
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
import { backtest, ema, exportBacktestArtifacts } from "tradelab";
|
|
32
|
+
|
|
33
|
+
const result = backtest({
|
|
34
|
+
candles,
|
|
35
|
+
symbol: "BTC-USD",
|
|
36
|
+
interval: "5m",
|
|
37
|
+
range: "60d",
|
|
38
|
+
equity: 10_000,
|
|
39
|
+
riskPct: 1,
|
|
40
|
+
signal({ candles: history }) {
|
|
41
|
+
if (history.length < 50) return null;
|
|
42
|
+
|
|
43
|
+
const closes = history.map((bar) => bar.close);
|
|
44
|
+
const fast = ema(closes, 10);
|
|
45
|
+
const slow = ema(closes, 30);
|
|
46
|
+
const last = closes.length - 1;
|
|
47
|
+
|
|
48
|
+
if (fast[last - 1] <= slow[last - 1] && fast[last] > slow[last]) {
|
|
49
|
+
const entry = history[last].close;
|
|
50
|
+
const stop = Math.min(...history.slice(-15).map((bar) => bar.low));
|
|
51
|
+
const risk = entry - stop;
|
|
52
|
+
if (risk <= 0) return null;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
side: "long",
|
|
56
|
+
entry,
|
|
57
|
+
stop,
|
|
58
|
+
rr: 2,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
exportBacktestArtifacts({
|
|
67
|
+
result,
|
|
68
|
+
outDir: "./output",
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Getting Historical Data
|
|
73
|
+
|
|
74
|
+
The simplest entry point is `getHistoricalCandles()`.
|
|
75
|
+
|
|
76
|
+
### Yahoo Finance
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
import { getHistoricalCandles, backtest } from "tradelab";
|
|
80
|
+
|
|
81
|
+
const candles = await getHistoricalCandles({
|
|
82
|
+
source: "yahoo",
|
|
83
|
+
symbol: "SPY",
|
|
84
|
+
interval: "1d",
|
|
85
|
+
period: "2y",
|
|
86
|
+
cache: true,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const result = backtest({
|
|
90
|
+
candles,
|
|
91
|
+
symbol: "SPY",
|
|
92
|
+
interval: "1d",
|
|
93
|
+
range: "2y",
|
|
94
|
+
signal,
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Supported period examples: `5d`, `60d`, `6mo`, `1y`.
|
|
99
|
+
|
|
100
|
+
### CSV
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
import { getHistoricalCandles } from "tradelab";
|
|
104
|
+
|
|
105
|
+
const candles = await getHistoricalCandles({
|
|
106
|
+
source: "csv",
|
|
107
|
+
symbol: "BTC-USD",
|
|
108
|
+
interval: "5m",
|
|
109
|
+
csvPath: "./data/btc-5m.csv",
|
|
110
|
+
csv: {
|
|
111
|
+
timeCol: "time",
|
|
112
|
+
openCol: "open",
|
|
113
|
+
highCol: "high",
|
|
114
|
+
lowCol: "low",
|
|
115
|
+
closeCol: "close",
|
|
116
|
+
volumeCol: "volume",
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
If you pass `csvPath` and omit `source`, the loader will auto-detect CSV mode.
|
|
122
|
+
|
|
123
|
+
## Signal Contract
|
|
124
|
+
|
|
125
|
+
Your strategy function receives:
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
{
|
|
129
|
+
candles, // history through the current bar
|
|
130
|
+
index, // current index in the original candle array
|
|
131
|
+
bar, // current candle
|
|
132
|
+
equity, // realized equity
|
|
133
|
+
openPosition, // null or current position
|
|
134
|
+
pendingOrder // null or current pending entry
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Return `null` for no trade, or a signal object:
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
{
|
|
142
|
+
side: "long" | "short",
|
|
143
|
+
entry: Number,
|
|
144
|
+
stop: Number,
|
|
145
|
+
takeProfit: Number
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Quality-of-life behavior:
|
|
150
|
+
|
|
151
|
+
- `side` also accepts `buy` and `sell`
|
|
152
|
+
- `entry` can be omitted and will default to the current bar close
|
|
153
|
+
- `takeProfit` can be omitted if `rr` or `_rr` is provided
|
|
154
|
+
- `qty` or `size` can override risk-based sizing
|
|
155
|
+
- `riskPct` or `riskFraction` can override the global risk setting per signal
|
|
156
|
+
|
|
157
|
+
Optional engine hints:
|
|
158
|
+
|
|
159
|
+
- `_entryExpiryBars`
|
|
160
|
+
- `_cooldownBars`
|
|
161
|
+
- `_breakevenAtR`
|
|
162
|
+
- `_trailAfterR`
|
|
163
|
+
- `_maxBarsInTrade`
|
|
164
|
+
- `_maxHoldMin`
|
|
165
|
+
- `_rr`
|
|
166
|
+
- `_initRisk`
|
|
167
|
+
- `_imb`
|
|
168
|
+
|
|
169
|
+
## Result Shape
|
|
170
|
+
|
|
171
|
+
`backtest()` returns:
|
|
172
|
+
|
|
173
|
+
- `trades`: every realized leg, including scale-outs
|
|
174
|
+
- `positions`: completed positions only
|
|
175
|
+
- `metrics`: aggregate performance stats
|
|
176
|
+
- `eqSeries`: realized equity history
|
|
177
|
+
- `replay`: chart-friendly frame and event data
|
|
178
|
+
|
|
179
|
+
## Main Exports
|
|
180
|
+
|
|
181
|
+
- `backtest(options)`
|
|
182
|
+
- `backtestHistorical({ data, backtestOptions })`
|
|
183
|
+
- `getHistoricalCandles(options)`
|
|
184
|
+
- `fetchHistorical(symbol, interval, period)`
|
|
185
|
+
- `loadCandlesFromCSV(filePath, options)`
|
|
186
|
+
- `saveCandlesToCache(candles, meta)`
|
|
187
|
+
- `loadCandlesFromCache(symbol, interval, period, outDir)`
|
|
188
|
+
- `exportBacktestArtifacts({ result, outDir })`
|
|
189
|
+
|
|
190
|
+
## Reports
|
|
191
|
+
|
|
192
|
+
The HTML report is self-contained apart from the Plotly CDN script. Report markup, CSS, and client-side chart code live under `templates/`, not inline in the report renderer.
|
|
193
|
+
|
|
194
|
+
Export helpers default CSV output to completed positions. Use `csvSource: "trades"` if you want every realized leg in the CSV.
|
|
195
|
+
|
|
196
|
+
## Examples
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
node examples/emaCross.js
|
|
200
|
+
node examples/yahooEmaCross.js SPY 1d 1y
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Local Scripts
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
npm run prefetch -- --symbol SPY --interval 1d --period 1y
|
|
207
|
+
npm run import-csv -- ./data/sample.csv --symbol BTC-USD --interval 5m
|
|
208
|
+
npm test
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Publishing
|
|
212
|
+
|
|
213
|
+
Validate the package contents before publishing:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npm test
|
|
217
|
+
npm pack --dry-run
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Publish when the dry run looks correct:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
npm publish
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Notes
|
|
227
|
+
|
|
228
|
+
- Yahoo downloads can be cached under `output/data` by default.
|
|
229
|
+
- The engine is intended for historical research, not brokerage execution.
|
|
230
|
+
- File output only happens through the reporting and cache helpers.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
|
|
4
|
+
import { backtest, ema, exportBacktestArtifacts } from "../src/index.js";
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
function formatNumber(value, digits = 2) {
|
|
10
|
+
if (!Number.isFinite(value)) return value > 0 ? "Inf" : "0";
|
|
11
|
+
return value.toFixed(digits);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function generateCandles(count = 900) {
|
|
15
|
+
const candles = [];
|
|
16
|
+
const start = Date.UTC(2025, 0, 2, 14, 30, 0);
|
|
17
|
+
let price = 100;
|
|
18
|
+
|
|
19
|
+
for (let index = 0; index < count; index += 1) {
|
|
20
|
+
const drift = Math.sin(index / 24) * 0.3 + Math.cos(index / 11) * 0.15;
|
|
21
|
+
const shock = Math.sin(index / 7) * 0.6;
|
|
22
|
+
const close = Math.max(20, price + drift + shock);
|
|
23
|
+
const open = price;
|
|
24
|
+
const high = Math.max(open, close) + 0.35 + Math.abs(Math.sin(index / 5)) * 0.2;
|
|
25
|
+
const low = Math.min(open, close) - 0.35 - Math.abs(Math.cos(index / 6)) * 0.2;
|
|
26
|
+
|
|
27
|
+
candles.push({
|
|
28
|
+
time: start + index * 5 * 60 * 1000,
|
|
29
|
+
open,
|
|
30
|
+
high,
|
|
31
|
+
low,
|
|
32
|
+
close,
|
|
33
|
+
volume: 1_000 + index,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
price = close;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return candles;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const candles = generateCandles();
|
|
43
|
+
|
|
44
|
+
const result = backtest({
|
|
45
|
+
candles,
|
|
46
|
+
symbol: "DEMO",
|
|
47
|
+
interval: "5m",
|
|
48
|
+
range: "synthetic",
|
|
49
|
+
equity: 25_000,
|
|
50
|
+
riskPct: 0.5,
|
|
51
|
+
collectEqSeries: true,
|
|
52
|
+
collectReplay: true,
|
|
53
|
+
signal({ candles: history }) {
|
|
54
|
+
if (history.length < 60) return null;
|
|
55
|
+
|
|
56
|
+
const closes = history.map((bar) => bar.close);
|
|
57
|
+
const fast = ema(closes, 12);
|
|
58
|
+
const slow = ema(closes, 26);
|
|
59
|
+
const lastIndex = closes.length - 1;
|
|
60
|
+
|
|
61
|
+
const fastNow = fast[lastIndex];
|
|
62
|
+
const slowNow = slow[lastIndex];
|
|
63
|
+
const fastPrev = fast[lastIndex - 1];
|
|
64
|
+
const slowPrev = slow[lastIndex - 1];
|
|
65
|
+
|
|
66
|
+
const crossedUp = fastPrev <= slowPrev && fastNow > slowNow;
|
|
67
|
+
const crossedDown = fastPrev >= slowPrev && fastNow < slowNow;
|
|
68
|
+
if (!crossedUp && !crossedDown) return null;
|
|
69
|
+
|
|
70
|
+
const recentBars = history.slice(-20);
|
|
71
|
+
const entry = history[lastIndex].close;
|
|
72
|
+
const stop = crossedUp
|
|
73
|
+
? Math.min(...recentBars.map((bar) => bar.low))
|
|
74
|
+
: Math.max(...recentBars.map((bar) => bar.high));
|
|
75
|
+
const risk = Math.abs(entry - stop);
|
|
76
|
+
if (!Number.isFinite(risk) || risk <= 0) return null;
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
side: crossedUp ? "long" : "short",
|
|
80
|
+
entry,
|
|
81
|
+
stop,
|
|
82
|
+
takeProfit: crossedUp ? entry + risk * 2.2 : entry - risk * 2.2,
|
|
83
|
+
_rr: 2.2,
|
|
84
|
+
_entryExpiryBars: 2,
|
|
85
|
+
_breakevenAtR: 1,
|
|
86
|
+
_trailAfterR: 1.5,
|
|
87
|
+
_cooldownBars: 6,
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const outputDir = path.join(__dirname, "output");
|
|
93
|
+
const artifacts = exportBacktestArtifacts({
|
|
94
|
+
result,
|
|
95
|
+
outDir: outputDir,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
console.table({
|
|
99
|
+
trades: result.metrics.trades,
|
|
100
|
+
winRate: `${(result.metrics.winRate * 100).toFixed(1)}%`,
|
|
101
|
+
profitFactor: formatNumber(result.metrics.profitFactor),
|
|
102
|
+
totalPnL: formatNumber(result.metrics.totalPnL),
|
|
103
|
+
returnPct: `${(result.metrics.returnPct * 100).toFixed(2)}%`,
|
|
104
|
+
maxDrawdownPct: `${(result.metrics.maxDrawdownPct * 100).toFixed(2)}%`,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
console.log("CSV:", artifacts.csv);
|
|
108
|
+
console.log("HTML:", artifacts.html);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
backtest,
|
|
6
|
+
ema,
|
|
7
|
+
exportBacktestArtifacts,
|
|
8
|
+
getHistoricalCandles,
|
|
9
|
+
} from "../src/index.js";
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
14
|
+
function formatNumber(value, digits = 2) {
|
|
15
|
+
if (!Number.isFinite(value)) return value > 0 ? "Inf" : "0";
|
|
16
|
+
return value.toFixed(digits);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const symbol = process.argv[2] || "SPY";
|
|
20
|
+
const interval = process.argv[3] || "1d";
|
|
21
|
+
const period = process.argv[4] || "1y";
|
|
22
|
+
|
|
23
|
+
const candles = await getHistoricalCandles({
|
|
24
|
+
source: "yahoo",
|
|
25
|
+
symbol,
|
|
26
|
+
interval,
|
|
27
|
+
period,
|
|
28
|
+
cache: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const result = backtest({
|
|
32
|
+
candles,
|
|
33
|
+
symbol,
|
|
34
|
+
interval,
|
|
35
|
+
range: period,
|
|
36
|
+
equity: 25_000,
|
|
37
|
+
riskPct: 1,
|
|
38
|
+
collectEqSeries: true,
|
|
39
|
+
collectReplay: true,
|
|
40
|
+
warmupBars: 50,
|
|
41
|
+
signal({ candles: history }) {
|
|
42
|
+
if (history.length < 50) return null;
|
|
43
|
+
|
|
44
|
+
const closes = history.map((bar) => bar.close);
|
|
45
|
+
const fast = ema(closes, 10);
|
|
46
|
+
const slow = ema(closes, 20);
|
|
47
|
+
const last = closes.length - 1;
|
|
48
|
+
|
|
49
|
+
const crossedUp = fast[last - 1] <= slow[last - 1] && fast[last] > slow[last];
|
|
50
|
+
const crossedDown = fast[last - 1] >= slow[last - 1] && fast[last] < slow[last];
|
|
51
|
+
if (!crossedUp && !crossedDown) return null;
|
|
52
|
+
|
|
53
|
+
const lookback = history.slice(-12);
|
|
54
|
+
const entry = history[last].close;
|
|
55
|
+
const stop = crossedUp
|
|
56
|
+
? Math.min(...lookback.map((bar) => bar.low))
|
|
57
|
+
: Math.max(...lookback.map((bar) => bar.high));
|
|
58
|
+
const risk = Math.abs(entry - stop);
|
|
59
|
+
if (!(risk > 0)) return null;
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
side: crossedUp ? "long" : "short",
|
|
63
|
+
entry,
|
|
64
|
+
stop,
|
|
65
|
+
rr: 2,
|
|
66
|
+
_entryExpiryBars: 1,
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const outDir = path.join(__dirname, "output");
|
|
72
|
+
const artifacts = exportBacktestArtifacts({
|
|
73
|
+
result,
|
|
74
|
+
outDir,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
console.table({
|
|
78
|
+
symbol,
|
|
79
|
+
candles: candles.length,
|
|
80
|
+
trades: result.metrics.trades,
|
|
81
|
+
winRate: `${(result.metrics.winRate * 100).toFixed(1)}%`,
|
|
82
|
+
profitFactor: formatNumber(result.metrics.profitFactor),
|
|
83
|
+
totalPnL: formatNumber(result.metrics.totalPnL),
|
|
84
|
+
returnPct: `${(result.metrics.returnPct * 100).toFixed(2)}%`,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
console.log("CSV:", artifacts.csv);
|
|
88
|
+
console.log("HTML:", artifacts.html);
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tradelab",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Reusable trading and backtesting engine for candle-based strategies",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=18"
|
|
10
|
+
},
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./src/index.js",
|
|
14
|
+
"./data": "./src/data/index.js",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"templates",
|
|
20
|
+
"examples/*.js",
|
|
21
|
+
"scripts",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "node --test",
|
|
27
|
+
"prefetch": "node scripts/prefetch.js",
|
|
28
|
+
"import-csv": "node scripts/import-csv.js",
|
|
29
|
+
"example:ema": "node examples/emaCross.js",
|
|
30
|
+
"example:yahoo": "node examples/yahooEmaCross.js"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"trading",
|
|
34
|
+
"backtesting",
|
|
35
|
+
"algorithmic-trading",
|
|
36
|
+
"ohlcv",
|
|
37
|
+
"quant"
|
|
38
|
+
],
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
candleStats,
|
|
5
|
+
loadCandlesFromCSV,
|
|
6
|
+
saveCandlesToCache,
|
|
7
|
+
} from "../src/index.js";
|
|
8
|
+
|
|
9
|
+
function parseArgs(argv) {
|
|
10
|
+
const args = {};
|
|
11
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
12
|
+
const token = argv[index];
|
|
13
|
+
if (!token.startsWith("--")) {
|
|
14
|
+
if (!args.file) args.file = token;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const key = token.slice(2);
|
|
19
|
+
const next = argv[index + 1];
|
|
20
|
+
if (next && !next.startsWith("--")) {
|
|
21
|
+
args[key] = next;
|
|
22
|
+
index += 1;
|
|
23
|
+
} else {
|
|
24
|
+
args[key] = "true";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return args;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const args = parseArgs(process.argv.slice(2));
|
|
31
|
+
|
|
32
|
+
if (!args.file || !args.symbol) {
|
|
33
|
+
console.log(
|
|
34
|
+
"Usage: node scripts/import-csv.js <file.csv> --symbol BTC-USD [--interval 5m] [--period 90d]"
|
|
35
|
+
);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const candles = loadCandlesFromCSV(args.file, {
|
|
41
|
+
delimiter: args.delimiter || ",",
|
|
42
|
+
timeCol: args.timeCol || "time",
|
|
43
|
+
openCol: args.openCol || "open",
|
|
44
|
+
highCol: args.highCol || "high",
|
|
45
|
+
lowCol: args.lowCol || "low",
|
|
46
|
+
closeCol: args.closeCol || "close",
|
|
47
|
+
volumeCol: args.volumeCol || "volume",
|
|
48
|
+
startDate: args.startDate,
|
|
49
|
+
endDate: args.endDate,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const stats = candleStats(candles);
|
|
53
|
+
const interval = args.interval || "1d";
|
|
54
|
+
const period = args.period || `${Math.max(1, Math.ceil(stats?.durationDays || 1))}d`;
|
|
55
|
+
const outputPath = saveCandlesToCache(candles, {
|
|
56
|
+
symbol: args.symbol,
|
|
57
|
+
interval,
|
|
58
|
+
period,
|
|
59
|
+
outDir: args.outDir || "output/data",
|
|
60
|
+
source: "csv",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
console.log(`Loaded ${stats?.count ?? 0} candles`);
|
|
64
|
+
console.log(`Range: ${stats?.firstTime ?? "—"} -> ${stats?.lastTime ?? "—"}`);
|
|
65
|
+
console.log(`Saved cache: ${outputPath}`);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(error.message);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { getHistoricalCandles, saveCandlesToCache } from "../src/index.js";
|
|
4
|
+
|
|
5
|
+
function parseArgs(argv) {
|
|
6
|
+
const args = {};
|
|
7
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
8
|
+
const token = argv[index];
|
|
9
|
+
if (!token.startsWith("--")) continue;
|
|
10
|
+
|
|
11
|
+
const key = token.slice(2);
|
|
12
|
+
const next = argv[index + 1];
|
|
13
|
+
if (next && !next.startsWith("--")) {
|
|
14
|
+
args[key] = next;
|
|
15
|
+
index += 1;
|
|
16
|
+
} else {
|
|
17
|
+
args[key] = "true";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return args;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const args = parseArgs(process.argv.slice(2));
|
|
24
|
+
const symbol = args.symbol || "SPY";
|
|
25
|
+
const interval = args.interval || "1d";
|
|
26
|
+
const period = args.period || "1y";
|
|
27
|
+
const outDir = args.outDir || "output/data";
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
const candles = await getHistoricalCandles({
|
|
31
|
+
source: "yahoo",
|
|
32
|
+
symbol,
|
|
33
|
+
interval,
|
|
34
|
+
period,
|
|
35
|
+
cache: false,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const outputPath = saveCandlesToCache(candles, {
|
|
39
|
+
symbol,
|
|
40
|
+
interval,
|
|
41
|
+
period,
|
|
42
|
+
outDir,
|
|
43
|
+
source: "yahoo",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
console.log(`Saved ${candles.length} candles to ${outputPath}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
main().catch((error) => {
|
|
50
|
+
console.error(error.message);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
});
|