rodders 2.1.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -12,5 +12,5 @@ group :development do
12
12
  gem "yard", "~> 0.6.0"
13
13
  gem "bundler", "~> 1.0.0"
14
14
  gem "jeweler", "~> 1.6.4"
15
- gem "rcov", ">= 0"
15
+ gem "simplecov"
16
16
  end
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.1)
12
- money (4.0.1)
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.0)
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
- 2.1.0
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 self.fractional_odds?(odds) then self.fractional_odds odds
21
- when self.moneyline_odds?(odds) then self.moneyline_odds odds
22
- when self.decimal_odds?(odds) then self.decimal_odds odds
23
- else raise ArgumentError, %{could not parse "#{odds}"}
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(Rational('1/1')) if fractional == 'evens' || fractional == 'even money'
62
- if /(?<numerator>\d+)(\/|-to-)(?<denominator>\d+)/ =~ fractional then r = Rational("#{numerator}/#{denominator}") end
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(Rational(r))
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("#{moneyline}/100"))
83
- else new(Rational("100/#{moneyline.to_i.magnitude}"))
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(Rational(decimal.to_f - 1))
94
+ new(decimal.to_r - 1)
95
95
  end
96
96
 
97
- # calculates the profit won on a winning bet
97
+ # calculates the profit on a winning stake
98
98
  # @param [Money] stake the stake
99
99
  # @return [Money] the profit
100
- def profit_on_winning_stake(stake)
100
+ def profit_on_stake(stake)
101
101
  stake * @fractional_odds
102
102
  end
103
103
 
104
- # calculates the total return on a winning bet
105
- # (which is the profit plus the initial stake)
106
- # @param (see #profit_on_winning_stake)
107
- # @return [Money] the total winnings
108
- def total_return_on_winning_stake(stake)
109
- profit_on_winning_stake(stake) + stake
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 magnitude of the stake needed to
112
+ # calculates the stake needed to
113
113
  # win the specified amount in profit
114
- # @param [Money] win the desired profit
114
+ # @param [Money] profit the desired profit
115
115
  # @return [Money] the stake required to realise that profit on a winning bet
116
- def stake_needed_to_win win
117
- win / @fractional_odds
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.0
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.0 / fractional_odds)
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 events.
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 events
4
- # @param [Array<FixedOdds>] events the events
5
- def initialize(events)
6
- @events = events.sort
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 events to occur
10
- # @return [FixedOdds] the least likely event
11
+ # the least likely of the odds to occur
12
+ # @return [FixedOdds] the least likely odd
11
13
  def least_likely
12
- @events.first
14
+ @mutually_exclusive_outcome_odds.first
13
15
  end
14
16
 
15
- # the most likely of the events to occur
16
- # @return [FixedOdds] the most likely event
17
+ # the most likely of the odds to occur
18
+ # @return [FixedOdds] the most likely odd
17
19
  def most_likely
18
- @events.last
20
+ @mutually_exclusive_outcome_odds.last
19
21
  end
20
22
 
21
- # the events in ascending order of probability
22
- # @return [Array<FixedOdds>] events in ascending probability
23
+ # the odds in ascending order of probability
24
+ # @return [Array<FixedOdds>] odds in ascending probability
23
25
  def in_ascending_probability
24
- @events
26
+ @mutually_exclusive_outcome_odds
25
27
  end
26
28
 
27
- # the events in descending order of probability
28
- # @return [Array<FixedOdds>] events in descending probability
29
+ # the odds in descending order of probability
30
+ # @return [Array<FixedOdds>] odds in descending probability
29
31
  def in_descending_probability
30
- @events.reverse
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 = "2.1.0"
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-03-21"
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<rcov>, [">= 0"])
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<rcov>, [">= 0"])
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<rcov>, [">= 0"])
66
+ s.add_dependency(%q<simplecov>, [">= 0"])
67
67
  end
68
68
  end
69
69
 
