#!/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) def cancel_order(self, order_id, dry=False): """Cancel an open order by ID.""" if dry: return {"simulated": True, "cancelled": order_id} return self.req("DELETE", f"/portfolio/events/orders/{order_id}") def amend_order(self, order_id, new_price_cents, dry=False): """Amend an existing order's price.""" body = {"price": f"{new_price_cents/100:.4f}"} if dry: return {"simulated": True, "amended": order_id} return self.req("POST", f"/portfolio/events/orders/{order_id}/amend", 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") # Learned patterns from past bets learnings = conn.execute("SELECT v FROM kv WHERE k='learnings'").fetchone() if learnings: try: for l in json.loads(learnings[0])[-5:]: lines.append(f"LEARNED: {l}") except Exception: pass 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) def llm_vote(cfg, price, mom, mom_score, conn): """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() # extract learnings from resolved bets lp = ( f"Recent trades: {json.dumps([{'side':r[0],'pnl':r[1],'price':r[2]} for r in resolved[:10]])}. " "Extract 1-2 actionable trading patterns. Reply with ONLY JSON array of strings, e.g.: " '["RSI < 20 UP bets won 3/4 times","DOGE fades after 0.1% spike lose 60%"]' ) try: r3 = requests.post(fast_url+"/api/generate", json={"model": cfg.get("fast_llm_model","qwen3.5:4b"), "prompt": lp, "stream": False, "think": False, "options": {"temperature": 0.1, "num_predict": 60}}, timeout=10) txt3 = r3.json().get("response","") s3, e3 = txt3.find("["), txt3.rfind("]")+1 new_learnings = json.loads(txt3[s3:e3]) if s3 >= 0 else [] if new_learnings: existing = conn.execute("SELECT v FROM kv WHERE k='learnings'").fetchone() old = json.loads(existing[0]) if existing else [] old.extend(new_learnings) conn.execute("INSERT OR REPLACE INTO kv(k,v) VALUES ('learnings',?)", (json.dumps(old[-20:]),)) # keep last 20 conn.commit() LOG.info(f"🧠 learned: {new_learnings}") except Exception: pass 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) # cancel stale unfilled orders close to market expiration try: open_orders = conn.execute( "SELECT id, ticker, order_id, side, price_cents FROM orders WHERE status IN ('placed','posted') AND dry=0" ).fetchall() for oid, ticker, koid, side, px in open_orders: try: m = kx.req("GET", f"/markets/{ticker}") ct = m.get("close_time","") if ct: close_ts = datetime.fromisoformat(ct.replace("Z","+00:00")).timestamp() mins_left = (close_ts - time.time())/60 if mins_left < 1.5: kx.cancel_order(koid, cfg.get("dry_run", False)) conn.execute("UPDATE orders SET status='cancelled' WHERE id=?", (oid,)) conn.commit() log_event(conn, "info", f"CANCEL stale {ticker} {side}@{px}¢ (mins_left={mins_left:.1f})") except Exception: pass except Exception: pass 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()