cddea50c5a
Correzione post-micro-test (il conto e' USDC, non BTC/ETH): - deribit.py: INSTRUMENT -> BTC/ETH_USDC-PERPETUAL (lineari, gli unici eseguibili sul conto USDC); notional_to_amount gestisce i lineari (amount in base-coin = notional/price); + quantize_price; trade_history (read-only) per i trade reali. build_rebalance_order passa il prezzo. - shadow.py: sizing col prezzo; espone live_trades (trade reali eseguiti su Deribit). Entrata/uscita verificate (logica presa da Old/src/live/execution.py): - execution.py: open() market verificato (state=='filled' + trade, fill/fee reali, filled_amount autorevole), close() market reduce_only (le CHIUSURE si tentano SEMPRE, senza cap), disaster-SL STOP_MARKET reduce_only. Cap di size SOLO sulle aperture. Fill dataclass. - microtest.py: usa open()/close(); safe-close se l'apertura non e' verificata. Dashboard: sezione PAPER (backtest+forward) separata da sezione LIVE (conto reale Deribit: shadow TP01 + Trades REALI eseguiti). Test 27/27. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
166 lines
6.5 KiB
Python
166 lines
6.5 KiB
Python
"""Stato SHADOW di TP01 (Deribit mainnet, SOLA LETTURA): target causali + conto/posizioni REALI +
|
|
ordini di ribilancio COSTRUITI (mai inviati). Modulo condiviso da `scripts/live/live_trend.py` (CLI)
|
|
e dalla dashboard, cosi' i due non divergono. Robusto ai fallimenti di rete: degrada a offline/flat
|
|
senza sollevare eccezioni (la dashboard non deve crashare se il mainnet non risponde)."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
from src.backtest.harness import load
|
|
from src.live.deribit import INSTRUMENT, DeribitRead, build_rebalance_order
|
|
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
ASSETS = ["BTC", "ETH"]
|
|
WEIGHT = 0.5
|
|
FALLBACK_CAPITAL = 2000.0
|
|
PAPER_STATE = PROJECT_ROOT / "data" / "paper_trend" / "state.json"
|
|
|
|
|
|
def tp01_trades(limit: int = 15) -> list[dict]:
|
|
"""Lista dei TRADE di TP01 = cambi di posizione (ENTRY long / EXIT flat) dedotti dal segnale
|
|
causale `target_series` sui dati certificati. Account-independent (gira anche offline nel
|
|
container). Si ricalcola a ogni render -> include sia lo storico sia i trade forward man mano
|
|
che arrivano. Ritorna gli ultimi `limit` (piu' recenti primi)."""
|
|
tp = TrendPortfolio(**CANONICAL)
|
|
out: list[dict] = []
|
|
for a in ASSETS:
|
|
df = resample_1d(load(a, "1h"))
|
|
c = df["close"].to_numpy(dtype=float)
|
|
dt = pd.to_datetime(df["datetime"]).to_numpy()
|
|
tgt = tp.target_series(df)
|
|
prev = 0.0
|
|
for i in range(len(tgt)):
|
|
if np.sign(tgt[i]) != np.sign(prev):
|
|
out.append(dict(
|
|
ts=int(pd.Timestamp(dt[i]).value // 10**6),
|
|
date=str(pd.Timestamp(dt[i]).date()),
|
|
asset=a,
|
|
action="ENTRY" if tgt[i] != 0 else "EXIT",
|
|
from_pos=round(float(prev), 3),
|
|
to_pos=round(float(tgt[i]), 3),
|
|
price=round(float(c[i]), 1),
|
|
))
|
|
prev = tgt[i]
|
|
out.sort(key=lambda r: r["ts"], reverse=True)
|
|
return out[:limit]
|
|
|
|
|
|
def _safe_client() -> DeribitRead | None:
|
|
try:
|
|
return DeribitRead()
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def _marks(client, dfs):
|
|
marks, src = {}, {}
|
|
for a in ASSETS:
|
|
if client is None:
|
|
marks[a], src[a] = float(dfs[a]["close"].iloc[-1]), "close certificata"
|
|
continue
|
|
try:
|
|
marks[a], src[a] = client.mark_price(INSTRUMENT[a]), "mainnet"
|
|
except Exception as e:
|
|
marks[a], src[a] = float(dfs[a]["close"].iloc[-1]), f"fallback close ({type(e).__name__})"
|
|
return marks, src
|
|
|
|
|
|
def _positions(client):
|
|
if client is None:
|
|
return {a: 0.0 for a in ASSETS}, "offline -> assunto flat"
|
|
pos, note = {}, "mainnet"
|
|
for a in ASSETS:
|
|
try:
|
|
pos[a] = client.position_usd(INSTRUMENT[a])
|
|
except Exception as e:
|
|
pos[a], note = 0.0, f"read fallito ({type(e).__name__}) -> assunto flat"
|
|
return pos, note
|
|
|
|
|
|
def _equity(client, marks):
|
|
if client is None:
|
|
return None, "offline"
|
|
try:
|
|
eq = float(client.account_summary("USDC").get("equity") or 0)
|
|
if eq > 1:
|
|
return eq, "mainnet USDC"
|
|
except Exception:
|
|
pass
|
|
tot, any_ok = 0.0, False
|
|
for a in ASSETS:
|
|
try:
|
|
eq = float(client.account_summary(a).get("equity") or 0)
|
|
tot += eq * marks[a]
|
|
any_ok = True
|
|
except Exception:
|
|
pass
|
|
if any_ok and tot > 1:
|
|
return tot, "mainnet coin-margined"
|
|
return None, "conto flat / non finanziato"
|
|
|
|
|
|
def shadow_report(offline: bool = False, equity_override: float | None = None) -> dict:
|
|
"""Calcola lo stato shadow completo. NON invia nulla. Ritorna un dict serializzabile."""
|
|
dfs = {a: resample_1d(load(a, "1h")) for a in ASSETS}
|
|
tp = TrendPortfolio(**CANONICAL)
|
|
targets = {a: tp.current_target(dfs[a]) for a in ASSETS}
|
|
last_ts = min(int(dfs[a]["timestamp"].iloc[-1]) for a in ASSETS)
|
|
|
|
client = None if offline else _safe_client()
|
|
marks, marks_src = _marks(client, dfs)
|
|
positions, pos_src = _positions(client)
|
|
real_eq, eq_src = _equity(client, marks)
|
|
|
|
paper = json.loads(PAPER_STATE.read_text()) if PAPER_STATE.exists() else None
|
|
paper_cap = float(paper["capital"]) if paper else FALLBACK_CAPITAL
|
|
paper_pos = paper.get("positions") if paper else None
|
|
paper_ts = int(paper["last_ts"]) if paper else 0
|
|
|
|
equity = equity_override if equity_override is not None else (real_eq if real_eq else paper_cap)
|
|
eq_basis = ("override" if equity_override is not None
|
|
else eq_src if real_eq else "paper capital (ipotetico: conto non finanziato)")
|
|
|
|
assets, orders = [], []
|
|
for a in ASSETS:
|
|
inst = INSTRUMENT[a]
|
|
order = build_rebalance_order(inst, targets[a], WEIGHT, equity, positions[a], price=marks[a])
|
|
if order:
|
|
orders.append(order)
|
|
parity = None
|
|
if paper_pos is not None:
|
|
parity = abs(float(paper_pos.get(a, 0.0)) - targets[a]) < 1e-9
|
|
assets.append(dict(
|
|
asset=a, instrument=inst, target=targets[a],
|
|
target_notional=WEIGHT * targets[a] * equity,
|
|
position_usd=positions[a], mark=marks[a], mark_src=marks_src[a],
|
|
order=order, paper=(float(paper_pos.get(a, 0.0)) if paper_pos else None), parity=parity,
|
|
))
|
|
live_trades = []
|
|
if client is not None:
|
|
for a in ASSETS:
|
|
try:
|
|
for tr in client.trade_history(INSTRUMENT[a], limit=8):
|
|
live_trades.append(dict(
|
|
ts=int(tr.get("timestamp") or 0), instrument=INSTRUMENT[a],
|
|
direction=(tr.get("direction") or "").upper(),
|
|
amount=float(tr.get("amount") or 0), price=float(tr.get("price") or 0),
|
|
fee=float(tr.get("fee") or 0)))
|
|
except Exception:
|
|
pass
|
|
live_trades.sort(key=lambda r: r["ts"], reverse=True)
|
|
live_trades = live_trades[:12]
|
|
|
|
return dict(
|
|
last_data=str(pd.Timestamp(last_ts, unit="ms", tz="UTC").date()),
|
|
online=(client is not None and marks_src.get("BTC") == "mainnet"),
|
|
real_equity=real_eq, equity=equity, eq_basis=eq_basis,
|
|
pos_src=pos_src, assets=assets, orders=orders, live_trades=live_trades,
|
|
flat=all(abs(targets[a]) < 1e-9 for a in ASSETS),
|
|
paper_aligned=(paper_ts == last_ts),
|
|
)
|