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 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: