@dsh-stack/trading-backtest 0.0.0-stage → 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/README.md CHANGED
@@ -1,3 +1,3 @@
1
- # Temporary Holding Version
1
+ # trading-backtest
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Backtesting primitives for Stack trading research and strategy evaluation.
package/lib/index.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ import type { Candle } from "@dsh-stack/trading-research";
2
+ export type PositionSide = "long" | "short";
3
+ export interface Position {
4
+ readonly side: PositionSide;
5
+ readonly quantity: number;
6
+ readonly entryPrice: number;
7
+ readonly entryTime: number;
8
+ }
9
+ export interface BacktestContext {
10
+ readonly candle: Candle;
11
+ readonly position: Position | null;
12
+ readonly equity: number;
13
+ readonly cash: number;
14
+ readonly history: readonly Trade[];
15
+ buy(quantity: number): void;
16
+ sell(quantity: number): void;
17
+ close(): void;
18
+ }
19
+ export interface Strategy {
20
+ readonly id: string;
21
+ onCandle(context: BacktestContext): void;
22
+ }
23
+ export interface Trade {
24
+ readonly side: PositionSide;
25
+ readonly quantity: number;
26
+ readonly entryPrice: number;
27
+ readonly exitPrice: number;
28
+ readonly entryTime: number;
29
+ readonly exitTime: number;
30
+ readonly pnl: number;
31
+ }
32
+ export interface BacktestResult {
33
+ readonly initialCapital: number;
34
+ readonly finalEquity: number;
35
+ readonly totalReturn: number;
36
+ readonly maxDrawdown: number;
37
+ readonly winRate: number;
38
+ readonly trades: readonly Trade[];
39
+ }
40
+ export interface BacktestOptions {
41
+ readonly initialCapital?: number;
42
+ readonly commissionPerTrade?: number;
43
+ readonly slippageBps?: number;
44
+ }
45
+ /**
46
+ * Runs a backtest using the provided candles, strategy, and options to simulate trades and generate a backtest result.
47
+ *
48
+ * @param candles - The historical price data used for the backtest.
49
+ * @param strategy - The trading strategy to execute during the backtest.
50
+ * @param options - Configuration options for the backtest, including initial capital, commission, and slippage.
51
+ * @returns A BacktestResult containing the equity curve and list of trades.
52
+ * @throws Will throw an error if the candle series is empty or if the initial capital is not positive.
53
+ */
54
+ export declare function runBacktest(candles: readonly Candle[], strategy: Strategy, options?: BacktestOptions): BacktestResult;
package/lib/index.js ADDED
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Runs a backtest using the provided candles, strategy, and options to simulate trades and generate a backtest result.
3
+ *
4
+ * @param candles - The historical price data used for the backtest.
5
+ * @param strategy - The trading strategy to execute during the backtest.
6
+ * @param options - Configuration options for the backtest, including initial capital, commission, and slippage.
7
+ * @returns A BacktestResult containing the equity curve and list of trades.
8
+ * @throws Will throw an error if the candle series is empty or if the initial capital is not positive.
9
+ */
10
+ export function runBacktest(candles, strategy, options = {}) {
11
+ if (candles.length === 0)
12
+ throw new Error("Cannot backtest an empty candle series");
13
+ const initialCapital = options.initialCapital ?? 10_000;
14
+ if (!(initialCapital > 0))
15
+ throw new Error("Initial capital must be positive");
16
+ const commission = options.commissionPerTrade ?? 0;
17
+ const slippage = (options.slippageBps ?? 0) / 10_000;
18
+ let cash = initialCapital;
19
+ let position = null;
20
+ let currentCandle = candles[0];
21
+ const trades = [];
22
+ const equityCurve = [];
23
+ /**
24
+ * Adjusts the price based on the trade side, applying slippage for buys and slippage/leverage for sells.
25
+ *
26
+ * @param price - The base price for the trade.
27
+ * @param side - The side of the trade, either "buy" or "sell".
28
+ * @returns The adjusted price accounting for slippage or leverage.
29
+ * @throws Throws an error if the side is neither "buy" nor "sell".
30
+ */
31
+ const executionPrice = (price, side) => side === "buy" ? price * (1 + slippage) : price * (1 - slippage);
32
+ /**
33
+ * Calculates the current equity or margin level based on the current position and cash balance.
34
+ *
35
+ * @param price - The price used to calculate the equity or margin.
36
+ * @returns The current equity or margin level.
37
+ * @throws Throws an error if the position is null and cash is not defined.
38
+ */
39
+ const mark = (price) => position === null
40
+ ? cash
41
+ : cash + (position.side === "long" ? position.quantity * price : -position.quantity * price);
42
+ for (const candle of candles) {
43
+ currentCandle = candle;
44
+ const context = {
45
+ candle,
46
+ position,
47
+ equity: mark(candle.close),
48
+ cash,
49
+ history: trades,
50
+ buy: (quantity) => {
51
+ if (!(quantity > 0))
52
+ throw new Error("Buy quantity must be positive");
53
+ if (position?.side === "short") {
54
+ close();
55
+ if (position !== null)
56
+ throw new Error("Unable to close short before buying");
57
+ openLong(quantity);
58
+ return;
59
+ }
60
+ if (position !== null)
61
+ throw new Error("A long position is already open");
62
+ openLong(quantity);
63
+ },
64
+ sell: (quantity) => {
65
+ if (!(quantity > 0))
66
+ throw new Error("Sell quantity must be positive");
67
+ if (position?.side === "long") {
68
+ close();
69
+ if (position !== null)
70
+ throw new Error("Unable to close long before selling");
71
+ openShort(quantity);
72
+ return;
73
+ }
74
+ if (position !== null)
75
+ throw new Error("A short position is already open");
76
+ openShort(quantity);
77
+ },
78
+ close,
79
+ };
80
+ strategy.onCandle(context);
81
+ equityCurve.push(mark(candle.close));
82
+ }
83
+ close();
84
+ const finalEquity = cash;
85
+ let peak = initialCapital;
86
+ let maxDrawdown = 0;
87
+ for (const equity of [...equityCurve, finalEquity]) {
88
+ peak = Math.max(peak, equity);
89
+ if (peak > 0)
90
+ maxDrawdown = Math.max(maxDrawdown, (peak - equity) / peak);
91
+ }
92
+ const wins = trades.filter((trade) => trade.pnl > 0).length;
93
+ return {
94
+ initialCapital,
95
+ finalEquity,
96
+ totalReturn: (finalEquity - initialCapital) / initialCapital,
97
+ maxDrawdown,
98
+ winRate: trades.length === 0 ? 0 : wins / trades.length,
99
+ trades,
100
+ };
101
+ /**
102
+ * Opens a long position with the specified quantity.
103
+ * Guarantees that no long position is already open before opening.
104
+ * Throws an error if a long position is already open or if the sell quantity is not positive.
105
+ * On failure, throws an error indicating the inability to open a long position.
106
+ */
107
+ function openLong(quantity) {
108
+ const price = executionPrice(currentCandle.close, "buy");
109
+ position = { side: "long", quantity, entryPrice: price, entryTime: currentCandle.time };
110
+ cash -= quantity * price + commission;
111
+ }
112
+ /** openShort implementation. */
113
+ function openShort(quantity) {
114
+ const price = executionPrice(currentCandle.close, "sell");
115
+ position = { side: "short", quantity, entryPrice: price, entryTime: currentCandle.time };
116
+ cash += quantity * price - commission;
117
+ }
118
+ /**
119
+ * Closes the trading strategy, calculates performance metrics, and returns the strategy's performance report.
120
+ *
121
+ * Guarantees that the strategy's trades, equity curve, and final equity are computed before returning.
122
+ * Returns an object containing initial capital, final equity, total return, max drawdown, win rate, and the list of trades.
123
+ * Fails if the strategy has not been initialized or if there are no trades to evaluate.
124
+ */
125
+ function closeAt(price, time) {
126
+ if (position === null)
127
+ return;
128
+ const exitPrice = position.side === "long" ? executionPrice(price, "sell") : executionPrice(price, "buy");
129
+ const pnl = position.side === "long"
130
+ ? position.quantity * (exitPrice - position.entryPrice) - commission * 2
131
+ : position.quantity * (position.entryPrice - exitPrice) - commission * 2;
132
+ if (position.side === "long")
133
+ cash += position.quantity * exitPrice - commission;
134
+ else
135
+ cash -= position.quantity * exitPrice + commission;
136
+ trades.push({
137
+ side: position.side,
138
+ quantity: position.quantity,
139
+ entryPrice: position.entryPrice,
140
+ exitPrice,
141
+ entryTime: position.entryTime,
142
+ exitTime: time,
143
+ pnl,
144
+ });
145
+ position = null;
146
+ }
147
+ /**
148
+ * Closes the trading strategy by evaluating performance metrics and returns a performance report.
149
+ *
150
+ * Guarantees that the strategy's trades, equity curve, and final equity are computed before returning.
151
+ * Returns an object containing initial capital, final equity, total return, max drawdown, win rate, and the list of trades.
152
+ * Fails if the strategy has not been initialized or if there are no trades to evaluate.
153
+ */
154
+ function close() {
155
+ closeAt(currentCandle.close, currentCandle.time);
156
+ }
157
+ }
package/package.json CHANGED
@@ -1,6 +1,40 @@
1
1
  {
2
2
  "name": "@dsh-stack/trading-backtest",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./lib/index.d.ts",
10
+ "default": "./lib/index.js"
11
+ }
12
+ },
13
+ "scripts": {
14
+ "build": "tsc -p tsconfig.json",
15
+ "typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
16
+ "verify": "node verify.mjs"
17
+ },
18
+ "dependencies": {
19
+ "@dsh-stack/trading-research": "0.1.0"
20
+ },
21
+ "devDependencies": {
22
+ "typescript": "^6.0.3"
23
+ },
24
+ "stack": {
25
+ "kind": "plugin",
26
+ "id": "stack.trading.backtest"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "files": [
32
+ "lib",
33
+ "README.md"
34
+ ],
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/marius-patrik/dsh-stack.git",
38
+ "directory": "plugins/trading-backtest"
39
+ }
40
+ }