Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions 8-application-demos/6-kalshi-bet-predictor/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.DS_Store
.env
.venv/
.vscode/
23 changes: 23 additions & 0 deletions 8-application-demos/6-kalshi-bet-predictor/cerebrium.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[cerebrium.deployment]
name = "kalshi-bet-predictor"
python_version = "3.11"
docker_base_image_url = "debian:bookworm-slim"
disable_auth = true
include = ['./*', 'main.py', 'cerebrium.toml']
exclude = ['.*']

[cerebrium.dependencies.paths]
pip = "requirements.txt"

[cerebrium.hardware]
cpu = 4
memory = 36
compute = "ADA_L40"

[cerebrium.scaling]
min_replicas = 1
max_replicas = 2
cooldown = 30
replica_concurrency = 1
scaling_metric = "concurrency_utilization"

170 changes: 170 additions & 0 deletions 8-application-demos/6-kalshi-bet-predictor/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from typing import Tuple
import requests
import re
from dotenv import load_dotenv
import os
from exa_py import Exa

def getKalshiQuestion(market_ticker)->Tuple[str,str]:
url = f"https://api.elections.kalshi.com/trade-api/v2/markets/{market_ticker}"
try:
res = requests.get(url)
res.raise_for_status()
obj = res.json()
return obj['market']['rules_primary']
except requests.exceptions.RequestException as e:
raise RuntimeError(f"Error fetching Kalshi market data: {e}")

def getKalshiOdds(market_ticker)->Tuple[str, str]:
url = f"https://api.elections.kalshi.com/trade-api/v2/markets/{market_ticker}"
try:
res = requests.get(url)
res.raise_for_status()
obj = res.json()
return obj['market']['yes_ask'], obj['market']['no_ask']
except requests.exceptions.RequestException as e:
raise RuntimeError(f"Error fetching Kalshi market data: {e}")


class BetPredictor:
def __init__(self, model_name: str = "Qwen/Qwen3-4B-Instruct-2507"):

self.tokenizer = AutoTokenizer.from_pretrained(model_name)
self.model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype = torch.bfloat16,
device_map="auto"
)

load_dotenv()

self.exa = Exa(os.environ.get("EXA_API_KEY"))

print(f"Loaded model {model_name}!")

def _generate_response(self, prompt: str, max_new_tokens: int) -> str:
inputs = self.tokenizer(prompt, return_tensors="pt").to(self.model.device)
input_ids_len = inputs['input_ids'].shape[-1]

output_sequences = self.model.generate(
**inputs,
max_new_tokens=max_new_tokens,
pad_token_id=self.tokenizer.eos_token_id,
do_sample=False,
)

newly_generated_ids = output_sequences[0, input_ids_len:]

response = self.tokenizer.decode(newly_generated_ids, skip_special_tokens=True).strip()

print(f"Generated this response! {response}")
return response

def convert_rules_to_question(self, rules:str) -> str:
prompt = (
"You will receive a sentence that is a statement of the following type:"
"If <conditional>, then the market resolves to Yes"
"Convert the conditional to a yes/no question"
"Your response SHOULD ONLY BE a SINGLE line consisting of the yes/no question:\n"
"Do not add ANY preamble, conclusion, or extra text.\n\n"
f"STATEMENT: {rules}\n"
)

raw_response = self._generate_response(prompt, max_new_tokens=400)

return raw_response

def get_relevant_questions(self, question: str) -> list[str]:

prompt = (
"Based on the following question, generate a list of 5 relevant questions "
"that one could search online to gather more information. "
"These questions should yield information that would be helpful to answering "
"the following question in an objective manner.\n\n"
"Your response SHOULD ONLY BE the following lines, in this exact format:\n"
"1. <question 1>\n"
"2. <question 2>\n"
"3. <question 3>\n"
"4. <question 4>\n"
"5. <question 5>\n"
"Do not add ANY preamble, conclusion, or extra text.\n\n"
f"Question: \"{question}\"\n"
)

raw_response = self._generate_response(prompt, max_new_tokens=400)