@@ -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 "recognises '4-to-1' as '4/1'" do
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 "recognises '4-to-1 against' as '4/1'" do
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 "recognises '4-to-1 on' as '1/4'" do
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.profit_on_winning_stake(Money.from_fixnum(100, :GBP)).should == Money.from_fixnum(400, :GBP)
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.profit_on_winning_stake(Money.from_fixnum(100, :GBP)).should == Money.from_fixnum(100, :GBP)
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.profit_on_winning_stake(Money.from_fixnum(400, :GBP)).should == Money.from_fixnum(100, :GBP)
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.profit_on_winning_stake(Money.from_fixnum(100, :GBP)).should == Money.from_fixnum(100, :GBP)
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.profit_on_winning_stake(Money.from_fixnum(100, :GBP)).should == Money.from_fixnum(100, :GBP)
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 £100 to win £100" do
144
- d2 = FixedOdds.decimal_odds('2')
145
- d2.profit_on_winning_stake(Money.from_fixnum(100, :GBP)).should == Money.from_fixnum(100, :GBP)
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 £100 to win £400" do
149
- d5 = FixedOdds.decimal_odds('5')
150
- d5.profit_on_winning_stake(Money.from_fixnum(100, :GBP)).should == Money.from_fixnum(400, :GBP)
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 yo have to wager £400 to win £100" do
154
- d1_25 = FixedOdds.decimal_odds('1.25')
155
- d1_25.profit_on_winning_stake(Money.from_fixnum(400, :GBP)).should == Money.from_fixnum(100, :GBP)
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('garbage')
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('4/0')
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('2')
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 "#profit_on_winning_stake" do
357
- it "is £400 on a £100 stake on a 4/1 bet" do
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.profit_on_winning_stake(Money.from_fixnum(100, :GBP)).should == Money.from_fixnum(400, :GBP)
359
+ fourToOne.profit_on_stake(Money.parse '£1').should == Money.parse('£4')
360
360
  end
361
361
 
362
- it "is £25 on a £100 stake with a 1/4 bet" do
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.profit_on_winning_stake(Money.from_fixnum(100, :GBP)).should == Money.from_fixnum(25, :GBP)
364
+ oneToFour.profit_on_stake(Money.parse '£1').should == Money.parse('£0.25')
365
365
  end
366
366
  end
367
367
 
368
- describe "#total_return_on_winning_stake" do
369
- it "is £500 on a winning 4/1 bet with a £100 stake" do
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.total_return_on_winning_stake(Money.from_fixnum(100, :GBP)).should == Money.from_fixnum(500, :GBP)
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.total_return_on_winning_stake(Money.from_fixnum(100, :GBP)).should == Money.from_fixnum(125, :GBP)
376
+ oneToFour.total_return_on_stake(Money.parse '£100').should == Money.parse(125')
377
377
  end
378
378
  end
379
379
 
380
- describe "#stake_needed_to_win" do
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.stake_needed_to_win(Money.from_fixnum(1, :GBP)).should == Money.from_fixnum(1, :GBP)
383
+ oneToOne.stake_to_profit(Money.parse '£1').should == Money.parse(1')
384
384
  end
385
385
 
386
- it "is £100 on 4/1 to win £400" do
387
- fourToOne = FixedOdds.fractional_odds '4/1'
388
- fourToOne.stake_needed_to_win(Money.from_fixnum(400, :GBP)).should == Money.from_fixnum(100, :GBP)
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 "'+915' is less likely than '-275'" do
394
- FixedOdds.from_s('+915').should be < FixedOdds.from_s('-275')
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 "'-275' is more likely than '+915'" do
398
- FixedOdds.from_s('-275').should be > FixedOdds.from_s('+915')
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
- @events = MutuallyExclusiveCollection.new [@draw, @bad_team, @good_team]
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
- describe "#least_likely" do
14
- it "is the least likely event" do
15
- @events.least_likely.should == @bad_team
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
- describe "#most_likely" do
20
- it "is the most likely event" do
21
- @events.most_likely.should == @good_team
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
- describe "#in_descending_probability" do
26
- it "is in descending order of probability" do
27
- @events.in_descending_probability.should == [@good_team, @draw, @bad_team]
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
- describe "#in_ascending_probability" do
32
- it "is in ascending order of probability" do
33
- @events.in_ascending_probability.should == [@bad_team, @draw, @good_team]
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
@@ -9,5 +9,5 @@ require 'mutually_exclusive_collection'
9
9
  Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
10
10
 
11
11
  RSpec.configure do |config|
12
-
12
+ config.before(:suite) { Money.assume_from_symbol = true }
13
13
  end
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: 2.1.0
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-03-21 00:00:00.000000000 Z
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: &70280798836640 !ruby/object:Gem::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: *70280798836640
24
+ version_requirements: *70206634864520
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rspec
27
- requirement: &70280798852160 !ruby/object:Gem::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: *70280798852160
35
+ version_requirements: *70206634864040
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: yard
38
- requirement: &70280798851420 !ruby/object:Gem::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: *70280798851420
46
+ version_requirements: *70206634880700
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: bundler
49
- requirement: &70280798850860 !ruby/object:Gem::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: *70280798850860
57
+ version_requirements: *70206634880220
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: jeweler
60
- requirement: &70280798850280 !ruby/object:Gem::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: *70280798850280
68
+ version_requirements: *70206634879740
69
69
  - !ruby/object:Gem::Dependency
70
- name: rcov
71
- requirement: &70280798849340 !ruby/object:Gem::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: *70280798849340
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: -1572583609096736307
119
+ hash: -4185691699245948853
120
120
  required_rubygems_version: !ruby/object:Gem::Requirement
121
121
  none: false
122
122
  requirements: