quantex 0.4.1__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.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "quantex"
3
- version = "0.4.1"
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"}
@@ -298,32 +298,78 @@ def _run_price_path_simulation(
298
298
  synthetic_sources = {}
299
299
 
300
300
  for symbol, source in data_sources.items():
301
- close_prices = source.data['Close'].values
302
-
303
- # 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.
304
309
  log_returns = np.diff(np.log(close_prices))
305
-
306
- # Resample with replacement
307
310
  n_samples = len(log_returns)
308
- resampled_indices = np.random.randint(0, n_samples, size=n_samples)
309
- resampled_returns = log_returns[resampled_indices]
310
-
311
- # Reconstruct price path starting from initial price
312
- synthetic_prices = np.zeros(n_samples + 1)
313
- synthetic_prices[0] = close_prices[0]
314
- synthetic_prices[1:] = close_prices[0] * np.exp(np.cumsum(resampled_returns))
315
-
316
- # Create synthetic OHLCV data
317
- # Use the same pattern but with resampled close prices
318
- synthetic_df = source.data.copy()
319
- synthetic_df['Close'] = synthetic_prices
320
-
321
- # Adjust Open, High, Low based on close (simple approximation)
322
- # This is a simplification - real implementation would need proper OHLC generation
323
- synthetic_df['Open'] = synthetic_prices * (1 + np.random.uniform(-0.001, 0.001, n_samples + 1))
324
- synthetic_df['High'] = np.maximum(synthetic_prices, synthetic_df['Open']) * (1 + np.random.uniform(0, 0.002, n_samples + 1))
325
- synthetic_df['Low'] = np.minimum(synthetic_prices, synthetic_df['Open']) * (1 - np.random.uniform(0, 0.002, n_samples + 1))
326
-
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
+
327
373
  synthetic_sources[symbol] = DataSource(synthetic_df)
328
374
 
329
375
  # Update strategy with synthetic data sources
File without changes
File without changes
File without changes
File without changes
File without changes