quantex 0.4.0__tar.gz → 0.4.2__tar.gz
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.
- {quantex-0.4.0 → quantex-0.4.2}/PKG-INFO +79 -21
- {quantex-0.4.0 → quantex-0.4.2}/README.md +78 -20
- {quantex-0.4.0 → quantex-0.4.2}/pyproject.toml +1 -1
- {quantex-0.4.0 → quantex-0.4.2}/src/quantex/backtester/montecarlo.py +92 -30
- {quantex-0.4.0 → quantex-0.4.2}/LICENSE.md +0 -0
- {quantex-0.4.0 → quantex-0.4.2}/src/quantex/__init__.py +0 -0
- {quantex-0.4.0 → quantex-0.4.2}/src/quantex/backtester/__init__.py +0 -0
- {quantex-0.4.0 → quantex-0.4.2}/src/quantex/backtester/backtester.py +0 -0
- {quantex-0.4.0 → quantex-0.4.2}/src/quantex/backtester/constants.py +0 -0
- {quantex-0.4.0 → quantex-0.4.2}/src/quantex/backtester/data_splits.py +0 -0
- {quantex-0.4.0 → quantex-0.4.2}/src/quantex/backtester/metrics.py +0 -0
- {quantex-0.4.0 → quantex-0.4.2}/src/quantex/backtester/parallel.py +0 -0
- {quantex-0.4.0 → quantex-0.4.2}/src/quantex/backtester/reports.py +0 -0
- {quantex-0.4.0 → quantex-0.4.2}/src/quantex/broker/__init__.py +0 -0
- {quantex-0.4.0 → quantex-0.4.2}/src/quantex/broker/broker.py +0 -0
- {quantex-0.4.0 → quantex-0.4.2}/src/quantex/broker/types.py +0 -0
- {quantex-0.4.0 → quantex-0.4.2}/src/quantex/datasource.py +0 -0
- {quantex-0.4.0 → quantex-0.4.2}/src/quantex/helpers.py +0 -0
- {quantex-0.4.0 → quantex-0.4.2}/src/quantex/indicators.py +0 -0
- {quantex-0.4.0 → quantex-0.4.2}/src/quantex/strategy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: quantex
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: A simple quant strategy creation and backtesting package.
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Daniel Green
|
|
@@ -26,9 +26,9 @@ It gives you a small set of building blocks:
|
|
|
26
26
|
|
|
27
27
|
- [`Strategy`](src/quantex/strategy.py:9) for your trading rules
|
|
28
28
|
- [`DataSource`](src/quantex/datasource.py:6) plus [`CSVDataSource`](src/quantex/datasource.py:194) and [`ParquetDataSource`](src/quantex/datasource.py:228) for OHLCV market data
|
|
29
|
-
- [`Broker`](src/quantex/broker.py:113) objects, created automatically per symbol, for order placement and position state
|
|
30
|
-
- [`SimpleBacktester`](src/quantex/backtester.py:
|
|
31
|
-
- [`BacktestReport`](src/quantex/backtester.py:
|
|
29
|
+
- [`Broker`](src/quantex/broker/broker.py:113) objects, created automatically per symbol, for order placement and position state
|
|
30
|
+
- [`SimpleBacktester`](src/quantex/backtester/backtester.py:26) for simulation and parameter search
|
|
31
|
+
- [`BacktestReport`](src/quantex/backtester/reports.py:4) for results, equity history, and summary statistics
|
|
32
32
|
|
|
33
33
|
The project is intentionally small. It does not try to be a full research platform, portfolio database, or live-trading engine. Instead, it focuses on a straightforward workflow: load historical bars, define strategy logic, simulate orders, and inspect the results.
|
|
34
34
|
|
|
@@ -48,8 +48,8 @@ At runtime, a typical Quantex workflow looks like this:
|
|
|
48
48
|
2. In [`Strategy.init()`](src/quantex/strategy.py:52), attach one or more data sources with [`Strategy.add_data()`](src/quantex/strategy.py:98).
|
|
49
49
|
3. Still in [`Strategy.init()`](src/quantex/strategy.py:52), build indicator arrays with the built-in indicator catalog on `self.ta` or the package-level [`indicators`](src/quantex/indicators.py), then register them with [`Strategy.Indicator()`](src/quantex/strategy.py:126).
|
|
50
50
|
4. In [`Strategy.next()`](src/quantex/strategy.py:71), read the current bar through properties such as [`DataSource.COpen`](src/quantex/datasource.py:145) and [`DataSource.CClose`](src/quantex/datasource.py:175), then place orders through the broker stored in [`Strategy.positions`](src/quantex/strategy.py:47).
|
|
51
|
-
5. Run the strategy with [`SimpleBacktester.run()`](src/quantex/backtester.py:414).
|
|
52
|
-
6. Inspect the returned [`BacktestReport`](src/quantex/backtester.py:
|
|
51
|
+
5. Run the strategy with [`SimpleBacktester.run()`](src/quantex/backtester/backtester.py:414).
|
|
52
|
+
6. Inspect the returned [`BacktestReport`](src/quantex/backtester/reports.py:4), including [`BacktestReport.total_return`](src/quantex/backtester/reports.py:68), [`BacktestReport.periods_per_year`](src/quantex/backtester/reports.py:58), [`BacktestReport.plot()`](src/quantex/backtester/reports.py:87), and the printable summary from [`BacktestReport.__str__()`](src/quantex/backtester/reports.py:101).
|
|
53
53
|
|
|
54
54
|
## Usage
|
|
55
55
|
|
|
@@ -125,22 +125,22 @@ report.plot()
|
|
|
125
125
|
|
|
126
126
|
### 4. Understand what happens during the backtest
|
|
127
127
|
|
|
128
|
-
When you call [`SimpleBacktester.run()`](src/quantex/backtester.py:414):
|
|
128
|
+
When you call [`SimpleBacktester.run()`](src/quantex/backtester/backtester.py:414):
|
|
129
129
|
|
|
130
|
-
- the backtester deep-copies your strategy in [`SimpleBacktester.__init__()`](src/quantex/backtester.py:
|
|
131
|
-
- starting cash is split evenly across all attached symbols in [`SimpleBacktester.run()`](src/quantex/backtester.py:439)
|
|
130
|
+
- the backtester deep-copies your strategy in [`SimpleBacktester.__init__()`](src/quantex/backtester/backtester.py:26)
|
|
131
|
+
- starting cash is split evenly across all attached symbols in [`SimpleBacktester.run()`](src/quantex/backtester/backtester.py:439)
|
|
132
132
|
- each data source advances one bar at a time by updating [`DataSource.current_index`](src/quantex/datasource.py:62)
|
|
133
|
-
- each symbol's broker processes pending orders through [`Broker._iterate()`](src/quantex/broker.py:483)
|
|
133
|
+
- each symbol's broker processes pending orders through [`Broker._iterate()`](src/quantex/broker/broker.py:483)
|
|
134
134
|
- market orders execute at the current bar's open price through [`DataSource.COpen`](src/quantex/datasource.py:145)
|
|
135
|
-
- equity is tracked into the final [`BacktestReport.PnlRecord`](src/quantex/backtester.py:
|
|
135
|
+
- equity is tracked into the final [`BacktestReport.PnlRecord`](src/quantex/backtester/reports.py:52)
|
|
136
136
|
|
|
137
137
|
### 5. Understand order sizing
|
|
138
138
|
|
|
139
139
|
Order sizing in Quantex is simple but important:
|
|
140
140
|
|
|
141
|
-
- [`Broker.buy()`](src/quantex/broker.py:159) treats `quantity` as a fraction of available cash unless you pass `amount`
|
|
142
|
-
- [`Broker.sell()`](src/quantex/broker.py:235) uses the same sizing calculation and can open or increase a short position
|
|
143
|
-
- [`Broker.close()`](src/quantex/broker.py:307) places a market order that offsets the current position
|
|
141
|
+
- [`Broker.buy()`](src/quantex/broker/broker.py:159) treats `quantity` as a fraction of available cash unless you pass `amount`
|
|
142
|
+
- [`Broker.sell()`](src/quantex/broker/broker.py:235) uses the same sizing calculation and can open or increase a short position
|
|
143
|
+
- [`Broker.close()`](src/quantex/broker/broker.py:307) places a market order that offsets the current position
|
|
144
144
|
|
|
145
145
|
Because of this design, `buy(0.5)` means “use roughly half of the broker cash for this symbol”, not “buy half a share”.
|
|
146
146
|
|
|
@@ -194,20 +194,20 @@ class MacdTrendStrategy(Strategy):
|
|
|
194
194
|
|
|
195
195
|
### Broker and orders
|
|
196
196
|
|
|
197
|
-
Each call to [`Strategy.add_data()`](src/quantex/strategy.py:98) also creates a [`Broker`](src/quantex/broker.py:113) for that symbol.
|
|
197
|
+
Each call to [`Strategy.add_data()`](src/quantex/strategy.py:98) also creates a [`Broker`](src/quantex/broker/broker.py:113) for that symbol.
|
|
198
198
|
|
|
199
199
|
Supported order behavior in the current codebase:
|
|
200
200
|
|
|
201
|
-
- market orders and limit orders via [`OrderType`](src/quantex/broker.py:
|
|
202
|
-
- pending, active, and complete order states via [`OrderStatus`](src/quantex/broker.py:
|
|
203
|
-
- optional stop-loss and take-profit triggers stored on [`Order`](src/quantex/broker.py:52)
|
|
204
|
-
- percentage or cash commissions via [`CommissionType`](src/quantex/
|
|
201
|
+
- market orders and limit orders via [`OrderType`](src/quantex/broker/types.py:4)
|
|
202
|
+
- pending, active, and complete order states via [`OrderStatus`](src/quantex/broker/types.py:16)
|
|
203
|
+
- optional stop-loss and take-profit triggers stored on [`Order`](src/quantex/broker/broker.py:52)
|
|
204
|
+
- percentage or cash commissions via [`CommissionType`](src/quantex/broker/types.py:28)
|
|
205
205
|
|
|
206
206
|
## Optimization
|
|
207
207
|
|
|
208
|
-
[`SimpleBacktester.optimize()`](src/quantex/backtester.py:485) runs a grid search over every parameter combination you provide.
|
|
208
|
+
[`SimpleBacktester.optimize()`](src/quantex/backtester/backtester.py:485) runs a grid search over every parameter combination you provide.
|
|
209
209
|
|
|
210
|
-
[`SimpleBacktester.optimize_parallel()`](src/quantex/backtester.py:659) does the same work in multiple processes, then re-runs the best parameter set locally to produce a full [`BacktestReport`](src/quantex/backtester.py:
|
|
210
|
+
[`SimpleBacktester.optimize_parallel()`](src/quantex/backtester/backtester.py:659) does the same work in multiple processes, then re-runs the best parameter set locally to produce a full [`BacktestReport`](src/quantex/backtester/reports.py:4).
|
|
211
211
|
|
|
212
212
|
Minimal example:
|
|
213
213
|
|
|
@@ -227,6 +227,64 @@ print(best_report)
|
|
|
227
227
|
print(results.head())
|
|
228
228
|
```
|
|
229
229
|
|
|
230
|
+
### Train/Validate/Test Optimization
|
|
231
|
+
|
|
232
|
+
[`SimpleBacktester.optimize_with_split()`](src/quantex/backtester/backtester.py:564) performs grid search with ML-style train/validate/test data splits to help detect overfitting.
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
result = backtester.optimize_with_split(
|
|
236
|
+
{"fast_period": [5, 10, 15], "slow_period": [20, 30, 50]},
|
|
237
|
+
train_ratio=0.6,
|
|
238
|
+
validate_ratio=0.2,
|
|
239
|
+
test_ratio=0.2,
|
|
240
|
+
selection_criterion="validate", # Select best params based on validate performance
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
print(f"Best params: {result.best_params}")
|
|
244
|
+
print(f"Train Sharpe: {result.train_metrics['sharpe']}")
|
|
245
|
+
print(f"Validate Sharpe: {result.validate_metrics['sharpe']}")
|
|
246
|
+
print(f"Test Sharpe: {result.test_metrics['sharpe']}")
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Gradient Descent Optimization
|
|
250
|
+
|
|
251
|
+
[`SimpleBacktester.optimize_gradient_descent()`](src/quantex/backtester/backtester.py:852) uses gradient descent for continuous parameter optimization, supporting momentum, learning rate schedules, and integer parameter handling.
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
result = backtester.optimize_gradient_descent(
|
|
255
|
+
param_init={"fast_period": 10.0, "slow_period": 30.0},
|
|
256
|
+
param_bounds={
|
|
257
|
+
"fast_period": (2.0, 50.0),
|
|
258
|
+
"slow_period": (10.0, 100.0)
|
|
259
|
+
},
|
|
260
|
+
learning_rate=0.01,
|
|
261
|
+
iterations=100,
|
|
262
|
+
momentum=0.9,
|
|
263
|
+
integer_params={"fast_period", "slow_period"},
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
print(f"Optimized params: {result.best_params}")
|
|
267
|
+
print(result.train_report)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Monte Carlo Simulation
|
|
271
|
+
|
|
272
|
+
[`SimpleBacktester.monte_carlo()`](src/quantex/backtester/backtester.py:1183) runs Monte Carlo simulations to test strategy robustness through two modes:
|
|
273
|
+
|
|
274
|
+
- **Trade Order Randomization** ([`MonteCarloMode.TRADE_ORDER`](src/quantex/backtester/montecarlo.py:25)): Shuffles the sequence of trade execution while keeping the same trades
|
|
275
|
+
- **Price Path Resampling** ([`MonteCarloMode.PRICE_PATH`](src/quantex/backtester/montecarlo.py:27)): Creates synthetic market scenarios from historical returns
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
result = backtester.monte_carlo(
|
|
279
|
+
simulations=500,
|
|
280
|
+
mode="both", # Run both trade order and price path simulations
|
|
281
|
+
seed=42,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
print(result) # Print summary statistics (percentiles, confidence intervals)
|
|
285
|
+
result.plot() # Show spaghetti plot of equity curves
|
|
286
|
+
```
|
|
287
|
+
|
|
230
288
|
## Documentation
|
|
231
289
|
|
|
232
290
|
Project documentation is built with MkDocs. The main docs entry point is [`docs/index.md`](docs/index.md), and usage guides live under [`docs/usage/`](docs/usage/).
|
|
@@ -6,9 +6,9 @@ It gives you a small set of building blocks:
|
|
|
6
6
|
|
|
7
7
|
- [`Strategy`](src/quantex/strategy.py:9) for your trading rules
|
|
8
8
|
- [`DataSource`](src/quantex/datasource.py:6) plus [`CSVDataSource`](src/quantex/datasource.py:194) and [`ParquetDataSource`](src/quantex/datasource.py:228) for OHLCV market data
|
|
9
|
-
- [`Broker`](src/quantex/broker.py:113) objects, created automatically per symbol, for order placement and position state
|
|
10
|
-
- [`SimpleBacktester`](src/quantex/backtester.py:
|
|
11
|
-
- [`BacktestReport`](src/quantex/backtester.py:
|
|
9
|
+
- [`Broker`](src/quantex/broker/broker.py:113) objects, created automatically per symbol, for order placement and position state
|
|
10
|
+
- [`SimpleBacktester`](src/quantex/backtester/backtester.py:26) for simulation and parameter search
|
|
11
|
+
- [`BacktestReport`](src/quantex/backtester/reports.py:4) for results, equity history, and summary statistics
|
|
12
12
|
|
|
13
13
|
The project is intentionally small. It does not try to be a full research platform, portfolio database, or live-trading engine. Instead, it focuses on a straightforward workflow: load historical bars, define strategy logic, simulate orders, and inspect the results.
|
|
14
14
|
|
|
@@ -28,8 +28,8 @@ At runtime, a typical Quantex workflow looks like this:
|
|
|
28
28
|
2. In [`Strategy.init()`](src/quantex/strategy.py:52), attach one or more data sources with [`Strategy.add_data()`](src/quantex/strategy.py:98).
|
|
29
29
|
3. Still in [`Strategy.init()`](src/quantex/strategy.py:52), build indicator arrays with the built-in indicator catalog on `self.ta` or the package-level [`indicators`](src/quantex/indicators.py), then register them with [`Strategy.Indicator()`](src/quantex/strategy.py:126).
|
|
30
30
|
4. In [`Strategy.next()`](src/quantex/strategy.py:71), read the current bar through properties such as [`DataSource.COpen`](src/quantex/datasource.py:145) and [`DataSource.CClose`](src/quantex/datasource.py:175), then place orders through the broker stored in [`Strategy.positions`](src/quantex/strategy.py:47).
|
|
31
|
-
5. Run the strategy with [`SimpleBacktester.run()`](src/quantex/backtester.py:414).
|
|
32
|
-
6. Inspect the returned [`BacktestReport`](src/quantex/backtester.py:
|
|
31
|
+
5. Run the strategy with [`SimpleBacktester.run()`](src/quantex/backtester/backtester.py:414).
|
|
32
|
+
6. Inspect the returned [`BacktestReport`](src/quantex/backtester/reports.py:4), including [`BacktestReport.total_return`](src/quantex/backtester/reports.py:68), [`BacktestReport.periods_per_year`](src/quantex/backtester/reports.py:58), [`BacktestReport.plot()`](src/quantex/backtester/reports.py:87), and the printable summary from [`BacktestReport.__str__()`](src/quantex/backtester/reports.py:101).
|
|
33
33
|
|
|
34
34
|
## Usage
|
|
35
35
|
|
|
@@ -105,22 +105,22 @@ report.plot()
|
|
|
105
105
|
|
|
106
106
|
### 4. Understand what happens during the backtest
|
|
107
107
|
|
|
108
|
-
When you call [`SimpleBacktester.run()`](src/quantex/backtester.py:414):
|
|
108
|
+
When you call [`SimpleBacktester.run()`](src/quantex/backtester/backtester.py:414):
|
|
109
109
|
|
|
110
|
-
- the backtester deep-copies your strategy in [`SimpleBacktester.__init__()`](src/quantex/backtester.py:
|
|
111
|
-
- starting cash is split evenly across all attached symbols in [`SimpleBacktester.run()`](src/quantex/backtester.py:439)
|
|
110
|
+
- the backtester deep-copies your strategy in [`SimpleBacktester.__init__()`](src/quantex/backtester/backtester.py:26)
|
|
111
|
+
- starting cash is split evenly across all attached symbols in [`SimpleBacktester.run()`](src/quantex/backtester/backtester.py:439)
|
|
112
112
|
- each data source advances one bar at a time by updating [`DataSource.current_index`](src/quantex/datasource.py:62)
|
|
113
|
-
- each symbol's broker processes pending orders through [`Broker._iterate()`](src/quantex/broker.py:483)
|
|
113
|
+
- each symbol's broker processes pending orders through [`Broker._iterate()`](src/quantex/broker/broker.py:483)
|
|
114
114
|
- market orders execute at the current bar's open price through [`DataSource.COpen`](src/quantex/datasource.py:145)
|
|
115
|
-
- equity is tracked into the final [`BacktestReport.PnlRecord`](src/quantex/backtester.py:
|
|
115
|
+
- equity is tracked into the final [`BacktestReport.PnlRecord`](src/quantex/backtester/reports.py:52)
|
|
116
116
|
|
|
117
117
|
### 5. Understand order sizing
|
|
118
118
|
|
|
119
119
|
Order sizing in Quantex is simple but important:
|
|
120
120
|
|
|
121
|
-
- [`Broker.buy()`](src/quantex/broker.py:159) treats `quantity` as a fraction of available cash unless you pass `amount`
|
|
122
|
-
- [`Broker.sell()`](src/quantex/broker.py:235) uses the same sizing calculation and can open or increase a short position
|
|
123
|
-
- [`Broker.close()`](src/quantex/broker.py:307) places a market order that offsets the current position
|
|
121
|
+
- [`Broker.buy()`](src/quantex/broker/broker.py:159) treats `quantity` as a fraction of available cash unless you pass `amount`
|
|
122
|
+
- [`Broker.sell()`](src/quantex/broker/broker.py:235) uses the same sizing calculation and can open or increase a short position
|
|
123
|
+
- [`Broker.close()`](src/quantex/broker/broker.py:307) places a market order that offsets the current position
|
|
124
124
|
|
|
125
125
|
Because of this design, `buy(0.5)` means “use roughly half of the broker cash for this symbol”, not “buy half a share”.
|
|
126
126
|
|
|
@@ -174,20 +174,20 @@ class MacdTrendStrategy(Strategy):
|
|
|
174
174
|
|
|
175
175
|
### Broker and orders
|
|
176
176
|
|
|
177
|
-
Each call to [`Strategy.add_data()`](src/quantex/strategy.py:98) also creates a [`Broker`](src/quantex/broker.py:113) for that symbol.
|
|
177
|
+
Each call to [`Strategy.add_data()`](src/quantex/strategy.py:98) also creates a [`Broker`](src/quantex/broker/broker.py:113) for that symbol.
|
|
178
178
|
|
|
179
179
|
Supported order behavior in the current codebase:
|
|
180
180
|
|
|
181
|
-
- market orders and limit orders via [`OrderType`](src/quantex/broker.py:
|
|
182
|
-
- pending, active, and complete order states via [`OrderStatus`](src/quantex/broker.py:
|
|
183
|
-
- optional stop-loss and take-profit triggers stored on [`Order`](src/quantex/broker.py:52)
|
|
184
|
-
- percentage or cash commissions via [`CommissionType`](src/quantex/
|
|
181
|
+
- market orders and limit orders via [`OrderType`](src/quantex/broker/types.py:4)
|
|
182
|
+
- pending, active, and complete order states via [`OrderStatus`](src/quantex/broker/types.py:16)
|
|
183
|
+
- optional stop-loss and take-profit triggers stored on [`Order`](src/quantex/broker/broker.py:52)
|
|
184
|
+
- percentage or cash commissions via [`CommissionType`](src/quantex/broker/types.py:28)
|
|
185
185
|
|
|
186
186
|
## Optimization
|
|
187
187
|
|
|
188
|
-
[`SimpleBacktester.optimize()`](src/quantex/backtester.py:485) runs a grid search over every parameter combination you provide.
|
|
188
|
+
[`SimpleBacktester.optimize()`](src/quantex/backtester/backtester.py:485) runs a grid search over every parameter combination you provide.
|
|
189
189
|
|
|
190
|
-
[`SimpleBacktester.optimize_parallel()`](src/quantex/backtester.py:659) does the same work in multiple processes, then re-runs the best parameter set locally to produce a full [`BacktestReport`](src/quantex/backtester.py:
|
|
190
|
+
[`SimpleBacktester.optimize_parallel()`](src/quantex/backtester/backtester.py:659) does the same work in multiple processes, then re-runs the best parameter set locally to produce a full [`BacktestReport`](src/quantex/backtester/reports.py:4).
|
|
191
191
|
|
|
192
192
|
Minimal example:
|
|
193
193
|
|
|
@@ -207,6 +207,64 @@ print(best_report)
|
|
|
207
207
|
print(results.head())
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
+
### Train/Validate/Test Optimization
|
|
211
|
+
|
|
212
|
+
[`SimpleBacktester.optimize_with_split()`](src/quantex/backtester/backtester.py:564) performs grid search with ML-style train/validate/test data splits to help detect overfitting.
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
result = backtester.optimize_with_split(
|
|
216
|
+
{"fast_period": [5, 10, 15], "slow_period": [20, 30, 50]},
|
|
217
|
+
train_ratio=0.6,
|
|
218
|
+
validate_ratio=0.2,
|
|
219
|
+
test_ratio=0.2,
|
|
220
|
+
selection_criterion="validate", # Select best params based on validate performance
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
print(f"Best params: {result.best_params}")
|
|
224
|
+
print(f"Train Sharpe: {result.train_metrics['sharpe']}")
|
|
225
|
+
print(f"Validate Sharpe: {result.validate_metrics['sharpe']}")
|
|
226
|
+
print(f"Test Sharpe: {result.test_metrics['sharpe']}")
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Gradient Descent Optimization
|
|
230
|
+
|
|
231
|
+
[`SimpleBacktester.optimize_gradient_descent()`](src/quantex/backtester/backtester.py:852) uses gradient descent for continuous parameter optimization, supporting momentum, learning rate schedules, and integer parameter handling.
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
result = backtester.optimize_gradient_descent(
|
|
235
|
+
param_init={"fast_period": 10.0, "slow_period": 30.0},
|
|
236
|
+
param_bounds={
|
|
237
|
+
"fast_period": (2.0, 50.0),
|
|
238
|
+
"slow_period": (10.0, 100.0)
|
|
239
|
+
},
|
|
240
|
+
learning_rate=0.01,
|
|
241
|
+
iterations=100,
|
|
242
|
+
momentum=0.9,
|
|
243
|
+
integer_params={"fast_period", "slow_period"},
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
print(f"Optimized params: {result.best_params}")
|
|
247
|
+
print(result.train_report)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Monte Carlo Simulation
|
|
251
|
+
|
|
252
|
+
[`SimpleBacktester.monte_carlo()`](src/quantex/backtester/backtester.py:1183) runs Monte Carlo simulations to test strategy robustness through two modes:
|
|
253
|
+
|
|
254
|
+
- **Trade Order Randomization** ([`MonteCarloMode.TRADE_ORDER`](src/quantex/backtester/montecarlo.py:25)): Shuffles the sequence of trade execution while keeping the same trades
|
|
255
|
+
- **Price Path Resampling** ([`MonteCarloMode.PRICE_PATH`](src/quantex/backtester/montecarlo.py:27)): Creates synthetic market scenarios from historical returns
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
result = backtester.monte_carlo(
|
|
259
|
+
simulations=500,
|
|
260
|
+
mode="both", # Run both trade order and price path simulations
|
|
261
|
+
seed=42,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
print(result) # Print summary statistics (percentiles, confidence intervals)
|
|
265
|
+
result.plot() # Show spaghetti plot of equity curves
|
|
266
|
+
```
|
|
267
|
+
|
|
210
268
|
## Documentation
|
|
211
269
|
|
|
212
270
|
Project documentation is built with MkDocs. The main docs entry point is [`docs/index.md`](docs/index.md), and usage guides live under [`docs/usage/`](docs/usage/).
|
|
@@ -112,10 +112,23 @@ class MonteCarloResult:
|
|
|
112
112
|
|
|
113
113
|
fig, ax = plt.subplots(figsize=figsize)
|
|
114
114
|
|
|
115
|
+
# Plot using a numeric simulation step axis to avoid date conversion
|
|
116
|
+
# artifacts when equity curves share the same time index.
|
|
117
|
+
if not self.equity_curves:
|
|
118
|
+
ax.set_xlabel("Step")
|
|
119
|
+
ax.set_ylabel("Portfolio Value")
|
|
120
|
+
ax.set_title(f"Monte Carlo Simulation Results ({self.simulations} simulations)")
|
|
121
|
+
ax.grid(alpha=0.3)
|
|
122
|
+
plt.tight_layout()
|
|
123
|
+
plt.show()
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
step_index = np.arange(len(self.equity_curves[0]), dtype=np.float64)
|
|
127
|
+
|
|
115
128
|
# Plot all simulation curves with low alpha (transparency)
|
|
116
129
|
# This makes the average path appear lightest due to overlap
|
|
117
130
|
for curve in self.equity_curves:
|
|
118
|
-
x_vals = np.
|
|
131
|
+
x_vals = np.arange(len(curve), dtype=np.float64)
|
|
119
132
|
y_vals = np.asarray(curve.values, dtype=np.float64)
|
|
120
133
|
ax.plot(x_vals, y_vals, color="steelblue", alpha=0.1, linewidth=0.5)
|
|
121
134
|
|
|
@@ -127,18 +140,18 @@ class MonteCarloResult:
|
|
|
127
140
|
median_curve = aligned.median(axis=1)
|
|
128
141
|
|
|
129
142
|
# Plot mean curve (thicker, lighter)
|
|
130
|
-
x_mean = np.
|
|
143
|
+
x_mean = np.arange(len(mean_curve), dtype=np.float64)
|
|
131
144
|
y_mean = np.asarray(mean_curve.values, dtype=np.float64)
|
|
132
145
|
ax.plot(x_mean, y_mean, color="darkblue", alpha=0.8, linewidth=2, label="Mean")
|
|
133
146
|
|
|
134
147
|
# Plot median curve
|
|
135
|
-
x_med = np.
|
|
148
|
+
x_med = np.arange(len(median_curve), dtype=np.float64)
|
|
136
149
|
y_med = np.asarray(median_curve.values, dtype=np.float64)
|
|
137
150
|
ax.plot(x_med, y_med, color="navy", alpha=0.6, linewidth=1.5, linestyle="--", label="Median")
|
|
138
151
|
|
|
139
152
|
# Show original equity curve if requested
|
|
140
153
|
if show_original and self.original_equity is not None:
|
|
141
|
-
x_orig = np.
|
|
154
|
+
x_orig = np.arange(len(self.original_equity), dtype=np.float64)
|
|
142
155
|
y_orig = np.asarray(self.original_equity.values, dtype=np.float64)
|
|
143
156
|
ax.plot(x_orig, y_orig, color="red", alpha=0.9, linewidth=2, label="Original Backtest")
|
|
144
157
|
|
|
@@ -147,16 +160,19 @@ class MonteCarloResult:
|
|
|
147
160
|
aligned = pd.concat(self.equity_curves, axis=1)
|
|
148
161
|
p5 = aligned.quantile(0.05, axis=1)
|
|
149
162
|
p95 = aligned.quantile(0.95, axis=1)
|
|
150
|
-
x_p5 = np.
|
|
163
|
+
x_p5 = np.arange(len(p5), dtype=np.float64)
|
|
151
164
|
y_p5 = np.asarray(p5.values, dtype=np.float64)
|
|
152
165
|
y_p95 = np.asarray(p95.values, dtype=np.float64)
|
|
153
166
|
ax.fill_between(x_p5, y_p5, y_p95, alpha=0.2, color="steelblue", label="5th-95th Percentile")
|
|
154
167
|
|
|
155
|
-
ax.set_xlabel("
|
|
168
|
+
ax.set_xlabel("Step")
|
|
156
169
|
ax.set_ylabel("Portfolio Value")
|
|
157
170
|
ax.set_title(f"Monte Carlo Simulation Results ({self.simulations} simulations)")
|
|
158
171
|
ax.legend(loc="best")
|
|
159
172
|
ax.grid(alpha=0.3)
|
|
173
|
+
|
|
174
|
+
# Match the more compact spaghetti-plot look by tightening x-limits.
|
|
175
|
+
ax.set_xlim(step_index[0], step_index[-1])
|
|
160
176
|
|
|
161
177
|
plt.tight_layout()
|
|
162
178
|
plt.show()
|
|
@@ -282,32 +298,78 @@ def _run_price_path_simulation(
|
|
|
282
298
|
synthetic_sources = {}
|
|
283
299
|
|
|
284
300
|
for symbol, source in data_sources.items():
|
|
285
|
-
close_prices = source.data[
|
|
286
|
-
|
|
287
|
-
|
|
301
|
+
close_prices = np.asarray(source.data["Close"].values, dtype=np.float64)
|
|
302
|
+
if len(close_prices) < 2:
|
|
303
|
+
synthetic_sources[symbol] = DataSource(source.data.copy())
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
# Use a block-bootstrap on log returns to preserve local serial dependence
|
|
307
|
+
# and then re-price the path using a geometric Brownian motion style
|
|
308
|
+
# reconstruction with the sampled return distribution.
|
|
288
309
|
log_returns = np.diff(np.log(close_prices))
|
|
289
|
-
|
|
290
|
-
# Resample with replacement
|
|
291
310
|
n_samples = len(log_returns)
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
#
|
|
306
|
-
#
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
+
block_size = max(2, min(10, int(np.sqrt(n_samples))))
|
|
312
|
+
synthetic_log_returns = []
|
|
313
|
+
|
|
314
|
+
while len(synthetic_log_returns) < n_samples:
|
|
315
|
+
start = int(np.random.randint(0, n_samples))
|
|
316
|
+
block = log_returns[start : start + block_size]
|
|
317
|
+
if len(block) < block_size:
|
|
318
|
+
wrap = block_size - len(block)
|
|
319
|
+
block = np.concatenate((block, log_returns[:wrap]))
|
|
320
|
+
synthetic_log_returns.extend(block.tolist())
|
|
321
|
+
|
|
322
|
+
synthetic_log_returns = np.asarray(synthetic_log_returns[:n_samples], dtype=np.float64)
|
|
323
|
+
|
|
324
|
+
# Keep the simulated path realistic by matching the original return
|
|
325
|
+
# center and volatility rather than letting the bootstrap drift too far.
|
|
326
|
+
original_mean = float(np.mean(log_returns))
|
|
327
|
+
original_std = float(np.std(log_returns))
|
|
328
|
+
synthetic_mean = float(np.mean(synthetic_log_returns))
|
|
329
|
+
synthetic_std = float(np.std(synthetic_log_returns))
|
|
330
|
+
if synthetic_std > 0 and original_std > 0:
|
|
331
|
+
synthetic_log_returns = (synthetic_log_returns - synthetic_mean) * (original_std / synthetic_std) + original_mean
|
|
332
|
+
else:
|
|
333
|
+
synthetic_log_returns = synthetic_log_returns - synthetic_mean + original_mean
|
|
334
|
+
|
|
335
|
+
synthetic_close = np.empty(n_samples + 1, dtype=np.float64)
|
|
336
|
+
synthetic_close[0] = close_prices[0]
|
|
337
|
+
synthetic_close[1:] = synthetic_close[0] * np.exp(np.cumsum(synthetic_log_returns))
|
|
338
|
+
synthetic_close = np.maximum(synthetic_close, np.finfo(np.float64).tiny)
|
|
339
|
+
|
|
340
|
+
# Derive intraday range from the historical candle shape so OHLC remains coherent.
|
|
341
|
+
source_df = source.data.copy()
|
|
342
|
+
if "Open" in source_df.columns:
|
|
343
|
+
open_close_gap = np.log(np.asarray(source_df["Open"].values, dtype=np.float64) / close_prices)
|
|
344
|
+
open_close_gap = np.nan_to_num(open_close_gap, nan=0.0, posinf=0.0, neginf=0.0)
|
|
345
|
+
else:
|
|
346
|
+
open_close_gap = np.zeros_like(synthetic_close)
|
|
347
|
+
|
|
348
|
+
open_noise = np.random.choice(open_close_gap, size=n_samples + 1, replace=True)
|
|
349
|
+
synthetic_open = synthetic_close * np.exp(open_noise)
|
|
350
|
+
|
|
351
|
+
if {"High", "Low"}.issubset(source_df.columns):
|
|
352
|
+
high_wick = np.log(np.asarray(source_df["High"].values, dtype=np.float64) / np.maximum(close_prices, np.finfo(np.float64).tiny))
|
|
353
|
+
low_wick = np.log(np.asarray(source_df["Low"].values, dtype=np.float64) / np.maximum(close_prices, np.finfo(np.float64).tiny))
|
|
354
|
+
high_wick = np.nan_to_num(high_wick, nan=0.0, posinf=0.0, neginf=0.0)
|
|
355
|
+
low_wick = np.nan_to_num(low_wick, nan=0.0, posinf=0.0, neginf=0.0)
|
|
356
|
+
synthetic_high = np.maximum(synthetic_open, synthetic_close) * np.exp(np.abs(np.random.choice(high_wick, size=n_samples + 1, replace=True)))
|
|
357
|
+
synthetic_low = np.minimum(synthetic_open, synthetic_close) * np.exp(-np.abs(np.random.choice(low_wick, size=n_samples + 1, replace=True)))
|
|
358
|
+
else:
|
|
359
|
+
synthetic_high = np.maximum(synthetic_open, synthetic_close)
|
|
360
|
+
synthetic_low = np.minimum(synthetic_open, synthetic_close)
|
|
361
|
+
|
|
362
|
+
synthetic_df = source_df
|
|
363
|
+
synthetic_df["Close"] = synthetic_close
|
|
364
|
+
synthetic_df["Open"] = synthetic_open
|
|
365
|
+
synthetic_df["High"] = np.maximum.reduce([synthetic_high, synthetic_open, synthetic_close])
|
|
366
|
+
synthetic_df["Low"] = np.minimum.reduce([synthetic_low, synthetic_open, synthetic_close])
|
|
367
|
+
|
|
368
|
+
if "Volume" in synthetic_df.columns:
|
|
369
|
+
volume = np.asarray(source_df["Volume"].values, dtype=np.float64)
|
|
370
|
+
if len(volume) == n_samples + 1:
|
|
371
|
+
synthetic_df["Volume"] = np.maximum(0.0, np.random.choice(volume, size=n_samples + 1, replace=True))
|
|
372
|
+
|
|
311
373
|
synthetic_sources[symbol] = DataSource(synthetic_df)
|
|
312
374
|
|
|
313
375
|
# Update strategy with synthetic data sources
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|