Arbitrage betting is a financial strategy that takes advantage of the variation in odds offered by different bookmakers to make a profit regardless of the outcome of an event. By placing bets on all possible outcomes of a sporting event at odds that guarantee a total return greater than the total stake, risk is effectively eliminated.
"Daily Arbies" is an automated system designed to identify and capitalize on these opportunities. The system scans odds from multiple bookmakers, calculates potential arbitrage situations, and automatically broadcasts the best opportunity of the day to X (formerly Twitter).
It leverages the Odds API for real-time data, OpenAI's GPT-4o for generating engaging and human-like social media posts, and GitHub Actions for scheduled, serverless execution. This project demonstrates proficiency in Python scripting, API integration, cloud automation, and AI application.
Python API Integration OpenAI / LLMs GitHub Actions Automation Data Processing
Jarrad McKay
November 2025
Requests Tweepy OpenAI JSON CI/CD
The core of the application is a Python script that performs a multi-step process:
1. Data Acquisition: The script queries the Odds API to fetch live odds for a configurable
list of sports (NBA, NFL, MLB, etc.) and markets (Head-to-Head, Spreads, Totals). It implements robust error
handling and API key rotation to ensure reliability.
2. Arbitrage Calculation: It iterates through the fetched odds to find discrepancies
between bookmakers. By calculating the implied probabilities, it identifies "arbs" – scenarios where the sum
of the inverse odds is less than 1, guaranteeing a profit. It also calculates optimal stake sizing and ROI.
3. AI Content Generation: Once the best arbitrage opportunity is identified, the script
uses OpenAI's GPT-4o model to craft a witty, engaging, and concise tweet. The prompt ensures the tweet
includes all necessary details (teams, odds, ROI) while adhering to Twitter's character limits and using
relevant hashtags.
4. Automated Posting: Finally, the script uses the Tweepy library to authenticate with the
X API and post the generated tweet. The entire process is orchestrated by GitHub Actions, running on a daily
schedule without manual intervention.
import os
import time
import math
import requests
import re
from typing import Dict, List, Tuple, Optional, Any
from datetime import datetime, timezone
import json
import sys
# Note: requests, tweepy, and openai must be installed via requirements.txt in the GitHub Action environment.
import tweepy
from openai import OpenAI
# =============================================================================
# ========= Configuration (Read from Environment Variables) =========
# =============================================================================
# Load all potential keys from environment variables
ODDS_API_KEY_PRIMARY = os.environ.get("ODDS_API_KEY", "")
ODDS_API_KEYS_CSV = os.environ.get("ODDS_API_KEYS_CSV", "")
# Create a master list of keys to cycle through (primary first, then backups)
ALL_ODDS_API_KEYS = [ODDS_API_KEY_PRIMARY]
if ODDS_API_KEYS_CSV:
ALL_ODDS_API_KEYS.extend([k.strip() for k in ODDS_API_KEYS_CSV.split(',') if k.strip()])
# Clean up: Remove empty strings and duplicates.
ALL_ODDS_API_KEYS = list(dict.fromkeys([k for k in ALL_ODDS_API_KEYS if k]))
# Global variable to track the currently active key's index for cycling across calls
current_api_key_index = 0
# OpenAI & X Credentials
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
X_CONSUMER_KEY = os.environ.get("X_CONSUMER_KEY", "")
X_CONSUMER_SECRET = os.environ.get("X_CONSUMER_SECRET", "")
X_ACCESS_TOKEN = os.environ.get("X_ACCESS_TOKEN", "")
X_ACCESS_TOKEN_SECRET = os.environ.get("X_ACCESS_TOKEN_SECRET", "")
# General Config (Can be overridden by environment variables in the workflow file)
API_BASE = "https://api.the-odds-api.com/v4"
REGIONS = os.environ.get("REGIONS", "us")
ODDS_FORMAT = os.environ.get("ODDS_FORMAT", "decimal")
SPORTS = os.environ.get("SPORTS", "basketball_nba,baseball_mlb,americanfootball_nfl,icehockey_nhl,soccer_epl,americanfootball_ncaaf,boxing_boxing,soccer_brazil_serie_b,soccer_usa_mls").split(',')
MARKETS = os.environ.get("MARKETS", "h2h,spreads,totals").split(',')
MARKETS = [m.strip() for m in MARKETS if m.strip()]
# Arbitrage Thresholds
BANKROLL = float(os.environ.get("BANKROLL", 100))
MIN_EDGE_BPS = float(os.environ.get("MIN_EDGE_BPS", 25))
MIN_EDGE_BPS_POST_ROUND = float(os.environ.get("MIN_EDGE_BPS_POST_ROUND", 10))
MIN_START_MINUTES = int(os.environ.get("MIN_START_MINUTES", 20))
# Bookmaker Filters
BOOKIES_WHITELIST: List[str] = [b.strip() for b in os.environ.get("BOOKIES_WHITELIST", "").split(',') if b.strip()]
STAKE_INCREMENT = { "_default": 0.01 }
MIN_STAKE = { "_default": 1.00 }
HTTP_TIMEOUT = 15
SLEEP_BETWEEN_CALLS = 0.10
session = requests.Session()
session.headers.update({"User-Agent": "arb-lines-github-action/1.0"})
# =============================================================================
# ========= Core HTTP & Helper Functions (with Key Failover) =========
# =============================================================================
def _get_json(url: str, params: Dict[str, Any], api_key_override: str) -> Any:
"""Makes an authenticated GET request to the Odds API and returns JSON. Raises HTTPError on failure."""
if not api_key_override:
raise ValueError("API Key is empty.")
params["apiKey"] = api_key_override
r = session.get(url, params=params, timeout=HTTP_TIMEOUT)
r.raise_for_status()
return r.json()
def get_events_with_odds(sport_key: str, market: str) -> List[dict]:
"""Fetches odds, cycling through API keys on failure."""
global current_api_key_index
if not ALL_ODDS_API_KEYS:
print("[FATAL] No valid ODDS_API_KEYs configured.")
return []
key_list = ALL_ODDS_API_KEYS
num_keys = len(key_list)
start_index = current_api_key_index
for i in range(num_keys):
key_index = (start_index + i) % num_keys
current_key = key_list[key_index]
url = f"{API_BASE}/sports/{sport_key}/odds"
params = {
"regions": REGIONS,
"markets": market,
"oddsFormat": ODDS_FORMAT,
}
try:
print(f"[INFO] Attempting fetch for {sport_key}/{market} with key index {key_index + 1}/{num_keys}...")
result = _get_json(url, params, current_key)
# Success! Update the global index to remember the working key
current_api_key_index = key_index
return result
except requests.exceptions.HTTPError as e:
status_code = e.response.status_code
# Retry only on 401 (invalid key/plan limit) or 429 (rate limit)
if status_code in [401, 429]:
print(f"[WARN] Key index {key_index} failed with status {status_code}. Cycling to next key...")
else:
print(f"[ERROR] Non-retryable HTTP error {status_code} for {sport_key}/{market}. Stopping attempts for this sport/market.")
return []
except Exception as e:
print(f"[ERROR] Unexpected error during API call with key index {key_index}: {e}. Stopping attempts for this sport/market.")
return []
print(f"[FATAL] All {num_keys} API keys failed for {sport_key}/{market}.")
return []
# --- Standard Helper functions (unchanged logic) ---
def iso_to_dt(iso: str) -> datetime:
return datetime.fromisoformat(iso.replace("Z", "+00:00"))
def minutes_until(iso: str) -> float:
try:
t = iso_to_dt(iso)
return (t - datetime.now(timezone.utc)).total_seconds() / 60.0
except Exception:
return 999999.0
def within_start_window(iso: str) -> bool:
return minutes_until(iso) >= MIN_START_MINUTES
def book_ok(book: str) -> bool:
return (not BOOKIES_WHITELIST) or (book in BOOKIES_WHITELIST)
def stake_round_for_book(book: str) -> float:
return STAKE_INCREMENT.get(book, STAKE_INCREMENT.get("_default", 0.01))
def min_stake_for_book(book: str) -> float:
return MIN_STAKE.get(book, MIN_STAKE.get("_default", 1.00))
def arb_ratio(odds: List[float]) -> float:
return sum(0 if (o is None or o <= 0) else 1.0 / o for o in odds)
def compute_balanced_stakes(odds: List[float], bankroll: float) -> Tuple[List[float], float, float]:
inv_sum = arb_ratio(odds)
if inv_sum >= 1.0:
return [], 0.0, 0.0
payout = bankroll / inv_sum
stakes = [(bankroll * (1.0 / o)) / inv_sum for o in odds]
profit = payout - bankroll
return stakes, payout, profit
def round_stakes_by_book(stakes: List[float], books: List[str]) -> List[float]:
rounded = []
for s, b in zip(stakes, books):
inc = max(0.01, stake_round_for_book(b))
m = max(min_stake_for_book(b), 0.0)
s2 = max(m, round(s / inc) * inc)
s2 = float(f"{s2:.2f}")
rounded.append(s2)
return rounded
def worst_case_after_rounding(odds: List[float], rounded_stakes: List[float]) -> Tuple[float, float]:
payouts = [rounded_stakes[i] * odds[i] for i in range(len(odds))]
worst_payout = min(payouts)
total_stakes = sum(rounded_stakes)
profit = worst_payout - total_stakes
roi = (profit / total_stakes) * 100.0 if total_stakes > 0 else -999.0
return worst_payout, roi
# --- Price Finders (H2H, Spreads, Totals) ---
def best_prices_h2h(event: dict) -> List[Tuple[str, str, float]]:
best: Dict[str, Tuple[str, float]] = {}
for bm in event.get("bookmakers", []):
bkey = bm.get("key")
if not book_ok(bkey): continue
for m in bm.get("markets", []):
if m.get("key") != "h2h": continue
for o in m.get("outcomes", []):
name = o.get("name"); price = o.get("price")
if name is None or price is None: continue
price = float(price)
if name not in best or price > best[name][1]:
best[name] = (bkey, price)
outs = [(n, b, p) for n, (b, p) in best.items()]
return outs if len(outs) in (2, 3) else []
def best_prices_spreads(event: dict) -> List[Tuple[float, Tuple[str, float], Tuple[str, float]]]:
"""
REFACTORED: Matches spreads based on exact opposition logic to avoid phantom arbs.
It finds the best 'Home' price at point P and matches it ONLY with 'Away' at point -P.
Returns: [(home_point, (homeBook, homeOdds), (awayBook, awayOdds))]
"""
# Map: point -> { "home": (book, odds), "away": (book, odds) }
# We store them by the raw point value (e.g., -1.5, +1.5)
lines: Dict[float, Dict[str, Tuple[str, float]]] = {}
home = event.get("home_team")
away = event.get("away_team")
for bm in event.get("bookmakers", []):
bkey = bm.get("key")
if not book_ok(bkey):
continue
for m in bm.get("markets", []):
if m.get("key") != "spreads":
continue
for o in m.get("outcomes", []):
name = o.get("name")
price = o.get("price")
point = o.get("point")
if price is None or point is None:
continue
price = float(price)
point = float(point)
# Determine side
side = None
if name == "Home" or name == home:
side = "home"
elif name == "Away" or name == away:
side = "away"
# Fallback logic if names don't match perfectly
if side is None:
# usually negative point is favored, but this is risky without name match.
# safer to skip if we can't confirm name
continue
lines.setdefault(point, {})
# Store best price for this specific point & side
if side not in lines[point] or price > lines[point][side][1]:
lines[point][side] = (bkey, price)
out = []
# Now look for valid pairs: Home @ P vs Away @ -P
# Example: Home at -1.5 needs Away at +1.5
for point, sides in lines.items():
if "home" in sides:
target_away_point = -1 * point
# Do we have the opposite line for the away team?
if target_away_point in lines and "away" in lines[target_away_point]:
home_data = sides["home"]
away_data = lines[target_away_point]["away"]
# We found a valid pair.
# We return the 'home_point' as the reference line.
out.append((point, home_data, away_data))
return out
def best_prices_totals(event: dict) -> List[Tuple[float, Tuple[str, float], Tuple[str, float]]]:
totals: Dict[float, Dict[str, Tuple[str, float]]] = {}
for bm in event.get("bookmakers", []):
bkey = bm.get("key")
if not book_ok(bkey): continue
for m in bm.get("markets", []):
if m.get("key") != "totals": continue
for o in m.get("outcomes", []):
name = o.get("name"); price = o.get("price"); point = o.get("point")
if name is None or price is None or point is None: continue
price = float(price); point = float(point)
totals.setdefault(point, {})
side = "over" if name.lower().startswith("over") else "under"
if side not in totals[point] or price > totals[point][side][1]:
totals[point][side] = (bkey, price)
out = []
for point, sides in totals.items():
if "over" in sides and "under" in sides:
out.append((point, sides["over"], sides["under"]))
return out
# --- Arbitrage Finders (H2H, Spreads, Totals) ---
def find_arbs_h2h(event: dict) -> List[dict]:
if not within_start_window(event.get("commence_time", "")): return []
outs = best_prices_h2h(event);
if not outs: return []
names, books, odds = zip(*outs); odds_list = list(odds); ratio = arb_ratio(odds_list)
if ratio >= 1.0: return []
stakes, payout, profit = compute_balanced_stakes(odds_list, BANKROLL)
roi_pct = (profit / BANKROLL) * 100.0
if roi_pct * 100 < MIN_EDGE_BPS: return []
r_stakes = round_stakes_by_book(list(stakes), list(books))
worst_payout, roi_post = worst_case_after_rounding(odds_list, r_stakes)
if roi_post * 100 < MIN_EDGE_BPS_POST_ROUND: return []
return [{"market": "h2h", "sport": event.get("sport_title"), "commence_time": event.get("commence_time"),
"teams": (event.get("home_team"), event.get("away_team")), "arb_ratio": round(ratio, 6),
"roi_pct": round(roi_pct, 4), "roi_post_pct": round(roi_post, 4), "payout": round(payout, 2),
"payout_post": round(worst_payout, 2), "profit": round(profit, 2),
"profit_post": round(worst_payout - sum(r_stakes), 2), "total_stake_rounded": round(sum(r_stakes), 2),
"legs": [{"outcome": names[i], "book": books[i], "odds": odds[i], "stake": round(stakes[i], 2), "stake_rounded": r_stakes[i]} for i in range(len(outs))]}]
def find_arbs_spreads(event: dict) -> List[dict]:
if not within_start_window(event.get("commence_time", "")):
return []
out = []
# Note: home_point is the spread from Home's perspective (e.g., -1.5)
for home_point, (homeBook, homeOdds), (awayBook, awayOdds) in best_prices_spreads(event):
odds = [homeOdds, awayOdds]
ratio = arb_ratio(odds)
if ratio >= 1.0:
continue
stakes, payout, profit = compute_balanced_stakes(odds, BANKROLL)
roi_pct = (profit / BANKROLL) * 100.0
if roi_pct * 100 < MIN_EDGE_BPS:
continue
books = [homeBook, awayBook]
r_stakes = round_stakes_by_book(stakes, books)
worst_payout, roi_post = worst_case_after_rounding(odds, r_stakes)
if roi_post * 100 < MIN_EDGE_BPS_POST_ROUND:
continue
# Calculate the Away point for display (always opposite of home)
away_point = -1 * home_point
# Formatting: Force "+" sign for positive numbers for clarity
# e.g. -1.5 stays "-1.5", 1.5 becomes "+1.5"
h_fmt = f"{home_point:+}"
a_fmt = f"{away_point:+}"
out.append({
"market": "spreads",
"line": abs(home_point), # For generic sorting/display, abs is fine
"sport": event.get("sport_title"),
"commence_time": event.get("commence_time"),
"teams": (event.get("home_team"), event.get("away_team")),
"arb_ratio": ratio,
"roi_pct": roi_pct,
"roi_post_pct": roi_post,
"payout": payout,
"payout_post": worst_payout,
"profit": profit,
"profit_post": worst_payout - sum(r_stakes),
"legs": [
# Explicitly use the formatted string with the sign
{"outcome": f"Home {h_fmt}", "book": homeBook, "odds": homeOdds, "stake": stakes[0], "stake_rounded": r_stakes[0]},
{"outcome": f"Away {a_fmt}", "book": awayBook, "odds": awayOdds, "stake": stakes[1], "stake_rounded": r_stakes[1]},
]
})
return out
def find_arbs_totals(event: dict) -> List[dict]:
if not within_start_window(event.get("commence_time", "")): return []
out = []
for point, (overBook, overOdds), (underBook, underOdds) in best_prices_totals(event):
odds = [overOdds, underOdds]; ratio = arb_ratio(odds)
if ratio >= 1.0: continue
stakes, payout, profit = compute_balanced_stakes(odds, BANKROLL)
roi_pct = (profit / BANKROLL) * 100.0
if roi_pct * 100 < MIN_EDGE_BPS: continue
books = [overBook, underBook]; r_stakes = round_stakes_by_book(stakes, books)
worst_payout, roi_post = worst_case_after_rounding(odds, r_stakes)
if roi_post * 100 < MIN_EDGE_BPS_POST_ROUND: continue
out.append({"market": "totals", "line": point, "sport": event.get("sport_title"), "commence_time": event.get("commence_time"),
"teams": (event.get("home_team"), event.get("away_team")), "arb_ratio": round(ratio, 6), "roi_pct": round(roi_pct, 4),
"roi_post_pct": round(roi_post, 4), "payout": round(payout, 2), "payout_post": round(worst_payout, 2),
"profit": round(profit, 2), "profit_post": round(worst_payout - sum(r_stakes), 2), "total_stake_rounded": round(sum(r_stakes), 2),
"legs": [{"outcome": f"Over {point}", "book": overBook, "odds": overOdds, "stake": round(stakes[0], 2), "stake_rounded": r_stakes[0]},
{"outcome": f"Under {point}", "book": underBook, "odds": underOdds, "stake": round(stakes[1], 2), "stake_rounded": r_stakes[1]},]
})
return out
def scan() -> List[dict]:
"""Main scanning logic, iterates through all configured sports and markets."""
all_opps: List[dict] = []
for sport in SPORTS:
for market in MARKETS:
events = get_events_with_odds(sport, market)
for ev in events:
if market == "h2h":
all_opps.extend(find_arbs_h2h(ev))
elif market == "spreads":
all_opps.extend(find_arbs_spreads(ev))
elif market == "totals":
all_opps.extend(find_arbs_totals(ev))
time.sleep(SLEEP_BETWEEN_CALLS)
return sorted(all_opps, key=lambda x: (x["roi_post_pct"], x["roi_pct"]), reverse=True)
# =============================================================================
# ========= X/Twitter Posting Functions =========
# =============================================================================
def _format_top_arb_to_str(o: dict) -> str:
"""Formats the top arb dict into the multi-line string used as input for the post generator."""
ht, at = o["teams"]
line_txt = f" | line: {o.get('line', '')}" if "line" in o else ""
Arb_str = (f"{o['market'].upper()} | {o['sport']} | {ht} vs {at} | ROI: {o['roi_pct']:.3f}%")
Arb_str += "\n" + (f"{o['commence_time']}{line_txt}")
for L in o["legs"]:
Arb_str += "\n" + (f" - {L['outcome']} @ {L['book']} odds {L['odds']:>6.3f} ")
return Arb_str.strip()
def parse_arb_str(arb_str: str) -> dict:
"""Parses key data points from the multi-line Arb_str for template use."""
out = {"league": None, "matchup": None, "roi": None, "time_utc": None, "line": None,
"home_team": None, "away_team": None, "legs": []}
m_head = re.search(r'ROI:\s*([\d\.]+)', arb_str, re.I)
if m_head: out["roi"] = float(m_head.group(1))
m_info = re.search(r'\|\s*(.*?)\s*\|\s*(.*?)\s*vs\s*(.*?)\s*\|\s*ROI', arb_str, re.I)
if m_info:
out["league"] = m_info.group(1).strip().upper()
out["home_team"] = m_info.group(2).strip()
out["away_team"] = m_info.group(3).strip()
out["matchup"] = f"{out['home_team']} vs {out['away_team']}"
m_time_line = re.search(r'(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)\s*\|\s*line:\s*([-\d\.]+)', arb_str, re.I)
if m_time_line:
out["time_utc"] = m_time_line.group(1)
out["line"] = m_time_line.group(2)
leg_pat = r'-\s*(.*?)\s*@\s*([A-Za-z0-9_\.]+)\s*odds\s*([\d\.]+)'
legs = re.findall(leg_pat, arb_str, re.I)
for outcome, book, odds in legs:
out["legs"].append({"outcome": outcome.strip(), "book": book.strip(), "odds": float(odds)})
return out
def clamp_280(s: str) -> str:
"""Truncates string to <= 280 characters, intelligently removing tags/extra info first."""
s = re.sub(r'\s+', ' ', s).strip()
if len(s) <= 280: return s
if '#' in s:
s_no_tags = re.sub(r'\s#[A-Za-z0-9_]+', '', s).strip()
if len(s_no_tags) <= 280: s = s_no_tags
s = re.sub(r'\s\|\s?\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z.*', '', s)
if len(s) <= 280: return s
return s[:279].rstrip() + "…"
def spicy_fallback(fmt: dict) -> str:
"""Generates a deterministic spicy post when AI key is unavailable or fails."""
if not fmt.get("matchup") or fmt.get("roi") is None or len(fmt.get("legs", [])) < 2:
return clamp_280(f"New Arb Alert! ROI: {fmt.get('roi', 'N/A')}% found. Check lines now. #Arbitrage #SportsBetting")
matchup = fmt['matchup']; roi_disp = f"{round(fmt['roi'], 1)}"; league = fmt.get('league', 'SPORT').upper().replace(' ', '')
leg1 = fmt['legs'][0]; leg2 = fmt['legs'][1]
team_tags = []
try:
if fmt.get("home_team"): team_tags.append(f"#{fmt['home_team'].replace(' ', '')}")
if fmt.get("away_team"): team_tags.append(f"#{fmt['away_team'].replace(' ', '')}")
except: pass
hashtags = ["#Arbitrage", "#SportsBetting"]
if league not in ("SPREADS", "H2H", "TOTALS"): hashtags.append(f"#{league}")
hashtags = list(dict.fromkeys(hashtags + team_tags))[:5]
text = (
f"🚨 {roi_disp}% ROI Arb Spot! {league}: {matchup}. "
f"Leg 1: {leg1['outcome']} @ {leg1['book']} ({leg1['odds']:.2f}). "
f"Leg 2: {leg2['outcome']} @ {leg2['book']} ({leg2['odds']:.2f}). "
f"Window may close quickly! {' '.join(hashtags)}"
)
return clamp_280(text)
def craft_x_post(arb_str: str) -> str:
"""Generates the X post using AI if keys are present, otherwise falls back to a template."""
data = parse_arb_str(arb_str)
if not OPENAI_API_KEY:
print("[INFO] OpenAI API key missing. Using deterministic spicy fallback.")
return spicy_fallback(data)
try:
oaiclient = OpenAI(api_key=OPENAI_API_KEY)
sys_prompt = ("You are a copywriter for quant sports traders on X. "
"Goal: Rewrite an arbitrage summary into a single engauging, scannable X post."
"STRICT RULES: - Put ROI first with an exciting emoji hook! (e.g., 🔥 37.3% ROI Arb Spot!). "
"- Preserve facts exactly: league, matchup, line, odds, sportsbooks, ROI, time (Convert time to Eastern Time if possible). "
"- Format legs compactly: 'Home -1.5 @ Lowvig (2.85) vs Away +1.5 @ BetRivers (2.65)'. - Add one urgency line. "
"- Add 2–3 relevant, popular hashtags (#SportsBetting and league tag like #MLB, trending tags like #expertpicks, #bets). "
"- Add hastags for the teams involved on either side of the bet (#Avalanche #Yankee)"
"- Confident tone, but do NOT promise guaranteed profits. - Output ONE line of plain text. - ABSOLUTE MAX 280 characters.")
user_msg = (f"Rewrite this arbitrage string per the rules. Be sure to start the post with an emoji.\n\n"
f"{arb_str}")
response = oaiclient.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "system", "content": sys_prompt}, {"role": "user", "content": user_msg}],
max_tokens=240,
)
text = response.choices[0].message.content.strip()
print("[INFO] Post generated using OpenAI.")
return clamp_280(text)
except Exception as e:
print(f"[ERROR] OpenAI API call failed: {e}. Falling back to deterministic template.")
return spicy_fallback(data)
def post_to_x(text: str) -> str:
"""Posts the generated text to X/Twitter using Tweepy."""
if not (X_CONSUMER_KEY and X_CONSUMER_SECRET and X_ACCESS_TOKEN and X_ACCESS_TOKEN_SECRET):
raise ValueError("One or more required X/Twitter credentials are missing.")
if not text or len(text) > 280:
raise ValueError(f"Text must be 1–280 chars (got {len(text) if text else 0}).")
try:
client = tweepy.Client(
consumer_key=X_CONSUMER_KEY, consumer_secret=X_CONSUMER_SECRET,
access_token=X_ACCESS_TOKEN, access_token_secret=X_ACCESS_TOKEN_SECRET,
)
resp = client.create_tweet(text=text)
tweet_id = resp.data["id"]
url = f"https://x.com/i/web/status/{tweet_id}"
print(f"✅ Posted successfully to X: {url}")
return url
except Exception as e:
raise Exception(f"Failed to post to X: {e}")
# =============================================================================
# ========= Execution and Entry Points =========
# =============================================================================
def lambda_handler(event, context):
"""Core logic wrapper executed by Lambda or main_cli."""
print(f"Starting arbitrage scan for {len(SPORTS)} sports and {len(MARKETS)} markets...")
opportunities = scan()
print(f"Scan complete. Found {len(opportunities)} arbitrage opportunities (filtered by ROI).")
post_url = None
post_text = None
if opportunities:
top_arb = opportunities[0]
teams = top_arb.get("teams", ("N/A", "N/A"))
print(f"Top Opportunity: {top_arb['market'].upper()} | {top_arb['sport']} | {teams[0]} vs {teams[1]} | ROI Post: {top_arb['roi_post_pct']:.3f}%")
arb_input_str = _format_top_arb_to_str(top_arb)
post_text = craft_x_post(arb_input_str)
print(f"[INFO] Generated Post Text ({len(post_text)} chars): {post_text}")
try:
post_url = post_to_x(post_text)
except Exception as e:
print(f"[ERROR] Failed to post to X: {e}")
# Return a structured response (used by AWS Lambda, ignored by GitHub Actions)
return {
'statusCode': 200,
'body': json.dumps({'total_opportunities': len(opportunities), 'x_post_url': post_url}),
'headers': {'Content-Type': 'application/json'}
}
def main_cli():
"""Entry point for direct execution (GitHub Actions)."""
try:
# Call the core logic. Errors will bubble up and fail the GitHub Action.
lambda_handler(None, None)
except Exception as e:
print(f"\n--- FATAL ERROR ---")
print(f"The execution failed critically: {e}")
# Use sys.exit(1) to explicitly fail the GitHub Action step
sys.exit(1)
if __name__ == "__main__":
main_cli()
This project successfully bridges the gap between financial theory and technical implementation. By automating the complex process of data aggregation, arbitrage calculation, and content generation, "Daily Arbies" demonstrates the power of modern Python stacks in solving real-world problems. It highlights not just coding ability, but the capacity to architect end-to-end solutions that integrate third-party APIs, cloud infrastructure, and generative AI.
Countries Worked In
Data Points
Dollars Saved
Coffee Drinked
Some of the most widely consumed chemicals within the Oil and Gas industry are drag reducing agents. Often the industry simplifies physical characteristics of these agents to small-scale bench-top laboratory friction reduction tests. I authored this paper to tie together decades of academic research on drag reducing agents to their practical application within well completions.
This work highlights meaningful physical traits of drag reducing agents, and their practical use applications to allow operators to make better informed decisions on their chemical selections and capital allocations.
Read more
Clients spend millions of dollars annually stimulating wells to enhance production with the ultimate goal of getting the best return on investment. Well stimulation requires injecting fluid at high pressure and velocity into the reservoir to produce conductive fractures. This analysis was used to investigate the best return on investment when selecting a fluid composition for completions.
Read more
Deriving insights with Python. Automating multivariate comparative analysis of diagnostic results composed of samples comprised of 130 independent features for hundreds of comparisons.
Read moreWhile learning python programming I decided to dive into web development. This site is the product of my continual learning, and drive to expand my technical knowledge.
Read more