Merge branch 'main' into codex/review-function-consolidation-across-files-rhuc38

This commit is contained in:
fredmaloggia
2025-11-17 17:12:38 +01:00
committed by GitHub
3 changed files with 257 additions and 195 deletions

View File

@@ -75,6 +75,10 @@ DB_CONFIG = require_section(CONFIG, "db")
PATTERN_CONFIG = require_section(CONFIG, "pattern") PATTERN_CONFIG = require_section(CONFIG, "pattern")
TAGGING_CONFIG = require_section(CONFIG, "tagging") TAGGING_CONFIG = require_section(CONFIG, "tagging")
RANKING_CONFIG = require_section(CONFIG, "ranking") RANKING_CONFIG = require_section(CONFIG, "ranking")
DB_CONFIG = CONFIG.get("db", {})
PATTERN_CONFIG = CONFIG.get("pattern", {})
TAGGING_CONFIG = CONFIG.get("tagging", {})
RANKING_CONFIG = CONFIG.get("ranking", {})
UNIVERSO_XLSX = "Universo per Trading System.xlsx" UNIVERSO_XLSX = "Universo per Trading System.xlsx"
@@ -112,6 +116,26 @@ if RP_MAX_WEIGHT is None:
RP_MAX_WEIGHT = 2 / max(TOP_N_MAX, 1) RP_MAX_WEIGHT = 2 / max(TOP_N_MAX, 1)
else: else:
RP_MAX_WEIGHT = float(RP_MAX_WEIGHT) RP_MAX_WEIGHT = float(RP_MAX_WEIGHT)
STORED_PROC = DB_CONFIG.get("stored_proc", "opt_RendimentoGiornaliero1_ALL")
N_BARS = DB_CONFIG.get("n_bars", 1305)
PTF_CURR = DB_CONFIG.get("ptf_curr", "EUR")
# Pattern-matching (iper-parametri)
WP = PATTERN_CONFIG.get("wp", 60) # lunghezza finestra pattern (barre)
HA = PATTERN_CONFIG.get("ha", 10) # orizzonte outcome (barre)
KNN_K = PATTERN_CONFIG.get("knn_k", 25) # numero di vicini
THETA = PATTERN_CONFIG.get("theta", 0.005) # soglia su outcome per generare segnale
EMBARGO = PATTERN_CONFIG.get("embargo", WP + HA)
# Tagging rule-based (soglie)
Z_REV = TAGGING_CONFIG.get("z_rev", 2.0)
Z_VOL = TAGGING_CONFIG.get("z_vol", 2.0)
STD_COMP_PCT = TAGGING_CONFIG.get("std_comp_pct", 0.15)
DAYS_PER_YEAR = 252
TOP_N_MAX = RANKING_CONFIG.get("top_n_max", 15) # numero massimo di asset ammessi
RP_MAX_WEIGHT = RANKING_CONFIG.get("rp_max_weight", 2 / max(TOP_N_MAX, 1)) # 2 x 1/15 ≈ 0.1333 = 13,33%
# ========================================= # =========================================
# UTILS GENERALI # UTILS GENERALI

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import json import json
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence, Tuple from typing import Any, Dict, List, Optional, Sequence, Tuple
from typing import Dict, List, Optional, Sequence, Tuple
import numpy as np import numpy as np
import pandas as pd import pandas as pd

View File

