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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: quantex
3
- Version: 0.4.0
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:356) for simulation and parameter search
31
- - [`BacktestReport`](src/quantex/backtester.py:188) for results, equity history, and summary statistics
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:188), including [`BacktestReport.total_return`](src/quantex/backtester.py:227), [`BacktestReport.periods_per_year`](src/quantex/backtester.py:214), [`BacktestReport.plot()`](src/quantex/backtester.py:252), and the printable summary from [`BacktestReport.__str__()`](src/quantex/backtester.py:299).
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:380)
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:205)
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:24)
202
- - pending, active, and complete order states via [`OrderStatus`](src/quantex/broker.py:37)
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/enums.py:4)
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:188).
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:356) for simulation and parameter search
11
- - [`BacktestReport`](src/quantex/backtester.py:188) for results, equity history, and summary statistics
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:188), including [`BacktestReport.total_return`](src/quantex/backtester.py:227), [`BacktestReport.periods_per_year`](src/quantex/backtester.py:214), [`BacktestReport.plot()`](src/quantex/backtester.py:252), and the printable summary from [`BacktestReport.__str__()`](src/quantex/backtester.py:299).
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:380)
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:205)
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:24)
182
- - pending, active, and complete order states via [`OrderStatus`](src/quantex/broker.py:37)
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/enums.py:4)
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:188).
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/).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "quantex"
3
- version = "0.4.0"
3
+ version = "0.4.2"
4
4
  description = "A simple quant strategy creation and backtesting package."
5
5
  authors = [
6
6
  {name = "Daniel Green",email = "dangreen07@outlook.com"}
@@ -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.asarray(curve.index, dtype=np.float64)
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.asarray(mean_curve.index, dtype=np.float64)
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.asarray(median_curve.index, dtype=np.float64)
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.asarray(self.original_equity.index, dtype=np.float64)
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.asarray(p5.index, dtype=np.float64)
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("Date")
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['Close'].values
286
-
287
- # Calculate log returns
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
- resampled_indices = np.random.randint(0, n_samples, size=n_samples)
293
- resampled_returns = log_returns[resampled_indices]
294
-
295
- # Reconstruct price path starting from initial price
296
- synthetic_prices = np.zeros(n_samples + 1)
297
- synthetic_prices[0] = close_prices[0]
298
- synthetic_prices[1:] = close_prices[0] * np.exp(np.cumsum(resampled_returns))
299
-
300
- # Create synthetic OHLCV data
301
- # Use the same pattern but with resampled close prices
302
- synthetic_df = source.data.copy()
303
- synthetic_df['Close'] = synthetic_prices
304
-
305
- # Adjust Open, High, Low based on close (simple approximation)
306
- # This is a simplification - real implementation would need proper OHLC generation
307
- synthetic_df['Open'] = synthetic_prices * (1 + np.random.uniform(-0.001, 0.001, n_samples + 1))
308
- synthetic_df['High'] = np.maximum(synthetic_prices, synthetic_df['Open']) * (1 + np.random.uniform(0, 0.002, n_samples + 1))
309
- synthetic_df['Low'] = np.minimum(synthetic_prices, synthetic_df['Open']) * (1 - np.random.uniform(0, 0.002, n_samples + 1))
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