When to Use
Invoke this skill when a strategy's evidence base is one short price history and you need to know how much of the result is the strategy and how much is that particular path. It supplies three generators — a Geometric Brownian Motion diffusion baseline, a GARCH(1,1) path with volatility clustering, and a circular block bootstrap that resamples the empirical series while preserving short-range serial dependence — plus validate_synthetic_path, which compares the first four moments of a synthetic sample against its empirical baseline before anything downstream trusts it.
Use it to answer "would this strategy have survived a differently-ordered version of the same history, or a path with the same volatility dynamics but different draws?" The synthetic paths are inputs to a robustness argument, never evidence of performance.
When NOT to Use
- As a source of tail risk beyond the sample. The bootstrap re-orders observed returns; it cannot produce a bar worse than the worst bar in the input. GBM's log returns are IID normal by construction — no fat tails, no clustering — so a GBM path materially understates tail risk. For shocks outside the sample use
scenario-based-stress-testing-custom-shocksandstress-testing-against-historical-crash-scenarios. - To manufacture history you do not have. A 60-bar sample block-bootstrapped to 10,000 bars still contains 60 bars of information. The synthetic series will look statistically respectable and the confidence intervals derived from it will be far too narrow.
- As a substitute for out-of-sample testing. Every path here is generated from, or parameterized by, the same data the strategy was fitted on. It says nothing about overfitting — that is
walk-forward-validation-setupandfactor-research-multiple-testing-correction. - For multi-asset backtests, as-is. Each generator produces one univariate series. Running it per symbol yields independent paths and destroys the cross-sectional correlation that dominates portfolio risk. Correlated multi-asset simulation is out of scope; see
cross-asset-correlation-regime-shifts. - To fit GARCH parameters. This module simulates from parameters you supply; it performs no QMLE estimation. Estimate them with a dedicated package.
- To advertise performance. See the regulatory note in
references/standards.md: performance computed on synthetic paths is hypothetical performance under the SEC Marketing Rule and carries specific conditions for a registered adviser.
Prerequisites
- An empirical log-return series for bootstrapping or for the validation baseline — gap-free, finite, at one consistent bar frequency. Every series this module produces or consumes is a log return,
r_t = ln(P_t / P_{t-1}); mixing simple and log returns silently biases every reported moment. - For GBM:
muandsigmaper unit ofdt. At the defaultdt = 1/252they are annualized — passsigma=0.20for 20% annualized volatility, not the per-day 0.0126. - For GARCH:
omega,alpha,beta,muper bar, satisfyingalpha + beta < 1. - An explicit integer seed.
SyntheticDataGenerator(seed=None)runs from OS entropy and logs a warning: an augmentation run that cannot be reproduced cannot be audited. - A defensible volatility tolerance for the parity gate. The 0.35 default is a house heuristic, not a standard.
Workflow
-
Choose the generator against what you are actually testing:
generate_gbm(GBMConfig(mu, sigma, S0, dt, steps))— an IID-normal null. Use it to establish what a result looks like when there is no volatility structure, not as a realistic market.generate_garch(GARCHConfig(omega, alpha, beta, mu, S0, steps))— volatility clustering with Gaussian innovations.block_bootstrap_returns(historical_returns, steps, block_size)— empirical, non-parametric, keeps short-range dependence.- Decision point — do not reach for
bootstrap_returns(IID) to augment a real series. It samples individual returns independently, destroying volatility clustering and the loss runs that produce the deepest drawdowns. A drawdown measured on an IID resample of a dependent series is optimistically biased. It is provided only as a deliberate null for measuring how much dependence matters.
-
Set the block length from the series, not from the default:
- Decision point —
DEFAULT_BLOCK_SIZE = 5is a placeholder, not a standard. Too short and the resample behaves like an IID bootstrap, breaking up the dependence it exists to preserve; too long and the number of distinct blocks collapses and every path looks like the original. The bias/variance-optimal length depends on sample size and autocorrelation structure (Politis & White 2004, corrected 2009) — seereferences/standards.md. block_size=1degenerates to the IID bootstrap and logs a warning;block_size > len(series)raises.
- Decision point —
-
Keep GARCH parameters inside the stationary region:
- Decision point —
alpha + beta >= 1raises, it is not clamped. At or above 1 the process has no finite unconditional variance, so there is nothing for moment validation to compare against. Flooring the denominator at 0.001 instead fabricates an unconditional volatility for a process that has none. Nelson (1990) shows such a process can still be strictly stationary (IGARCH); that case is deliberately out of scope here rather than silently approximated. - The recursion starts at the stationary point (
sigma_0^2 = eps_0^2 = omega / (1 - alpha - beta)), so no burn-in period needs to be discarded.
- Decision point —
-
Validate before use, and read the verdict for exactly what it says:
validate_synthetic_path(historical_returns, synthetic_returns, vol_tolerance)returns aSyntheticValidationReportcarrying mean, per-bar volatility, skewness and Pearson kurtosis for both series.- Decision point —
is_statistically_consistentis a volatility-parity gate only. It tests|sigma_synth - sigma_hist| / sigma_hist <= vol_toleranceand nothing else. It deliberately does not gate on the mean (unestimable to useful precision over a backtest-length sample) or on skewness/kurtosis (sampling error at a few hundred observations would reject correct generators). Judge those from the reported numbers yourself. Passing is necessary, not sufficient. - Decision point —
Nonemoments mean "not measurable", never 0.0. Skewness and kurtosis areNonefor a constant series or fewer than four observations. A constant baseline raises: there is no scale against which to measure relative error. - Kurtosis is Pearson (raw) kurtosis — 3.0 for a normal sample. Excess kurtosis is that value minus 3.
-
Carry the parameters with the result: seed, generator, all parameters, block length, tolerance, and the source series identity. A synthetic-path result without them is not reproducible and not auditable.
Full procedure: see
references/workflows.md. Standards, sources, and the regulatory note: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- IID resampling of a dependent series: sampling returns independently destroys volatility clustering and serial correlation. The resulting drawdown distribution is optimistically biased, so the augmentation makes the strategy look safer than the original backtest.
- A non-circular block bootstrap sold as circular: if block starts are drawn from
[0, n - B]rather than wrapped modulon, observations near the start and end of the series appear far less often than interior ones — atn = 20, B = 5, roughly a quarter as often. Whichever end of the window holds the crash is then systematically under-sampled, and the resampled mean is biased. The wrap is what makes every observation equally likely. - Explosive GARCH parameters accepted silently:
alpha + beta >= 1gives a process with no finite unconditional variance. Clamping the denominator produces a plausible-looking number for a quantity that does not exist. - Additive epsilons in standardized moments: dividing by
sigma^4 + 1e-9is scale-dependent. At a daily return scale (sigma = 0.01,sigma^4 = 1e-8) the epsilon is 10% of the denominator and kurtosis reads 2.74 instead of 3.0; at an intraday scale it reads 0.003. Guard degenerate dispersion with a relative test, not an additive one. - Confusing simple and log returns: feeding simple returns to a validator comparing against log-return synthetics biases every moment, and nothing in the report can detect it.
- Confusing per-bar and annualized parameters:
GARCHConfig.omegais per-bar variance;GBMConfig.sigmais per unit ofdt. Passing an annualizedomegainflates simulated volatility by roughlysqrt(252). - Treating moment parity as sufficient: matching four moments says nothing about autocorrelation, tail index, or cross-asset dependence. A shuffled series matches every moment of the original exactly and has none of its dynamics.
- Bootstrapping a short sample to a long one: resampling 60 bars into 10,000 does not create information. Confidence intervals from such a series are far too narrow.
- Losing the seed: an unreproducible augmentation run cannot be re-audited, and no reviewer can distinguish it from a favourable draw that was kept.
- Presenting synthetic-path results as performance: they are hypothetical performance, not a track record — see
references/standards.md.
Verification
- Circularity: resample
np.arange(20)withblock_size=5,steps=20, over 3,000 draws and count occurrences per index. Every observation's relative weight must be ~1.0; the non-circular predecessor gave ~0.26 at both ends. Withblock_size == na single draw must reproduce the whole series as a rotation, adjacent modulon. - Moment correctness at every scale: validate a 200,000-sample Gaussian series against itself at
sigma= 0.20, 0.01, 0.001 and 0.0001. Kurtosis must be 3.0 ± 0.10 and skewness 0.0 ± 0.05 in all four cases. Confirm skewness and kurtosis are unchanged when the series is scaled by 1e-4. - GBM discretization: reconstruct the path from the raw normals of an identically seeded stream and confirm each step equals
S_{t-1} * exp((mu - sigma^2/2) dt + sigma sqrt(dt) Z_t). Withsigma = 0,paths[-1]must equalS0 * exp(mu * steps * dt). - GARCH recursion: unroll
sigma_t^2 = omega + alpha eps_{t-1}^2 + beta sigma_{t-1}^2by hand for six steps and match. Confirmsigmas[0] == sigmas[1] == sqrt(omega / (1 - alpha - beta))exactly — the stationary start — and thatreturnsequalsnp.diff(np.log(prices)). - Clustering is real: lag-1 autocorrelation of squared GARCH returns must exceed 0.10 over 100,000 bars, while the same statistic on a GBM path stays under 0.02.
- Rejections:
alpha + beta >= 1,omega <= 0, negativealpha/beta,S0 <= 0,steps < 1,block_sizeof 0 or greater than the series length, NaN/Inf in either series, a single-observation series, a constant historical baseline, and a negative or non-finitevol_tolerancemust each raiseValueError.block_size=0previously spun in an unbounded loop. - Boundary: with a baseline of population sd exactly 1.0, a synthetic series scaled by exactly 1.25 must pass
vol_tolerance=0.25(the gate is<=) and fail at1.25 + 1e-6. - Run
python -m unittest discover -s skills/synthetic-data-generation-for-backtest-augmentation/scriptsand confirm 100% pass rate (41 tests).
Related Skills
monte-carlo-strategy-robustness-testingwalk-forward-validation-setupscenario-based-stress-testing-custom-shocksstress-testing-against-historical-crash-scenariosmarket-data-simulator-for-offline-developmentbacktest-determinism-and-reproducibilitysurvivorship-bias-free-universe-constructionportfolio-stress-test-including-liquidity-crunch-scenarios