Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Automated FX Trading System — GBPUSD / EURUSD / GBPEUR

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.


⚠️ Headline finding: read this first

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.


Update 3 — backtest extended to a full year (266 OOS days)

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.


Update 2 — a NEW strategy that caps drawdown at ≤10%

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.

The efficient frontier (full OOS, risk-parity portfolio)

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 beautifullygross 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.

Final verdict on "1%/day at ≤10% DD"

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.


Results (walk-forward, out-of-sample)

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.


Why you can trust the backtest (no look-ahead)

Three independent guardrails:

  1. One-bar execution delay. The engine (backtest.py) computes position[t] = signal[t-1] and return[t] = position[t]·(close[t]/close[t-1]-1). A bar can never trade on its own contemporaneous return.
  2. Causal strategies only. Every strategy uses only rolling/EWM/diff/shift operators over past data. test_no_lookahead.py proves 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.
  3. Walk-forward selection. optimize.py picks 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.


Data sources (free, real)

  • 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/.


Files

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.

How to run

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 account

Automate 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

Choosing leverage

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.


Limitations & honesty notes

  • 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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages