stock/strategies.py
2025-08-15 12:32:27 +02:00

138 lines
4.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:0021: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)