oddsmaker 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +99 -0
- data/Rakefile +10 -0
- data/bin/console +10 -0
- data/bin/setup +8 -0
- data/lib/oddsmaker.rb +17 -0
- data/lib/oddsmaker/market.rb +115 -0
- data/lib/oddsmaker/odd.rb +60 -0
- data/lib/oddsmaker/odd/american.rb +83 -0
- data/lib/oddsmaker/odd/base.rb +100 -0
- data/lib/oddsmaker/odd/decimal.rb +56 -0
- data/lib/oddsmaker/odd/fractional.rb +55 -0
- data/lib/oddsmaker/odd/implied_probability.rb +125 -0
- data/lib/oddsmaker/version.rb +3 -0
- data/lib/oddsmaker/wager.rb +44 -0
- data/oddsmaker.gemspec +29 -0
- metadata +136 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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).
|
data/Rakefile
ADDED
data/bin/console
ADDED
data/bin/setup
ADDED
data/lib/oddsmaker.rb
ADDED
@@ -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,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
|
data/oddsmaker.gemspec
ADDED
@@ -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: []
|