First commit

This commit is contained in:
Henrik Jess Nielsen
2026-05-26 22:21:27 +02:00
parent 2743a236b2
commit 05eed51e7d
90 changed files with 8690 additions and 0 deletions

327
dashboard.py Normal file
View File

@@ -0,0 +1,327 @@
"""
dashboard.py — MoneyMaker live monitoring dashboard.
Usage:
python dashboard.py # starts on http://localhost:5001
python dashboard.py --port 5002
Auto-refreshes every 60 seconds. Shows:
• Portfolio P&L + C25 benchmark
• Open positions with live prices
• Closed trades (win/loss)
• Signal accuracy
• Recent runner log tail
"""
import argparse
import json
import time
from datetime import datetime, timezone
from pathlib import Path
import yfinance as yf
from flask import Flask, render_template_string
from db import get_conn, DB_TYPE
from report import _c25_day_return, _unrealised_pnl, _realised_pnl, _total_fees, _signal_accuracy
CAPITAL = 10_000
LOG_DIR = Path(__file__).parent / "logs"
REFRESH = 60 # seconds
app = Flask(__name__)
# ── HTML template ────────────────────────────────────────────────────────────
TEMPLATE = """
<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="refresh" content="{{ refresh }}">
<title>MoneyMaker Dashboard</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #0f1117; color: #e2e8f0; padding: 24px; }
h1 { font-size: 1.4rem; font-weight: 700; color: #7dd3fc; margin-bottom: 4px; }
.subtitle { font-size: 0.8rem; color: #64748b; margin-bottom: 24px; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
.card { background: #1e2535; border-radius: 10px; padding: 18px 20px; border: 1px solid #2d3748; }
.card .label { font-size: 0.72rem; text-transform: uppercase; letter-spacing: .06em; color: #64748b; margin-bottom: 6px; }
.card .value { font-size: 1.6rem; font-weight: 700; }
.card .sub { font-size: 0.78rem; color: #94a3b8; margin-top: 4px; }
.pos { color: #4ade80; }
.neg { color: #f87171; }
.neu { color: #94a3b8; }
.warn { color: #fbbf24; }
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
th { text-align: left; padding: 8px 12px; background: #1e2535; color: #64748b; font-size: 0.72rem; text-transform: uppercase; letter-spacing: .06em; border-bottom: 1px solid #2d3748; }
td { padding: 9px 12px; border-bottom: 1px solid #1a2030; }
tr:hover td { background: #1e2535; }
.section { background: #161c2d; border-radius: 10px; border: 1px solid #2d3748; margin-bottom: 20px; overflow: hidden; }
.section-title { padding: 14px 18px; font-size: 0.8rem; font-weight: 600; color: #7dd3fc; text-transform: uppercase; letter-spacing: .08em; border-bottom: 1px solid #2d3748; background: #1a2236; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 99px; font-size: 0.72rem; font-weight: 600; }
.badge-green { background: #14532d; color: #4ade80; }
.badge-red { background: #450a0a; color: #f87171; }
.badge-gray { background: #1e2535; color: #94a3b8; }
pre { font-family: 'Cascadia Code', 'Fira Mono', monospace; font-size: 0.75rem; color: #94a3b8; padding: 16px 18px; overflow-x: auto; white-space: pre-wrap; line-height: 1.6; }
.footer { font-size: 0.72rem; color: #334155; text-align: center; margin-top: 32px; }
</style>
</head>
<body>
<h1>📈 MoneyMaker</h1>
<div class="subtitle">DB: {{ db_type }} &nbsp;·&nbsp; Opdateret: {{ now }} &nbsp;·&nbsp; Refresh om {{ refresh }}s</div>
<!-- KPI cards -->
<div class="grid">
<div class="card">
<div class="label">Net P&amp;L</div>
<div class="value {{ 'pos' if net_pnl >= 0 else 'neg' }}">{{ "{:+,.0f}".format(net_pnl) }} kr</div>
<div class="sub">{{ "{:+.2f}%".format(net_pct) }}</div>
</div>
<div class="card">
<div class="label">Urealiseret</div>
<div class="value {{ 'pos' if unreal >= 0 else 'neg' }}">{{ "{:+,.0f}".format(unreal) }} kr</div>
<div class="sub">{{ open_count }} åben{{ 'e' if open_count != 1 else '' }} position{{ 'er' if open_count != 1 else '' }}</div>
</div>
<div class="card">
<div class="label">Realiseret</div>
<div class="value {{ 'pos' if realised >= 0 else 'neg' }}">{{ "{:+,.0f}".format(realised) }} kr</div>
<div class="sub">Gebyrer: {{ "{:,.0f}".format(fees) }} kr</div>
</div>
<div class="card">
<div class="label">C25 i dag</div>
{% if c25_ret is not none %}
<div class="value {{ 'pos' if c25_ret >= 0 else 'neg' }}">{{ "{:+.2f}%".format(c25_ret) }}</div>
<div class="sub {{ 'pos' if vs_bench and vs_bench >= 0 else 'neg' }}">
vs benchmark: {{ "{:+.2f}%".format(vs_bench) if vs_bench is not none else "" }}
</div>
{% else %}
<div class="value neu">—</div>
<div class="sub">Marked lukket?</div>
{% endif %}
</div>
<div class="card">
<div class="label">Signal accuracy</div>
{% if sig.total_trades > 0 %}
<div class="value {{ 'pos' if sig.accuracy_pct >= 50 else 'neg' }}">{{ "{:.0f}%".format(sig.accuracy_pct) }}</div>
<div class="sub">{{ sig.correct }} / {{ sig.total_trades }} handler korrekte</div>
{% else %}
<div class="value neu">—</div>
<div class="sub">Ingen lukkede handler</div>
{% endif %}
</div>
<div class="card">
<div class="label">Kapital</div>
<div class="value neu">{{ "{:,.0f}".format(capital) }} kr</div>
<div class="sub">Kontant: {{ "{:,.0f}".format(cash) }} kr</div>
</div>
</div>
<!-- Open positions -->
<div class="section">
<div class="section-title">📊 Åbne positioner</div>
{% if positions %}
<table>
<tr><th>Ticker</th><th>Antal</th><th>Købt</th><th>Nu</th><th>P&amp;L</th><th>Ændring</th><th>Stop</th><th>Take</th><th>Status</th></tr>
{% for p in positions %}
<tr>
<td><strong>{{ p.ticker }}</strong></td>
<td>{{ "{:.0f}".format(p.shares) }}</td>
<td>{{ "{:,.0f}".format(p.entry) }}</td>
<td>{{ "{:,.0f}".format(p.last) }}</td>
<td class="{{ 'pos' if p.unreal >= 0 else 'neg' }}">{{ "{:+,.0f}".format(p.unreal) }}</td>
<td class="{{ 'pos' if p.pct >= 0 else 'neg' }}">{{ "{:+.1f}%".format(p.pct) }}</td>
<td class="neg">{{ "{:,.0f}".format(p.stop) }}</td>
<td class="pos">{{ "{:,.0f}".format(p.take) }}</td>
<td>
{% if p.stop_hit %}<span class="badge badge-red">🔴 STOP</span>
{% elif p.take_hit %}<span class="badge badge-green">🟡 TAKE</span>
{% else %}<span class="badge badge-gray">⏳ HOLD</span>{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p style="padding:16px 18px; color:#64748b; font-size:0.85rem;">Ingen åbne positioner.</p>
{% endif %}
</div>
<!-- Closed trades -->
<div class="section">
<div class="section-title">🏁 Lukkede handler</div>
{% if trades %}
<table>
<tr><th>Ticker</th><th>Handling</th><th>Antal</th><th>Kurs</th><th>Total</th><th>P&amp;L</th><th>Signal</th><th>Dato</th></tr>
{% for t in trades %}
<tr>
<td><strong>{{ t.ticker }}</strong></td>
<td>{{ t.action.upper() }}</td>
<td>{{ "{:.0f}".format(t.shares) }}</td>
<td>{{ "{:,.0f}".format(t.price) }}</td>
<td>{{ "{:,.0f}".format(t.total_dkk) }}</td>
<td class="{{ 'pos' if t.pnl_dkk and t.pnl_dkk >= 0 else ('neg' if t.pnl_dkk else 'neu') }}">
{{ "{:+,.0f}".format(t.pnl_dkk) if t.pnl_dkk is not none else "" }}
</td>
<td>
{% if t.signal_correct == 1 %}<span class="badge badge-green">✅ korrekt</span>
{% elif t.signal_correct == 0 %}<span class="badge badge-red">❌ forkert</span>
{% else %}<span class="badge badge-gray">—</span>{% endif %}
</td>
<td>{{ t.event_date }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<p style="padding:16px 18px; color:#64748b; font-size:0.85rem;">Ingen handler endnu.</p>
{% endif %}
</div>
<!-- Signal pipeline stats -->
<div class="section">
<div class="section-title">🔬 NLP signal pipeline</div>
<table>
<tr><th>Analyserede signaler</th><th>Alert-triggers (≥threshold)</th><th>Gns. score</th></tr>
<tr>
<td>{{ sig.total }}</td>
<td>{{ sig.alerts }}</td>
<td>{{ "{:.3f}".format(sig.avg_score) }}</td>
</tr>
</table>
</div>
<!-- Log tail -->
{% if log_tail %}
<div class="section">
<div class="section-title">📋 Seneste log ({{ log_file }})</div>
<pre>{{ log_tail }}</pre>
</div>
{% endif %}
<div class="footer">MoneyMaker · {{ db_type }} · hjess-desktop · auto-refresh {{ refresh }}s</div>
</body>
</html>
"""
# ── Data helpers ─────────────────────────────────────────────────────────────
def _latest_log_tail(lines: int = 40) -> tuple[str, str]:
"""Return (filename, last N lines) from most recent runner log."""
if not LOG_DIR.exists():
return "", ""
logs = sorted(LOG_DIR.glob("runner_*.log"), reverse=True)
if not logs:
return "", ""
path = logs[0]
try:
content = path.read_text(errors="replace")
tail = "\n".join(content.splitlines()[-lines:])
return path.name, tail
except Exception:
return path.name, ""
def _open_positions_live(db) -> list[dict]:
"""Fetch open positions with live yfinance prices."""
rows = db.execute("SELECT * FROM positions").fetchall()
result = []
for p in rows:
ticker = p["ticker"]
from report import C25
yf_ticker = C25.get(ticker, {}).get("ticker_yahoo", ticker + ".CO")
try:
last = yf.Ticker(yf_ticker).fast_info.get("lastPrice") or p["entry_price"]
except Exception:
last = p["entry_price"]
entry = float(p["entry_price"])
shares = float(p["shares"])
pct = (last - entry) / entry * 100
result.append({
"ticker": ticker,
"shares": shares,
"entry": entry,
"last": last,
"unreal": shares * (last - entry),
"pct": pct,
"stop": float(p["stop_loss"]),
"take": float(p["take_profit"]),
"stop_hit": last <= p["stop_loss"],
"take_hit": last >= p["take_profit"],
})
return result
def _closed_trades(db, limit: int = 20) -> list[dict]:
rows = db.execute("""
SELECT ticker, action, shares, price, total_dkk, pnl_dkk, signal_correct, event_date
FROM position_events
ORDER BY id DESC LIMIT ?
""", (limit,)).fetchall()
return [dict(r) for r in rows]
# ── Routes ────────────────────────────────────────────────────────────────────
@app.route("/")
def index():
db = get_conn()
try:
positions = _open_positions_live(db)
trades = _closed_trades(db)
unreal = sum(p["unreal"] for p in positions)
invested = sum(p["shares"] * p["entry"] for p in positions)
realised = _realised_pnl(db)
fees = _total_fees(db)
sig = _signal_accuracy(db)
c25_ret = _c25_day_return()
net_pnl = realised + unreal - fees
net_pct = net_pnl / CAPITAL * 100
vs_bench = (net_pct - c25_ret) if c25_ret is not None else None
cash = CAPITAL - invested
log_file, log_tail = _latest_log_tail()
now = datetime.now(timezone.utc).strftime("%d %b %Y %H:%M UTC")
return render_template_string(
TEMPLATE,
now=now,
db_type=DB_TYPE,
refresh=REFRESH,
capital=CAPITAL,
cash=cash,
unreal=unreal,
realised=realised,
fees=fees,
net_pnl=net_pnl,
net_pct=net_pct,
c25_ret=c25_ret,
vs_bench=vs_bench,
positions=positions,
open_count=len(positions),
trades=trades,
sig=type("Sig", (), sig)(),
log_file=log_file,
log_tail=log_tail,
)
finally:
db.close()
@app.route("/health")
def health():
return {"status": "ok", "db": DB_TYPE, "ts": time.time()}
# ── Main ──────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="MoneyMaker Dashboard")
parser.add_argument("--port", type=int, default=5001)
parser.add_argument("--host", default="0.0.0.0")
args = parser.parse_args()
print(f"\n MoneyMaker Dashboard → http://localhost:{args.port}\n")
app.run(host=args.host, port=args.port, debug=False)
if __name__ == "__main__":
main()