TrendFollowingBot¶

A pragmatic trend-following strategy that pairs EMA trend detection with two simple, complementary entry patterns:

  1. a crossback into trend (price crosses the EMA and the very next bar opens on the trend side), and
  2. an ATR-scaled breakout away from the EMA (current open is beyond the EMA by k × ATR).

Exits (initial stop, trail/TP) and sizing are delegated to the shared bot base via your chosen ExitStrategy (e.g., TrailingATRExit, FixedRatioExit), so this class focuses purely on when to enter. It reads signals from the previous closed bar and places orders at the current bar open to avoid look-ahead. :contentReference[oaicite:0]{index=0}


Overview¶

  • Goal: Participate in sustained moves while avoiding most chop by requiring either a confirmed cross of the trend filter (EMA) or enough volatility-adjusted distance from it.
  • Trend filter: Exponential moving average of length trend_ema_span (default 50).
  • Volatility gate: ATR from the prior closed bar; breakout_atr_mult (default 1.5) scales how far from the EMA price must be to count as a breakout.
  • Signal timing: Compute on **bar *t-1; place the order at the **open of bar *t (no look-ahead).
  • Exits & sizing: Inherited from the base strategy (risk %, SL/TP policy, trailing logic).
  • Data requirements: The dataframe must include an EMA column named ema_{trend_ema_span} (e.g., ema_50) and an ATR series used by the base bot.

Algorithm & Entry Logic¶

Let:

  • ema_prev = EMA value on the previous (closed) bar
  • prev_close = prior bar’s close
  • entry_open = current bar open (where your order will execute)
  • atr_prev = prior bar’s ATR (from the base helper)

1) Crossback into trend

  • Long: prev_close ≤ ema_prev and entry_open > ema_prev → buy
  • Short: prev_close ≥ ema_prev and entry_open < ema_prev → sell

This insists the trend filter is actually crossed and that the very next open is on the trend side—reducing whipsaw from intrabar pokes.

2) EMA-±(k × ATR) breakout
Compute a volatility buffer: threshold = breakout_atr_mult × atr_prev.

  • Long: entry_open > ema_prev + threshold → buy
  • Short: entry_open < ema_prev − threshold → sell

If neither rule fires, the bot stays flat for that bar.


Key Parameters¶

Param Default What it does
trend_ema_span 50 Length of the EMA trend filter (expects a column named ema_50, etc.).
breakout_atr_mult 1.5 Volatility buffer (in ATRs) required for the EMA-distance breakout entries.

Tuning tips: Smaller breakout_atr_mult captures more breakouts (higher turnover, more noise). Larger values trade less but target stronger impulses. Shorter trend_ema_span reacts faster (more trades), longer spans are steadier.


Notes on Exits & Risk¶

This bot only decides side/entry. Stops, trailing, take-profits, and position sizing are handled by the shared base (BaseStrategyBot) through the configured ExitStrategy—so you can mix and match exits (e.g., ATR-trailing) without rewriting entry logic. :contentReference[oaicite:1]{index=1}

Assests Included in Portfolio¶

Assest name Notebook symbol (example) Comment
Gold GC=F COMEX Gold
Silver SI=F COMEX Silver
U.S. Bonds ZB=F 30-Year Treasury Bond
Crude Oil CL=F NYMEX WTI
Soybeans ZS=F CBOT Soybeans
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¶

In [1]:
# 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
In [2]:
!./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.
In [3]:
# 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).

In [4]:
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
In [5]:
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
from bots.trend_following_bot.trend_following_bot import TrendFollowingBot

# Exits
from bots.exit_strategies import TrailingATRExit, FixedRatioExit

Build the exit strategy¶

In [6]:
exit_strategy = TrailingATRExit(atr_multiple=3.0)

Build the bot¶

In [7]:
bot = TrendFollowingBot(
    exit_strategy=exit_strategy,
    base_risk_percent=0.01,
    enforce_sessions=False,
    flatten_before_maintenance=True,
    enable_online_learning=False
)

Initialize engine and environment¶

In [8]:
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).

In [9]:
launch_gui(env, api)

Backtesting Results¶

Show Statistics¶

In [10]:
# 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 1.545871e+06 0.0 15.238082
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 83 38 45 38 45 0.457831 11652.302632 -7216.388890 1.363524 1422.289156 2100.0 249.0 0.069206
1 CL=F 72 26 46 40 32 0.361111 13742.307693 -6131.956521 1.266707 1044.861112 472.0 216.0 0.108080
2 6E=F 82 31 51 36 46 0.378049 11842.943546 -7097.058825 1.014315 63.185974 1368.0 246.0 0.118169
3 GC=F 100 39 61 55 45 0.390000 15513.333341 -6805.901640 1.457318 1898.600003 820.0 300.0 0.059000
4 LE=F 88 43 45 54 34 0.488636 11444.186049 -8417.555555 1.299137 1287.613638 1836.0 264.0 0.110060
5 SI=F 138 49 89 73 65 0.355072 11692.857146 -6729.775281 0.956591 -188.405796 1116.0 414.0 0.194345
6 ZS=F 81 28 53 40 41 0.345679 17283.035713 -7013.207546 1.301924 1385.493827 1400.0 243.0 0.127162
7 ZB=F 76 28 48 38 38 0.368421 11052.455355 -7070.963540 0.911794 -393.914473 776.0 228.0 0.107688

Show Equity Curve¶

In [11]:
# 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()
No description has been provided for this image
In [ ]: