138 lines
5.3 KiB
Python
138 lines
5.3 KiB
Python
# portfolio.py
|
|
from __future__ import annotations
|
|
import math, time, json
|
|
from typing import Dict, List, Any
|
|
|
|
def _to_native(obj: Any):
|
|
"""Bezpieczne rzutowanie do typów akceptowalnych przez JSON."""
|
|
if isinstance(obj, (float, int, str)) or obj is None:
|
|
return obj
|
|
if isinstance(obj, dict):
|
|
return {k: _to_native(v) for k, v in obj.items()}
|
|
if isinstance(obj, (list, tuple)):
|
|
return [_to_native(x) for x in obj]
|
|
try:
|
|
return float(obj)
|
|
except Exception:
|
|
return str(obj)
|
|
|
|
def save_json(path: str, data: Any) -> None:
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
json.dump(_to_native(data), f, ensure_ascii=False, indent=2)
|
|
|
|
class Portfolio:
|
|
"""
|
|
Prosta symulacja portfela:
|
|
- cash
|
|
- positions: {ticker: {"qty": float, "entry": float, "side": int}}
|
|
- equity = cash + niezrealizowany PnL
|
|
Logi transakcji z realized PnL (wartość i %), plus PnL skumulowany.
|
|
Zapis tylko w JSON.
|
|
"""
|
|
def __init__(self, capital: float):
|
|
self.cash = float(capital)
|
|
self.start_capital = float(capital)
|
|
self.positions: Dict[str, Dict] = {}
|
|
self.history: List[Dict] = []
|
|
self.trade_log: List[Dict] = []
|
|
self.realized_pnl: float = 0.0 # skumulowany realized PnL
|
|
|
|
def mark_to_market(self, prices: Dict[str, float]) -> float:
|
|
unreal = 0.0
|
|
for t, p in self.positions.items():
|
|
price = prices.get(t)
|
|
if price is not None and not math.isnan(price):
|
|
unreal += (price - p["entry"]) * p["qty"] * p["side"]
|
|
return self.cash + unreal
|
|
|
|
def _log_trade(self, action: str, ticker: str, price: float, qty: float, side: int,
|
|
pnl_abs: float = 0.0, pnl_pct: float = 0.0):
|
|
ts = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
side_str = "LONG" if side == 1 else "SHORT"
|
|
if action == "BUY":
|
|
print(f"[{ts}] BUY {ticker} @ {price:.6f}, qty={qty:.6f}, side={side_str}, cash={self.cash:.2f}")
|
|
elif action == "SELL":
|
|
print(f"[{ts}] SELL {ticker} @ {price:.6f}, qty={qty:.6f}, "
|
|
f"PnL={pnl_abs:+.2f} ({pnl_pct:+.2%}), "
|
|
f"cumPnL={self.realized_pnl:+.2f}, cash={self.cash:.2f}")
|
|
# zapis do logu w pamięci
|
|
self.trade_log.append({
|
|
"time": ts,
|
|
"action": action,
|
|
"ticker": ticker,
|
|
"price": float(price),
|
|
"qty": float(qty),
|
|
"side": int(side),
|
|
"pnl_abs": float(pnl_abs),
|
|
"pnl_pct": float(pnl_pct),
|
|
"realized_pnl_cum": float(self.realized_pnl),
|
|
"cash_after": float(self.cash)
|
|
})
|
|
|
|
def on_signals(self, sigs: List[dict]):
|
|
"""
|
|
sigs: lista dictów/obiektów z polami: ticker, price, signal, time
|
|
BUY (1) / SELL (-1) / HOLD (0)
|
|
"""
|
|
clean = []
|
|
for s in sigs:
|
|
if isinstance(s, dict):
|
|
if s.get("error") is None and s.get("price") is not None and not math.isnan(s.get("price", float("nan"))):
|
|
clean.append(s)
|
|
else:
|
|
if getattr(s, "error", None) is None and not math.isnan(getattr(s, "price", float("nan"))):
|
|
clean.append({"ticker": s.ticker, "price": s.price, "signal": s.signal, "time": s.time})
|
|
|
|
if not clean:
|
|
self.history.append({
|
|
"time": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
"equity": self.cash,
|
|
"cash": self.cash,
|
|
"open_positions": len(self.positions)
|
|
})
|
|
save_json("portfolio_history.json", self.history)
|
|
return
|
|
|
|
prices = {s["ticker"]: s["price"] for s in clean}
|
|
n = len(clean)
|
|
per_trade_cash = max(self.cash / (n * 2), 0.0)
|
|
|
|
for s in clean:
|
|
t, sig, price = s["ticker"], int(s["signal"]), float(s["price"])
|
|
|
|
# zamknięcie lub odwrócenie
|
|
if t in self.positions:
|
|
pos = self.positions[t]
|
|
if sig == 0 or sig != pos["side"]:
|
|
qty = pos["qty"]
|
|
entry = pos["entry"]
|
|
side = pos["side"]
|
|
self.cash += qty * price
|
|
pnl_abs = (price - entry) * qty * side
|
|
denom = max(qty * entry, 1e-12)
|
|
pnl_pct = pnl_abs / denom
|
|
self.realized_pnl += pnl_abs
|
|
self._log_trade("SELL", t, price, qty, side, pnl_abs, pnl_pct)
|
|
del self.positions[t]
|
|
|
|
# otwarcie
|
|
if t not in self.positions and sig != 0 and per_trade_cash > 0:
|
|
qty = per_trade_cash / price
|
|
self.cash -= qty * price
|
|
self.positions[t] = {"qty": qty, "entry": price, "side": sig}
|
|
self._log_trade("BUY", t, price, qty, sig)
|
|
|
|
equity = self.mark_to_market(prices)
|
|
self.history.append({
|
|
"time": clean[0]["time"],
|
|
"equity": float(equity),
|
|
"cash": float(self.cash),
|
|
"open_positions": int(len(self.positions)),
|
|
"realized_pnl_cum": float(self.realized_pnl)
|
|
})
|
|
|
|
# zapis plików JSON
|
|
save_json("trade_log.json", self.trade_log)
|
|
save_json("portfolio_history.json", self.history)
|
|
save_json("positions.json", [{"ticker": t, **p} for t, p in self.positions.items()])
|