bet-journal

star 3

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.

ianalloway By ianalloway schedule Updated 2/24/2026

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

Install via CLI
npx skills add https://github.com/ianalloway/openclaw-skills --skill bet-journal
Repository Details
star Stars 3
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator