Tom Basso’s “Coin-Flip” Experiment — Overview¶
Tom Basso’s coin-flip experiment showed that an edge in a trading system can come from risk management and exits, not clever entries. Instead of hunting for a perfect signal, Basso (working with Van K. Tharp in the 1990s) asked a sharper question: what happens if entries are random but exits and sizing are disciplined? Their tests across a diversified basket of futures suggested that combining a small, fixed risk per trade (≈1% of equity), volatility-based trailing stops (e.g., 3× ATR), and consistent rules can yield positive expectancy even when the entry is decided by chance.
Equally important, diversification across relatively uncorrelated markets was a major contributor to the results. By holding assets whose returns don’t move in lockstep (currencies, rates, energies, metals, grains, livestock), the portfolio benefits when one market draws down while another trends or mean-reverts. Uncorrelated streams:
- Dampen portfolio volatility
- Stabilize compounding (geometric returns) by avoiding large equity cliffs
- Often improve risk-adjusted performance
In short: profitability came not from prediction but from position sizing, risk management, and cross-market diversification: sizing kept the system alive through losing streaks, the risk management rules would cut losers while letting winners run, and diversification added uncorrelated returns that tempered drawdowns. This notebook mirrors that setup with a coin-flip bot, ~1% risk per trade, a 3 × ATR trailing stop, and a diversified futures basket, so you can see the same principles play out in your own results.
How the experiment was conducted¶
- Universe: 10 liquid futures markets spanning commodities, currencies, rates, and energy (this notebooke only includes 8 though).
- Entry: entirely random (flip a coin to choose long/short) for each market.
- Always in: when a position closed, the system stood ready to re-enter on the next bar using the same random entry logic.
- Evaluation: results tracked across all markets to observe the combined equity curve and distribution of wins/losses.
The rules that mattered (and that we reproduce here)¶
- Risk per trade: ~1% of equity (fixed-fractional position sizing).
- Volatility measure: 10-day EMA of ATR (a smoothed ATR).
- Initial stop: 3 × volatility (i.e., 3 × the 10-day EMA-ATR) from the entry price.
- Trailing exit: the same 3× ATR stop ratchets with price only in your favor (never loosening).
- Diversification: apply the identical rules across multiple, low-correlated futures to smooth outcomes.
Why it’s instructive: the outcome is the classic trend-following profile—many small losses, a handful of outsized winners—and, crucially, compounding over time when position sizing and exits are kept consistent. This notebook recreates that setup end-to-end with a coin-flip bot, ~1% risk per trade, and a 3× ATR trailing stop, run over a representative futures portfolio, with commissions, slippage, ticks, and margins modeled so you can see how systematic exits and sizing turn randomness into results.
What this notebook does: It recreates that setup end-to-end with a coin-flip bot, 1% risk per trade, and a 3× ATR trailing stop, run over a representative portfolio. We also model commissions, slippage, ticks, and margins, and surface clear diagnostics—per-asset stats, win rate, expectancy, drawdown, and a live equity curve—so you can see how systematic exits and sizing turn randomness into results.
Markets used in Basso’s test (10 futures)¶
- Gold
- Silver
- U.S. Bonds (long-term Treasury bond futures)
- Eurodollars (short-term interest rate futures)
- Crude Oil
- Soybeans
- Sugar
- Deutsche Mark (pre-euro currency future)
- British Pound (Pound sterling)
- Live Cattle
Notes: The Deutsche Mark was later replaced by the euro; Eurodollars have since largely been superseded by SOFR futures in modern markets.
Mapping to this notebook’s symbols (using 8 of the 10 contracts from Basso's Portfolio)¶
| Basso market | Notebook symbol (example) | Comment |
|---|---|---|
| Gold | GC=F |
COMEX Gold |
| Silver | SI=F |
COMEX Silver |
| U.S. Bonds | ZB=F |
30-Year Treasury Bond |
| Eurodollars | — | Not included here (SOFR analog) |
| Crude Oil | CL=F |
NYMEX WTI |
| Soybeans | ZS=F |
CBOT Soybeans |
| Sugar | — | Not included here |
| Deutsche Mark | 6E=F |
Euro FX as successor to DEM |
| British Pound | 6B=F |
British Pound FX |
| Live Cattle | LE=F |
CME Live Cattle |
Setup¶
# jump to repo root (fallback: parent if in notebooks/)
ROOT = !git rev-parse --show-toplevel 2>/dev/null
%cd {ROOT[0] if ROOT else '..'}
/home/dennis/Algo-Trading-Stack
!./setup/fetch_sample_portfolio_futures_data.sh
========== Last 10 years ========== ⏭️ Gold: already exists, skipping. ⏭️ Silver: already exists, skipping. ⏭️ Crude_Oil: already exists, skipping. ⏭️ Soybeans: already exists, skipping. ⏭️ Sugar: already exists, skipping. ⏭️ US_Treasury_Bonds: already exists, skipping. ⏭️ Euro: already exists, skipping. ⏭️ British_Pound: already exists, skipping. ⏭️ Live_Cattle: already exists, skipping. ========== Last 20 years ========== ⏭️ Gold: already exists, skipping. ⏭️ Silver: already exists, skipping. ⏭️ Crude_Oil: already exists, skipping. ⏭️ Soybeans: already exists, skipping. ⏭️ Sugar: already exists, skipping. ⏭️ US_Treasury_Bonds: already exists, skipping. ⏭️ Euro: already exists, skipping. ⏭️ British_Pound: already exists, skipping. ⏭️ Live_Cattle: already exists, skipping. ========== 2000 to 2015 ========== ⏭️ Gold: already exists, skipping. ⏭️ Silver: already exists, skipping. ⏭️ Crude_Oil: already exists, skipping. ⏭️ Soybeans: already exists, skipping. ⏭️ Sugar: already exists, skipping. ⏭️ US_Treasury_Bonds: already exists, skipping. ⏭️ Euro: already exists, skipping. ⏭️ British_Pound: already exists, skipping. ⏭️ Live_Cattle: already exists, skipping. ✅ All downloads complete.
# Enable autoreload (useful while iterating), and hook Qt into Jupyter
%load_ext autoreload
%autoreload 2
%gui qt
Project root & imports¶
Set the project root if your notebook isn't at the repo root. By default, we assume the notebook lives in the root (where classes/ and bots/ exist).
import sys, os, pathlib
PROJECT_ROOT = os.path.abspath('.')
if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT)
print('PROJECT_ROOT =', PROJECT_ROOT)
os.chdir(PROJECT_ROOT)
print("Current working directory:", os.getcwd())
PROJECT_ROOT = /home/dennis/Algo-Trading-Stack Current working directory: /home/dennis/Algo-Trading-Stack
from PyQt5 import QtWidgets
import gc
from classes.Backtester_Engine import BacktesterEngine
from classes.Trading_Environment import TradingEnvironment
from classes.ui_main_window import launch_gui
# Bots
from bots.coin_flip_bot.coin_flip_bot import CoinFlipBot
# Exits
from bots.exit_strategies import TrailingATRExit, FixedRatioExit
Build the exit strategy¶
exit_strategy = TrailingATRExit(atr_multiple=3.0)
Build the bot¶
bot = CoinFlipBot(
exit_strategy=exit_strategy,
base_risk_percent=0.01,
enforce_sessions=False,
flatten_before_maintenance=True,
enable_online_learning=False,
seed=42,
)
Initialize engine and environment¶
config_path = "backtest_configs/backtest_config_10_yrs.yaml"
api = BacktesterEngine(config_path=config_path)
api.connect()
env = TradingEnvironment()
env.set_api(api)
env.set_bot(bot)
# Initial indicator compute happens inside TradingEnvironment on connect.
print('Assets:', env.get_asset_list())
Assets: ['6B=F', 'CL=F', '6E=F', 'GC=F', 'LE=F', 'SI=F', 'ZS=F', 'ZB=F']
Launch GUI and Run Backtest¶
This starts the backtest control panel and charting UI. You can open charts, start/pause/restart, and view statistics.
If the window doesn't appear from within Jupyter, ensure you ran %gui qt above, or run this notebook locally (VS Code, JupyterLab).
launch_gui(env, api)
Backtesting Results¶
Show Statistics¶
# Minimal: pull stats from the running/backtested engine and show them inline
import pandas as pd
from IPython.display import display
stats = api.get_stats_snapshot() # live snapshot; safe to call anytime
# Portfolio (one row)
display(pd.DataFrame([{
"Initial Cash": stats["portfolio"].get("initial_cash", 0.0),
"Final Equity": stats["portfolio"].get("total_equity", 0.0),
"Used Margin": stats["portfolio"].get("used_margin", 0.0),
"Max Drawdown %": 100.0 * stats["portfolio"].get("max_drawdown", 0.0),
}]))
# Per-asset table
display(pd.DataFrame.from_dict(stats["per_asset"], orient="index").reset_index().rename(columns={"index":"Symbol"}))
| Initial Cash | Final Equity | Used Margin | Max Drawdown % | |
|---|---|---|---|---|
| 0 | 1000000.0 | 975162.999964 | 0.0 | 25.795293 |
| Symbol | trades | wins | losses | long_trades | short_trades | win_rate | avg_win | avg_loss | profit_factor | expectancy | commission_total | fee_total | max_drawdown | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 6B=F | 97 | 37 | 60 | 51 | 46 | 0.381443 | 10268.581082 | -5599.895833 | 1.130787 | 453.028351 | 1892.0 | 291.0 | 0.108263 |
| 1 | CL=F | 79 | 31 | 48 | 51 | 28 | 0.392405 | 8890.645164 | -4117.708332 | 1.394435 | 986.835445 | 380.0 | 237.0 | 0.087290 |
| 2 | 6E=F | 95 | 33 | 62 | 46 | 49 | 0.347368 | 9220.454545 | -4928.326613 | 0.995807 | -13.486843 | 1180.0 | 285.0 | 0.084019 |
| 3 | GC=F | 99 | 42 | 57 | 54 | 45 | 0.424242 | 12994.047623 | -5211.754387 | 1.837109 | 2511.919193 | 672.0 | 297.0 | 0.070470 |
| 4 | LE=F | 122 | 31 | 91 | 55 | 67 | 0.254098 | 9048.709679 | -6460.439562 | 0.477139 | -2519.590165 | 1992.0 | 366.0 | 0.315070 |
| 5 | SI=F | 150 | 52 | 98 | 71 | 79 | 0.346667 | 7839.423077 | -5719.897960 | 0.727232 | -1019.333334 | 1004.0 | 450.0 | 0.181125 |
| 6 | ZS=F | 93 | 36 | 57 | 48 | 45 | 0.387097 | 11621.527778 | -5680.921052 | 1.292029 | 1016.801076 | 1236.0 | 279.0 | 0.091750 |
| 7 | ZB=F | 88 | 35 | 53 | 37 | 51 | 0.397727 | 8010.714283 | -5608.490568 | 0.943230 | -191.761366 | 712.0 | 264.0 | 0.105281 |
Show Equity Curve¶
# Assuming `s` is the equity Series you already built
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter
# Times + equity (portfolio). Safe to call anytime; uses the engine's live history.
times, equity = api.get_equity_series() # None -> portfolio; pass a symbol for per-asset
n = min(len(times), len(equity))
if n == 0:
print("No equity data available yet.")
else:
s = pd.Series(equity[:n], index=pd.to_datetime(times[:n])).dropna()
# (Optional) smooth gaps like weekends/holidays:
s = s.resample("h").last().ffill()
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(s.index, s.values)
ax.set_title("Portfolio Equity")
ax.set_xlabel("Time"); ax.set_ylabel("Equity ($)")
ax.grid(True)
# Turn off scientific notation/offset and format with commas
ax.ticklabel_format(axis='y', style='plain', useOffset=False)
ax.yaxis.set_major_formatter(FuncFormatter(lambda x, pos: f'${x:,.0f}'))
fig.autofmt_xdate()
plt.show()