feat(phase-3): paper-trading runner BTC+ETH
Modulo paper_trading per forward-test virtuale Phase 3: - Portfolio multi-asset equal-weight sleeve, fees bp su round-trip - PaperExecutor compila strategia JSON e applica segnale a bar close - PaperRepository persiste runs/ticks/trades/equity in runs.db - CLI scripts/run_paper_trading.py: loop polling Cerbero, exec su nuovo bar Strategie deployate: - BTC fb63e851 (Sharpe OOS +0.50, mean rev RSI+ATR+hour gate) - ETH facd6af85d5d (Sharpe OOS +0.19, trend vol regime + SMA50/200) Capitale virtuale $1000 (sleeve $500 ciascuno), 2 settimane smoke. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,202 @@
|
|||||||
|
"""Paper-trading runner Phase 3 — forward-test virtuale BTC + ETH.
|
||||||
|
|
||||||
|
Loop infinito (o limitato via --max-ticks) che ogni ``--poll-seconds``:
|
||||||
|
1. fetch OHLCV 1h ultime ~500 barre via Cerbero
|
||||||
|
2. per ogni strategia: compile + esegui ultimo bar
|
||||||
|
3. apply segnale al portfolio multi-asset
|
||||||
|
4. snapshot equity in DB
|
||||||
|
|
||||||
|
I bar 1h chiudono al minuto :00. Il loop riconosce un "nuovo bar chiuso"
|
||||||
|
confrontando l'ultimo timestamp del DataFrame con quello dell'iterazione
|
||||||
|
precedente. Tick consecutivi su stesso bar = hold (no double-trade).
|
||||||
|
|
||||||
|
Esempio:
|
||||||
|
uv run python scripts/run_paper_trading.py \
|
||||||
|
--name phase3-papertrade-001 \
|
||||||
|
--initial-capital 1000 \
|
||||||
|
--max-ticks 336 # 2 settimane * 24 ore
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from multi_swarm.cerbero.client import CerberoClient
|
||||||
|
from multi_swarm.config import load_settings
|
||||||
|
from multi_swarm.data.cerbero_ohlcv import CerberoOHLCVLoader, OHLCVRequest
|
||||||
|
from multi_swarm.paper_trading.executor import PaperExecutor
|
||||||
|
from multi_swarm.paper_trading.persistence import PaperRepository
|
||||||
|
from multi_swarm.paper_trading.portfolio import Portfolio
|
||||||
|
from multi_swarm.persistence.repository import Repository
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AssetConfig:
|
||||||
|
symbol: str # es. "BTC-PERPETUAL"
|
||||||
|
strategy_file: Path
|
||||||
|
exchange: str = "deribit"
|
||||||
|
timeframe: str = "1h"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
p = argparse.ArgumentParser(description="Paper-trading runner Phase 3")
|
||||||
|
p.add_argument("--name", default="phase3-papertrade-001")
|
||||||
|
p.add_argument("--initial-capital", type=float, default=1000.0)
|
||||||
|
p.add_argument("--fees-bp", type=float, default=5.0)
|
||||||
|
p.add_argument("--poll-seconds", type=int, default=300, help="Polling interval (5min default)")
|
||||||
|
p.add_argument("--max-ticks", type=int, default=0, help="0 = infinito; per smoke test usa 1")
|
||||||
|
p.add_argument("--lookback-bars", type=int, default=500, help="Quante bar fetchare per indicatori")
|
||||||
|
p.add_argument(
|
||||||
|
"--strategies-dir",
|
||||||
|
default=str(PROJECT_ROOT / "strategies"),
|
||||||
|
help="Cartella contenente btc_*.json e eth_*.json",
|
||||||
|
)
|
||||||
|
return p.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def load_assets(strategies_dir: Path) -> list[AssetConfig]:
|
||||||
|
btc_files = sorted(strategies_dir.glob("btc_*.json"))
|
||||||
|
eth_files = sorted(strategies_dir.glob("eth_*.json"))
|
||||||
|
if not btc_files or not eth_files:
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Expected btc_*.json and eth_*.json in {strategies_dir}"
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
AssetConfig(symbol="BTC-PERPETUAL", strategy_file=btc_files[0]),
|
||||||
|
AssetConfig(symbol="ETH-PERPETUAL", strategy_file=eth_files[0]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = parse_args()
|
||||||
|
settings = load_settings()
|
||||||
|
|
||||||
|
# Inizializza schema (idempotente).
|
||||||
|
Repository(settings.db_path).init_schema()
|
||||||
|
|
||||||
|
token = (
|
||||||
|
settings.cerbero_mainnet_token.get_secret_value()
|
||||||
|
if settings.cerbero_mainnet_token
|
||||||
|
else settings.cerbero_testnet_token.get_secret_value()
|
||||||
|
)
|
||||||
|
cerbero = CerberoClient(
|
||||||
|
base_url=settings.cerbero_base_url,
|
||||||
|
token=token,
|
||||||
|
bot_tag=settings.cerbero_bot_tag,
|
||||||
|
)
|
||||||
|
loader = CerberoOHLCVLoader(client=cerbero, cache_dir=settings.series_dir)
|
||||||
|
|
||||||
|
assets = load_assets(Path(args.strategies_dir))
|
||||||
|
executors: list[PaperExecutor] = [
|
||||||
|
PaperExecutor(strategy_json_path=a.strategy_file, symbol=a.symbol) for a in assets
|
||||||
|
]
|
||||||
|
print(f"Loaded {len(assets)} strategies:")
|
||||||
|
for a, ex in zip(assets, executors, strict=True):
|
||||||
|
print(f" {a.symbol}: {a.strategy_file.name} -> {len(ex._strategy.rules)} rules")
|
||||||
|
|
||||||
|
portfolio = Portfolio(
|
||||||
|
initial_capital=args.initial_capital,
|
||||||
|
fees_bp=args.fees_bp,
|
||||||
|
n_sleeves=len(assets),
|
||||||
|
)
|
||||||
|
repo = PaperRepository(settings.db_path)
|
||||||
|
config = {
|
||||||
|
"assets": [
|
||||||
|
{"symbol": a.symbol, "strategy": a.strategy_file.name, "exchange": a.exchange}
|
||||||
|
for a in assets
|
||||||
|
],
|
||||||
|
"fees_bp": args.fees_bp,
|
||||||
|
"poll_seconds": args.poll_seconds,
|
||||||
|
"lookback_bars": args.lookback_bars,
|
||||||
|
}
|
||||||
|
run_id = repo.create_run(
|
||||||
|
name=args.name, initial_capital=args.initial_capital, config=config
|
||||||
|
)
|
||||||
|
print(f"Paper run started: {run_id} ({args.name})")
|
||||||
|
print(f" initial_capital=${args.initial_capital:.2f}, sleeve=${portfolio.sleeve_capital:.2f}")
|
||||||
|
|
||||||
|
tick_count = 0
|
||||||
|
last_bars_seen: dict[str, datetime] = {}
|
||||||
|
try:
|
||||||
|
while args.max_ticks == 0 or tick_count < args.max_ticks:
|
||||||
|
now = datetime.now(UTC)
|
||||||
|
last_prices: dict[str, float] = {}
|
||||||
|
for asset, executor in zip(assets, executors, strict=True):
|
||||||
|
# fetch OHLCV most recent lookback bars
|
||||||
|
end = now.replace(minute=0, second=0, microsecond=0)
|
||||||
|
start = end - timedelta(hours=args.lookback_bars + 1)
|
||||||
|
req = OHLCVRequest(
|
||||||
|
symbol=asset.symbol,
|
||||||
|
timeframe=asset.timeframe,
|
||||||
|
start=start,
|
||||||
|
end=end,
|
||||||
|
exchange=asset.exchange,
|
||||||
|
)
|
||||||
|
# bypass cache for live data
|
||||||
|
try:
|
||||||
|
ohlcv = loader._fetch(req) # noqa: SLF001
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[{now.isoformat()}] {asset.symbol} fetch FAIL: {e}")
|
||||||
|
continue
|
||||||
|
if len(ohlcv) < 10:
|
||||||
|
print(f"[{now.isoformat()}] {asset.symbol} too few bars ({len(ohlcv)})")
|
||||||
|
continue
|
||||||
|
|
||||||
|
bar_ts = ohlcv.index[-1]
|
||||||
|
last_bar_dt = bar_ts.to_pydatetime() if hasattr(bar_ts, "to_pydatetime") else bar_ts
|
||||||
|
# skip se barra gia' processata in questo tick
|
||||||
|
if last_bars_seen.get(asset.symbol) == last_bar_dt:
|
||||||
|
last_prices[asset.symbol] = float(ohlcv["close"].iloc[-1])
|
||||||
|
continue
|
||||||
|
last_bars_seen[asset.symbol] = last_bar_dt
|
||||||
|
|
||||||
|
result = executor.execute_tick(portfolio, ohlcv, now)
|
||||||
|
repo.save_tick(run_id, result)
|
||||||
|
last_prices[asset.symbol] = result.close_price
|
||||||
|
if result.action_taken != "hold":
|
||||||
|
pnl_str = (
|
||||||
|
f"pnl=${result.trade.net_pnl:+.2f}" if result.trade else ""
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"[{now.isoformat()}] {asset.symbol} bar={last_bar_dt} "
|
||||||
|
f"close={result.close_price:.2f} signal={result.signal.value} "
|
||||||
|
f"action={result.action_taken} {pnl_str}"
|
||||||
|
)
|
||||||
|
|
||||||
|
repo.sync_open_positions(run_id, portfolio)
|
||||||
|
eq, pos_val = portfolio.equity(last_prices)
|
||||||
|
repo.save_equity_snapshot(run_id, now, eq, portfolio.cash, pos_val)
|
||||||
|
|
||||||
|
tick_count += 1
|
||||||
|
print(
|
||||||
|
f"[{now.isoformat()}] tick={tick_count} "
|
||||||
|
f"equity=${eq:.2f} cash=${portfolio.cash:.2f} pos_val=${pos_val:.2f} "
|
||||||
|
f"open={list(portfolio.positions.keys())}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if args.max_ticks > 0 and tick_count >= args.max_ticks:
|
||||||
|
break
|
||||||
|
time.sleep(args.poll_seconds)
|
||||||
|
|
||||||
|
repo.stop_run(run_id, status="completed")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nInterrupted by user")
|
||||||
|
repo.stop_run(run_id, status="interrupted")
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"Run failed: {e}")
|
||||||
|
repo.stop_run(run_id, status="failed")
|
||||||
|
raise
|
||||||
|
|
||||||
|
print(f"Paper run {run_id} stopped after {tick_count} ticks")
|
||||||
|
print(f"Final equity: ${portfolio.equity({})[0]:.2f}")
|
||||||
|
print(f"Trades closed: {len(portfolio.closed_trades)}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
"""PaperExecutor: applica un segnale di strategia a un Portfolio.
|
||||||
|
|
||||||
|
Il flusso per ogni tick:
|
||||||
|
|
||||||
|
bar OHLCV chiuso -> compile_strategy(strategy) -> Series[Side]
|
||||||
|
-> last_signal = series.iloc[-1]
|
||||||
|
-> match con posizione attuale -> open / close / hold
|
||||||
|
|
||||||
|
Niente delay 1-bar: in paper-trading il segnale viene calcolato sulla
|
||||||
|
barra appena chiusa e applicato al prezzo close della stessa. La latenza
|
||||||
|
reale tra tick e ordine va misurata separatamente (Phase 3 spec).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pandas as pd # type: ignore[import-untyped]
|
||||||
|
|
||||||
|
from ..backtest.orders import Side, Trade
|
||||||
|
from ..protocol.compiler import compile_strategy
|
||||||
|
from ..protocol.parser import parse_strategy
|
||||||
|
from .portfolio import OpenPosition, Portfolio
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TickResult:
|
||||||
|
ts: datetime
|
||||||
|
symbol: str
|
||||||
|
bar_ts: datetime
|
||||||
|
close_price: float
|
||||||
|
signal: Side
|
||||||
|
action_taken: str # "open_long" | "open_short" | "close" | "reverse" | "hold"
|
||||||
|
trade: Trade | None = None
|
||||||
|
new_position: OpenPosition | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class PaperExecutor:
|
||||||
|
def __init__(self, strategy_json_path: Path, symbol: str) -> None:
|
||||||
|
text = strategy_json_path.read_text()
|
||||||
|
# parse_strategy si aspetta JSON pulito, non fence; il file e' gia' JSON.
|
||||||
|
self._strategy = parse_strategy(text)
|
||||||
|
self._compiled = compile_strategy(self._strategy)
|
||||||
|
self.symbol = symbol
|
||||||
|
self.strategy_path = strategy_json_path
|
||||||
|
|
||||||
|
def execute_tick(
|
||||||
|
self,
|
||||||
|
portfolio: Portfolio,
|
||||||
|
ohlcv: pd.DataFrame,
|
||||||
|
now: datetime,
|
||||||
|
) -> TickResult:
|
||||||
|
"""Esegui un tick: calcola segnale su tutto ``ohlcv`` (per indicatori
|
||||||
|
con lookback), prendi l'ultimo, e applica al portfolio."""
|
||||||
|
if len(ohlcv) == 0:
|
||||||
|
raise ValueError("Empty OHLCV passed to execute_tick")
|
||||||
|
signals = self._compiled(ohlcv)
|
||||||
|
# ultimo bar chiuso
|
||||||
|
bar_ts = ohlcv.index[-1]
|
||||||
|
close_price = float(ohlcv["close"].iloc[-1])
|
||||||
|
signal = Side(signals.iloc[-1]) if signals.iloc[-1] is not None else Side.FLAT
|
||||||
|
|
||||||
|
current = portfolio.positions.get(self.symbol)
|
||||||
|
action = "hold"
|
||||||
|
trade: Trade | None = None
|
||||||
|
new_position: OpenPosition | None = None
|
||||||
|
|
||||||
|
if current is None and signal != Side.FLAT:
|
||||||
|
new_position = portfolio.open(self.symbol, signal, close_price, now)
|
||||||
|
action = f"open_{signal.value}"
|
||||||
|
elif current is not None and signal == Side.FLAT:
|
||||||
|
trade = portfolio.close(self.symbol, close_price, now)
|
||||||
|
action = "close"
|
||||||
|
elif current is not None and signal != current.side:
|
||||||
|
# reverse: chiudi e riapri opposto
|
||||||
|
trade = portfolio.close(self.symbol, close_price, now)
|
||||||
|
new_position = portfolio.open(self.symbol, signal, close_price, now)
|
||||||
|
action = "reverse"
|
||||||
|
|
||||||
|
return TickResult(
|
||||||
|
ts=now,
|
||||||
|
symbol=self.symbol,
|
||||||
|
bar_ts=bar_ts.to_pydatetime() if hasattr(bar_ts, "to_pydatetime") else bar_ts,
|
||||||
|
close_price=close_price,
|
||||||
|
signal=signal,
|
||||||
|
action_taken=action,
|
||||||
|
trade=trade,
|
||||||
|
new_position=new_position,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def strategy_dict(self) -> dict:
|
||||||
|
return json.loads(self.strategy_path.read_text())
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
"""Persistenza paper-trading: usa lo stesso ``runs.db`` con tabelle dedicate
|
||||||
|
``paper_trading_*`` (vedi :mod:`multi_swarm.persistence.schema`).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
import uuid
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .executor import TickResult
|
||||||
|
from .portfolio import Portfolio
|
||||||
|
|
||||||
|
|
||||||
|
class PaperRepository:
|
||||||
|
def __init__(self, db_path: Path | str):
|
||||||
|
self.db_path = Path(db_path)
|
||||||
|
|
||||||
|
def _conn(self) -> sqlite3.Connection:
|
||||||
|
conn = sqlite3.connect(self.db_path, isolation_level=None)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
conn.execute("PRAGMA foreign_keys = ON")
|
||||||
|
conn.execute("PRAGMA journal_mode = WAL")
|
||||||
|
return conn
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _now() -> str:
|
||||||
|
return datetime.now(UTC).isoformat()
|
||||||
|
|
||||||
|
def create_run(self, name: str, initial_capital: float, config: dict[str, Any]) -> str:
|
||||||
|
rid = uuid.uuid4().hex
|
||||||
|
with self._conn() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO paper_trading_runs "
|
||||||
|
"(id, name, started_at, status, initial_capital, config_json) "
|
||||||
|
"VALUES (?,?,?,?,?,?)",
|
||||||
|
(rid, name, self._now(), "running", initial_capital, json.dumps(config)),
|
||||||
|
)
|
||||||
|
return rid
|
||||||
|
|
||||||
|
def stop_run(self, run_id: str, status: str = "stopped") -> None:
|
||||||
|
with self._conn() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE paper_trading_runs SET stopped_at=?, status=? WHERE id=?",
|
||||||
|
(self._now(), status, run_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
def save_tick(self, run_id: str, tick: TickResult) -> None:
|
||||||
|
with self._conn() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO paper_trading_ticks "
|
||||||
|
"(paper_run_id, symbol, ts, bar_ts, close_price, signal, action_taken) "
|
||||||
|
"VALUES (?,?,?,?,?,?,?)",
|
||||||
|
(
|
||||||
|
run_id,
|
||||||
|
tick.symbol,
|
||||||
|
tick.ts.isoformat(),
|
||||||
|
tick.bar_ts.isoformat() if hasattr(tick.bar_ts, "isoformat") else str(tick.bar_ts),
|
||||||
|
tick.close_price,
|
||||||
|
tick.signal.value,
|
||||||
|
tick.action_taken,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if tick.trade is not None:
|
||||||
|
t = tick.trade
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO paper_trading_trades "
|
||||||
|
"(paper_run_id, symbol, side, qty, entry_price, exit_price, "
|
||||||
|
"entry_ts, exit_ts, pnl, fees) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
||||||
|
(
|
||||||
|
run_id,
|
||||||
|
tick.symbol,
|
||||||
|
t.side.value,
|
||||||
|
t.size,
|
||||||
|
t.entry_price,
|
||||||
|
t.exit_price,
|
||||||
|
t.entry_ts.isoformat(),
|
||||||
|
t.exit_ts.isoformat(),
|
||||||
|
t.net_pnl,
|
||||||
|
t.fees,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def save_equity_snapshot(
|
||||||
|
self,
|
||||||
|
run_id: str,
|
||||||
|
ts: datetime,
|
||||||
|
equity: float,
|
||||||
|
cash: float,
|
||||||
|
positions_value: float,
|
||||||
|
) -> None:
|
||||||
|
with self._conn() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO paper_trading_equity "
|
||||||
|
"(paper_run_id, ts, equity, cash, positions_value) VALUES (?,?,?,?,?)",
|
||||||
|
(run_id, ts.isoformat(), equity, cash, positions_value),
|
||||||
|
)
|
||||||
|
|
||||||
|
def sync_open_positions(self, run_id: str, portfolio: Portfolio) -> None:
|
||||||
|
"""Sostituisce snapshot posizioni aperte. Idempotente: cancella e reinserisce."""
|
||||||
|
with self._conn() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM paper_trading_positions WHERE paper_run_id=?", (run_id,)
|
||||||
|
)
|
||||||
|
for sym, pos in portfolio.positions.items():
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO paper_trading_positions "
|
||||||
|
"(paper_run_id, symbol, side, qty, entry_price, entry_ts) "
|
||||||
|
"VALUES (?,?,?,?,?,?)",
|
||||||
|
(run_id, sym, pos.side.value, pos.qty, pos.entry_price, pos.entry_ts.isoformat()),
|
||||||
|
)
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
"""Portfolio multi-asset per paper-trading.
|
||||||
|
|
||||||
|
Modello semplificato: capitale unico ``cash``, allocazione equal-weight
|
||||||
|
fra N posizioni (sleeve = 1/N del capitale iniziale per ogni simbolo).
|
||||||
|
Niente leva, niente liquidation, fees su entry+exit (bp del notional).
|
||||||
|
|
||||||
|
Una :class:`Position` rappresenta una posizione aperta su un singolo
|
||||||
|
simbolo (long/short, qty in unita' dell'asset, prezzo di entry). La
|
||||||
|
posizione viene chiusa con :meth:`Portfolio.close` che produce un
|
||||||
|
:class:`Trade` realized e accredita ``cash``.
|
||||||
|
|
||||||
|
Mark-to-market via :meth:`Portfolio.equity`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from ..backtest.orders import Side, Trade
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class OpenPosition:
|
||||||
|
symbol: str
|
||||||
|
side: Side
|
||||||
|
qty: float
|
||||||
|
entry_price: float
|
||||||
|
entry_ts: datetime
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Portfolio:
|
||||||
|
initial_capital: float
|
||||||
|
fees_bp: float = 5.0
|
||||||
|
n_sleeves: int = 2 # numero strategie / asset previsti
|
||||||
|
cash: float = field(init=False)
|
||||||
|
positions: dict[str, OpenPosition] = field(default_factory=dict)
|
||||||
|
closed_trades: list[Trade] = field(default_factory=list)
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
self.cash = self.initial_capital
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sleeve_capital(self) -> float:
|
||||||
|
return self.initial_capital / self.n_sleeves
|
||||||
|
|
||||||
|
def open(
|
||||||
|
self,
|
||||||
|
symbol: str,
|
||||||
|
side: Side,
|
||||||
|
price: float,
|
||||||
|
ts: datetime,
|
||||||
|
) -> OpenPosition:
|
||||||
|
if symbol in self.positions:
|
||||||
|
raise ValueError(f"Position already open on {symbol}")
|
||||||
|
if side == Side.FLAT:
|
||||||
|
raise ValueError("Cannot open a FLAT position")
|
||||||
|
# sleeve fisso: alloca 1/n_sleeves del capitale iniziale, qty = notional/price.
|
||||||
|
notional = self.sleeve_capital
|
||||||
|
qty = notional / price
|
||||||
|
fees = notional * (self.fees_bp / 10000.0)
|
||||||
|
self.cash -= fees
|
||||||
|
pos = OpenPosition(symbol=symbol, side=side, qty=qty, entry_price=price, entry_ts=ts)
|
||||||
|
self.positions[symbol] = pos
|
||||||
|
return pos
|
||||||
|
|
||||||
|
def close(
|
||||||
|
self,
|
||||||
|
symbol: str,
|
||||||
|
price: float,
|
||||||
|
ts: datetime,
|
||||||
|
) -> Trade:
|
||||||
|
if symbol not in self.positions:
|
||||||
|
raise ValueError(f"No open position on {symbol}")
|
||||||
|
pos = self.positions.pop(symbol)
|
||||||
|
trade = Trade(
|
||||||
|
entry_ts=pos.entry_ts,
|
||||||
|
exit_ts=ts,
|
||||||
|
side=pos.side,
|
||||||
|
size=pos.qty,
|
||||||
|
entry_price=pos.entry_price,
|
||||||
|
exit_price=price,
|
||||||
|
fees_bp=self.fees_bp,
|
||||||
|
)
|
||||||
|
# net_pnl include gia' i fees sull'intero round-trip; abbiamo gia'
|
||||||
|
# addebitato meta' fees all'open, ora addebitiamo il resto.
|
||||||
|
self.cash += trade.gross_pnl - (trade.fees / 2.0)
|
||||||
|
self.closed_trades.append(trade)
|
||||||
|
return trade
|
||||||
|
|
||||||
|
def equity(self, last_prices: dict[str, float]) -> tuple[float, float]:
|
||||||
|
"""Ritorna (equity_totale, positions_value) marcando posizioni aperte
|
||||||
|
al ``last_prices[symbol]``. Posizioni senza prezzo disponibile valgono
|
||||||
|
notional di entry (fallback conservativo)."""
|
||||||
|
positions_value = 0.0
|
||||||
|
for sym, pos in self.positions.items():
|
||||||
|
price = last_prices.get(sym, pos.entry_price)
|
||||||
|
unreal = pos.qty * (
|
||||||
|
price - pos.entry_price if pos.side == Side.LONG
|
||||||
|
else pos.entry_price - price
|
||||||
|
)
|
||||||
|
positions_value += pos.qty * pos.entry_price + unreal
|
||||||
|
return self.cash + positions_value, positions_value
|
||||||
@@ -77,7 +77,68 @@ CREATE TABLE IF NOT EXISTS adversarial_findings (
|
|||||||
FOREIGN KEY (run_id) REFERENCES runs(id)
|
FOREIGN KEY (run_id) REFERENCES runs(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS paper_trading_runs (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
started_at TEXT NOT NULL,
|
||||||
|
stopped_at TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'running',
|
||||||
|
initial_capital REAL NOT NULL,
|
||||||
|
config_json TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS paper_trading_positions (
|
||||||
|
paper_run_id TEXT NOT NULL,
|
||||||
|
symbol TEXT NOT NULL,
|
||||||
|
side TEXT NOT NULL,
|
||||||
|
qty REAL NOT NULL,
|
||||||
|
entry_price REAL NOT NULL,
|
||||||
|
entry_ts TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (paper_run_id, symbol),
|
||||||
|
FOREIGN KEY (paper_run_id) REFERENCES paper_trading_runs(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS paper_trading_trades (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
paper_run_id TEXT NOT NULL,
|
||||||
|
symbol TEXT NOT NULL,
|
||||||
|
side TEXT NOT NULL,
|
||||||
|
qty REAL NOT NULL,
|
||||||
|
entry_price REAL NOT NULL,
|
||||||
|
exit_price REAL NOT NULL,
|
||||||
|
entry_ts TEXT NOT NULL,
|
||||||
|
exit_ts TEXT NOT NULL,
|
||||||
|
pnl REAL NOT NULL,
|
||||||
|
fees REAL NOT NULL,
|
||||||
|
FOREIGN KEY (paper_run_id) REFERENCES paper_trading_runs(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS paper_trading_equity (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
paper_run_id TEXT NOT NULL,
|
||||||
|
ts TEXT NOT NULL,
|
||||||
|
equity REAL NOT NULL,
|
||||||
|
cash REAL NOT NULL,
|
||||||
|
positions_value REAL NOT NULL,
|
||||||
|
FOREIGN KEY (paper_run_id) REFERENCES paper_trading_runs(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS paper_trading_ticks (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
paper_run_id TEXT NOT NULL,
|
||||||
|
symbol TEXT NOT NULL,
|
||||||
|
ts TEXT NOT NULL,
|
||||||
|
bar_ts TEXT NOT NULL,
|
||||||
|
close_price REAL NOT NULL,
|
||||||
|
signal TEXT NOT NULL,
|
||||||
|
action_taken TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (paper_run_id) REFERENCES paper_trading_runs(id)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_evaluations_fitness ON evaluations(run_id, fitness DESC);
|
CREATE INDEX IF NOT EXISTS idx_evaluations_fitness ON evaluations(run_id, fitness DESC);
|
||||||
CREATE INDEX IF NOT EXISTS idx_genomes_generation ON genomes(run_id, generation_idx);
|
CREATE INDEX IF NOT EXISTS idx_genomes_generation ON genomes(run_id, generation_idx);
|
||||||
CREATE INDEX IF NOT EXISTS idx_cost_run ON cost_records(run_id);
|
CREATE INDEX IF NOT EXISTS idx_cost_run ON cost_records(run_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_paper_trades_run ON paper_trading_trades(paper_run_id, exit_ts);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_paper_equity_run ON paper_trading_equity(paper_run_id, ts);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_paper_ticks_run ON paper_trading_ticks(paper_run_id, ts);
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
{
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"condition": {
|
||||||
|
"op": "and",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"op": "gt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "rsi",
|
||||||
|
"params": [
|
||||||
|
14
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 70.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "gt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "atr",
|
||||||
|
"params": [
|
||||||
|
14
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "sma",
|
||||||
|
"params": [
|
||||||
|
14
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "gt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "feature",
|
||||||
|
"name": "hour"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 9
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "lt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "feature",
|
||||||
|
"name": "hour"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 17
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"action": "entry-short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condition": {
|
||||||
|
"op": "and",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"op": "lt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "rsi",
|
||||||
|
"params": [
|
||||||
|
14
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 30.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "lt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "atr",
|
||||||
|
"params": [
|
||||||
|
14
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "sma",
|
||||||
|
"params": [
|
||||||
|
14
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "gt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "feature",
|
||||||
|
"name": "hour"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 9
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "lt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "feature",
|
||||||
|
"name": "hour"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 17
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"action": "entry-long"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
{
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"condition": {
|
||||||
|
"op": "and",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"op": "gt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "atr",
|
||||||
|
"params": [
|
||||||
|
14
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 0.02
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "gt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "realized_vol",
|
||||||
|
"params": [
|
||||||
|
20
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 0.03
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "gt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "sma",
|
||||||
|
"params": [
|
||||||
|
50
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "sma",
|
||||||
|
"params": [
|
||||||
|
200
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"action": "entry-long"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condition": {
|
||||||
|
"op": "and",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"op": "lt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "atr",
|
||||||
|
"params": [
|
||||||
|
14
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 0.01
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "lt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "realized_vol",
|
||||||
|
"params": [
|
||||||
|
20
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 0.02
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "lt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "sma",
|
||||||
|
"params": [
|
||||||
|
50
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "sma",
|
||||||
|
"params": [
|
||||||
|
200
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"action": "entry-short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condition": {
|
||||||
|
"op": "or",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"op": "crossover",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "rsi",
|
||||||
|
"params": [
|
||||||
|
14
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 70.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "crossunder",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "rsi",
|
||||||
|
"params": [
|
||||||
|
14
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 30.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"action": "exit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user