stock/portfolio.py
2025-08-15 12:19:07 +02:00

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()])