forexgen 2.1.0__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.
- forexgen-2.1.0/LICENSE +17 -0
- forexgen-2.1.0/PKG-INFO +303 -0
- forexgen-2.1.0/README.md +267 -0
- forexgen-2.1.0/forexgen/__init__.py +27 -0
- forexgen-2.1.0/forexgen/__main__.py +5 -0
- forexgen-2.1.0/forexgen/api.py +94 -0
- forexgen-2.1.0/forexgen/calibration.py +357 -0
- forexgen-2.1.0/forexgen/cli.py +108 -0
- forexgen-2.1.0/forexgen/costs.py +66 -0
- forexgen-2.1.0/forexgen/diagnostics.py +210 -0
- forexgen-2.1.0/forexgen/engine.py +313 -0
- forexgen-2.1.0/forexgen/events.py +269 -0
- forexgen-2.1.0/forexgen/export.py +81 -0
- forexgen-2.1.0/forexgen/factors.py +114 -0
- forexgen-2.1.0/forexgen/fixtures.py +174 -0
- forexgen-2.1.0/forexgen/marketcal.py +112 -0
- forexgen-2.1.0/forexgen/markets.py +76 -0
- forexgen-2.1.0/forexgen/pipeline.py +289 -0
- forexgen-2.1.0/forexgen/profile_data/EURUSD.json +135 -0
- forexgen-2.1.0/forexgen/profiles.py +286 -0
- forexgen-2.1.0/forexgen/regimes.py +115 -0
- forexgen-2.1.0/forexgen/report.py +135 -0
- forexgen-2.1.0/forexgen/scenarios.py +152 -0
- forexgen-2.1.0/forexgen/ticks.py +81 -0
- forexgen-2.1.0/forexgen/validation.py +100 -0
- forexgen-2.1.0/forexgen.egg-info/PKG-INFO +303 -0
- forexgen-2.1.0/forexgen.egg-info/SOURCES.txt +37 -0
- forexgen-2.1.0/forexgen.egg-info/dependency_links.txt +1 -0
- forexgen-2.1.0/forexgen.egg-info/entry_points.txt +2 -0
- forexgen-2.1.0/forexgen.egg-info/requires.txt +2 -0
- forexgen-2.1.0/forexgen.egg-info/top_level.txt +1 -0
- forexgen-2.1.0/pyproject.toml +34 -0
- forexgen-2.1.0/setup.cfg +4 -0
- forexgen-2.1.0/tests/test_calendar_repro.py +76 -0
- forexgen-2.1.0/tests/test_compat.py +109 -0
- forexgen-2.1.0/tests/test_events_scenarios.py +96 -0
- forexgen-2.1.0/tests/test_multi_cross.py +52 -0
- forexgen-2.1.0/tests/test_ticks_validation_profiles.py +140 -0
- forexgen-2.1.0/tests/test_v21_features.py +202 -0
forexgen-2.1.0/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Henry Nyoike
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
forexgen-2.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forexgen
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: Offline synthetic forex and gold data generator for indicator testing and strategy backtesting
|
|
5
|
+
Author: Henry Nyoike
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Henry Nyoike
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
23
|
+
|
|
24
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
25
|
+
Classifier: Intended Audience :: Developers
|
|
26
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
27
|
+
Classifier: Programming Language :: Python :: 3
|
|
28
|
+
Classifier: Topic :: Scientific/Engineering
|
|
29
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
30
|
+
Requires-Python: >=3.10
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
License-File: LICENSE
|
|
33
|
+
Requires-Dist: numpy>=1.26
|
|
34
|
+
Requires-Dist: pandas>=2.2
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# forexgen 2.1
|
|
38
|
+
|
|
39
|
+
Offline synthetic **forex and gold** data generator for **indicator testing and strategy backtesting**
|
|
40
|
+
(and, secondarily, producing training data). Pure Python, **numpy + pandas only**, no internet, no GUI.
|
|
41
|
+
|
|
42
|
+
* 11 markets: EURUSD GBPUSD USDJPY AUDUSD NZDUSD USDCHF USDCAD EURGBP EURJPY GBPJPY XAUUSD (+ your own).
|
|
43
|
+
* **Different data on every run by default**; the seed used is recorded so any run can be reproduced.
|
|
44
|
+
* Real market structure: volatility clustering, fat tails, DST-aware sessions, scheduled news (NFP, CPI,
|
|
45
|
+
FOMC, ECB, BoE ...), regimes, weekend gaps, spreads that widen at news/rollover, intrabar ticks with bid/ask.
|
|
46
|
+
* Several markets simulated **jointly** (shared factors, correlations that rise in crises); crosses derived
|
|
47
|
+
**exactly** from their legs.
|
|
48
|
+
* Backtest-validation tools: luck-only ("null") paths, planted-edge paths, Monte Carlo of your strategy.
|
|
49
|
+
* `calibrate()` fits a market to a real HistData/MetaTrader M1 file; `calibrate_many()` does it for several markets and
|
|
50
|
+
months at once and measures their correlations; `scorecard()` compares synthetic with real.
|
|
51
|
+
* **New in 2.1:** `selftest()` (is the generator itself giving strategies a free edge?), `robustness_report(strategy)`
|
|
52
|
+
(one-page verdict), indicator edge-case `fixtures` + `indicator_stress_test`, daily bars cut at the **17:00 New York
|
|
53
|
+
close** (`daily_anchor="ny"`), **HistData/MetaTrader file writers** (`export_histdata`, `generate_histdata`),
|
|
54
|
+
correlation control (`set_correlation`), and shared microstructure noise so tightly coupled pairs (EURUSD/USDCHF)
|
|
55
|
+
correlate more like real markets.
|
|
56
|
+
|
|
57
|
+
> **Read this first.** Only **EURUSD** was calibrated to real data (one real month, Aug 2026, 28,643 M1 bars).
|
|
58
|
+
> Every other market is built from **documented assumptions** about its typical behaviour, scaled from the
|
|
59
|
+
> EURUSD baseline, and says so in `forexgen.describe("GBPUSD")`. Synthetic data can test your **logic,
|
|
60
|
+
> robustness and cost sensitivity**; it **cannot prove a strategy makes money live** (no real news,
|
|
61
|
+
> central-bank behaviour or regime history). See [Limitations](#limitations).
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install forexgen
|
|
67
|
+
python -m pytest tests # optional
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Quick start
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import forexgen
|
|
74
|
+
|
|
75
|
+
df = forexgen.get_data("EURUSD", "M15", "3m") # different every call
|
|
76
|
+
print(df.attrs["seed"]) # ... but reproducible
|
|
77
|
+
df = forexgen.get_data("EURUSD", "M15", "3m", seed=df.attrs["seed"])
|
|
78
|
+
|
|
79
|
+
forexgen.generate_csv("GBPUSD", "H1", "1y", seed=7, folder="data") # data/GBPUSD_H1_1y_seed7.csv
|
|
80
|
+
data = forexgen.get_multi(["EURUSD", "GBPUSD", "XAUUSD", "EURGBP"], "M15", "1y", seed=1)
|
|
81
|
+
forexgen.describe("XAUUSD") # calibrated vs assumed
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Command line: `python -m forexgen EURUSD M15 3m --seed 7 --out data` (`--list`, `--profile`, `--scenario`,
|
|
85
|
+
`--sparse`, `--labels`, `--ticks`, `--multi EURUSD,GBPUSD`, `--describe`, `--calibrate FILE`). The seed used is printed.
|
|
86
|
+
|
|
87
|
+
## Output format (unchanged from v1)
|
|
88
|
+
|
|
89
|
+
`time` index/column = **naive UTC**, format `%Y-%m-%d %H:%M:%S`; columns `open, high, low, close, volume, spread_pips`
|
|
90
|
+
(`volume` = tick volume, int). Prices are bid-style OHLC rounded to the market's digits. All timeframes are
|
|
91
|
+
aggregated from the same M1 path, so they agree.
|
|
92
|
+
|
|
93
|
+
## API
|
|
94
|
+
|
|
95
|
+
### `get_data(market, timeframe="M15", duration="30d", start="2024-01-01 00:00", end=None, seed=None, start_price=None, **options)`
|
|
96
|
+
|
|
97
|
+
| argument | meaning |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `market` | e.g. `"EURUSD"`, `"XAUUSD"` (case/slash-insensitive: `"eur/usd"`). `list_markets()` |
|
|
100
|
+
| `timeframe` | `M1 M5 M15 M30 H1 H4 D1 W1` (any `M<n>`/`H<n>`; also `15m`, `1h`, `daily`) |
|
|
101
|
+
| `duration` | `30d`, `2w`, `3m`, `1y`, `12h`, `90min` (**market time**: 1w = 5 days, 1m = 21 days, 1y = 260 days) |
|
|
102
|
+
| `start`, `end` | naive UTC timestamps; `end` overrides `duration` |
|
|
103
|
+
| `seed` | `None` (default): fresh data every call. An integer reproduces the data exactly. Stored in `df.attrs["seed"]` |
|
|
104
|
+
| `start_price` | override the first price (for crosses it adjusts the first leg) |
|
|
105
|
+
| `profile` | `"typical"` (default), `"calm_aug2026"` (the real calm month's level), `"stressed"`, or a `Profile` |
|
|
106
|
+
| `scenario`, `scenario_at`, `scenario_strength`, `scenario_direction`, `scenario_markets` | inject a stress scenario (below) |
|
|
107
|
+
| `events` | `"default"` (rule calendar), `"none"`, or a DataFrame / CSV of your own events |
|
|
108
|
+
| `sparse` | `True`: broker-like flat bars and missing minutes (measured EURUSD rates) |
|
|
109
|
+
| `return_labels` | adds `regime`, `event_flag`, `true_vol` columns |
|
|
110
|
+
| `edge` | `dict(kind="momentum"\|"mean_reversion", strength=0.02, horizon="1h")` plants a known edge |
|
|
111
|
+
| `**model` | v1 options still accepted: `garch=(a,b)`, `t_df`, `jump_rate_per_day`, `jump_sigma`, `weekend_gap`, `kappa`, `drift`, `usd_factor`; plus `vol_scale` |
|
|
112
|
+
|
|
113
|
+
How the v1 options map: `garch` sets the GARCH alpha/beta; `t_df` the Student-t tail; `jump_rate_per_day` /
|
|
114
|
+
`jump_sigma` the **residual unscheduled** shock process (scheduled news now comes from the calendar);
|
|
115
|
+
`weekend_gap` is the weekend-gap standard deviation in per-minute sigmas; `kappa` the mean-reversion speed;
|
|
116
|
+
`drift` an annual log drift; `usd_factor=False` removes the cross-market factor.
|
|
117
|
+
|
|
118
|
+
### `generate_csv(market, timeframe="M15", duration="30d", path=None, folder="forex_data", **kwargs)`
|
|
119
|
+
Writes the CSV and returns its path. Default name `<folder>/<SYMBOL>_<TF>_<duration>_seed<seed>.csv`
|
|
120
|
+
(`..._to-<digits of end>_...` when `end` is given). With `seed=None` the seed actually used is in the name.
|
|
121
|
+
|
|
122
|
+
### `get_multi(markets, timeframe, duration, start, end, seed, profile, as_frame=False, **options)`
|
|
123
|
+
Joint simulation. Returns `{symbol: DataFrame}` (or one MultiIndex-column DataFrame). **A market inside
|
|
124
|
+
`get_multi` equals `get_data(market, seed=seed)`** for the same arguments. Crosses are derived exactly:
|
|
125
|
+
`EURGBP = EURUSD / GBPUSD`, `EURJPY = EURUSD * USDJPY`, `GBPJPY = GBPUSD * USDJPY`.
|
|
126
|
+
|
|
127
|
+
### `get_ticks(market, duration="1d", start, end, seed, profile, ticks_per_minute="auto", ...)`
|
|
128
|
+
Bid/ask ticks inside every M1 bar on a Brownian-bridge path **conditioned on the bar's OHLC** (it visits the
|
|
129
|
+
high and low in a defined order), so stop/target order inside a bar is decided by a path, not a guess. Bars
|
|
130
|
+
rebuilt from the bid ticks equal `get_data(..., "M1")` for the same seed. ~100 µs per minute bar (off by default).
|
|
131
|
+
|
|
132
|
+
### Events: `get_events`, `register_event`, `set_events`
|
|
133
|
+
`get_events("EURUSD", start, end)` returns the calendar used for that market (with its sensitivity).
|
|
134
|
+
`register_event(name, time_utc, importance=3, currencies=("USD",))` adds one event; `set_events(df_or_csv)`
|
|
135
|
+
replaces the rule calendar (columns `time_utc, name, importance, currencies`); `set_events(None)` restores it.
|
|
136
|
+
**All default dates are rule-based approximations, not the official calendar:**
|
|
137
|
+
|
|
138
|
+
| event | rule (local time) | importance |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| US NFP | first Friday 08:30 NY | 5 |
|
|
141
|
+
| US CPI | Wednesday in 11th-17th, 08:30 NY | 4 |
|
|
142
|
+
| US FOMC | 8/yr, Wednesday 14:00 NY | 5 |
|
|
143
|
+
| US retail sales / PCE / ISM / UMich | 08:30 or 10:00 NY | 2-3 |
|
|
144
|
+
| Jackson Hole | Friday in 22nd-28th of August, 10:00 NY | 4 |
|
|
145
|
+
| ECB | 8/yr, Thursday 14:15 Frankfurt | 4 |
|
|
146
|
+
| BoE | 8/yr, Thursday 12:00 London | 4 |
|
|
147
|
+
| BoJ, RBA, RBNZ, SNB, BoC | 3-8/yr, local release times | 3 |
|
|
148
|
+
| US jobless claims | every Thursday 08:30 NY | 1 |
|
|
149
|
+
|
|
150
|
+
Reaction (assumed shape; sizes anchored to the 8 events observed in the real file): vol x0.75 in the 30 minutes
|
|
151
|
+
before, a random-sign heavy-tailed shock (80/12/5/3% over the first 4 minutes), power-law volatility decay for
|
|
152
|
+
~4 hours, spread widening. USD events move every USD pair (opposite sign for USDxxx vs xxxUSD, x1.5 for gold);
|
|
153
|
+
other currencies' events move the pairs that contain them.
|
|
154
|
+
|
|
155
|
+
### Scenarios
|
|
156
|
+
`scenario=` one of `flash_crash, peg_break, rate_shock, trend_up, trend_down, range, high_vol, low_liquidity,
|
|
157
|
+
gap, intervention, crisis`, placed at `scenario_at` (timestamp, or a fraction 0..1 of the data) and scaled by
|
|
158
|
+
`scenario_strength`. They are **parameter transformations of the base model** (jumps, volatility/spread
|
|
159
|
+
multipliers, drift, mean reversion, forced global stress), documented in `forexgen/scenarios.py`.
|
|
160
|
+
`crisis` also forces the global stress state, so correlations rise toward 1 in `get_multi`.
|
|
161
|
+
`scenario_markets=[...]` restricts which markets get the scenario; `scenario_direction="up"|"down"|"auto"`.
|
|
162
|
+
|
|
163
|
+
### Backtest validation tools
|
|
164
|
+
```python
|
|
165
|
+
null = forexgen.monte_carlo(strategy, "EURUSD", "M5", "1m", n=200, mode="null") # luck only
|
|
166
|
+
edge = forexgen.monte_carlo(strategy, "EURUSD", "M5", "1m", n=50, mode="edge") # planted edge
|
|
167
|
+
reg = forexgen.monte_carlo(strategy, "EURUSD", "M5", "1m", n=50, mode="regular")
|
|
168
|
+
forexgen.percentile_in_null(my_real_backtest_return, null) # >95 = unlikely luck
|
|
169
|
+
paths = forexgen.get_null_paths("EURUSD", "H1", "3m", n=100) # the raw null paths
|
|
170
|
+
```
|
|
171
|
+
`strategy(df) -> pandas.Series` of target positions in [-1, 1]. Positions decided on bar *t*'s close are
|
|
172
|
+
executed at bar *t+1*'s open and pay half the bar's spread per unit of position change (+ `commission_pips`).
|
|
173
|
+
Returned metrics per path: `total_return, max_drawdown, sharpe, n_trades, cost_paid`.
|
|
174
|
+
**Null paths** keep volatility clustering, tails, spreads and bar shapes but give every bar and every gap an
|
|
175
|
+
independent random sign, so trends, momentum, mean reversion and drift cannot exist.
|
|
176
|
+
`forexgen.backtest(df, positions, pip, ...)` is the same engine for your own paths.
|
|
177
|
+
Costs: `slippage(df, market, order_type, model)`, `stop_fill(...)` (a stop that gaps fills at the gap, never at
|
|
178
|
+
the stop price), `swap_pips(...)` (17:00 NY rollover, Wednesday triple).
|
|
179
|
+
|
|
180
|
+
### Calibration and profiles
|
|
181
|
+
```python
|
|
182
|
+
profile = forexgen.calibrate("DAT_MT_GBPUSD_M1_202608.csv", market="GBPUSD", tz="America/New_York")
|
|
183
|
+
forexgen.use_profile(profile) # or register_market(..., profile=profile)
|
|
184
|
+
profile.save("gbpusd.json"); forexgen.load_profile("gbpusd.json")
|
|
185
|
+
real = forexgen.read_histdata(path) # UTC M1 DataFrame
|
|
186
|
+
forexgen.scorecard(forexgen.get_data("GBPUSD", "M1", "6m", profile="calm_aug2026"), "GBPUSD", reference=real)
|
|
187
|
+
```
|
|
188
|
+
`calibrate` reads the HistData/MT layout (no header: `YYYY.MM.DD,HH:MM,o,h,l,c,v`), converts the **New York
|
|
189
|
+
clock (DST-aware)** to UTC, and fits: volatility level, event-free seasonality, microstructure (lag-1 bounce and
|
|
190
|
+
variance ratios), GARCH persistence (simulated method of moments), bar-range scale, weekend gap, flat-bar and
|
|
191
|
+
missing-minute rates, and event sizes **only when >= 8 events of a class exist**. It cannot fit spreads or volume
|
|
192
|
+
from a price-only file (kept as "assumed").
|
|
193
|
+
|
|
194
|
+
## New in 2.1
|
|
195
|
+
|
|
196
|
+
### Is the generator itself fair? `selftest()`
|
|
197
|
+
```python
|
|
198
|
+
report = forexgen.selftest("EURUSD", profile="typical") # ~30 s
|
|
199
|
+
print(report.passed, report.warnings)
|
|
200
|
+
```
|
|
201
|
+
Checks market-like statistics on M1 paths (variance ratios - **a trend-biased generator has VR > 1**, lag-1
|
|
202
|
+
autocorrelation, volatility clustering, fat tails, OHLC validity) and a **free-edge test**: a plain trend strategy and a plain
|
|
203
|
+
mean-reversion strategy run on ordinary paths and on luck-only paths; if ordinary paths pay either one significantly more
|
|
204
|
+
than luck (z > 3.5) the generator is leaking an edge and the check FAILS. It also works as a guard for your own
|
|
205
|
+
`profile=` / `calibrate()` results: `forexgen.selftest("GBPUSD", profile=my_profile)`. CLI: `python -m forexgen --selftest`.
|
|
206
|
+
|
|
207
|
+
### One-page verdict for your strategy: `robustness_report(strategy, ...)`
|
|
208
|
+
```python
|
|
209
|
+
rep = forexgen.robustness_report(my_strategy, "EURUSD", "H1", "3m", n=30, commission_pips=0.3, real_result=0.04)
|
|
210
|
+
rep.tables["costs"], rep.tables["stress"], rep.passed
|
|
211
|
+
```
|
|
212
|
+
Runs the strategy on regular paths, luck-only paths, planted momentum and mean-reversion edges, ten stress scenarios and a
|
|
213
|
+
cost sweep (free / 1x / 2x / 3x spread), then prints a verdict: where your real result ranks among luck-only paths, whether the
|
|
214
|
+
strategy can exploit a known edge, whether it is cost-fragile, and which scenarios hurt far more than normal conditions.
|
|
215
|
+
It cannot prove the strategy works live.
|
|
216
|
+
|
|
217
|
+
### Indicator edge-case fixtures
|
|
218
|
+
```python
|
|
219
|
+
from forexgen import fixtures
|
|
220
|
+
report = forexgen.indicator_stress_test(my_rsi, timeframe="M5", bounds=(0, 100)) # one row per fixture
|
|
221
|
+
fixtures.fixture("flat", "M15", 500) # deterministic data
|
|
222
|
+
```
|
|
223
|
+
16 reproducible fixtures (`fixtures.FIXTURES`): flat, one-way trends, staircase, saw-tooth, huge gap, spike-and-reverse,
|
|
224
|
+
zero-range bars, volatility burst, missing bars, tiny/huge prices, one/two bars, NaN inside the input. The tester reports
|
|
225
|
+
exceptions, NaN **after** warm-up, inf, wrong output length and out-of-bounds values.
|
|
226
|
+
|
|
227
|
+
### Daily bars at the New York close
|
|
228
|
+
`get_data(..., "D1", daily_anchor="ny")` (and `W1`) cuts bars at **17:00 New York** (Sunday's session belongs to Monday's bar;
|
|
229
|
+
5 bars per week; the timestamp is the bar's open in UTC, 21:00 or 22:00). Default stays `"utc"` (as in v1). CLI `--daily-anchor ny`.
|
|
230
|
+
|
|
231
|
+
### HistData / MetaTrader writers
|
|
232
|
+
```python
|
|
233
|
+
forexgen.generate_histdata("EURUSD", "3m", folder="out", seed=7) # DAT_MT_EURUSD_M1_202401.csv ... per New York month
|
|
234
|
+
forexgen.export_histdata(df_m1, "out/x.csv", "EURUSD", fmt="mt") # or fmt="ascii" (HistData ASCII, fixed EST)
|
|
235
|
+
```
|
|
236
|
+
`fmt="mt"`: `YYYY.MM.DD,HH:MM,o,h,l,c,v`, New York local time with DST (same as the real Aug-2026 file). `fmt="ascii"`:
|
|
237
|
+
`YYYYMMDD HHMMSS;o;h;l;c;v`, fixed EST (UTC-5). Prices have digits+1 decimals; `volume="zero"` mimics the real MT files. Files
|
|
238
|
+
read back exactly with `read_histdata` (both layouts are auto-detected, including across the November DST change). CLI: `--histdata`.
|
|
239
|
+
|
|
240
|
+
### Calibrating many months / markets
|
|
241
|
+
```python
|
|
242
|
+
cs = forexgen.calibrate_many(glob.glob("real/DAT_MT_*_M1_*.csv")) # symbol read from the file name, or {symbol: paths}
|
|
243
|
+
cs.apply(); cs.save("my_calibration"); forexgen.CalibrationSet.load("my_calibration")
|
|
244
|
+
forexgen.set_correlation(cs.corr); forexgen.reset_correlation()
|
|
245
|
+
```
|
|
246
|
+
Months of one market are pooled into ONE profile (pooled level, seasonality, microstructure; **no regime fitting**); the
|
|
247
|
+
variance level is calm/typical automatically. The correlation matrix is measured from M15 returns of the base markets over
|
|
248
|
+
their common period and installed by `apply()` (`set_correlation` scales your desired matrix by the attenuation found when
|
|
249
|
+
tuning the defaults, so realised correlations follow it). Derived crosses are skipped (they follow their legs).
|
|
250
|
+
|
|
251
|
+
### Tested environments
|
|
252
|
+
Full test-suite (see REPORT.md) on **pandas 2.2.3 / numpy 1.26.4** and **pandas 3.0.2 / numpy 2.4.4**, Python 3.12.
|
|
253
|
+
Older pandas/numpy are untested.
|
|
254
|
+
|
|
255
|
+
## Markets: what is calibrated and what is assumed
|
|
256
|
+
|
|
257
|
+
| | EURUSD | all other markets |
|
|
258
|
+
|---|---|---|
|
|
259
|
+
| calm volatility level (4.4%) | **calibrated** (Aug-2026) | assumed (0.63 x long-run) |
|
|
260
|
+
| long-run volatility level | assumed (7%) | assumed (GBPUSD 8.5, AUDUSD 10, NZDUSD 10.5, USDJPY 9.5, USDCHF 8, USDCAD 6.5, XAUUSD 16 %) |
|
|
261
|
+
| hourly (NY) seasonality | **calibrated** (events excluded) | assumed (EURUSD curve reshaped for each market's sessions) |
|
|
262
|
+
| microstructure (bounce, variance ratios), GARCH, bar range, weekend gap, flat bars, missing minutes | **calibrated** | assumed (copied from EURUSD) |
|
|
263
|
+
| event shock sizes | assumed, sanity-checked against 8 observed events | assumed (scaled by volatility ratio) |
|
|
264
|
+
| spreads, volume | assumed (price-only file) | assumed |
|
|
265
|
+
| regimes, scenarios, correlations | assumed | assumed (latent matrix tuned so realised M15 correlations land near typical values) |
|
|
266
|
+
| EURGBP, EURJPY, GBPJPY | derived exactly from their legs (no separate parameters) | |
|
|
267
|
+
|
|
268
|
+
`forexgen.describe("GBPUSD")` prints this per market. Upgrade any market with `calibrate()` on a real file.
|
|
269
|
+
|
|
270
|
+
## Compatibility notes (changes from v1.0.0)
|
|
271
|
+
|
|
272
|
+
Kept: every function name and signature, the CSV layout and timestamp format, the CLI, durations/timeframes, the
|
|
273
|
+
11 markets, `register_market`, the v1 model options, and "same seed -> same first N days whatever the duration".
|
|
274
|
+
Changed on purpose:
|
|
275
|
+
* **`seed` now defaults to `None`** (v1: 42): data differs every call. Pass an integer to get v1-style repeatability.
|
|
276
|
+
* **Market hours** are Sunday 17:00 -> Friday 17:00 **New York time, DST-aware** (v1: fixed 22:00 UTC).
|
|
277
|
+
* The old random news jumps are replaced by the scheduled-event calendar plus a small residual shock process.
|
|
278
|
+
* Default start prices are unchanged (e.g. EURUSD 1.0850); the calibrated *behaviour* comes from the Aug-2026 data.
|
|
279
|
+
* Extra columns only appear when asked for (`return_labels=True`).
|
|
280
|
+
* Daily bars are still cut at **UTC midnight** by default (as in v1); use `daily_anchor="ny"` for the 17:00 New York close.
|
|
281
|
+
|
|
282
|
+
## Reproducibility rules
|
|
283
|
+
An explicit `seed` gives identical output for identical arguments. Every random stream is keyed by
|
|
284
|
+
(seed, stream, day-block) - and shared factors by absolute UTC time - so a 60-day request starts with exactly the
|
|
285
|
+
same 30 days as a 30-day request, and a market alone equals the same market inside `get_multi`.
|
|
286
|
+
Scenario positions given as a *fraction* depend on the duration; give a timestamp to keep prefix-stability.
|
|
287
|
+
|
|
288
|
+
## Limitations
|
|
289
|
+
* One real month of one market is the only real-data anchor. Non-EURUSD markets are plausible, not fitted;
|
|
290
|
+
`calibrate_many` has only been tested on synthetic multi-market files, never on real multi-month data.
|
|
291
|
+
* A calm month cannot teach crashes or regime history; `stressed`, regimes and scenarios are assumptions.
|
|
292
|
+
* Event dates are rule-based approximations; event sizes for importance classes are assumptions.
|
|
293
|
+
* Realised cross-market correlations are close to, not exactly, the targets (EURUSD/USDCHF reaches about -0.83
|
|
294
|
+
against a -0.90 target); `scorecard`/your own checks report actuals.
|
|
295
|
+
* Scheduled-event shock sizes use an uncapped lognormal, so the extreme tail is probably too fat: in the typical profile a
|
|
296
|
+
single 3-month path can contain a 40-95 bp one-minute move on an important release (real EURUSD rarely exceeds ~50 bp).
|
|
297
|
+
* The model has no central-bank regime shifts, trending macro cycles, or order-book dynamics.
|
|
298
|
+
* Spread and volume are models, not broker data. Tick generation is slow.
|
|
299
|
+
|
|
300
|
+
## Overfitting warning
|
|
301
|
+
Tuning a strategy until it looks good on synthetic data (or on a handful of paths) proves nothing. Always check a
|
|
302
|
+
result against `monte_carlo(mode="null")`, keep out-of-sample paths you never tuned on, include costs, and validate
|
|
303
|
+
on **real** data and a forward test before risking money.
|
forexgen-2.1.0/README.md
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# forexgen 2.1
|
|
2
|
+
|
|
3
|
+
Offline synthetic **forex and gold** data generator for **indicator testing and strategy backtesting**
|
|
4
|
+
(and, secondarily, producing training data). Pure Python, **numpy + pandas only**, no internet, no GUI.
|
|
5
|
+
|
|
6
|
+
* 11 markets: EURUSD GBPUSD USDJPY AUDUSD NZDUSD USDCHF USDCAD EURGBP EURJPY GBPJPY XAUUSD (+ your own).
|
|
7
|
+
* **Different data on every run by default**; the seed used is recorded so any run can be reproduced.
|
|
8
|
+
* Real market structure: volatility clustering, fat tails, DST-aware sessions, scheduled news (NFP, CPI,
|
|
9
|
+
FOMC, ECB, BoE ...), regimes, weekend gaps, spreads that widen at news/rollover, intrabar ticks with bid/ask.
|
|
10
|
+
* Several markets simulated **jointly** (shared factors, correlations that rise in crises); crosses derived
|
|
11
|
+
**exactly** from their legs.
|
|
12
|
+
* Backtest-validation tools: luck-only ("null") paths, planted-edge paths, Monte Carlo of your strategy.
|
|
13
|
+
* `calibrate()` fits a market to a real HistData/MetaTrader M1 file; `calibrate_many()` does it for several markets and
|
|
14
|
+
months at once and measures their correlations; `scorecard()` compares synthetic with real.
|
|
15
|
+
* **New in 2.1:** `selftest()` (is the generator itself giving strategies a free edge?), `robustness_report(strategy)`
|
|
16
|
+
(one-page verdict), indicator edge-case `fixtures` + `indicator_stress_test`, daily bars cut at the **17:00 New York
|
|
17
|
+
close** (`daily_anchor="ny"`), **HistData/MetaTrader file writers** (`export_histdata`, `generate_histdata`),
|
|
18
|
+
correlation control (`set_correlation`), and shared microstructure noise so tightly coupled pairs (EURUSD/USDCHF)
|
|
19
|
+
correlate more like real markets.
|
|
20
|
+
|
|
21
|
+
> **Read this first.** Only **EURUSD** was calibrated to real data (one real month, Aug 2026, 28,643 M1 bars).
|
|
22
|
+
> Every other market is built from **documented assumptions** about its typical behaviour, scaled from the
|
|
23
|
+
> EURUSD baseline, and says so in `forexgen.describe("GBPUSD")`. Synthetic data can test your **logic,
|
|
24
|
+
> robustness and cost sensitivity**; it **cannot prove a strategy makes money live** (no real news,
|
|
25
|
+
> central-bank behaviour or regime history). See [Limitations](#limitations).
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install forexgen
|
|
31
|
+
python -m pytest tests # optional
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import forexgen
|
|
38
|
+
|
|
39
|
+
df = forexgen.get_data("EURUSD", "M15", "3m") # different every call
|
|
40
|
+
print(df.attrs["seed"]) # ... but reproducible
|
|
41
|
+
df = forexgen.get_data("EURUSD", "M15", "3m", seed=df.attrs["seed"])
|
|
42
|
+
|
|
43
|
+
forexgen.generate_csv("GBPUSD", "H1", "1y", seed=7, folder="data") # data/GBPUSD_H1_1y_seed7.csv
|
|
44
|
+
data = forexgen.get_multi(["EURUSD", "GBPUSD", "XAUUSD", "EURGBP"], "M15", "1y", seed=1)
|
|
45
|
+
forexgen.describe("XAUUSD") # calibrated vs assumed
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Command line: `python -m forexgen EURUSD M15 3m --seed 7 --out data` (`--list`, `--profile`, `--scenario`,
|
|
49
|
+
`--sparse`, `--labels`, `--ticks`, `--multi EURUSD,GBPUSD`, `--describe`, `--calibrate FILE`). The seed used is printed.
|
|
50
|
+
|
|
51
|
+
## Output format (unchanged from v1)
|
|
52
|
+
|
|
53
|
+
`time` index/column = **naive UTC**, format `%Y-%m-%d %H:%M:%S`; columns `open, high, low, close, volume, spread_pips`
|
|
54
|
+
(`volume` = tick volume, int). Prices are bid-style OHLC rounded to the market's digits. All timeframes are
|
|
55
|
+
aggregated from the same M1 path, so they agree.
|
|
56
|
+
|
|
57
|
+
## API
|
|
58
|
+
|
|
59
|
+
### `get_data(market, timeframe="M15", duration="30d", start="2024-01-01 00:00", end=None, seed=None, start_price=None, **options)`
|
|
60
|
+
|
|
61
|
+
| argument | meaning |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `market` | e.g. `"EURUSD"`, `"XAUUSD"` (case/slash-insensitive: `"eur/usd"`). `list_markets()` |
|
|
64
|
+
| `timeframe` | `M1 M5 M15 M30 H1 H4 D1 W1` (any `M<n>`/`H<n>`; also `15m`, `1h`, `daily`) |
|
|
65
|
+
| `duration` | `30d`, `2w`, `3m`, `1y`, `12h`, `90min` (**market time**: 1w = 5 days, 1m = 21 days, 1y = 260 days) |
|
|
66
|
+
| `start`, `end` | naive UTC timestamps; `end` overrides `duration` |
|
|
67
|
+
| `seed` | `None` (default): fresh data every call. An integer reproduces the data exactly. Stored in `df.attrs["seed"]` |
|
|
68
|
+
| `start_price` | override the first price (for crosses it adjusts the first leg) |
|
|
69
|
+
| `profile` | `"typical"` (default), `"calm_aug2026"` (the real calm month's level), `"stressed"`, or a `Profile` |
|
|
70
|
+
| `scenario`, `scenario_at`, `scenario_strength`, `scenario_direction`, `scenario_markets` | inject a stress scenario (below) |
|
|
71
|
+
| `events` | `"default"` (rule calendar), `"none"`, or a DataFrame / CSV of your own events |
|
|
72
|
+
| `sparse` | `True`: broker-like flat bars and missing minutes (measured EURUSD rates) |
|
|
73
|
+
| `return_labels` | adds `regime`, `event_flag`, `true_vol` columns |
|
|
74
|
+
| `edge` | `dict(kind="momentum"\|"mean_reversion", strength=0.02, horizon="1h")` plants a known edge |
|
|
75
|
+
| `**model` | v1 options still accepted: `garch=(a,b)`, `t_df`, `jump_rate_per_day`, `jump_sigma`, `weekend_gap`, `kappa`, `drift`, `usd_factor`; plus `vol_scale` |
|
|
76
|
+
|
|
77
|
+
How the v1 options map: `garch` sets the GARCH alpha/beta; `t_df` the Student-t tail; `jump_rate_per_day` /
|
|
78
|
+
`jump_sigma` the **residual unscheduled** shock process (scheduled news now comes from the calendar);
|
|
79
|
+
`weekend_gap` is the weekend-gap standard deviation in per-minute sigmas; `kappa` the mean-reversion speed;
|
|
80
|
+
`drift` an annual log drift; `usd_factor=False` removes the cross-market factor.
|
|
81
|
+
|
|
82
|
+
### `generate_csv(market, timeframe="M15", duration="30d", path=None, folder="forex_data", **kwargs)`
|
|
83
|
+
Writes the CSV and returns its path. Default name `<folder>/<SYMBOL>_<TF>_<duration>_seed<seed>.csv`
|
|
84
|
+
(`..._to-<digits of end>_...` when `end` is given). With `seed=None` the seed actually used is in the name.
|
|
85
|
+
|
|
86
|
+
### `get_multi(markets, timeframe, duration, start, end, seed, profile, as_frame=False, **options)`
|
|
87
|
+
Joint simulation. Returns `{symbol: DataFrame}` (or one MultiIndex-column DataFrame). **A market inside
|
|
88
|
+
`get_multi` equals `get_data(market, seed=seed)`** for the same arguments. Crosses are derived exactly:
|
|
89
|
+
`EURGBP = EURUSD / GBPUSD`, `EURJPY = EURUSD * USDJPY`, `GBPJPY = GBPUSD * USDJPY`.
|
|
90
|
+
|
|
91
|
+
### `get_ticks(market, duration="1d", start, end, seed, profile, ticks_per_minute="auto", ...)`
|
|
92
|
+
Bid/ask ticks inside every M1 bar on a Brownian-bridge path **conditioned on the bar's OHLC** (it visits the
|
|
93
|
+
high and low in a defined order), so stop/target order inside a bar is decided by a path, not a guess. Bars
|
|
94
|
+
rebuilt from the bid ticks equal `get_data(..., "M1")` for the same seed. ~100 µs per minute bar (off by default).
|
|
95
|
+
|
|
96
|
+
### Events: `get_events`, `register_event`, `set_events`
|
|
97
|
+
`get_events("EURUSD", start, end)` returns the calendar used for that market (with its sensitivity).
|
|
98
|
+
`register_event(name, time_utc, importance=3, currencies=("USD",))` adds one event; `set_events(df_or_csv)`
|
|
99
|
+
replaces the rule calendar (columns `time_utc, name, importance, currencies`); `set_events(None)` restores it.
|
|
100
|
+
**All default dates are rule-based approximations, not the official calendar:**
|
|
101
|
+
|
|
102
|
+
| event | rule (local time) | importance |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| US NFP | first Friday 08:30 NY | 5 |
|
|
105
|
+
| US CPI | Wednesday in 11th-17th, 08:30 NY | 4 |
|
|
106
|
+
| US FOMC | 8/yr, Wednesday 14:00 NY | 5 |
|
|
107
|
+
| US retail sales / PCE / ISM / UMich | 08:30 or 10:00 NY | 2-3 |
|
|
108
|
+
| Jackson Hole | Friday in 22nd-28th of August, 10:00 NY | 4 |
|
|
109
|
+
| ECB | 8/yr, Thursday 14:15 Frankfurt | 4 |
|
|
110
|
+
| BoE | 8/yr, Thursday 12:00 London | 4 |
|
|
111
|
+
| BoJ, RBA, RBNZ, SNB, BoC | 3-8/yr, local release times | 3 |
|
|
112
|
+
| US jobless claims | every Thursday 08:30 NY | 1 |
|
|
113
|
+
|
|
114
|
+
Reaction (assumed shape; sizes anchored to the 8 events observed in the real file): vol x0.75 in the 30 minutes
|
|
115
|
+
before, a random-sign heavy-tailed shock (80/12/5/3% over the first 4 minutes), power-law volatility decay for
|
|
116
|
+
~4 hours, spread widening. USD events move every USD pair (opposite sign for USDxxx vs xxxUSD, x1.5 for gold);
|
|
117
|
+
other currencies' events move the pairs that contain them.
|
|
118
|
+
|
|
119
|
+
### Scenarios
|
|
120
|
+
`scenario=` one of `flash_crash, peg_break, rate_shock, trend_up, trend_down, range, high_vol, low_liquidity,
|
|
121
|
+
gap, intervention, crisis`, placed at `scenario_at` (timestamp, or a fraction 0..1 of the data) and scaled by
|
|
122
|
+
`scenario_strength`. They are **parameter transformations of the base model** (jumps, volatility/spread
|
|
123
|
+
multipliers, drift, mean reversion, forced global stress), documented in `forexgen/scenarios.py`.
|
|
124
|
+
`crisis` also forces the global stress state, so correlations rise toward 1 in `get_multi`.
|
|
125
|
+
`scenario_markets=[...]` restricts which markets get the scenario; `scenario_direction="up"|"down"|"auto"`.
|
|
126
|
+
|
|
127
|
+
### Backtest validation tools
|
|
128
|
+
```python
|
|
129
|
+
null = forexgen.monte_carlo(strategy, "EURUSD", "M5", "1m", n=200, mode="null") # luck only
|
|
130
|
+
edge = forexgen.monte_carlo(strategy, "EURUSD", "M5", "1m", n=50, mode="edge") # planted edge
|
|
131
|
+
reg = forexgen.monte_carlo(strategy, "EURUSD", "M5", "1m", n=50, mode="regular")
|
|
132
|
+
forexgen.percentile_in_null(my_real_backtest_return, null) # >95 = unlikely luck
|
|
133
|
+
paths = forexgen.get_null_paths("EURUSD", "H1", "3m", n=100) # the raw null paths
|
|
134
|
+
```
|
|
135
|
+
`strategy(df) -> pandas.Series` of target positions in [-1, 1]. Positions decided on bar *t*'s close are
|
|
136
|
+
executed at bar *t+1*'s open and pay half the bar's spread per unit of position change (+ `commission_pips`).
|
|
137
|
+
Returned metrics per path: `total_return, max_drawdown, sharpe, n_trades, cost_paid`.
|
|
138
|
+
**Null paths** keep volatility clustering, tails, spreads and bar shapes but give every bar and every gap an
|
|
139
|
+
independent random sign, so trends, momentum, mean reversion and drift cannot exist.
|
|
140
|
+
`forexgen.backtest(df, positions, pip, ...)` is the same engine for your own paths.
|
|
141
|
+
Costs: `slippage(df, market, order_type, model)`, `stop_fill(...)` (a stop that gaps fills at the gap, never at
|
|
142
|
+
the stop price), `swap_pips(...)` (17:00 NY rollover, Wednesday triple).
|
|
143
|
+
|
|
144
|
+
### Calibration and profiles
|
|
145
|
+
```python
|
|
146
|
+
profile = forexgen.calibrate("DAT_MT_GBPUSD_M1_202608.csv", market="GBPUSD", tz="America/New_York")
|
|
147
|
+
forexgen.use_profile(profile) # or register_market(..., profile=profile)
|
|
148
|
+
profile.save("gbpusd.json"); forexgen.load_profile("gbpusd.json")
|
|
149
|
+
real = forexgen.read_histdata(path) # UTC M1 DataFrame
|
|
150
|
+
forexgen.scorecard(forexgen.get_data("GBPUSD", "M1", "6m", profile="calm_aug2026"), "GBPUSD", reference=real)
|
|
151
|
+
```
|
|
152
|
+
`calibrate` reads the HistData/MT layout (no header: `YYYY.MM.DD,HH:MM,o,h,l,c,v`), converts the **New York
|
|
153
|
+
clock (DST-aware)** to UTC, and fits: volatility level, event-free seasonality, microstructure (lag-1 bounce and
|
|
154
|
+
variance ratios), GARCH persistence (simulated method of moments), bar-range scale, weekend gap, flat-bar and
|
|
155
|
+
missing-minute rates, and event sizes **only when >= 8 events of a class exist**. It cannot fit spreads or volume
|
|
156
|
+
from a price-only file (kept as "assumed").
|
|
157
|
+
|
|
158
|
+
## New in 2.1
|
|
159
|
+
|
|
160
|
+
### Is the generator itself fair? `selftest()`
|
|
161
|
+
```python
|
|
162
|
+
report = forexgen.selftest("EURUSD", profile="typical") # ~30 s
|
|
163
|
+
print(report.passed, report.warnings)
|
|
164
|
+
```
|
|
165
|
+
Checks market-like statistics on M1 paths (variance ratios - **a trend-biased generator has VR > 1**, lag-1
|
|
166
|
+
autocorrelation, volatility clustering, fat tails, OHLC validity) and a **free-edge test**: a plain trend strategy and a plain
|
|
167
|
+
mean-reversion strategy run on ordinary paths and on luck-only paths; if ordinary paths pay either one significantly more
|
|
168
|
+
than luck (z > 3.5) the generator is leaking an edge and the check FAILS. It also works as a guard for your own
|
|
169
|
+
`profile=` / `calibrate()` results: `forexgen.selftest("GBPUSD", profile=my_profile)`. CLI: `python -m forexgen --selftest`.
|
|
170
|
+
|
|
171
|
+
### One-page verdict for your strategy: `robustness_report(strategy, ...)`
|
|
172
|
+
```python
|
|
173
|
+
rep = forexgen.robustness_report(my_strategy, "EURUSD", "H1", "3m", n=30, commission_pips=0.3, real_result=0.04)
|
|
174
|
+
rep.tables["costs"], rep.tables["stress"], rep.passed
|
|
175
|
+
```
|
|
176
|
+
Runs the strategy on regular paths, luck-only paths, planted momentum and mean-reversion edges, ten stress scenarios and a
|
|
177
|
+
cost sweep (free / 1x / 2x / 3x spread), then prints a verdict: where your real result ranks among luck-only paths, whether the
|
|
178
|
+
strategy can exploit a known edge, whether it is cost-fragile, and which scenarios hurt far more than normal conditions.
|
|
179
|
+
It cannot prove the strategy works live.
|
|
180
|
+
|
|
181
|
+
### Indicator edge-case fixtures
|
|
182
|
+
```python
|
|
183
|
+
from forexgen import fixtures
|
|
184
|
+
report = forexgen.indicator_stress_test(my_rsi, timeframe="M5", bounds=(0, 100)) # one row per fixture
|
|
185
|
+
fixtures.fixture("flat", "M15", 500) # deterministic data
|
|
186
|
+
```
|
|
187
|
+
16 reproducible fixtures (`fixtures.FIXTURES`): flat, one-way trends, staircase, saw-tooth, huge gap, spike-and-reverse,
|
|
188
|
+
zero-range bars, volatility burst, missing bars, tiny/huge prices, one/two bars, NaN inside the input. The tester reports
|
|
189
|
+
exceptions, NaN **after** warm-up, inf, wrong output length and out-of-bounds values.
|
|
190
|
+
|
|
191
|
+
### Daily bars at the New York close
|
|
192
|
+
`get_data(..., "D1", daily_anchor="ny")` (and `W1`) cuts bars at **17:00 New York** (Sunday's session belongs to Monday's bar;
|
|
193
|
+
5 bars per week; the timestamp is the bar's open in UTC, 21:00 or 22:00). Default stays `"utc"` (as in v1). CLI `--daily-anchor ny`.
|
|
194
|
+
|
|
195
|
+
### HistData / MetaTrader writers
|
|
196
|
+
```python
|
|
197
|
+
forexgen.generate_histdata("EURUSD", "3m", folder="out", seed=7) # DAT_MT_EURUSD_M1_202401.csv ... per New York month
|
|
198
|
+
forexgen.export_histdata(df_m1, "out/x.csv", "EURUSD", fmt="mt") # or fmt="ascii" (HistData ASCII, fixed EST)
|
|
199
|
+
```
|
|
200
|
+
`fmt="mt"`: `YYYY.MM.DD,HH:MM,o,h,l,c,v`, New York local time with DST (same as the real Aug-2026 file). `fmt="ascii"`:
|
|
201
|
+
`YYYYMMDD HHMMSS;o;h;l;c;v`, fixed EST (UTC-5). Prices have digits+1 decimals; `volume="zero"` mimics the real MT files. Files
|
|
202
|
+
read back exactly with `read_histdata` (both layouts are auto-detected, including across the November DST change). CLI: `--histdata`.
|
|
203
|
+
|
|
204
|
+
### Calibrating many months / markets
|
|
205
|
+
```python
|
|
206
|
+
cs = forexgen.calibrate_many(glob.glob("real/DAT_MT_*_M1_*.csv")) # symbol read from the file name, or {symbol: paths}
|
|
207
|
+
cs.apply(); cs.save("my_calibration"); forexgen.CalibrationSet.load("my_calibration")
|
|
208
|
+
forexgen.set_correlation(cs.corr); forexgen.reset_correlation()
|
|
209
|
+
```
|
|
210
|
+
Months of one market are pooled into ONE profile (pooled level, seasonality, microstructure; **no regime fitting**); the
|
|
211
|
+
variance level is calm/typical automatically. The correlation matrix is measured from M15 returns of the base markets over
|
|
212
|
+
their common period and installed by `apply()` (`set_correlation` scales your desired matrix by the attenuation found when
|
|
213
|
+
tuning the defaults, so realised correlations follow it). Derived crosses are skipped (they follow their legs).
|
|
214
|
+
|
|
215
|
+
### Tested environments
|
|
216
|
+
Full test-suite (see REPORT.md) on **pandas 2.2.3 / numpy 1.26.4** and **pandas 3.0.2 / numpy 2.4.4**, Python 3.12.
|
|
217
|
+
Older pandas/numpy are untested.
|
|
218
|
+
|
|
219
|
+
## Markets: what is calibrated and what is assumed
|
|
220
|
+
|
|
221
|
+
| | EURUSD | all other markets |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| calm volatility level (4.4%) | **calibrated** (Aug-2026) | assumed (0.63 x long-run) |
|
|
224
|
+
| long-run volatility level | assumed (7%) | assumed (GBPUSD 8.5, AUDUSD 10, NZDUSD 10.5, USDJPY 9.5, USDCHF 8, USDCAD 6.5, XAUUSD 16 %) |
|
|
225
|
+
| hourly (NY) seasonality | **calibrated** (events excluded) | assumed (EURUSD curve reshaped for each market's sessions) |
|
|
226
|
+
| microstructure (bounce, variance ratios), GARCH, bar range, weekend gap, flat bars, missing minutes | **calibrated** | assumed (copied from EURUSD) |
|
|
227
|
+
| event shock sizes | assumed, sanity-checked against 8 observed events | assumed (scaled by volatility ratio) |
|
|
228
|
+
| spreads, volume | assumed (price-only file) | assumed |
|
|
229
|
+
| regimes, scenarios, correlations | assumed | assumed (latent matrix tuned so realised M15 correlations land near typical values) |
|
|
230
|
+
| EURGBP, EURJPY, GBPJPY | derived exactly from their legs (no separate parameters) | |
|
|
231
|
+
|
|
232
|
+
`forexgen.describe("GBPUSD")` prints this per market. Upgrade any market with `calibrate()` on a real file.
|
|
233
|
+
|
|
234
|
+
## Compatibility notes (changes from v1.0.0)
|
|
235
|
+
|
|
236
|
+
Kept: every function name and signature, the CSV layout and timestamp format, the CLI, durations/timeframes, the
|
|
237
|
+
11 markets, `register_market`, the v1 model options, and "same seed -> same first N days whatever the duration".
|
|
238
|
+
Changed on purpose:
|
|
239
|
+
* **`seed` now defaults to `None`** (v1: 42): data differs every call. Pass an integer to get v1-style repeatability.
|
|
240
|
+
* **Market hours** are Sunday 17:00 -> Friday 17:00 **New York time, DST-aware** (v1: fixed 22:00 UTC).
|
|
241
|
+
* The old random news jumps are replaced by the scheduled-event calendar plus a small residual shock process.
|
|
242
|
+
* Default start prices are unchanged (e.g. EURUSD 1.0850); the calibrated *behaviour* comes from the Aug-2026 data.
|
|
243
|
+
* Extra columns only appear when asked for (`return_labels=True`).
|
|
244
|
+
* Daily bars are still cut at **UTC midnight** by default (as in v1); use `daily_anchor="ny"` for the 17:00 New York close.
|
|
245
|
+
|
|
246
|
+
## Reproducibility rules
|
|
247
|
+
An explicit `seed` gives identical output for identical arguments. Every random stream is keyed by
|
|
248
|
+
(seed, stream, day-block) - and shared factors by absolute UTC time - so a 60-day request starts with exactly the
|
|
249
|
+
same 30 days as a 30-day request, and a market alone equals the same market inside `get_multi`.
|
|
250
|
+
Scenario positions given as a *fraction* depend on the duration; give a timestamp to keep prefix-stability.
|
|
251
|
+
|
|
252
|
+
## Limitations
|
|
253
|
+
* One real month of one market is the only real-data anchor. Non-EURUSD markets are plausible, not fitted;
|
|
254
|
+
`calibrate_many` has only been tested on synthetic multi-market files, never on real multi-month data.
|
|
255
|
+
* A calm month cannot teach crashes or regime history; `stressed`, regimes and scenarios are assumptions.
|
|
256
|
+
* Event dates are rule-based approximations; event sizes for importance classes are assumptions.
|
|
257
|
+
* Realised cross-market correlations are close to, not exactly, the targets (EURUSD/USDCHF reaches about -0.83
|
|
258
|
+
against a -0.90 target); `scorecard`/your own checks report actuals.
|
|
259
|
+
* Scheduled-event shock sizes use an uncapped lognormal, so the extreme tail is probably too fat: in the typical profile a
|
|
260
|
+
single 3-month path can contain a 40-95 bp one-minute move on an important release (real EURUSD rarely exceeds ~50 bp).
|
|
261
|
+
* The model has no central-bank regime shifts, trending macro cycles, or order-book dynamics.
|
|
262
|
+
* Spread and volume are models, not broker data. Tick generation is slow.
|
|
263
|
+
|
|
264
|
+
## Overfitting warning
|
|
265
|
+
Tuning a strategy until it looks good on synthetic data (or on a handful of paths) proves nothing. Always check a
|
|
266
|
+
result against `monte_carlo(mode="null")`, keep out-of-sample paths you never tuned on, include costs, and validate
|
|
267
|
+
on **real** data and a forward test before risking money.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""forexgen 2.0 - offline synthetic forex / gold data generator.
|
|
2
|
+
|
|
3
|
+
Quick start:
|
|
4
|
+
import forexgen
|
|
5
|
+
df = forexgen.get_data("EURUSD", "M15", "3m") # different every call; seed in df.attrs
|
|
6
|
+
df = forexgen.get_data("GBPUSD", "H1", "1y", seed=7) # reproducible
|
|
7
|
+
forexgen.describe("XAUUSD") # calibrated vs assumed parameters
|
|
8
|
+
"""
|
|
9
|
+
from .api import (generate_csv, get_data, get_multi, list_markets, parse_duration, # noqa: F401
|
|
10
|
+
parse_timeframe, register_market)
|
|
11
|
+
from .profiles import (Profile, describe, get_profile, load_profile, save_profile, # noqa: F401
|
|
12
|
+
use_profile, reset_profiles)
|
|
13
|
+
from .events import get_events, register_event, set_events, clear_user_events # noqa: F401
|
|
14
|
+
from .scenarios import SCENARIOS # noqa: F401
|
|
15
|
+
from .ticks import get_ticks # noqa: F401
|
|
16
|
+
from .report import scorecard, stats_m1 # noqa: F401
|
|
17
|
+
from .calibration import calibrate, calibrate_many, CalibrationSet, read_histdata # noqa: F401
|
|
18
|
+
from .validation import get_null_paths, monte_carlo, percentile_in_null, backtest # noqa: F401
|
|
19
|
+
from .costs import slippage, stop_fill, swap_pips # noqa: F401
|
|
20
|
+
|
|
21
|
+
from .factors import set_correlation, reset_correlation # noqa: F401
|
|
22
|
+
from .export import export_histdata, generate_histdata, to_histdata_frame # noqa: F401
|
|
23
|
+
from .diagnostics import selftest, robustness_report, trend_strategy, mean_reversion_strategy # noqa: F401
|
|
24
|
+
from . import fixtures # noqa: F401
|
|
25
|
+
from .fixtures import indicator_stress_test # noqa: F401
|
|
26
|
+
|
|
27
|
+
__version__ = "2.1.0"
|