name: bet-journal description: "Track your sports bets in a local CSV journal. Calculate ROI, CLV, win rate by sport/bet-type, and identify where you're actually making money." homepage: https://github.com/ianalloway/openclaw-skills metadata: { "openclaw": { "emoji": "๐", "requires": { "bins": ["python3"] }, "credentials": [], },
}
Bet Journal
The most important edge in sports betting isn't picking winners โ it's tracking your bets to learn where your edge is real vs. imagined. This skill creates and analyzes a local CSV bet journal.
Initialize the Journal
python3 -c "
import csv, os
JOURNAL_FILE = os.path.expanduser('~/.openclaw/bet-journal.csv')
os.makedirs(os.path.dirname(JOURNAL_FILE), exist_ok=True)
HEADERS = ['date', 'sport', 'game', 'bet_type', 'pick', 'odds_american',
'stake', 'result', 'pnl', 'closing_odds', 'notes']
if not os.path.exists(JOURNAL_FILE):
with open(JOURNAL_FILE, 'w', newline='') as f:
csv.DictWriter(f, fieldnames=HEADERS).writeheader()
print(f'Journal created: {JOURNAL_FILE}')
else:
print(f'Journal already exists: {JOURNAL_FILE}')
print()
print('Fields:')
for h in HEADERS:
print(f' {h}')
print()
print('result values: W (win), L (loss), P (push), V (void)')
print('bet_type values: ML (moneyline), SPREAD, TOTAL, PARLAY, PROP, FUTURES')
"
Add a Bet
python3 -c "
import csv, os
from datetime import date
JOURNAL_FILE = os.path.expanduser('~/.openclaw/bet-journal.csv')
def add_bet(sport, game, bet_type, pick, odds_american, stake, result, closing_odds=None, notes=''):
if result == 'W':
if odds_american > 0:
pnl = stake * odds_american / 100
else:
pnl = stake * 100 / abs(odds_american)
elif result in ('L',):
pnl = -stake
else:
pnl = 0 # push or void
row = {
'date': date.today().isoformat(),
'sport': sport,
'game': game,
'bet_type': bet_type,
'pick': pick,
'odds_american': odds_american,
'stake': stake,
'result': result,
'pnl': round(pnl, 2),
'closing_odds': closing_odds or '',
'notes': notes,
}
with open(JOURNAL_FILE, 'a', newline='') as f:
csv.DictWriter(f, fieldnames=row.keys()).writerow(row)
print(f'Added: {sport} | {game} | {pick} ({odds_american:+d}) | Result: {result} | P&L: {pnl:+.2f}')
# --- EDIT THESE TO LOG YOUR BET ---
add_bet(
sport='NBA',
game='Lakers vs Warriors',
bet_type='SPREAD',
pick='Warriors -3.5',
odds_american=-110,
stake=100,
result='W', # W / L / P / V
closing_odds=-115, # Line at close (for CLV tracking)
notes='Sharp action on Warriors, public on Lakers',
)
"
View Your Dashboard
python3 -c "
import csv, os
from collections import defaultdict
JOURNAL_FILE = os.path.expanduser('~/.openclaw/bet-journal.csv')
if not os.path.exists(JOURNAL_FILE):
print('No journal found. Run the init script first.')
exit()
bets = []
with open(JOURNAL_FILE) as f:
for row in csv.DictReader(f):
row['stake'] = float(row['stake'])
row['pnl'] = float(row['pnl'])
row['odds_american'] = int(row['odds_american'])
bets.append(row)
if not bets:
print('No bets logged yet.')
exit()
wins = [b for b in bets if b['result'] == 'W']
losses = [b for b in bets if b['result'] == 'L']
pushes = [b for b in bets if b['result'] in ('P', 'V')]
total_bets = len(wins) + len(losses)
win_rate = len(wins) / total_bets if total_bets else 0
total_wagered = sum(b['stake'] for b in bets)
total_pnl = sum(b['pnl'] for b in bets)
roi = total_pnl / total_wagered if total_wagered else 0
print('=== Bet Journal Dashboard ===')
print(f'Record: {len(wins)}-{len(losses)}-{len(pushes)}')
print(f'Win Rate: {win_rate:.1%}')
print(f'Total Wagered: \${total_wagered:,.2f}')
print(f'Net P&L: {total_pnl:+,.2f}')
print(f'ROI: {roi:+.1%}')
print()
# By sport
by_sport = defaultdict(list)
for b in bets:
by_sport[b['sport']].append(b)
print('=== P&L by Sport ===')
for sport, sbets in sorted(by_sport.items()):
sw = sum(1 for b in sbets if b['result'] == 'W')
sl = sum(1 for b in sbets if b['result'] == 'L')
spnl = sum(b['pnl'] for b in sbets)
swag = sum(b['stake'] for b in sbets)
sroi = spnl / swag if swag else 0
print(f' {sport:<8} {sw}-{sl} P&L: {spnl:+.2f} ROI: {sroi:+.1%}')
# By bet type
print()
print('=== P&L by Bet Type ===')
by_type = defaultdict(list)
for b in bets:
by_type[b['bet_type']].append(b)
for bt, tbets in sorted(by_type.items()):
tw = sum(1 for b in tbets if b['result'] == 'W')
tl = sum(1 for b in tbets if b['result'] == 'L')
tpnl = sum(b['pnl'] for b in tbets)
twag = sum(b['stake'] for b in tbets)
troi = tpnl / twag if twag else 0
print(f' {bt:<10} {tw}-{tl} P&L: {tpnl:+.2f} ROI: {troi:+.1%}')
"
Closing Line Value (CLV) Calculator
CLV is the #1 predictor of long-term betting success. If you consistently beat the closing line, you have a real edge:
python3 -c "
import csv, os
JOURNAL_FILE = os.path.expanduser('~/.openclaw/bet-journal.csv')
def american_to_prob(american):
if american > 0:
return 100 / (american + 100)
else:
return abs(american) / (abs(american) + 100)
bets_with_clv = []
with open(JOURNAL_FILE) as f:
for row in csv.DictReader(f):
if row['closing_odds']:
open_prob = american_to_prob(int(row['odds_american']))
close_prob = american_to_prob(int(row['closing_odds']))
clv = close_prob - open_prob # positive = you got better than closing line
bets_with_clv.append({**row, 'clv': clv, 'open_prob': open_prob, 'close_prob': close_prob})
if not bets_with_clv:
print('No CLV data yet โ add closing_odds when logging bets')
exit()
avg_clv = sum(b['clv'] for b in bets_with_clv) / len(bets_with_clv)
positive_clv = sum(1 for b in bets_with_clv if b['clv'] > 0)
print('=== Closing Line Value Report ===')
print(f'Bets with CLV data: {len(bets_with_clv)}')
print(f'Avg CLV: {avg_clv:+.2%}')
print(f'Beat closing line: {positive_clv}/{len(bets_with_clv)} ({positive_clv/len(bets_with_clv):.0%})')
print()
if avg_clv > 0.01:
print('VERDICT: โ
Positive CLV โ you are buying at better than market prices')
print(' Your edge is REAL. Keep doing what you are doing.')
elif avg_clv > -0.01:
print('VERDICT: โช Neutral CLV โ roughly breaking even vs. market')
print(' Improve line shopping and timing to push positive.')
else:
print('VERDICT: ๐ด Negative CLV โ you are consistently getting bad lines')
print(' Shop more books, bet earlier when sharp action moves lines')
print()
print('Recent CLV (last 5 bets):')
for b in bets_with_clv[-5:]:
clv_str = f\"{b['clv']:+.2%}\"
print(f\" {b['game']:<30} {b['pick']:<20} CLV: {clv_str}\")
"
Monthly P&L Chart
python3 -c "
import csv, os
from collections import defaultdict
JOURNAL_FILE = os.path.expanduser('~/.openclaw/bet-journal.csv')
monthly = defaultdict(float)
with open(JOURNAL_FILE) as f:
for row in csv.DictReader(f):
month = row['date'][:7] # YYYY-MM
monthly[month] += float(row['pnl'])
print('=== Monthly P&L ===')
running = 0
for month in sorted(monthly):
pnl = monthly[month]
running += pnl
bar_len = int(abs(pnl) / 10)
bar = ('โ' * min(bar_len, 30)) if pnl >= 0 else ('โ' * min(bar_len, 30))
sign = '+' if pnl >= 0 else ''
color_prefix = '๐ข' if pnl >= 0 else '๐ด'
print(f'{color_prefix} {month} {sign}{pnl:>8.2f} {bar} (running: {running:+.2f})')
"
Import from Bet Tracker Apps
Convert Action Network or Betstamp exports to journal format:
python3 -c "
import csv, os
# Converts a basic Action Network CSV export
def import_action_network(input_file, output_file):
rows_added = 0
with open(input_file) as fin, open(output_file, 'a', newline='') as fout:
reader = csv.DictReader(fin)
writer = None
for row in reader:
mapped = {
'date': row.get('Date', ''),
'sport': row.get('Sport', ''),
'game': row.get('Event', ''),
'bet_type': row.get('Type', 'ML'),
'pick': row.get('Pick', ''),
'odds_american': row.get('Odds', '0'),
'stake': row.get('Risk', '0').replace('\$', '').replace(',', ''),
'result': 'W' if row.get('Result') == 'Won' else 'L' if row.get('Result') == 'Lost' else 'P',
'pnl': row.get('Profit', '0').replace('\$', '').replace(',', ''),
'closing_odds': '',
'notes': 'imported from Action Network',
}
if writer is None:
writer = csv.DictWriter(fout, fieldnames=mapped.keys())
writer.writerow(mapped)
rows_added += 1
print(f'Imported {rows_added} bets from {input_file}')
print('Usage: import_action_network(\"action_export.csv\", \"~/.openclaw/bet-journal.csv\")')
print('Modify the field mapping above to match your export format.')
"
The Numbers That Matter
| Metric | Break-even | Good | Elite |
|---|---|---|---|
| Win Rate (at -110) | 52.4% | 54%+ | 57%+ |
| ROI | 0% | +3%+ | +8%+ |
| Average CLV | 0% | +1%+ | +2.5%+ |
| CLV Beat Rate | 50% | 55%+ | 60%+ |
Track everything. Your gut thinks you're winning everywhere. Your spreadsheet knows the truth.
Author
Created by Ian Alloway โ Data Scientist specializing in sports analytics and ML.
License
MIT License