K4LSH1_OPS: multi-coin LLM bot — DOGE/SOL/ETH, two-tier qwen+ornith, self-adapting

This commit is contained in:
drjones
2026-08-02 19:10:27 -07:00
commit c14bb98f7f
23 changed files with 3418 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

0
bot.lock Normal file
View File

1716
bot.log Normal file

File diff suppressed because it is too large Load Diff

617
bot.py Normal file
View File

@@ -0,0 +1,617 @@
#!/usr/bin/env python3
"""
K4LSH1_OPS — autonomous 15-minute BTC up/down trading bot.
Momentum + local-Ollama ensemble, tiny stakes, full sqlite audit trail.
Modes: AUTO | DOWN_SPAM | UP_SPAM | OFF (persisted in config.json, hot-reloaded)
DRY_RUN=true simulates orders (still logs + tracks hypothetical P&L).
"""
import base64, json, logging, os, sqlite3, sys, time
from datetime import datetime, timezone
from pathlib import Path
from uuid import uuid4
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
HOME = Path(__file__).parent
CFG_PATH = HOME / "config.json"
DB_PATH = HOME / "state.db"
# per-coin Kraken pairs
KRAKEN_PAIRS = {"BTC": "XBTUSD", "ETH": "ETHUSD", "DOGE": "XDGUSD", "SOL": "SOLUSD", "XRP": "XRPUSD"}
LOG = logging.getLogger("bot")
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s",
handlers=[logging.FileHandler(HOME/"bot.log"), logging.StreamHandler()])
DEFAULT_CFG = {
"api_key_id": "28d5876b-2ece-4aa3-aa17-ec96e1e706eb",
"key_path": str(HOME / "kalshi_private_key.pem"),
"mode": "AUTO", # AUTO | DOWN_SPAM | UP_SPAM | OFF
"dry_run": True, # simulated until flipped from dashboard
"stake_cents": 50, # taker: max ask we'll pay; maker: price we post
"post_price_cents": 45, # maker mode: resting bid price when book is empty
"max_contracts": 1,
"daily_loss_cap_cents": 500,
"min_conf": 0.55, # AUTO mode: min ensemble confidence to fire
"swing_threshold_pct": 0.12, # fade triggers when |weighted 15m move| >= this
"learning": True,
"ollama_url": "http://localhost:11434",
"ollama_model": "qwen3.5:4b",
"series": "KXBTC15M",
"kill": False,
}
def load_cfg():
cfg = dict(DEFAULT_CFG)
if CFG_PATH.exists():
try: cfg.update(json.loads(CFG_PATH.read_text()))
except Exception: pass
else:
CFG_PATH.write_text(json.dumps(cfg, indent=1))
return cfg
# ───────── kalshi client ─────────
class Kalshi:
BASE = "https://api.elections.kalshi.com"
PRE = "/trade-api/v2"
def __init__(self, key_id, key_path):
self.key_id = key_id
self.pk = serialization.load_pem_private_key(open(key_path, "rb").read(), password=None)
self.s = requests.Session()
def _h(self, method, path):
ts = str(int(time.time()*1000))
msg = ts + method.upper() + path.split("?")[0]
sig = self.pk.sign(msg.encode(), padding.PSS(mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.DIGEST_LENGTH), hashes.SHA256())
return {"KALSHI-ACCESS-KEY": self.key_id,
"KALSHI-ACCESS-SIGNATURE": base64.b64encode(sig).decode(),
"KALSHI-ACCESS-TIMESTAMP": ts}
def req(self, method, path, **kw):
full = self.PRE + path
for attempt in range(3):
try:
r = self.s.request(method, self.BASE+full, headers=self._h(method, full), timeout=15, **kw)
r.raise_for_status()
return r.json()
except requests.HTTPError as e:
if r.status_code in (401, 403): raise
if attempt == 2: raise
time.sleep(1.5*(attempt+1))
def markets(self, series, status="open", limit=5):
return self.req("GET", f"/markets?series_ticker={series}&status={status}&limit={limit}").get("markets", [])
def orderbook(self, ticker, depth=5):
return self.req("GET", f"/markets/{ticker}/orderbook?depth={depth}").get("orderbook", {})
def balance(self):
return self.req("GET", "/portfolio/balance")
def positions(self):
return self.req("GET", "/portfolio/positions?limit=50").get("market_positions", [])
def order(self, ticker, side, count, price_cents, dry):
"""side='yes'|'no'. price_cents = limit price in cents.
V2 endpoint: /portfolio/events/orders with bid/ask side + dollar prices."""
side_v2 = "bid" if side == "yes" else "ask"
price_dollars = f"{price_cents / 100:.4f}"
body = {"ticker": ticker, "client_order_id": str(uuid4()),
"side": side_v2, "count": f"{count:.2f}",
"price": price_dollars, "time_in_force": "good_till_canceled",
"self_trade_prevention_type": "taker_at_cross",
"post_only": False, "reduce_only": False}
if dry: return {"simulated": True, "order": body}
return self.req("POST", "/portfolio/events/orders", json=body)
# ───────── btc price feed ─────────
def btc_price(coin="BTC"):
pair = KRAKEN_PAIRS.get(coin, "XBTUSD")
try:
r = requests.get(f"https://api.kraken.com/0/public/Ticker?pair={pair}", timeout=8).json()
key = list(r["result"].keys())[0]
t = r["result"][key]
_intel_cache.setdefault("kraken", {})["open24"] = float(t["o"][1] if isinstance(t["o"], list) else t["o"])
return float(t["c"][0])
except Exception:
try:
cb = "BTC" if coin == "BTC" else coin
r = requests.get(f"https://api.coinbase.com/v2/prices/{cb}-USD/spot", timeout=8)
return float(r.json()["data"]["amount"])
except Exception:
return None
# ───────── db ─────────
def db():
conn = sqlite3.connect(DB_PATH)
conn.execute("""CREATE TABLE IF NOT EXISTS ticks (ts REAL, price REAL)""")
conn.execute("""CREATE TABLE IF NOT EXISTS decisions (
id INTEGER PRIMARY KEY AUTOINCREMENT, ts REAL, ticker TEXT, mode TEXT,
momentum TEXT, llm_vote TEXT, llm_conf REAL, llm_why TEXT,
final TEXT, price REAL, reason TEXT)""")
conn.execute("""CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT, ts REAL, ticker TEXT, side TEXT,
count INT, price_cents INT, dry INT, order_id TEXT, status TEXT,
settle_ts REAL, pnl_cents INT)""")
conn.execute("""CREATE TABLE IF NOT EXISTS events (ts REAL, level TEXT, msg TEXT)""")
conn.execute("""CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v REAL)""")
return conn
def log_event(conn, level, msg):
conn.execute("INSERT INTO events VALUES (?,?,?)", (time.time(), level, msg))
conn.commit()
getattr(LOG, level if level in ("info","warning","error") else "info")(msg)
# ───────── brain ─────────
def momentum(conn):
rows = conn.execute("SELECT ts, price FROM ticks WHERE ts > ? ORDER BY ts",
(time.time()-1800,)).fetchall()
if len(rows) < 4: return "FLAT", 0.0
now = rows[-1][1]
def ret(minutes):
ref = min(rows, key=lambda r: abs(r[0]-(time.time()-minutes*60)))[1]
return (now-ref)/ref*100 if ref else 0
r5, r15 = ret(5), ret(15)
score = r5*0.6 + r15*0.4
if score > 0.008: return "UP", abs(score)
if score < -0.008: return "DOWN", abs(score)
return "FLAT", abs(score)
# ───────── multi-source intel ─────────
_intel_cache = {"ts": 0, "data": {}}
def intel(coin="BTC"):
"""Slow external context, cached 5 min: Binance 24h stats, Fear&Greed, mempool fees."""
if time.time() - _intel_cache["ts"] < 300 and _intel_cache["data"] and coin in _intel_cache.get("coins", {}):
return _intel_cache["data"]
d = {}
# per-coin binance 24h
BINANCE_SYMBOLS = {"BTC": "BTCUSDT", "ETH": "ETHUSDT", "DOGE": "DOGEUSDT", "SOL": "SOLUSDT", "XRP": "XRPUSDT"}
bs = BINANCE_SYMBOLS.get(coin, "BTCUSDT")
try:
j = requests.get(f"https://api.binance.com/api/v3/ticker/24hr?symbol={bs}", timeout=8).json()
d["chg24"] = float(j["priceChangePercent"]); d["vol24"] = float(j["volume"])
d["high24"] = float(j["highPrice"]); d["low24"] = float(j["lowPrice"])
except Exception:
pass
if "chg24" not in d:
# kraken-derived 24h change (works everywhere, no geo block)
k = _intel_cache.get("kraken", {}).get("open24")
try:
cur = requests.get("https://api.kraken.com/0/public/Ticker?pair=XBTUSD", timeout=8).json()["result"]["XXBTZUSD"]
op = float(cur["o"][1] if isinstance(cur["o"], list) else cur["o"])
cl = float(cur["c"][0])
d["chg24"] = (cl - op) / op * 100
d["high24"] = float(cur["h"][1]); d["low24"] = float(cur["l"][1])
d["vol24"] = float(cur["v"][1])
except Exception:
pass
try:
j = requests.get("https://api.alternative.me/fng/?limit=1", timeout=8).json()
d["fng"] = int(j["data"][0]["value"]); d["fng_class"] = j["data"][0]["value_classification"]
except Exception: pass
try:
j = requests.get("https://mempool.space/api/v1/fees/recommended", timeout=8).json()
d["fee_fast"] = j.get("fastestFee")
except Exception: pass
_intel_cache.update({"ts": time.time(), "data": d, "coins": {coin: True}})
return d
def rsi(conn, period=14):
rows = conn.execute("SELECT price FROM ticks WHERE ts > ? ORDER BY ts", (time.time()-1200,)).fetchall()
px = [r[0] for r in rows]
if len(px) < period+2: return None
gains, losses = [], []
for a, b in zip(px[-period-1:-1], px[-period:]):
ch = b - a
(gains if ch >= 0 else losses).append(abs(ch))
ag = sum(gains)/period; al = sum(losses)/period if losses else 1e-9
return 100 - 100/(1 + ag/al)
def enriched_context(cfg, price, mom, mom_score, conn):
"""Build rich trading context: multi-TF trends, RSI, orderbook, BTC corr, WR history, regime."""
coin = cfg.get("coin","BTC")
ix = intel(coin)
r1 = rsi(conn, 14); r5 = rsi(conn, 14*5) # 1min and 5min RSI
lines = [f"=== {coin} MARKET SNAPSHOT ==="]
lines.append(f"spot: ${price:,.4f}")
lines.append(f"15m momentum: {mom} ({mom_score:.3f}%)")
if r1 is not None: lines.append(f"RSI(14,1m): {r1:.0f} {'OVERBOUGHT' if r1>70 else 'OVERSOLD' if r1<30 else 'neutral'}")
if r5 is not None: lines.append(f"RSI(14,5m): {r5:.0f} {'OVERBOUGHT' if r5>70 else 'OVERSOLD' if r5<30 else 'neutral'}")
# Multi-timeframe trend
rows = conn.execute("SELECT ts, price FROM ticks WHERE ts > ? ORDER BY ts",
(time.time()-7200,)).fetchall()
if len(rows) >= 8:
def ret_at(minutes):
ref = min(rows, key=lambda r: abs(r[0]-(time.time()-minutes*60)))[1]
return (price-ref)/ref*100 if ref else 0
lines.append(f"trend 1m: {ret_at(1):+.3f}% | 5m: {ret_at(5):+.3f}% | 15m: {ret_at(15):+.3f}% | 1h: {ret_at(60):+.2f}% | 2h: {ret_at(120):+.2f}%")
# 24h stats
if "chg24" in ix:
lines.append(f"24h change: {ix['chg24']:+.2f}% | range: ${ix.get('low24',0):,.2f}-${ix.get('high24',0):,.2f} | vol24: {ix.get('vol24',0):,.0f}")
# BTC correlation
btc_chg = ix.get("chg24", 0)
if abs(btc_chg) > 0.3:
lines.append(f"BTC 24h: {btc_chg:+.1f}% ({'bullish' if btc_chg>0 else 'bearish'}) — {'coin follows' if coin=='BTC' else 'alt follows macro'}")
# Orderbook depth (if ticker available)
ticker = getattr(cfg, '_current_ticker', None)
if ticker:
try:
kx = Kalshi(cfg["api_key_id"], cfg["key_path"])
book = kx.orderbook(ticker)
yes = book.get("yes",[]); no = book.get("no",[])
if yes and no:
y_ask = yes[0][0]; n_ask = no[0][0]
spread = abs(y_ask - n_ask)
lines.append(f"orderbook: YES ask={y_ask}¢ NO ask={n_ask}¢ spread={spread}¢")
# imbalance
y_depth = sum(o[1] for o in yes[:3]) if len(yes)>=3 else 0
n_depth = sum(o[1] for o in no[:3]) if len(no)>=3 else 0
if y_depth+n_depth > 0:
imb = (y_depth - n_depth) / (y_depth + n_depth)
lines.append(f"orderbook imbalance: {'YES-heavy' if imb>0.15 else 'NO-heavy' if imb<-0.15 else 'balanced'} ({imb:+.2f})")
except Exception:
pass
# Fear&Greed
if "fng" in ix:
lines.append(f"Fear&Greed: {ix['fng']} ({ix.get('fng_class','')})")
# Per-coin recent performance
perf = conn.execute("SELECT COUNT(*), SUM(CASE WHEN pnl_cents>0 THEN 1 ELSE 0 END) FROM orders WHERE dry=0 AND status IN ('won','lost') AND ts > ?",
(time.time()-86400,)).fetchone()
total, wins = perf[0] or 0, perf[1] or 0
if total >= 3:
wr = wins / total
lines.append(f"{coin} 24h record: {wins}W/{total-wins}L ({wr:.0%})")
# Market regime
if len(rows) >= 20:
prices = [r[1] for r in rows[-20:]]
mean = sum(prices)/len(prices)
std = (sum((p-mean)**2 for p in prices)/len(prices))**0.5
vol = std/mean*100 if mean else 0
regime = "choppy" if vol < 0.01 else "trending" if vol > 0.05 else "normal"
lines.append(f"market regime: {regime} (vol={vol:.3f}%)")
# On-chain
if "fee_fast" in ix:
lines.append(f"mempool fast fee: {ix['fee_fast']} sat/vB")
lines.append("")
lines.append("You trade 15-minute crypto up/down binary contracts by FADING spikes:")
lines.append("- Sharp UP move → bet DOWN (mean reversion)")
lines.append("- Sharp DOWN move → bet UP (mean reversion)")
lines.append("- SKIP when: strong trend continuation, thin orderbook, BTC-fighting trade, or no clear edge")
lines.append("- Consider: RSI extremes (>80 or <20 = high-prob fade), 24h range position, BTC macro direction")
lines.append("Reply with ONLY JSON: {\"vote\":\"UP\"|\"DOWN\"|\"SKIP\",\"conf\":0.0-1.0,\"why\":\"<10 words>\"}")
return "\n".join(lines)
"""Two-tier: qwen3.5 (fast gate, 0.4s on MacBook) → ornith (deep verify on RTX 3070).
Gate handles clear signals; borderline calls escalate to ornith for final verdict."""
coin = cfg.get("coin","BTC")
ix = intel(coin); r = rsi(conn)
prompt = enriched_context(cfg, price, mom, mom_score, conn)
try:
fast_url = cfg.get("fast_llm_url", cfg.get("ollama_url","http://localhost:11434"))
fast_model = cfg.get("fast_llm_model", "qwen3.5:4b")
r1 = requests.post(fast_url+"/api/generate",
json={"model": fast_model, "prompt": prompt, "stream": False, "think": False,
"options": {"temperature": 0.3, "num_predict": 60}}, timeout=8)
txt = r1.json().get("response","")
s, e = txt.find("{"), txt.rfind("}")+1
d1 = json.loads(txt[s:e]) if s >= 0 else {}
fast_v, fast_c, fast_w = str(d1.get("vote","BORDERLINE")).upper(), float(d1.get("conf",0.5)), str(d1.get("why",""))[:120]
except Exception as ex:
fast_v, fast_c, fast_w = "BORDERLINE", 0.50, f"gate: {str(ex)[:40]}"
if fast_v == "BORDERLINE" or (fast_v in ("UP","DOWN") and fast_c < 0.70):
try:
deep_url = cfg.get("deep_llm_url", cfg.get("ollama_url","http://10.30.20.186:11434"))
deep_model = cfg.get("deep_llm_model", cfg.get("ollama_model","ornith:latest"))
r2 = requests.post(deep_url+"/api/generate",
json={"model": deep_model, "prompt": prompt, "stream": False, "think": False,
"options": {"temperature": 0.2, "num_predict": 90}}, timeout=60)
txt2 = r2.json().get("response","")
s2, e2 = txt2.find("{"), txt2.rfind("}")+1
d2 = json.loads(txt2[s2:e2]) if s2 >= 0 else {}
v = str(d2.get("vote","SKIP")).upper()
if v not in ("UP","DOWN","SKIP"): v = "SKIP"
return v, float(d2.get("conf",0)), str(d2.get("why",""))[:120]
except Exception as ex2:
LOG.warning(f"deep llm failed: {ex2}")
if fast_v in ("UP","DOWN","SKIP"): return fast_v, fast_c, fast_w
return "SKIP", 0.0, "llm offline"
if fast_v not in ("UP","DOWN","SKIP"): fast_v = "SKIP"
return fast_v, fast_c, fast_w
def decide(cfg, conn, price):
"""Two-tier decision: qwen fast-gate → ornith deep-verify.
RSI extremes → force mean-reversion. BTC macro trend → gate counter-trend bets.
Self-adapting thresholds from resolved bet performance."""
mom, mom_score = momentum(conn)
lv, lc, lw = llm_vote(cfg, price, mom, mom_score, conn)
mode = cfg["mode"]
if mode == "DOWN_SPAM": return mom, lv, lc, lw, "DOWN", "spam-mode short"
if mode == "UP_SPAM": return mom, lv, lc, lw, "UP", "spam-mode long"
# ── RSI contrarian: extreme readings → auto-fade ──
r = rsi(conn)
if r is not None:
if r > 85:
return mom, lv, lc, lw, "DOWN", f"⚠ RSI {r:.0f} extreme overbought → fade DOWN"
if r < 15:
return mom, lv, lc, lw, "UP", f"⚠ RSI {r:.0f} extreme oversold → fade UP"
# ── swing threshold gate ──
thr = cfg.get("swing_threshold_pct", 0.005)
if mom == "FLAT" or mom_score < thr:
return mom, lv, lc, lw, "SKIP", f"no swing ({mom_score:.3f}% < {thr}%)"
if lv not in ("UP", "DOWN"):
return mom, lv, lc, lw, "SKIP", f"passes: {lw}"
# ── BTC macro correlation: don't fight the trend ──
coin = cfg.get("coin","BTC")
ix = intel(coin)
btc_chg = ix.get("chg24", 0)
if lv == "DOWN" and btc_chg > 0.5:
if lc < 0.78:
return mom, lv, lc, lw, "SKIP", f"BTC +{btc_chg:.1f}% 24h — no counter-trend shorts (conf {lc:.2f}<0.78)"
if lv == "UP" and btc_chg < -0.5:
if lc < 0.78:
return mom, lv, lc, lw, "SKIP", f"BTC {btc_chg:.1f}% 24h — no counter-trend longs (conf {lc:.2f}<0.78)"
# ── adaptive confidence (self-tunes from resolved bets) ──
min_conf = learned_min_conf(conn, cfg)
# ── self-adapting overrides (LLM-written after bet resolution) ──
override = conn.execute("SELECT v FROM kv WHERE k='control_override'").fetchone()
if override:
try:
ov = json.loads(override[0])
if ov.get("force_min_conf"): min_conf = float(ov["force_min_conf"])
thr = ov.get("force_swing_threshold", thr)
except Exception: pass
if lc < min_conf:
return mom, lv, lc, lw, "SKIP", f"conf {lc:.2f}<{min_conf:.2f} (adaptive)"
return mom, lv, lc, lw, lv, f"{lv}: {mom} spike {mom_score:.2f}% conf={lc:.2f} [{lw[:60]}]"
# ───────── learning: resolve + adapt ─────────
def learned_min_conf(conn, cfg):
"""Adaptive confidence: rises after losses, relaxes after wins. Persisted in kv table."""
if not cfg.get("learning", True):
return cfg.get("min_conf", 0.55)
row = conn.execute("SELECT v FROM kv WHERE k='min_conf'").fetchone()
stored = row[0] if row else cfg.get("min_conf", 0.55)
res = conn.execute("SELECT pnl_cents FROM orders WHERE status IN ('won','lost') ORDER BY id DESC LIMIT 20").fetchall()
if len(res) < 8:
return stored
wr = sum(1 for r in res if r[0] and r[0] > 0) / len(res)
new = stored
if wr < 0.45: new = min(0.80, stored + 0.05)
elif wr > 0.60: new = max(0.50, stored - 0.05)
if new != stored:
conn.execute("INSERT OR REPLACE INTO kv VALUES ('min_conf', ?)", (new,))
conn.commit()
return new
def resolve_bets(kx, conn):
"""Settle finished markets against real outcomes (sim AND live)."""
open_orders = conn.execute(
"SELECT id, ticker, side, count, price_cents, dry FROM orders WHERE status IN ('placed','posted')").fetchall()
for oid, ticker, side, count, px, dry in open_orders:
try:
m = kx.req("GET", f"/markets/{ticker}")
status = m.get("status")
if status not in ("settled", "finalized", "determined"):
# also catch expired-untraded: close_time passed + no result → mark dead
ct = m.get("close_time")
if ct:
from datetime import datetime, timezone
if datetime.fromisoformat(ct.replace("Z","+00:00")).timestamp() < time.time() - 120:
conn.execute("UPDATE orders SET status='expired', settle_ts=? WHERE id=?", (time.time(), oid))
conn.commit()
continue
result = (m.get("result") or "").lower() # 'yes' | 'no'
if not result:
continue
won = (result == side)
pnl = (100 - px) * count if won else -px * count
conn.execute("UPDATE orders SET status=?, settle_ts=?, pnl_cents=? WHERE id=?",
("won" if won else "lost", time.time(), pnl, oid))
conn.commit()
log_event(conn, "info", f"{'[SIM] ' if dry else ''}{'WIN' if won else 'LOSS'} {ticker} {side}@{px}¢ → {result} ({'+' if won else ''}{pnl}¢)")
except Exception:
continue
# ───────── counter-hedge: lock in profit on open positions ─────────
def hedge_positions(conn, kx, ticker, cfg, book):
"""Scan open positions. If opposite side is cheap enough, buy it to lock profit.
Returns 'hedged' if a counter-order was placed, else None."""
open_orders = conn.execute(
"SELECT id,side,price_cents FROM orders WHERE ticker=? AND status IN ('placed','posted')",
(ticker,)).fetchall()
if not open_orders:
return None
min_profit = cfg.get("min_profit_cents", 8)
for oid, side, entry_px in open_orders:
opp_side = "yes" if side == "no" else "no"
opp_ask_list = book.get("yes" if opp_side == "yes" else "no")
opp_ask = (opp_ask_list or [[None]])[0][0]
if opp_ask is None:
continue
profit = 100 - entry_px - opp_ask
if profit >= min_profit:
res = kx.order(ticker, opp_side, 1, opp_ask, cfg["dry_run"])
oid2 = res.get("order_id", res.get("order", {}).get("order_id", "hedge"))
conn.execute("INSERT INTO orders (ts,ticker,side,count,price_cents,dry,order_id,status) VALUES (?,?,?,?,?,?,?,?)",
(time.time(), ticker, opp_side, 1, opp_ask, 1 if cfg["dry_run"] else 0, str(oid2), "placed"))
conn.execute("UPDATE orders SET status='hedged' WHERE id=?", (oid,))
conn.commit()
log_event(conn, "info", f"HEDGE {ticker}: {side}@{entry_px}¢ + {opp_side}@{opp_ask}¢ → locked {profit}¢")
return "hedged"
return None
# ───────── main loop ─────────
def daily_pnl(conn):
return conn.execute("SELECT COALESCE(SUM(pnl_cents),0) FROM orders WHERE date(ts,'unixepoch','localtime')=date('now','localtime') AND dry=0").fetchone()[0]
_last_adapt_ts = 0
def adapt_controls(conn, cfg):
"""After bets resolve, ask qwen to self-tune: adjust thresholds, confidence, sizing."""
global _last_adapt_ts
if time.time() - _last_adapt_ts < 600: # max every 10 min
return
resolved = conn.execute(
"SELECT side, pnl_cents, price_cents FROM orders WHERE status IN ('won','lost') ORDER BY id DESC LIMIT 20"
).fetchall()
if len(resolved) < 5:
return
wins = sum(1 for r in resolved if r[1] and r[1] > 0)
wr = wins / len(resolved)
summary = {"recent_bets": len(resolved), "win_rate": round(wr, 2),
"wins": wins, "losses": len(resolved)-wins,
"avg_pnl": round(sum(r[1] for r in resolved if r[1])/len(resolved)) if resolved else 0}
prompt = (
f"Bot trading performance: {json.dumps(summary)}. "
"Current settings: swing_threshold={cfg.get('swing_threshold_pct',0.005)}% "
"min_conf={cfg.get('min_conf',0.50)}. "
"Suggest adjustments to maximize net profit. Reply with ONLY JSON: "
'{"force_min_conf":0.50,"force_swing_threshold":0.005,"reason":"<10 words>"} '
"or empty JSON {} if no changes needed."
)
try:
fast_url = cfg.get("fast_llm_url", "http://localhost:11434")
r = requests.post(fast_url+"/api/generate",
json={"model": cfg.get("fast_llm_model","qwen3.5:4b"), "prompt": prompt,
"stream": False, "think": False, "options": {"temperature": 0.1, "num_predict": 80}},
timeout=10)
txt = r.json().get("response","")
s, e = txt.find("{"), txt.rfind("}")+1
ov = json.loads(txt[s:e]) if s >= 0 else {}
if ov and ov.get("reason"):
conn.execute("INSERT OR REPLACE INTO kv(k,v) VALUES ('control_override',?)",
(json.dumps(ov),))
conn.commit()
LOG.info(f"🔧 self-adapt: {ov.get('reason')} — override={json.dumps({k:v for k,v in ov.items() if k!='reason'})}")
_last_adapt_ts = time.time()
except Exception:
pass # silently skip on failure
# ───────── main loop ─────────
return conn.execute("SELECT COALESCE(SUM(pnl_cents),0) FROM orders WHERE date(ts,'unixepoch','localtime')=date('now','localtime') AND dry=0").fetchone()[0]
def run():
global HOME, CFG_PATH, DB_PATH
import argparse, fcntl
ap = argparse.ArgumentParser()
ap.add_argument("--config", default=str(HOME/"config.json"), help="config path")
ap.add_argument("--lock", default="bot", help="lock file name stem")
ap.add_argument("--db", default=None, help="db path (overrides config)")
args, _ = ap.parse_known_args()
CFG_PATH = Path(args.config)
HOME = CFG_PATH.parent
DB_PATH = Path(args.db) if args.db else (HOME / "state.db")
lock_fd = open(HOME / f"{args.lock}.lock", "w")
try:
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
print("another bot instance is already running — exiting")
sys.exit(0)
cfg = load_cfg()
if cfg.get("proxy"):
os.environ["HTTP_PROXY"] = os.environ["HTTPS_PROXY"] = cfg["proxy"]
if cfg.get("db_path"):
DB_PATH = HOME / cfg["db_path"]
kx = Kalshi(cfg["api_key_id"], cfg["key_path"])
conn = db()
log_event(conn, "info", f"K4LSH1_OPS online — mode={cfg['mode']} dry_run={cfg['dry_run']} stake={cfg['stake_cents']}¢ max_spend={cfg.get('max_spend_cents',100)}¢")
last_bet_market = None
last_decision_ts = None
# stagger startup to avoid simultaneous LLM calls
stagger = cfg.get("start_delay_sec", 0)
if stagger:
LOG.info(f"staggering start by {stagger}s")
time.sleep(stagger)
while True:
cfg = load_cfg() # hot reload (dashboard edits)
if cfg.get("kill"):
log_event(conn, "warning", "KILL switch engaged — bot halted")
break
try:
resolve_bets(kx, conn)
adapt_controls(conn, cfg)
p = btc_price(cfg.get("coin", "BTC"))
if p:
conn.execute("INSERT INTO ticks VALUES (?,?)", (time.time(), p))
conn.execute("DELETE FROM ticks WHERE ts < ?", (time.time()-7200,))
conn.commit()
coin = cfg.get("coin", "BTC")
if cfg["mode"] != "OFF" and p:
mkts = kx.markets(cfg["series"], "open", 3)
if mkts:
m = sorted(mkts, key=lambda x: x["close_time"])[-1]
ticker = m["ticker"]
close_ts = datetime.fromisoformat(m["close_time"].replace("Z","+00:00")).timestamp()
mins_left = (close_ts - time.time())/60
if mins_left > 0.5:
# 5-min re-evaluation window
cycle_sec = cfg.get("decision_interval_sec", 300)
now_ts = time.time()
if last_decision_ts and (now_ts - last_decision_ts) < cycle_sec:
continue # wait for next decision window
last_decision_ts = now_ts
book = kx.orderbook(ticker)
# hedge check: close profitable positions
if hedge_positions(conn, kx, ticker, cfg, book) == "hedged":
continue
mom, lv, lc, lw, final, why = decide(cfg, conn, p)
yes_ask = (book.get("yes") or [[None]])[0][0]
no_ask = (book.get("no") or [[None]])[0][0]
ask = yes_ask if final == "UP" else (no_ask if final == "DOWN" else None)
side = "yes" if final == "UP" else "no"
conn.execute("INSERT INTO decisions (ts,ticker,mode,momentum,llm_vote,llm_conf,llm_why,final,price,reason) VALUES (?,?,?,?,?,?,?,?,?,?)",
(time.time(), ticker, cfg["mode"], mom, lv, lc, lw, final, p, why))
conn.commit()
if final in ("UP","DOWN"):
if daily_pnl(conn) <= -cfg["daily_loss_cap_cents"]:
log_event(conn, "warning", "daily loss cap hit — sitting out")
continue
try:
verify = kx.req("GET", f"/markets/{ticker}")
st = verify.get("status", "open")
if st and st not in ("open", "active"):
log_event(conn, "info", f"skip {ticker}: market closed (status={st})")
continue
except Exception:
pass
# confidence-scaled contracts: max $1.00 total spend
max_spend = cfg.get("max_spend_cents", 100)
if ask and ask <= max_spend:
n = 1 if lc < 0.70 else (2 if lc < 0.80 else (3 if lc < 0.90 else 4))
while n * ask > max_spend and n > 1:
n -= 1
px = ask
maker = False
else:
n = 1
px = min(cfg.get("post_price_cents", 50), max_spend)
maker = True
try:
res = kx.order(ticker, side, n, px, cfg["dry_run"])
oid = res.get("order_id", res.get("order", {}).get("order_id", "sim"))
conn.execute("INSERT INTO orders (ts,ticker,side,count,price_cents,dry,order_id,status) VALUES (?,?,?,?,?,?,?,?)",
(time.time(), ticker, side, n, px, 1 if cfg["dry_run"] else 0, str(oid), "placed"))
conn.commit()
tag = "SIM" if cfg["dry_run"] else ("TAKER" if not maker else "POST")
log_event(conn, "info", f"{tag} {side.upper()} x{n} {ticker} @ {px}¢ — {why}")
except Exception as e:
log_event(conn, "warning", f"order failed: {e}")
else:
LOG.info(f"skip {ticker}: {why}")
except Exception as e:
log_event(conn, "error", f"loop error: {e}")
time.sleep(20)
if __name__ == "__main__":
run()

