rodders 2.1.0 → 3.0.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.
- data/Gemfile +1 -1
- data/Gemfile.lock +9 -5
- data/VERSION +1 -1
- data/lib/fixed_odds.rb +24 -24
- data/lib/mutually_exclusive_collection.rb +87 -17
- data/rodders.gemspec +5 -5
- data/spec/fixed_odds_spec.rb +39 -39
- data/spec/mutually_exclusive_collection_spec.rb +116 -18
- data/spec/spec_helper.rb +1 -1
- metadata +16 -16
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -8,20 +8,24 @@ GEM
|
|
8
8
|
bundler (~> 1.0)
|
9
9
|
git (>= 1.2.5)
|
10
10
|
rake
|
11
|
-
json (1.6.
|
12
|
-
money (
|
11
|
+
json (1.6.6)
|
12
|
+
money (5.0.0)
|
13
13
|
i18n (~> 0.4)
|
14
14
|
json
|
15
|
+
multi_json (1.3.2)
|
15
16
|
rake (0.9.2.2)
|
16
|
-
rcov (0.9.11)
|
17
17
|
rspec (2.9.0)
|
18
18
|
rspec-core (~> 2.9.0)
|
19
19
|
rspec-expectations (~> 2.9.0)
|
20
20
|
rspec-mocks (~> 2.9.0)
|
21
21
|
rspec-core (2.9.0)
|
22
|
-
rspec-expectations (2.9.
|
22
|
+
rspec-expectations (2.9.1)
|
23
23
|
diff-lcs (~> 1.1.3)
|
24
24
|
rspec-mocks (2.9.0)
|
25
|
+
simplecov (0.6.1)
|
26
|
+
multi_json (~> 1.0)
|
27
|
+
simplecov-html (~> 0.5.3)
|
28
|
+
simplecov-html (0.5.3)
|
25
29
|
yard (0.6.8)
|
26
30
|
|
27
31
|
PLATFORMS
|
@@ -31,6 +35,6 @@ DEPENDENCIES
|
|
31
35
|
bundler (~> 1.0.0)
|
32
36
|
jeweler (~> 1.6.4)
|
33
37
|
money
|
34
|
-
rcov
|
35
38
|
rspec (~> 2.9.0)
|
39
|
+
simplecov
|
36
40
|
yard (~> 0.6.0)
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
3.0.0
|
data/lib/fixed_odds.rb
CHANGED
@@ -17,10 +17,10 @@ class FixedOdds
|
|
17
17
|
# @return [FixedOdds]
|
18
18
|
def FixedOdds.from_s(odds)
|
19
19
|
case
|
20
|
-
when
|
21
|
-
when
|
22
|
-
when
|
23
|
-
else
|
20
|
+
when fractional_odds?(odds) then fractional_odds odds
|
21
|
+
when moneyline_odds?(odds) then moneyline_odds odds
|
22
|
+
when decimal_odds?(odds) then decimal_odds odds
|
23
|
+
else raise ArgumentError, %{could not parse "#{odds}"}
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
@@ -58,10 +58,10 @@ class FixedOdds
|
|
58
58
|
# @return (see FixedOdds.from_s)
|
59
59
|
def FixedOdds.fractional_odds(fractional)
|
60
60
|
raise %{could not parse "#{fractional}" as fractional odds} unless self.fractional_odds?(fractional)
|
61
|
-
return new(
|
62
|
-
if /(?<numerator>\d+)(\/|-to-)(?<denominator>\d+)/ =~ fractional then r = Rational(
|
61
|
+
return new(1.to_r) if fractional == 'evens' || fractional == 'even money'
|
62
|
+
if /(?<numerator>\d+)(\/|-to-)(?<denominator>\d+)/ =~ fractional then r = Rational(numerator, denominator) end
|
63
63
|
r = r.reciprocal if fractional.end_with? ' on'
|
64
|
-
new
|
64
|
+
new r
|
65
65
|
end
|
66
66
|
|
67
67
|
# creates a new FixedOdds from a Rational
|
@@ -79,8 +79,8 @@ class FixedOdds
|
|
79
79
|
def FixedOdds.moneyline_odds(moneyline)
|
80
80
|
raise %{could not parse "#{moneyline}" as moneyline odds} unless self.moneyline_odds?(moneyline)
|
81
81
|
sign = moneyline[0]
|
82
|
-
if sign == '+' then new(Rational(
|
83
|
-
else new(Rational(
|
82
|
+
if sign == '+' then new(Rational(moneyline, 100))
|
83
|
+
else new(Rational(100, -moneyline.to_i))
|
84
84
|
end
|
85
85
|
end
|
86
86
|
|
@@ -91,30 +91,30 @@ class FixedOdds
|
|
91
91
|
# @return (see FixedOdds.from_s)
|
92
92
|
def FixedOdds.decimal_odds(decimal)
|
93
93
|
raise %{could not parse "#{decimal}" as decimal odds} unless self.decimal_odds?(decimal)
|
94
|
-
new(
|
94
|
+
new(decimal.to_r - 1)
|
95
95
|
end
|
96
96
|
|
97
|
-
# calculates the profit
|
97
|
+
# calculates the profit on a winning stake
|
98
98
|
# @param [Money] stake the stake
|
99
99
|
# @return [Money] the profit
|
100
|
-
def
|
100
|
+
def profit_on_stake(stake)
|
101
101
|
stake * @fractional_odds
|
102
102
|
end
|
103
103
|
|
104
|
-
# calculates the total return on a winning
|
105
|
-
# (
|
106
|
-
# @param (see #
|
107
|
-
# @return [Money] the total
|
108
|
-
def
|
109
|
-
|
104
|
+
# calculates the total return on a winning stake
|
105
|
+
# (the profit plus the stake)
|
106
|
+
# @param (see #profit_on_stake)
|
107
|
+
# @return [Money] the total returned
|
108
|
+
def total_return_on_stake(stake)
|
109
|
+
profit_on_stake(stake) + stake
|
110
110
|
end
|
111
111
|
|
112
|
-
# calculates the
|
112
|
+
# calculates the stake needed to
|
113
113
|
# win the specified amount in profit
|
114
|
-
# @param [Money]
|
114
|
+
# @param [Money] profit the desired profit
|
115
115
|
# @return [Money] the stake required to realise that profit on a winning bet
|
116
|
-
def
|
117
|
-
|
116
|
+
def stake_to_profit profit
|
117
|
+
profit / @fractional_odds
|
118
118
|
end
|
119
119
|
|
120
120
|
# string representation in fractional form like '4/1'
|
@@ -134,10 +134,10 @@ class FixedOdds
|
|
134
134
|
def to_s_moneyline
|
135
135
|
integral_number_with_sign_regex = '%+d'
|
136
136
|
|
137
|
-
if @fractional_odds > 1
|
137
|
+
if @fractional_odds > 1
|
138
138
|
integral_number_with_sign_regex % (fractional_odds * 100).to_i
|
139
139
|
else
|
140
|
-
integral_number_with_sign_regex % (-100
|
140
|
+
integral_number_with_sign_regex % (-100 / fractional_odds)
|
141
141
|
end
|
142
142
|
end
|
143
143
|
|
@@ -1,32 +1,102 @@
|
|
1
|
-
# A collection of mutually exclusive
|
1
|
+
# A collection of mutually exclusive odds for different outcomes.
|
2
|
+
# Includes methods for sports betting arbitrage. Due to the discrete nature
|
3
|
+
# of real-world monetary amounts, there may be small rounding errors.
|
2
4
|
class MutuallyExclusiveCollection
|
3
|
-
# create a new collection with the given
|
4
|
-
# @param [Array<FixedOdds>]
|
5
|
-
def initialize(
|
6
|
-
@
|
5
|
+
# create a new collection with the given odds
|
6
|
+
# @param [Array<FixedOdds>] mutually_exclusive_outcome_odds the odds for all the mutually exclusive outcomes
|
7
|
+
def initialize(mutually_exclusive_outcome_odds)
|
8
|
+
@mutually_exclusive_outcome_odds = mutually_exclusive_outcome_odds.sort
|
7
9
|
end
|
8
10
|
|
9
|
-
# the least likely of the
|
10
|
-
# @return [FixedOdds] the least likely
|
11
|
+
# the least likely of the odds to occur
|
12
|
+
# @return [FixedOdds] the least likely odd
|
11
13
|
def least_likely
|
12
|
-
@
|
14
|
+
@mutually_exclusive_outcome_odds.first
|
13
15
|
end
|
14
16
|
|
15
|
-
# the most likely of the
|
16
|
-
# @return [FixedOdds] the most likely
|
17
|
+
# the most likely of the odds to occur
|
18
|
+
# @return [FixedOdds] the most likely odd
|
17
19
|
def most_likely
|
18
|
-
@
|
20
|
+
@mutually_exclusive_outcome_odds.last
|
19
21
|
end
|
20
22
|
|
21
|
-
# the
|
22
|
-
# @return [Array<FixedOdds>]
|
23
|
+
# the odds in ascending order of probability
|
24
|
+
# @return [Array<FixedOdds>] odds in ascending probability
|
23
25
|
def in_ascending_probability
|
24
|
-
@
|
26
|
+
@mutually_exclusive_outcome_odds
|
25
27
|
end
|
26
28
|
|
27
|
-
# the
|
28
|
-
# @return [Array<FixedOdds>]
|
29
|
+
# the odds in descending order of probability
|
30
|
+
# @return [Array<FixedOdds>] odds in descending probability
|
29
31
|
def in_descending_probability
|
30
|
-
@
|
32
|
+
@mutually_exclusive_outcome_odds.reverse
|
31
33
|
end
|
34
|
+
|
35
|
+
# tells if arbitrage is possible for a collection of odds
|
36
|
+
# @return [Boolean] true if profit can be made regardless
|
37
|
+
# of the outcome, false otherwise
|
38
|
+
def arbitrageable?
|
39
|
+
sum_inverse_outcome < 1
|
40
|
+
end
|
41
|
+
|
42
|
+
# the bookmaker's return rate
|
43
|
+
# @return [Number] the bookmaker's return rate as a percentage
|
44
|
+
def bookmakers_return_rate
|
45
|
+
fs = fractions
|
46
|
+
1 - fs.reduce(:*) / fs.reduce(:+)
|
47
|
+
end
|
48
|
+
|
49
|
+
# hash of the odds and what percentage of the total stake should go on each
|
50
|
+
# @return [Hash<FixedOdds, Number>] hash of odds to percentages
|
51
|
+
def percentages
|
52
|
+
hash = {}
|
53
|
+
@mutually_exclusive_outcome_odds.each {|odds| hash[odds] = 1 / odds.fractional_odds / sum_inverse_outcome }
|
54
|
+
hash
|
55
|
+
end
|
56
|
+
|
57
|
+
# hash of the odds and what stakes to put on each given a total stake
|
58
|
+
# @param [Money] total_stake the money to distribute on the outcomes
|
59
|
+
# @return [Hash<FixedOdds, Money>] hash of odds to stakes
|
60
|
+
def stakes_for_total_stake total_stake
|
61
|
+
hash = {}
|
62
|
+
@mutually_exclusive_outcome_odds.each {|odds| hash[odds] = total_stake / odds.fractional_odds / sum_inverse_outcome }
|
63
|
+
hash
|
64
|
+
end
|
65
|
+
|
66
|
+
# hash of the odds and the stakes needed to make the specified profit
|
67
|
+
# @param (see #stakes_for_total_stake)
|
68
|
+
# @return (see #stakes_for_total_stake)
|
69
|
+
def stakes_for_profit desired_profit
|
70
|
+
stakes_for_total_stake(stake_to_profit(desired_profit))
|
71
|
+
end
|
72
|
+
|
73
|
+
# the stake needed to win the desired profit
|
74
|
+
# @param [Money] desired_profit the profit to gain from arbitrage
|
75
|
+
# @return [Money] the stake necessary to realise the desired profit
|
76
|
+
def stake_to_profit desired_profit
|
77
|
+
desired_profit / profit_percentage
|
78
|
+
end
|
79
|
+
|
80
|
+
# the profit won given the total stake to distribute
|
81
|
+
# @param (see #stakes_for_total_stake)
|
82
|
+
# @return [Money] the profit that can be made from a total stake
|
83
|
+
def profit_on_stake total_stake
|
84
|
+
total_stake * profit_percentage
|
85
|
+
end
|
86
|
+
|
87
|
+
# profit percentage available on this arb
|
88
|
+
# @return [Number] the percentage profit available on the arb
|
89
|
+
def profit_percentage
|
90
|
+
sum = sum_inverse_outcome
|
91
|
+
(1 - sum) / sum
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
def sum_inverse_outcome
|
96
|
+
fractions.reduce(0) {|sum, n| sum + Rational(1, n) }
|
97
|
+
end
|
98
|
+
|
99
|
+
def fractions
|
100
|
+
@mutually_exclusive_outcome_odds.collect {|o| o.fractional_odds }
|
101
|
+
end
|
32
102
|
end
|
data/rodders.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "rodders"
|
8
|
-
s.version = "
|
8
|
+
s.version = "3.0.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Nigel Lowry"]
|
12
|
-
s.date = "2012-
|
12
|
+
s.date = "2012-04-25"
|
13
13
|
s.description = "Converts between fractional, decimal and moneyline odds for betting and gambling. Calculates how much can be won on a given stake."
|
14
14
|
s.email = "nigel-lowry@ultra.eclipse.co.uk"
|
15
15
|
s.extra_rdoc_files = [
|
@@ -48,14 +48,14 @@ Gem::Specification.new do |s|
|
|
48
48
|
s.add_development_dependency(%q<yard>, ["~> 0.6.0"])
|
49
49
|
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
50
50
|
s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
|
51
|
-
s.add_development_dependency(%q<
|
51
|
+
s.add_development_dependency(%q<simplecov>, [">= 0"])
|
52
52
|
else
|
53
53
|
s.add_dependency(%q<money>, [">= 0"])
|
54
54
|
s.add_dependency(%q<rspec>, ["~> 2.9.0"])
|
55
55
|
s.add_dependency(%q<yard>, ["~> 0.6.0"])
|
56
56
|
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
57
57
|
s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
|
58
|
-
s.add_dependency(%q<
|
58
|
+
s.add_dependency(%q<simplecov>, [">= 0"])
|
59
59
|
end
|
60
60
|
else
|
61
61
|
s.add_dependency(%q<money>, [">= 0"])
|
@@ -63,7 +63,7 @@ Gem::Specification.new do |s|
|
|
63
63
|
s.add_dependency(%q<yard>, ["~> 0.6.0"])
|
64
64
|
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
65
65
|
s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
|
66
|
-
s.add_dependency(%q<
|
66
|
+
s.add_dependency(%q<simplecov>, [">= 0"])
|
67
67
|
end
|
68
68
|
end
|
69
69
|
|
data/spec/fixed_odds_spec.rb
CHANGED
@@ -41,15 +41,15 @@ describe "FixedOdds" do
|
|
41
41
|
FixedOdds.fractional_odds('even money').should == FixedOdds.fractional_odds('1/1')
|
42
42
|
end
|
43
43
|
|
44
|
-
it "
|
44
|
+
it "treats '4-to-1' as '4/1'" do
|
45
45
|
FixedOdds.fractional_odds('4-to-1').should == FixedOdds.fractional_odds('4/1')
|
46
46
|
end
|
47
47
|
|
48
|
-
it "
|
48
|
+
it "treats '4-to-1 against' as '4/1'" do
|
49
49
|
FixedOdds.fractional_odds('4-to-1 against').should == FixedOdds.fractional_odds('4/1')
|
50
50
|
end
|
51
51
|
|
52
|
-
it "
|
52
|
+
it "treats '4-to-1 on' as '1/4'" do
|
53
53
|
FixedOdds.fractional_odds('4-to-1 on').should == FixedOdds.fractional_odds('1/4')
|
54
54
|
end
|
55
55
|
|
@@ -103,29 +103,29 @@ describe "FixedOdds" do
|
|
103
103
|
describe "positive figures" do
|
104
104
|
it "treats '+400' as meaning winning £400 on a £100 bet" do
|
105
105
|
plus400 = FixedOdds.moneyline_odds('+400')
|
106
|
-
plus400.
|
106
|
+
plus400.profit_on_stake(Money.parse '£100').should == Money.parse('£400')
|
107
107
|
end
|
108
108
|
|
109
109
|
it "treats +100 as meaning winning £100 on a £100 bet" do
|
110
110
|
plus100 = FixedOdds.moneyline_odds('+100')
|
111
|
-
plus100.
|
111
|
+
plus100.profit_on_stake(Money.parse '£100').should == Money.parse('£100')
|
112
112
|
end
|
113
113
|
end
|
114
114
|
|
115
115
|
describe "negative figures" do
|
116
116
|
it "treats '-400' as meaning you need to wager £400 to win £100" do
|
117
117
|
minus400 = FixedOdds.moneyline_odds('-400')
|
118
|
-
minus400.
|
118
|
+
minus400.profit_on_stake(Money.parse '£400').should == Money.parse('£100')
|
119
119
|
end
|
120
120
|
|
121
121
|
it "treats '-100' as meaning you need to wager £100 to win £100" do
|
122
122
|
minus100 = FixedOdds.moneyline_odds('-100')
|
123
|
-
minus100.
|
123
|
+
minus100.profit_on_stake(Money.parse '£100').should == Money.parse('£100')
|
124
124
|
end
|
125
125
|
|
126
126
|
it "treats '+100' as meaning you need to wager £100 to win £100" do
|
127
127
|
plus100 = FixedOdds.moneyline_odds('+100')
|
128
|
-
plus100.
|
128
|
+
plus100.profit_on_stake(Money.parse '£100').should == Money.parse('£100')
|
129
129
|
end
|
130
130
|
end
|
131
131
|
end
|
@@ -140,19 +140,19 @@ describe "FixedOdds" do
|
|
140
140
|
)
|
141
141
|
end
|
142
142
|
|
143
|
-
it "treats '2' as meaning you have to wager £
|
144
|
-
d2 = FixedOdds.decimal_odds
|
145
|
-
d2.
|
143
|
+
it "treats '2' as meaning you have to wager £1 to win £1" do
|
144
|
+
d2 = FixedOdds.decimal_odds '2'
|
145
|
+
d2.profit_on_stake(Money.parse '£1').should == Money.parse('£1')
|
146
146
|
end
|
147
147
|
|
148
|
-
it "treats '5' as meaning you have to wager £
|
149
|
-
d5 = FixedOdds.decimal_odds
|
150
|
-
d5.
|
148
|
+
it "treats '5' as meaning you have to wager £1 to win £4" do
|
149
|
+
d5 = FixedOdds.decimal_odds '5'
|
150
|
+
d5.profit_on_stake(Money.parse '£1').should == Money.parse('£4')
|
151
151
|
end
|
152
152
|
|
153
|
-
it "treats '1.25' as meaning
|
154
|
-
d1_25 = FixedOdds.decimal_odds
|
155
|
-
d1_25.
|
153
|
+
it "treats '1.25' as meaning you have to wager £4 to win £1" do
|
154
|
+
d1_25 = FixedOdds.decimal_odds '1.25'
|
155
|
+
d1_25.profit_on_stake(Money.parse '£4').should == Money.parse('£1')
|
156
156
|
end
|
157
157
|
end
|
158
158
|
|
@@ -160,7 +160,7 @@ describe "FixedOdds" do
|
|
160
160
|
describe "bad input" do
|
161
161
|
it "rejects garbage" do
|
162
162
|
expect {
|
163
|
-
FixedOdds.from_s
|
163
|
+
FixedOdds.from_s 'garbage'
|
164
164
|
}.to raise_error(
|
165
165
|
ArgumentError,
|
166
166
|
/could not parse "garbage"/
|
@@ -170,7 +170,7 @@ describe "FixedOdds" do
|
|
170
170
|
|
171
171
|
it "rejects an empty string" do
|
172
172
|
expect {
|
173
|
-
FixedOdds.from_s
|
173
|
+
FixedOdds.from_s ''
|
174
174
|
}.to raise_error(
|
175
175
|
ArgumentError,
|
176
176
|
/could not parse ""/
|
@@ -212,7 +212,7 @@ describe "FixedOdds" do
|
|
212
212
|
|
213
213
|
it "raises an error for a zero denominator" do
|
214
214
|
expect {
|
215
|
-
FixedOdds.from_s
|
215
|
+
FixedOdds.from_s '4/0'
|
216
216
|
}.to raise_error(
|
217
217
|
ZeroDivisionError
|
218
218
|
)
|
@@ -231,7 +231,7 @@ describe "FixedOdds" do
|
|
231
231
|
|
232
232
|
describe "decimal odds" do
|
233
233
|
it "parses integral odds of '2' as decimal odds, not as fractional odds of '2/1'" do
|
234
|
-
decimal_odds_2 = FixedOdds.from_s
|
234
|
+
decimal_odds_2 = FixedOdds.from_s '2'
|
235
235
|
|
236
236
|
decimal_odds_2.should == FixedOdds.decimal_odds('2')
|
237
237
|
decimal_odds_2.should_not == FixedOdds.fractional_odds('2/1')
|
@@ -353,49 +353,49 @@ describe "FixedOdds" do
|
|
353
353
|
end
|
354
354
|
end
|
355
355
|
|
356
|
-
describe "#
|
357
|
-
it "is £
|
356
|
+
describe "#profit_on_stake" do
|
357
|
+
it "is £4 on a £1 stake on a 4/1 bet" do
|
358
358
|
fourToOne = FixedOdds.fractional_odds '4/1'
|
359
|
-
fourToOne.
|
359
|
+
fourToOne.profit_on_stake(Money.parse '£1').should == Money.parse('£4')
|
360
360
|
end
|
361
361
|
|
362
|
-
it "is £25 on a £
|
362
|
+
it "is £0.25 on a £1 stake with a 1/4 bet" do
|
363
363
|
oneToFour = FixedOdds.fractional_odds '1/4'
|
364
|
-
oneToFour.
|
364
|
+
oneToFour.profit_on_stake(Money.parse '£1').should == Money.parse('£0.25')
|
365
365
|
end
|
366
366
|
end
|
367
367
|
|
368
|
-
describe "#
|
369
|
-
it "is £
|
368
|
+
describe "#total_return_on_stake" do
|
369
|
+
it "is £5 on a winning 4/1 bet with a £1 stake" do
|
370
370
|
fourToOne = FixedOdds.fractional_odds '4/1'
|
371
|
-
fourToOne.
|
371
|
+
fourToOne.total_return_on_stake(Money.parse '£1').should == Money.parse('£5')
|
372
372
|
end
|
373
373
|
|
374
374
|
it "is £125 on a winning 1/4 bet with a £100 stake" do
|
375
375
|
oneToFour = FixedOdds.fractional_odds '1/4'
|
376
|
-
oneToFour.
|
376
|
+
oneToFour.total_return_on_stake(Money.parse '£100').should == Money.parse('£125')
|
377
377
|
end
|
378
378
|
end
|
379
379
|
|
380
|
-
describe "#
|
380
|
+
describe "#stake_to_profit" do
|
381
381
|
it "is £1 on a 1/1 to win £1" do
|
382
382
|
oneToOne = FixedOdds.fractional_odds '1/1'
|
383
|
-
oneToOne.
|
383
|
+
oneToOne.stake_to_profit(Money.parse '£1').should == Money.parse('£1')
|
384
384
|
end
|
385
385
|
|
386
|
-
it "is £
|
387
|
-
fourToOne = FixedOdds.fractional_odds '
|
388
|
-
fourToOne.
|
386
|
+
it "is £1 on 2/1 to win £2" do
|
387
|
+
fourToOne = FixedOdds.fractional_odds '2/1'
|
388
|
+
fourToOne.stake_to_profit(Money.parse '£2').should == Money.parse('£1')
|
389
389
|
end
|
390
390
|
end
|
391
391
|
|
392
392
|
describe "object comparison" do
|
393
|
-
it "'+
|
394
|
-
FixedOdds.from_s('+
|
393
|
+
it "'+200' is less likely than '-200'" do
|
394
|
+
FixedOdds.from_s('+200').should be < FixedOdds.from_s('-200')
|
395
395
|
end
|
396
396
|
|
397
|
-
it "'-
|
398
|
-
FixedOdds.from_s('-
|
397
|
+
it "'-200' is more likely than '+200'" do
|
398
|
+
FixedOdds.from_s('-200').should be > FixedOdds.from_s('+200')
|
399
399
|
end
|
400
400
|
end
|
401
401
|
end
|
@@ -1,36 +1,134 @@
|
|
1
|
+
# coding: utf-8
|
1
2
|
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
3
|
|
3
4
|
describe "MutuallyExclusiveCollection" do
|
4
|
-
|
5
|
-
before(:each) do
|
6
|
-
@good_team = FixedOdds.from_s '-275'
|
7
|
-
@draw = FixedOdds.from_s '+429'
|
8
|
-
@bad_team = FixedOdds.from_s '+915'
|
9
5
|
|
10
|
-
|
6
|
+
context "non-arbitrage methods" do
|
7
|
+
before(:each) do
|
8
|
+
@good_team = FixedOdds.from_s '-275'
|
9
|
+
@draw = FixedOdds.from_s '+429'
|
10
|
+
@bad_team = FixedOdds.from_s '+915'
|
11
|
+
|
12
|
+
@events = MutuallyExclusiveCollection.new [@draw, @bad_team, @good_team]
|
13
|
+
end
|
14
|
+
|
15
|
+
subject { @events }
|
16
|
+
|
17
|
+
its(:most_likely) { should == @good_team }
|
18
|
+
its(:least_likely) { should == @bad_team }
|
19
|
+
its(:in_descending_probability) { should == [@good_team, @draw, @bad_team] }
|
20
|
+
its(:in_ascending_probability) { should == [@bad_team, @draw, @good_team] }
|
11
21
|
end
|
12
22
|
|
13
|
-
|
14
|
-
|
15
|
-
@
|
23
|
+
context "decimal odds arbitrage" do
|
24
|
+
before(:each) do
|
25
|
+
@bookmaker1outcome1 = FixedOdds.from_s '2.25'
|
26
|
+
@bookmaker1outcome2 = FixedOdds.from_s '4.9'
|
27
|
+
@bookmaker2outcome1 = FixedOdds.from_s '2.43'
|
28
|
+
@bookmaker2outcome2 = FixedOdds.from_s '3.85'
|
29
|
+
|
30
|
+
@bookmaker1 = MutuallyExclusiveCollection.new [@bookmaker1outcome1, @bookmaker1outcome2]
|
31
|
+
@bookmaker2 = MutuallyExclusiveCollection.new [@bookmaker2outcome1, @bookmaker2outcome2]
|
32
|
+
|
33
|
+
@bookmaker_vulnerable_to_arbitrage = MutuallyExclusiveCollection.new [@bookmaker2outcome1, @bookmaker1outcome2]
|
16
34
|
end
|
35
|
+
|
36
|
+
specify { @bookmaker1.should_not be_arbitrageable }
|
37
|
+
specify { @bookmaker2.should_not be_arbitrageable }
|
38
|
+
specify { @bookmaker_vulnerable_to_arbitrage.should be_arbitrageable }
|
39
|
+
|
40
|
+
specify { @bookmaker1.bookmakers_return_rate.should be_within(0.0001).of(0.0534) }
|
41
|
+
specify { @bookmaker2.bookmakers_return_rate.should be_within(0.0001).of(0.0478) }
|
42
|
+
|
43
|
+
specify { @bookmaker_vulnerable_to_arbitrage.profit_on_stake(Money.parse '£100').should == Money.parse('£4.63') }
|
44
|
+
specify { @bookmaker_vulnerable_to_arbitrage.profit_percentage.should be_within(0.001).of(0.046) }
|
17
45
|
end
|
18
46
|
|
19
|
-
|
20
|
-
|
21
|
-
@
|
47
|
+
context "fractional odds arbitrage" do
|
48
|
+
before(:each) do
|
49
|
+
@odds1 = FixedOdds.from_s '2/1'
|
50
|
+
@odds2 = FixedOdds.from_s '3/1'
|
51
|
+
|
52
|
+
@bookmaker_vulnerable_to_arbitrage = MutuallyExclusiveCollection.new [@odds1, @odds2]
|
22
53
|
end
|
54
|
+
|
55
|
+
subject { @bookmaker_vulnerable_to_arbitrage }
|
56
|
+
|
57
|
+
specify { @bookmaker_vulnerable_to_arbitrage.should be_arbitrageable }
|
58
|
+
its(:profit_percentage) { should be_within(0.001).of(0.2) }
|
59
|
+
specify { @bookmaker_vulnerable_to_arbitrage.profit_on_stake(Money.parse '£500').should == Money.parse('£100') }
|
23
60
|
end
|
24
61
|
|
25
|
-
|
26
|
-
|
27
|
-
@
|
62
|
+
context "more than two mutually exclusive events" do
|
63
|
+
before(:each) do
|
64
|
+
@odds1 = FixedOdds.from_s '2.3'
|
65
|
+
@odds2 = FixedOdds.from_s '8.0'
|
66
|
+
@odds3 = FixedOdds.from_s '18.0'
|
67
|
+
|
68
|
+
@bookmaker_vulnerable_to_arbitrage = MutuallyExclusiveCollection.new [@odds1, @odds2, @odds3]
|
28
69
|
end
|
70
|
+
|
71
|
+
subject { @bookmaker_vulnerable_to_arbitrage }
|
72
|
+
|
73
|
+
specify { @bookmaker_vulnerable_to_arbitrage.should be_arbitrageable }
|
74
|
+
its(:profit_percentage) { should be_within(0.0000001).of(0.02996) }
|
75
|
+
|
76
|
+
describe "#percentages" do
|
77
|
+
it "gives the percentages to put on each bet" do
|
78
|
+
percentages = @bookmaker_vulnerable_to_arbitrage.percentages
|
79
|
+
percentages.should have(3).items
|
80
|
+
percentages[@odds1].should be_within(0.0001).of(0.7922)
|
81
|
+
percentages[@odds2].should be_within(0.0001).of(0.1472)
|
82
|
+
percentages[@odds3].should be_within(0.0001).of(0.0606)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe "#stakes_for_total_stake" do
|
87
|
+
it "gives the right amounts" do
|
88
|
+
total = Money.parse '£500'
|
89
|
+
amounts = @bookmaker_vulnerable_to_arbitrage.stakes_for_total_stake total
|
90
|
+
amounts.should have(3).items
|
91
|
+
amounts[@odds1].should == Money.parse('£396.14')
|
92
|
+
amounts[@odds2].should == Money.parse('£73.57')
|
93
|
+
amounts[@odds3].should == Money.parse('£30.29')
|
94
|
+
amounts.values.reduce(:+).should == total
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
specify { @bookmaker_vulnerable_to_arbitrage.profit_on_stake(Money.parse '£500').should == Money.parse('£14.98') }
|
99
|
+
|
100
|
+
describe "#stakes_for_profit" do
|
101
|
+
it "gives the right amounts" do
|
102
|
+
amounts = @bookmaker_vulnerable_to_arbitrage.stakes_for_profit Money.parse '£750'
|
103
|
+
amounts.should have(3).items
|
104
|
+
amounts[@odds1].should == Money.parse('£19833.33')
|
105
|
+
amounts[@odds2].should == Money.parse('£3683.33')
|
106
|
+
amounts[@odds3].should == Money.parse('£1516.67')
|
107
|
+
amounts.values.reduce(:+).should == Money.parse('£25033.33')
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
specify { @bookmaker_vulnerable_to_arbitrage.stake_to_profit(Money.parse '£750').should == Money.parse('£25033.33') }
|
29
112
|
end
|
30
113
|
|
31
|
-
|
32
|
-
|
33
|
-
@
|
114
|
+
context "different events with same odds" do
|
115
|
+
before(:each) do
|
116
|
+
@odds1 = FixedOdds.from_s '15/1'
|
117
|
+
@odds2 = FixedOdds.from_s '15/1'
|
118
|
+
|
119
|
+
@bookmaker_vulnerable_to_arbitrage = MutuallyExclusiveCollection.new [@odds1, @odds2]
|
120
|
+
end
|
121
|
+
|
122
|
+
specify { @bookmaker_vulnerable_to_arbitrage.should be_arbitrageable }
|
123
|
+
specify { @bookmaker_vulnerable_to_arbitrage.profit_on_stake(Money.parse '£100').should == Money.parse('£650') }
|
124
|
+
|
125
|
+
describe "#percentages" do
|
126
|
+
it "gives the percentages to put on each bet" do
|
127
|
+
percentages = @bookmaker_vulnerable_to_arbitrage.percentages
|
128
|
+
percentages.should have(2).items
|
129
|
+
percentages[@odds1].should be_within(0.0001).of(0.5)
|
130
|
+
percentages[@odds2].should be_within(0.0001).of(0.5)
|
131
|
+
end
|
34
132
|
end
|
35
133
|
end
|
36
134
|
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rodders
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-04-25 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: money
|
16
|
-
requirement: &
|
16
|
+
requirement: &70206634864520 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70206634864520
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: rspec
|
27
|
-
requirement: &
|
27
|
+
requirement: &70206634864040 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ~>
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: 2.9.0
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70206634864040
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: yard
|
38
|
-
requirement: &
|
38
|
+
requirement: &70206634880700 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ~>
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: 0.6.0
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70206634880700
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: bundler
|
49
|
-
requirement: &
|
49
|
+
requirement: &70206634880220 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ~>
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: 1.0.0
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70206634880220
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: jeweler
|
60
|
-
requirement: &
|
60
|
+
requirement: &70206634879740 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ~>
|
@@ -65,10 +65,10 @@ dependencies:
|
|
65
65
|
version: 1.6.4
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *70206634879740
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
71
|
-
requirement: &
|
70
|
+
name: simplecov
|
71
|
+
requirement: &70206634879260 !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
74
74
|
- - ! '>='
|
@@ -76,7 +76,7 @@ dependencies:
|
|
76
76
|
version: '0'
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
|
-
version_requirements: *
|
79
|
+
version_requirements: *70206634879260
|
80
80
|
description: Converts between fractional, decimal and moneyline odds for betting and
|
81
81
|
gambling. Calculates how much can be won on a given stake.
|
82
82
|
email: nigel-lowry@ultra.eclipse.co.uk
|
@@ -116,7 +116,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
116
116
|
version: '0'
|
117
117
|
segments:
|
118
118
|
- 0
|
119
|
-
hash: -
|
119
|
+
hash: -4185691699245948853
|
120
120
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
121
121
|
none: false
|
122
122
|
requirements:
|