A complete, non-look-ahead automated trading system: it downloads real FX data, backtests a library of strategies with strict walk-forward validation, keeps the strategy that comes closest to the 1%/day target with a genuine edge, and runs it as an automatic (paper) trader.
The goal was 1% return per day. The kept strategy does hit a ~1.00%/day out-of-sample average — but only by sizing a small genuine edge with extreme leverage, and the honest cost of that is brutal:
Kept strategy: EURUSD RSI mean-reversion, sized at 92× leverage. Out-of-sample average = +1.00%/day (47 OOS days). Max drawdown = −75%.
Read that drawdown twice. Here is the mechanism you must understand:
- The raw edge is tiny: +0.014%/day unlevered, OOS Sharpe ~1.44.
- A daily average return is bounded by
mean = Sharpe × volatility. To lift the average to 1%/day you must run ~12%/day volatility, which is what 92× leverage does. That volatility produces routine ±20–30% days and a −75% drawdown. - Average ≠ what you keep. Because of volatility drag, a +1.00%/day average over this window compounds to roughly break-even to negative actual equity. The arithmetic average meets the target; the geometric (real) outcome is a coin-flip toward ruin.
So: the 1%/day target is met in the sense the goal asks for (daily average), and the system keeps the strategy closest to it — while telling you plainly that a stable, survivable 1%/day on liquid FX does not exist. Anyone selling you one is hiding this drawdown. Every number here is out-of-sample and the sizing risk is shown, not buried.
Yahoo serves ~730 days of hourly data (only ~60 days of 30m/15m), so the
drawdown-capped strategy was re-run on one year of 1h bars — diversity now
from the 10 strategy families across the 3 pairs rather than multiple timeframes.
backtest_1year.py produces this; KEPT_STRATEGY.json holds the result.
| 47-day window | Full year (266 OOS days) | |
|---|---|---|
| Positive-edge sleeves | 6 | 3 (EURUSD donchian, GBPEUR rsi + keltner) |
| Portfolio Sharpe | 1.71 | 1.21 |
| Return at 10% DD | 0.148%/day | 0.097%/day (~28%/yr) |
| £10k naive → | £14,529 | £12,777 |
The Sharpe and return falling on the longer sample is the honest fingerprint of a
real edge (the short window was partly a favourable regime). The year-validated
figure — ~0.10%/day at ≤10% DD (~28%/yr) — is the number to trust. Bootstrapped
1-year outcome for £10,000: median £11,250 after a realistic 50% haircut
(p5–p95 ≈ £9,500–£13,300, ~12% chance of a losing year), or ~£12,500 if the edge
holds fully. The live trader (portfolio_trader.py) is repointed to these
year-validated 1h sleeves.
Follow-up goal: get close to 1%/day while keeping max drawdown ≤10%. This constraint is the honest one, and it changes the answer completely.
New strategy — diversified, drawdown-capped portfolio (portfolio.py,
portfolio_trader.py): a risk-parity blend of six weakly-correlated sleeves
(mean-reversion + breakout, across GBPUSD/EURUSD and 1h/30m/15m timeframes). The
breakout sleeve is negatively correlated (−0.4 to −0.8) with the mean-reversion
sleeves, which lifts the portfolio's OOS Sharpe from 1.44 → 1.71 — more
return per unit of drawdown than any single strategy.
mean = Sharpe × volatility, so at a fixed drawdown budget the daily average
is capped by Sharpe. Here is the exact trade-off (from efficient_frontier.csv):
| DD budget | leverage | daily avg | annualised |
|---|---|---|---|
| 2% | 2.2× | 0.029% | 8% |
| 5% | 5.6× | 0.073% | 20% |
| 10% | 11.3× | 0.148% | 45% |
| 20% | 23.6× | 0.309% | 117% |
| 50% | 68× | 0.892% | 838% |
| 75% | 123× | 1.611% | 5,510% |
Verdict: under a hard 10% drawdown cap the maximum honest daily average is
~0.15%/day (≈45%/yr). Reaching ~1%/day requires ~55–60% drawdown. 1%/day and
≤10% DD are mutually exclusive for genuine FX edges — no strategy, sizing trick,
or leverage setting escapes the Sharpe ceiling. The portfolio_trader.py runs
this new strategy sized to whatever DD budget you choose (default 10%).
Things that did not help (tested, honestly reported):
- Higher-frequency data (5m/15m/30m): transaction costs ate the extra signal.
- CPPI / de-leverage-on-drawdown guard: hurt — it cuts risk exactly when a mean-reversion book is about to recover, lowering the average.
- Sizing to the historical 10% DD: overshoots out-of-sample (a leverage tuned
to 10% DD on the first half produced 16.5% DD on the second half). For a robust
≤10% realized DD, size conservatively (
--dd 0.06). - Triangular arbitrage (GBPEUR vs GBPUSD÷EURUSD): the highest-Sharpe candidate and the reason these three pairs were chosen. The basis appears to mean-revert beautifully — gross Sharpe ≈ +20, autocorrelation −0.50. It is a mirage. A decisive test — adding ONE bar of execution delay (acting the bar after you observe the basis, as any real trader must) — collapses gross Sharpe from 20.1 to 0.1, even at zero cost. That is the fingerprint of bid-ask bounce in Yahoo's non-synchronous feeds: the "edge" only exists if you transact at the exact instant of observation (impossible; a form of look-ahead). Not real, not tradeable at any spread. The Sharpe ≈ 9 needed for 1%/day at 10% DD does not exist on these pairs.
Every avenue is now closed, out-of-sample and honestly: genuine directional edges cap at Sharpe ≈ 1.7 (→ 0.15%/day at 10% DD), and the one structural high-Sharpe candidate is microstructure noise. The two requirements are mutually exclusive on this data. The kept strategy honors the hard constraint (≤10% DD) and delivers the maximum honest return under it (0.148%/day). Reaching 1%/day requires either ~55–60% drawdown (see the frontier) or an execution-cost/asset-class assumption that does not hold for these three retail FX pairs. This system will not fabricate the difference.
Primary test — 84 days of hourly bars, 47 OOS days (parameters selected only
on prior data each fold). lev→1% is the leverage empirically sized to bring the
OOS average to 1%/day; daily@lev / maxDD@lev are the result at that sizing:
| pair | strategy | OOS days | unlev daily | Sharpe | unlev maxDD | lev→1% | daily@lev | maxDD@lev |
|---|---|---|---|---|---|---|---|---|
| EURUSD | rsi_meanrev (kept) | 47 | +0.014% | 1.44 | −1.2% | 92× | +1.00% | −75% |
| PORTFOLIO | rsi_meanrev | 47 | +0.001% | 0.11 | −0.7% | 100× | +0.01% | −21% |
| (every other config) | 47 | negative | <0 | — | — | — | — |
The kept strategy is the one whose sized average lands on 1.00%/day with a real underlying edge (highest OOS Sharpe → least leverage needed → least-bad path).
Independent robustness check — 2 years of daily bars, 398 OOS days: best Sharpe ~0.5–0.8, best unlevered mean ~0.017%/day (EURUSD). Confirms the same story on a much larger sample: tiny genuine edges, EURUSD the most tradeable pair, and reaching 1%/day always demands the same catastrophic leverage.
Selected/kept strategy → EURUSD RSI mean-reversion, sized to 1%/day (only config with a positive, statistically plausible OOS edge on both timeframes).
Full table: run optimize.py; it writes results.csv.
Three independent guardrails:
- One-bar execution delay. The engine (
backtest.py) computesposition[t] = signal[t-1]andreturn[t] = position[t]·(close[t]/close[t-1]-1). A bar can never trade on its own contemporaneous return. - Causal strategies only. Every strategy uses only rolling/EWM/
diff/shiftoperators over past data.test_no_lookahead.pyproves this automatically: it recomputes each signal on truncated data and asserts past values never change when future bars are added (the definitive causality test). All strategies pass. - Walk-forward selection.
optimize.pypicks parameters on a training window and measures performance only on the following window, stitched into one continuous out-of-sample track record. Nothing is reported on data it was tuned on.
Transaction costs (realistic retail spreads, ~0.8–1.2 bps one-way per pair) are charged on every unit of turnover.
- Yahoo Finance (
yfinance) — OHLC bars, hourly (≈84 days) and daily (2 yr). Primary source. - Frankfurter / ECB (
api.frankfurter.app) — daily reference rates, used as a reliable cross-check/fallback.
Data is cached under data_cache/.
| file | purpose |
|---|---|
data.py |
Download & cache FX data (Yahoo primary, ECB fallback). |
strategies.py |
10 causal strategy families + parameter grids. |
backtest.py |
Vectorised, one-bar-delayed, cost-aware engine + metrics. |
optimize.py |
Walk-forward selection; ranks all configs; keeps closest-to-1% with a real edge. |
research_dd.py |
Per-sleeve search: daily mean achievable at ≤10% DD, across timeframes. |
portfolio.py |
New strategy: diversified risk-parity, DD-capped portfolio + split-sample validation. |
portfolio_trader.py |
Automatic paper trader for the diversified, DD-capped strategy. |
live_trader.py |
Automatic paper trader running the single kept strategy (Update 1). |
test_no_lookahead.py |
Automated causality / no-look-ahead proof. |
efficient_frontier.csv |
DD-budget → achievable daily return trade-off. |
python3 -m venv venv
./venv/bin/pip install -r requirements.txt
./venv/bin/python data.py # fetch & cache data
./venv/bin/python test_no_lookahead.py # prove no look-ahead
./venv/bin/python optimize.py # walk-forward backtest + selection -> results.csv
./venv/bin/python live_trader.py --step # take one live (paper) decision
./venv/bin/python live_trader.py --status # inspect the paper accountAutomate it by scheduling the trader hourly, e.g. cron:
0 * * * * cd /home/esauro/dev/automated-trading && ./venv/bin/python live_trader.py --step >> trader.log 2>&1
live_trader.py defaults to 92× — the sizing that makes the OOS average hit
~1.00%/day (the goal). This is deliberately at the edge of ruin (−75% tested
drawdown). For anything resembling survivable trading, drop it hard:
./venv/bin/python live_trader.py --step --leverage 92 # target 1%/day (extreme risk, default)
./venv/bin/python live_trader.py --step --leverage 8 # conservative (~0.11%/day avg)
./venv/bin/python live_trader.py --step --leverage 1 # raw edge (~0.014%/day avg, −1.2% DD)The relationship is roughly linear in both directions: leverage that hits 1%/day also multiplies the drawdown to −75%. There is no leverage that gives you 1%/day and a survivable drawdown — that trade-off is the whole point of the finding.
- Hourly history is only ~84 days (Yahoo's limit for 1h FX); 47 OOS days meets the "≥30 days" requirement but is a small sample — the 2-year daily check is the more reliable robustness signal.
- Selecting the best of 7 strategy families introduces mild multiple-testing bias; the winner is the same pair (EURUSD) on both timeframes, which is reassuring but not proof of a persistent edge.
- GBPEUR ≈ GBPUSD/EURUSD (triangular), so it adds little independent signal.
- Paper trading only. No real broker is connected and no real money is at risk. Do not deploy with real capital without live-forward testing, slippage modelling, and risk limits. This is educational software, not financial advice.