Betting Line Optimizer

Developer: Eric Detjen

I created this calculator while leading the development on the Devin’s Bookie(DBA) data analytics application. We have 6 different calculators on site, though this one is my favorite and required the most effort. On this page, I'll run you through how the calculator works, and a bit of the code that goes into it. Unfortunately, I cannot share the bulk of the code for the calculations, since this is a live site and they are proprietary.

The idea for it came from the fact that there is often a situation where a user wants to bet on a line that doesn't exist. For example, let's say a user wants to bet on a favorite spread at -7(spread) but their sportsbook is only offering -7.5(spread) for -110(odds) and -6.5 for -140(odds). -7 is a key number in football because teams often win by a single touchdown. The user can pay to “juice” the bet at -6.5, so the bet can cash on a team winning by a single touchdown, but that reduces the payout and is not ideal in the long run.

The Line Optimizer Calculator comes in to solve this issue by combining two bets to simulate the desired line. The previous example can be seen below:

Here the user is instructed by our algorithm to risk $64.17 on the -7.5 line and $58.33 on the -6.5 line. By doing this they create a new line for the -7 spread with odds of -122.50. Meaning that if they bet the given bets for a combined $122.50, they stand to make $100.

The calculator also allows the user to customize their bet to fit many other situations. In addition to the spread option outlined above, we offer situations where users can bet Totals, Player Props, and Baseball or hockey -1 lines.

On the front end, the calculators are all relatively similar, just with different text boxes. The calculations done on the backend though are obviously different but again, I cannot share those publicly.

Another feature that we allow across our entire site, is the ability to change the odds type between american, decimal, and fraction. For all of our algorithms, we do the calculations in decimal, so the odds will need to be converted to and from decimal. For this, I can show the code.

We have a couple of functions that allow this…, fromUserTypeToDecimal() which converts the user's data to decimal, and toUserOddType() which is used after the calculations to convert back to their odds type.


  function toUserOddType(val) {
    switch (localStorage.getItem("devinbookie:oddsformat")) {
      case "AMERICAN":
        return decimalToAmerican(val).toFixed(2);
      case "FRACTIONAL":
        return isNaN(decimalToFractional(val.toString()))
          ? 0
          : decimalToFractional(val.toString());
      case "DECIMAL":
        return val.toFixed(2);
      default:
        break;
      }
    }
        
        

