-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlambda.py
More file actions
520 lines (436 loc) · 21.9 KB
/
Copy pathlambda.py
File metadata and controls
520 lines (436 loc) · 21.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
import json
import boto3
import pandas as pd
import numpy as np
import logging
from io import StringIO
import os
from datetime import datetime
# Configure logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# --- Arbitrage Functions ---
# Convert American odds to implied probability
def implied_probability(american_odds):
if american_odds > 0:
return 100 / (american_odds + 100)
else:
return -american_odds / (-american_odds + 100)
# Convert American odds to payout multiplier
def decimal_odds(american_odds):
if american_odds > 0:
return (american_odds / 100) + 1
elif american_odds < 0:
return 100 / abs(american_odds) + 1
# Convert decimal odds to American odds
def decimal_to_american_odds(decimal_odds):
if pd.isna(decimal_odds):
return None
# Handle edge cases
if decimal_odds <= 1.0:
# Invalid odds, return None
return None
if decimal_odds >= 2.0:
# For decimal odds >= 2.0, American odds are positive
return round((decimal_odds - 1) * 100)
else:
# For decimal odds < 2.0, American odds are negative
return round(-100 / (decimal_odds - 1))
# Convert implied probability to American odds
def prob_to_american_odds(prob):
if prob > 0.5:
return -100 * (prob / (1 - prob))
else:
return 100 * ((1 - prob) / prob)
# Calculate no-vig (fair) odds
def no_vig(over_odds, under_odds):
over_implied_prob = implied_probability(over_odds)
under_implied_prob = implied_probability(under_odds)
sum_prob = over_implied_prob + under_implied_prob
no_vig_over_odds = over_implied_prob / sum_prob
no_vig_under_odds = under_implied_prob / sum_prob
return prob_to_american_odds(no_vig_over_odds), prob_to_american_odds(no_vig_under_odds)
# Calculate EV as a percentage - updated to match the approach in paste-2.txt
def expected_value(fair_odds, best_odds):
win_prob = implied_probability(fair_odds)
payout = 100 * decimal_odds(best_odds)
edge = (win_prob * (payout - 100)) - ((1 - win_prob) * (100))
return edge / 100 if edge > 0 else None
# Calculate Kelly Criterion as a percentage - updated to use quarter Kelly
def kelly_criterion(fair_odds, best_odds):
win_prob = implied_probability(fair_odds)
b = decimal_odds(best_odds) - 1
q = 1 - win_prob
return ((win_prob * b - q) / b) / 4 # Quarter Kelly instead of full Kelly
# Function to calculate arbitrage opportunity
def calculate_arbitrage(over_odds, under_odds, bet_on_under):
over_decimal = decimal_odds(over_odds)
under_decimal = decimal_odds(under_odds)
over_implied_prob = implied_probability(over_odds)
under_implied_prob = implied_probability(under_odds)
if over_implied_prob + under_implied_prob < 1:
bet_on_over = (bet_on_under * under_decimal) / over_decimal
return bet_on_over, bet_on_under
return None, None
# Find the best odds
def best_odds(row, sportsbooks, bet_type):
best_odds = None
best_book = None
for book in sportsbooks:
col = f"{book}_{bet_type}_price"
odds = row.get(col)
if pd.notna(odds):
if best_odds is None or decimal_odds(odds) > decimal_odds(best_odds):
best_odds = odds
best_book = book
return best_odds, best_book
def calculate_arbitrage_opportunities(df):
"""Calculate arbitrage opportunities in player props data."""
# Create a copy for arbitrage calculation
arb_df = df.copy()
# List of sportsbooks - using the ones from the original code
sportsbooks = ['fliff', 'fanatics', 'prophetX', 'betmgm', 'bet365', 'draftkings', 'caesars', 'fanduel', 'hard_rock_bet', 'bally_bet']
# Initialize new columns for arbitrage DataFrame
arb_df['Arb %'] = None
arb_df['Player Prop'] = None
arb_df['Game'] = None
arb_df['Line'] = None
arb_df['Book 1'] = None
arb_df['Odds Over'] = None
arb_df['Bet Over'] = None
arb_df['Book 2'] = None
arb_df['Odds Under'] = None
arb_df['Bet Under'] = None
arb_df['timestamp'] = datetime.now().strftime('%m:%d:%y:%H:%M:%S.%f')
# Process each row for arbitrage opportunities
for i, row in df.iterrows():
# Find best odds from two books
best_over_odds, best_over_book = best_odds(row, sportsbooks, 'over')
best_under_odds, best_under_book = best_odds(row, sportsbooks, 'under')
# Skip if any odds are missing
if best_over_odds is None or best_under_odds is None:
continue
# Get line values for best over and under books
over_line = row.get(f"{best_over_book}_line")
under_line = row.get(f"{best_under_book}_line")
# Skip if line values are missing or don't match
if pd.isna(over_line) or pd.isna(under_line) or over_line != under_line:
continue
# Calculate if arbitrage opportunity exists
bet_over, bet_under = calculate_arbitrage(best_over_odds, best_under_odds, 100)
if bet_over is not None and bet_under is not None:
# Calculate arbitrage percentage
implied_over = implied_probability(best_over_odds)
implied_under = implied_probability(best_under_odds)
arb_percent = round((1 - (implied_over + implied_under)) * 100, 2)
# Save the arbitrage opportunity
arb_df.at[i, 'Arb %'] = arb_percent
arb_df.at[i, 'Player Prop'] = row.get('player_name', '') + " - " + row.get('market', '')
arb_df.at[i, 'Game'] = row.get('away_team', '') + " vs " + row.get('home_team', '')
arb_df.at[i, 'Line'] = over_line # Using the matching line value
arb_df.at[i, 'Book 1'] = best_over_book
arb_df.at[i, 'Odds Over'] = best_over_odds
arb_df.at[i, 'Bet Over'] = round(bet_over, 2)
arb_df.at[i, 'Book 2'] = best_under_book
arb_df.at[i, 'Odds Under'] = best_under_odds
arb_df.at[i, 'Bet Under'] = round(bet_under, 2)
arb_df.at[i, 'bet_id'] = f"{row.get('game_id')}_{row.get('market')}_{row.get('player_name')}_{over_line}"
# Filter out rows with no arbitrage opportunities
arb_df = arb_df[arb_df['Arb %'].notnull()]
# Sort by best arbitrage opportunities (descending)
arb_df = arb_df.sort_values(by='Arb %', ascending=False)
# Return only the columns we need
return arb_df[['Arb %', 'Player Prop', 'Game', 'Line', 'Book 1', 'Odds Over', 'Bet Over', 'Book 2', 'Odds Under', 'Bet Under', 'timestamp', 'bet_id']]
def calculate_ev_opportunities(df):
"""Calculate positive EV opportunities in player props data - updated to use new approach."""
# Create a copy for EV calculation
ev_df = df.copy()
# List of sportsbooks - expanded to match the updated list
sportsbooks = ['fliff', 'fanatics', 'prophetX', 'betmgm', 'bet365', 'draftkings', 'caesars', 'fanduel', 'hard_rock_bet', 'bally_bet', 'pinnacle']
# Initialize new columns for EV DataFrame
ev_df['EV %'] = None
ev_df['Player Prop'] = None
ev_df['Game'] = None
ev_df['Line'] = None
ev_df['Book'] = None
ev_df['Odds'] = None
ev_df['Novig Odds'] = None
ev_df['Bet Size'] = None
ev_df['Bet Type'] = None
ev_df['timestamp'] = datetime.now().strftime('%m:%d:%y:%H:%M:%S.%f')
# Process each row for EV opportunities
for i, row in df.iterrows():
# Skip if Pinnacle odds are missing
if pd.isna(row.get('pinnacle_over_price')) or pd.isna(row.get('pinnacle_under_price')):
continue
# Calculate fair odds using Pinnacle as the reference book
fair_over_odds, fair_under_odds = no_vig(
row.get('pinnacle_over_price'),
row.get('pinnacle_under_price')
)
# Find best over odds from all books
best_over_odds, best_over_book = best_odds(row, sportsbooks, 'over')
# Find best under odds from all books
best_under_odds, best_under_book = best_odds(row, sportsbooks, 'under')
# Skip if any odds are missing
if best_over_odds is None or best_under_odds is None:
continue
# Get line values for best odds books
over_line = row.get(f"{best_over_book}_line")
under_line = row.get(f"{best_under_book}_line")
# Skip if line values are missing or don't match
if pd.isna(over_line) or pd.isna(under_line) or over_line != under_line:
continue
# Check if there is value in either over or under bet
if decimal_odds(best_over_odds) > decimal_odds(fair_over_odds) or decimal_odds(best_under_odds) > decimal_odds(fair_under_odds):
# Calculate EV for over and under bets
ev_over = expected_value(fair_over_odds, best_over_odds)
ev_under = expected_value(fair_under_odds, best_under_odds)
# Calculate Kelly criterion for bet sizing
kelly_over = kelly_criterion(fair_over_odds, best_over_odds)
kelly_under = kelly_criterion(fair_under_odds, best_under_odds)
# Process both over and under opportunities
if ev_over is not None and ev_under is not None:
# Choose the better EV between over and under
if ev_over > ev_under:
ev_df.at[i, 'EV %'] = round(ev_over * 100, 2)
ev_df.at[i, 'Player Prop'] = row.get('player_name', '') + " - " + row.get('market', '')
ev_df.at[i, 'Game'] = row.get('away_team', '') + " vs " + row.get('home_team', '')
ev_df.at[i, 'Line'] = over_line
ev_df.at[i, 'Book'] = best_over_book
ev_df.at[i, 'Odds'] = best_over_odds
ev_df.at[i, 'Novig Odds'] = round(fair_over_odds, 2)
ev_df.at[i, 'Bet Size'] = round(kelly_over * 100, 2) if kelly_over else None
ev_df.at[i, 'Bet Type'] = 'Over'
ev_df.at[i, 'bet_id'] = f"{row.get('game_id')}_{row.get('market')}_{row.get('player_name')}_{over_line}_over"
else:
ev_df.at[i, 'EV %'] = round(ev_under * 100, 2)
ev_df.at[i, 'Player Prop'] = row.get('player_name', '') + " - " + row.get('market', '')
ev_df.at[i, 'Game'] = row.get('away_team', '') + " vs " + row.get('home_team', '')
ev_df.at[i, 'Line'] = under_line
ev_df.at[i, 'Book'] = best_under_book
ev_df.at[i, 'Odds'] = best_under_odds
ev_df.at[i, 'Novig Odds'] = round(fair_under_odds, 2)
ev_df.at[i, 'Bet Size'] = round(kelly_under * 100, 2) if kelly_under else None
ev_df.at[i, 'Bet Type'] = 'Under'
ev_df.at[i, 'bet_id'] = f"{row.get('game_id')}_{row.get('market')}_{row.get('player_name')}_{under_line}_under"
# If only over EV exists
elif ev_over is not None:
ev_df.at[i, 'EV %'] = round(ev_over * 100, 2)
ev_df.at[i, 'Player Prop'] = row.get('player_name', '') + " - " + row.get('market', '')
ev_df.at[i, 'Game'] = row.get('away_team', '') + " vs " + row.get('home_team', '')
ev_df.at[i, 'Line'] = over_line
ev_df.at[i, 'Book'] = best_over_book
ev_df.at[i, 'Odds'] = best_over_odds
ev_df.at[i, 'Novig Odds'] = round(fair_over_odds, 2)
ev_df.at[i, 'Bet Size'] = round(kelly_over * 100, 2) if kelly_over else None
ev_df.at[i, 'Bet Type'] = 'Over'
ev_df.at[i, 'bet_id'] = f"{row.get('game_id')}_{row.get('market')}_{row.get('player_name')}_{over_line}_over"
# If only under EV exists
elif ev_under is not None:
ev_df.at[i, 'EV %'] = round(ev_under * 100, 2)
ev_df.at[i, 'Player Prop'] = row.get('player_name', '') + " - " + row.get('market', '')
ev_df.at[i, 'Game'] = row.get('away_team', '') + " vs " + row.get('home_team', '')
ev_df.at[i, 'Line'] = under_line
ev_df.at[i, 'Book'] = best_under_book
ev_df.at[i, 'Odds'] = best_under_odds
ev_df.at[i, 'Novig Odds'] = round(fair_under_odds, 2)
ev_df.at[i, 'Bet Size'] = round(kelly_under * 100, 2) if kelly_under else None
ev_df.at[i, 'Bet Type'] = 'Under'
ev_df.at[i, 'bet_id'] = f"{row.get('game_id')}_{row.get('market')}_{row.get('player_name')}_{under_line}_under"
# Filter out rows with no EV opportunities
ev_df = ev_df[ev_df['EV %'].notnull()]
# Sort by best EV opportunities (descending)
ev_df = ev_df.sort_values(by='EV %', ascending=False)
# Return only the columns we need
return ev_df[['EV %', 'Player Prop', 'Game', 'Line', 'Book', 'Odds', 'Novig Odds', 'Bet Size', 'Bet Type', 'timestamp', 'bet_id']]
def update_bet_history(arb_df, ev_df, s3_client, bucket_name):
"""Update the bet history by combining active bet information."""
# Combine arbitrage and EV data
combined_df = pd.DataFrame()
if not arb_df.empty:
arb_copy = arb_df.copy()
arb_copy['Type'] = 'Arbitrage'
combined_df = pd.concat([combined_df, arb_copy], ignore_index=True)
if not ev_df.empty:
ev_copy = ev_df.copy()
ev_copy['Type'] = 'Expected Value'
combined_df = pd.concat([combined_df, ev_copy], ignore_index=True)
if combined_df.empty:
logger.info("No bets to track history for.")
return
# Load existing active bets
try:
response = s3_client.get_object(Bucket=bucket_name, Key='active/active_bets.csv')
active_df = pd.read_csv(response['Body'])
active_df['timestamp'] = pd.to_datetime(active_df['timestamp'], errors='coerce')
if 'duration' not in active_df.columns:
active_df['duration'] = 0
except Exception as e:
logger.info(f"No active bets file found or error reading file: {e}")
active_df = pd.DataFrame(columns=list(combined_df.columns) + ['duration'])
current_time = datetime.now()
# Map existing bets by bet_id to their rows
active_dict = {
row['bet_id']: row
for _, row in active_df.iterrows()
if 'bet_id' in row and pd.notna(row['bet_id'])
}
updated_active = []
expired_bets = []
# Iterate through current combined bets
for _, row in combined_df.iterrows():
bet_id = row.get('bet_id')
if pd.isna(bet_id):
continue
row_copy = row.copy()
if bet_id in active_dict:
# Existing bet: preserve original timestamp and update duration
start_time = active_dict[bet_id]['timestamp']
elapsed = (current_time - start_time).total_seconds()
row_copy['duration'] = elapsed
row_copy['timestamp'] = start_time
updated_active.append(row_copy)
del active_dict[bet_id]
else:
# New bet: initialize timestamp and duration
row_copy['duration'] = 0
row_copy['timestamp'] = current_time
updated_active.append(row_copy)
# Any remaining in active_dict are expired bets
for bet_id, expired_row in active_dict.items():
start_time = expired_row['timestamp']
expired_row['duration'] = (current_time - start_time).total_seconds()
expired_bets.append(expired_row)
# Save updated active bets back to S3
if updated_active:
updated_active_df = pd.DataFrame(updated_active)
csv_buffer = StringIO()
updated_active_df.to_csv(csv_buffer, index=False)
s3_client.put_object(
Bucket=bucket_name,
Key='active/active_bets.csv',
Body=csv_buffer.getvalue()
)
logger.info(f"Updated active bets file with {len(updated_active_df)} bets")
# Append expired bets to history
if expired_bets:
expired_df = pd.DataFrame(expired_bets)
try:
response = s3_client.get_object(Bucket=bucket_name, Key='history/bet_history.csv')
history_df = pd.read_csv(response['Body'])
history_df = pd.concat([history_df, expired_df], ignore_index=True)
except Exception as e:
logger.info(f"No history file found or error reading file: {e}")
history_df = expired_df
csv_buffer = StringIO()
history_df.to_csv(csv_buffer, index=False)
s3_client.put_object(
Bucket=bucket_name,
Key='history/bet_history.csv',
Body=csv_buffer.getvalue()
)
logger.info(f"Added {len(expired_bets)} expired bets to history")
def lambda_handler(event, context):
try:
# Get S3 bucket name from environment variable
s3_bucket = os.environ['S3_BUCKET_NAME']
# Initialize S3 client
s3_client = boto3.client('s3')
# Check if event is from S3
if 'Records' in event:
# Get file info from S3 event
for record in event['Records']:
if record['eventSource'] == 'aws:s3':
file_key = record['s3']['object']['key']
logger.info(f"Processing file: {file_key}")
if 'player_props.csv' in file_key:
# Download the file from S3
response = s3_client.get_object(
Bucket=s3_bucket,
Key=file_key
)
# Read the CSV into a DataFrame
df = pd.read_csv(response['Body'])
# Calculate arbitrage opportunities
arb_df = calculate_arbitrage_opportunities(df)
# Calculate EV opportunities using new approach
ev_df = calculate_ev_opportunities(df)
# Update bet history
update_bet_history(arb_df, ev_df, s3_client, s3_bucket)
# Save arbitrage results to S3
arb_output_key = 'final/arbitrage_results_with_ev_and_kelly.csv'
arb_csv_buffer = StringIO()
arb_df.to_csv(arb_csv_buffer, index=False)
s3_client.put_object(
Bucket=s3_bucket,
Key=arb_output_key,
Body=arb_csv_buffer.getvalue()
)
# Save EV results to S3
ev_output_key = 'final/ev_results.csv'
ev_csv_buffer = StringIO()
ev_df.to_csv(ev_csv_buffer, index=False)
s3_client.put_object(
Bucket=s3_bucket,
Key=ev_output_key,
Body=ev_csv_buffer.getvalue()
)
logger.info(f"Successfully calculated arbitrage and EV opportunities and saved to S3")
return {
'statusCode': 200,
'body': json.dumps('Successfully processed arbitrage and EV opportunities')
}
else:
logger.info(f"File {file_key} is not player_props.csv, skipping")
else:
# If not triggered by S3, try to get the file from raw folder
try:
response = s3_client.get_object(
Bucket=s3_bucket,
Key='raw/player_props.csv'
)
# Read the CSV into a DataFrame
df = pd.read_csv(response['Body'])
# Calculate arbitrage opportunities
arb_df = calculate_arbitrage_opportunities(df)
# Calculate EV opportunities using new approach
ev_df = calculate_ev_opportunities(df)
# Update bet history
update_bet_history(arb_df, ev_df, s3_client, s3_bucket)
# Save arbitrage results to S3
arb_output_key = 'final/arbitrage_results_with_ev_and_kelly.csv'
arb_csv_buffer = StringIO()
arb_df.to_csv(arb_csv_buffer, index=False)
s3_client.put_object(
Bucket=s3_bucket,
Key=arb_output_key,
Body=arb_csv_buffer.getvalue()
)
# Save EV results to S3
ev_output_key = 'final/ev_results.csv'
ev_csv_buffer = StringIO()
ev_df.to_csv(ev_csv_buffer, index=False)
s3_client.put_object(
Bucket=s3_bucket,
Key=ev_output_key,
Body=ev_csv_buffer.getvalue()
)
logger.info(f"Successfully calculated arbitrage and EV opportunities and saved to S3")
return {
'statusCode': 200,
'body': json.dumps('Successfully processed arbitrage and EV opportunities')
}
except Exception as e:
logger.error(f"Error retrieving or processing file: {str(e)}")
raise e
return {
'statusCode': 200,
'body': json.dumps('No appropriate file to process')
}
except Exception as e:
logger.error(f"Error in lambda_handler: {str(e)}")
return {
'statusCode': 500,
'body': json.dumps(f'Error: {str(e)}')
}