211
bot_console.log Normal file
View File

@@ -0,0 +1,211 @@
2026-08-01 21:32:22,951 K4LSH1_OPS online — mode=AUTO dry_run=True stake=50¢
2026-08-01 21:32:27,370 skip KXBTC15M-26AUG020045-45: mom=FLAT llm=SKIP
2026-08-01 21:35:07,483 K4LSH1_OPS online — mode=AUTO dry_run=True stake=50¢
2026-08-01 21:35:11,460 skip KXBTC15M-26AUG020045-45: mom=FLAT llm=SKIP
2026-08-01 21:35:46,925 K4LSH1_OPS online — mode=DOWN_SPAM dry_run=True stake=45¢
2026-08-01 21:35:50,882 [SIM] MAKER-POST NO KXBTC15M-26AUG020045-45 @ 45¢ — spam-mode short
2026-08-01 21:36:36,636 K4LSH1_OPS online — mode=AUTO dry_run=True stake=50¢
2026-08-01 21:36:39,490 skip KXBTC15M-26AUG020045-45: mom=FLAT llm=SKIP
2026-08-01 21:45:38,730 skip KXBTC15M-26AUG020100-00: mom=FLAT llm=SKIP
2026-08-01 21:45:40,622 skip KXBTC15M-26AUG020100-00: mom=FLAT llm=SKIP
2026-08-01 21:45:42,761 skip KXBTC15M-26AUG020100-00: mom=FLAT llm=SKIP
2026-08-01 21:45:45,645 skip KXBTC15M-26AUG020100-00: mom=FLAT llm=SKIP
2026-08-01 22:00:46,431 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-01 22:00:48,836 skip KXBTC15M-26AUG020115-15: ask None¢ > stake cap 20¢
2026-08-01 22:00:56,705 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-01 22:00:58,206 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-01 22:01:07,999 skip KXBTC15M-26AUG020115-15: mom=UP llm=SKIP
2026-08-01 22:01:11,843 skip KXBTC15M-26AUG020115-15: ask None¢ > stake cap 20¢
2026-08-01 22:01:24,692 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-01 22:01:26,687 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-01 22:01:35,077 skip KXBTC15M-26AUG020115-15: mom=UP llm=SKIP
2026-08-01 22:01:52,938 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-01 22:01:54,595 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-01 22:02:16,143 skip KXBTC15M-26AUG020115-15: mom=UP llm=SKIP
2026-08-01 22:02:23,023 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-01 22:02:46,485 skip KXBTC15M-26AUG020115-15: mom=FLAT llm=SKIP
2026-08-01 22:15:51,326 skip KXBTC15M-26AUG020130-30: mom=DOWN llm=SKIP
2026-08-01 22:15:53,252 skip KXBTC15M-26AUG020130-30: mom=DOWN llm=SKIP
2026-08-01 22:15:57,991 skip KXBTC15M-26AUG020130-30: mom=DOWN llm=SKIP
2026-08-01 22:16:00,178 skip KXBTC15M-26AUG020130-30: mom=DOWN llm=SKIP
2026-08-01 22:30:28,534 skip KXBTC15M-26AUG020145-45: mom=FLAT llm=SKIP
2026-08-01 22:30:30,798 skip KXBTC15M-26AUG020145-45: mom=FLAT llm=SKIP
2026-08-01 22:30:33,469 skip KXBTC15M-26AUG020145-45: mom=FLAT llm=SKIP
2026-08-01 22:30:35,447 skip KXBTC15M-26AUG020145-45: mom=FLAT llm=SKIP
2026-08-01 22:45:45,108 skip KXBTC15M-26AUG020200-00: mom=DOWN llm=SKIP
2026-08-01 22:45:46,902 skip KXBTC15M-26AUG020200-00: mom=DOWN llm=SKIP
2026-08-01 22:45:49,008 skip KXBTC15M-26AUG020200-00: mom=DOWN llm=SKIP
2026-08-01 22:45:50,847 skip KXBTC15M-26AUG020200-00: mom=DOWN llm=SKIP
2026-08-01 23:00:42,225 skip KXBTC15M-26AUG020215-15: mom=FLAT llm=SKIP
2026-08-01 23:00:44,082 skip KXBTC15M-26AUG020215-15: mom=FLAT llm=SKIP
2026-08-01 23:00:46,004 skip KXBTC15M-26AUG020215-15: mom=FLAT llm=SKIP
2026-08-01 23:00:55,206 skip KXBTC15M-26AUG020215-15: mom=FLAT llm=SKIP
2026-08-01 23:15:39,537 skip KXBTC15M-26AUG020230-30: mom=FLAT llm=SKIP
2026-08-01 23:15:41,592 skip KXBTC15M-26AUG020230-30: mom=FLAT llm=SKIP
2026-08-01 23:15:43,457 skip KXBTC15M-26AUG020230-30: mom=FLAT llm=SKIP
2026-08-01 23:15:47,287 skip KXBTC15M-26AUG020230-30: mom=FLAT llm=SKIP
2026-08-01 23:30:39,844 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-01 23:30:41,094 skip KXBTC15M-26AUG020245-45: mom=UP llm=SKIP
2026-08-01 23:30:43,887 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-01 23:30:47,443 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-01 23:31:02,912 skip KXBTC15M-26AUG020245-45: mom=UP llm=SKIP
2026-08-01 23:31:06,956 skip KXBTC15M-26AUG020245-45: mom=UP llm=SKIP
2026-08-01 23:31:15,212 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-01 23:31:38,420 skip KXBTC15M-26AUG020245-45: mom=UP llm=SKIP
2026-08-01 23:45:33,888 skip KXBTC15M-26AUG020300-00: mom=FLAT llm=SKIP
2026-08-01 23:45:35,575 skip KXBTC15M-26AUG020300-00: mom=FLAT llm=SKIP
2026-08-01 23:45:37,428 skip KXBTC15M-26AUG020300-00: mom=FLAT llm=SKIP
2026-08-01 23:45:42,154 skip KXBTC15M-26AUG020300-00: mom=FLAT llm=SKIP
2026-08-02 00:00:28,526 skip KXBTC15M-26AUG020315-15: mom=FLAT llm=SKIP
2026-08-02 00:00:30,216 skip KXBTC15M-26AUG020315-15: mom=FLAT llm=SKIP
2026-08-02 00:00:32,066 skip KXBTC15M-26AUG020315-15: mom=FLAT llm=SKIP
2026-08-02 00:00:35,709 skip KXBTC15M-26AUG020315-15: mom=FLAT llm=SKIP
2026-08-02 00:15:26,330 skip KXBTC15M-26AUG020330-30: mom=FLAT llm=SKIP
2026-08-02 00:15:28,171 skip KXBTC15M-26AUG020330-30: mom=FLAT llm=SKIP
2026-08-02 00:15:30,051 skip KXBTC15M-26AUG020330-30: mom=FLAT llm=SKIP
2026-08-02 00:15:31,894 skip KXBTC15M-26AUG020330-30: mom=FLAT llm=SKIP
2026-08-02 00:30:41,938 skip KXBTC15M-26AUG020345-45: mom=FLAT llm=SKIP
2026-08-02 00:30:43,537 skip KXBTC15M-26AUG020345-45: mom=FLAT llm=SKIP
2026-08-02 00:30:45,416 skip KXBTC15M-26AUG020345-45: mom=FLAT llm=SKIP
2026-08-02 00:30:47,247 skip KXBTC15M-26AUG020345-45: mom=FLAT llm=SKIP
2026-08-02 00:45:35,241 skip KXBTC15M-26AUG020400-00: mom=UP llm=SKIP
2026-08-02 00:45:38,670 skip KXBTC15M-26AUG020400-00: mom=UP llm=SKIP
2026-08-02 00:45:45,948 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-02 00:45:47,847 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-02 00:46:08,932 skip KXBTC15M-26AUG020400-00: mom=FLAT llm=SKIP
2026-08-02 00:46:11,559 skip KXBTC15M-26AUG020400-00: mom=FLAT llm=SKIP
2026-08-02 01:00:42,618 skip KXBTC15M-26AUG020415-15: mom=FLAT llm=SKIP
2026-08-02 01:00:44,309 skip KXBTC15M-26AUG020415-15: mom=FLAT llm=SKIP
2026-08-02 01:00:45,798 skip KXBTC15M-26AUG020415-15: mom=FLAT llm=SKIP
2026-08-02 01:00:49,527 skip KXBTC15M-26AUG020415-15: mom=FLAT llm=SKIP
2026-08-02 01:15:57,060 skip KXBTC15M-26AUG020430-30: mom=FLAT llm=SKIP
2026-08-02 01:15:58,723 skip KXBTC15M-26AUG020430-30: mom=FLAT llm=SKIP
2026-08-02 01:16:01,867 skip KXBTC15M-26AUG020430-30: mom=FLAT llm=SKIP
2026-08-02 01:16:03,992 skip KXBTC15M-26AUG020430-30: mom=FLAT llm=SKIP
2026-08-02 01:30:09,358 skip KXBTC15M-26AUG020445-45: mom=DOWN llm=SKIP
2026-08-02 01:30:12,000 skip KXBTC15M-26AUG020445-45: mom=DOWN llm=SKIP
2026-08-02 01:30:13,246 skip KXBTC15M-26AUG020445-45: mom=DOWN llm=SKIP
2026-08-02 01:30:14,406 skip KXBTC15M-26AUG020445-45: ask None¢ > stake cap 20¢
2026-08-02 01:30:36,032 skip KXBTC15M-26AUG020445-45: mom=DOWN llm=SKIP
2026-08-02 01:45:43,435 skip KXBTC15M-26AUG020500-00: mom=DOWN llm=SKIP
2026-08-02 01:45:44,342 skip KXBTC15M-26AUG020500-00: mom=DOWN llm=SKIP
2026-08-02 01:45:45,943 skip KXBTC15M-26AUG020500-00: mom=DOWN llm=SKIP
2026-08-02 01:45:46,896 skip KXBTC15M-26AUG020500-00: mom=DOWN llm=SKIP
2026-08-02 02:00:37,677 skip KXBTC15M-26AUG020515-15: mom=FLAT llm=SKIP
2026-08-02 02:00:39,938 skip KXBTC15M-26AUG020515-15: mom=FLAT llm=SKIP
2026-08-02 02:00:40,906 skip KXBTC15M-26AUG020515-15: mom=FLAT llm=SKIP
2026-08-02 02:00:41,808 skip KXBTC15M-26AUG020515-15: mom=FLAT llm=SKIP
2026-08-02 02:15:30,670 skip KXBTC15M-26AUG020530-30: mom=UP llm=SKIP
2026-08-02 02:15:31,451 skip KXBTC15M-26AUG020530-30: mom=UP llm=SKIP
2026-08-02 02:15:32,257 skip KXBTC15M-26AUG020530-30: mom=UP llm=SKIP
2026-08-02 02:15:33,869 skip KXBTC15M-26AUG020530-30: ask None¢ > stake cap 20¢
2026-08-02 02:15:55,453 skip KXBTC15M-26AUG020530-30: mom=UP llm=SKIP
2026-08-02 02:30:18,871 skip KXBTC15M-26AUG020545-45: mom=DOWN llm=SKIP
2026-08-02 02:30:19,876 skip KXBTC15M-26AUG020545-45: mom=DOWN llm=SKIP
2026-08-02 02:30:20,815 skip KXBTC15M-26AUG020545-45: mom=DOWN llm=SKIP
2026-08-02 02:30:22,225 skip KXBTC15M-26AUG020545-45: mom=DOWN llm=SKIP
2026-08-02 02:45:52,336 skip KXBTC15M-26AUG020600-00: mom=FLAT llm=SKIP
2026-08-02 02:45:53,166 skip KXBTC15M-26AUG020600-00: mom=FLAT llm=SKIP
2026-08-02 02:45:53,865 skip KXBTC15M-26AUG020600-00: mom=FLAT llm=SKIP
2026-08-02 02:45:54,729 skip KXBTC15M-26AUG020600-00: mom=FLAT llm=SKIP
2026-08-02 02:58:32,790 loop error: HTTPSConnectionPool(host='api.elections.kalshi.com', port=443): Read timed out. (read timeout=15)
2026-08-02 02:58:33,719 loop error: HTTPSConnectionPool(host='api.elections.kalshi.com', port=443): Read timed out. (read timeout=15)
2026-08-02 02:58:34,346 loop error: HTTPSConnectionPool(host='api.elections.kalshi.com', port=443): Read timed out. (read timeout=15)
2026-08-02 02:58:35,283 loop error: HTTPSConnectionPool(host='api.elections.kalshi.com', port=443): Read timed out. (read timeout=15)
2026-08-02 02:59:04,974 loop error: HTTPSConnectionPool(host='api.elections.kalshi.com', port=443): Max retries exceeded with url: /trade-api/v2/markets?series_ticker=KXBTC15M&status=open&limit=3 (Caused by SSLError(SSLEOFError(8, '[SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1016)')))
2026-08-02 02:59:05,856 loop error: HTTPSConnectionPool(host='api.elections.kalshi.com', port=443): Max retries exceeded with url: /trade-api/v2/markets?series_ticker=KXBTC15M&status=open&limit=3 (Caused by SSLError(SSLEOFError(8, '[SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1016)')))
2026-08-02 02:59:06,478 loop error: HTTPSConnectionPool(host='api.elections.kalshi.com', port=443): Max retries exceeded with url: /trade-api/v2/markets?series_ticker=KXBTC15M&status=open&limit=3 (Caused by SSLError(SSLEOFError(8, '[SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1016)')))
2026-08-02 02:59:07,417 loop error: HTTPSConnectionPool(host='api.elections.kalshi.com', port=443): Max retries exceeded with url: /trade-api/v2/markets?series_ticker=KXBTC15M&status=open&limit=3 (Caused by SSLError(SSLEOFError(8, '[SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1016)')))
2026-08-02 02:59:37,144 loop error: HTTPSConnectionPool(host='api.elections.kalshi.com', port=443): Max retries exceeded with url: /trade-api/v2/markets?series_ticker=KXBTC15M&status=open&limit=3 (Caused by SSLError(SSLEOFError(8, '[SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1016)')))
2026-08-02 02:59:38,018 loop error: HTTPSConnectionPool(host='api.elections.kalshi.com', port=443): Max retries exceeded with url: /trade-api/v2/markets?series_ticker=KXBTC15M&status=open&limit=3 (Caused by SSLError(SSLEOFError(8, '[SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1016)')))
2026-08-02 02:59:38,626 loop error: HTTPSConnectionPool(host='api.elections.kalshi.com', port=443): Max retries exceeded with url: /trade-api/v2/markets?series_ticker=KXBTC15M&status=open&limit=3 (Caused by SSLError(SSLEOFError(8, '[SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1016)')))
2026-08-02 02:59:39,575 loop error: HTTPSConnectionPool(host='api.elections.kalshi.com', port=443): Max retries exceeded with url: /trade-api/v2/markets?series_ticker=KXBTC15M&status=open&limit=3 (Caused by SSLError(SSLEOFError(8, '[SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1016)')))
2026-08-02 03:00:43,157 skip KXBTC15M-26AUG020615-15: mom=FLAT llm=SKIP
2026-08-02 03:00:45,057 skip KXBTC15M-26AUG020615-15: mom=FLAT llm=SKIP
2026-08-02 03:00:45,891 skip KXBTC15M-26AUG020615-15: mom=FLAT llm=SKIP
2026-08-02 03:00:46,736 skip KXBTC15M-26AUG020615-15: mom=FLAT llm=SKIP
2026-08-02 03:15:16,790 skip KXBTC15M-26AUG020630-30: mom=DOWN llm=SKIP
2026-08-02 03:15:18,273 skip KXBTC15M-26AUG020630-30: mom=DOWN llm=SKIP
2026-08-02 03:15:18,602 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-02 03:15:19,599 skip KXBTC15M-26AUG020630-30: mom=DOWN llm=SKIP
2026-08-02 03:16:12,711 skip KXBTC15M-26AUG020630-30: mom=DOWN llm=SKIP
2026-08-02 03:31:24,240 ollama vote failed: HTTPConnectionPool(host='localhost', port=11434): Read timed out. (read timeout=45)
2026-08-02 03:31:24,365 skip KXBTC15M-26AUG020645-45: mom=FLAT llm=SKIP
2026-08-02 03:31:25,838 skip KXBTC15M-26AUG020645-45: mom=FLAT llm=SKIP
2026-08-02 03:31:29,474 skip KXBTC15M-26AUG020645-45: mom=FLAT llm=SKIP
2026-08-02 03:31:30,958 skip KXBTC15M-26AUG020645-45: mom=FLAT llm=SKIP
2026-08-02 03:45:56,982 skip KXBTC15M-26AUG020700-00: mom=UP llm=SKIP
2026-08-02 03:45:59,371 skip KXBTC15M-26AUG020700-00: mom=UP llm=SKIP
2026-08-02 03:46:02,520 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-02 03:46:04,909 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-02 03:46:23,849 skip KXBTC15M-26AUG020700-00: mom=UP llm=SKIP
2026-08-02 03:46:26,271 skip KXBTC15M-26AUG020700-00: mom=UP llm=SKIP
2026-08-02 04:00:19,304 skip KXBTC15M-26AUG020715-15: mom=FLAT llm=SKIP
2026-08-02 04:00:26,091 skip KXBTC15M-26AUG020715-15: mom=FLAT llm=SKIP
2026-08-02 04:00:28,049 skip KXBTC15M-26AUG020715-15: mom=FLAT llm=SKIP
2026-08-02 04:00:31,611 skip KXBTC15M-26AUG020715-15: mom=FLAT llm=SKIP
2026-08-02 04:15:31,974 skip KXBTC15M-26AUG020730-30: mom=FLAT llm=SKIP
2026-08-02 04:15:34,753 skip KXBTC15M-26AUG020730-30: mom=FLAT llm=SKIP
2026-08-02 04:15:36,597 skip KXBTC15M-26AUG020730-30: mom=FLAT llm=SKIP
2026-08-02 04:15:39,965 skip KXBTC15M-26AUG020730-30: mom=FLAT llm=SKIP
2026-08-02 04:30:24,112 skip KXBTC15M-26AUG020745-45: mom=FLAT llm=SKIP
2026-08-02 04:30:25,736 skip KXBTC15M-26AUG020745-45: mom=FLAT llm=SKIP
2026-08-02 04:30:26,838 skip KXBTC15M-26AUG020745-45: mom=FLAT llm=SKIP
2026-08-02 04:30:27,694 skip KXBTC15M-26AUG020745-45: mom=FLAT llm=SKIP
2026-08-02 04:45:36,785 skip KXBTC15M-26AUG020800-00: mom=DOWN llm=SKIP
2026-08-02 04:45:37,654 skip KXBTC15M-26AUG020800-00: mom=DOWN llm=SKIP
2026-08-02 04:45:38,559 skip KXBTC15M-26AUG020800-00: mom=DOWN llm=SKIP
2026-08-02 04:45:40,349 skip KXBTC15M-26AUG020800-00: mom=DOWN llm=SKIP
2026-08-02 05:00:49,635 skip KXBTC15M-26AUG020815-15: mom=FLAT llm=SKIP
2026-08-02 05:00:50,833 skip KXBTC15M-26AUG020815-15: mom=FLAT llm=SKIP
2026-08-02 05:00:52,855 skip KXBTC15M-26AUG020815-15: mom=FLAT llm=SKIP
2026-08-02 05:00:53,719 skip KXBTC15M-26AUG020815-15: mom=FLAT llm=SKIP
2026-08-02 05:15:26,120 skip KXBTC15M-26AUG020830-30: ask None¢ > stake cap 20¢
2026-08-02 05:15:28,115 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-02 05:15:28,866 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-02 05:15:30,023 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-02 05:15:47,650 skip KXBTC15M-26AUG020830-30: mom=UP llm=SKIP
2026-08-02 05:15:49,350 skip KXBTC15M-26AUG020830-30: mom=UP llm=SKIP
2026-08-02 05:15:50,158 skip KXBTC15M-26AUG020830-30: mom=UP llm=SKIP
2026-08-02 05:15:56,001 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-02 05:16:22,405 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-02 05:16:48,860 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-02 05:17:10,408 skip KXBTC15M-26AUG020830-30: mom=UP llm=SKIP
2026-08-02 05:30:35,693 skip KXBTC15M-26AUG020845-45: mom=FLAT llm=SKIP
2026-08-02 05:30:36,662 skip KXBTC15M-26AUG020845-45: mom=FLAT llm=SKIP
2026-08-02 05:30:37,971 skip KXBTC15M-26AUG020845-45: mom=FLAT llm=SKIP
2026-08-02 05:30:38,801 skip KXBTC15M-26AUG020845-45: mom=FLAT llm=SKIP
2026-08-02 05:45:26,802 skip KXBTC15M-26AUG020900-00: mom=FLAT llm=SKIP
2026-08-02 05:45:27,631 skip KXBTC15M-26AUG020900-00: mom=FLAT llm=SKIP
2026-08-02 05:45:28,427 skip KXBTC15M-26AUG020900-00: mom=FLAT llm=SKIP
2026-08-02 05:45:30,046 skip KXBTC15M-26AUG020900-00: mom=FLAT llm=SKIP
2026-08-02 06:01:18,133 ollama vote failed: HTTPConnectionPool(host='localhost', port=11434): Read timed out. (read timeout=45)
2026-08-02 06:01:18,224 skip KXBTC15M-26AUG020915-15: mom=DOWN llm=SKIP
2026-08-02 06:01:19,219 ollama vote failed: HTTPConnectionPool(host='localhost', port=11434): Read timed out. (read timeout=45)
2026-08-02 06:01:19,298 skip KXBTC15M-26AUG020915-15: mom=DOWN llm=SKIP
2026-08-02 06:01:19,397 ollama vote failed: HTTPConnectionPool(host='localhost', port=11434): Read timed out. (read timeout=45)
2026-08-02 06:01:19,526 skip KXBTC15M-26AUG020915-15: mom=DOWN llm=SKIP
2026-08-02 06:01:21,685 ollama vote failed: HTTPConnectionPool(host='localhost', port=11434): Read timed out. (read timeout=45)
2026-08-02 06:01:21,767 skip KXBTC15M-26AUG020915-15: mom=DOWN llm=SKIP
2026-08-02 06:15:47,149 skip KXBTC15M-26AUG020930-30: mom=FLAT llm=SKIP
2026-08-02 06:15:48,039 skip KXBTC15M-26AUG020930-30: mom=FLAT llm=SKIP
2026-08-02 06:15:49,804 skip KXBTC15M-26AUG020930-30: mom=FLAT llm=SKIP
2026-08-02 06:15:51,210 skip KXBTC15M-26AUG020930-30: mom=FLAT llm=SKIP
2026-08-02 06:30:39,593 skip KXBTC15M-26AUG020945-45: mom=FLAT llm=SKIP
2026-08-02 06:30:40,731 skip KXBTC15M-26AUG020945-45: mom=FLAT llm=SKIP
2026-08-02 06:30:41,544 skip KXBTC15M-26AUG020945-45: mom=FLAT llm=SKIP
2026-08-02 06:30:43,148 skip KXBTC15M-26AUG020945-45: mom=FLAT llm=SKIP
2026-08-02 06:45:31,886 skip KXBTC15M-26AUG021000-00: mom=UP llm=SKIP
2026-08-02 06:45:32,885 skip KXBTC15M-26AUG021000-00: mom=UP llm=SKIP
2026-08-02 06:45:33,761 skip KXBTC15M-26AUG021000-00: ask None¢ > stake cap 20¢
2026-08-02 06:45:40,288 order failed: 410 Client Error: Gone for url: https://api.elections.kalshi.com/trade-api/v2/portfolio/orders
2026-08-02 06:45:55,288 skip KXBTC15M-26AUG021000-00: mom=UP llm=SKIP
2026-08-02 06:46:01,890 skip KXBTC15M-26AUG021000-00: mom=FLAT llm=SKIP
2026-08-02 07:00:24,822 skip KXBTC15M-26AUG021015-15: mom=FLAT llm=SKIP
2026-08-02 07:00:25,531 skip KXBTC15M-26AUG021015-15: mom=FLAT llm=SKIP
2026-08-02 07:00:26,365 skip KXBTC15M-26AUG021015-15: mom=FLAT llm=SKIP
2026-08-02 07:00:29,759 skip KXBTC15M-26AUG021015-15: mom=FLAT llm=SKIP
2026-08-02 07:15:34,740 skip KXBTC15M-26AUG021030-30: mom=FLAT llm=SKIP
2026-08-02 07:15:35,872 skip KXBTC15M-26AUG021030-30: mom=FLAT llm=SKIP
2026-08-02 07:15:36,714 skip KXBTC15M-26AUG021030-30: mom=FLAT llm=SKIP
2026-08-02 07:15:38,394 skip KXBTC15M-26AUG021030-30: mom=FLAT llm=SKIP

16
config.json Normal file
View File

@@ -0,0 +1,16 @@
{
"api_key_id": "28d5876b-2ece-4aa3-aa17-ec96e1e706eb",
"key_path": "/Users/drjones/kalshi-bot/kalshi_private_key.pem",
"mode": "AUTO",
"dry_run": false,
"stake_cents": 15,
"max_contracts": 1,
"daily_loss_cap_cents": 500,
"min_conf": 0.55,
"ollama_url": "http://10.30.20.186:11434",
"ollama_model": "ornith:latest",
"series": "KXDOGE15M",
"kill": false,
"swing_threshold_pct": 0.06,
"learning": true
}

75
dash_console.log Normal file
View File

@@ -0,0 +1,75 @@
* Serving Flask app 'dashboard'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5053
* Running on http://10.5.0.2:5053
Press CTRL+C to quit
127.0.0.1 - - [01/Aug/2026 21:32:48] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:32:48,554 127.0.0.1 - - [01/Aug/2026 21:32:48] "GET /api/state HTTP/1.1" 200 -
* Serving Flask app 'dashboard'
* Debug mode: off
Address already in use
Port 5053 is in use by another program. Either identify and stop that program, or start the server with a different port.
127.0.0.1 - - [01/Aug/2026 21:35:45] "POST /api/control HTTP/1.1" 200 -
2026-08-01 21:35:45,702 127.0.0.1 - - [01/Aug/2026 21:35:45] "POST /api/control HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:36:35] "POST /api/control HTTP/1.1" 200 -
2026-08-01 21:36:35,412 127.0.0.1 - - [01/Aug/2026 21:36:35] "POST /api/control HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:36:51] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:36:51,971 127.0.0.1 - - [01/Aug/2026 21:36:51] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:37:51] "GET / HTTP/1.1" 200 -
2026-08-01 21:37:51,322 127.0.0.1 - - [01/Aug/2026 21:37:51] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:37:51] "GET /favicon.ico HTTP/1.1" 404 -
2026-08-01 21:37:51,475 127.0.0.1 - - [01/Aug/2026 21:37:51] "GET /favicon.ico HTTP/1.1" 404 -
127.0.0.1 - - [01/Aug/2026 21:37:51] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:37:51,931 127.0.0.1 - - [01/Aug/2026 21:37:51] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:37:56] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:37:56,802 127.0.0.1 - - [01/Aug/2026 21:37:56] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:38:01] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:38:01,781 127.0.0.1 - - [01/Aug/2026 21:38:01] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:38:06] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:38:06,979 127.0.0.1 - - [01/Aug/2026 21:38:06] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:38:11] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:38:11,858 127.0.0.1 - - [01/Aug/2026 21:38:11] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:38:16] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:38:16,859 127.0.0.1 - - [01/Aug/2026 21:38:16] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:38:21] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:38:21,861 127.0.0.1 - - [01/Aug/2026 21:38:21] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:38:26] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:38:26,861 127.0.0.1 - - [01/Aug/2026 21:38:26] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:38:31] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:38:31,775 127.0.0.1 - - [01/Aug/2026 21:38:31] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:38:36] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:38:36,887 127.0.0.1 - - [01/Aug/2026 21:38:36] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:38:41] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:38:41,957 127.0.0.1 - - [01/Aug/2026 21:38:41] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:38:46] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:38:46,751 127.0.0.1 - - [01/Aug/2026 21:38:46] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:38:51] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:38:51,850 127.0.0.1 - - [01/Aug/2026 21:38:51] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:38:56] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:38:56,742 127.0.0.1 - - [01/Aug/2026 21:38:56] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:39:01] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:39:01,740 127.0.0.1 - - [01/Aug/2026 21:39:01] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:39:06] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:39:06,809 127.0.0.1 - - [01/Aug/2026 21:39:06] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:39:11] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:39:11,751 127.0.0.1 - - [01/Aug/2026 21:39:11] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:39:17] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:39:17,389 127.0.0.1 - - [01/Aug/2026 21:39:17] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:39:21] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:39:21,827 127.0.0.1 - - [01/Aug/2026 21:39:21] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:39:27] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:39:27,040 127.0.0.1 - - [01/Aug/2026 21:39:27] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:39:31] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:39:31,777 127.0.0.1 - - [01/Aug/2026 21:39:31] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:39:37] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:39:37,052 127.0.0.1 - - [01/Aug/2026 21:39:37] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:39:41] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:39:41,781 127.0.0.1 - - [01/Aug/2026 21:39:41] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:39:46] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:39:46,761 127.0.0.1 - - [01/Aug/2026 21:39:46] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:39:51] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:39:51,853 127.0.0.1 - - [01/Aug/2026 21:39:51] "GET /api/state HTTP/1.1" 200 -
127.0.0.1 - - [01/Aug/2026 21:39:56] "GET /api/state HTTP/1.1" 200 -
2026-08-01 21:39:56,761 127.0.0.1 - - [01/Aug/2026 21:39:56] "GET /api/state HTTP/1.1" 200 -

