feat: order management (cancel/amend), stale order cleanup before market close, enriched LLM reasoning (RSI+BTC+regime), fleet dashboard with SVG charts/gauges/donuts

This commit is contained in:
drjones
2026-08-02 20:10:24 -07:00
parent 8e860361ec
commit c817216ab1
9 changed files with 301 additions and 26 deletions

View File

@@ -169,7 +169,6 @@ refresh();setInterval(refresh,5000);
</script></body></html>"""
FLEET_HTML = r"""<!DOCTYPE html><html><head><meta charset="utf-8"><title>K4LSH1 FLEET</title>
<meta http-equiv="refresh" content="15">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#020603;color:#00ff41;font-family:'SF Mono',Menlo,monospace;overflow-x:hidden}
@@ -177,61 +176,138 @@ body::before{content:'';position:fixed;inset:0;pointer-events:none;z-index:99;ba
.glow{text-shadow:0 0 8px #00ff41,0 0 22px #00ff4133}
header{display:flex;align-items:center;justify-content:space-between;padding:10px 18px;border-bottom:1px solid #00ff4133;background:#031007}
h1{font-size:1rem;letter-spacing:3px}.dim{color:#0a5f2e}
.fleet{display:grid;grid-template-columns:repeat(auto-fit,minmax(400px,1fr));gap:10px;padding:10px 18px}
.topbar{display:flex;gap:14px;padding:10px 18px;border-bottom:1px solid #00ff4122;background:#020804}
.metric{text-align:center;min-width:80px}
.metric .val{font-size:1.3rem;font-weight:800}
.metric .lab{font-size:.55rem;color:#0a5f2e;letter-spacing:1px}
.fleet{display:grid;grid-template-columns:repeat(auto-fit,minmax(440px,1fr));gap:10px;padding:10px 18px}
.card{border:1px solid #00ff4144;background:#031007cc;border-radius:8px;padding:12px;box-shadow:0 0 18px #00ff4112 inset}
.card h2{font-size:.85rem;letter-spacing:2px;margin-bottom:8px}
.big{font-size:2rem;font-weight:800}
.card h2{font-size:.85rem;letter-spacing:2px;margin-bottom:6px}
.big{font-size:1.8rem;font-weight:800}
.small{font-size:.68rem;color:#2ea043}
.row{display:flex;justify-content:space-between;font-size:.72rem;padding:2px 0;border-bottom:1px dashed #00ff4122}
.row{display:flex;justify-content:space-between;font-size:.7rem;padding:2px 0;border-bottom:1px dashed #00ff4122}
.row:last-child{border:none}
.up{color:#00ff41}.dn{color:#ff3131;text-shadow:0 0 8px #ff313166}.skip{color:#0a5f2e}
.dec{margin:2px 0;padding:3px 0;border-bottom:1px dashed #00ff4118;font-size:.7rem;line-height:1.5}
.dec .why{color:#0a5f2e;font-size:.62rem}
.dec .llm{color:#2ea043;font-size:.62rem;margin-top:1px}
.badge{display:inline-block;padding:1px 6px;border:1px solid;border-radius:3px;font-size:.6rem}
.badge.up{border-color:#00ff41;color:#00ff41}
.badge.dn{border-color:#ff3131;color:#ff3131}
.badge.skip{border-color:#0a5f2e;color:#0a5f2e}
#statusbar{position:fixed;bottom:0;left:0;right:0;z-index:100;display:flex;background:#010502f2;border-top:1px solid #00ff4166;backdrop-filter:blur(8px);font-size:.68rem;padding:0}
#statusbar .cell{padding:6px 12px;border-right:1px solid #00ff4122;white-space:nowrap}
#statusbar .lab{color:#0a5f2e;font-size:.55rem;letter-spacing:1px;display:block}
.badge.up{border-color:#00ff41;color:#00ff41}.badge.dn{border-color:#ff3131;color:#ff3131}.badge.skip{border-color:#0a5f2e;color:#0a5f2e}
.chart-row{display:flex;gap:8px;align-items:flex-end;margin:6px 0}
.chart-label{font-size:.55rem;color:#0a5f2e;writing-mode:vertical-rl;transform:rotate(180deg);text-align:center;min-width:12px}
svg{display:block;flex:1}
.dec{margin:2px 0;padding:3px 0;border-bottom:1px dashed #00ff4118;font-size:.68rem;line-height:1.5}
.dec .why{color:#0a5f2e;font-size:.6rem}
.dec .llm{color:#2ea043;font-size:.6rem;margin-top:1px}
.heatmap{display:grid;grid-template-columns:repeat(24,1fr);gap:1px;margin:4px 0}
.heat{aspect-ratio:1;border-radius:2px;background:#031007}
#statusbar{position:fixed;bottom:0;left:0;right:0;z-index:100;display:flex;background:#010502f2;border-top:1px solid #00ff4166;backdrop-filter:blur(8px);font-size:.65rem;padding:0}
#statusbar .cell{padding:5px 10px;border-right:1px solid #00ff4122;white-space:nowrap}
#statusbar .lab{color:#0a5f2e;font-size:.5rem;letter-spacing:1px;display:block}
#statusbar .cell b{color:#00ff41}
.gauge-wrap{display:flex;align-items:center;gap:8px;margin:4px 0}
.gauge{flex:1;height:8px;background:#031007;border-radius:4px;overflow:hidden;position:relative}
.gauge-fill{height:100%;border-radius:4px;transition:width .3s}
.gauge-label{font-size:.6rem;min-width:28px;text-align:right}
.donut-wrap{display:flex;align-items:center;gap:8px}
.donut-lab{font-size:.6rem;color:#2ea043}
</style></head><body>
<header><h1 class="glow">K4LSH1<span class="dim">_FLEET</span></h1><span id="tot"></span></header>
<header><h1 class="glow">K4LSH1<span class="dim">_FLEET</span></h1><span id="clock"></span></header>
<div class="topbar" id="topbar"></div>
<div class="fleet" id="cards"></div>
<div id="statusbar"></div>
<script>
const C={DOGE:"#c2a633",SOL:"#9945FF",ETH:"#627EEA",XRP:"#00AAE4",BTC:"#f7931a"};
function fmt(c){return c<0?`<span class="dn">-${Math.abs(c)}¢</span>`:c>0?`<span class="up">+${c}¢</span>`:`<span class="skip">${c}¢</span>`}
function ago(ts){const d=Date.now()/1000-ts;return d<60?Math.floor(d)+'s':d<3600?Math.floor(d/60)+'m':Math.floor(d/3600)+'h'}
function sparkline(points,color,w=200,h=40){
if(!points||points.length<2)return `<svg width="${w}" height="${h}"></svg>`;
const min=Math.min(...points),max=Math.max(...points),range=max-min||1;
const pts=points.map((p,i)=>`${(i/(points.length-1))*w},${h-((p-min)/range)*(h-4)-2}`).join(' ');
return `<svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
<polyline fill="none" stroke="${color}" stroke-width="1.5" points="${pts}" opacity=".8"/>
<circle cx="${pts.split(' ').pop().split(',')[0]}" cy="${pts.split(' ').pop().split(',')[1]}" r="2.5" fill="${color}"/>
</svg>`;
}
function gauge(val,color){
const pct=Math.max(0,Math.min(100,val));
const fill=pct>80?'#ff3131':pct>60?'#ffb000':pct>40?'#00ff41':'#00ff41';
return `<div class="gauge-wrap"><div class="gauge"><div class="gauge-fill" style="width:${pct}%;background:${fill}"></div></div><span class="gauge-label">${val?.toFixed(0)||'-'}</span></div>`;
}
function donut(won,total){
const losses=total-won,pct=total>0?won/total:0;
const angle=pct*360,r=16,cx=20,cy=20;
const x1=cx+r*Math.cos(-Math.PI/2),y1=cy+r*Math.sin(-Math.PI/2);
const x2=cx+r*Math.cos(-Math.PI/2+angle*Math.PI/180),y2=cy+r*Math.sin(-Math.PI/2+angle*Math.PI/180);
const large=angle>180?1:0;
return `<div class="donut-wrap">
<svg width="40" height="40">
<circle cx="${cx}" cy="${cy}" r="${r}" fill="#031007" stroke="#0a5f2e" stroke-width="2"/>
${angle>0?`<path d="M${cx},${cy} L${x1},${y1} A${r},${r} 0 ${large} 1 ${x2},${y2} Z" fill="#00ff41" opacity=".7"/>`:''}
${angle<360?`<path d="M${cx},${cy} L${x2},${y2} A${r},${r} 0 ${large} 0 ${x1},${y1} Z" fill="#ff3131" opacity=".5"/>`:''}
<text x="${cx}" y="${cy+3}" text-anchor="middle" fill="#00ff41" font-size="8" font-weight="bold">${total>0?Math.round(pct*100):'-'}%</text>
</svg>
<div><div class="donut-lab">${won}W/${losses}L</div><div class="donut-lab">${total} total</div></div>
</div>`;
}
function momArrow(mom,score){
const dir=mom==='UP'?'':mom==='DOWN'?'':'';
const cls=mom==='UP'?'up':mom==='DOWN'?'dn':'skip';
return `<span class="${cls}">${dir} ${score.toFixed(3)}%</span>`;
}
async function load(){
const[fr,sr]=await Promise.all([fetch('/api/fleet'),fetch('/api/state')]);
const d=await fr.json(),bs=await sr.json();
let h='',tp=0,to=0,tb=0,tw=0;
// top metrics
let tp=0,to=0,tb=0,tw=0;
for(const v of Object.values(d)){tp+=v.pnl||0;to+=v.live_bets||0;tb+=v.total_bets||0;tw+=v.won||0}
document.getElementById('topbar').innerHTML=`
<div class="metric"><div class="val">$${(bs.balance/100).toFixed(2)}</div><div class="lab">BALANCE</div></div>
<div class="metric"><div class="val">${fmt(tp)}</div><div class="lab">TOTAL PNL</div></div>
<div class="metric"><div class="val">${to}</div><div class="lab">OPEN</div></div>
<div class="metric"><div class="val">${tw}W/${tb-tw}L</div><div class="lab">RECORD</div></div>
<div class="metric"><div class="val">${tb}</div><div class="lab">TOTAL BETS</div></div>
<div class="metric"><div class="val">${Object.keys(d).length}</div><div class="lab">COINS</div></div>`;
document.getElementById('clock').textContent=new Date().toLocaleTimeString();
// coin cards
let h='';
for(const[coin,v]of Object.entries(d)){
if(!v.mode||v.mode==='OFF')continue;
tp+=v.pnl||0;to+=v.live_bets||0;tb+=v.total_bets||0;tw+=v.won||0;
const wr=v.total_bets>0?((v.won||0)/v.total_bets*100).toFixed(0):'-';
const wr=v.total_bets>0?(v.won||0)/v.total_bets:0;
const chg=v.chg24||0;
// fetch tick history for sparkline
let ticks=[];
try{
const tr=await fetch(`/api/fleet/ticks?coin=${coin.toLowerCase()}&mins=60`);
const tj=await tr.json();
ticks=tj.prices||[];
}catch(e){}
h+=`<div class="card" style="border-color:${C[coin]}44">
<h2 style="color:${C[coin]}">${v.label} <span class="badge ${v.mode==='AUTO'?'up':'dn'}">${v.mode}</span></h2>
<div class="big">$${(v.price||0).toFixed(v.price<1?6:2)}</div>
<div class="row"><span>24h</span><span class="${(v.chg24||0)>=0?'up':'dn'}">${(v.chg24||0).toFixed(2)}%</span></div>
<div class="row"><span>RSI</span><span>${v.rsi||'-'}</span></div>
<div class="row"><span>Momentum</span><span>${v.mom||'-'}</span></div>
<div class="row"><span>WR</span><span>${wr}% (${v.won||0}W)</span></div>
<div class="row"><span>Open</span><span>${v.live_bets||0}</span></div>
<div class="chart-row">
<span class="chart-label">60m</span>
${sparkline(ticks,C[coin],200,40)}
</div>
<div class="row"><span>24h</span><span class="${chg>=0?'up':'dn'}">${chg.toFixed(2)}%</span></div>
<div class="row"><span>RSI</span>${gauge(v.rsi||0,C[coin])}</div>
<div class="row"><span>Momentum</span>${momArrow(v.mom||'FLAT',parseFloat((v.mom||'FLAT 0').split(' ')[1]||0))}</div>
<div class="row"><span>Win Rate</span>${donut(v.won||0,v.total_bets||0)}</div>
<div class="row"><span>Open</span><span>${v.live_bets||0} live</span></div>
<div class="row"><span>PNL</span><span>${fmt(v.pnl||0)}</span></div>
<div class="row"><span>Stake</span><span>${v.stake||10}¢</span></div>
${(v.decisions||[]).slice(0,2).map(dec=>`
${(v.decisions||[]).slice(0,3).map(dec=>`
<div class="dec"><span class="badge ${dec.final==='UP'?'up':dec.final==='DOWN'?'dn':'skip'}">${dec.final}</span>
<span class="small">${ago(dec.ts)}</span>
<div class="llm">LLM: ${dec.llm_vote} conf=${dec.llm_conf}</div>
<div class="why">${(dec.reason||'').substring(0,110)}</div></div>`).join('')}
<div class="why">${(dec.reason||'').substring(0,120)}</div></div>`).join('')}
</div>`;
}
document.getElementById('cards').innerHTML=h||'<div style="padding:20px">no coins</div>';
document.getElementById('statusbar').innerHTML=`<div class="cell"><span class="lab">BAL</span><b>$${(bs.balance/100).toFixed(2)}</b></div><div class="cell"><span class="lab">PNL</span>${fmt(tp)}</div><div class="cell"><span class="lab">OPEN</span>${to}</div><div class="cell"><span class="lab">REC</span>${tw}W/${tb-tw}L</div>`;
document.getElementById('tot').textContent=`PNL ${tp}¢`;
}
load();setInterval(load,15000);
</script></body></html>"""
@@ -390,6 +466,23 @@ def fleet_api():
return jsonify(result)
@app.route("/api/fleet/ticks")
def fleet_ticks():
"""Price tick history for sparkline charts."""
coin = request.args.get("coin", "doge").lower()
mins = int(request.args.get("mins", 60))
fpath = HOME / f"{coin}.json"
if not fpath.exists(): return jsonify({"prices": []})
c = json.loads(fpath.read_text())
db_path = HOME / c.get("db_path", f"{coin}.db")
if not db_path.exists(): return jsonify({"prices": []})
conn = sqlite3.connect(str(db_path))
rows = conn.execute("SELECT ts, price FROM ticks WHERE ts > ? ORDER BY ts",
(time.time() - mins*60,)).fetchall()
conn.close()
return jsonify({"prices": [r[1] for r in rows], "times": [r[0] for r in rows]})
@app.route("/fleet")
def fleet_page():
return FLEET_HTML