relevant_questions = []
for line in raw_response.split('\n'):
line = line.strip()
if line and line[0].isdigit():
clean_question = line.split('.', 1)[-1].strip()
relevant_questions.append(clean_question)

return relevant_questions


def get_information(self, questions):
results = [self.exa.answer(q, text=True) for q in questions]
answers = [r.answer for r in results]
return answers

def get_binary_answer_with_percentage(self, information: str, question: str) -> Tuple[str, str, str]:
prompt = (
"Analyze the provided information below to answer the given binary question. "
"Based on the information, determine the probability that the answer is 'Yes' or 'No'.\n\n"
"--- Information ---\n"
f"{information}\n\n"
"--- Question ---\n"
f"{question}\n\n"
"IMPORTANT INSTRUCTIONS:\n"
"1. Your response MUST ONLY be a single line in THIS EXACT FORMAT:\n"
" Yes: <YES PERCENTAGE>%, No: <NO PERCENTAGE>%, Explanation: <EXPLANATION>\n"
"2. Percentages must sum to 100%.\n"
"3. Do NOT include any preamble, summary, or additional text.\n"
"4. Provide a brief but clear explanation supporting your probabilities.\n\n"
"AGAIN, Your response MUST ONLY be a single line in THIS EXACT FORMAT: Yes: <YES PERCENTAGE>%, No: <NO PERCENTAGE>%, Explanation: <EXPLANATION>"
)

response = self._generate_response(prompt, max_new_tokens=800)

match = re.search(r"Yes: (.*?), No: (.*?), Explanation: (.*)", response, re.DOTALL)

if match:
yes, no, explanation = match.groups()
return yes.strip(), no.strip(), explanation.strip()
else:
raise ValueError(f"Failed to parse LLM response: {response}")

def predict(self, question):
relevant_questions = self.get_relevant_questions(question)
answers = self.get_information(relevant_questions)

information = ""
for i, v in enumerate(relevant_questions):
information += f"INFORMATION {i+1}: \n"
information += f"QUESTION {i+1}: {v}\n"
information += f"ANSWER {i+1}: {answers[i]} \n\n"

yes, no, explanation = self.get_binary_answer_with_percentage(information, question)
return yes, no, explanation


predictor = BetPredictor()

def predict(ticker: str):
rules = getKalshiQuestion(ticker)
question = predictor.convert_rules_to_question(rules)

predYes, predNo, explanation = predictor.predict(question)

realYes, realNo = getKalshiOdds(ticker)

if realYes < predYes: # undervalued
buyYes = True
if realNo < predNo: # undervalued
buyNo = True

return {"buy_yes":buyYes, "buy_no": buyNo, "yes": predYes, "no": predNo, "explanation": explanation}

44 changes: 44 additions & 0 deletions 8-application-demos/6-kalshi-bet-predictor/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
accelerate==1.10.1
annotated-types==0.7.0
anyio==4.11.0
certifi==2025.10.5
charset-normalizer==3.4.4
distro==1.9.0
exa-py==1.16.1
filelock==3.20.0
fsspec==2025.9.0
greenlet==3.2.4
h11==0.16.0
hf-xet==1.1.10
httpcore==1.0.9
httpx==0.28.1
huggingface-hub==0.35.3
idna==3.11
Jinja2==3.1.6
jiter==0.11.1
MarkupSafe==3.0.3
mpmath==1.3.0
networkx==3.5
numpy==2.3.4
openai==2.5.0
packaging==25.0
playwright==1.55.0
psutil==7.1.0
pydantic==2.12.3
pydantic_core==2.41.4
pyee==13.0.0
PyPDF2==3.0.1
python-dotenv==1.1.1
PyYAML==6.0.3
regex==2025.9.18
requests==2.32.5
safetensors==0.6.2
sniffio==1.3.1
sympy==1.14.0
tokenizers==0.22.1
torch==2.9.0
tqdm==4.67.1
transformers==4.57.1
typing-inspection==0.4.2
typing_extensions==4.15.0
urllib3==2.5.0