384
dashboard.py Normal file
View File

@@ -0,0 +1,384 @@
#!/usr/bin/env python3
"""K4LSH1_OPS — dashboard. Hacker console on :5053. Controls write config.json (bot hot-reloads)."""
import json, sqlite3, time, threading
from pathlib import Path
from flask import Flask, jsonify, request
HOME = Path(__file__).parent
CFG_PATH = HOME / "config.json"
DB_PATH = HOME / "state.db"
app = Flask(__name__)
HTML = r"""<!DOCTYPE html><html><head><meta charset="utf-8"><title>K4LSH1_OPS</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#020603;color:#00ff41;font-family:'SF Mono',Menlo,monospace;overflow-x:hidden}
body::before{content:'';position:fixed;inset:0;pointer-events:none;z-index:99;background:repeating-linear-gradient(0deg,transparent 0 2px,rgba(0,0,0,.22) 2px 4px)}
.glow{text-shadow:0 0 8px #00ff41,0 0 22px #00ff4133}
header{display:flex;align-items:center;justify-content:space-between;padding:12px 18px;border-bottom:1px solid #00ff4133;background:#031007}
h1{font-size:1.15rem;letter-spacing:3px}
h1 .dim{color:#0a5f2e}
.badge{padding:3px 10px;border:1px solid #00ff41;border-radius:3px;font-size:.68rem;margin-left:8px}
.badge.warn{border-color:#ffb000;color:#ffb000;text-shadow:0 0 8px #ffb000}
.badge.danger{border-color:#ff3131;color:#ff3131;text-shadow:0 0 8px #ff3131}
.badge.off{border-color:#0a5f2e;color:#0a5f2e;text-shadow:none}
.grid{display:grid;grid-template-columns:1.15fr 1.5fr 1fr;gap:10px;padding:12px 18px}
.panel{border:1px solid #00ff4144;background:#031007cc;border-radius:6px;padding:12px;margin-bottom:10px;box-shadow:0 0 18px #00ff4112 inset}
.panel h3{font-size:.66rem;color:#2ea043;letter-spacing:2px;margin-bottom:10px;text-transform:uppercase}
.panel h3::before{content:''}
.big{font-size:2.4rem;font-weight:800}
.small{font-size:.7rem;color:#2ea043}
.row{display:flex;justify-content:space-between;font-size:.78rem;padding:3px 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}.warn-t{color:#ffb000}
.log{max-height:420px;overflow-y:auto;font-size:.72rem;scrollbar-width:thin}
.log .e{padding:4px 0;border-bottom:1px dashed #00ff4118;line-height:1.5}
.log .why{color:#0a5f2e;font-size:.64rem}
button,select{background:#020603;border:1px solid #00ff41;color:#00ff41;font-family:inherit;padding:6px 12px;border-radius:3px;cursor:pointer;font-size:.72rem;letter-spacing:1px}
button:hover,select:hover{background:#00ff4122;box-shadow:0 0 12px #00ff4144}
button.danger{border-color:#ff3131;color:#ff3131}
button.danger:hover{background:#ff313122}
.ctl{display:flex;flex-direction:column;gap:10px}
canvas{width:100%;display:block}
.count{font-size:1.5rem;font-weight:800}
.book span{display:inline-block;width:44%;font-size:.72rem}
.cursor{display:inline-block;width:8px;height:14px;background:#00ff41;animation:b 1s steps(1) infinite;vertical-align:middle}
@keyframes b{50%{opacity:0}}
#statusbar{position:fixed;bottom:0;left:0;right:0;z-index:100;display:flex;align-items:center;gap:0;background:#010502f2;border-top:1px solid #00ff4166;backdrop-filter:blur(8px);font-size:.72rem;padding:0}
#statusbar .cell{padding:8px 14px;border-right:1px solid #00ff4122;white-space:nowrap}
#statusbar .cell b{color:#00ff41}
#statusbar .lab{color:#0a5f2e;font-size:.6rem;letter-spacing:1px;display:block}
#statusbar .live{background:#ff313111;color:#ff3131;border-right-color:#ff313144;text-shadow:0 0 8px #ff3131}
#statusbar .sim{background:#ffb00011;color:#ffb000}
#statusbar .up{color:#00ff41}#statusbar .dn{color:#ff3131}
body{padding-bottom:44px}
</style></head><body>
<header>
<div><h1 class="glow">K4LSH1<span class="dim">_OPS</span> <span class="cursor"></span></h1>
<span id="badges"></span></div>
<div class="small" id="clock">—</div>
</header>
<div class="grid">
<div>
<div class="panel"><h3>BTC/USD — kraken</h3>
<div class="big glow" id="btc">$—</div>
<div class="small" id="btc-mom">—</div>
<canvas id="spark" height="70" style="margin-top:8px"></canvas>
</div>
<div class="panel"><h3>active market</h3>
<div class="row"><span>ticker</span><b id="m-ticker">—</b></div>
<div class="row"><span>question</span><b id="m-q" style="font-size:.68rem">—</b></div>
<div class="row"><span>closes in</span><span class="count glow" id="m-close">—</span></div>
<div class="book" style="margin-top:6px"><span id="m-yes">YES —¢</span><span id="m-no">NO —¢</span></div>
</div>
<div class="panel"><h3>account</h3>
<div class="row"><span>balance</span><b id="acct-bal" class="glow">—</b></div>
<div class="row"><span>open positions</span><b id="acct-pos">—</b></div>
<div class="row"><span>bets placed (session)</span><b id="acct-bets">—</b></div>
</div>
</div>
<div>
<div class="panel"><h3>AI decision stream — ollama local</h3><div class="log" id="dec"></div></div>
<div class="panel"><h3>system events</h3><div class="log" id="ev" style="max-height:140px"></div></div>
</div>
<div>
<div class="panel"><h3>controls</h3><div class="ctl">
<div class="small">STRATEGY MODE</div>
<select id="c-mode" onchange="ctl()">
<option value="AUTO">AUTO (AI+momentum)</option>
<option value="DOWN_SPAM">DOWN_SPAM (perma-short)</option>
<option value="UP_SPAM">UP_SPAM (perma-long)</option>
<option value="OFF">OFF (paused)</option>
</select>
<div class="small">MAX STAKE / BET: <b id="stake-v">50¢</b></div>
<input type="range" id="c-stake" min="10" max="99" step="1" oninput="document.getElementById('stake-v').textContent=this.value+'¢'" onchange="ctl()" style="width:100%">
<button id="c-dry" onclick="toggleDry()">—</button>
<button class="danger" onclick="kill()">⛔ KILL BOT</button>
</div></div>
<div class="panel"><h3>order blotter</h3><div class="log" id="ord" style="max-height:300px"></div></div>
</div>
</div>
<div id="statusbar">
<div class="cell" id="sb-state">—</div>
<div class="cell"><span class="lab">MODE</span><b id="sb-mode">—</b></div>
<div class="cell"><span class="lab">BTC</span><b id="sb-btc">—</b></div>
<div class="cell"><span class="lab">24H</span><b id="sb-24h">—</b></div>
<div class="cell"><span class="lab">FEAR/GREED</span><b id="sb-fng">—</b></div>
<div class="cell"><span class="lab">OPEN BETS</span><b id="sb-open">—</b></div>
<div class="cell"><span class="lab">RESOLVED P&L</span><b id="sb-pnl">—</b></div>
<div class="cell"><span class="lab">WIN RATE</span><b id="sb-wr">—</b></div>
<div class="cell"><span class="lab">AI CONF GATE</span><b id="sb-conf">—</b></div>
<div class="cell" style="margin-left:auto;border-right:none"><span class="lab">NEXT MARKET</span><b id="sb-next">—</b></div>
</div>
<script>
const $=i=>document.getElementById(i);
setInterval(()=>{$('clock').textContent=new Date().toLocaleTimeString()+' local';},1000);
function spark(pts){const c=$('spark'),x=c.getContext('2d'),dpr=devicePixelRatio||1,r=c.getBoundingClientRect();
c.width=r.width*dpr;c.height=r.height*dpr;x.scale(dpr,dpr);const W=r.width,H=r.height;if(pts.length<2)return;
const mn=Math.min(...pts),mx=Math.max(...pts),rg=(mx-mn)||1;
x.strokeStyle='#00ff41';x.lineWidth=1.4;x.shadowColor='#00ff41';x.shadowBlur=6;x.beginPath();
pts.forEach((p,i)=>{const px=i/(pts.length-1)*W,py=H-((p-mn)/rg)*(H-8)-4;i?x.lineTo(px,py):x.moveTo(px,py)});x.stroke();}
async function refresh(){
const d=await (await fetch('/api/state')).json();
$('btc').textContent='$'+Number(d.btc||0).toLocaleString(undefined,{maximumFractionDigits:0});
$('btc-mom').textContent='momentum: '+d.momentum+(d.price_age!=null?' · feed '+d.price_age+'s ago':'');
spark(d.spark||[]);
$('m-ticker').textContent=d.market.ticker||'';
$('m-q').textContent=d.market.title||'waiting for next 15m market…';
$('m-close').textContent=d.market.mins_left!=null?d.market.mins_left.toFixed(1)+'m':'';
$('m-yes').textContent='YES '+(d.market.yes_ask??'')+'¢';$('m-no').textContent='NO '+(d.market.no_ask??'')+'¢';
$('acct-bal').textContent=d.balance!=null?'$'+(d.balance/100).toFixed(2):'';
$('acct-pos').textContent=d.positions;$('acct-bets').textContent=d.bets_session;
let b='';b+=d.cfg.dry_run?'<span class="badge warn">DRY_RUN</span>':'<span class="badge danger">LIVE</span>';
b+='<span class="badge">'+d.cfg.mode+'</span>';
if(d.cfg.kill)b+='<span class="badge danger">KILLED</span>';
$('badges').innerHTML=b;
$('c-mode').value=d.cfg.mode;$('c-stake').value=d.cfg.stake_cents;$('stake-v').textContent=d.cfg.stake_cents+'¢';
$('c-dry').textContent=d.cfg.dry_run?'⚠ DRY RUN ON — click to go LIVE':'● LIVE — click to pause to DRY';
$('dec').innerHTML=d.decisions.map(e=>`<div class="e"><span class="small">${e.t}</span>
<span class="${e.final==='UP'?'up':e.final==='DOWN'?'dn':'skip'}">[${e.final}]</span>
${e.ticker.slice(-12)} · mom:${e.momentum} llm:${e.llm_vote}(${(e.llm_conf||0).toFixed(2)})
<div class="why">${e.llm_why||''} → ${e.reason}</div></div>`).join('')||'<div class="e">waiting for signals…</div>';
$('ord').innerHTML=d.orders.map(o=>`<div class="e"><span class="small">${o.t}</span>
<b class="${o.side==='yes'?'up':'dn'}">${o.side.toUpperCase()}</b> ${o.count}x @ ${o.price_cents}¢ ${o.dry?'<span class="skip">[SIM]</span>':''}
<div class="why">${o.ticker}</div></div>`).join('')||'<div class="e">no orders yet</div>';
$('ev').innerHTML=d.events.map(e=>`<div class="e"><span class="small">${e.t}</span> <span class="${e.level==='error'?'dn':e.level==='warning'?'warn-t':''}">${e.msg}</span></div>`).join('');
/* status bar */
const st=$('sb-state');
st.className='cell '+(d.cfg.dry_run?'sim':'live');
st.textContent=d.cfg.dry_run?'◐ SIMULATED':'● LIVE';
$('sb-mode').textContent=d.cfg.mode;
$('sb-btc').textContent='$'+Number(d.btc||0).toLocaleString(undefined,{maximumFractionDigits:0});
const c24=d.intel&&d.intel.chg24;
$('sb-24h').textContent=c24!=null?(c24>=0?'+':'')+c24.toFixed(2)+'%':'';
$('sb-24h').className=c24>=0?'up':'dn';
$('sb-fng').textContent=d.intel&&d.intel.fng!=null?d.intel.fng+' '+(d.intel.fng_class||''):'';
$('sb-open').textContent=d.perf.open;
const pnl=d.perf.pnl||0;
$('sb-pnl').textContent=(pnl>=0?'+':'')+pnl+'¢';
$('sb-pnl').className=pnl>=0?'up':'dn';
$('sb-wr').textContent=d.perf.resolved?((d.perf.won/d.perf.resolved*100).toFixed(0)+'% ('+d.perf.won+'/'+d.perf.resolved+')'):'';
$('sb-conf').textContent=d.perf.min_conf!=null?(d.perf.min_conf*100).toFixed(0)+'%':'';
$('sb-next').textContent=d.market.mins_left!=null?d.market.mins_left.toFixed(1)+'m':'';
}
async function ctl(){await fetch('/api/control',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({mode:$('c-mode').value,stake_cents:+$('c-stake').value})});refresh();}
async function toggleDry(){await fetch('/api/toggle-dry',{method:'POST'});refresh();}
async function kill(){if(confirm('HALT THE BOT? (relaunch manually to revive)')){await fetch('/api/kill',{method:'POST'});refresh();}}
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}
body::before{content:'';position:fixed;inset:0;pointer-events:none;z-index:99;background:repeating-linear-gradient(0deg,transparent 0 2px,rgba(0,0,0,.22) 2px 4px)}
.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}
.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}
.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: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}
#statusbar .cell b{color:#00ff41}
</style></head><body>
<header><h1 class="glow">K4LSH1<span class="dim">_FLEET</span></h1><span id="tot"></span></header>
<div class="fleet" id="cards"></div>
<div id="statusbar"></div>
<script>
const C={DOGE:"#c2a633",SOL:"#9945FF",ETH:"#627EEA"};
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'}
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;
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):'-';
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="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=>`
<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>`;
}
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>"""
def cfg():
return json.loads(CFG_PATH.read_text())
@app.route("/")
def index():
return HTML
@app.route("/api/state")
def state():
c = cfg()
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
now = time.time()
ticks = [r[0] for r in conn.execute("SELECT price FROM ticks WHERE ts > ? ORDER BY ts", (now-1800,)).fetchall()]
last_price = ticks[-1] if ticks else None
age = None
row = conn.execute("SELECT MAX(ts) FROM ticks").fetchone()
if row and row[0]: age = int(now - row[0])
dec = [{"t": time.strftime("%H:%M:%S", time.localtime(r["ts"])), "ticker": r["ticker"],
"momentum": r["momentum"], "llm_vote": r["llm_vote"], "llm_conf": r["llm_conf"],
"llm_why": r["llm_why"], "final": r["final"], "reason": r["reason"]}
for r in conn.execute("SELECT * FROM decisions ORDER BY id DESC LIMIT 30")]
orders = [{"t": time.strftime("%H:%M:%S", time.localtime(r["ts"])), "ticker": r["ticker"],
"side": r["side"], "count": r["count"], "price_cents": r["price_cents"], "dry": r["dry"]}
for r in conn.execute("SELECT * FROM orders ORDER BY id DESC LIMIT 20")]
events = [{"t": time.strftime("%H:%M:%S", time.localtime(r[0])), "level": r[1], "msg": r[2]}
for r in conn.execute("SELECT * FROM events ORDER BY ts DESC LIMIT 12")]
bets = conn.execute("SELECT COUNT(*) FROM orders").fetchone()[0]
perf_rows = conn.execute("SELECT status, COALESCE(pnl_cents,0) FROM orders WHERE status IN ('won','lost')").fetchall()
won = sum(1 for s,p in perf_rows if s == 'won')
pnl = sum(p for s,p in perf_rows)
open_bets = conn.execute("SELECT COUNT(*) FROM orders WHERE status IN ('placed','posted')").fetchone()[0]
kv = {r[0]: r[1] for r in conn.execute("SELECT k, v FROM kv")}
conn.close()
# live market + balance (proxy through bot's client without importing heavy code)
market, balance, positions = {}, None, 0
try:
import sys
sys.path.insert(0, str(HOME))
from bot import Kalshi, btc_price, DEFAULT_CFG
kx = Kalshi(c["api_key_id"], c["key_path"])
mkts = kx.markets(c.get("series", "KXBTC15M"), "open", 2)
if mkts:
m = sorted(mkts, key=lambda x: x["close_time"])[-1]
from datetime import datetime
close_ts = datetime.fromisoformat(m["close_time"].replace("Z","+00:00")).timestamp()
book = kx.orderbook(m["ticker"])
market = {"ticker": m["ticker"], "title": m.get("title",""),
"mins_left": (close_ts-time.time())/60,
"yes_ask": (book.get("yes") or [[None]])[0][0],
"no_ask": (book.get("no") or [[None]])[0][0]}
b = kx.balance()
balance = b.get("balance", 0) # available cash (cents)
positions = len(kx.positions())
except Exception as e:
market = {"error": str(e)[:80]}
if last_price is None:
try:
from bot import btc_price
last_price = btc_price()
except Exception: pass
try:
from bot import intel as bot_intel
ix = bot_intel()
except Exception:
ix = {}
# momentum label
mom = "FLAT"
if len(ticks) > 10:
r = (ticks[-1]-ticks[0])/ticks[0]*100
mom = f"{'UP' if r>0 else 'DOWN'} {abs(r):.3f}%/30m"
return jsonify({"cfg": c, "btc": last_price, "price_age": age, "spark": ticks[-120:],
"momentum": mom, "market": market, "balance": balance,
"positions": positions, "bets_session": bets,
"decisions": dec, "orders": orders, "events": events,
"intel": ix,
"perf": {"open": open_bets, "resolved": len(perf_rows), "won": won,
"pnl": pnl, "min_conf": kv.get("min_conf", c.get("min_conf", 0.55))}})
@app.route("/api/control", methods=["POST"])
def control():
c = cfg()
d = request.get_json(force=True)
if d.get("mode") in ("AUTO","DOWN_SPAM","UP_SPAM","OFF"): c["mode"] = d["mode"]
if isinstance(d.get("stake_cents"), int): c["stake_cents"] = max(1, min(99, d["stake_cents"]))
c["kill"] = False
CFG_PATH.write_text(json.dumps(c, indent=1))
return jsonify({"ok": True})
@app.route("/api/toggle-dry", methods=["POST"])
def toggle_dry():
c = cfg(); c["dry_run"] = not c["dry_run"]; c["kill"] = False
CFG_PATH.write_text(json.dumps(c, indent=1))
return jsonify({"ok": True, "dry_run": c["dry_run"]})
@app.route("/api/fleet")
def fleet_api():
"""All active coins in one call — reads each coin's own DB."""
COINS = {"🐶DOGE": "#c2a633", "🔮SOL": "#9945FF", "💎ETH": "#627EEA"}
result = {}
for label, color in COINS.items():
coin = label[1:] # strip leading emoji
fpath = HOME / f"{coin.lower()}.json"
if not fpath.exists(): continue
c = json.loads(fpath.read_text())
if c.get("mode","OFF") == "OFF": continue
db_path = HOME / c.get("db_path", f"{coin.lower()}.db")
if not db_path.exists(): continue
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
now = time.time()
ticks = [r[0] for r in conn.execute("SELECT price FROM ticks WHERE ts > ? ORDER BY ts", (now-1800,)).fetchall()]
last_price = ticks[-1] if ticks else None
# enriched per-coin data
decs = [dict(r) for r in conn.execute("SELECT * FROM decisions ORDER BY id DESC LIMIT 3")]
ords = [dict(r) for r in conn.execute("SELECT * FROM orders ORDER BY id DESC LIMIT 5")]
live_bets = conn.execute("SELECT COUNT(*) FROM orders WHERE status IN ('placed','posted') AND dry=0").fetchone()[0]
total_bets = conn.execute("SELECT COUNT(*) FROM orders WHERE dry=0").fetchone()[0]
pnl = conn.execute("SELECT COALESCE(SUM(pnl_cents),0) FROM orders WHERE dry=0").fetchone()[0]
won = conn.execute("SELECT COUNT(*) FROM orders WHERE status='won' AND dry=0").fetchone()[0]
import sys; sys.path.insert(0, str(HOME))
from bot import rsi as _rsi, momentum as _mom
c_rsi = _rsi(conn); c_mom, c_score = _mom(conn)
chg24 = c.get("chg24_cache", 0)
result[coin] = {"label": label, "color": color, "series": c["series"],
"price": last_price, "mode": c["mode"],
"live_bets": live_bets, "total_bets": total_bets,
"pnl": pnl, "won": won, "stake": c.get("stake_cents", 10),
"swing": c.get("swing_threshold_pct", 0.005),
"rsi": c_rsi, "mom": f"{c_mom} {c_score:.3f}%" if c_mom != "FLAT" else c_mom,
"chg24": chg24, "decisions": decs, "orders": ords}
conn.close()
return jsonify(result)
@app.route("/fleet")
def fleet_page():
return FLEET_HTML
def kill():
c = cfg(); c["kill"] = True
CFG_PATH.write_text(json.dumps(c, indent=1))
return jsonify({"ok": True})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5053, debug=False)

