← back to Zero Code Trading Bot

assets/bot-template/dashboard.py

95 lines

#!/usr/bin/env python3
"""
dashboard.py — a zero-dependency P&L viewer for the paper ledger.

    python dashboard.py            # print a P&L snapshot to the terminal
    python dashboard.py --serve    # serve an auto-refreshing HTML table on a free port

Marks open positions against live prices (venue_adapter) so unrealized P&L is
current. Read-only — it never trades.
"""

import argparse
import json
import socket
from http.server import BaseHTTPRequestHandler, HTTPServer

import ledger
from venue_adapter import get_adapter

_adapter = None


def _mark(mid):
    global _adapter
    if _adapter is None:
        _adapter = get_adapter("polymarket")
    return _adapter.mark_price(mid)


def snapshot():
    return ledger.summary(mark_fn=_mark)


def render_html(s):
    rows = "".join(
        f"<tr><td>{p['title'][:70]}</td><td>{p['side']}</td>"
        f"<td>{p['entry_price']:.3f}</td><td>{p.get('mark','—')}</td>"
        f"<td>${p['size_usd']:.0f}</td><td>${p.get('unrealized_usd',0):.2f}</td></tr>"
        for p in s["open_positions"]
    ) or "<tr><td colspan=6>no open positions</td></tr>"
    return f"""<!doctype html><meta charset=utf-8><meta http-equiv=refresh content=15>
<title>paper P&amp;L</title>
<style>body{{font:14px/1.5 system-ui;margin:2rem;color:#111}}
table{{border-collapse:collapse;width:100%}}td,th{{border:1px solid #ddd;padding:.4rem .6rem;text-align:left}}
.big{{font-size:1.4rem;font-weight:600}}.k{{color:#666}}</style>
<h2>Paper trading — P&amp;L <span class=k>(read-only · $0)</span></h2>
<p class=big>realized ${s['realized_pnl_usd']:.2f} · unrealized ${s['unrealized_pnl_usd']:.2f}
· today ${s['pnl_today_usd']:.2f}</p>
<p class=k>open {s['open_count']} · exposure ${s['exposure_usd']:.0f}</p>
<table><tr><th>market</th><th>side</th><th>entry</th><th>mark</th><th>size</th><th>unreal</th></tr>
{rows}</table>"""


def _free_port():
    s = socket.socket()
    s.bind(("127.0.0.1", 0))
    port = s.getsockname()[1]
    s.close()
    return port


def serve():
    port = _free_port()

    class H(BaseHTTPRequestHandler):
        def do_GET(self):
            body = (json.dumps(snapshot()) if self.path.startswith("/api")
                    else render_html(snapshot())).encode()
            self.send_response(200)
            self.send_header("Content-Type",
                             "application/json" if self.path.startswith("/api") else "text/html")
            self.end_headers()
            self.wfile.write(body)

        def log_message(self, *a):
            pass

    print(f"paper P&L dashboard → http://127.0.0.1:{port}   (Ctrl-C to stop)")
    HTTPServer(("127.0.0.1", port), H).serve_forever()


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--serve", action="store_true")
    args = ap.parse_args()
    if args.serve:
        serve()
    else:
        s = snapshot()
        print(json.dumps(s, indent=2))


if __name__ == "__main__":
    main()