Initial commit: multi-symbol bot with backtest engine and RSI trend strategy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 21:09:12 +02:00
commit ad8dfa27d7
32 changed files with 2644 additions and 0 deletions
View File
+138
View File
@@ -0,0 +1,138 @@
"""
Trend-Filtered RSI Pullback Strategy
=====================================
Core idea
---------
Use the H1 EMA(50)/EMA(200) crossover to establish the macro trend, then
enter M15 trades only in the trend direction when RSI pulls back to an
extreme and *exits* that extreme. This turns RSI from a counter-trend tool
into a high-probability trend-entry timer.
Entry logic
-----------
LONG : H1 EMA(50) > EMA(200) [bullish bias] AND M15 RSI crosses back
above `os_level` from below (RSI was oversold, now recovering).
SHORT : H1 EMA(50) < EMA(200) [bearish bias] AND M15 RSI crosses back
below `ob_level` from above (RSI was overbought, now rolling over).
Exits
-----
TP : entry ± tp_atr × ATR(14)
SL : entry ∓ sl_atr × ATR(14)
Session filter
--------------
Only trades during [session_start, session_end) UTC to avoid thin
Asian-overnight and weekend conditions.
"""
from __future__ import annotations
from typing import Optional
import pandas as pd
from engine import indicators as ind
class RSITrendStrategy:
def __init__(
self,
rsi_period: int = 14,
atr_period: int = 14,
os_level: float = 30.0, # oversold threshold
ob_level: float = 70.0, # overbought threshold
sl_atr: float = 1.5,
tp_atr: float = 3.0,
trend_fast: int = 50, # H1 EMA periods
trend_slow: int = 200,
session_start: int = 7, # UTC hour (inclusive)
session_end: int = 20, # UTC hour (exclusive)
):
self.rsi_period = rsi_period
self.atr_period = atr_period
self.os_level = os_level
self.ob_level = ob_level
self.sl_atr = sl_atr
self.tp_atr = tp_atr
self.trend_fast = trend_fast
self.trend_slow = trend_slow
self.session_start = session_start
self.session_end = session_end
# ── Strategy protocol ─────────────────────────────────────────────────────
def prepare(self, df: pd.DataFrame) -> pd.DataFrame:
# M15 indicators
df["rsi"] = ind.rsi(df["close"], self.rsi_period)
df["atr"] = ind.atr(df, self.atr_period)
# Build H1 trend from M15 data (resample, compute EMAs, forward-fill)
h1 = df["close"].resample("1h", label="left", closed="left").last().dropna()
h1_ema_fast = ind.ema(h1, self.trend_fast)
h1_ema_slow = ind.ema(h1, self.trend_slow)
df["h1_ema_fast"] = h1_ema_fast.reindex(df.index, method="ffill")
df["h1_ema_slow"] = h1_ema_slow.reindex(df.index, method="ffill")
# EMA spread: positive = bullish, negative = bearish
df["trend"] = df["h1_ema_fast"] - df["h1_ema_slow"]
return df
def signal(
self,
df: pd.DataFrame,
i: int,
) -> Optional[tuple[str, float, float]]:
if i < 1:
return None
row = df.iloc[i]
prev = df.iloc[i - 1]
# guard: need valid indicators
if pd.isna(row["rsi"]) or pd.isna(row["atr"]) or pd.isna(row["trend"]):
return None
if pd.isna(prev["rsi"]):
return None
# session filter
hour = row.name.hour
if not (self.session_start <= hour < self.session_end):
return None
rsi_now = row["rsi"]
rsi_prev = prev["rsi"]
trend = row["trend"]
atr = row["atr"]
# entry is on next bar's open — approximate SL/TP from current close
ref = row["close"]
# LONG: bullish trend AND RSI exits oversold
if trend > 0 and rsi_prev < self.os_level and rsi_now >= self.os_level:
sl = ref - self.sl_atr * atr
tp = ref + self.tp_atr * atr
return "long", sl, tp
# SHORT: bearish trend AND RSI exits overbought
if trend < 0 and rsi_prev > self.ob_level and rsi_now <= self.ob_level:
sl = ref + self.sl_atr * atr
tp = ref - self.tp_atr * atr
return "short", sl, tp
return None
def describe(self, params: Optional[dict] = None) -> str:
p = params or {}
return (
f"RSITrend("
f"os={p.get('os_level', self.os_level)}, "
f"ob={p.get('ob_level', self.ob_level)}, "
f"sl={p.get('sl_atr', self.sl_atr)}, "
f"tp={p.get('tp_atr', self.tp_atr)}, "
f"fast={p.get('trend_fast', self.trend_fast)}, "
f"slow={p.get('trend_slow', self.trend_slow)})"
)