Then the functions that actually do the calculations, and throw errors if the odds type is not in the correct format(ie american odds must be < -100 or >= +100.


    function americanToDecimal(odds) {
      odds = Number(odds);
      // odds here must be < -100 or >= +100
      if (odds < -100) {
        return 100 / Math.abs(odds) + 1;
      }
      if (odds >= 100) {
        return odds / 100 + 1;
      }

      return 0;
    }

    function decimalToAmerican(odds) {
      odds = Number(odds);
      // odds here must be > +1
      if (odds < 2 && odds > 1) {
        return 100 / ((odds - 1) * -1);
      }
      if (odds >= 2) {
        return (odds - 1) * 100;
      }
      return 0;
    }

    function decimalToFractional(odds) {
      odds = Number(odds);
      odds--;
      var gcd = function (a, b) {
        if (!b) return a;
        a = parseInt(a);
        b = parseInt(b);
        return gcd(b, a % b);
      };

      var fraction = odds;
      var len = fraction.toString().length - 2;

      var denominator = Math.pow(10, len);
      var numerator = fraction * denominator;

      var divisor = gcd(numerator, denominator);

      numerator /= divisor;
      denominator /= divisor;

      return numerator.toFixed() + "/" + denominator.toFixed();
    }

    function decimalToImpliedOdds(odds) {
      odds = Number(odds);
      if (!odds) return 0;
      return 100 / odds;
    }

    function fractionalToDecimal(odds) {
      try {
        const [numerator, denominator] = odds.split("/");
        if (!numerator || !denominator) return NaN;
        return numerator / denominator + 1;
      } catch (err) {
        return NaN;
      }
    }
          
          

We also have the check the bounds on each input box, there are many inputs that could be impossible so we want to alert the user of this to ensure they are entering the correct data.


  // Pass in the target node, as well as the observer options.
  observer.observe(selected_sport, {
    attributes: true,
    childList: true,
    characterData: true,
  });
  
  //ensures input_smallerspread value ends in .25,.5, or .75
  const isendingInputValid = (inputEl) => {
    if (!inputEl.value.includes(".")) {
      return true;
    }
    if (
      inputEl.value.endsWith(".5") ||
      inputEl.value.endsWith(".25") ||
      inputEl.value.endsWith(".75")
    ) {
      return true;
    } else {
      return false;
    }
  };
  
  //ensures the amount is greator than 0
  line_amount.addEventListener("input", (e) => {
    var amount = Number(e.target.value);
    if (amount < 0) {
      line_errorEl.innerText = "amount must be greater than 0";
      line_amount.parentElement.classList.add("error");
      return;
    }
    line_errorEl.innerText = "";
    line_amount.parentElement.classList.remove("error");
  });
  
  function line_validateInput(e) {
  
  
  
    // checks that input_largerspread iswhole number or ends with .5
    if (e.target.value.includes(".")) {
      if (!e.target.value.endsWith(".5")) {
        line_errorEl.innerText = `${line_input_1_label.innerText} and
          ${line_input_2_label.innerText} must be a whole number or end in .5`;
        e.target.parentElement.classList.add("error");
        return;
      }
    }

    //removes error once fixed
    line_errorEl.innerText = ``;
    e.target.parentElement.classList.remove("error");
    line_computeValues();

    //makes the input values computable
      b_temp = Number(line_input_1.value)
      c_temp = Number(line_input_2.value)
  
    //if sport is TOTAL, then this must be true: B>C
    if (
      selected_sport.innerText = "TOTAL" &&
      b_temp < c_temp
    ) {
        line_errorEl.innerText = `Please double check the totals you entered`;
        e.target.parentElement.classList.add("error");
      }
    
    //if sport is SPREAD, then this must be true: B and C share signs or 
    //B can equal zero and C is negative && abs(b) math.abs(c_temp)))
        )
        {
        line_errorEl.innerText = `Please double check the spreads you entered`;
        e.target.parentElement.classList.add("error");
        return;
      }
    }
  
  
  
  function getSign(val) {
    if (localStorage.getItem("devinbookie:oddsformat") == "AMERICAN" && val > 0)
      return "+";
    return "";
  }
  
  function line_validateUserInput(e) {
    // check D & E for valid odds
    validateUserInput(e, line_errorEl);
    if (line_errorEl.innerText != "") return;
  
    d_tmp = fromUserTypeToDecimal(line_user_input_1.value);
    e_tmp = fromUserTypeToDecimal(line_user_input_2.value);
    toggler_option_name = //class="toggler".name ... for example it would be "OVER".
  
    // check D > E
    if(d_tmp > 0 && e_tmp > 0 && d_tmp > e_tmp){
      if(toggler_option_name == "OVER") {
        line_errorEl.innerText = `When betting an Over, the Higher total must have better odds`;
      }
      if(toggler_option_name == "FAVORITE") {
        line_errorEl.innerText = `When betting a Favorite, the Larger spread must have better odds`;
      }
      e.target.parentElement.classList.add("error");
      return;
    }
      // check D < E
    if(d_tmp > 0 && e_tmp > 0 && d_tmp < e_tmp){
      if(toggler_option_name == "UNDER") {
        line_errorEl.innerText = `When betting an Under, the Lower total must have better odds`;
  
      }
      if(toggler_option_name == "UNDERDOG") {
        line_errorEl.innerText = `When betting a Underdog, the Smaller spread must have better odds`;
      }
      e.target.parentElement.classList.add("error");
      return;
    }   
  
  
    line_errorEl.innerText = ``;
    line_user_input_1.parentElement.classList.remove("error");
    line_user_input_2.parentElement.classList.remove("error");
    line_computeValues();
  }
  
  function toUserOddType(val) {
    switch (localStorage.getItem("devinbookie:oddsformat")) {
      case "AMERICAN":
        return decimalToAmerican(val).toFixed(2);
      case "FRACTIONAL":
        return isNaN(decimalToFractional(val.toString()))
          ? 0
          : decimalToFractional(val.toString());
      case "DECIMAL":
        return val.toFixed(2);
      default:
        break;
    }
  }
  
  function line_computeValues() {
    var a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, x;
    a = Number(line_amount.value);
    b = Number(line_input_1.value) || 0;
    c = Number(line_input_2.value) || 0;
  
    // convert d & e to decimal for calculations
    d = fromUserTypeToDecimal(line_user_input_1.value);
    e = fromUserTypeToDecimal(line_user_input_2.value);
  
    f = b;
    j = c;
    g = d;
    k = e;
  
  
    //to add the letter to odds in the title N. example could be "Odds for O22"
    toggler_option_name = //class="toggler".name ... for example it would be "OVER".
    toggler_first_letter = 'O'
    if(toggler_option_name == "UNDER"||
    toggler_option_name == "UNDERDOG"){
      toggler_first_letter = 'U'
    }
    if (
      selected_sport.innerText == "BASEBALL/HOCKEY -1 LINE"
    ) {
      x = "-1 line";
    }
    
    x = toggler_first_letter + (b + c) / 2;
    if (line_to_win_radio.checked) {
      l = e == 0 ? 0 : a / e;
      i = l;
      h = g == 1 ? 0 : i / (g - 1);
      m = l * (k - 1);
    
      p = i + m;
      o = h + l;
      n = toUserOddType(o == 0 ? 1 : p / o + 1);
    } else {
      h = d == 0 ? 0 : a / d;
      i = h * (g - 1);
      l = i;
      m = l * (e - 1);
  
      //will need to be converted to the users selected odds type
      o = h + i;
      p = i + m;
      n = toUserOddType(o == 0 ? 1 : p / o + 1);
    }
  
        
        

Unfortunately, that is all the code I can show. If this calculator ever becomes deprecated then I will post the code to the actual algorithm here.