BIN
doge.db Normal file

Binary file not shown.

28
doge.json Normal file
View File

@@ -0,0 +1,28 @@
{
"api_key_id": "28d5876b-2ece-4aa3-aa17-ec96e1e706eb",
"key_path": "/Users/drjones/kalshi-bot/kalshi_private_key.pem",
"mode": "AUTO",
"dry_run": false,
"stake_cents": 10,
"max_contracts": 1,
"daily_loss_cap_cents": 500,
"min_conf": 0.5,
"ollama_url": "http://10.30.20.186:11434",
"ollama_model": "ornith:latest",
"swing_threshold_pct": 0.005,
"learning": true,
"kill": false,
"coin": "DOGE",
"series": "KXDOGE15M",
"start_delay": 0,
"db_path": "doge.db",
"post_price_cents": 50,
"proxy": "http://10.30.20.154:3128",
"max_spend_cents": 100,
"decision_interval_sec": 300,
"start_delay_sec": 0,
"fast_llm_url": "http://localhost:11434",
"fast_llm_model": "qwen3.5:4b",
"deep_llm_url": "http://10.30.20.186:11434",
"deep_llm_model": "ornith:latest"
}

0
doge.lock Normal file
View File

BIN
eth.db Normal file

Binary file not shown.

28
eth.json Normal file
View File

