oddsmaker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1376d7ace6063ba0f7eaf60c8dc1b35d76bb505d
4
+ data.tar.gz: e01409a3acd1fdb92da2cce2223b35b6987832e3
5
+ SHA512:
6
+ metadata.gz: d9184cc277ac8eaeb9d9a4fb1475c2c9a37341531ffd995f84e2ebf47d763bb7e736c15881379fefb9a69ec3473eb9ad76d75e293f436654ba45e2bd9fca985a
7
+ data.tar.gz: 92c6699135010fd0734d7edbbfa229c76af9b159c6a785121313fa836a037dc978b8ac7cbc05aaf1ad3d17b248cf498a5729e2db68bf5af6a45b2a49a70ea837
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
@@ -0,0 +1,8 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.4
5
+ - 2.4.1
6
+ - ruby-head
7
+ - jruby-head
8
+ before_install: gem install bundler -v 1.15.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in oddsmaker.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Paul Hoffer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,99 @@
1
+ # Oddsmaker
2
+
3
+ Oddsmaker is to represent and manipulate betting odds. Multiple odds can be represented together, as a `Market`. A `Market` is able to calculate total probability and vig, and can also calculate true probabilities (total of 100%) and calculate overrounded odds (greater than 100%).
4
+
5
+ Oddsmaker can handle American odds, decimal odds, fractional odds, and implied probability, and allows for easy conversion between all of those types.
6
+
7
+ [SportsBookReview](https://www.sportsbookreview.com/picks/tools/odds-converter/) has an excellent page which explains the different types of odds and converts between them.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'oddsmaker'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install oddsmaker
24
+
25
+ ## Usage
26
+
27
+ Let's start with a simple odd:
28
+
29
+ ```ruby
30
+ # create an Odd
31
+ odd = Oddsmaker::Odd.american(300) # => <Oddsmaker::Odd::American @id=300, @value=300>
32
+ odd = Oddsmaker::Odd.american('+300', 'underdog') # optional 2nd param allows for identifying the odd
33
+ odd # => <Oddsmaker::Odd::American @id="underdog", @value=300>
34
+ odd.name # => "underdog"
35
+ odd.value # => 300
36
+ odd.to_s # => "+300"
37
+
38
+ # can convert between formats
39
+ odd.decimal # => <Oddsmaker::Odd::Decimal @id="underdog, @value=4.0>
40
+ odd.fractional # => <Oddsmaker::Odd::Fractional @id="underdog, @value=(3/1)>
41
+ odd.implied_probability # => <Oddsmaker::Odd::ImpliedProbability @id="underdog", @value=0.25>
42
+
43
+ # easy creation any type of odd
44
+ # any odd type can be compared as well
45
+ Oddsmaker::Odd.american(300) == Oddsmaker::Odd.decimal(4)
46
+ Oddsmaker::Odd.decimal(4) == Oddsmaker::Odd.fractional(3/1)
47
+ Oddsmaker::Odd.fractional(3/1) == Oddsmaker::Odd.implied(25)
48
+ Oddsmaker::Odd.implied(25) == Oddsmaker::Odd.american(300)
49
+
50
+ Oddsmaker::Odd.american(300, 'underdog').to_json # => {"name":"underdog","american":300,"decimal":4.0,"fractional":"3/1","implied":0.25}
51
+ ```
52
+
53
+ A collection of `Odd`s can be represented as a `Market`:
54
+
55
+ ```ruby
56
+ # Create a Market with odds as the arguments
57
+ market = Oddsmaker::Market.new(Oddsmaker::Odd.american(300), Oddsmaker::Odd.american(-400))
58
+ market.total_probability # => 1.05
59
+ market.vig # => 0.04761904761904767 (equivalent to (1 - total_probability**-1))
60
+
61
+ # get the odds without vig
62
+ no_vig = market.without_vig # => new market with the vig/overround removed
63
+ no_vig.odds # => [#<Oddsmaker::Odd::American @id=300, @value=320>, #<Oddsmaker::Odd::American @id=-400, @value=-320>]
64
+ no_vig.total_probability # => 1.0
65
+ no_vig.vig # => 0
66
+
67
+ # apply an overround to have vigged odds
68
+ vigged = no_vig.overround!(5) # => 5% overround, returns a new Market
69
+ vigged.odds # => [#<Oddsmaker::Odd::American @id=300, @value=300>, #<Oddsmaker::Odd::American @id=-400, @value=-400>]
70
+ vigged.total_probability # => 1.05
71
+ vigged.vig # => 0.04761904761904767
72
+
73
+ vigged == market # => true # equality comparison only cares about the odds' values. Name and ordering is not relevant
74
+ ```
75
+
76
+ There is also functionality for calculating wagers:
77
+
78
+ ```ruby
79
+ odd = Oddsmaker::Odd.american(300) # => <Oddsmaker::Odd::American @id=300, @value=300>
80
+ wager = odd.wager(100) # => <Oddsmaker::Wager @amount=100, @odd=#<Oddsmaker::Odd::American @id=300, @value=300>>
81
+ wager.amount # => 100
82
+ wager.profit # => 300
83
+ wager.return # => 400
84
+
85
+ # odds are able to calculate their own profit for a wager amount
86
+ odd.profit(100) # => 300
87
+ ```
88
+
89
+ ## Development
90
+
91
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
92
+
93
+ ## Contributing
94
+
95
+ Bug reports and pull requests are welcome on GitHub at https://github.com/phoffer/oddsmaker.
96
+
97
+ ## License
98
+
99
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "oddsmaker"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ require "pry"
10
+ Pry.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,17 @@
1
+ require 'bigdecimal'
2
+ require "oddsmaker/version"
3
+ require "oddsmaker/market"
4
+ require "oddsmaker/odd"
5
+ require "oddsmaker/wager"
6
+ require "oddsmaker/odd/base"
7
+ require "oddsmaker/odd/implied_probability"
8
+ require "oddsmaker/odd/american"
9
+ require "oddsmaker/odd/decimal"
10
+ require "oddsmaker/odd/fractional"
11
+
12
+ # Oddsmaker is to assist with the calculations involved with betting, especially sports betting.
13
+ # Both a single odd and a set of odds can be represented and manipulated.
14
+ # Additionally, wagers can be represented, allowing for flexibility in calculating profit/return.
15
+ module Oddsmaker
16
+
17
+ end
@@ -0,0 +1,115 @@
1
+ module Oddsmaker
2
+ # Market is a container of multiple odds. This is useful to represent the full data of sports betting.
3
+ # A generic moneyline of +300/-400 can be represented by a market, containing both of the individual odds.
4
+ # This also allows for calculating the total probability, which will be over 100% if set by a sportsbook.
5
+ # Additional functions include the ability to determine the odds without vig (the book's profit),
6
+ # and the ability to calculate odds for a given overround (the total probability above 100%).
7
+ class Market
8
+ attr_reader :odds, :vigged, :name
9
+
10
+ def initialize(*odds, name: nil)
11
+ @name = name
12
+ @odds = odds.flatten
13
+ end
14
+
15
+ # Retrieve an odd by identifier.
16
+ #
17
+ # @param id [String, Integer] Odd identifier. Defaults to provided value.
18
+ # @return [Odd]
19
+ def odd(id)
20
+ odds_hash[id]
21
+ end
22
+
23
+ # Total probability for a set of odds.
24
+ # This will be over 1 if set by a sportsbook.
25
+ #
26
+ # @return [Float] Total probability, with 1.0 representing 100%
27
+ def total_probability
28
+ @total_probability ||= @odds.inject(0) { |total, odd| total + odd.implied_probability.value } # Change to #sum when 2.3 support can be dropped
29
+ end
30
+
31
+ # Calculate equivalent market without any overround (vig).
32
+ #
33
+ # @return [Market] New market with total probability == 1.0
34
+ def without_vig
35
+ self.class.new(without_vig_odds)
36
+ end
37
+
38
+ # Calculate the odds without any overround (vig).
39
+ #
40
+ # @return [<Odd>] Array of odds with the vig removed
41
+ def without_vig_odds
42
+ @without_vig_odds = if total_probability != 1.0
43
+ @odds.map { |odd| odd.without_vig(total_probability) }
44
+ else
45
+ @odds
46
+ end
47
+ end
48
+
49
+ # Total vig (maximum vig under balanced book scenario)
50
+ #
51
+ # @return [Float]
52
+ def vig
53
+ 1 - total_probability**-1
54
+ end
55
+
56
+ # Create market with an added overround amount
57
+ #
58
+ # @param v [Integer] Overround percent
59
+ # @return [Market] New market with overrounded odds
60
+ def overround!(v = 5)
61
+ @vigged = self.class.new(overround_odds!(v))
62
+ end
63
+
64
+ # Calculate the odds with given overround
65
+ #
66
+ # @param v [Integer] Overround percent
67
+ # @return [Array<Odd>] Overrounded odds
68
+ def overround_odds!(v = 5)
69
+ without_vig_odds.map { |odd| odd.overround!(v) }
70
+ end
71
+
72
+ # Compare equality to another market
73
+ #
74
+ # @param other [Market]
75
+ # @return [Boolean]
76
+ def ==(other)
77
+ odds.sort == other.odds.sort
78
+ end
79
+
80
+ # Hash representation of a market.
81
+ # @return [Hash]
82
+ def to_h
83
+ full_odds = if self.total_probability != 1
84
+ no_vig = self.without_vig_odds
85
+ odds.map.with_index { |odd, index| odd.to_h.merge(actual: no_vig[index].implied_probability.value, without_vig: no_vig[index].value) }
86
+ else
87
+ odds.map(&:to_h)
88
+ end
89
+ {
90
+ name: self.name,
91
+ total_probability: self.total_probability,
92
+ vig: self.vig,
93
+ odds: full_odds,
94
+ }
95
+ end
96
+
97
+ # JSON representation of a market.
98
+ # @return [String]
99
+ def to_json
100
+ to_h.to_json
101
+ end
102
+
103
+ private
104
+
105
+ # Hash of odds by name or identifier
106
+ #
107
+ # @return [Hash{String=>Odd}]
108
+ def odds_hash
109
+ @odds_hash ||= @odds.map { |o| [o.id, o] }.to_h.tap do |hash|
110
+ raise 'Must use unique odds identifiers' if hash.size != @odds.size # TODO create custom error classes
111
+ end
112
+ end
113
+
114
+ end
115
+ end
@@ -0,0 +1,60 @@
1
+ module Oddsmaker
2
+ # Odds represent a single value that can be bet on.
3
+ # This can be represented and converted between the various forms it can take:
4
+ #
5
+ # - Implied probability
6
+ # - Decimal odds
7
+ # - Fractional odds
8
+ # - American odds
9
+ #
10
+ module Odd
11
+
12
+ # Create an implied probability with the given value.
13
+ #
14
+ # @param value [Float, Integer] Probability value
15
+ # @param id [String] Identifier for this odd. Useful when multiple odds are used in a market
16
+ # @return [ImpliedProbability]
17
+ def self.implied(value, id = nil)
18
+ ImpliedProbability.new(value, id)
19
+ end
20
+
21
+ # Create American odds with the given value.
22
+ #
23
+ # @param value [String, Integer] Odds value
24
+ # @param id [String] Identifier for this odd. Useful when multiple odds are used in a market
25
+ # @return [American]
26
+ def self.american(value, id = nil)
27
+ American.new(value, id)
28
+ end
29
+
30
+ # Create decimal odds with the given value.
31
+ #
32
+ # @param value [Float, Integer] Odds value
33
+ # @param id [String] Identifier for this odd. Useful when multiple odds are used in a market
34
+ # @return [Decimal]
35
+ def self.decimal(value, id = nil)
36
+ Decimal.new(value, id)
37
+ end
38
+
39
+ # Create fractional odds with the given value.
40
+ #
41
+ # @param value [String, Integer] Odds value
42
+ # @param id [String] Identifier for this odd. Useful when multiple odds are used in a market
43
+ # @return [Fractional]
44
+ def self.fractional(value, id = nil)
45
+ Fractional.new(value, id)
46
+ end
47
+
48
+ # Shortcut to creating a new odd based off hash params (i.e. request params).
49
+ # Requires one of `%w[american decimal fraction implied]` keys.
50
+ # Also accepts param `'name'`.
51
+ #
52
+ # @return [Odd]
53
+ def self.new(params = {})
54
+ if (type = %w[american decimal fraction implied].detect { |type| params.key?(type) })
55
+ send(type, params[type], params['name'])
56
+ end
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,83 @@
1
+ module Oddsmaker
2
+ module Odd
3
+ # American Odds class, handling both positive and negative odds.
4
+ # A negative value expresses the dollar amount that would need to be wagered in order to win $100.
5
+ # A positive number expresses the dollar amount that would be won from a $100 wager, excluding the original wager.
6
+ class American < Base
7
+
8
+ def initialize(value, id = nil)
9
+ @id = id || value
10
+ @value = value.to_i
11
+ end
12
+
13
+ # Properly display both positive and negative odds.
14
+ #
15
+ # @return [String]
16
+ def to_s
17
+ @value.positive? ? "+#{@value}" : @value.to_s
18
+ end
19
+
20
+ # Returns self. This creates a consistent API for all odds.
21
+ #
22
+ # @return [self]
23
+ def american
24
+ self
25
+ end
26
+
27
+ # Convert to decimal odds, returning a new object.
28
+ #
29
+ # @return [Decimal]
30
+ def decimal
31
+ @decimal ||= implied_probability.decimal
32
+ end
33
+
34
+ # Convert to fractional odds, returning a new object.
35
+ #
36
+ # @return [Fractional]
37
+ def fractional
38
+ @fractional ||= Fractional.new(calculate_fractional, id)
39
+ end
40
+
41
+ private
42
+
43
+ # Allows for implied probability to create a vigged/unvigged odd and convert it back to source format.
44
+ def type
45
+ :american
46
+ end
47
+
48
+ # Calculate implied probability of an odd.
49
+ #
50
+ # @return [Float]
51
+ def calculate_probability
52
+ calculate_probability ||= if @value.positive?
53
+ 100.0 / (@value + 100)
54
+ else
55
+ -@value.to_f / (100 - @value)
56
+ end
57
+ end
58
+
59
+ # Calculate fractional representation of an odd.
60
+ #
61
+ # @return [Float]
62
+ def calculate_fractional
63
+ calculate_fractional ||= if @value.positive?
64
+ @value / 100.rationalize
65
+ else
66
+ -100 / @value.rationalize
67
+ end
68
+ end
69
+
70
+ # Calculate profit for a wager.
71
+ #
72
+ # @return [Float, Integer]
73
+ def calculate_profit(amount)
74
+ if @value.positive?
75
+ (amount/100.rationalize) * @value
76
+ else
77
+ -100/@value.rationalize * amount
78
+ end
79
+ end
80
+
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,100 @@
1
+ module Oddsmaker
2
+ module Odd
3
+ # @abstract Base class to implement common functionality to various types of odds.
4
+ class Base
5
+ include Comparable
6
+ attr_reader :id, :value
7
+ alias :name :id # name is more common scenario, but anything works as an identifier
8
+
9
+ # Display odds as a string
10
+ #
11
+ # @return [String]
12
+ def to_s
13
+ @value.to_s
14
+ end
15
+
16
+ # Calculates a new odd, removing the vig.
17
+ #
18
+ # @return [Odd] Returns the same class as the original object
19
+ def without_vig(total_probability)
20
+ @without_vig = implied_probability.without_vig(total_probability).send(type)
21
+ end
22
+
23
+ # Calculates a new odd, with the given overround percentage.
24
+ #
25
+ # @param v [Integer] Overround percentage (as whole number)
26
+ # @return [Odd] Returns the same class as the original object
27
+ def overround!(v)
28
+ implied_probability.overround!(v).send(type)
29
+ end
30
+
31
+ # Calculate implied probability.
32
+ #
33
+ # @return [ImpliedProbability]
34
+ def implied_probability
35
+ @implied_probability ||= ImpliedProbability.new(calculate_probability, id)
36
+ end
37
+
38
+ # Multiplier is commonly used for parlay and other calculations.
39
+ # It's just decimal odds, but we define it to match common terminology.
40
+ #
41
+ # @return [Float]
42
+ def multiplier
43
+ self.decimal.value
44
+ end
45
+
46
+ # Check two odds for equality.
47
+ #
48
+ # @param other [Odd]
49
+ # @return [Boolean]
50
+ def ==(other)
51
+ implied_probability == other.implied_probability
52
+ end
53
+
54
+ # Compare two odds against each other.
55
+ #
56
+ # @param other [Odd]
57
+ # @return [-1,0,1]
58
+ def <=>(other)
59
+ implied_probability <=> other.implied_probability
60
+ end
61
+
62
+ # Make a wager with this odd.
63
+ #
64
+ # @param amount [Integer, Float] Amount wagered. Can be integer or float.
65
+ # @return [Wager]
66
+ def wager(amount)
67
+ Wager.new(amount, self)
68
+ end
69
+
70
+ # Calculate profit for an amount wagered.
71
+ # Convenient if a `Wager` object is unnecessary.
72
+ #
73
+ # @param amount [Integer, Float] Amount wagered. Can be integer or float.
74
+ # @return [Float, Integer, Rational]
75
+ def profit(amount)
76
+ calculate_profit(amount)
77
+ end
78
+
79
+ # Represent odd as a hash.
80
+ # This will include all forms of the odd.
81
+ # @return [Hash]
82
+ def to_h
83
+ {
84
+ name: self.name,
85
+ american: self.american.value,
86
+ decimal: self.decimal.value,
87
+ fractional: self.fractional.value,
88
+ implied: self.implied_probability.value,
89
+ }
90
+ end
91
+
92
+ # JSON representation of the odd.
93
+ # @return [String]
94
+ def to_json
95
+ to_h.to_json
96
+ end
97
+
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,56 @@
1
+ module Oddsmaker
2
+ module Odd
3
+ # Decimal odds express the amount that would be returned from a $1 wager, including the original wager amount.
4
+ # Decimal odds will always be greater than 1.0.
5
+ class Decimal < Base
6
+
7
+ def initialize(value, id = nil)
8
+ @id = id || value
9
+ @value = value.to_f
10
+ end
11
+
12
+ # Returns self. This creates a consistent API for all odds.
13
+ #
14
+ # @return [self]
15
+ def decimal
16
+ self
17
+ end
18
+
19
+ # Convert to American odds, returning a new object.
20
+ #
21
+ # @return [American]
22
+ def american
23
+ @american ||= implied_probability.american
24
+ end
25
+
26
+ # Convert to fractional odds, returning a new object.
27
+ #
28
+ # @return [Fractional]
29
+ def fractional
30
+ @fractional ||= implied_probability.fractional
31
+ end
32
+
33
+ private
34
+
35
+ # Allows for implied probability to create a vigged/unvigged odd and convert it back to source format.
36
+ def type
37
+ :decimal
38
+ end
39
+
40
+ # Calculate implied probability of an odd.
41
+ #
42
+ # @return [Float]
43
+ def calculate_probability
44
+ 1.0 / @value
45
+ end
46
+
47
+ # Calculate profit for a wager.
48
+ #
49
+ # @return [Float, Integer]
50
+ def calculate_profit(amount)
51
+ (@value - 1) * amount
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,55 @@
1
+ module Oddsmaker
2
+ module Odd
3
+ # Fractional odds express the fraction of a dollar that would be won from a $1 bet.
4
+ class Fractional < Base
5
+
6
+ def initialize(value, id = nil)
7
+ @id = id || value
8
+ @value = value.is_a?(String) ? value.to_r : value.rationalize
9
+ end
10
+
11
+ # Returns self. This creates a consistent API for all odds.
12
+ #
13
+ # @return [self]
14
+ def fractional
15
+ self
16
+ end
17
+
18
+ # Convert to American odds, returning a new object.
19
+ #
20
+ # @return [American]
21
+ def american
22
+ @american ||= implied_probability.american
23
+ end
24
+
25
+ # Convert to decimal odds, returning a new object.
26
+ #
27
+ # @return [Decimal]
28
+ def decimal
29
+ @decimal ||= implied_probability.decimal
30
+ end
31
+
32
+ private
33
+
34
+ # Allows for implied probability to create a vigged/unvigged odd and convert it back to source format.
35
+ def type
36
+ :fractional
37
+ end
38
+
39
+ # Calculate implied probability of an odd.
40
+ #
41
+ # @return [Float]
42
+ def calculate_probability
43
+ @value.denominator.fdiv(@value.denominator + @value.numerator)
44
+ end
45
+
46
+ # Calculate profit for a wager.
47
+ #
48
+ # @return [Float, Integer]
49
+ def calculate_profit(amount)
50
+ @value * amount
51
+ end
52
+
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,125 @@
1
+ module Oddsmaker
2
+ module Odd
3
+ # Implied probability is the probability of an event, based on the odds for that event.
4
+ # Implied probability is what translates between various types of odds.
5
+ class ImpliedProbability < Base
6
+
7
+ def initialize(value, id = nil)
8
+ @id = id || value
9
+ @value = value >= 1 ? value.fdiv(100) : value
10
+ end
11
+
12
+ # Calculates a new odd, removing the vig.
13
+ #
14
+ # @return [ImpliedProbability]
15
+ def without_vig(total_probability)
16
+ self.class.new(@value / total_probability, id)
17
+ end
18
+
19
+ # Calculates a new odd, with the given overround percentage.
20
+ #
21
+ # @param v [Integer] Overround percentage (as whole number)
22
+ # @return [ImpliedProbability]
23
+ def overround!(v)
24
+ self.class.new(@value * ((100 + v)/100.rationalize), id)
25
+ end
26
+
27
+ # Convert probability to percent.
28
+ # Optionally round the resulting decimal.
29
+ #
30
+ # @param n [Integer] Number of decimal places for rounding.
31
+ def to_percent(n = nil)
32
+ n ? (@value * 100).round(n) : @value * 100
33
+ end
34
+
35
+ # Round decimal value.
36
+ #
37
+ # @param n [Integer] Number of decimal places for rounding.
38
+ def round(n = 2)
39
+ @value.round(n)
40
+ end
41
+
42
+ # Check two odds for equality.
43
+ #
44
+ # @param other [ImpliedProbability]
45
+ # @return [Boolean]
46
+ def ==(other)
47
+ if other.is_a?(self.class)
48
+ @value == other.value
49
+ else
50
+ @value == other.implied_probability.value
51
+ end
52
+ end
53
+
54
+ # Compare two odds against each other.
55
+ #
56
+ # @param other [ImpliedProbability]
57
+ # @return [-1,0,1]
58
+ def <=>(other)
59
+ value <=> other.value
60
+ end
61
+
62
+ # Returns self. This creates a consistent API for all odds.
63
+ #
64
+ # @return [self]
65
+ def implied_probability
66
+ self
67
+ end
68
+
69
+ # Convert to American odds, returning a new object.
70
+ #
71
+ # @return [American]
72
+ def american
73
+ @american ||= American.new(calculate_american, id)
74
+ end
75
+
76
+ # Convert to decimal odds, returning a new object.
77
+ #
78
+ # @return [Decimal]
79
+ def decimal
80
+ @decimal ||= Decimal.new(calculate_decimal, id)
81
+ end
82
+
83
+ # Convert to fractional odds, returning a new object.
84
+ #
85
+ # @return [Fractional]
86
+ def fractional
87
+ @fractional ||= Fractional.new(calculate_fractional, id)
88
+ end
89
+
90
+ private
91
+
92
+ # Allows for implied probability to create a vigged/unvigged odd and convert it back to source format.
93
+ def type
94
+ :implied_probability
95
+ end
96
+
97
+ # Calculate American odds value.
98
+ #
99
+ # @return [Integer, Float]
100
+ def calculate_american
101
+ value = @value.rationalize
102
+ if value >= 0.5
103
+ - ( value / (1 - value)) * 100
104
+ else
105
+ ((1 - value) / value ) * 100
106
+ end
107
+ end
108
+
109
+ # Calculate decimal odds value.
110
+ #
111
+ # @return [Float]
112
+ def calculate_decimal
113
+ 1.0 / @value
114
+ end
115
+
116
+ # Calculate fractional odds value.
117
+ #
118
+ # @return [Rational]
119
+ def calculate_fractional
120
+ (1.0 / @value - 1).rationalize
121
+ end
122
+
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,3 @@
1
+ module Oddsmaker
2
+ VERSION = "0.1.0" # :nodoc:
3
+ end
@@ -0,0 +1,44 @@
1
+ module Oddsmaker
2
+ # Wager represents an odd and a wagered amount.
3
+ # Odds can directly calculate their profit, so this is just a convenience class.
4
+ class Wager
5
+ attr_reader :amount, :odd
6
+
7
+ def initialize(amount, odd)
8
+ @amount = amount
9
+ @odd = odd
10
+ end
11
+
12
+ # Calculate profit for a wager.
13
+ #
14
+ # @return [Float, Integer]
15
+ def profit
16
+ @profit ||= odd.profit(@amount)
17
+ end
18
+
19
+ # Calculate return for a wager.
20
+ # Return is profit plus wager amount.
21
+ #
22
+ # @return [Float, Integer]
23
+ def return
24
+ @return ||= profit + @amount
25
+ end
26
+
27
+ # Hash representation of the wager.
28
+ # @return [Hash]
29
+ def to_h
30
+ {
31
+ amount: self.amount.to_f,
32
+ profit: self.profit.to_f,
33
+ return: self.return.to_f,
34
+ odd: odd.to_h,
35
+ }
36
+ end
37
+
38
+ # JSON representation of the wager.
39
+ # @return [String]
40
+ def to_json
41
+ to_h.to_json
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "oddsmaker/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "oddsmaker"
8
+ spec.version = Oddsmaker::VERSION
9
+ spec.authors = ["Paul Hoffer"]
10
+ spec.email = ["git@paulhoffer.com"]
11
+
12
+ spec.summary = %q{Calculate and convert sportsbook betting odds, probabilities, wagers, and collections of odds.}
13
+ spec.description = %q{Convert and manipulate sportsbook betting odds (American, decimal, fractional) and probabilities. Calculate wagering profit and return, and calculate total probability and vig for a collection of odds.}
14
+ spec.homepage = "https://www.github.com/phoffer/oddsmaker"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.15"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "minitest", "~> 5.0"
27
+ spec.add_development_dependency "simplecov"
28
+ spec.add_development_dependency "pry"
29
+ end
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: oddsmaker
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Paul Hoffer
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-10-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.15'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.15'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Convert and manipulate sportsbook betting odds (American, decimal, fractional)
84
+ and probabilities. Calculate wagering profit and return, and calculate total probability
85
+ and vig for a collection of odds.
86
+ email:
87
+ - git@paulhoffer.com
88
+ executables: []
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - ".gitignore"
93
+ - ".travis.yml"
94
+ - Gemfile
95
+ - LICENSE.txt
96
+ - README.md
97
+ - Rakefile
98
+ - bin/console
99
+ - bin/setup
100
+ - lib/oddsmaker.rb
101
+ - lib/oddsmaker/market.rb
102
+ - lib/oddsmaker/odd.rb
103
+ - lib/oddsmaker/odd/american.rb
104
+ - lib/oddsmaker/odd/base.rb
105
+ - lib/oddsmaker/odd/decimal.rb
106
+ - lib/oddsmaker/odd/fractional.rb
107
+ - lib/oddsmaker/odd/implied_probability.rb
108
+ - lib/oddsmaker/version.rb
109
+ - lib/oddsmaker/wager.rb
110
+ - oddsmaker.gemspec
111
+ homepage: https://www.github.com/phoffer/oddsmaker
112
+ licenses:
113
+ - MIT
114
+ metadata: {}
115
+ post_install_message:
116
+ rdoc_options: []
117
+ require_paths:
118
+ - lib
119
+ required_ruby_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ required_rubygems_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ requirements: []
130
+ rubyforge_project:
131
+ rubygems_version: 2.6.11
132
+ signing_key:
133
+ specification_version: 4
134
+ summary: Calculate and convert sportsbook betting odds, probabilities, wagers, and
135
+ collections of odds.
136
+ test_files: []