Moving Average Bot Development (MA/EMA)
Moving Averages (MA) are a basic and one of the most reliable technical analysis tools. A bot based on MA/EMA generates signals from crossovers and price position relative to the average. Simple to implement, clear in logic, and with proper parameterization provides stable results on trending markets.
MA vs EMA: What's the Difference
SMA (Simple MA) — simple average over N periods. All candles have equal weight.
EMA (Exponential MA) — weighted average, recent candles have more weight. Reacts faster to price changes.
import pandas as pd
def sma(close: pd.Series, period: int) -> pd.Series:
return close.rolling(period).mean()
def ema(close: pd.Series, period: int) -> pd.Series:
return close.ewm(span=period, adjust=False).mean()
For trading, EMA is preferable: it signals trend reversals faster.
Classic Strategies
Golden Cross / Death Cross
Crossing of EMA 50 and EMA 200:
- Golden Cross: EMA50 crosses EMA200 from below → bullish signal
- Death Cross: EMA50 crosses EMA200 from above → bearish signal
class GoldenCrossStrategy:
def generate_signal(self, df: pd.DataFrame) -> str:
ema_fast = ema(df['close'], 50).shift(1)
ema_slow = ema(df['close'], 200).shift(1)
# Crossover: different relationship on previous candle than current
prev_fast = ema_fast.iloc[-2]
prev_slow = ema_slow.iloc[-2]
curr_fast = ema_fast.iloc[-1]
curr_slow = ema_slow.iloc[-1]
if prev_fast <= prev_slow and curr_fast > curr_slow:
return 'BUY' # Golden Cross
if prev_fast >= prev_slow and curr_fast < curr_slow:
return 'SELL' # Death Cross
return 'HOLD'
Triple EMA Strategy (9/21/55)
Three averages provide more confirmations:
class TripleEMAStrategy:
def generate_signal(self, df: pd.DataFrame) -> str:
e9 = ema(df['close'], 9).shift(1)
e21 = ema(df['close'], 21).shift(1)
e55 = ema(df['close'], 55).shift(1)
last_9 = e9.iloc[-1]
last_21 = e21.iloc[-1]
last_55 = e55.iloc[-1]
price = df['close'].iloc[-1]
# Bullish signal: all EMAs aligned in order
if last_9 > last_21 > last_55 and price > last_9:
return 'BUY'
# Bearish signal: reverse order
if last_9 < last_21 < last_55 and price < last_9:
return 'SELL'
return 'HOLD'
Bot Implementation
class MABot:
def __init__(self, strategy, exchange_client, config):
self.strategy = strategy
self.exchange = exchange_client
self.config = config
self.position = None
self.candles = []
async def on_candle(self, candle: dict):
self.candles.append(candle)
if len(self.candles) > 300:
self.candles = self.candles[-300:] # keep history
if len(self.candles) < 210: # need warmup for EMA 200
return
df = pd.DataFrame(self.candles)
signal = self.strategy.generate_signal(df)
if signal == 'BUY' and not self.position:
order = await self.exchange.place_market_order(
self.config.symbol, 'buy', self.config.position_size
)
self.position = {'entry': order.fill_price, 'side': 'long'}
elif signal == 'SELL' and self.position and self.position['side'] == 'long':
await self.exchange.place_market_order(
self.config.symbol, 'sell', self.config.position_size
)
pnl = (order.fill_price - self.position['entry']) / self.position['entry'] * 100
logger.info(f"Closed long, PnL: {pnl:.2f}%")
self.position = None
Parameterization and Optimization
Optimal EMA periods depend on timeframe:
| Timeframe | Fast EMA | Slow EMA | Application |
|---|---|---|---|
| 1h | 9 | 21 | Intraday |
| 4h | 21 | 55 | Swing |
| Daily | 50 | 200 | Golden/Death Cross |
| Weekly | 20 | 50 | Long-term |
MA-strategies perform poorly on sideways markets — they give many false signals. Add ADX (Average Directional Index) filter: trade only when ADX > 25 (market is in trend). This reduces trade count but improves quality.







