tradelab 0.2.0 → 0.4.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.
@@ -32,6 +32,7 @@ __export(index_exports, {
32
32
  atr: () => atr,
33
33
  backtest: () => backtest,
34
34
  backtestHistorical: () => backtestHistorical,
35
+ backtestPortfolio: () => backtestPortfolio,
35
36
  bpsOf: () => bpsOf,
36
37
  buildMetrics: () => buildMetrics,
37
38
  cachedCandlesPath: () => cachedCandlesPath,
@@ -61,7 +62,8 @@ __export(index_exports, {
61
62
  saveCandlesToCache: () => saveCandlesToCache,
62
63
  structureState: () => structureState,
63
64
  swingHigh: () => swingHigh,
64
- swingLow: () => swingLow
65
+ swingLow: () => swingLow,
66
+ walkForwardOptimize: () => walkForwardOptimize
65
67
  });
66
68
  module.exports = __toCommonJS(index_exports);
67
69
 
@@ -789,14 +791,37 @@ function inWindowsET(timeMs, windows) {
789
791
  }
790
792
 
791
793
  // src/engine/execution.js
792
- function applyFill(price, side, { slippageBps = 0, feeBps = 0, kind = "market" } = {}) {
794
+ function resolveSlippageBps(kind, slippageBps, slippageByKind) {
795
+ if (Number.isFinite(slippageByKind?.[kind])) {
796
+ return slippageByKind[kind];
797
+ }
793
798
  let effectiveSlippageBps = slippageBps;
794
799
  if (kind === "limit") effectiveSlippageBps *= 0.25;
795
800
  if (kind === "stop") effectiveSlippageBps *= 1.25;
796
- const slippage = effectiveSlippageBps / 1e4 * price;
801
+ return effectiveSlippageBps;
802
+ }
803
+ function applyFill(price, side, { slippageBps = 0, feeBps = 0, kind = "market", qty = 0, costs = {} } = {}) {
804
+ const model = costs || {};
805
+ const modelSlippageBps = Number.isFinite(model.slippageBps) ? model.slippageBps : slippageBps;
806
+ const modelFeeBps = Number.isFinite(model.commissionBps) ? model.commissionBps : feeBps;
807
+ const effectiveSlippageBps = resolveSlippageBps(
808
+ kind,
809
+ modelSlippageBps,
810
+ model.slippageByKind
811
+ );
812
+ const halfSpreadBps = Number.isFinite(model.spreadBps) ? model.spreadBps / 2 : 0;
813
+ const slippage = (effectiveSlippageBps + halfSpreadBps) / 1e4 * price;
797
814
  const filledPrice = side === "long" ? price + slippage : price - slippage;
798
- const feePerUnit = feeBps / 1e4 * Math.abs(filledPrice);
799
- return { price: filledPrice, fee: feePerUnit };
815
+ const variableFeePerUnit = (modelFeeBps || 0) / 1e4 * Math.abs(filledPrice) + (Number.isFinite(model.commissionPerUnit) ? model.commissionPerUnit : 0);
816
+ const variableFeeTotal = variableFeePerUnit * Math.max(0, qty);
817
+ const fixedFeeTotal = Number.isFinite(model.commissionPerOrder) ? model.commissionPerOrder : 0;
818
+ const grossFeeTotal = variableFeeTotal + fixedFeeTotal;
819
+ const feeTotal = Math.max(
820
+ Number.isFinite(model.minCommission) ? model.minCommission : 0,
821
+ grossFeeTotal
822
+ );
823
+ const feePerUnit = qty > 0 ? feeTotal / qty : variableFeePerUnit;
824
+ return { price: filledPrice, fee: feePerUnit, feeTotal };
800
825
  }
801
826
  function clampStop(marketPrice, proposedStop, side, oco) {
802
827
  const epsilon = (oco?.clampEpsBps ?? 0.25) / 1e4;
@@ -918,6 +943,7 @@ function mergeOptions(options) {
918
943
  warmupBars: options.warmupBars ?? 200,
919
944
  slippageBps: options.slippageBps ?? 1,
920
945
  feeBps: options.feeBps ?? 0,
946
+ costs: options.costs ?? null,
921
947
  scaleOutAtR: options.scaleOutAtR ?? 1,
922
948
  scaleOutFrac: options.scaleOutFrac ?? 0.5,
923
949
  finalTP_R: options.finalTP_R ?? 3,
@@ -1018,6 +1044,7 @@ function backtest(rawOptions) {
1018
1044
  signal,
1019
1045
  slippageBps,
1020
1046
  feeBps,
1047
+ costs,
1021
1048
  scaleOutAtR,
1022
1049
  scaleOutFrac,
1023
1050
  finalTP_R,
@@ -1087,12 +1114,11 @@ function backtest(rawOptions) {
1087
1114
  });
1088
1115
  }
1089
1116
  }
1090
- function closeLeg({ openPos, qty, exitPx, exitFeePerUnit, time, reason }) {
1117
+ function closeLeg({ openPos, qty, exitPx, exitFeeTotal = 0, time, reason }) {
1091
1118
  const direction = openPos.side === "long" ? 1 : -1;
1092
1119
  const entryFill = openPos.entryFill;
1093
1120
  const grossPnl = (exitPx - entryFill) * direction * qty;
1094
1121
  const entryFeePortion = (openPos.entryFeeTotal || 0) * (qty / openPos.initSize);
1095
- const exitFeeTotal = exitFeePerUnit * qty;
1096
1122
  const pnl = grossPnl - entryFeePortion - exitFeeTotal;
1097
1123
  currentEquity += pnl;
1098
1124
  dayPnl += pnl;
@@ -1145,16 +1171,18 @@ function backtest(rawOptions) {
1145
1171
  function forceExit(reason, bar) {
1146
1172
  if (!open) return;
1147
1173
  const exitSide = open.side === "long" ? "short" : "long";
1148
- const { price: filled, fee: exitFee } = applyFill(bar.close, exitSide, {
1174
+ const { price: filled, feeTotal: exitFeeTotal } = applyFill(bar.close, exitSide, {
1149
1175
  slippageBps,
1150
1176
  feeBps,
1151
- kind: "market"
1177
+ kind: "market",
1178
+ qty: open.size,
1179
+ costs
1152
1180
  });
1153
1181
  closeLeg({
1154
1182
  openPos: open,
1155
1183
  qty: open.size,
1156
1184
  exitPx: filled,
1157
- exitFeePerUnit: exitFee,
1185
+ exitFeeTotal,
1158
1186
  time: bar.time,
1159
1187
  reason
1160
1188
  });
@@ -1195,13 +1223,15 @@ function backtest(rawOptions) {
1195
1223
  });
1196
1224
  const size = roundStep2(rawSize, qtyStep);
1197
1225
  if (size < minQty) return false;
1198
- const { price: entryFill, fee: entryFee } = applyFill(
1226
+ const { price: entryFill, feeTotal: entryFeeTotal } = applyFill(
1199
1227
  entryPrice,
1200
1228
  pending.side,
1201
1229
  {
1202
1230
  slippageBps,
1203
1231
  feeBps,
1204
- kind: fillKind
1232
+ kind: fillKind,
1233
+ qty: size,
1234
+ costs
1205
1235
  }
1206
1236
  );
1207
1237
  open = {
@@ -1215,7 +1245,7 @@ function backtest(rawOptions) {
1215
1245
  size,
1216
1246
  openTime: bar.time,
1217
1247
  entryFill,
1218
- entryFeeTotal: entryFee * size,
1248
+ entryFeeTotal,
1219
1249
  initSize: size,
1220
1250
  baseSize: size,
1221
1251
  _mfeR: 0,
@@ -1311,16 +1341,16 @@ function backtest(rawOptions) {
1311
1341
  const cutQty = roundStep2(open.size * volScale.cutFrac, qtyStep);
1312
1342
  if (cutQty >= minQty && cutQty < open.size) {
1313
1343
  const exitSide2 = open.side === "long" ? "short" : "long";
1314
- const { price: filled, fee: exitFee } = applyFill(
1344
+ const { price: filled, feeTotal: exitFeeTotal } = applyFill(
1315
1345
  bar.close,
1316
1346
  exitSide2,
1317
- { slippageBps, feeBps, kind: "market" }
1347
+ { slippageBps, feeBps, kind: "market", qty: cutQty, costs }
1318
1348
  );
1319
1349
  closeLeg({
1320
1350
  openPos: open,
1321
1351
  qty: cutQty,
1322
1352
  exitPx: filled,
1323
- exitFeePerUnit: exitFee,
1353
+ exitFeeTotal,
1324
1354
  time: bar.time,
1325
1355
  reason: "SCALE"
1326
1356
  });
@@ -1340,13 +1370,13 @@ function backtest(rawOptions) {
1340
1370
  const baseSize = open.baseSize || open.initSize;
1341
1371
  const addQty = roundStep2(baseSize * pyramiding.addFrac, qtyStep);
1342
1372
  if (addQty >= minQty) {
1343
- const { price: addFill, fee: addFee } = applyFill(
1373
+ const { price: addFill, feeTotal: addFeeTotal } = applyFill(
1344
1374
  triggerPrice,
1345
1375
  open.side,
1346
- { slippageBps, feeBps, kind: "limit" }
1376
+ { slippageBps, feeBps, kind: "limit", qty: addQty, costs }
1347
1377
  );
1348
1378
  const newSize = open.size + addQty;
1349
- open.entryFeeTotal += addFee * addQty;
1379
+ open.entryFeeTotal += addFeeTotal;
1350
1380
  open.entryFill = (open.entryFill * open.size + addFill * addQty) / newSize;
1351
1381
  open.size = newSize;
1352
1382
  open.initSize += addQty;
@@ -1361,18 +1391,20 @@ function backtest(rawOptions) {
1361
1391
  const touched = open.side === "long" ? trigger === "intrabar" ? bar.high >= triggerPrice : bar.close >= triggerPrice : trigger === "intrabar" ? bar.low <= triggerPrice : bar.close <= triggerPrice;
1362
1392
  if (touched) {
1363
1393
  const exitSide2 = open.side === "long" ? "short" : "long";
1364
- const { price: filled, fee: exitFee } = applyFill(triggerPrice, exitSide2, {
1365
- slippageBps,
1366
- feeBps,
1367
- kind: "limit"
1368
- });
1369
1394
  const qty = roundStep2(open.size * scaleOutFrac, qtyStep);
1370
1395
  if (qty >= minQty && qty < open.size) {
1396
+ const { price: filled, feeTotal: exitFeeTotal } = applyFill(triggerPrice, exitSide2, {
1397
+ slippageBps,
1398
+ feeBps,
1399
+ kind: "limit",
1400
+ qty,
1401
+ costs
1402
+ });
1371
1403
  closeLeg({
1372
1404
  openPos: open,
1373
1405
  qty,
1374
1406
  exitPx: filled,
1375
- exitFeePerUnit: exitFee,
1407
+ exitFeeTotal,
1376
1408
  time: bar.time,
1377
1409
  reason: "SCALE"
1378
1410
  });
@@ -1394,17 +1426,19 @@ function backtest(rawOptions) {
1394
1426
  });
1395
1427
  if (hit) {
1396
1428
  const exitKind = hit === "TP" ? "limit" : "stop";
1397
- const { price: filled, fee: exitFee } = applyFill(px, exitSide, {
1429
+ const { price: filled, feeTotal: exitFeeTotal } = applyFill(px, exitSide, {
1398
1430
  slippageBps,
1399
1431
  feeBps,
1400
- kind: exitKind
1432
+ kind: exitKind,
1433
+ qty: open.size,
1434
+ costs
1401
1435
  });
1402
1436
  const localCooldown = open._cooldownBars || 0;
1403
1437
  closeLeg({
1404
1438
  openPos: open,
1405
1439
  qty: open.size,
1406
1440
  exitPx: filled,
1407
- exitFeePerUnit: exitFee,
1441
+ exitFeeTotal,
1408
1442
  time: bar.time,
1409
1443
  reason: hit
1410
1444
  });
@@ -1542,6 +1576,232 @@ function backtest(rawOptions) {
1542
1576
  };
1543
1577
  }
1544
1578
 
1579
+ // src/engine/portfolio.js
1580
+ function asWeight(value) {
1581
+ return Number.isFinite(value) && value > 0 ? value : 0;
1582
+ }
1583
+ function combineEquitySeries(systemRuns, totalEquity) {
1584
+ const timeline = /* @__PURE__ */ new Set();
1585
+ for (const run of systemRuns) {
1586
+ for (const point of run.result.eqSeries || []) {
1587
+ timeline.add(point.time);
1588
+ }
1589
+ }
1590
+ const times = [...timeline].sort((left, right) => left - right);
1591
+ if (!times.length) {
1592
+ return [{ time: 0, timestamp: 0, equity: totalEquity }];
1593
+ }
1594
+ const states = systemRuns.map((run) => ({
1595
+ points: run.result.eqSeries || [],
1596
+ index: 0,
1597
+ lastEquity: run.allocationEquity
1598
+ }));
1599
+ return times.map((time) => {
1600
+ let equity = 0;
1601
+ states.forEach((state) => {
1602
+ while (state.index < state.points.length && state.points[state.index].time <= time) {
1603
+ state.lastEquity = state.points[state.index].equity;
1604
+ state.index += 1;
1605
+ }
1606
+ equity += state.lastEquity;
1607
+ });
1608
+ return { time, timestamp: time, equity };
1609
+ });
1610
+ }
1611
+ function combineReplay(systemRuns, eqSeries, collectReplay) {
1612
+ if (!collectReplay) {
1613
+ return { frames: [], events: [] };
1614
+ }
1615
+ const events = systemRuns.flatMap(
1616
+ (run) => (run.result.replay?.events || []).map((event) => ({
1617
+ ...event,
1618
+ symbol: event.symbol || run.symbol
1619
+ }))
1620
+ ).sort((left, right) => new Date(left.t).getTime() - new Date(right.t).getTime());
1621
+ const frames = eqSeries.map((point) => ({
1622
+ t: new Date(point.time).toISOString(),
1623
+ price: 0,
1624
+ equity: point.equity,
1625
+ posSide: null,
1626
+ posSize: 0
1627
+ }));
1628
+ return { frames, events };
1629
+ }
1630
+ function backtestPortfolio({
1631
+ systems = [],
1632
+ equity = 1e4,
1633
+ allocation = "equal",
1634
+ collectEqSeries = true,
1635
+ collectReplay = false
1636
+ } = {}) {
1637
+ if (!Array.isArray(systems) || systems.length === 0) {
1638
+ throw new Error("backtestPortfolio() requires a non-empty systems array");
1639
+ }
1640
+ const weights = allocation === "equal" ? systems.map(() => 1) : systems.map((system) => asWeight(system.weight || 0));
1641
+ const totalWeight = weights.reduce((sum2, weight) => sum2 + weight, 0);
1642
+ if (!(totalWeight > 0)) {
1643
+ throw new Error("backtestPortfolio() requires positive allocation weights");
1644
+ }
1645
+ const systemRuns = systems.map((system, index) => {
1646
+ const allocationEquity = equity * (weights[index] / totalWeight);
1647
+ const result = backtest({
1648
+ ...system,
1649
+ equity: allocationEquity,
1650
+ collectEqSeries,
1651
+ collectReplay
1652
+ });
1653
+ return {
1654
+ symbol: system.symbol ?? result.symbol ?? `system-${index + 1}`,
1655
+ weight: weights[index],
1656
+ allocationEquity,
1657
+ result
1658
+ };
1659
+ });
1660
+ const trades = systemRuns.flatMap(
1661
+ (run) => run.result.trades.map((trade) => ({
1662
+ ...trade,
1663
+ symbol: trade.symbol || run.symbol
1664
+ }))
1665
+ ).sort((left, right) => left.exit.time - right.exit.time);
1666
+ const positions = systemRuns.flatMap(
1667
+ (run) => run.result.positions.map((trade) => ({
1668
+ ...trade,
1669
+ symbol: trade.symbol || run.symbol
1670
+ }))
1671
+ ).sort((left, right) => left.exit.time - right.exit.time);
1672
+ const eqSeries = collectEqSeries ? combineEquitySeries(systemRuns, equity) : [];
1673
+ const replay = combineReplay(systemRuns, eqSeries, collectReplay);
1674
+ const allCandles = systems.flatMap((system) => system.candles || []);
1675
+ const orderedCandles = [...allCandles].sort((left, right) => left.time - right.time);
1676
+ const metrics = buildMetrics({
1677
+ closed: trades,
1678
+ equityStart: equity,
1679
+ equityFinal: eqSeries.length ? eqSeries[eqSeries.length - 1].equity : equity,
1680
+ candles: orderedCandles,
1681
+ estBarMs: estimateBarMs(orderedCandles),
1682
+ eqSeries
1683
+ });
1684
+ return {
1685
+ symbol: "PORTFOLIO",
1686
+ interval: void 0,
1687
+ range: void 0,
1688
+ trades,
1689
+ positions,
1690
+ metrics,
1691
+ eqSeries,
1692
+ replay,
1693
+ systems: systemRuns.map((run) => ({
1694
+ symbol: run.symbol,
1695
+ weight: run.weight / totalWeight,
1696
+ equity: run.allocationEquity,
1697
+ result: run.result
1698
+ }))
1699
+ };
1700
+ }
1701
+
1702
+ // src/engine/walkForward.js
1703
+ function scoreOf(metrics, scoreBy) {
1704
+ const value = metrics?.[scoreBy];
1705
+ return Number.isFinite(value) ? value : -Infinity;
1706
+ }
1707
+ function stitchEquitySeries(target, source) {
1708
+ if (!source?.length) return;
1709
+ if (!target.length) {
1710
+ target.push(...source);
1711
+ return;
1712
+ }
1713
+ const lastTime = target[target.length - 1].time;
1714
+ const nextPoints = source.filter((point) => point.time > lastTime);
1715
+ target.push(...nextPoints);
1716
+ }
1717
+ function walkForwardOptimize({
1718
+ candles = [],
1719
+ signalFactory,
1720
+ parameterSets = [],
1721
+ trainBars,
1722
+ testBars,
1723
+ stepBars = testBars,
1724
+ scoreBy = "profitFactor",
1725
+ backtestOptions = {}
1726
+ } = {}) {
1727
+ if (!Array.isArray(candles) || candles.length === 0) {
1728
+ throw new Error("walkForwardOptimize() requires a non-empty candles array");
1729
+ }
1730
+ if (typeof signalFactory !== "function") {
1731
+ throw new Error("walkForwardOptimize() requires a signalFactory function");
1732
+ }
1733
+ if (!Array.isArray(parameterSets) || parameterSets.length === 0) {
1734
+ throw new Error("walkForwardOptimize() requires parameterSets");
1735
+ }
1736
+ if (!(trainBars > 0) || !(testBars > 0) || !(stepBars > 0)) {
1737
+ throw new Error("walkForwardOptimize() requires positive trainBars, testBars, and stepBars");
1738
+ }
1739
+ const windows = [];
1740
+ const allTrades = [];
1741
+ const allPositions = [];
1742
+ const eqSeries = [];
1743
+ let rollingEquity = backtestOptions.equity ?? 1e4;
1744
+ for (let start = 0; start + trainBars + testBars <= candles.length; start += stepBars) {
1745
+ const trainSlice = candles.slice(start, start + trainBars);
1746
+ const testSlice = candles.slice(start + trainBars, start + trainBars + testBars);
1747
+ let best = null;
1748
+ for (const params of parameterSets) {
1749
+ const trainResult = backtest({
1750
+ ...backtestOptions,
1751
+ candles: trainSlice,
1752
+ equity: rollingEquity,
1753
+ signal: signalFactory(params)
1754
+ });
1755
+ const score = scoreOf(trainResult.metrics, scoreBy);
1756
+ if (!best || score > best.score) {
1757
+ best = { params, score, metrics: trainResult.metrics };
1758
+ }
1759
+ }
1760
+ const testResult = backtest({
1761
+ ...backtestOptions,
1762
+ candles: testSlice,
1763
+ equity: rollingEquity,
1764
+ signal: signalFactory(best.params)
1765
+ });
1766
+ rollingEquity = testResult.metrics.finalEquity;
1767
+ allTrades.push(...testResult.trades);
1768
+ allPositions.push(...testResult.positions);
1769
+ stitchEquitySeries(eqSeries, testResult.eqSeries);
1770
+ windows.push({
1771
+ train: {
1772
+ start: trainSlice[0]?.time ?? null,
1773
+ end: trainSlice[trainSlice.length - 1]?.time ?? null
1774
+ },
1775
+ test: {
1776
+ start: testSlice[0]?.time ?? null,
1777
+ end: testSlice[testSlice.length - 1]?.time ?? null
1778
+ },
1779
+ bestParams: best.params,
1780
+ trainScore: best.score,
1781
+ trainMetrics: best.metrics,
1782
+ testMetrics: testResult.metrics,
1783
+ result: testResult
1784
+ });
1785
+ }
1786
+ const metrics = buildMetrics({
1787
+ closed: allTrades,
1788
+ equityStart: backtestOptions.equity ?? 1e4,
1789
+ equityFinal: rollingEquity,
1790
+ candles,
1791
+ estBarMs: estimateBarMs(candles),
1792
+ eqSeries
1793
+ });
1794
+ return {
1795
+ windows,
1796
+ trades: allTrades,
1797
+ positions: allPositions,
1798
+ metrics,
1799
+ eqSeries,
1800
+ replay: { frames: [], events: [] },
1801
+ bestParams: windows.map((window) => window.bestParams)
1802
+ };
1803
+ }
1804
+
1545
1805
  // src/data/index.js
1546
1806
  var import_path2 = __toESM(require("path"), 1);
1547
1807
 
@@ -2261,6 +2521,7 @@ function exportBacktestArtifacts({
2261
2521
  atr,
2262
2522
  backtest,
2263
2523
  backtestHistorical,
2524
+ backtestPortfolio,
2264
2525
  bpsOf,
2265
2526
  buildMetrics,
2266
2527
  cachedCandlesPath,
@@ -2290,5 +2551,6 @@ function exportBacktestArtifacts({
2290
2551
  saveCandlesToCache,
2291
2552
  structureState,
2292
2553
  swingHigh,
2293
- swingLow
2554
+ swingLow,
2555
+ walkForwardOptimize
2294
2556
  });
package/docs/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # tradelab docs
2
+
3
+ ## Guides
4
+
5
+ - [Backtest engine](backtest-engine.md)
6
+ - [Data, reporting, and CLI](data-reporting-cli.md)
7
+ - [API reference](api-reference.md)
8
+
9
+ ## Choose a path
10
+
11
+ | Goal | Start here |
12
+ | --- | --- |
13
+ | Run one strategy on one dataset | [Backtest engine](backtest-engine.md) |
14
+ | Load Yahoo or CSV data | [Data, reporting, and CLI](data-reporting-cli.md) |
15
+ | Export reports or machine-readable results | [Data, reporting, and CLI](data-reporting-cli.md) |
16
+ | Run multiple symbols together | [Backtest engine](backtest-engine.md) |
17
+ | Run walk-forward validation | [Backtest engine](backtest-engine.md) |
18
+ | Check the exact public exports | [API reference](api-reference.md) |
19
+
20
+ ## Package scope
21
+
22
+ tradelab is built for:
23
+
24
+ - candle-based strategy research
25
+ - historical backtests with configurable fills and costs
26
+ - CSV and Yahoo-based data workflows
27
+ - exportable outputs for review or automation
28
+
29
+ tradelab is not built for:
30
+
31
+ - live broker execution
32
+ - tick-level simulation
33
+ - exchange microstructure modeling
34
+
35
+ ## Common workflows
36
+
37
+ ### Single strategy workflow
38
+
39
+ 1. Load candles with `getHistoricalCandles()` or your own dataset
40
+ 2. Run `backtest()`
41
+ 3. Inspect `result.metrics` and `result.positions`
42
+ 4. Export HTML, CSV, or JSON if needed
43
+
44
+ ### Multi-symbol workflow
45
+
46
+ 1. Prepare one candle array per symbol
47
+ 2. Run `backtestPortfolio()`
48
+ 3. Review combined `metrics`, `positions`, and `eqSeries`
49
+
50
+ ### Validation workflow
51
+
52
+ 1. Build a `signalFactory(params)`
53
+ 2. Create parameter sets
54
+ 3. Run `walkForwardOptimize()`
55
+ 4. Review per-window winners before trusting the aggregate result
56
+
57
+ ## Documentation map
58
+
59
+ - [Backtest engine](backtest-engine.md): strategy inputs, engine options, result shape, portfolio mode, walk-forward mode
60
+ - [Data, reporting, and CLI](data-reporting-cli.md): data loading, cache behavior, exports, terminal usage
61
+ - [API reference](api-reference.md): compact export index
@@ -0,0 +1,70 @@
1
+ # API reference
2
+
3
+ This page is the compact index of public exports.
4
+
5
+ If you are learning the package, start with [backtest-engine.md](backtest-engine.md) or [data-reporting-cli.md](data-reporting-cli.md). This page is for quick lookup.
6
+
7
+ ## Backtesting
8
+
9
+ | Export | Summary |
10
+ | --- | --- |
11
+ | `backtest(options)` | Run one strategy on one candle series |
12
+ | `backtestPortfolio(options)` | Run multiple systems and merge the result |
13
+ | `walkForwardOptimize(options)` | Run rolling train/test validation |
14
+ | `buildMetrics(input)` | Compute metrics from realized trades and equity data |
15
+
16
+ ## Data
17
+
18
+ | Export | Summary |
19
+ | --- | --- |
20
+ | `getHistoricalCandles(options)` | Load candles from Yahoo or CSV |
21
+ | `backtestHistorical({ data, backtestOptions })` | Load candles and immediately run `backtest()` |
22
+ | `fetchHistorical(symbol, interval, period, options)` | Call the Yahoo layer directly |
23
+ | `fetchLatestCandle(symbol, interval, options)` | Fetch the latest Yahoo candle |
24
+ | `loadCandlesFromCSV(filePath, options)` | Parse and normalize a CSV file |
25
+ | `normalizeCandles(candles)` | Normalize candle field names and sort/dedupe |
26
+ | `mergeCandles(...arrays)` | Merge multiple candle arrays |
27
+ | `candleStats(candles)` | Return summary stats for a candle array |
28
+ | `saveCandlesToCache(candles, meta)` | Write normalized candles to the local cache |
29
+ | `loadCandlesFromCache(symbol, interval, period, outDir)` | Read normalized candles from the local cache |
30
+ | `cachedCandlesPath(symbol, interval, period, outDir)` | Return the expected cache path |
31
+
32
+ ## Reporting
33
+
34
+ | Export | Summary |
35
+ | --- | --- |
36
+ | `renderHtmlReport(options)` | Return the HTML report as a string |
37
+ | `exportHtmlReport(options)` | Write the HTML report to disk |
38
+ | `exportTradesCsv(trades, options)` | Write a CSV ledger of trades or positions |
39
+ | `exportMetricsJSON(options)` | Write machine-readable metrics JSON |
40
+ | `exportBacktestArtifacts(options)` | Write HTML, CSV, and metrics JSON together |
41
+
42
+ ## Indicators and utilities
43
+
44
+ ### Indicators
45
+
46
+ - `ema(values, period)`
47
+ - `atr(bars, period)`
48
+ - `swingHigh(bars, index, left, right)`
49
+ - `swingLow(bars, index, left, right)`
50
+ - `detectFVG(bars, index)`
51
+ - `lastSwing(bars, index, direction)`
52
+ - `structureState(bars, index)`
53
+ - `bpsOf(price, bps)`
54
+ - `pct(a, b)`
55
+
56
+ ### Position sizing
57
+
58
+ - `calculatePositionSize(input)`
59
+
60
+ ### Time helpers
61
+
62
+ - `offsetET(timeMs)`
63
+ - `minutesET(timeMs)`
64
+ - `isSession(timeMs, session)`
65
+ - `parseWindowsCSV(csv)`
66
+ - `inWindowsET(timeMs, windows)`
67
+
68
+ ## Types
69
+
70
+ The package ships declarations in [../types/index.d.ts](../types/index.d.ts). Use that file when you need the exact option and result contracts in TypeScript or editor IntelliSense.