@@ -0,0 +1,28 @@
{
"api_key_id": "28d5876b-2ece-4aa3-aa17-ec96e1e706eb",
"key_path": "/Users/drjones/kalshi-bot/kalshi_private_key.pem",
"mode": "AUTO",
"dry_run": false,
"stake_cents": 10,
"max_contracts": 1,
"daily_loss_cap_cents": 500,
"min_conf": 0.5,
"ollama_url": "http://10.30.20.186:11434",
"ollama_model": "ornith:latest",
"swing_threshold_pct": 0.005,
"learning": true,
"kill": false,
"coin": "ETH",
"series": "KXETH15M",
"start_delay": 14,
"db_path": "eth.db",
"post_price_cents": 50,
"proxy": "http://10.30.20.189:3128",
"max_spend_cents": 100,
"decision_interval_sec": 300,
"start_delay_sec": 40,
"fast_llm_url": "http://localhost:11434",
"fast_llm_model": "qwen3.5:4b",
"deep_llm_url": "http://10.30.20.186:11434",
"deep_llm_model": "ornith:latest"
}

0
eth.lock Normal file
View File

27
kalshi_private_key.pem Normal file
View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAsiT6Qipbe9ntO7PScWfRdcSiZAuVie6RHJiO033kuinl34pj
L2kjnUSdUDez5DsVGgVfRH34lT8wnhLW/PhPScKJd1lmV5evxsPv1PZroNyOiS0j
vy5pFKgWG/hXmGoiQysufgsNOhANouxIoI7ubyKUVesUq6CEHDwTPNuC5gPliUnw
gXyB/1j6I21ghsIjifICm8UBYv1J5W3gQ1GYD5vF0Nc47Ugn3tq4mDFjvgIzulxF
HtcCdewVPH+BG3lNoTAlLK+WS3JXzrxq0rKYFIIoKyCjY4Aan2bUPfu7e74ODkX4
EQCHKOVTgzyk3HfKkwgVmVGyavlh2zSrxlKUaQIDAQABAoIBACv6xAvcsCwx34cL
PGuB8WytKFghof/k9U9FfJFK9dJd8KHQ5O65+rBjVaENt2wjh/CeHehnXxzddk+4
H/phBRYVIGHmIMWELcGY8tc1JNoYSVolWjtjJk8Sti6ej2WR4jaJ7Rbx/Ih2mR89
TSrSTpwCQfO6Lya14Jel3yv5l2XYa/dlWDaiHNfbeymXEYVDTI4NEKMnzByZnour
jCsEhPIS3YEa0yLtS6FIhNftTya0q8AbghDtkXN1Pwm89XKLzRpkLZuE0v79mojB
miAifetImm6bZOwxr2C4vCmUvroIp0sCS40VTn7GHyCi6XYohGm/rk9difhVURzC
Q+AhpxkCgYEA0eaQXICA9qycos9es92rvvYgojumF9wFP5Hl+qNZv4g/KppoZGRq
7iICubJhoBSGZvHiZ34Lf8jH9v7I5JHaznqZmh60VCMm3GKUDtSWzkZcBI/FmlrM
QTrfkmiMQabWNXVw9RMgUEqnc8vHYu18Xaul1nOtXP7tuVvHxD4S7+UCgYEA2UT0
toVdPZZ3dyuENcfnlb2CHFfawrFKWLzsSQtiUugGOubPFya35DdCjuQhyW/gcIiY
t4NCmbCSSkqHk4C7GfR3QxtMFFinm7HWdsjE4eYIXZvZTmP3B8UXWru7uDkmo+/q
7MVcMrirWezr7r7j10In6ocSQVBN3kb2/DS4ojUCgYBwUX08UnMwDEvA/1BA1UR6
6f+qcFq/d7fDPZIf3VtAF2sdpARqfCWQCdkFf0OoQnNtENBC6vrCRTFjpRDg5n98
b7RT+UwrnafE1Gc/zFhqRtIkir6Fi0KzQQqzsIgk9V64uoBYVAS9kkUqvIeUE1qy
URBrwfipw6x3vHoYM9M0VQKBgQDU/vzIUkXuMqw7b7XU43ztFzOOhbI4W4X/KHab
xUVG8Z/p7ovmrlZorKPbYVqeF8d3ivHsNgn8kVVY72ep+vFxqG+ioggbii4GPHl7
Sad5rgFVg4utheKPA3C5yoFlQOHYpUMxiumv78pMBi/lhc3nITvwdyo2TXuJBRjs
duLqYQKBgDZ/a+mzI3SrA6iFYU8DuI0e5IgW92B4NHuav31ffyD5ah0gGTZ+o+H4
Sp7xuVbg4Qr6DVY27Ky8kPL3eYDFPDPqpGFD9aNTzRxYdxqHi0jEph/MGiel4fKn
AzPZ6T8pf7rAxVhbcdChe9mvLWrb7Q1SOkVA9aXg9QuNW7zUd7Ip
-----END RSA PRIVATE KEY-----