@@ -20,97 +20,134 @@ Pipeline (giorno D, EOD -> t+1 OPEN):
from __future__ import annotations from __future__ import annotations
import os import os
import ssl import ssl
import json import json
import time import time
import shutil import shutil
import warnings import warnings
import datetime as dt import datetime as dt
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Tuple, Iterable, Set from typing import Dict, List, Optional, Tuple, Iterable, Set
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from urllib.request import urlopen from urllib.request import urlopen
from urllib.error import URLError, HTTPError from urllib.error import URLError, HTTPError
# DB # DB
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import text as sql_text from sqlalchemy import text as sql_text
from shared_utils import ( from shared_utils import (
build_hurst_map, build_hurst_map,
build_pattern_library, build_pattern_library,
characterize_window, characterize_window,
detect_column, detect_column,
load_config, load_config,
predict_from_library, predict_from_library,
read_connection_txt, read_connection_txt,
require_section, require_section,
require_value, require_value,
z_norm, z_norm,
) )
# ========================= # =========================
# CONFIG # CONFIG
# ========================= # =========================
CONFIG = load_config() CONFIG = load_config()
DB_CONFIG = require_section(CONFIG, "db") DB_CONFIG = require_section(CONFIG, "db")
PATTERN_CONFIG = require_section(CONFIG, "pattern") PATTERN_CONFIG = require_section(CONFIG, "pattern")
TAGGING_CONFIG = require_section(CONFIG, "tagging") TAGGING_CONFIG = require_section(CONFIG, "tagging")
RANKING_CONFIG = require_section(CONFIG, "ranking") RANKING_CONFIG = require_section(CONFIG, "ranking")
SIGNALS_CONFIG = require_section(CONFIG, "signals") SIGNALS_CONFIG = require_section(CONFIG, "signals")
DB_CONFIG = CONFIG.get("db", {})
BASE_DIR = Path(".") PATTERN_CONFIG = CONFIG.get("pattern", {})
UNIVERSO_XLSX = BASE_DIR / "Universo per Trading System.xlsx" TAGGING_CONFIG = CONFIG.get("tagging", {})
CONNECTION_TXT = BASE_DIR / "connection.txt" RANKING_CONFIG = CONFIG.get("ranking", {})
AUDIT_LOG_CSV = BASE_DIR / "trades_audit_log.csv" SIGNALS_CONFIG = CONFIG.get("signals", {})
OPEN_TRADES_DIR = BASE_DIR / "open_trades"
DROPBOX_EXPORT_DIR = Path(r"C:\Users\Admin\Dropbox\Condivisa Lavoro\Segnali di trading su ETF") BASE_DIR = Path(".")
UNIVERSO_XLSX = BASE_DIR / "Universo per Trading System.xlsx"
CONNECTION_TXT = BASE_DIR / "connection.txt"
AUDIT_LOG_CSV = BASE_DIR / "trades_audit_log.csv"
OPEN_TRADES_DIR = BASE_DIR / "open_trades"
DROPBOX_EXPORT_DIR = Path(r"C:\Users\Admin\Dropbox\Condivisa Lavoro\Segnali di trading su ETF")
def _dated_signals_filename() -> Path: def _dated_signals_filename() -> Path:
date_prefix = pd.Timestamp.today().strftime("%Y%m%d") date_prefix = pd.Timestamp.today().strftime("%Y%m%d")
return BASE_DIR / f"{date_prefix}_signals.xlsx" return BASE_DIR / f"{date_prefix}_signals.xlsx"
# Stored procedure / parametri DB # Stored procedure / parametri DB
SP_NAME_DEFAULT = str(require_value(DB_CONFIG, "stored_proc", "db")) SP_NAME_DEFAULT = str(require_value(DB_CONFIG, "stored_proc", "db"))
SP_N_DEFAULT = int(require_value(DB_CONFIG, "n_bars", "db")) SP_N_DEFAULT = int(require_value(DB_CONFIG, "n_bars", "db"))
PTF_CURR_DEFAULT = str(require_value(DB_CONFIG, "ptf_curr", "db")) PTF_CURR_DEFAULT = str(require_value(DB_CONFIG, "ptf_curr", "db"))
# Pattern recognition (come backtest) # Pattern recognition (come backtest)
WP = int(require_value(PATTERN_CONFIG, "wp", "pattern")) WP = int(require_value(PATTERN_CONFIG, "wp", "pattern"))
HA = int(require_value(PATTERN_CONFIG, "ha", "pattern")) HA = int(require_value(PATTERN_CONFIG, "ha", "pattern"))
KNN_K = int(require_value(PATTERN_CONFIG, "knn_k", "pattern")) KNN_K = int(require_value(PATTERN_CONFIG, "knn_k", "pattern"))
THETA = float(require_value(PATTERN_CONFIG, "theta", "pattern")) # 0,005% in decimali (identico al backtest) THETA = float(require_value(PATTERN_CONFIG, "theta", "pattern")) # 0,005% in decimali (identico al backtest)
Z_REV = float(require_value(TAGGING_CONFIG, "z_rev", "tagging")) Z_REV = float(require_value(TAGGING_CONFIG, "z_rev", "tagging"))
Z_VOL = float(require_value(TAGGING_CONFIG, "z_vol", "tagging")) Z_VOL = float(require_value(TAGGING_CONFIG, "z_vol", "tagging"))
STD_COMP_PCT = float(require_value(TAGGING_CONFIG, "std_comp_pct", "tagging")) STD_COMP_PCT = float(require_value(TAGGING_CONFIG, "std_comp_pct", "tagging"))
# Exit rules (identiche al backtest) # Exit rules (identiche al backtest)
SL_BPS = float(require_value(SIGNALS_CONFIG, "sl_bps", "signals")) SL_BPS = float(require_value(SIGNALS_CONFIG, "sl_bps", "signals"))
TP_BPS = float(require_value(SIGNALS_CONFIG, "tp_bps", "signals")) TP_BPS = float(require_value(SIGNALS_CONFIG, "tp_bps", "signals"))
TRAIL_BPS = float(require_value(SIGNALS_CONFIG, "trail_bps", "signals")) TRAIL_BPS = float(require_value(SIGNALS_CONFIG, "trail_bps", "signals"))
TIME_STOP_BARS = int(require_value(SIGNALS_CONFIG, "time_stop_bars", "signals")) TIME_STOP_BARS = int(require_value(SIGNALS_CONFIG, "time_stop_bars", "signals"))
THETA_EXIT = float(require_value(SIGNALS_CONFIG, "theta_exit", "signals")) # soglia debolezza THETA_EXIT = float(require_value(SIGNALS_CONFIG, "theta_exit", "signals")) # soglia debolezza
WEAK_DAYS_EXIT = require_value(SIGNALS_CONFIG, "weak_days_exit", "signals") # uscita IMMEDIATA in caso di debolezza (come backtest) WEAK_DAYS_EXIT = require_value(SIGNALS_CONFIG, "weak_days_exit", "signals") # uscita IMMEDIATA in caso di debolezza (come backtest)
# Ranking e selezione Top-N per APERTURE # Ranking e selezione Top-N per APERTURE
MAX_OPEN = int(require_value(SIGNALS_CONFIG, "max_open", "signals")) # cap strumenti aperti oggi (come backtest) MAX_OPEN = int(require_value(SIGNALS_CONFIG, "max_open", "signals")) # cap strumenti aperti oggi (come backtest)
# Allineamento al backtest v3.1.5 per il cap del Risk Parity # Allineamento al backtest v3.1.5 per il cap del Risk Parity
TOP_N_MAX = int(require_value(RANKING_CONFIG, "top_n_max", "ranking")) TOP_N_MAX = int(require_value(RANKING_CONFIG, "top_n_max", "ranking"))
RP_MAX_WEIGHT = require_value(RANKING_CONFIG, "rp_max_weight", "ranking") # ≈ 0.1333 = 13,33% per singolo asset RP_MAX_WEIGHT = require_value(RANKING_CONFIG, "rp_max_weight", "ranking") # ≈ 0.1333 = 13,33% per singolo asset
if RP_MAX_WEIGHT is None: if RP_MAX_WEIGHT is None:
RP_MAX_WEIGHT = 2 / max(TOP_N_MAX, 1) RP_MAX_WEIGHT = 2 / max(TOP_N_MAX, 1)
else: else:
RP_MAX_WEIGHT = float(RP_MAX_WEIGHT) RP_MAX_WEIGHT = float(RP_MAX_WEIGHT)
# Sizing # Sizing
BASE_CAPITAL_PER_STRATEGY = float(require_value(SIGNALS_CONFIG, "base_capital_per_strategy", "signals")) BASE_CAPITAL_PER_STRATEGY = float(require_value(SIGNALS_CONFIG, "base_capital_per_strategy", "signals"))
MIN_TRADE_NOTIONAL = float(require_value(SIGNALS_CONFIG, "min_trade_notional", "signals")) MIN_TRADE_NOTIONAL = float(require_value(SIGNALS_CONFIG, "min_trade_notional", "signals"))
RISK_PARITY_LOOKBACK = int(require_value(SIGNALS_CONFIG, "risk_parity_lookback", "signals")) RISK_PARITY_LOOKBACK = int(require_value(SIGNALS_CONFIG, "risk_parity_lookback", "signals"))
SP_NAME_DEFAULT = DB_CONFIG.get("stored_proc", "opt_RendimentoGiornaliero1_ALL")
SP_N_DEFAULT = DB_CONFIG.get("n_bars", 1305)
PTF_CURR_DEFAULT = DB_CONFIG.get("ptf_curr", "EUR")
# Pattern recognition (come backtest)
WP = PATTERN_CONFIG.get("wp", 60)
HA = PATTERN_CONFIG.get("ha", 10)
KNN_K = PATTERN_CONFIG.get("knn_k", 25)
THETA = PATTERN_CONFIG.get("theta", 0.005) # 0,005% in decimali (identico al backtest)
Z_REV = TAGGING_CONFIG.get("z_rev", 2.0)
Z_VOL = TAGGING_CONFIG.get("z_vol", 2.0)
STD_COMP_PCT = TAGGING_CONFIG.get("std_comp_pct", 0.15)
# Exit rules (identiche al backtest)
SL_BPS = SIGNALS_CONFIG.get("sl_bps", 300.0)
TP_BPS = SIGNALS_CONFIG.get("tp_bps", 800.0)
TRAIL_BPS = SIGNALS_CONFIG.get("trail_bps", 300.0)
TIME_STOP_BARS = SIGNALS_CONFIG.get("time_stop_bars", 20)
THETA_EXIT = SIGNALS_CONFIG.get("theta_exit", 0.0) # soglia debolezza
WEAK_DAYS_EXIT = SIGNALS_CONFIG.get("weak_days_exit") # uscita IMMEDIATA in caso di debolezza (come backtest)
# Ranking e selezione Top-N per APERTURE
MAX_OPEN = SIGNALS_CONFIG.get("max_open", 15) # cap strumenti aperti oggi (come backtest)
# Allineamento al backtest v3.1.5 per il cap del Risk Parity
TOP_N_MAX = RANKING_CONFIG.get("top_n_max", MAX_OPEN)
RP_MAX_WEIGHT = RANKING_CONFIG.get("rp_max_weight", 2 / max(TOP_N_MAX, 1)) # ≈ 0.1333 = 13,33% per singolo asset
# Sizing
BASE_CAPITAL_PER_STRATEGY = SIGNALS_CONFIG.get("base_capital_per_strategy", 100.0)
MIN_TRADE_NOTIONAL = SIGNALS_CONFIG.get("min_trade_notional", 0.01)
RISK_PARITY_LOOKBACK = SIGNALS_CONFIG.get("risk_parity_lookback", 60)
# Calendario # Calendario
BUSINESS_DAYS_ONLY = True BUSINESS_DAYS_ONLY = True
@@ -122,20 +159,20 @@ np.random.seed(SEED)
# ========================= # =========================
# UTILS # UTILS
# ========================= # =========================
def ensure_dir(p: Path): def ensure_dir(p: Path):
p.mkdir(parents=True, exist_ok=True) p.mkdir(parents=True, exist_ok=True)
def copy_to_dropbox(src: Path, dst_dir: Path = DROPBOX_EXPORT_DIR): def copy_to_dropbox(src: Path, dst_dir: Path = DROPBOX_EXPORT_DIR):
if not src or not dst_dir: if not src or not dst_dir:
return return
if not src.exists(): if not src.exists():
return return
try: try:
ensure_dir(dst_dir) ensure_dir(dst_dir)
dst = dst_dir / src.name dst = dst_dir / src.name
shutil.copy2(src, dst) shutil.copy2(src, dst)
except Exception as exc: except Exception as exc:
print(f"[WARN] impossibile copiare {src} su {dst_dir}: {exc}") print(f"[WARN] impossibile copiare {src} su {dst_dir}: {exc}")
def next_business_day(d: dt.date) -> dt.date: def next_business_day(d: dt.date) -> dt.date:
nd = d + dt.timedelta(days=1) nd = d + dt.timedelta(days=1)
@@ -151,9 +188,9 @@ def _safe_to_float(x) -> Optional[float]:
except Exception: except Exception:
return None return None
def _db_fetch_returns(conn_str: str, def _db_fetch_returns(conn_str: str,
isins: List[str], isins: List[str],
sp_name: Optional[str] = None, sp_name: Optional[str] = None,
n_bars: Optional[int] = None, n_bars: Optional[int] = None,
ptf_curr: Optional[str] = None) -> pd.DataFrame: ptf_curr: Optional[str] = None) -> pd.DataFrame:
engine = sa.create_engine(conn_str, fast_executemany=True) engine = sa.create_engine(conn_str, fast_executemany=True)
@@ -164,11 +201,11 @@ def _db_fetch_returns(conn_str: str,
sql_sp = sql_text(f"EXEC {sp} @ISIN = :isin, @n = :n, @PtfCurr = :ptf") sql_sp = sql_text(f"EXEC {sp} @ISIN = :isin, @n = :n, @PtfCurr = :ptf")
frames: List[pd.DataFrame] = [] frames: List[pd.DataFrame] = []
with engine.begin() as conn: with engine.begin() as conn:
for i, isin in enumerate(isins, start=1): for i, isin in enumerate(isins, start=1):
print(f"[DB] ({i}/{len(isins)}) scarico serie storica per {isin} ...", flush=True) print(f"[DB] ({i}/{len(isins)}) scarico serie storica per {isin} ...", flush=True)
try: try:
df = pd.read_sql_query(sql_sp, conn, params={"isin": str(isin), "n": int(n_val), "ptf": ptf}) df = pd.read_sql_query(sql_sp, conn, params={"isin": str(isin), "n": int(n_val), "ptf": ptf})
except Exception as e: except Exception as e:
print(f"[ERROR] SP {sp} fallita per {isin}: {e}") print(f"[ERROR] SP {sp} fallita per {isin}: {e}")
continue continue
@@ -177,11 +214,11 @@ def _db_fetch_returns(conn_str: str,
print(f"[WARN] Nessun dato per {isin}") print(f"[WARN] Nessun dato per {isin}")
continue continue
col_date = detect_column(df, ["Date", "Data", "Datetime", "Timestamp", "Time"]) col_date = detect_column(df, ["Date", "Data", "Datetime", "Timestamp", "Time"])
col_ret = detect_column(df, ["Ret", "Return", "Rendimento", "Rend", "Ret_%", "RET"]) col_ret = detect_column(df, ["Ret", "Return", "Rendimento", "Rend", "Ret_%", "RET"])
if not col_date or not col_ret: if not col_date or not col_ret:
print(f"[WARN] Colonne mancanti per {isin}") print(f"[WARN] Colonne mancanti per {isin}")
continue continue
out = df[[col_date, col_ret]].copy() out = df[[col_date, col_ret]].copy()
out.columns = ["Date", "Ret"] out.columns = ["Date", "Ret"]
@@ -309,17 +346,17 @@ def generate_signals_today(universe: pd.DataFrame,
lib_wins, lib_out = build_pattern_library(r, WP, HA) lib_wins, lib_out = build_pattern_library(r, WP, HA)
if lib_wins is None or len(r) < WP + HA: if lib_wins is None or len(r) < WP + HA:
est_out, avg_dist, sig = np.nan, np.nan, 0 est_out, avg_dist, sig = np.nan, np.nan, 0
ptype, pconf = characterize_window(r, WP, z_rev=Z_REV, z_vol=Z_VOL, std_comp_pct=STD_COMP_PCT) ptype, pconf = characterize_window(r, WP, z_rev=Z_REV, z_vol=Z_VOL, std_comp_pct=STD_COMP_PCT)
else: else:
curr = r.values[-WP:] curr = r.values[-WP:]
curr_zn = z_norm(curr) curr_zn = z_norm(curr)
if curr_zn is None: if curr_zn is None:
est_out, avg_dist, sig = np.nan, np.nan, 0 est_out, avg_dist, sig = np.nan, np.nan, 0
ptype, pconf = characterize_window(r, WP, z_rev=Z_REV, z_vol=Z_VOL, std_comp_pct=STD_COMP_PCT) ptype, pconf = characterize_window(r, WP, z_rev=Z_REV, z_vol=Z_VOL, std_comp_pct=STD_COMP_PCT)
else: else:
est_out, avg_dist, _ = predict_from_library(curr_zn, lib_wins, lib_out, k=KNN_K) est_out, avg_dist, _ = predict_from_library(curr_zn, lib_wins, lib_out, k=KNN_K)
sig = 1 if (pd.notna(est_out) and float(est_out) > float(theta_entry)) else 0 sig = 1 if (pd.notna(est_out) and float(est_out) > float(theta_entry)) else 0
ptype, pconf = characterize_window(r, WP, z_rev=Z_REV, z_vol=Z_VOL, std_comp_pct=STD_COMP_PCT) ptype, pconf = characterize_window(r, WP, z_rev=Z_REV, z_vol=Z_VOL, std_comp_pct=STD_COMP_PCT)
rows.append({ rows.append({
"Date": decision_date, "ISIN": isin, "Date": decision_date, "ISIN": isin,
@@ -388,21 +425,21 @@ def open_trades_path(strategy: str) -> Path:
ensure_dir(OPEN_TRADES_DIR) ensure_dir(OPEN_TRADES_DIR)
return OPEN_TRADES_DIR / f"open_{strategy}.csv" return OPEN_TRADES_DIR / f"open_{strategy}.csv"
def load_open_trades(strategy: str) -> pd.DataFrame: def load_open_trades(strategy: str) -> pd.DataFrame:
p = open_trades_path(strategy) p = open_trades_path(strategy)
if not p.exists(): if not p.exists():
return pd.DataFrame(columns=[ return pd.DataFrame(columns=[
"Strategy","ISIN","AssetName","EntryDate","EntryIndex","EntryAmount","SizeWeight","PeakPnL","WeakDays","Notes" "Strategy","ISIN","AssetName","EntryDate","EntryIndex","EntryAmount","SizeWeight","PeakPnL","WeakDays","Notes"
]) ])
df = pd.read_csv(p) df = pd.read_csv(p)
if "EntryDate" in df.columns: if "EntryDate" in df.columns:
df["EntryDate"] = pd.to_datetime(df["EntryDate"], errors="coerce").dt.date df["EntryDate"] = pd.to_datetime(df["EntryDate"], errors="coerce").dt.date
if "WeakDays" not in df.columns: if "WeakDays" not in df.columns:
df["WeakDays"] = 0 df["WeakDays"] = 0
if "AssetName" not in df.columns: if "AssetName" not in df.columns:
df["AssetName"] = "" df["AssetName"] = ""
df["Strategy"] = strategy df["Strategy"] = strategy
return df return df
def save_open_trades(strategy: str, df: pd.DataFrame): def save_open_trades(strategy: str, df: pd.DataFrame):
p = open_trades_path(strategy) p = open_trades_path(strategy)
@@ -492,13 +529,13 @@ def _risk_exit_flags(isin: str,
reasons.append("WEAK") reasons.append("WEAK")
return reasons return reasons
def update_positions_and_build_orders(universe: pd.DataFrame, def update_positions_and_build_orders(universe: pd.DataFrame,
returns_long: pd.DataFrame, returns_long: pd.DataFrame,
signals_today: pd.DataFrame, signals_today: pd.DataFrame,
today: dt.date, today: dt.date,
buy_rank_df: Optional[pd.DataFrame], buy_rank_df: Optional[pd.DataFrame],
allowed_open_isins: Optional[List[str]] = None, allowed_open_isins: Optional[List[str]] = None,
asset_name_map: Optional[pd.Series] = None) -> Tuple[pd.DataFrame, List[Dict]]: asset_name_map: Optional[pd.Series] = None) -> Tuple[pd.DataFrame, List[Dict]]:
""" """
- decision_date = ultima data disponibile (EOD) - decision_date = ultima data disponibile (EOD)
- target giornaliero = primi MAX_OPEN del ranking buy (uguale per tutte le strategie) - target giornaliero = primi MAX_OPEN del ranking buy (uguale per tutte le strategie)
@@ -627,20 +664,20 @@ def update_positions_and_build_orders(universe: pd.DataFrame,
}])], ignore_index=True) }])], ignore_index=True)
current_set.add(isin) current_set.add(isin)
if asset_name_map is not None: if asset_name_map is not None:
df_open["AssetName"] = df_open["ISIN"].astype(str).map(asset_name_map).fillna("") df_open["AssetName"] = df_open["ISIN"].astype(str).map(asset_name_map).fillna("")
else: else:
if "AssetName" not in df_open.columns: if "AssetName" not in df_open.columns:
df_open["AssetName"] = "" df_open["AssetName"] = ""
if "AssetName" in df_open.columns: if "AssetName" in df_open.columns:
cols = list(df_open.columns) cols = list(df_open.columns)
if "ISIN" in cols and "AssetName" in cols: if "ISIN" in cols and "AssetName" in cols:
cols.insert(cols.index("ISIN") + 1, cols.pop(cols.index("AssetName"))) cols.insert(cols.index("ISIN") + 1, cols.pop(cols.index("AssetName")))
df_open = df_open[cols] df_open = df_open[cols]
save_open_trades(strat, df_open) save_open_trades(strat, df_open)
df_open["Strategy"] = strat df_open["Strategy"] = strat
open_concat.append(df_open) open_concat.append(df_open)
# ---- FETCH UNA VOLTA (OPEN + CLOSE) ---- # ---- FETCH UNA VOLTA (OPEN + CLOSE) ----
fetch_isins = sorted(isins_for_open_fetch.union(isins_for_close_fetch)) fetch_isins = sorted(isins_for_open_fetch.union(isins_for_close_fetch))
@@ -662,24 +699,24 @@ def update_positions_and_build_orders(universe: pd.DataFrame,
# ========================= # =========================
# MAIN RUN # MAIN RUN
# ========================= # =========================
def main_run(run_date: Optional[dt.date] = None): def main_run(run_date: Optional[dt.date] = None):
today = run_date or dt.date.today() today = run_date or dt.date.today()
# 1) Universo # 1) Universo
universe = load_universe(UNIVERSO_XLSX) universe = load_universe(UNIVERSO_XLSX)
asset_name_col = detect_column(universe, [ asset_name_col = detect_column(universe, [
"Nome", "Name", "Asset", "Asset Name", "Descrizione", "Description" "Nome", "Name", "Asset", "Asset Name", "Descrizione", "Description"
]) ])
if not asset_name_col: if not asset_name_col:
print("[WARN] Colonna con il nome dell'asset non trovata nell'universo.") print("[WARN] Colonna con il nome dell'asset non trovata nell'universo.")
asset_name_map: Optional[pd.Series] = None asset_name_map: Optional[pd.Series] = None
if asset_name_col: if asset_name_col:
asset_name_map = ( asset_name_map = (
universe[["ISIN", asset_name_col]] universe[["ISIN", asset_name_col]]
.dropna(subset=["ISIN"]) .dropna(subset=["ISIN"])
.assign(ISIN=lambda df: df["ISIN"].astype(str).str.strip()) .assign(ISIN=lambda df: df["ISIN"].astype(str).str.strip())
) )
asset_name_map = asset_name_map.set_index("ISIN")[asset_name_col].astype(str).str.strip() asset_name_map = asset_name_map.set_index("ISIN")[asset_name_col].astype(str).str.strip()
# 2) Ritorni (DB) # 2) Ritorni (DB)
conn_str = read_connection_txt(CONNECTION_TXT) conn_str = read_connection_txt(CONNECTION_TXT)
@@ -706,52 +743,52 @@ def main_run(run_date: Optional[dt.date] = None):
allowed_open = _select_top_signals(buy_rank_df, MAX_OPEN) # top-N ISIN allowed_open = _select_top_signals(buy_rank_df, MAX_OPEN) # top-N ISIN
# 4) Posizioni + audit (OPEN/CLOSE) con target Top-N # 4) Posizioni + audit (OPEN/CLOSE) con target Top-N
open_df, audit_rows = update_positions_and_build_orders( open_df, audit_rows = update_positions_and_build_orders(
universe, returns_long, sig_df, today, universe, returns_long, sig_df, today,
buy_rank_df=buy_rank_df, buy_rank_df=buy_rank_df,
allowed_open_isins=allowed_open, allowed_open_isins=allowed_open,
asset_name_map=asset_name_map, asset_name_map=asset_name_map,
) )
# 5) Append audit log (TUTTE le strategie operative) # 5) Append audit log (TUTTE le strategie operative)
if audit_rows: if audit_rows:
append_audit_rows(audit_rows) append_audit_rows(audit_rows)
# 6) Snapshot Excel datato — fogli con nomi completi # 6) Snapshot Excel datato — fogli con nomi completi
ensure_dir(OPEN_TRADES_DIR) ensure_dir(OPEN_TRADES_DIR)
signals_path = _dated_signals_filename() signals_path = _dated_signals_filename()
signals_sheet = sig_df.reset_index() signals_sheet = sig_df.reset_index()
if asset_name_map is not None: if asset_name_map is not None:
signals_sheet["AssetName"] = signals_sheet["ISIN"].astype(str).map(asset_name_map).fillna("") signals_sheet["AssetName"] = signals_sheet["ISIN"].astype(str).map(asset_name_map).fillna("")
else: else:
signals_sheet["AssetName"] = "" signals_sheet["AssetName"] = ""
# inserisci la colonna subito dopo l'ISIN # inserisci la colonna subito dopo l'ISIN
if "AssetName" in signals_sheet.columns: if "AssetName" in signals_sheet.columns:
cols = list(signals_sheet.columns) cols = list(signals_sheet.columns)
cols.insert(cols.index("ISIN") + 1, cols.pop(cols.index("AssetName"))) cols.insert(cols.index("ISIN") + 1, cols.pop(cols.index("AssetName")))
signals_sheet = signals_sheet[cols] signals_sheet = signals_sheet[cols]
with pd.ExcelWriter(signals_path) as xw: with pd.ExcelWriter(signals_path) as xw:
signals_sheet.to_excel(xw, sheet_name="Signals", index=False) signals_sheet.to_excel(xw, sheet_name="Signals", index=False)
if not open_df.empty: if not open_df.empty:
for strat, g in open_df.groupby("Strategy"): for strat, g in open_df.groupby("Strategy"):
sheet_name_map = { sheet_name_map = {
"Equal_Weight": "Open_Equal_Weight", "Equal_Weight": "Open_Equal_Weight",
"Risk_Parity": "Open_Risk_Parity", "Risk_Parity": "Open_Risk_Parity",
} }
sheet_name = sheet_name_map.get(strat, f"Open_{strat}")[:31] sheet_name = sheet_name_map.get(strat, f"Open_{strat}")[:31]
g.to_excel(xw, sheet_name=sheet_name, index=False) g.to_excel(xw, sheet_name=sheet_name, index=False)
copy_to_dropbox(signals_path) copy_to_dropbox(signals_path)
for strat in ["Equal_Weight", "Risk_Parity"]: for strat in ["Equal_Weight", "Risk_Parity"]:
csv_path = open_trades_path(strat) csv_path = open_trades_path(strat)
if csv_path.exists(): if csv_path.exists():
copy_to_dropbox(csv_path) copy_to_dropbox(csv_path)
print(f"✅ Signals generated for {today}. Saved to {signals_path}") print(f"✅ Signals generated for {today}. Saved to {signals_path}")
print(f"Open trades saved in {OPEN_TRADES_DIR}") print(f"Open trades saved in {OPEN_TRADES_DIR}")
print(f"Audit log updated at {AUDIT_LOG_CSV}") print(f"Audit log updated at {AUDIT_LOG_CSV}")
# ========================= # =========================
# ENTRY POINT # ENTRY POINT