138 lines
4.9 KiB
Python
138 lines
4.9 KiB
Python
# strategies.py
|
||
from __future__ import annotations
|
||
import numpy as np
|
||
import pandas as pd
|
||
|
||
# ===== PRESET =====
|
||
BUY_TH = 0.55
|
||
SELL_TH = -0.55
|
||
REQUIRE_TREND_CONFLUENCE = True # EMA50 vs EMA200 musi się zgadzać z kierunkiem
|
||
USE_ADX_IN_FILTER = True # używaj ADX w filtrze kierunku
|
||
TREND_FILTER_ADX_MIN = 18.0
|
||
MTF_CONFLICT_DAMP = 0.6 # konflikt 1m vs 15m – osłab wynik
|
||
|
||
# ===== FILTRY DODATKOWE =====
|
||
SESSION_FILTER = True
|
||
SESS_UTC_START = 6 # handluj tylko 06:00–21:00 UTC (LON+NY)
|
||
SESS_UTC_END = 21
|
||
|
||
VOL_FILTER = True
|
||
MIN_ATR_PCT = 0.0005 # min ATR/price (0.05%)
|
||
MAX_ATR_PCT = 0.02 # max ATR/price (2%) – unikaj paniki
|
||
OVEREXT_COEF = 1.2 # nie wchodź, jeśli |close-EMA20| > 1.2*ATR
|
||
|
||
def _resample_ohlc(df: pd.DataFrame, rule: str = "15T") -> pd.DataFrame:
|
||
o = {"open":"first","high":"max","low":"min","close":"last"}
|
||
return df[["open","high","low","close"]].resample(rule).agg(o).dropna(how="any")
|
||
|
||
def _safe(df: pd.DataFrame, col: str, default=None):
|
||
return float(df[col].iloc[-1]) if col in df.columns else (float(default) if default is not None else None)
|
||
|
||
def _ema(series: pd.Series, span: int) -> float:
|
||
return float(series.ewm(span=span, adjust=False).mean().iloc[-1])
|
||
|
||
def _atr_pct(df: pd.DataFrame, n: int = 14) -> float:
|
||
if "atr" in df.columns:
|
||
atr = float(df["atr"].iloc[-1])
|
||
else:
|
||
h, l, c = df["high"], df["low"], df["close"]
|
||
prev_c = c.shift(1)
|
||
tr = pd.concat([h - l, (h - prev_c).abs(), (l - prev_c).abs()], axis=1).max(axis=1)
|
||
atr = float(tr.ewm(span=n, adjust=False).mean().iloc[-1])
|
||
price = float(df["close"].iloc[-1])
|
||
return 0.0 if price <= 0 else atr / price
|
||
|
||
def _in_session(df: pd.DataFrame) -> bool:
|
||
ts = pd.Timestamp(df.index[-1])
|
||
try:
|
||
hour = ts.tz_convert("UTC").hour if ts.tzinfo else ts.hour
|
||
except Exception:
|
||
hour = ts.hour
|
||
return SESS_UTC_START <= hour <= SESS_UTC_END
|
||
|
||
def _trend_score_1m(df: pd.DataFrame) -> float:
|
||
s = 0.0
|
||
ema20 = _safe(df, "ema20", _ema(df["close"], 20))
|
||
ema50 = _safe(df, "ema50", _ema(df["close"], 50))
|
||
s += 0.5 if ema20 > ema50 else -0.5
|
||
|
||
macd = _safe(df, "macd", _ema(df["close"],12) - _ema(df["close"],26))
|
||
macd_sig = _safe(df, "macd_sig", macd)
|
||
s += 0.3 if macd > macd_sig else -0.3
|
||
|
||
adx = _safe(df, "adx", None)
|
||
if adx is not None:
|
||
if adx >= 18: s += 0.2
|
||
elif adx <= 12: s -= 0.1
|
||
|
||
c = float(df["close"].iloc[-1])
|
||
st = _safe(df, "supertrend", c)
|
||
s += 0.2 if c > st else -0.2
|
||
|
||
up = _safe(df, "bb_up", c + 1e9); dn = _safe(df, "bb_dn", c - 1e9)
|
||
if c > up: s += 0.2
|
||
if c < dn: s -= 0.2
|
||
|
||
rsi = _safe(df, "rsi14", 50.0)
|
||
if rsi >= 70: s -= 0.2
|
||
if rsi <= 30: s += 0.2
|
||
return s
|
||
|
||
def _trend_score_15m(df_1m: pd.DataFrame) -> float:
|
||
try:
|
||
htf = _resample_ohlc(df_1m, "15T")
|
||
if len(htf) < 30: return 0.0
|
||
close = htf["close"]
|
||
ema20 = close.ewm(span=20, adjust=False).mean().iloc[-1]
|
||
ema50 = close.ewm(span=50, adjust=False).mean().iloc[-1]
|
||
macd_line = close.ewm(span=12, adjust=False).mean().iloc[-1] - close.ewm(span=26, adjust=False).mean().iloc[-1]
|
||
macd_sig = pd.Series(close).ewm(span=9, adjust=False).mean().iloc[-1]
|
||
s = (0.6 if ema20 > ema50 else -0.6) + (0.4 if macd_line > macd_sig else -0.4)
|
||
return float(s)
|
||
except Exception:
|
||
return 0.0
|
||
|
||
def _hysteresis_map(score: float) -> int:
|
||
if score >= BUY_TH: return 1
|
||
if score <= SELL_TH: return -1
|
||
return 0
|
||
|
||
def get_signal(df: pd.DataFrame) -> int:
|
||
if len(df) < 200:
|
||
return 0
|
||
|
||
# Filtry ex-ante
|
||
if SESSION_FILTER and not _in_session(df):
|
||
return 0
|
||
atrp = _atr_pct(df)
|
||
if VOL_FILTER and not (MIN_ATR_PCT <= atrp <= MAX_ATR_PCT):
|
||
return 0
|
||
|
||
s1 = _trend_score_1m(df)
|
||
s15 = _trend_score_15m(df)
|
||
score = 0.7 * s1 + 0.3 * s15
|
||
if (s1 > 0 and s15 < 0) or (s1 < 0 and s15 > 0):
|
||
score *= MTF_CONFLICT_DAMP
|
||
sig = _hysteresis_map(score)
|
||
|
||
# Nie handluj pod prąd + nie gonić świecy
|
||
if sig != 0 and REQUIRE_TREND_CONFLUENCE:
|
||
ema50 = _safe(df, "ema50", _ema(df["close"], 50))
|
||
ema200 = _safe(df, "ema200", _ema(df["close"], 200))
|
||
adx = _safe(df, "adx", None)
|
||
long_ok = (ema50 > ema200) and (not USE_ADX_IN_FILTER or adx is None or adx >= TREND_FILTER_ADX_MIN)
|
||
short_ok = (ema50 < ema200) and (not USE_ADX_IN_FILTER or adx is None or adx >= TREND_FILTER_ADX_MIN)
|
||
if sig == 1 and not long_ok: sig = 0
|
||
if sig == -1 and not short_ok: sig = 0
|
||
|
||
# za daleko od EMA20 -> poczekaj na pullback
|
||
if sig != 0:
|
||
ema20 = _safe(df, "ema20", _ema(df["close"], 20))
|
||
c = float(df["close"].iloc[-1])
|
||
# przy braku atr w df liczymy atrp już powyżej
|
||
atr = atrp * c if c > 0 else 0.0
|
||
if atr > 0.0 and abs(c - ema20) > OVEREXT_COEF * atr:
|
||
sig = 0
|
||
|
||
return int(sig)
|