changes to bot.py certificate store

This commit is contained in:
2026-02-09 19:56:34 -05:00
parent 3083ff471d
commit dd292741c1
4 changed files with 110 additions and 145 deletions

249
bot.py
View File

@@ -1,11 +1,10 @@
from __future__ import annotations from __future__ import annotations
import datetime as dt import datetime as dt
from http import client
import json import json
import time import time
import uuid import uuid
from typing import Dict, List, Optional, Tuple from typing import Dict, Tuple
import yaml import yaml
@@ -16,14 +15,43 @@ from spot_feed import SpotFeed
from storage import Storage from storage import Storage
# ---------- helpers ----------
def _parse_iso_z(s: str) -> dt.datetime: def _parse_iso_z(s: str) -> dt.datetime:
# Example: "2023-11-07T05:31:56Z" """Parse ISO timestamps ending in Z."""
return dt.datetime.fromisoformat(s.replace("Z", "+00:00")) return dt.datetime.fromisoformat(s.replace("Z", "+00:00"))
def discover_open_crypto_15m_markets(client: KalshiClient, symbols: list[str]) -> dict[str, dict]:
def dollars_to_cents_price(p: float) -> int:
"""Convert $0.00$1.00 price to 199 cents."""
return max(1, min(99, int(round(p * 100))))
def extract_strike_and_rule(market: dict) -> Tuple[float, bool]:
""" """
Discover open 15-minute crypto markets by scanning open markets directly. Returns (strike, resolves_yes_if_spot_ge_strike).
Returns mapping: symbol -> market_dict (nearest close) """
strike_type = market.get("strike_type")
if strike_type == "greater":
return float(market["floor_strike"]), True
if strike_type == "less":
return float(market["cap_strike"]), False
if market.get("floor_strike") is not None:
return float(market["floor_strike"]), True
if market.get("cap_strike") is not None:
return float(market["cap_strike"]), False
raise RuntimeError(f"Unable to determine strike from market: {json.dumps(market)[:400]}")
def discover_open_crypto_15m_markets(
client: KalshiClient,
symbols: list[str],
) -> dict[str, dict]:
"""
Discover nearest-closing open 15-minute crypto markets by scanning open markets directly.
""" """
data = client.get_markets(series_ticker=None, status="open", limit=500) data = client.get_markets(series_ticker=None, status="open", limit=500)
markets = data.get("markets", []) markets = data.get("markets", [])
@@ -33,6 +61,7 @@ def discover_open_crypto_15m_markets(client: KalshiClient, symbols: list[str]) -
for m in markets: for m in markets:
title = (m.get("title") or "").upper() title = (m.get("title") or "").upper()
if "UP OR DOWN" not in title: if "UP OR DOWN" not in title:
continue continue
if "15" not in title: if "15" not in title:
@@ -53,99 +82,54 @@ def discover_open_crypto_15m_markets(client: KalshiClient, symbols: list[str]) -
return per_symbol return per_symbol
def choose_current_market(client: KalshiClient, series_ticker: str) -> Optional[dict]: # ---------- main bot ----------
"""
From open markets in a series, pick the one with the nearest close_time in the future.
"""
data = client.get_markets(series_ticker=series_ticker, status="open", limit=200)
markets = data.get("markets", [])
now = dt.datetime.now(dt.timezone.utc)
best = None
best_close = None
for m in markets:
close_time = _parse_iso_z(m["close_time"])
if close_time <= now:
continue
if best_close is None or close_time < best_close:
best = m
best_close = close_time
return best
def extract_strike_and_rule(market: dict) -> Tuple[float, bool]:
"""
Returns (strike, resolves_yes_if_spot_ge_strike).
For many threshold markets, Kalshi provides strike_type + floor_strike/cap_strike.
"""
strike_type = market.get("strike_type") # e.g., "greater" or "less"
if strike_type == "greater":
strike = float(market.get("floor_strike"))
return strike, True # YES if spot >= strike
if strike_type == "less":
strike = float(market.get("cap_strike"))
return strike, False # YES if spot <= strike
# Fallback: if missing, try floor_strike then cap_strike
if market.get("floor_strike") is not None:
return float(market["floor_strike"]), True
if market.get("cap_strike") is not None:
return float(market["cap_strike"]), False
raise RuntimeError(f"Could not determine strike from market fields: {json.dumps(market)[:500]}")
def dollars_to_cents_price(p: float) -> int:
# p is dollars 0.00..1.00; convert to cents 1..99
c = int(round(p * 100))
return max(1, min(99, c))
def main() -> None: def main() -> None:
with open("config.yaml", "r") as f: with open("config.yaml", "r") as f:
cfg = yaml.safe_load(f) cfg = yaml.safe_load(f)
mode = cfg["mode"] mode = cfg["mode"] # "paper" or "live"
symbols = cfg["symbols"]
client = KalshiClient.from_env() client = KalshiClient.from_env()
spot = SpotFeed(
if len(series_map) == 0: urls=cfg["coinbase"],
raise RuntimeError("Could not discover any matching 15-min crypto series. Check title/category filters.") lookback_seconds=int(cfg["vol_lookback_seconds"]),
)
spot = SpotFeed(urls=cfg["coinbase"], lookback_seconds=int(cfg["vol_lookback_seconds"]))
storage = Storage("storage.sqlite") storage = Storage("storage.sqlite")
risk = RiskManager(cfg["daily_loss_limit"], cfg["max_consecutive_losses"]) risk = RiskManager(
cfg["daily_loss_limit"],
cfg["max_consecutive_losses"],
)
last_traded_market: Dict[str, str] = {} # symbol -> market_ticker last_traded_market: Dict[str, str] = {}
print(f"[init] mode={mode} series_map={series_map}") print(f"[init] mode={mode} symbols={symbols}")
while True: while True:
if risk.trading_halted(): if risk.trading_halted():
print("[risk] Trading halted by risk manager. Sleeping 60s.") print("[risk] Trading halted — sleeping 60s")
time.sleep(60) time.sleep(60)
continue continue
# refresh spot history # update spot feed
try: try:
spot.update() spot.update()
except Exception as e: except Exception as e:
print(f"[spot] Error fetching spot: {e}") print(f"[spot] update failed: {e}")
time.sleep(5) time.sleep(5)
continue continue
now = dt.datetime.now(dt.timezone.utc) now = dt.datetime.now(dt.timezone.utc)
symbols = cfg["symbols"]
markets_by_symbol = discover_open_crypto_15m_markets(client, symbols) markets_by_symbol = discover_open_crypto_15m_markets(client, symbols)
for sym, m in markets_by_symbol.items(): for sym, m in markets_by_symbol.items():
try:
market_ticker = m["ticker"] market_ticker = m["ticker"]
close_time = _parse_iso_z(m["close_time"]) close_time = _parse_iso_z(m["close_time"])
# Only fire in a tight window starting at (close_time - lead_seconds) # Trade window: T6 minutes for a short window
lead = dt.timedelta(seconds=int(cfg["lead_seconds"])) lead = dt.timedelta(seconds=int(cfg["lead_seconds"]))
window = dt.timedelta(seconds=int(cfg["trade_window_seconds"])) window = dt.timedelta(seconds=int(cfg["trade_window_seconds"]))
start = close_time - lead start = close_time - lead
@@ -154,61 +138,37 @@ def main() -> None:
if not (start <= now <= end): if not (start <= now <= end):
continue continue
# prevent duplicate trade on same market
if last_traded_market.get(sym) == market_ticker: if last_traded_market.get(sym) == market_ticker:
continue continue
# Pull full market details (strike fields more reliable)
market_full = client.get_market(market_ticker)["market"] market_full = client.get_market(market_ticker)["market"]
strike, yes_if_ge = extract_strike_and_rule(market_full) strike, yes_if_ge = extract_strike_and_rule(market_full)
# Market best prices (dollars strings included)
yes_bid = float(market_full.get("yes_bid_dollars") or 0.0) yes_bid = float(market_full.get("yes_bid_dollars") or 0.0)
yes_ask = float(market_full.get("yes_ask_dollars") or 1.0) yes_ask = float(market_full.get("yes_ask_dollars") or 1.0)
spread = yes_ask - yes_bid spread = yes_ask - yes_bid
market_prob = yes_ask
market_prob = yes_ask # to buy YES, you pay the ask
# Guardrails: spread + prob bounds
if spread > float(cfg["max_spread_dollars"]): if spread > float(cfg["max_spread_dollars"]):
reason = f"skip: spread {spread:.4f} > max"
storage.log_decision(
ts=time.time(),
symbol=sym, series_ticker=series_ticker, market_ticker=market_ticker,
close_time=m["close_time"], strike=strike, side="yes",
market_prob=market_prob, fair_prob=0.0, edge=0.0,
spread=spread, jump=0.0, reason=reason
)
continue continue
if not (float(cfg["min_market_prob"]) <= market_prob <= float(cfg["max_market_prob"])): if not (cfg["min_market_prob"] <= market_prob <= cfg["max_market_prob"]):
reason = f"skip: market_prob {market_prob:.4f} outside bounds"
storage.log_decision(
ts=time.time(),
symbol=sym, series_ticker=series_ticker, market_ticker=market_ticker,
close_time=m["close_time"], strike=strike, side="yes",
market_prob=market_prob, fair_prob=0.0, edge=0.0,
spread=spread, jump=0.0, reason=reason
)
continue continue
# Jump filter (tail risk) jump = abs(
jump = abs(spot.returns_over_window(sym, int(cfg["jump_lookback_seconds"]))) spot.returns_over_window(sym, int(cfg["jump_lookback_seconds"]))
if jump > float(cfg["max_abs_jump"]):
reason = f"skip: jump {jump:.5f} > max"
storage.log_decision(
ts=time.time(),
symbol=sym, series_ticker=series_ticker, market_ticker=market_ticker,
close_time=m["close_time"], strike=strike, side="yes",
market_prob=market_prob, fair_prob=0.0, edge=0.0,
spread=spread, jump=jump, reason=reason
) )
if jump > cfg["max_abs_jump"]:
continue continue
# Fair probability
spot_px = spot.latest(sym) spot_px = spot.latest(sym)
sigma = spot.realized_vol(sym) sigma = spot.realized_vol(sym)
time_remaining = max(1.0, (close_time - now).total_seconds())
# IMPORTANT: settlement is the AVERAGE of the final 60 seconds
time_remaining = max(
60.0,
(close_time - now).total_seconds(),
)
fair = fair_prob_threshold( fair = fair_prob_threshold(
spot=spot_px, spot=spot_px,
@@ -219,54 +179,55 @@ def main() -> None:
) )
edge = fair - market_prob edge = fair - market_prob
if edge < float(cfg["min_edge"]): if edge < cfg["min_edge"]:
reason = f"skip: edge {edge:.4f} < min"
storage.log_decision(
ts=time.time(),
symbol=sym, series_ticker=series_ticker, market_ticker=market_ticker,
close_time=m["close_time"], strike=strike, side="yes",
market_prob=market_prob, fair_prob=fair, edge=edge,
spread=spread, jump=jump, reason=reason
)
continue continue
# Determine limit price (cents)
improve = int(cfg.get("limit_price_improve_cents", 0))
yes_ask_cents = dollars_to_cents_price(yes_ask) yes_ask_cents = dollars_to_cents_price(yes_ask)
improve = int(cfg.get("limit_price_improve_cents", 0))
limit_cents = max(1, yes_ask_cents - improve) limit_cents = max(1, yes_ask_cents - improve)
# Size by max_cost max_cost_cents = int(round(cfg["max_cost_dollars"] * 100))
max_cost_cents = int(round(float(cfg["max_cost_dollars"]) * 100)) count = max_cost_cents // limit_cents
# worst-case cost ≈ count * price_cents
count = max(0, max_cost_cents // limit_cents)
if count <= 0: if count <= 0:
reason = "skip: count computed as 0"
storage.log_decision(
ts=time.time(),
symbol=sym, series_ticker=series_ticker, market_ticker=market_ticker,
close_time=m["close_time"], strike=strike, side="yes",
market_prob=market_prob, fair_prob=fair, edge=edge,
spread=spread, jump=jump, reason=reason
)
continue continue
# Log decision
storage.log_decision( storage.log_decision(
ts=time.time(), ts=time.time(),
symbol=sym, series_ticker=series_ticker, market_ticker=market_ticker, symbol=sym,
close_time=m["close_time"], strike=strike, side="yes", series_ticker="",
market_prob=market_prob, fair_prob=fair, edge=edge, market_ticker=market_ticker,
spread=spread, jump=jump, reason="trade" close_time=m["close_time"],
strike=strike,
side="yes",
market_prob=market_prob,
fair_prob=fair,
edge=edge,
spread=spread,
jump=jump,
reason="trade",
) )
client_order_id = f"{sym}-{uuid.uuid4().hex[:12]}" client_order_id = f"{sym}-{uuid.uuid4().hex[:10]}"
if mode == "paper": if mode == "paper":
print(f"[PAPER] {sym} trade {market_ticker} count={count} limit={limit_cents}c max_cost={max_cost_cents}c edge={edge:.3f}") print(
storage.log_order(market_ticker, order_id=None, mode="paper", status="simulated", f"[PAPER] {sym} {market_ticker} "
details=f"count={count} yes_price={limit_cents} buy_max_cost={max_cost_cents} edge={edge:.4f}") f"count={count} limit={limit_cents}c "
f"edge={edge:.3f}"
)
storage.log_order(
market_ticker,
order_id=None,
mode="paper",
status="simulated",
details=f"count={count} limit={limit_cents} edge={edge:.4f}",
)
else: else:
print(f"[LIVE] {sym} placing order {market_ticker} count={count} limit={limit_cents}c max_cost={max_cost_cents}c edge={edge:.3f}") print(
f"[LIVE] {sym} {market_ticker} "
f"count={count} limit={limit_cents}c "
f"edge={edge:.3f}"
)
resp = client.create_order( resp = client.create_order(
ticker=market_ticker, ticker=market_ticker,
side="yes", side="yes",
@@ -278,14 +239,18 @@ def main() -> None:
client_order_id=client_order_id, client_order_id=client_order_id,
) )
order = resp.get("order", {}) order = resp.get("order", {})
order_id = order.get("order_id") storage.log_order(
status = order.get("status", "unknown") market_ticker,
storage.log_order(market_ticker, order_id=order_id, mode="live", status=status, details=json.dumps(order)[:2000]) order_id=order.get("order_id"),
mode="live",
status=order.get("status", "unknown"),
details=json.dumps(order)[:1500],
)
last_traded_market[sym] = market_ticker last_traded_market[sym] = market_ticker
except Exception as e: except Exception as e:
print(f"[loop] Error for {sym}: {e}") print(f"[loop] error for {sym}: {e}")
time.sleep(2) time.sleep(2)

View File

@@ -4,6 +4,7 @@ import time
from collections import deque from collections import deque
from dataclasses import dataclass from dataclasses import dataclass
from typing import Deque, Dict, Tuple from typing import Deque, Dict, Tuple
import certifi
import requests import requests
@@ -24,10 +25,9 @@ class SpotFeed:
self.history: Dict[str, Deque[SpotPoint]] = {sym: deque() for sym in urls.keys()} self.history: Dict[str, Deque[SpotPoint]] = {sym: deque() for sym in urls.keys()}
def _fetch(self, url: str) -> float: def _fetch(self, url: str) -> float:
r = requests.get(url, timeout=10) r = requests.get(url, timeout=10, verify=certifi.where())
r.raise_for_status() r.raise_for_status()
data = r.json() data = r.json()
# Coinbase shape: {"data": {"amount": "70428.82", "currency": "USD"}}
return float(data["data"]["amount"]) return float(data["data"]["amount"])
def update(self) -> Dict[str, float]: def update(self) -> Dict[str, float]:

BIN
storage.sqlite-shm Normal file

Binary file not shown.

0
storage.sqlite-wal Normal file
View File