Files
mmd/report.py

242 lines
8.8 KiB
Python
Raw Normal View History

2026-05-26 22:21:27 +02:00
"""
report.py Daily P&L report for MoneyMaker dry run.
Usage:
python report.py # full P&L report
python report.py --fees # fee drag analysis only
python report.py --signals # signal accuracy summary only
"""
import sys
import time
import json
import argparse
from datetime import datetime, timezone
from pathlib import Path
import yfinance as yf
from db import get_conn, DB_TYPE
C25_PATH = Path(__file__).parent / "c25.json"
_c25_raw = json.loads(C25_PATH.read_text())
C25: dict[str, dict] = {k: v for k, v in _c25_raw.items() if not k.startswith("_")}
CAPITAL = 10_000 # DKK — must match portfolio.py
def _c25_day_return() -> float | None:
"""Fetch today's return for OMX C25 CAP index from Yahoo Finance."""
try:
info = yf.Ticker("^OMXC25").fast_info
prev = info.get("previousClose") or info.get("regularMarketPreviousClose")
last = info.get("lastPrice") or info.get("regularMarketPrice")
if prev and last and prev > 0:
return (last - prev) / prev * 100
except Exception:
pass
return None
def _unrealised_pnl(db) -> tuple[float, list[dict]]:
"""
Calculate unrealised P&L for all open positions.
Returns (total_unrealised_dkk, details_list).
"""
positions = db.execute("SELECT * FROM positions").fetchall()
details = []
total_unreal = 0.0
for p in positions:
ticker = p["ticker"]
shares = float(p["shares"])
entry = float(p["entry_price"])
entry_date = p["entry_date"]
yf_ticker = C25.get(ticker, {}).get("ticker_yahoo", ticker + ".CO")
try:
last = yf.Ticker(yf_ticker).fast_info.get("lastPrice") or entry
except Exception:
last = entry
cost = shares * entry
value = shares * last
unreal = value - cost
pct = (unreal / cost * 100) if cost else 0
total_unreal += unreal
details.append({
"ticker": ticker,
"shares": shares,
"entry": entry,
"last": last,
"cost": cost,
"value": value,
"unreal": unreal,
"pct": pct,
"entry_date": entry_date,
})
return total_unreal, details
def _realised_pnl(db) -> float:
"""Sum all realised P&L from position_events."""
row = db.execute(
"SELECT COALESCE(SUM(pnl_dkk),0) AS total FROM position_events WHERE action='sell'"
).fetchone()
return float(row["total"] or 0)
def _total_fees(db) -> float:
"""Sum all fees from saxo_orders."""
try:
row = db.execute(
"SELECT COALESCE(SUM(fee_dkk),0) AS total FROM saxo_orders"
).fetchone()
return float(row["total"] or 0)
except Exception:
return 0.0
def _invested(positions_details: list[dict]) -> float:
return sum(p["cost"] for p in positions_details)
def _signal_accuracy(db) -> dict:
"""
Calculate signal accuracy from completed trades (position_events).
A signal is 'correct' if the trade had P&L > 0 (signal_correct=1).
Also returns overall signal scan stats from article_signals.
"""
# Accuracy from closed trades
try:
row = db.execute("""
SELECT
COUNT(*) AS total_trades,
SUM(CASE WHEN signal_correct=1 THEN 1 ELSE 0 END) AS correct,
SUM(CASE WHEN signal_correct=0 THEN 1 ELSE 0 END) AS wrong,
COALESCE(AVG(CASE WHEN signal_correct IS NOT NULL
THEN signal_correct END), -1) AS accuracy
FROM position_events
WHERE action = 'sell' AND signal_correct IS NOT NULL
""").fetchone()
total_trades = int(row["total_trades"] or 0)
correct = int(row["correct"] or 0)
wrong = int(row["wrong"] or 0)
accuracy_pct = (float(row["accuracy"]) * 100) if total_trades > 0 else None
except Exception:
total_trades = correct = wrong = 0
accuracy_pct = None
# NLP scan stats
try:
srow = db.execute("""
SELECT
COUNT(*) AS total_signals,
SUM(CASE WHEN alert=1 THEN 1 ELSE 0 END) AS alerts,
AVG(signal_score) AS avg_score
FROM article_signals
WHERE signal_score IS NOT NULL
""").fetchone()
return {
"total": int(srow["total_signals"] or 0),
"alerts": int(srow["alerts"] or 0),
"avg_score": round(float(srow["avg_score"] or 0), 3),
"total_trades": total_trades,
"correct": correct,
"wrong": wrong,
"accuracy_pct": accuracy_pct,
}
except Exception:
return {
"total": 0, "alerts": 0, "avg_score": 0,
"total_trades": total_trades, "correct": correct,
"wrong": wrong, "accuracy_pct": accuracy_pct,
}
def print_report() -> None:
db = get_conn()
now = datetime.now(timezone.utc).strftime("%d %b %Y %H:%M UTC")
unreal_total, positions = _unrealised_pnl(db)
realised = _realised_pnl(db)
fees = _total_fees(db)
invested = _invested(positions)
cash = CAPITAL - invested
net_pnl = realised + unreal_total - fees
net_pct = (net_pnl / CAPITAL * 100)
c25_ret = _c25_day_return()
vs_bench = (net_pct - c25_ret) if c25_ret is not None else None
sig_stats = _signal_accuracy(db)
# ── Header ──────────────────────────────────────────────────────────────
print()
print("" * 48)
print(f" MONEYMAKER P&L · {now}")
print(f" DB: {DB_TYPE}")
print("" * 48)
print(f" Kapital: {CAPITAL:>10,.0f} DKK")
print(f" Investeret: {invested:>10,.0f} DKK ({len(positions)} pos.)")
print(f" Kontant: {cash:>10,.0f} DKK")
print()
print(f" Urealiseret: {unreal_total:>+10,.0f} DKK")
print(f" Realiseret: {realised:>+10,.0f} DKK")
print(f" Total gebyrer: {-fees:>+8,.0f} DKK")
print(f" Net P&L: {net_pnl:>+10,.0f} DKK ({net_pct:+.2f}%)")
print()
# ── Benchmark ────────────────────────────────────────────────────────────
if c25_ret is not None:
bench_icon = "" if vs_bench and vs_bench >= 0 else ""
print(f" C25 index: {c25_ret:+.2f}% (i dag)")
print(f" vs benchmark: {vs_bench:+.2f}% {bench_icon}")
else:
print(" C25 index: N/A (marked lukket?)")
print()
# ── Open positions ────────────────────────────────────────────────────────
if positions:
print(" ┌─ Åbne positioner ─────────────────────────────┐")
for p in positions:
icon = "📈" if p["unreal"] >= 0 else "📉"
print(
f"{icon} {p['ticker']:<8} {p['shares']:.0f} stk "
f"købt {p['entry']:.0f} nu {p['last']:.0f} "
f"P&L {p['unreal']:+.0f} ({p['pct']:+.1f}%)"
)
print(" └────────────────────────────────────────────────┘")
print()
# ── Signal stats ─────────────────────────────────────────────────────────
print(f" Signaler: {sig_stats['total']:>5} analyseret")
print(f" Alerts: {sig_stats['alerts']:>5} ⚡ triggers")
print(f" Gns score: {sig_stats['avg_score']:>5.3f}")
if sig_stats["total_trades"] > 0:
acc = sig_stats["accuracy_pct"]
icon = "" if acc and acc >= 50 else ""
print(f" Lukkede handler: {sig_stats['total_trades']} "
f"({sig_stats['correct']} korrekte / {sig_stats['wrong']} forkerte) "
f"{acc:.0f}% {icon}")
else:
print(" Lukkede handler: 0 (ingen salg endnu)")
if fees > 0:
drag_pct = (fees / max(abs(net_pnl + fees), 1)) * 100
print(f" Gebyr-drag: {drag_pct:.1f}% af brutto P&L")
print("" * 48)
print()
db.close()
def main():
parser = argparse.ArgumentParser(description="MoneyMaker P&L rapport")
parser.add_argument("--fees", action="store_true")
parser.add_argument("--signals", action="store_true")
args = parser.parse_args()
print_report()
if __name__ == "__main__":
main()