Trend Following
TSMomentum + MovingAverageCross ensemble
A classical trend-following setup using two trend indicators that catch moves at different timescales. TSMomentum fires on short-term returns; MovingAverageCross fires on longer trends.
The file
# trend_following.py
import horizon as hz
from horizon.asset_classes import AssetClass, Equity
from horizon.data import SyntheticRegimes
from horizon.discovery import StaticUniverse
from horizon.discovery.base import Market
from horizon.portfolio import KellyOptimizer
from horizon.quant import MovingAverageCrossStrategy, TSMomentum
from horizon.risk import RiskProfile, StopLoss
def main():
tickers = ["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN", "META"]
universe = StaticUniverse([
Market(id=t, asset_class=AssetClass.Equity) for t in tickers
])
# Regime-switching data. includes an uptrend for the trend strategies to exploit
data = SyntheticRegimes(
market_ids=tickers,
n_bars=400,
regimes=[
(0.40, 0.30, 0.18), # strong uptrend
(0.30, 0.00, 0.20), # chop
(0.30, -0.20, 0.30), # declining
],
seed=42,
)
result = hz.run(
mode="backtest",
strategies=[
TSMomentum(lookback=20, edge_bps=60, horizon_days=5),
MovingAverageCrossStrategy(fast=10, slow=30, edge_bps=40, horizon_days=10),
],
asset_classes=[Equity],
universe=universe,
portfolio=KellyOptimizer(
kelly_fraction=0.20, # diluted. 2 strategies
max_gross_leverage=1.5,
transaction_cost_bps=5,
),
risk=RiskProfile.moderate().override(
stop_loss=StopLoss(per_position_pct=0.10, trailing_pct=0.05),
),
data_source=data,
backtest=hz.BacktestConfig(initial_cash_usd=100_000),
)
print("=" * 60)
print("TREND FOLLOWING. TSMomentum + MovingAverageCross")
print("=" * 60)
print(f" Ticks: {len(result.equity_curve)}")
print(f" Trades: {result.n_trades}")
print(f" Start equity: ${result.equity_curve[0][1]:,.2f}")
print(f" End equity: ${result.equity_curve[-1][1]:,.2f}")
print(f" Total return: {result.total_return:+.2%}")
print(f" Sharpe: {result.sharpe:+.3f}")
print(f" Max drawdown: {result.max_drawdown:.2%}")
if __name__ == "__main__":
main()
Why these two together
When both agree, the portfolio optimizer sees stronger combined signal and sizes bigger. When they disagree (one sees trend, one doesn’t), the net is smaller. Automatic regime detection via signal agreement.
Add a trailing stop
The StopLoss(trailing_pct=0.05) override is important for trend following. Trend strategies ride winners: they need to give the position room to run, but cut when the trend dies. A trailing stop:
- Gives room when P&L is growing (stop moves up with the position)
- Locks in gains when P&L peaks
- Closes the position on a 5% reversal from peak
Expected behavior
On SyntheticRegimes with a 40% uptrend regime, trend strategies should capture most of the move. Sharpe will be positive: unlike pure GBM, the regime data has exploitable structure.
Run it
PYTHONPATH=. python3 trend_following.py