Files
mmd/__pycache__/dashboard.cpython-314.pyc

630 lines
35 KiB
Plaintext
Raw Normal View History

2026-05-26 22:21:27 +02:00
+
2026-05-27 22:02:43 +02:00
<00>
j+|<00>
<00><><00>Rt^RIt^RIt^RIt^RIt^RIHtHt^RIHt^RI t
^RI H t H t Ht^RIHtHt^RIHtHtHtHtHtRt]!]P2!R]!]!]4P8R , 444t^<t] !]4t R
t!RR R llt"R Rlt#RRRllt$RRlt%RRlt&RRlt'] PQR4R4t)] PQR4R4t*Rt+]R8Xd
]+!4R#R#)u<>
2026-05-26 22:21:27 +02:00
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:
2026-05-27 22:02:43 +02:00
- Portfolio P&L + C25 benchmark
- Equity curve, win/loss, position charts (Apache ECharts)
- Open positions with live prices
- Closed trades (win/loss)
- Signal accuracy
- Recent runner log tail
N)<02>datetime<6D>timezone)<01>Path)<03>Flask<73>jsonify<66>render_template_string)<02>get_conn<6E>DB_TYPE)<05>_c25_day_return<72>_unrealised_pnl<6E> _realised_pnl<6E> _total_fees<65>_signal_accuracyi'<00>LOG_DIR<49>logsuQT<!DOCTYPE html>
2026-05-26 22:21:27 +02:00
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
2026-05-27 22:02:43 +02:00
<title>MoneyMaker</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
2026-05-26 22:21:27 +02:00
<style>
2026-05-27 22:02:43 +02:00
:root {
--bg: #eef2f7;
--surface: #ffffff;
--border: #e2e8f0;
--text: #0f172a;
--muted: #64748b;
--accent: #2563eb;
--green: #16a34a;
--red: #dc2626;
--yellow: #d97706;
--shadow-sm: 0 1px 2px rgba(0,0,0,.05);
--shadow: 0 1px 3px rgba(0,0,0,.07), 0 1px 2px rgba(0,0,0,.04);
}
2026-05-26 22:21:27 +02:00
* { box-sizing: border-box; margin: 0; padding: 0; }
2026-05-27 22:02:43 +02:00
body {
font-family: 'Inter', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
padding: 24px 32px 52px;
min-height: 100vh;
}
/* ─ Header ─ */
.header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 22px; padding-bottom: 18px;
border-bottom: 1px solid var(--border);
}
.header-left { display: flex; align-items: center; gap: 12px; }
.header h1 { font-size: 1.25rem; font-weight: 800; letter-spacing: -.03em; color: var(--text); }
.header h1 span { color: var(--accent); }
.header-pill {
font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: .08em;
color: var(--accent); background: rgba(37,99,235,.09);
padding: 3px 9px; border-radius: 20px; border: 1px solid rgba(37,99,235,.18);
}
.header-meta { font-size: 0.7rem; color: var(--muted); text-align: right; line-height: 1.9; }
/* ─ KPI strip ─ */
.kpis { display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px; margin-bottom: 18px; }
@media (max-width: 1100px) { .kpis { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 600px) { .kpis { grid-template-columns: repeat(2, 1fr); } }
.kpi {
background: var(--surface);
border-radius: 10px; padding: 15px 16px;
box-shadow: var(--shadow);
border-top: 3px solid transparent;
}
.kpi.kpi-neutral { border-top-color: var(--border); }
.kpi.kpi-blue { border-top-color: var(--accent); }
.kpi.kpi-green { border-top-color: var(--green); }
.kpi.kpi-red { border-top-color: var(--red); }
.kpi-label { font-size: 0.64rem; font-weight: 700; text-transform: uppercase; letter-spacing: .09em; color: var(--muted); margin-bottom: 9px; }
.kpi-value { font-size: 1.6rem; font-weight: 800; line-height: 1; letter-spacing: -.04em; }
.kpi-sub { font-size: 0.71rem; color: var(--muted); margin-top: 6px; }
/* ─ Charts row ─ */
.charts { display: grid; grid-template-columns: 1fr 260px 260px; gap: 10px; margin-bottom: 18px; }
@media (max-width: 1100px) { .charts { grid-template-columns: 1fr; } }
.chart-card { background: var(--surface); border-radius: 10px; box-shadow: var(--shadow); }
.chart-card-header {
padding: 13px 16px 0;
font-size: 0.64rem; font-weight: 700; text-transform: uppercase;
letter-spacing: .09em; color: var(--muted);
}
/* ─ Sections ─ */
.section { background: var(--surface); border-radius: 10px; box-shadow: var(--shadow); margin-bottom: 10px; overflow: hidden; }
.section-header {
padding: 11px 16px;
font-size: 0.64rem; font-weight: 700; text-transform: uppercase; letter-spacing: .09em;
color: var(--muted); background: #f8fafc;
border-bottom: 1px solid var(--border);
display: flex; justify-content: space-between; align-items: center;
}
.section-header .count {
font-family: 'JetBrains Mono', monospace; font-size: 0.68rem;
color: var(--accent); background: rgba(37,99,235,.08);
padding: 1px 8px; border-radius: 20px;
}
/* ─ Tables ─ */
table { width: 100%; border-collapse: collapse; }
th {
padding: 8px 14px; font-size: 0.63rem; font-weight: 700;
text-transform: uppercase; letter-spacing: .08em;
color: var(--muted); text-align: left;
border-bottom: 1px solid var(--border); white-space: nowrap;
background: #f8fafc;
}
td { padding: 10px 14px; font-size: 0.81rem; border-bottom: 1px solid #f1f5f9; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: rgba(37,99,235,.025); }
td.mono { font-family: 'JetBrains Mono', monospace; font-size: 0.77rem; }
td strong { font-weight: 700; color: var(--text); }
.empty { padding: 18px 16px; font-size: 0.8rem; color: var(--muted); font-style: italic; }
/* ─ Colors ─ */
.pos { color: var(--green); font-weight: 600; }
.neg { color: var(--red); font-weight: 600; }
.neu { color: var(--muted); }
/* ─ Badges ─ */
.badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 20px; font-size: 0.64rem; font-weight: 700; letter-spacing: .04em; }
.badge-green { background: rgba(22,163,74,.1); color: var(--green); }
.badge-red { background: rgba(220,38,38,.1); color: var(--red); }
.badge-blue { background: rgba(37,99,235,.1); color: var(--accent); }
.badge-yellow { background: rgba(215,119,6,.1); color: var(--yellow); }
.badge-gray { background: rgba(100,116,139,.1); color: var(--muted); }
/* ─ Log ─ */
details summary {
padding: 11px 16px; font-size: 0.64rem; font-weight: 700;
text-transform: uppercase; letter-spacing: .09em;
color: var(--muted); background: #f8fafc;
cursor: pointer; list-style: none;
display: flex; justify-content: space-between;
}
details summary::-webkit-details-marker { display: none; }
details summary::after { content: '+'; font-size: 1rem; color: var(--accent); }
details[open] summary::after { content: ''; }
pre {
font-family: 'JetBrains Mono', monospace; font-size: 0.71rem; color: var(--muted);
padding: 12px 16px 16px; white-space: pre-wrap; line-height: 1.65;
overflow-x: auto; background: #fafbfc; border-top: 1px solid var(--border);
}
/* ─ Footer ─ */
.footer { font-size: 0.64rem; color: #cbd5e1; text-align: center; margin-top: 32px; letter-spacing: .05em; }
/* ─ Refresh bar ─ */
#refresh-bar { position: fixed; bottom: 0; left: 0; height: 2px; background: var(--accent); opacity: .45; transition: width 1s linear; }
2026-05-26 22:21:27 +02:00
</style>
</head>
<body>
2026-05-27 22:02:43 +02:00
<div class="header">
<div class="header-left">
<h1>Money<span>Maker</span></h1>
<span class="header-pill">{{ db_type }}</span>
</div>
<div class="header-meta">
{{ now }}<br>
auto-refresh {{ refresh }}s
</div>
</div>
<!-- KPI strip -->
<div class="kpis">
<div class="kpi {{ 'kpi-green' if net_pnl >= 0 else 'kpi-red' }}">
<div class="kpi-label">Net P&amp;L</div>
<div class="kpi-value {{ 'pos' if net_pnl >= 0 else 'neg' }}">{{ "{:+,.0f}".format(net_pnl) }}</div>
<div class="kpi-sub {{ 'pos' if net_pct >= 0 else 'neg' }}">{{ "{:+.2f}%".format(net_pct) }} af kapital</div>
2026-05-26 22:21:27 +02:00
</div>
2026-05-27 22:02:43 +02:00
<div class="kpi {{ 'kpi-green' if unreal >= 0 else 'kpi-red' }}">
<div class="kpi-label">Urealiseret</div>
<div class="kpi-value {{ 'pos' if unreal >= 0 else 'neg' }}">{{ "{:+,.0f}".format(unreal) }}</div>
<div class="kpi-sub">{{ open_count }} position{{ 'er' if open_count != 1 else '' }}</div>
2026-05-26 22:21:27 +02:00
</div>
2026-05-27 22:02:43 +02:00
<div class="kpi {{ 'kpi-green' if realised >= 0 else 'kpi-neutral' }}">
<div class="kpi-label">Realiseret</div>
<div class="kpi-value {{ 'pos' if realised >= 0 else 'neg' }}">{{ "{:+,.0f}".format(realised) }}</div>
<div class="kpi-sub neu">gebyrer: {{ "{:,.0f}".format(fees) }} kr</div>
2026-05-26 22:21:27 +02:00
</div>
2026-05-27 22:02:43 +02:00
<div class="kpi {{ 'kpi-green' if c25_ret and c25_ret >= 0 else 'kpi-neutral' }}">
<div class="kpi-label">C25 i dag</div>
2026-05-26 22:21:27 +02:00
{% if c25_ret is not none %}
2026-05-27 22:02:43 +02:00
<div class="kpi-value {{ 'pos' if c25_ret >= 0 else 'neg' }}">{{ "{:+.2f}%".format(c25_ret) }}</div>
<div class="kpi-sub {{ 'pos' if vs_bench and vs_bench >= 0 else 'neg' }}">
2026-05-26 22:21:27 +02:00
vs benchmark: {{ "{:+.2f}%".format(vs_bench) if vs_bench is not none else "—" }}
</div>
{% else %}
2026-05-27 22:02:43 +02:00
<div class="kpi-value neu">—</div>
<div class="kpi-sub">marked lukket</div>
2026-05-26 22:21:27 +02:00
{% endif %}
</div>
2026-05-27 22:02:43 +02:00
<div class="kpi {{ 'kpi-green' if sig.total_trades > 0 and sig.accuracy_pct >= 50 else 'kpi-neutral' }}">
<div class="kpi-label">Signal accuracy</div>
2026-05-26 22:21:27 +02:00
{% if sig.total_trades > 0 %}
2026-05-27 22:02:43 +02:00
<div class="kpi-value {{ 'pos' if sig.accuracy_pct >= 50 else 'neg' }}">{{ "{:.0f}%".format(sig.accuracy_pct) }}</div>
<div class="kpi-sub">{{ sig.correct }}/{{ sig.total_trades }} handler</div>
2026-05-26 22:21:27 +02:00
{% else %}
2026-05-27 22:02:43 +02:00
<div class="kpi-value neu">—</div>
<div class="kpi-sub">ingen handler endnu</div>
2026-05-26 22:21:27 +02:00
{% endif %}
</div>
2026-05-27 22:02:43 +02:00
<div class="kpi kpi-blue">
<div class="kpi-label">Kapital</div>
<div class="kpi-value neu">{{ "{:,.0f}".format(capital) }}</div>
<div class="kpi-sub">kontant: {{ "{:,.0f}".format(cash) }} kr</div>
</div>
</div>
<!-- Charts row -->
<div class="charts">
<div class="chart-card">
<div class="chart-card-header">Equity curve</div>
<div id="chart-equity" style="height:220px;"></div>
</div>
<div class="chart-card">
<div class="chart-card-header">Win / Loss</div>
<div id="chart-winloss" style="height:220px;"></div>
</div>
<div class="chart-card">
<div class="chart-card-header">Positioner P&amp;L</div>
<div id="chart-positions" style="height:220px;"></div>
2026-05-26 22:21:27 +02:00
</div>
</div>
<!-- Open positions -->
<div class="section">
2026-05-27 22:02:43 +02:00
<div class="section-header">
Aben positioner
<span class="count">{{ open_count }}</span>
</div>
2026-05-26 22:21:27 +02:00
{% if positions %}
<table>
2026-05-27 22:02:43 +02:00
<tr>
<th>Ticker</th><th>Antal</th>
<th>Kob</th><th>Nu</th>
<th>P&amp;L</th><th>Aendring</th>
<th>Stop</th><th>Take</th><th>Status</th>
</tr>
2026-05-26 22:21:27 +02:00
{% for p in positions %}
<tr>
<td><strong>{{ p.ticker }}</strong></td>
2026-05-27 22:02:43 +02:00
<td class="mono">{{ "{:.0f}".format(p.shares) }}</td>
<td class="mono">{{ "{:,.0f}".format(p.entry) }}</td>
<td class="mono">{{ "{:,.0f}".format(p.last) }}</td>
<td class="mono {{ 'pos' if p.unreal >= 0 else 'neg' }}">{{ "{:+,.0f}".format(p.unreal) }}</td>
<td class="mono {{ 'pos' if p.pct >= 0 else 'neg' }}">{{ "{:+.1f}%".format(p.pct) }}</td>
<td class="mono neg">{{ "{:,.0f}".format(p.stop) }}</td>
<td class="mono pos">{{ "{:,.0f}".format(p.take) }}</td>
2026-05-26 22:21:27 +02:00
<td>
2026-05-27 22:02:43 +02:00
{% 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-blue">HOLD</span>{% endif %}
2026-05-26 22:21:27 +02:00
</td>
</tr>
{% endfor %}
</table>
{% else %}
2026-05-27 22:02:43 +02:00
<div class="empty">Ingen abne positioner.</div>
2026-05-26 22:21:27 +02:00
{% endif %}
</div>
<!-- Closed trades -->
<div class="section">
2026-05-27 22:02:43 +02:00
<div class="section-header">
Lukkede handler
<span class="count">{{ trades|length }}</span>
</div>
2026-05-26 22:21:27 +02:00
{% if trades %}
<table>
2026-05-27 22:02:43 +02:00
<tr><th>Ticker</th><th>Type</th><th>Antal</th><th>Kurs</th><th>Total</th><th>P&amp;L</th><th>Signal</th><th>Dato</th></tr>
2026-05-26 22:21:27 +02:00
{% for t in trades %}
<tr>
<td><strong>{{ t.ticker }}</strong></td>
2026-05-27 22:02:43 +02:00
<td><span class="badge {{ 'badge-green' if t.action == 'buy' else 'badge-red' }}">{{ t.action.upper() }}</span></td>
<td class="mono">{{ "{:.0f}".format(t.shares) }}</td>
<td class="mono">{{ "{:,.0f}".format(t.price) }}</td>
<td class="mono">{{ "{:,.0f}".format(t.total_dkk) }}</td>
<td class="mono {{ 'pos' if t.pnl_dkk and t.pnl_dkk >= 0 else ('neg' if t.pnl_dkk else 'neu') }}">
2026-05-26 22:21:27 +02:00
{{ "{:+,.0f}".format(t.pnl_dkk) if t.pnl_dkk is not none else "—" }}
</td>
<td>
2026-05-27 22:02:43 +02:00
{% if t.signal_correct == 1 %}<span class="badge badge-green">korrekt</span>
{% elif t.signal_correct == 0 %}<span class="badge badge-red">forkert</span>
2026-05-26 22:21:27 +02:00
{% else %}<span class="badge badge-gray">—</span>{% endif %}
</td>
2026-05-27 22:02:43 +02:00
<td class="mono" style="color:var(--muted)">{{ t.event_date }}</td>
2026-05-26 22:21:27 +02:00
</tr>
{% endfor %}
</table>
{% else %}
2026-05-27 22:02:43 +02:00
<div class="empty">Ingen handler endnu.</div>
2026-05-26 22:21:27 +02:00
{% endif %}
</div>
2026-05-27 22:02:43 +02:00
<!-- Signal pipeline -->
<div class="section">
<div class="section-header">NLP signal pipeline</div>
<table>
<tr>
<th>Analyserede signaler</th>
<th>Alert-triggers</th>
<th>Gns. score</th>
<th>Korrekte handler</th>
</tr>
<tr>
<td class="mono">{{ sig.total }}</td>
<td class="mono">{{ sig.alerts }}</td>
<td class="mono">{{ "{:.3f}".format(sig.avg_score) }}</td>
<td class="mono">{{ sig.correct }} / {{ sig.total_trades }}</td>
</tr>
</table>
</div>
<!-- Signal Board -->
2026-05-26 22:21:27 +02:00
<div class="section">
2026-05-27 22:02:43 +02:00
<div class="section-header">
C25 Signal Board &nbsp;·&nbsp; 7 dage
<span class="count">{{ signal_board|length }}</span>
</div>
{% if signal_board %}
2026-05-26 22:21:27 +02:00
<table>
<tr>
2026-05-27 22:02:43 +02:00
<th>Selskab</th>
<th>Pris</th>
<th>Analytiker</th>
<th>Nyheder</th>
<th>Dækning</th>
<th style="min-width:140px">Signalstyrke</th>
<th>Vurdering</th>
</tr>
{% for s in signal_board %}
<tr>
<td>
<strong>{{ s.ticker }}</strong>
<span style="font-size:.72rem;color:var(--muted);margin-left:6px">{{ s.name }}</span>
{% if s.alerts %}<span class="badge badge-red" style="margin-left:6px">{{ s.alerts }} alert</span>{% endif %}
</td>
<td class="mono">{{ s.price }} kr</td>
<td>
<span class="badge {{ s.rec_class }}">{{ s.rec_clean }}</span>
<span style="font-size:.7rem;color:var(--muted);margin-left:4px">{{ s.rec_count }}</span>
</td>
<td><span class="badge {{ s.sent_class }}">{{ s.sent_text }}</span></td>
<td class="mono" style="font-size:.75rem">{{ s.n_articles }} art &middot; {{ s.n_sources }} src
{% if s.n_sources < 3 %}<span style="color:var(--red);font-size:.65rem"> lav</span>{% endif %}
</td>
<td>
<div style="display:flex;align-items:center;gap:8px">
<div style="flex:1;height:5px;background:var(--border);border-radius:3px">
<div style="height:100%;width:{{ s.signal_pct }}%;background:{{ s.sig_color }};border-radius:3px;transition:width .4s"></div>
</div>
<span class="mono" style="font-size:.7rem;color:var(--muted);white-space:nowrap">{{ s.signal_str }}</span>
</div>
</td>
<td><span class="badge {{ s.agree_class }}">{{ s.agree_text }}</span></td>
2026-05-26 22:21:27 +02:00
</tr>
2026-05-27 22:02:43 +02:00
{% endfor %}
2026-05-26 22:21:27 +02:00
</table>
2026-05-27 22:02:43 +02:00
{% else %}
<div class="empty">Ingen signaler de seneste 7 dage &mdash; kør: make</div>
{% endif %}
2026-05-26 22:21:27 +02:00
</div>
2026-05-27 22:02:43 +02:00
<!-- Log (collapsible) -->
2026-05-26 22:21:27 +02:00
{% if log_tail %}
<div class="section">
2026-05-27 22:02:43 +02:00
<details>
<summary>Runner log <span style="margin-left:8px;font-family:JetBrains Mono,monospace;font-size:.68rem;color:var(--accent)">{{ log_file }}</span></summary>
<pre>{{ log_tail }}</pre>
</details>
2026-05-26 22:21:27 +02:00
</div>
{% endif %}
2026-05-27 22:02:43 +02:00
<div class="footer">MoneyMaker &nbsp;·&nbsp; {{ db_type }} &nbsp;·&nbsp; refresh {{ refresh }}s</div>
<div id="refresh-bar" style="width:100%"></div>
<script>
// ── ECharts theme ────────────────────────────────────────────────────────
const C = {
bg: '#ffffff',
border: '#e2e8f0',
muted: '#94a3b8',
text: '#0f172a',
green: '#16a34a',
red: '#dc2626',
blue: '#2563eb',
yellow: '#d97706',
};
const axisBase = {
axisLine: { lineStyle: { color: C.border } },
splitLine: { lineStyle: { color: C.border, type: 'dashed' } },
axisLabel: { color: C.muted, fontSize: 10 },
axisTick: { show: false },
};
// ── Equity chart ─────────────────────────────────────────────────────────
const equityData = {{ equity_json }};
const eqChart = echarts.init(document.getElementById('chart-equity'), null, { renderer: 'svg' });
eqChart.setOption({
backgroundColor: 'transparent',
grid: { top: 20, right: 16, bottom: 28, left: 52 },
tooltip: {
trigger: 'axis',
backgroundColor: '#f8fafc',
borderColor: C.border,
textStyle: { color: C.text, fontSize: 11 },
formatter: params => {
const d = params[0].data;
return `${params[0].axisValue}<br/><b style="color:${d[1]>=0?C.green:C.red}">${d[1]>=0?'+':''}${d[1].toFixed(0)} kr</b>`;
}
},
xAxis: { ...axisBase, type: 'category', data: equityData.map(d => d[0]), boundaryGap: false },
yAxis: { ...axisBase, type: 'value', splitNumber: 3 },
series: [{
type: 'line',
data: equityData.map(d => d[1]),
smooth: true,
showSymbol: false,
lineStyle: { color: C.blue, width: 2 },
areaStyle: {
color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1,
colorStops: [{ offset: 0, color: 'rgba(59,130,246,.2)' }, { offset: 1, color: 'rgba(59,130,246,0)' }] }
},
markLine: {
silent: true, symbol: 'none',
data: [{ yAxis: 0, lineStyle: { color: C.muted, type: 'dashed', width: 1 } }]
}
}]
});
// ── Win/Loss donut ────────────────────────────────────────────────────────
const wl = {{ winloss_json }};
const wlChart = echarts.init(document.getElementById('chart-winloss'), null, { renderer: 'svg' });
wlChart.setOption({
backgroundColor: 'transparent',
tooltip: {
trigger: 'item',
backgroundColor: '#f8fafc',
borderColor: C.border,
textStyle: { color: C.text, fontSize: 11 },
},
legend: { show: false },
series: [{
type: 'pie',
radius: ['48%', '70%'],
center: ['50%', '52%'],
itemStyle: { borderRadius: 4, borderColor: C.bg, borderWidth: 2 },
label: {
show: true, position: 'center',
formatter: () => wl.total > 0 ? `${Math.round(wl.win/wl.total*100)}%
korrekt` : '—',
color: C.text, fontSize: 13, fontWeight: '700', lineHeight: 18,
fontFamily: 'Inter, system-ui'
},
data: wl.total > 0
? [
{ value: wl.win, name: 'Korrekt', itemStyle: { color: C.green } },
{ value: wl.loss, name: 'Forkert', itemStyle: { color: C.red } },
{ value: wl.open, name: 'Aaben', itemStyle: { color: C.muted } },
]
: [{ value: 1, name: 'Ingen data', itemStyle: { color: C.border } }]
}]
});
// ── Positions bar ─────────────────────────────────────────────────────────
const posData = {{ positions_json }};
const posChart = echarts.init(document.getElementById('chart-positions'), null, { renderer: 'svg' });
const posColors = posData.map(d => d[1] >= 0 ? C.green : C.red);
posChart.setOption({
backgroundColor: 'transparent',
grid: { top: 12, right: 16, bottom: 40, left: 52 },
tooltip: {
trigger: 'axis', axisPointer: { type: 'none' },
backgroundColor: '#f8fafc', borderColor: C.border,
textStyle: { color: C.text, fontSize: 11 },
formatter: params => `${params[0].name}<br/><b style="color:${params[0].data>=0?C.green:C.red}">${params[0].data>=0?'+':''}${params[0].data.toFixed(0)} kr</b>`
},
xAxis: { ...axisBase, type: 'category', data: posData.map(d => d[0]),
axisLabel: { color: C.muted, fontSize: 10, rotate: posData.length > 5 ? 30 : 0 } },
yAxis: { ...axisBase, type: 'value', splitNumber: 3 },
series: [{
type: 'bar', data: posData.map(d => d[1]),
barMaxWidth: 40,
itemStyle: { color: p => posColors[p.dataIndex], borderRadius: [3,3,0,0] },
markLine: {
silent: true, symbol: 'none',
data: [{ yAxis: 0, lineStyle: { color: C.muted, type: 'dashed', width: 1 } }]
}
}]
});
// ── Resize ────────────────────────────────────────────────────────────────
window.addEventListener('resize', () => { eqChart.resize(); wlChart.resize(); posChart.resize(); });
// ── Refresh countdown bar ─────────────────────────────────────────────────
const REFRESH = {{ refresh }};
const bar = document.getElementById('refresh-bar');
let elapsed = 0;
const tick = setInterval(() => {
elapsed++;
bar.style.width = (100 - elapsed / REFRESH * 100) + '%';
if (elapsed >= REFRESH) { clearInterval(tick); location.reload(); }
}, 1000);
</script>
2026-05-26 22:21:27 +02:00
</body>
</html>
2026-05-27 22:02:43 +02:00
c<00>R<00>V^8<>dQhR\R\\\3,/#)<03><00>lines<65>return)<03>int<6E>tuple<6C>str)<01>formats"<22>,/home/hjess/Projects/MoneyMaker/dashboard.py<70> __annotate__rs"<00><00> <1D> <1D>C<EFBFBD> <1D><15>s<EFBFBD>C<EFBFBD>x<EFBFBD><1F> <1D>c<04>f<00>\P4'gR #\\PR4RR7pV'gR #V^,pVP RR7pRP VP 4V)R4pVPV3# \dTPR3u#i;i)
2026-05-26 22:21:27 +02:00
z<Return (filename, last N lines) from most recent runner log.<2E>z runner_*.logT)<01>reverse<73>replace)<01>errors<72>
2026-05-27 22:02:43 +02:00
N)rr) r<00>exists<74>sorted<65>glob<6F> read_text<78>join<69>
splitlines<EFBFBD>name<6D> Exception)rr<00>path<74>content<6E>tails& r<00>_latest_log_tailr-s<><00><00> <12>><3E>><3E> <1B> <1B><15> <0A> <11>'<27>,<2C>,<2C>~<7E>.<2E><04> =<3D>D<EFBFBD> <0F><15> <0A> <0F><01>7<EFBFBD>D<EFBFBD><1D><16>.<2E>.<2E> <09>.<2E>2<><07><13>y<EFBFBD>y<EFBFBD><17>+<2B>+<2B>-<2D>u<EFBFBD>f<EFBFBD>g<EFBFBD>6<>7<><04><13>y<EFBFBD>y<EFBFBD>$<24><EFBFBD><1E><> <14><1D><13>y<EFBFBD>y<EFBFBD>"<22>}<7D><1C><1D>s<00>AB<00>B0<03>/B0c<00>:<00>V^8<>dQhR\\,/#<00>rr<00><02>list<73>dict)rs"rrr$s<00><00><12><12><04>T<EFBFBD>
<EFBFBD>rc<04><><00>VPR4P4p.pVEFpVR,p^RIHpVP V/4P RVR,4p\
2026-05-26 22:21:27 +02:00
P !V4PP R4;'g
VR,p\VR,4p\VR,4p Wx,
V, ^d,p
VPRVRV R VR
VR W<>V,
,R V
R \VR,4R\VR,4RWsR,8*RWsR,8<>/
2026-05-27 22:02:43 +02:00
4EK V# \d TR,pL<>i;i)z/Fetch open positions with live yfinance prices.zSELECT * FROM positions<6E>ticker)<01>C25<32> ticker_yahooz.CO<43> lastPrice<63> entry_price<63>shares<65>entry<72>last<73>unreal<61>pct<63>stop<6F> stop_loss<73>take<6B> take_profit<69>stop_hit<69>take_hit) <0B>execute<74>fetchall<6C>reportr5<00>get<65>yf<79>Ticker<65> fast_infor)<00>float<61>append) <0B>db<64>rows<77>result<6C>pr4r5<00> yf_tickerr;r:r9r=s & r<00>_open_positions_liverR$sE<00><00> <0A>:<3A>:<3A>/<2F> 0<> 9<> 9<> ;<3B>D<EFBFBD> <0F>F<EFBFBD> <11><01><12>8<EFBFBD><1B><06><1E><17>G<EFBFBD>G<EFBFBD>F<EFBFBD>B<EFBFBD>'<27>+<2B>+<2B>N<EFBFBD>F<EFBFBD>U<EFBFBD>N<EFBFBD>K<> <09> $<24><15>9<EFBFBD>9<EFBFBD>Y<EFBFBD>'<27>1<>1<>5<>5<>k<EFBFBD>B<>V<>V<>a<EFBFBD> <0A>FV<46>D<EFBFBD><17>q<EFBFBD><1D>'<27>(<28><05><16>q<EFBFBD><18>{<7B>#<23><06><16>,<2C>%<25>'<27>#<23>-<2D><03><0E> <0A> <0A> <14><06> <14><06> <13><05> <12><04> <14><06><15>,<2C>/<2F> <11><03> <12><05>a<EFBFBD> <0B>n<EFBFBD>-<2D> <12><05>a<EFBFBD> <0A>.<2E>/<2F> <16><04>+<2B><0E>.<2E> <16><04>-<2D> 0<>0<> 
2026-05-26 22:21:27 +02:00
<EFBFBD> <0B><12>. <12>M<EFBFBD><4D>#<19> $<24><14>]<5D>#<23>D<EFBFBD> $<24>s<00>!5E<02>
2026-05-27 22:02:43 +02:00
E<02>E<05>Ec<00>F<00>V^8<>dQhR\R\\,/#)r<00>limitr)rr1r2)rs"rrrBs<00><00>#<23>#<23>c<EFBFBD>#<23>4<EFBFBD><04>:<3A>#rc<00><><00>VPRV34P4pVUu.uFp\V4NK up#uupi)z<>
2026-05-26 22:21:27 +02:00
SELECT ticker, action, shares, price, total_dkk, pnl_dkk, signal_correct, event_date
FROM position_events
ORDER BY id DESC LIMIT ?
2026-05-27 22:02:43 +02:00
)rDrEr2)rMrTrN<00>rs&& r<00>_closed_tradesrWBsF<00><00> <0A>:<3A>:<3A><08> <10><18>  <13><1C>8<EFBFBD>:<3A>  <09>
"<22> "<22>T<EFBFBD><01>D<EFBFBD><11>G<EFBFBD>T<EFBFBD> "<22>"<22><> "s<00>=c<00>:<00>V^8<>dQhR\\,/#r/)r1)rs"rrrKs<00><00><12><12><14>d<EFBFBD><1A>rc<04>@<00>VPR4P4pRp.pVFJpT\VR,;'g^4, pVPVR,\ V^4.4KL V'g#^RIHp\VP44R..pV#)zHCumulative realised P&L per day from position_events (sell events only).z<>
SELECT event_date, SUM(COALESCE(pnl_dkk, 0)) as daily_pnl
FROM position_events
WHERE action = 'sell'
GROUP BY event_date
ORDER BY event_date
g<00> daily_pnl<6E>
event_date)<01>date) rDrErKrL<00>roundrr\r<00>today)rMrN<00>
cumulativerOrVr\s& r<00> _equity_curver`Ks<><00><00> <0A>:<3A>:<3A><08> <09>
<12><18><1A>  <09><15>J<EFBFBD> <0F>F<EFBFBD> <11><01><12>e<EFBFBD>A<EFBFBD>k<EFBFBD>N<EFBFBD>/<2F>/<2F>a<EFBFBD>0<>0<>
<EFBFBD><0E> <0A> <0A>q<EFBFBD><1C><EFBFBD><05>j<EFBFBD>!<21>(<<3C>=<3D>><3E><12> <12>!<21><16>t<EFBFBD>z<EFBFBD>z<EFBFBD>|<7C>$<24>c<EFBFBD>*<2A>+<2B><06> <11>Mrc<00>$<00>V^8<>dQhR\/#r/)r2)rs"rrr_s<00><00><06><06><14>rc <04>*<00>VPR4P4pVUu/uFq"R,VR,bK ppRVP^^4RVP^^4RVPR^4R\VP 44/#uupi) z%Win/loss/open counts for donut chart.z<>
SELECT signal_correct, COUNT(*) as cnt
FROM position_events
WHERE action = 'sell'
GROUP BY signal_correct
<20>signal_correct<63>cnt<6E>win<69>loss<73>openN<6E>total)rDrErG<00>sum<75>values)rMrNrV<00>countss& r<00> _winloss_datarl_s<><00><00> <0A>:<3A>:<3A><08> <09>
<12><18><1A>  <09> 6:<3A> :<3A>T<EFBFBD><01> <20>!<21>1<EFBFBD>U<EFBFBD>8<EFBFBD>+<2B>T<EFBFBD>F<EFBFBD> :<3A> <0A><16><1A><1A>A<EFBFBD>q<EFBFBD>!<21><0E><16><1A><1A>A<EFBFBD>q<EFBFBD>!<21><0E><16><1A><1A>D<EFBFBD>!<21>$<24><0F><13>V<EFBFBD>]<5D>]<5D>_<EFBFBD>%<25>  <06><06><>;s<00>Bc<00>:<00>V^8<>dQhR\\,/#r/r0)rs"rrrps<00><00>I<12>I<12>d<EFBFBD>4<EFBFBD>j<EFBFBD>Irc
<04><><00>^RIHpHpHpV!V^R7p.pVEFJpVR,pVP V/4pVR,;'g^p V R8<>dRRr<>MV R:8dRR r<>MR
R r<>V!V4p V R ,fR
Rr<>MKV R ,R8:dRRr<>M9V R ,R8:dRRr<>M'V R ,R8:dRRr<>MV R ,R8:dRRr<>MRRr<>V R8<>pV R:8pV R ,R J;'d V R ,R8*pV R ,R J;'d V R ,R8<>pV'd V'gV'dV'dRRppM)V'd V'gV'dV'dRRppMR
RppVP R4;'g^p\ \ V^<5E>,4^d4pVR8<>dRpMVR8<>dR pM V^8<>dR!pMR"pTP/RVbR#VP R#V4bR$VP R%R&4bR'V bR(VbR)V R*,bR+V
bR,V bR-VR.,bR/\ VR0,;'g^4bR1VbR2VR3 bR4VbR5VbR6VbR7VbR8\ VP R94;'g^4b4EKM V#);zBBuild signal board rows from DB + Yahoo Finance analyst consensus.)<03> company_stats<74> analyst_recr5)<01>daysr4<00> avg_sentimentg<74><67><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>?z badge-green<65>Positivz badge-red<65>Negativz
badge-gray<61>Neutral<61>meanN<6E>Ukendtg<00>?u Stærk KØBg@uKØBg @z badge-yellow<6F>HOLDg@uSÆLGu Stærk SÆLG<4C>Enige<67>Uenige<67><>
max_signalg<00>?z var(--green)g<><67><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>?z var(--yellow)z var(--accent)z var(--border)r(<00>price<63>price_dkk_approx<6F>?<3F> rec_class<73> rec_clean<61> rec_count<6E>count<6E>
sent_class<EFBFBD> sent_text<78>
n_articles<EFBFBD>mention_articles<65> n_sources<65> avg_sources<65>signal<61>
signal_strz.2f<EFBFBD>
signal_pct<EFBFBD> sig_color<6F> agree_class<73>
agree_text<EFBFBD>alerts<74> alert_countg<74><67><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>)<08>signalsrorpr5rG<00>minrrL)rMrorpr5rNrOrVr4<00>company<6E>avg_sr<73>r<><00>recr<63>r<><00> news_bull<6C> news_bear<61>ana_bull<6C>ana_bearr<72>r<><00>sigr<67>r<>s& r<00>_signal_board_datar<61>ps<><00><00>7<>7<> <18><12>!<21> $<24>D<EFBFBD> <0F>F<EFBFBD> <11><01><13>H<EFBFBD>+<2B><06><15>'<27>'<27>&<26>"<22>%<25><07><13>O<EFBFBD>$<24>)<29>)<29><01><05> <11>3<EFBFBD>;<3B>$1<>9<EFBFBD> <09> <12>T<EFBFBD>\<5C>$/<2F><19> <09>$0<>)<29> <09><1A>&<26>!<21><03> <0E>v<EFBFBD>;<3B> <1E>#/<2F><18>y<EFBFBD> <10><16>[<5B>C<EFBFBD> <1F>#0<>-<2D>y<EFBFBD> <10><16>[<5B>C<EFBFBD> <1F>#0<>&<26>y<EFBFBD> <10><16>[<5B>C<EFBFBD> <1F>#1<>6<EFBFBD>y<EFBFBD> <10><16>[<5B>C<EFBFBD> <1F>#.<2E><07>y<EFBFBD>#.<2E><0E>y<EFBFBD><1A>C<EFBFBD>K<EFBFBD> <09><19>D<EFBFBD>L<EFBFBD> <09><17><06>K<EFBFBD>t<EFBFBD>+<2B>B<>B<><03>F<EFBFBD> <0B>s<EFBFBD>0B<30><08><17><06>K<EFBFBD>t<EFBFBD>+<2B>B<>B<><03>F<EFBFBD> <0B>s<EFBFBD>0B<30><08> <15>(<28> <09>h<EFBFBD>&3<>W<EFBFBD><1A>K<EFBFBD><1A><17>H<EFBFBD>)<29><08>&1<>8<EFBFBD><1A>K<EFBFBD><1A>&2<>E<EFBFBD><1A>K<EFBFBD><10>e<EFBFBD>e<EFBFBD>L<EFBFBD>!<21>&<26>&<26>Q<EFBFBD><03><18><13>S<EFBFBD>3<EFBFBD>Y<EFBFBD><1E><13>-<2D>
<EFBFBD> <0E>#<23>:<3A>^<5E><19> <10>C<EFBFBD>Z<EFBFBD>_<EFBFBD><19> <10>1<EFBFBD>W<EFBFBD>_<EFBFBD><19>%4<><19><0E> <0A> <0A>
<EFBFBD> <14>&<26>
<EFBFBD> <12>'<27>+<2B>+<2B>f<EFBFBD>f<EFBFBD>5<>
<EFBFBD> <14>'<27>+<2B>+<2B>&8<>#<23>><3E>
<EFBFBD> <18>)<29> 
<EFBFBD>
<18>)<29> 
<EFBFBD> <18>#<23>g<EFBFBD>,<2C> 
<EFBFBD> <19>*<2A>
<EFBFBD> <18>)<29>
<EFBFBD> <19>!<21>.<2E>/<2F>
<EFBFBD> <18>#<23>a<EFBFBD> <0A>.<2E>3<>3<>!<21>4<>
<EFBFBD> <15>#<23>
<EFBFBD> <19>S<EFBFBD><13>I<EFBFBD>
<EFBFBD> <19>*<2A>
<EFBFBD> <18>)<29>
<EFBFBD> <1A>;<3B>
<EFBFBD> <19>:<3A>!
<EFBFBD>" <15>#<23>a<EFBFBD>e<EFBFBD>e<EFBFBD>M<EFBFBD>2<>7<>7<>a<EFBFBD>8<>#
<EFBFBD> <0B>a<12>F <12>Mr<00>/c
<00><><00>\4p\V4p\V4p\RV44p\RV44p\ V4p\ V4p\ V4p\4pWS,V,
2026-05-26 22:21:27 +02:00
p V \, ^d,p
Ve W<>,
MRp \V,
2026-05-27 22:02:43 +02:00
p \4wr<>\P!\P4PR4p\V4p\!V4pVUu.uFpVR,\#VR,^4.NK! up;'gR^..p\%V4p\/\03/RVbR \2bR
\4bR \bR V bRVbR VbRVbRV bRV
bRVbRV bRVbR\7V4bRVbR\9RRV4!4bRV bRVbR\:P<!V4bR\:P<!V4bR\:P<!V4bRVbVP?4#uupi \&d-p.p\(P*P-RT4Rp?L<>Rp?ii;i TP?4i;i)c3<00>2"<00>TF qR,x<00>K R#5i)r<N<><00><02>.0rPs& r<00> <genexpr><3E>index.<locals>.<genexpr><3E>s<00><00><00>8<>i<EFBFBD><11>8<EFBFBD><1B><1B>i<EFBFBD>s<00>c3<00>N"<00>TFqR,VR,,x<00>K R#5i)r9r:Nr<4E>r<>s& rr<>r<><00>s<00><00><00>E<>9<EFBFBD>a<EFBFBD>8<EFBFBD><1B>q<EFBFBD><17>z<EFBFBD>1<>1<>9<EFBFBD>s<00>#%Nz%d %b %Y %H:%M UTCr4r<r{zsignal_board error: %s<>now<6F>db_type<70>refresh<73>capital<61>cash<73>realised<65>fees<65>net_pnl<6E>net_pct<63>c25_ret<65>vs_bench<63> positions<6E>
open_count<EFBFBD>tradesr<73><00>Sig<69>log_file<6C>log_tail<69> equity_json<6F> winloss_json<6F>positions_json<6F> signal_boardr<64>) rrRrWrir r rr
<00>CAPITALr-rr<>r<00>utc<74>strftimer`rlr]r<>r)<00>app<70>logger<65>warningr<00>TEMPLATEr <00>REFRESH<53>len<65>type<70>json<6F>dumps<70>close)rMr<>r<>r<<00>investedr<64>r<>r<>r<>r<>r<>r<>r<>r<>r<>r<><00>equity<74>winlossrP<00>pos_barsr<73><00>excs r<00>indexr<78><00>s<><00><00> <11><1A>B<EFBFBD>4<13>)<29>"<22>-<2D> <09>#<23>B<EFBFBD>'<27><06><18>8<>i<EFBFBD>8<>8<><06><18>E<>9<EFBFBD>E<>E<><08>"<22>2<EFBFBD>&<26><08> <20><12>_<EFBFBD><04>%<25>b<EFBFBD>)<29><03>$<24>&<26><07><1D>&<26><14>-<2D><07><1C>w<EFBFBD>&<26><13>,<2C><07>,3<>,?<3F>g<EFBFBD>'<27>T<EFBFBD><08><1C>x<EFBFBD>'<27><04>-<2D>/<2F><1A><08><16>l<EFBFBD>l<EFBFBD>8<EFBFBD><<3C><<3C>(<28>1<>1<>2F<32>G<><03> <20><12>$<24><06> <20><12>$<24><07>BK<42>L<>)<29>Q<EFBFBD>Q<EFBFBD>x<EFBFBD>[<5B>%<25><01>(<28> <0B>Q<EFBFBD>"7<>8<>)<29>L<>\<5C>\<5C>RW<52>YZ<59>Q[<5B>P\<5C><08> ><3E>-<2D>b<EFBFBD>1<>L<EFBFBD>
&<26> <14>
<EFBFBD><13>
<EFBFBD><1C>
<EFBFBD><1C> 
2026-05-26 22:21:27 +02:00
<EFBFBD>
2026-05-27 22:02:43 +02:00
<1C> 
<EFBFBD> <16> 
<EFBFBD><1A>
<EFBFBD><1E>
<EFBFBD><16>
<EFBFBD><1C>
<EFBFBD><1C>
<EFBFBD><1C>
<EFBFBD><1E>
<EFBFBD> <20>
<EFBFBD><1B>9<EFBFBD>~<7E>
<EFBFBD> <1A>!
<EFBFBD>"<15>U<EFBFBD>B<EFBFBD><03>$<24>&<26>#
<EFBFBD>$<1E>%
<EFBFBD>&<1E>'
<EFBFBD>(<1D>
<EFBFBD>
<EFBFBD>6<EFBFBD>*<2A>)
<EFBFBD>*<1E><1A><1A>G<EFBFBD>,<2C>+
<EFBFBD>, <20>:<3A>:<3A>h<EFBFBD>/<2F>-
<EFBFBD>.&<26>/
<EFBFBD>4 <0B><08><08>
<EFBFBD><EFBFBD>EM<01><><19> ><3E><1D>L<EFBFBD> <0F>J<EFBFBD>J<EFBFBD> <1E> <1E>7<><13> =<3D> =<3D><> ><3E><>< <0B><08><08>
<EFBFBD>sO<00>C<I <00>%H
<04>- I <00>7I <00>> H<00> B0I <00>
I <00> I<03>"I<03><I <00>I<03>I <00> Iz/healthc<00>@<00>RRR\R\P!4/#)<04>status<75>okrM<00>ts)r <00>timer<65>rr<00>healthr<68><00>s<00><00> <14>d<EFBFBD>D<EFBFBD>'<27>4<EFBFBD><14><19><19><1B> =<3D>=rc
<00><><00>\P!RR7pVPR\\\P
!RR44R7VPR\P
!RR4R 7VP 4p\R
VP R 24\PVPVPR R 7R#)zMoneyMaker Dashboard)<01> descriptionz--port<72>PORTi<54>)r<><00>defaultz--host<73>HOSTz0.0.0.0)r<>z,
MoneyMaker Dashboard -> http://localhost:r!F)<03>host<73>port<72>debugN) <0C>argparse<73>ArgumentParser<65> add_argumentr<00>os<6F>getenv<6E>
parse_args<EFBFBD>printr<74>r<><00>runr<6E>)<02>parser<65>argss r<00>mainr<6E><00>s<><00><00> <15> $<24> $<24>1G<31> H<>F<EFBFBD>
<EFBFBD><17><17><08>s<EFBFBD>C<EFBFBD><02> <09> <09>&<26>$<24>8O<38>4P<34><17>Q<>
<EFBFBD><17><17><08>"<22>)<29>)<29>F<EFBFBD>I<EFBFBD>*F<><17>G<> <11> <1C> <1C> <1E>D<EFBFBD> <09> 9<>$<24>)<29>)<29><1B>B<EFBFBD>
G<EFBFBD>H<><07>G<EFBFBD>G<EFBFBD><14><19><19><14><19><19>%<25>G<EFBFBD>8r<00>__main__)<01>()<01>),<2C>__doc__r<5F>r<>r<>r<>rr<00>pathlibr<00>yfinancerH<00>flaskrrrrMrr rFr
r r r rr<>r<>r<00>__file__<5F>parentrr<><00>__name__r<5F>r<>r-rRrWr`rlr<><00>router<65>r<>r<>r<>rr<00><module>r<>s<><00><01><04><10> <0B> <09> <0B>'<27><18><15>8<>8<> <20>a<>a<> <11><07> <0F><02> <09> <09>)<29>S<EFBFBD><14>h<EFBFBD><1E>)><3E>)><3E><16>)G<>%H<>I<> J<><07> <0A><07> <0B>H<EFBFBD>o<EFBFBD><03>j <04><08>^ <1D> <12><#<23><12>(<06>"I<12>\<05><19><19>3<EFBFBD><1E>6<13><10>6<13>r<05><19><19>9<EFBFBD><15>><3E><16>><3E> 9<> <0C>z<EFBFBD><19><08>F<EFBFBD>r