#!/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-mlx", "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}%)") # Pattern matching: find similar historical states in Chroma try: state_text = f"{mom} {mom_score:.3f}% RSI1m:{r1 or '?'} RSI5m:{r5 or '?'} {coin} {btc_chg:+.1f}% 24h vol:{ix.get('vol24',0):.0f}" embed = requests.post(cfg.get("embed_url","http://10.30.20.186:11434")+"/api/embed", json={"model": cfg.get("embed_model","nomic-embed-text-v2-moe:latest"), "input": state_text}, timeout=8).json() vec = embed.get("embeddings",[[]])[0] if vec: chroma_r = requests.post("http://10.30.20.89:27124/api/v1/collections/kalshi-patterns/query", json={"query_embeddings": [vec], "n_results": 5, "include": ["metadatas"]}, timeout=8) if chroma_r.status_code == 200: results = chroma_r.json() if results.get("metadatas") and results["metadatas"][0]: outcomes = [m.get("outcome","?") for m in results["metadatas"][0] if m] wins = outcomes.count("won"); losses = outcomes.count("lost") if wins + losses > 0: lines.append(f"PATTERN: 5 nearest historical market states → {wins}W/{losses}L") except Exception: pass # silently skip if Chroma/embed not available # 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-mlx") r1 = requests.post(fast_url+"/api/generate", json={"model": fast_model, "prompt": prompt, "stream": False, "think": False, "keep_alive": "10m", "options": {"temperature": 0.3, "num_predict": 60}}, timeout=8, proxies={"http": None, "https": None}) # bypass proxy for localhost 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]}" # ── 2-model: ornith leads when confident, qwen vetoes borderline ── NO_PROXY = {"proxies": {"http": None, "https": None}} # Model 1: qwen3.5 fast gate (MacBook — already ran) v1 = fast_v if fast_v in ("UP","DOWN","SKIP") else "SKIP" c1 = fast_c if fast_v in ("UP","DOWN","SKIP") else 0.50 # Fast-gate skip: if qwen is confident enough, skip ornith entirely fast_gate_conf = cfg.get("fast_gate_conf", 0.65) if v1 in ("UP","DOWN") and c1 >= fast_gate_conf: LOG.info(f"[fast-gate {v1} {c1:.2f}] skipping ornith — qwen confident") return v1, c1, f"[fast-gate {v1} {c1:.2f}] {fast_w[:80]}" # Model 2: ornith deep verify (GamingPC RTX 3070) — only on borderline/uncertain try: r2 = requests.post("http://10.30.20.186:11434/api/generate", json={"model": "ornith:latest", "prompt": prompt, "stream": False, "think": False, "keep_alive": "10m", "options": {"temperature": 0.2, "num_predict": 90}}, timeout=60, **NO_PROXY) txt2 = r2.json().get("response","") s2, e2 = txt2.find("{"), txt2.rfind("}")+1 d2 = json.loads(txt2[s2:e2]) if s2 >= 0 else {} v2 = str(d2.get("vote","SKIP")).upper() if v2 not in ("UP","DOWN","SKIP"): v2 = "SKIP" c2 = float(d2.get("conf",0)) w2 = str(d2.get("why",""))[:120] except Exception as ex2: LOG.warning(f"ornith failed: {ex2}") return v1, c1, f"[qwen-only: {v1} {c1:.2f}] {fast_w[:80]}" # ornith confident (>0.55) → lead, regardless of qwen if v2 in ("UP","DOWN") and c2 >= 0.55: return v2, c2, f"[ornith-lead {v2} {c2:.2f} | qwen={v1}] {w2[:60]}" # both agree → go if v1 == v2 and v1 in ("UP","DOWN"): avg = (c1 + c2) / 2 return v1, avg, f"[agree {v1} {avg:.2f}] {w2[:60]}" # qwen confident + ornith uncertain → use qwen if v1 in ("UP","DOWN") and c1 >= 0.65 and c2 < 0.55: return v1, c1, f"[qwen-lead {v1} {c1:.2f} | ornith={v2}] {fast_w[:60]}" # disagreement or both uncertain → skip return "SKIP", 0.0, f"[split qwen={v1}({c1:.2f}) ornith={v2}({c2:.2f})] {w2[:50]}" 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: only on TRUE extremes + trend alignment ── r = rsi(conn) ix_pre = intel(cfg.get("coin","BTC")) chg24 = ix_pre.get("chg24", 0) if r is not None: # Fade DOWN only when: extreme OB + momentum reversing + not deep downtrend if r > 92 and mom == "DOWN" and mom_score > 0.01: return mom, lv, lc, lw, "DOWN", f"⚠ RSI {r:.0f} extreme OB + momentum reversing → fade DOWN" # Fade UP only when: extreme OS + momentum reversing + NOT in deep downtrend (don't catch falling knives) if r < 8 and mom == "UP" and mom_score > 0.01 and chg24 > -0.5: return mom, lv, lc, lw, "UP", f"⚠ RSI {r:.0f} extreme OS + momentum reversing → fade UP" # RSI extreme in downtrend → SKIP, don't catch the knife if r < 8 and chg24 <= -0.5: return mom, lv, lc, lw, "SKIP", f"⚠ RSI {r:.0f} extreme OS but 24h {chg24:.1f}% downtrend — no knife catch" # ── 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 (both directions) ── 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}") mk = m.get("market", m) # Kalshi nests under "market" key status = mk.get("status") if status not in ("settled", "finalized", "determined", "closed"): ct = mk.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 = (mk.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}¢)") # Store pattern in Chroma for future matching try: from bot import rsi as _rsi, momentum as _mom c_rsi = _rsi(conn); c_mom, c_score = _mom(conn) state_text = f"{c_mom} {c_score:.3f}% RSI:{c_rsi or '?'} ticker:{ticker[:6]}" embed = requests.post("http://10.30.20.186:11434/api/embed", json={"model": "nomic-embed-text-v2-moe:latest", "input": state_text}, timeout=8).json() vec = embed.get("embeddings",[[]])[0] if vec: requests.post("http://10.30.20.89:27124/api/v1/collections/kalshi-patterns/add", json={"embeddings": [vec], "metadatas": [{"outcome": "won" if won else "lost", "coin": ticker[:4], "pnl": pnl, "side": side, "ticker": ticker, "ts": time.time()}], "ids": [f"{ticker}-{oid}"], "documents": [state_text]}, timeout=8) except Exception: pass except Exception: continue # ───────── exposure cap: total open ≤ 25% of balance ───────── _exposure_cache = {"ts": 0, "balance": 0, "open_cents": 0} def check_exposure_cap(kx, cfg, new_cost_cents): """Block trades if total open exposure would exceed 25% of balance.""" global _exposure_cache now = time.time() if now - _exposure_cache["ts"] < 60: # cache for 60s bal = _exposure_cache["balance"]; open_c = _exposure_cache["open_cents"] else: try: b = kx.balance() bal = float(b.get("balance_dollars", 0)) * 100 # cents # Sum open positions from Kalshi positions = kx.positions() open_c = sum(int(float(p.get("exposure_dollars", 0)) * 100) for p in positions) _exposure_cache.update({"ts": now, "balance": bal, "open_cents": open_c}) except Exception: bal = 2500; open_c = 0 # fallback: assume $25, no open cap = bal * 0.25 projected = open_c + new_cost_cents allowed = projected <= cap if not allowed: LOG.info(f"exposure cap: open={open_c}¢ + new={new_cost_cents}¢ = {projected}¢ > 25% of {bal:.0f}¢ ({cap:.0f}¢)") return allowed, open_c, bal # ───────── 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", 5) # scalp: lock smaller profits more often 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 # ── WR-based stake rebalancing (always runs, not dependent on LLM) ── coin_name = cfg.get("coin","?") max_spend = cfg.get("max_spend_cents", 100) wins_rb = conn.execute("SELECT COUNT(*) FROM orders WHERE status='won' AND dry=0").fetchone()[0] total_rb = conn.execute("SELECT COUNT(*) FROM orders WHERE status IN ('won','lost') AND dry=0").fetchone()[0] if total_rb >= 5: wr_rb = wins_rb / total_rb new_stake = max_spend if wr_rb >= 0.60: new_stake = min(250, max_spend + 25) elif wr_rb < 0.35: new_stake = max(25, max_spend - 25) if new_stake != max_spend: conn.execute("INSERT OR REPLACE INTO kv(k,v) VALUES ('stake_rebalance',?)", (json.dumps({"from": max_spend, "to": new_stake, "wr": round(wr_rb,2), "total": total_rb}),)) conn.commit() LOG.info(f"⚖ {coin_name} WR={wr_rb:.0%} → stake ${max_spend/100:.2f}→${new_stake/100:.2f} (bets:{total_rb})") _last_adapt_ts = time.time() # throttle rebalance logs to 10min 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-mlx"), "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-mlx"), "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) # WR-based stake rebalance (from adapt_controls) — override config try: reb = conn.execute("SELECT v FROM kv WHERE k='stake_rebalance'").fetchone() if reb: cfg["max_spend_cents"] = json.loads(reb[0]).get("to", cfg.get("max_spend_cents", 100)) except Exception: pass # 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}") mk = m.get("market", m) # Kalshi nests under "market" ct = mk.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: now_ts = time.time() # ── ALWAYS watching: hedge check EVERY tick (every 15s) ── book = kx.orderbook(ticker) if hedge_positions(conn, kx, ticker, cfg, book) == "hedged": continue # ── micro-swing detector: price moved 0.03%+ in last 60s ── swing_60s = False recent = conn.execute( "SELECT price FROM ticks WHERE ts > ? ORDER BY ts DESC LIMIT 5", (now_ts - 60,)).fetchall() if len(recent) >= 4: hi = max(t[0] for t in recent); lo = min(t[0] for t in recent) if lo > 0 and (hi - lo) / lo * 100 >= 0.03: swing_60s = True # ── 5-min re-evaluation window (skip unless micro-swing) ── cycle_sec = cfg.get("decision_interval_sec", 300) if last_decision_ts and (now_ts - last_decision_ts) < cycle_sec and not swing_60s: continue last_decision_ts = now_ts if swing_60s and last_decision_ts: LOG.info(f"⚡ micro-swing 0.03%+ in 60s on {ticker} — evaluating entry") 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 # skip-if-positioned: one open position per ticker max existing = conn.execute( "SELECT COUNT(*) FROM orders WHERE ticker=? AND status IN ('placed','posted') AND dry=0", (ticker,)).fetchone()[0] if existing > 0: continue # already positioned on this market try: verify = kx.req("GET", f"/markets/{ticker}") vm = verify.get("market", verify) # Kalshi nests under "market" st = vm.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_spend cap + 25% exposure cap 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 # 25% total exposure cap trade_cost = n * px ok, open_c, bal = check_exposure_cap(kx, cfg, trade_cost) if not ok: log_event(conn, "info", f"skip {ticker}: exposure cap ({open_c}¢+{trade_cost}¢ > 25% of {bal:.0f}¢)") continue 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(cfg.get("scalp_interval_sec", 60)) # configurable scalp loop if __name__ == "__main__": run()