Skip to content

Commit b612ba9

Browse files
committed
feat: add EMA strategy, refactor strategy pkg, add full pytest suite
- Refactoring: Spostate strategie in src/strategies con Factory pattern - New: Implementata strategia EMA Crossover - Test: Aggiunta infrastruttura Pytest completa (Core + Strategies) - Test: Aggiunti Integration Test per DB e Mock per servizi esterni - Ops: Aggiornato manager.sh con comando 'test' - Ops: Aggiornato requirements.txt e CI workflow
1 parent 3db5afb commit b612ba9

20 files changed

Lines changed: 634 additions & 7 deletions

.dockerignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@ __pycache__
1010
# Ignora cartelle dati e log (fondamentale per evitare errori permessi)
1111
data/
1212
logs/
13-
config/credentials/
13+
config/credentials/
14+
.pytest_cache
15+
htmlcov
16+
.coverage

manager.sh

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ case "$1" in
5454
docker compose run --rm app python -m services.backtest
5555
;;
5656

57+
test)
58+
echo -e "${GREEN}Running Tests (Pytest inside Docker)...${NC}"
59+
# Esegue pytest sulla cartella tests/ con verbosità attiva (-v)
60+
docker compose run --rm app pytest tests/ -v
61+
;;
62+
5763
shell)
5864
echo -e "${GREEN}Opening Shell...${NC}"
5965
docker compose run --rm -it app /bin/bash
@@ -73,8 +79,8 @@ case "$1" in
7379
docker compose logs -f dashboard
7480
;;
7581

76-
*)
77-
echo "Usage: $0 {setup|start|stop|status|daily|weekly|backtest|shell}"
82+
*)
83+
echo "Usage: $0 {setup|start|stop|status|daily|weekly|backtest|test|shell|dashboard}"
7884
exit 1
7985
;;
8086
esac

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ requests>=2.31,<3.0
1616
yfinance>=0.2,<0.3
1717
matplotlib>=3.8,<4.0
1818
streamlit>=1.30,<2.0
19-
plotly>=5.18,<6.0
19+
plotly>=5.18,<6.0
20+
pytest>=7.0,<8.0

services/backtest.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from src.logger import get_logger
1616

1717
# Import Strategie
18-
from src.strategy_RSI import StrategyRSI
18+
from src.strategies import StrategyRSI, StrategyEMA
1919

2020
logger = get_logger("Backtester")
2121

@@ -79,6 +79,12 @@ def run_backtest(strategy_name: str = "RSI",
7979
rsi_upper=strategy_params.get('rsi_upper', 70),
8080
atr_period=strategy_params.get('atr_period', 14)
8181
)
82+
elif strategy_name == "EMA":
83+
strategy = StrategyEMA(
84+
short_window=strategy_params.get('short_window', 50),
85+
long_window=strategy_params.get('long_window', 200),
86+
atr_period=strategy_params.get('atr_period', 14)
87+
)
8288
else:
8389
logger.error(f"Strategia {strategy_name} non implementata.")
8490
return

services/weekly_run.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from src.drive_manager import DriveManager
88
from src.risk_manager import RiskManager
99
from src.logger import get_logger
10-
from src.strategy_RSI import StrategyRSI
10+
from src.strategies import StrategyRSI, StrategyEMA
1111

1212
logger = get_logger("WeeklyRun")
1313

src/strategies/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .base import StrategyBase
2+
from .rsi import StrategyRSI
3+
from .ema import StrategyEMA
4+
5+
__all__ = ["StrategyBase", "StrategyRSI", "StrategyEMA"]

src/strategies/ema.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import pandas as pd
2+
import numpy as np
3+
from typing import Dict
4+
from .base import StrategyBase
5+
6+
class StrategyEMA(StrategyBase):
7+
"""
8+
Strategia Trend Following basata su incrocio medie mobili esponenziali (EMA).
9+
Segnale BUY: EMA_short > EMA_long
10+
Segnale SELL: EMA_short < EMA_long
11+
"""
12+
13+
def __init__(self, short_window: int = 50, long_window: int = 200, atr_period: int = 14):
14+
super().__init__("EMA_Crossover")
15+
self.short_window = short_window
16+
self.long_window = long_window
17+
self.atr_period = atr_period
18+
19+
def _calculate_atr(self, df: pd.DataFrame, period: int) -> pd.Series:
20+
"""Calcolo ATR manuale (identico a RSI, potremmo portarlo in Base in futuro)."""
21+
high = df['high']
22+
low = df['low']
23+
close = df['close']
24+
25+
tr1 = high - low
26+
tr2 = (high - close.shift()).abs()
27+
tr3 = (low - close.shift()).abs()
28+
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
29+
return tr.ewm(alpha=1.0/period, adjust=False).mean()
30+
31+
def compute(self, data_map: Dict[str, pd.DataFrame]) -> pd.DataFrame:
32+
signals_list = []
33+
self.logger.info(f"Avvio strategia EMA Cross ({self.short_window}/{self.long_window}) su {len(data_map)} ticker.")
34+
35+
for ticker, df in data_map.items():
36+
# 1. Validazione Dati
37+
if len(df) < self.long_window:
38+
continue
39+
40+
df = df.copy().sort_values('date')
41+
42+
# 2. Calcolo Indicatori
43+
try:
44+
df['EMA_short'] = df['close'].ewm(span=self.short_window, adjust=False).mean()
45+
df['EMA_long'] = df['close'].ewm(span=self.long_window, adjust=False).mean()
46+
df['ATR'] = self._calculate_atr(df, self.atr_period)
47+
except Exception as e:
48+
self.logger.error(f"Errore calcolo math {ticker}: {e}")
49+
continue
50+
51+
# 3. Analisi Ultima Riga
52+
last_row = df.iloc[-1]
53+
ema_s = last_row['EMA_short']
54+
ema_l = last_row['EMA_long']
55+
atr_val = last_row['ATR']
56+
price = last_row['close']
57+
58+
if pd.isna(ema_s) or pd.isna(ema_l) or pd.isna(atr_val):
59+
continue
60+
61+
# 4. Logica Trading
62+
# Trend Following: Se short > long siamo in trend UP -> BUY/HOLD
63+
# Se short < long siamo in trend DOWN -> SELL
64+
65+
signal = "HOLD"
66+
if ema_s > ema_l:
67+
signal = "BUY"
68+
elif ema_s < ema_l:
69+
signal = "SELL"
70+
71+
# 5. Output
72+
signals_list.append({
73+
"ticker": ticker,
74+
"date": last_row['date'],
75+
"signal": signal,
76+
"atr": atr_val,
77+
"price": price,
78+
"meta": {
79+
"ema_short": round(ema_s, 2),
80+
"ema_long": round(ema_l, 2),
81+
"diff": round(ema_s - ema_l, 2)
82+
}
83+
})
84+
85+
if not signals_list:
86+
return pd.DataFrame(columns=['ticker', 'date', 'signal', 'atr', 'meta'])
87+
88+
return pd.DataFrame(signals_list)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pandas as pd
22
import numpy as np
33
from typing import Dict
4-
from src.strategy_base import StrategyBase
4+
from .base import StrategyBase
55

66
class StrategyRSI(StrategyBase):
77
"""

tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)