K4LSH1_OPS: multi-coin LLM bot — DOGE/SOL/ETH, two-tier qwen+ornith, self-adapting
This commit is contained in:
BIN
__pycache__/bot.cpython-311.pyc
Normal file
BIN
__pycache__/bot.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/bot.cpython-314.pyc
Normal file
BIN
__pycache__/bot.cpython-314.pyc
Normal file
Binary file not shown.
BIN
__pycache__/dashboard.cpython-311.pyc
Normal file
BIN
__pycache__/dashboard.cpython-311.pyc
Normal file
Binary file not shown.
617
bot.py
Normal file
617
bot.py
Normal 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
211
bot_console.log
Normal 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
16
config.json
Normal 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
75
dash_console.log
Normal file
@@ -0,0 +1,75 @@
|
||||
* Serving Flask app 'dashboard'
|
||||
* Debug mode: off
|
||||
[31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* Running on all addresses (0.0.0.0)
|
||||
* Running on http://127.0.0.1:5053
|
||||
* Running on http://10.5.0.2:5053
|
||||
[33mPress CTRL+C to quit[0m
|
||||
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] "[33mGET /favicon.ico HTTP/1.1[0m" 404 -
|
||||
2026-08-01 21:37:51,475 127.0.0.1 - - [01/Aug/2026 21:37:51] "[33mGET /favicon.ico HTTP/1.1[0m" 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
384
dashboard.py
Normal 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)
|
||||
28
doge.json
Normal file
28
doge.json
Normal 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"
|
||||
}
|
||||
28
eth.json
Normal file
28
eth.json
Normal 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"
|
||||
}
|
||||
27
kalshi_private_key.pem
Normal file
27
kalshi_private_key.pem
Normal 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
9
launch_all.sh
Executable 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"
|
||||
28
sol.json
Normal file
28
sol.json
Normal 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"
|
||||
}
|
||||
279
test_bot.py
Normal file
279
test_bot.py
Normal 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)
|
||||
Reference in New Issue
Block a user