9
launch_all.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/bash
# K4LSH1_OPS multi-coin launcher
cd /Users/drjones/kalshi-bot
for coin in doge sol eth; do
echo "=== launching bot: $coin ==="
nohup .venv/bin/python bot.py --config /Users/drjones/kalshi-bot/$coin.json --lock $coin > /dev/null 2>&1 &
sleep 1
done
echo "all 3 bots launched"

BIN
sol.db Normal file

Binary file not shown.

28
sol.json Normal file
View File

@@ -0,0 +1,28 @@
{
"api_key_id": "28d5876b-2ece-4aa3-aa17-ec96e1e706eb",
"key_path": "/Users/drjones/kalshi-bot/kalshi_private_key.pem",
"mode": "AUTO",
"dry_run": false,
"stake_cents": 10,
"max_contracts": 1,
"daily_loss_cap_cents": 500,
"min_conf": 0.5,
"ollama_url": "http://10.30.20.186:11434",
"ollama_model": "ornith:latest",
"swing_threshold_pct": 0.005,
"learning": true,
"kill": false,
"coin": "SOL",
"series": "KXSOL15M",
"start_delay": 7,
"db_path": "sol.db",
"post_price_cents": 50,
"proxy": "http://10.30.20.71:3128",
"max_spend_cents": 100,
"decision_interval_sec": 300,
"start_delay_sec": 20,
"fast_llm_url": "http://localhost:11434",
"fast_llm_model": "qwen3.5:4b",
"deep_llm_url": "http://10.30.20.186:11434",
"deep_llm_model": "ornith:latest"
}

0
sol.lock Normal file
View File

BIN
state.db Normal file

Binary file not shown.

279
test_bot.py Normal file
View File

@@ -0,0 +1,279 @@
#!/usr/bin/env python3
"""K4LSH1_OPS unit tests — verify every decision-making component."""
import json, sqlite3, sys, time, tempfile
from pathlib import Path
from unittest.mock import patch, MagicMock
sys.path.insert(0, str(Path(__file__).parent))
from bot import (momentum, decide, rsi, hedge_positions, llm_vote,
Kalshi, load_cfg, learned_min_conf, KRAKEN_PAIRS,
btc_price, intel)
PASS = 0
FAIL = 0
def check(name, condition, detail=""):
global PASS, FAIL
if condition:
PASS += 1
print(f"{name}")
else:
FAIL += 1
print(f"{name}{detail}")
def test_momentum():
print("\n═══ MOMENTUM ═══")
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE ticks (ts REAL, price REAL)")
now = time.time()
# flat market
for i in range(20):
conn.execute("INSERT INTO ticks VALUES (?, 100.0)", (now - 300 + i*15,))
mom, score = momentum(conn)
check("flat market → FLAT", mom == "FLAT", f"got {mom}/{score:.4f}")
check("flat score near 0", score < 0.01, f"score={score:.4f}")
# up market
conn.execute("DELETE FROM ticks")
for i in range(20):
p = 100.0 + i * 0.5
conn.execute("INSERT INTO ticks VALUES (?,?)", (now - 300 + i*15, p))
mom, score = momentum(conn)
check("rising market → UP", mom == "UP", f"got {mom}/{score:.3f}%")
check("up score positive", score > 0.05, f"score={score:.3f}%")
# down market
conn.execute("DELETE FROM ticks")
for i in range(20):
p = 200.0 - i * 0.3
conn.execute("INSERT INTO ticks VALUES (?,?)", (now - 300 + i*15, p))
mom, score = momentum(conn)
check("falling market → DOWN", mom == "DOWN", f"got {mom}/{score:.3f}%")
check("down score positive", score > 0.01, f"score={score:.3f}%")
# too few ticks
conn.execute("DELETE FROM ticks")
conn.execute("INSERT INTO ticks VALUES (?, 100)", (now,))
mom, score = momentum(conn)
check("1 tick → FLAT", mom == "FLAT", f"got {mom}")
check("1 tick score 0", score == 0.0, f"score={score}")
conn.close()
def test_rsi():
print("\n═══ RSI ═══")
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE ticks (ts REAL, price REAL)")
now = time.time()
# oversold: steady decline
for i in range(30):
p = 100.0 - i * 0.5
conn.execute("INSERT INTO ticks VALUES (?,?)", (now - 1800 + i*60, p))
val = rsi(conn)
check("decline → RSI < 30", val is not None and val < 30, f"RSI={val}")
# overbought: steady climb
conn.execute("DELETE FROM ticks")
for i in range(30):
p = 100.0 + i * 0.8
conn.execute("INSERT INTO ticks VALUES (?,?)", (now - 1800 + i*60, p))
val = rsi(conn)
check("climb → RSI > 70", val is not None and val > 70, f"RSI={val}")
conn.close()
def test_learned_min_conf():
print("\n═══ ADAPTIVE CONFIDENCE ═══")
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE orders (id INTEGER PRIMARY KEY, pnl_cents INT, status TEXT)")
conn.execute("CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v REAL)")
cfg = {"learning": True, "min_conf": 0.55}
# < 8 resolved bets → use default
for i in range(5):
conn.execute("INSERT INTO orders(pnl_cents,status) VALUES (?,?)", (10, "won"))
lc = learned_min_conf(conn, cfg)
check("<8 bets → default", lc == 0.55, f"got {lc}")
# majority wins → relax confidence
for i in range(15):
conn.execute("INSERT INTO orders(pnl_cents,status) VALUES (?,?)", (15 if i < 10 else -10, "won" if i < 10 else "lost"))
lc = learned_min_conf(conn, cfg)
check("high WR → relax", lc < 0.55, f"got {lc}")
# majority losses → tighten (need fresh conn, kv starts empty)
conn.execute("DELETE FROM orders")
conn.execute("DELETE FROM kv")
for i in range(15):
conn.execute("INSERT INTO orders(pnl_cents,status) VALUES (?,?)",
(-10 if i < 10 else 15, "lost" if i < 10 else "won"))
lc = learned_min_conf(conn, {"learning": True, "min_conf": 0.55})
check("low WR → tighten", lc > 0.55, f"got {lc}")
conn.close()
def test_hedge():
print("\n═══ HEDGE LOGIC ═══")
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE orders (id INTEGER PRIMARY KEY, ts REAL, ticker TEXT, "
"side TEXT, count INT, price_cents INT, dry INT, order_id TEXT, status TEXT, "
"settle_ts REAL, pnl_cents INT)")
conn.execute("CREATE TABLE events (ts REAL, level TEXT, msg TEXT)")
cfg = {"dry_run": True, "min_profit_cents": 8}
ticker = "KXBTC15M-TEST"
book = {"yes": [[42]], "no": [[48]]}
# scenario: we bought NO at 45¢, YES ask is 42¢ → profit = 100-45-42=13¢ > 8¢ min
conn.execute("INSERT INTO orders VALUES (1,0,?,?,?,?,0,'sim','posted',NULL,NULL)",
(ticker, "no", 1, 45))
result = hedge_positions(conn, MagicMock(), ticker, cfg, book)
check("NO@45 YES@42 → hedge fires", result == "hedged", f"got {result}")
hedged = conn.execute("SELECT status FROM orders WHERE id=1").fetchone()[0]
check("original marked hedged", hedged == "hedged", f"got {hedged}")
new_orders = conn.execute("SELECT COUNT(*) FROM orders WHERE side='yes'").fetchone()[0]
check("counter-order placed", new_orders == 1)
# scenario: profit too small
conn.execute("DELETE FROM orders")
conn.execute("INSERT INTO orders VALUES (2,0,?,?,?,?,0,'sim','posted',NULL,NULL)",
(ticker, "no", 1, 48))
book2 = {"yes": [[49]], "no": [[50]]}
result2 = hedge_positions(conn, MagicMock(), ticker, cfg, book2)
check("NO@48 YES@49 → no hedge (1¢ profit)", result2 is None)
conn.close()
def test_decision_logic():
print("\n═══ DECISION LOGIC ═══")
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE ticks (ts REAL, price REAL)")
conn.execute("CREATE TABLE decisions (id INTEGER PRIMARY KEY, ts REAL, ticker TEXT, "
"mode TEXT, momentum TEXT, llm_vote TEXT, llm_conf REAL, llm_why TEXT, "
"final TEXT, price REAL, reason TEXT)")
conn.execute("CREATE TABLE orders (id INTEGER PRIMARY KEY, pnl_cents INT, status TEXT)")
conn.execute("CREATE TABLE events (ts REAL, level TEXT, msg TEXT)")
conn.execute("CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v REAL)")
now = time.time()
for i in range(20):
p = 100.0 + i * 0.3
conn.execute("INSERT INTO ticks VALUES (?,?)", (now - 300 + i*15, p))
cfg = {"swing_threshold_pct": 0.005, "min_conf": 0.50, "learning": False,
"mode": "AUTO", "coin": "BTC", "ollama_url": "http://x", "ollama_model": "x"}
# LLM confirms: UP spike, LLM votes DOWN (fade confirmed)
with patch("bot.intel", return_value={"chg24": 0.85, "fng": 45, "fee_fast": 8}), \
patch("bot.llm_vote", return_value=("DOWN", 0.72, "strong fade signal")):
mom, lv, lc, lw, final, why = decide(cfg, conn, 63400)
check("LLM fade confirm → DOWN", final == "DOWN", f"got {final}")
check("confidence preserved", lc == 0.72, f"got {lc}")
# LLM passes (not enough signal)
with patch("bot.intel", return_value={"chg24": 0.1}), \
patch("bot.llm_vote", return_value=("SKIP", 0.30, "no clear signal")):
mom, lv, lc, lw, final, why = decide(cfg, conn, 63400)
check("LLM skip → SKIP", final == "SKIP", f"got {final}")
# LLM goes against fade but high conf — should follow LLM
with patch("bot.intel", return_value={"chg24": 0.5}), \
patch("bot.llm_vote", return_value=("UP", 0.82, "continuation strong")):
mom, lv, lc, lw, final, why = decide(cfg, conn, 63400)
check("LLM continuation with high conf → trust LLM", final == "UP", f"got {final}/{why}")
# Low confidence → skip
with patch("bot.intel", return_value={"chg24": 0.3}), \
patch("bot.llm_vote", return_value=("DOWN", 0.35, "weak")):
mom, lv, lc, lw, final, why = decide(cfg, conn, 63400)
check("low conf → SKIP", final == "SKIP", f"got {final}")
conn.close()
def test_price_sources():
print("\n═══ PRICE SOURCES ═══")
for coin, pair in [("BTC","XBTUSD"), ("DOGE","XDGUSD"), ("SOL","SOLUSD"), ("ETH","ETHUSD")]:
p = btc_price(coin)
check(f"{coin} price from Kraken", p is not None and p > 0, f"${p}" if p else "None")
# coinbase fallback
with patch("bot.requests.get") as mock_get:
mock_kraken = MagicMock()
mock_kraken.json.side_effect = Exception("kraken down")
mock_cb = MagicMock()
mock_cb.json.return_value = {"data": {"amount": "99999"}}
mock_get.side_effect = [mock_kraken, mock_cb]
p = btc_price("BTC")
check("coinbase fallback works", p == 99999.0, f"got {p}")
def test_llm_vote_parsing():
print("\n═══ LLM VOTE PARSING ═══")
cfg = {"ollama_url": "http://x", "ollama_model": "x", "coin": "BTC"}
# ornith says DOWN with confidence
mock_resp = MagicMock()
mock_resp.text = json.dumps({"response": "DOWN|0.72|RSI oversold, fading up spike"})
with patch("bot.requests.post", return_value=mock_resp):
with patch("bot.intel", return_value={"chg24": 0.5}):
with patch("bot.rsi", return_value=28):
lv, lc, lw = llm_vote(cfg, 63400, "UP", 0.12, sqlite3.connect(":memory:"))
check("parses DOWN", lv == "DOWN", f"got {lv}")
check("parses confidence", lc == 0.72, f"got {lc}")
check("parses why", len(lw) > 5, f"'{lw}'")
# ornith says SKIP
mock_resp.text = json.dumps({"response": "SKIP|0.40|no clear direction"})
with patch("bot.requests.post", return_value=mock_resp):
with patch("bot.intel", return_value={"chg24": 0.1}):
with patch("bot.rsi", return_value=55):
lv, lc, lw = llm_vote(cfg, 63400, "FLAT", 0.001, sqlite3.connect(":memory:"))
check("parses SKIP", lv == "SKIP", f"got {lv}")
# malformed response
mock_resp.text = json.dumps({"response": "garbage noise no pipe"})
with patch("bot.requests.post", return_value=mock_resp):
with patch("bot.intel", return_value={}):
with patch("bot.rsi", return_value=None):
lv, lc, lw = llm_vote(cfg, 63400, "UP", 0.10, sqlite3.connect(":memory:"))
check("malformed → SKIP", lv == "SKIP", f"got {lv}")
check("malformed → 0 conf", lc == 0.0, f"got {lc}")
def test_config():
print("\n═══ CONFIG ═══")
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
json.dump({"mode": "AUTO", "stake_cents": 15, "coin": "TEST"}, f)
fpath = f.name
try:
from bot import HOME, CFG_PATH, load_cfg
old = str(CFG_PATH)
import bot
bot.CFG_PATH = Path(fpath)
c = load_cfg()
check("loads mode", c["mode"] == "AUTO")
check("loads stake", c["stake_cents"] == 15)
bot.CFG_PATH = Path(old)
finally:
Path(fpath).unlink(missing_ok=True)
def test_kraken_pairs():
print("\n═══ KRAKEN PAIRS ═══")
check("BTC → XBTUSD", KRAKEN_PAIRS["BTC"] == "XBTUSD")
check("DOGE → XDGUSD", KRAKEN_PAIRS["DOGE"] == "XDGUSD")
check("SOL → SOLUSD", KRAKEN_PAIRS["SOL"] == "SOLUSD")
check("ETH → ETHUSD", KRAKEN_PAIRS["ETH"] == "ETHUSD")
check("XRP → XRPUSD", KRAKEN_PAIRS["XRP"] == "XRPUSD")
if __name__ == "__main__":
print("K4LSH1_OPS UNIT TESTS")
print("=" * 50)
tests = [test_momentum, test_rsi, test_learned_min_conf, test_hedge,
test_decision_logic, test_price_sources, test_llm_vote_parsing,
test_config, test_kraken_pairs]
for t in tests:
try:
t()
except Exception as e:
print(f" ✗ CRASH: {e}")
FAIL += 1
print(f"\n{'='*50}")
print(f"PASS: {PASS} FAIL: {FAIL}")
sys.exit(0 if FAIL == 0 else 1)