Visualization from bbycroft.net/llm – Annotated with Nano Banana
Welcome to the LLM Architecture Series
This comprehensive 20-part series takes you from the fundamentals to advanced concepts in Large Language Model architecture. Using interactive visualizations from Brendan Bycroft’s excellent LLM Visualization, we explore every component of a GPT-style transformer.
A comprehensive 10-part course teaching you to build, backtest, and optimize an algorithmic trading strategy combining Ichimoku Cloud with EMA trend filtering.
“Only one issue is that we have only 13 trades in here and that is the main weak point of this strategy. It is very selective.”
This is actually a feature:
Fewer trades = lower commission costs
Each trade has high conviction
Manageable for manual oversight
2. Better for Trending Markets
“I do not think Forex is the best place to test this strategy… It works better on stocks, especially winning stocks.”
The strategy excels when:
Markets have clear trends
Retracements to cloud are clean
Less choppy price action
3. Best as a Signal Generator
“Ideally, we would deploy this strategy online live, but it will not trade on its own. It will actually send you signals… emails or notifications whenever the pattern is there and then you can jump in.”
The 4-hour timeframe means:
No rush to enter
Time to verify setup manually
Filter out borderline signals
Recommended Deployment
Option 1: Alert System
Strategy runs on server → Detects signal → Sends email/Telegram alert → You verify and enter manually
Option 2: Semi-Automated
Strategy generates signal → Places order with your manual approval → Manages SL/TP automatically
Ideas for Improvement
1. Filter Long Candles
“It would be good to add one additional filter where if candles are way too long, we are going to discard the signal because we want to squeeze the best risk-reward ratio.”
# Reject signals where entry candle is too large
max_candle_atr = 1.5
candle_range = abs(df["Close"] - df["Open"])
valid_entry = candle_range < (df["ATR"] * max_candle_atr)
2. Add Volume Confirmation
Higher volume on the bounce confirms institutional interest.
3. Multiple Timeframe Analysis
Use daily chart for trend, 4H for entry.
4. Asset-Specific Optimization
Each instrument has different optimal parameters.
Final Thoughts
This Ichimoku + EMA strategy provides:
✔ Clear, mechanical rules
✔ Trend-following with retracement entries
✔ Proper risk management
✔ Realistic backtesting (no look-ahead bias)
The 28-43% returns with controlled drawdowns make this a solid addition to any trader toolkit.
The optimization produces a heat map showing returns for each parameter combination.
Key Pattern: The Diagonal
“Notice those ridges, those clusters of returns… It is showing this decreasing slope. And this is totally normal.”
Why the diagonal?
High ATR multiplier = wider stop-loss → needs lower R:R
Low ATR multiplier = tighter stop-loss → can use higher R:R
“Either you have a high stop-loss distance and a low risk-reward ratio, OR you have a low ATR multiplier and a high risk-reward ratio — which actually is working the best for this strategy.”
The backtesting library uses a class-based approach where:
init() runs once at the start
next() runs on every candle
Complete Strategy Class
from backtesting import Strategy
class SignalStrategy(Strategy):
"""
Ichimoku + EMA trend-following strategy.
Entry: Pre-computed signal column (+1 long, -1 short)
Exit: ATR-based SL and RR-based TP
"""
# Class-level parameters (can be optimized)
atr_mult_sl: float = 1.5 # SL distance = ATR x this
rr_mult_tp: float = 2.0 # TP distance = SL x this
def init(self):
"""Initialize indicators (we pre-compute, so nothing needed here)."""
pass
def next(self):
"""Called on every bar. Check for signals and manage positions."""
i = -1 # Current bar
signal = int(self.data.signal[i]) # +1 long, -1 short, 0 none
close = float(self.data.Close[i])
atr = float(self.data.ATR[i])
# Safety check
if not (atr > 0):
return
# --- Manage open trades ---
if self.position:
# Let SL/TP handle exits automatically
return
# --- New entry logic ---
sl_dist = atr * self.atr_mult_sl
tp_dist = sl_dist * self.rr_mult_tp
if signal == 1: # LONG entry
sl = close - sl_dist
tp = close + tp_dist
self.buy(size=0.99, sl=sl, tp=tp)
elif signal == -1: # SHORT entry
sl = close + sl_dist
tp = close - tp_dist
self.sell(size=0.99, sl=sl, tp=tp)
Key Design Decisions
1. Pre-Computed Signals
We calculate signals BEFORE backtesting (in pandas), then the strategy just reads them. This is cleaner and faster.
2. Position Check
if self.position:
return
We do not stack trades — one position at a time.
3. Size = 0.99
self.buy(size=0.99, sl=sl, tp=tp)
Using 99% of available equity leaves room for rounding.
from __future__ import annotations
import warnings
warnings.filterwarnings("ignore", category=FutureWarning)
import numpy as np
import pandas as pd
import pandas_ta as ta # Technical analysis
import yfinance as yf # Free market data
from backtesting import Backtest, Strategy
“The best set of parameters is decreasing like this. Either you have a high stop-loss distance and a low risk-reward ratio, or you have a low ATR multiplier or stop-loss distance and a high risk-reward ratio — which actually is working the best for this strategy.”
Why Tight Stops Work Here
Because we are entering at cloud bounces (retracements):
“We are squeezing our entry position to the retracement to the minimum of the retracement when we are dipping within inside of the cloud and just getting out of it. So this is why you do not need a very wide stop-loss distance.”
Code Implementation
# Risk settings
ATR_LEN = 14
ATR_MULT_SL = 1.5 # Tight stop-loss
ATR_MULT_TP = 3.0 # Higher R:R (2R)
# In strategy:
sl_dist = atr * self.atr_mult_sl
tp_dist = sl_dist * self.rr_mult_tp
if signal == 1: # Long entry
sl = close - sl_dist
tp = close + tp_dist
self.buy(size=0.99, sl=sl, tp=tp)
elif signal == -1: # Short entry
sl = close + sl_dist
tp = close - tp_dist
self.sell(size=0.99, sl=sl, tp=tp)
Coming Up Next: Our rules are defined — let us build the Python backtesting infrastructure to test them. Continue to Part 7 →
We now combine the EMA trend filter with Ichimoku cloud conditions to find high-probability entries.
Entry Rules for LONG Position
Step 1: Confirm Uptrend (EMA Filter)
EMA_signal == +1 (at least 7 candles fully above EMA 100)
Step 2: Confirm Momentum (Ichimoku Cloud)
Within the last 10 candles, at least 7 must be FULLY ABOVE the cloud
“Fully above” means: Open > cloud_top AND Close > cloud_top
Step 3: The Entry Trigger (Cloud Pierce)
Current candle opens INSIDE the cloud AND closes ABOVE it
This represents a retracement that is bouncing back into the trend direction.
Entry Rules for SHORT Position
Mirror image:
EMA_signal == -1
At least 7 of 10 candles fully BELOW cloud
Current candle opens inside cloud, closes BELOW
Why These Rules Work
“We are trading with a trend and we are trying to capture patterns where candles are also above the Ichimoku cloud confirming a strong momentum. But then we look for a candle dipping or bouncing in and out of the cloud because we are looking for some kind of a retracement.”
The key insight:
“When the candle closes above the cloud, we assume that the retracement is over and the price will probably continue in the direction of the main trend.”
The Ichimoku Cloud alone can generate signals in choppy markets. The EMA filter ensures we only trade when a clear trend is established.
The Rule: EMA 100
We use a 100-period Exponential Moving Average as our trend identifier.
Long Trend Conditions
Current candle + at least 5 previous candles ALL trading FULLY ABOVE the EMA
“Fully above” means:
Open > EMA
Close > EMA
Short Trend Conditions
Current candle + at least 5 previous candles ALL trading FULLY BELOW the EMA
Why 5+ Candles?
From the source material:
“We need the current candle plus at least five previous candles all trading fully above the EMA curve. Meaning both the open and close of each of these candles are above the line.”
This confirms we are in a sustained trend, not just a momentary spike.
Code Implementation
def MovingAverageSignal(df: pd.DataFrame, back_candles: int = 5) -> pd.DataFrame:
"""
Add a single-column EMA trend signal to the DataFrame.
Rules (evaluated per bar, using *only* current/past data):
+1 (uptrend): For the window [t-back_candles .. t], EVERY bar has
Open > EMA and Close > EMA.
-1 (downtrend): For the same window, EVERY bar has
Open < EMA and Close < EMA.
0 otherwise.
"""
out = df.copy()
# Window size: current bar + back_candles bars behind it
w = int(back_candles) + 1
# Booleans per-bar relative to EMA
above = (out["Open"] > out["EMA"]) & (out["Close"] > out["EMA"])
below = (out["Open"] < out["EMA"]) & (out["Close"] < out["EMA"])
# "All true in the last w bars" via rolling sum == w
above_all = (above.rolling(w, min_periods=w).sum() == w)
below_all = (below.rolling(w, min_periods=w).sum() == w)
# Single signal column
signal = np.where(above_all, 1, np.where(below_all, -1, 0)).astype(int)
out["EMA_signal"] = signal
return out
# Usage: require 7 candles above/below EMA
df = MovingAverageSignal(df, back_candles=7)
Tuning the Parameter
Setting
Effect
back_candles=5
More signals, less trend confirmation
back_candles=7
Balanced (recommended)
back_candles=10
Fewer signals, stronger trend confirmation
Coming Up Next: In Part 5, we will define our exact entry conditions — when the EMA trend is confirmed AND the Ichimoku cloud gives us a retracement signal. Continue to Part 5 →
The cloud projects forward, showing where support/resistance WILL BE.
Critical: Avoiding Look-Ahead Bias
The Problem
Standard Ichimoku implementations shift Span A and Span B 26 periods into the future. In backtesting, this means your strategy “knows” future support/resistance levels — data leakage!
Our Solution
# We use UNshifted spans for signal logic
span_a_raw = (tenkan_line + kijun_line) / 2.0 # raw (no forward shift)
span_b_raw = (h.rolling(senkou_b).max() + l.rolling(senkou_b).min()) / 2.0 # raw
From the source material:
“I decided to compute the Ichimoku manually for one reason… The Ichimoku by default shifts or reads a bit in the future. This would be a look-ahead bias for our backtesting.”
When the cloud changes color (Span A and Span B cross), it signals a potential trend reversal. This is called a Kumo Twist or Senkou Span Cross.
Bullish Twist: Span A crosses above Span B → Cloud turns green
Bearish Twist: Span A crosses below Span B → Cloud turns red
Coming Up Next: The cloud tells us about support and resistance, but we need a trend filter to avoid false signals. In Part 4, we add our EMA Trend Filter. Continue to Part 4 →
Strongest: When cross happens above the cloud (bullish) or below (bearish)
Chikou Confirmation
Bullish: Chikou is above price from 26 periods ago
Bearish: Chikou is below price from 26 periods ago
Coming Up Next: In Part 3, we will explore the Kumo Cloud itself — how to read bullish vs bearish clouds and why cloud thickness matters. Continue to Part 3 →