Codex commited on
Commit
a16ce98
·
1 Parent(s): 5c4238c

Improve Circa odds cleanup and sign inference

Browse files
Files changed (1) hide show
  1. src/market-scanner.js +139 -3
src/market-scanner.js CHANGED
@@ -351,7 +351,10 @@ function normalizeOcrOddsToken(token) {
351
  const normalized = normalizeWhitespace(token)
352
  .replace(/[~−–—]/g, '-')
353
  .replace(/B/g, '8')
354
- .replace(/O/g, '0');
 
 
 
355
 
356
  if (/^[+-]\d{2,4}$/i.test(normalized)) {
357
  return normalized;
@@ -360,6 +363,29 @@ function normalizeOcrOddsToken(token) {
360
  return null;
361
  }
362
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  function isReasonableCircaOdds(oddsInput, section) {
364
  if (!oddsInput) {
365
  return false;
@@ -428,6 +454,60 @@ function extractBinaryYesNoEntries(line, section) {
428
  return entries;
429
  }
430
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  function extractOverUnderEntries(line, section) {
432
  const entries = [];
433
  const normalizedLine = normalizeWhitespace(line).toUpperCase().replace(/[~]/g, '-');
@@ -480,6 +560,62 @@ function extractOverUnderEntries(line, section) {
480
  return entries;
481
  }
482
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
  export function parseCircaOcrText(text) {
484
  const entries = [];
485
  const lines = String(text ?? '')
@@ -497,7 +633,7 @@ export function parseCircaOcrText(text) {
497
  }
498
 
499
  if (currentSection?.mode === 'binary_yes_no') {
500
- const binaryEntries = extractBinaryYesNoEntries(line, currentSection);
501
  if (binaryEntries.length > 0) {
502
  entries.push(...binaryEntries);
503
  continue;
@@ -505,7 +641,7 @@ export function parseCircaOcrText(text) {
505
  }
506
 
507
  if (currentSection?.mode === 'over_under') {
508
- const overUnderEntries = extractOverUnderEntries(line, currentSection);
509
  if (overUnderEntries.length > 0) {
510
  entries.push(...overUnderEntries);
511
  continue;
 
351
  const normalized = normalizeWhitespace(token)
352
  .replace(/[~−–—]/g, '-')
353
  .replace(/B/g, '8')
354
+ .replace(/O/g, '0')
355
+ .replace(/S/g, '5')
356
+ .replace(/I/g, '1')
357
+ .replace(/[)\],.;:]/g, '');
358
 
359
  if (/^[+-]\d{2,4}$/i.test(normalized)) {
360
  return normalized;
 
363
  return null;
364
  }
365
 
366
+ function normalizeOcrOddsTokenForSide(token, side) {
367
+ const direct = normalizeOcrOddsToken(token);
368
+ if (direct) {
369
+ return direct;
370
+ }
371
+
372
+ const cleaned = normalizeWhitespace(token)
373
+ .replace(/[~−–—]/g, '-')
374
+ .replace(/B/g, '8')
375
+ .replace(/O/g, '0')
376
+ .replace(/S/g, '5')
377
+ .replace(/I/g, '1')
378
+ .replace(/[)\],.;:]/g, '');
379
+
380
+ if (/^\d{2,4}$/.test(cleaned)) {
381
+ return side === 'no' || side === 'under'
382
+ ? `-${cleaned}`
383
+ : `+${cleaned}`;
384
+ }
385
+
386
+ return null;
387
+ }
388
+
389
  function isReasonableCircaOdds(oddsInput, section) {
390
  if (!oddsInput) {
391
  return false;
 
454
  return entries;
455
  }
456
 
457
+ function extractBinaryYesNoEntriesV2(line, section) {
458
+ const entries = [];
459
+ const normalizedLine = normalizeWhitespace(line)
460
+ .toUpperCase()
461
+ .replace(/\//g, ' ')
462
+ .replace(/\s+/g, ' ');
463
+ const pattern = /([A-Z][A-Z.'\- ]{2,40}\([A-Z]{2,3}\))\s+\d{4,6}\s+([+\-~]?[0-9BOSI]{2,4})\s+\d{4,6}\s+([+\-~]?[0-9BOSI]{2,4})/g;
464
+
465
+ for (const match of normalizedLine.matchAll(pattern)) {
466
+ const { playerName, team } = extractPlayerAndTeam(match[1]);
467
+ const yesOdds = normalizeOcrOddsTokenForSide(match[2], 'yes');
468
+ const noOdds = normalizeOcrOddsTokenForSide(match[3], 'no');
469
+
470
+ if (!playerName || playerName.length < 3) {
471
+ continue;
472
+ }
473
+
474
+ for (const sideEntry of [
475
+ { side: section.side ?? 'yes', oddsInput: yesOdds },
476
+ { side: 'no', oddsInput: noOdds },
477
+ ]) {
478
+ if (!sideEntry.oddsInput || !isReasonableCircaOdds(sideEntry.oddsInput, section)) {
479
+ continue;
480
+ }
481
+
482
+ const impliedProbability = americanToImpliedProbability(sideEntry.oddsInput);
483
+ if (impliedProbability === null) {
484
+ continue;
485
+ }
486
+
487
+ const entry = {
488
+ source: 'circa',
489
+ book: 'Circa',
490
+ eventName: 'Circa MLB',
491
+ eventId: null,
492
+ team,
493
+ playerName,
494
+ playerKey: normalizePlayerName(playerName),
495
+ marketType: section.type,
496
+ marketLabel: section.label,
497
+ side: sideEntry.side,
498
+ lineValue: section.lineValue,
499
+ oddsInput: sideEntry.oddsInput,
500
+ impliedProbability,
501
+ rawLabel: line,
502
+ };
503
+ entry.marketKey = buildMarketKey(entry);
504
+ entries.push(entry);
505
+ }
506
+ }
507
+
508
+ return entries;
509
+ }
510
+
511
  function extractOverUnderEntries(line, section) {
512
  const entries = [];
513
  const normalizedLine = normalizeWhitespace(line).toUpperCase().replace(/[~]/g, '-');
 
560
  return entries;
561
  }
562
 
563
+ function extractOverUnderEntriesV2(line, section) {
564
+ const entries = [];
565
+ const normalizedLine = normalizeWhitespace(line)
566
+ .toUpperCase()
567
+ .replace(/[~]/g, '-')
568
+ .replace(/\//g, ' ')
569
+ .replace(/\s+/g, ' ');
570
+ const pattern = /([A-Z][A-Z.'\- ]{2,40}\([A-Z]{2,3}\))\s+(?:O|OVER)\s*(\d+(?:\.\d+)?)\s+([+\-~]?[0-9BOSI]{2,4})\s+(?:U|UNDER)\s*\d+(?:\.\d+)?\s+([+\-~]?[0-9BOSI]{2,4})/g;
571
+
572
+ for (const match of normalizedLine.matchAll(pattern)) {
573
+ const { playerName, team } = extractPlayerAndTeam(match[1]);
574
+ const lineValue = Number(match[2]);
575
+ const overOdds = normalizeOcrOddsTokenForSide(match[3], 'over');
576
+ const underOdds = normalizeOcrOddsTokenForSide(match[4], 'under');
577
+
578
+ if (!playerName || playerName.length < 3 || !Number.isFinite(lineValue)) {
579
+ continue;
580
+ }
581
+
582
+ for (const sideEntry of [
583
+ { side: 'over', oddsInput: overOdds },
584
+ { side: 'under', oddsInput: underOdds },
585
+ ]) {
586
+ if (!sideEntry.oddsInput || !isReasonableCircaOdds(sideEntry.oddsInput, section)) {
587
+ continue;
588
+ }
589
+
590
+ const impliedProbability = americanToImpliedProbability(sideEntry.oddsInput);
591
+ if (impliedProbability === null) {
592
+ continue;
593
+ }
594
+
595
+ const entry = {
596
+ source: 'circa',
597
+ book: 'Circa',
598
+ eventName: 'Circa MLB',
599
+ eventId: null,
600
+ team,
601
+ playerName,
602
+ playerKey: normalizePlayerName(playerName),
603
+ marketType: section.type,
604
+ marketLabel: section.label,
605
+ side: sideEntry.side,
606
+ lineValue,
607
+ oddsInput: sideEntry.oddsInput,
608
+ impliedProbability,
609
+ rawLabel: line,
610
+ };
611
+ entry.marketKey = buildMarketKey(entry);
612
+ entries.push(entry);
613
+ }
614
+ }
615
+
616
+ return entries;
617
+ }
618
+
619
  export function parseCircaOcrText(text) {
620
  const entries = [];
621
  const lines = String(text ?? '')
 
633
  }
634
 
635
  if (currentSection?.mode === 'binary_yes_no') {
636
+ const binaryEntries = extractBinaryYesNoEntriesV2(line, currentSection);
637
  if (binaryEntries.length > 0) {
638
  entries.push(...binaryEntries);
639
  continue;
 
641
  }
642
 
643
  if (currentSection?.mode === 'over_under') {
644
+ const overUnderEntries = extractOverUnderEntriesV2(line, currentSection);
645
  if (overUnderEntries.length > 0) {
646
  entries.push(...overUnderEntries);
647
  continue;