ib-ruby 0.7.11 → 0.7.12
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/HISTORY +4 -0
- data/VERSION +1 -1
- data/db/migrate/121_add_order_states.rb +6 -6
- data/db/migrate/141_add_combo_legs.rb +7 -6
- data/lib/ib-ruby/models/bag.rb +2 -7
- data/lib/ib-ruby/models/combo_leg.rb +8 -4
- data/lib/ib-ruby/models/contract.rb +23 -16
- data/lib/ib-ruby/models/model.rb +3 -3
- data/lib/ib-ruby/models/model_properties.rb +50 -50
- data/lib/ib-ruby/models/option.rb +1 -1
- data/spec/db_helper.rb +57 -54
- data/spec/ib-ruby/models/bag_spec.rb +38 -51
- data/spec/ib-ruby/models/bar_spec.rb +36 -35
- data/spec/ib-ruby/models/combo_leg_spec.rb +108 -31
- data/spec/ib-ruby/models/contract_detail_spec.rb +39 -47
- data/spec/ib-ruby/models/contract_spec.rb +57 -67
- data/spec/ib-ruby/models/execution_spec.rb +53 -56
- data/spec/ib-ruby/models/option_spec.rb +48 -48
- data/spec/ib-ruby/models/order_spec.rb +93 -95
- data/spec/ib-ruby/models/order_state_spec.rb +43 -56
- data/spec/ib-ruby/models/underlying_spec.rb +18 -25
- data/spec/integration/contract_info_spec.rb +4 -0
- data/spec/model_helper.rb +77 -32
- data/spec/test.rb +61 -0
- metadata +14 -34
data/HISTORY
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.7.
|
1
|
+
0.7.12
|
@@ -13,15 +13,15 @@ class AddOrderStates < ActiveRecord::Migration
|
|
13
13
|
t.integer :remaining
|
14
14
|
t.float :price # double
|
15
15
|
t.float :average_price # double
|
16
|
-
t.
|
17
|
-
t.
|
18
|
-
t.
|
16
|
+
t.string :why_held # String: comma-separated list of reasons for order to be held.
|
17
|
+
t.string :warning_text # String: Displays a warning message if warranted.
|
18
|
+
t.string :commission_currency, :limit => 4 # String: Shows the currency of the commission.
|
19
19
|
t.float :commission # double: Shows the commission amount on the order.
|
20
20
|
t.float :min_commission # The possible min range of the actual order commission.
|
21
21
|
t.float :max_commission # The possible max range of the actual order commission.
|
22
|
-
t.
|
23
|
-
t.
|
24
|
-
t.
|
22
|
+
t.float :init_margin # Float: The impact the order would have on your initial margin.
|
23
|
+
t.float :maint_margin # Float: The impact the order would have on your maintenance margin.
|
24
|
+
t.float :equity_with_loan # Float: The impact the order would have on your equity
|
25
25
|
t.timestamps
|
26
26
|
end
|
27
27
|
end
|
@@ -3,15 +3,16 @@ class AddComboLegs < ActiveRecord::Migration
|
|
3
3
|
def change
|
4
4
|
# ComboLeg objects represent individual security legs in a "BAG"
|
5
5
|
create_table(:combo_legs) do |t|
|
6
|
-
t.references :
|
6
|
+
t.references :combo
|
7
|
+
t.references :leg_contract
|
7
8
|
t.integer :con_id # # int: The unique contract identifier specifying the security.
|
8
|
-
t.integer :ratio # int: Select the relative number of contracts for the leg you
|
9
|
-
t.string :exchange # String: exchange to which the complete combo order will be routed.
|
10
9
|
t.string :side, :limit => 1 # Action/side: BUY/SELL/SSHORT/SSHORTX
|
11
|
-
t.integer :
|
12
|
-
t.
|
10
|
+
t.integer :ratio, :limit => 2 # int: Select the relative number of contracts for the leg you
|
11
|
+
t.string :exchange # String: exchange to which the complete combo order will be routed.
|
12
|
+
t.integer :exempt_code, :limit => 2 # int:
|
13
|
+
t.integer :short_sale_slot, :limit => 2 # int: 0 - retail(default), 1 = clearing broker, 2 = third party
|
14
|
+
t.integer :open_close, :limit => 2 # SAME = 0; OPEN = 1; CLOSE = 2; UNKNOWN = 3
|
13
15
|
t.string :designated_location # Otherwise leave blank or orders will be rejected.:status # String: Displays the order status.Possible values include:
|
14
|
-
t.integer :open_close # SAME = 0; OPEN = 1; CLOSE = 2; UNKNOWN = 3
|
15
16
|
t.timestamps
|
16
17
|
end
|
17
18
|
end
|
data/lib/ib-ruby/models/bag.rb
CHANGED
@@ -15,15 +15,10 @@ module IB
|
|
15
15
|
validates_format_of :sec_type, :with => /^bag$/, :message => "should be a bag"
|
16
16
|
validates_format_of :right, :with => /^none$/, :message => "should be none"
|
17
17
|
validates_format_of :expiry, :with => /^$/, :message => "should be blank"
|
18
|
-
validate :legs_cannot_be_empty
|
19
|
-
|
20
|
-
def legs_cannot_be_empty
|
21
|
-
errors.add(:legs, "legs cannot be empty") if legs.empty?
|
22
|
-
end
|
23
18
|
|
24
19
|
def default_attributes
|
25
|
-
super.merge :legs => Array.new,
|
26
|
-
|
20
|
+
super.merge :sec_type => :bag #,:legs => Array.new,
|
21
|
+
|
27
22
|
end
|
28
23
|
|
29
24
|
def description
|
@@ -1,13 +1,15 @@
|
|
1
1
|
module IB
|
2
2
|
module Models
|
3
3
|
|
4
|
-
# ComboLeg
|
5
|
-
#
|
6
|
-
# or bag of securities.
|
4
|
+
# ComboLeg is essentially a join Model between Combo (BAG) Contract and
|
5
|
+
# individual Contracts (securities) that this BAG contains.
|
7
6
|
class ComboLeg < Model.for(:combo_leg)
|
8
7
|
include ModelProperties
|
9
8
|
|
10
|
-
|
9
|
+
# BAG Combo Contract that contains this Leg
|
10
|
+
belongs_to :combo, :class_name => 'Contract'
|
11
|
+
# Contract that constitutes this Leg
|
12
|
+
belongs_to :leg_contract, :class_name => 'Contract', :foreign_key => :leg_contract_id
|
11
13
|
|
12
14
|
# General Notes:
|
13
15
|
# 1. The exchange for the leg definition must match that of the combination order.
|
@@ -40,6 +42,8 @@ module IB
|
|
40
42
|
|
41
43
|
def default_attributes
|
42
44
|
super.merge :con_id => 0,
|
45
|
+
:ratio => 1,
|
46
|
+
:side => :buy,
|
43
47
|
:open_close => :same, # The only option for retail customers.
|
44
48
|
:short_sale_slot => :default,
|
45
49
|
:designated_location => '',
|
@@ -49,14 +49,14 @@ module IB
|
|
49
49
|
{:set => proc { |val|
|
50
50
|
self[:right] =
|
51
51
|
case val.to_s.upcase
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
52
|
+
when 'NONE', '', '0', '?'
|
53
|
+
''
|
54
|
+
when 'PUT', 'P'
|
55
|
+
'P'
|
56
|
+
when 'CALL', 'C'
|
57
|
+
'C'
|
58
|
+
else
|
59
|
+
val
|
60
60
|
end },
|
61
61
|
:validate => {:format => {:with => /^put$|^call$|^none$/,
|
62
62
|
:message => "should be put, call or none"}}
|
@@ -70,13 +70,20 @@ module IB
|
|
70
70
|
|
71
71
|
has_one :contract_detail # Volatile info about this Contract
|
72
72
|
|
73
|
-
|
73
|
+
# For Contracts that are part of BAG
|
74
|
+
has_one :leg, :class_name => 'ComboLeg', :foreign_key => :leg_contract_id
|
75
|
+
has_one :combo, :class_name => 'Contract', :through => :leg
|
76
|
+
|
77
|
+
# for Combo/BAG Contracts that contain ComboLegs
|
78
|
+
has_many :combo_legs, :foreign_key => :combo_id
|
79
|
+
has_many :leg_contracts, :class_name => 'Contract', :through => :combo_legs
|
74
80
|
alias legs combo_legs
|
75
81
|
alias legs= combo_legs=
|
76
82
|
alias combo_legs_description legs_description
|
77
83
|
alias combo_legs_description= legs_description=
|
78
84
|
|
79
|
-
|
85
|
+
# for Delta-Neutral Combo Contracts
|
86
|
+
has_one :underlying
|
80
87
|
alias under_comp underlying
|
81
88
|
alias under_comp= underlying=
|
82
89
|
|
@@ -142,12 +149,12 @@ module IB
|
|
142
149
|
# Defined in Contract, not BAG subclass to keep code DRY
|
143
150
|
def serialize_legs *fields
|
144
151
|
case
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
152
|
+
when !bag?
|
153
|
+
[]
|
154
|
+
when legs.empty?
|
155
|
+
[0]
|
156
|
+
else
|
157
|
+
[legs.size, legs.map { |leg| leg.serialize *fields }].flatten
|
151
158
|
end
|
152
159
|
end
|
153
160
|
|
data/lib/ib-ruby/models/model.rb
CHANGED
@@ -25,11 +25,11 @@ module IB
|
|
25
25
|
|
26
26
|
# If a opts hash is given, keys are taken as attribute names, values as data.
|
27
27
|
# The model instance fields are then set automatically from the opts Hash.
|
28
|
-
def initialize opts={}
|
28
|
+
def initialize attributes={}, opts={}
|
29
29
|
run_callbacks :initialize do
|
30
|
-
error "Argument must be a Hash", :args unless
|
30
|
+
error "Argument must be a Hash", :args unless attributes.is_a?(Hash)
|
31
31
|
|
32
|
-
self.attributes = default_attributes.merge(
|
32
|
+
self.attributes = default_attributes.merge(attributes)
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
@@ -75,65 +75,65 @@ module IB
|
|
75
75
|
def self.define_property_methods name, body={}
|
76
76
|
#p name, body
|
77
77
|
case body
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
end
|
78
|
+
when '' # default getter and setter
|
79
|
+
define_property_methods name
|
80
|
+
|
81
|
+
when Array # [setter, getter, validators]
|
82
|
+
define_property_methods name,
|
83
|
+
:get => body[0],
|
84
|
+
:set => body[1],
|
85
|
+
:validate => body[2]
|
86
|
+
|
87
|
+
when Hash # recursion base case
|
88
|
+
getter = case # Define getter
|
89
|
+
when body[:get].respond_to?(:call)
|
90
|
+
body[:get]
|
91
|
+
when body[:get]
|
92
|
+
proc { self[name].send "to_#{body[:get]}" }
|
93
|
+
when VALUES[name] # property is encoded
|
94
|
+
proc { VALUES[name][self[name]] }
|
95
|
+
#when respond_to?(:column_names) && column_names.include?(name.to_s)
|
96
|
+
# # noop, ActiveRecord will take care of it...
|
97
|
+
# p "#{name} => get noop"
|
98
|
+
# p respond_to?(:column_names) && column_names
|
99
|
+
else
|
100
|
+
proc { self[name] }
|
101
|
+
end
|
102
|
+
define_method name, &getter if getter
|
103
|
+
|
104
|
+
setter = case # Define setter
|
105
|
+
when body[:set].respond_to?(:call)
|
106
|
+
body[:set]
|
107
|
+
when body[:set]
|
108
|
+
proc { |value| self[name] = value.send "to_#{body[:set]}" }
|
109
|
+
when CODES[name] # property is encoded
|
110
|
+
proc { |value| self[name] = CODES[name][value] || value }
|
111
|
+
else
|
112
|
+
proc { |value| self[name] = value } # p name, value;
|
113
|
+
end
|
114
|
+
define_method "#{name}=", &setter if setter
|
115
|
+
|
116
|
+
# Define validator(s)
|
117
|
+
[body[:validate]].flatten.compact.each do |validator|
|
118
|
+
case validator
|
119
|
+
when Proc
|
120
|
+
validates_each name, &validator
|
121
|
+
when Hash
|
122
|
+
validates name, validator.dup
|
124
123
|
end
|
124
|
+
end
|
125
125
|
|
126
126
|
# TODO define self[:name] accessors for :virtual and :flag properties
|
127
127
|
|
128
|
-
|
129
|
-
|
128
|
+
else # setter given
|
129
|
+
define_property_methods name, :set => body, :get => body
|
130
130
|
end
|
131
131
|
end
|
132
132
|
|
133
133
|
# Extending AR-backed Model class with attribute defaults
|
134
134
|
if defined?(ActiveRecord::Base) && ancestors.include?(ActiveRecord::Base)
|
135
|
-
def initialize opts={}
|
136
|
-
super default_attributes.merge(opts
|
135
|
+
def initialize attributes={}, opts={}
|
136
|
+
super default_attributes.merge(attributes), opts
|
137
137
|
end
|
138
138
|
else
|
139
139
|
# Timestamps
|
@@ -7,7 +7,7 @@ module IB
|
|
7
7
|
validates_numericality_of :strike, :greater_than => 0
|
8
8
|
validates_format_of :sec_type, :with => /^option$/,
|
9
9
|
:message => "should be an option"
|
10
|
-
validates_format_of :local_symbol, :with => /^\w+\s*\d{
|
10
|
+
validates_format_of :local_symbol, :with => /^\w+\s*\d{6}[pcPC]\d{8}$|^$/,
|
11
11
|
:message => "invalid OSI code"
|
12
12
|
validates_format_of :right, :with => /^put$|^call$/,
|
13
13
|
:message => "should be put or call"
|
data/spec/db_helper.rb
CHANGED
@@ -32,11 +32,13 @@ shared_examples_for 'Valid DB-backed Model' do
|
|
32
32
|
end
|
33
33
|
|
34
34
|
it 'and with the same properties' do
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
35
|
+
if init_with_props?
|
36
|
+
model = described_class.find(:first)
|
37
|
+
#p model.attributes
|
38
|
+
#p model.content_attributes
|
39
|
+
props.each do |name, value|
|
40
|
+
model.send(name).should == value
|
41
|
+
end
|
40
42
|
end
|
41
43
|
end
|
42
44
|
|
@@ -75,79 +77,80 @@ end
|
|
75
77
|
shared_examples_for 'Model with associations' do
|
76
78
|
|
77
79
|
it 'works with associations, if any' do
|
78
|
-
if defined? associations
|
79
80
|
|
80
|
-
|
81
|
+
subject_name_plural = described_class.to_s.demodulize.tableize
|
81
82
|
|
82
|
-
|
83
|
-
|
84
|
-
|
83
|
+
associations.each do |name, item_props|
|
84
|
+
item = "IB::Models::#{name.to_s.classify}".constantize.new item_props
|
85
|
+
#item = const_get("IB::#{name.to_s.classify}").new item_props
|
86
|
+
puts "Testing single association #{name}"
|
87
|
+
subject.association(name).reflection.should_not be_collection
|
85
88
|
|
86
|
-
|
87
|
-
|
89
|
+
# Assign item to association
|
90
|
+
expect { subject.send "#{name}=", item }.to_not raise_error
|
88
91
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
+
association = subject.send name #, :reload
|
93
|
+
association.should == item
|
94
|
+
association.should be_new_record
|
92
95
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
+
# Reverse association does not include subject
|
97
|
+
reverse_association = association.send(subject_name_plural)
|
98
|
+
reverse_association.should be_empty
|
96
99
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
+
# Now let's save subject
|
101
|
+
if subject.valid?
|
102
|
+
subject.save
|
100
103
|
|
101
|
-
|
102
|
-
|
104
|
+
association = subject.send name
|
105
|
+
association.should_not be_new_record
|
103
106
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
end
|
107
|
+
# Reverse association now DOES include subject (if reloaded!)
|
108
|
+
reverse_association = association.send(subject_name_plural, :reload)
|
109
|
+
reverse_association.should include subject
|
108
110
|
end
|
109
111
|
end
|
110
112
|
end
|
111
113
|
|
112
114
|
it 'works with associated collections, if any' do
|
113
|
-
|
114
|
-
|
115
|
-
subject_name = described_class.to_s.demodulize.tableize.singularize
|
115
|
+
subject_name = described_class.to_s.demodulize.tableize.singularize
|
116
116
|
|
117
|
-
|
118
|
-
|
119
|
-
|
117
|
+
collections.each do |name, items|
|
118
|
+
puts "Testing associated collection #{name}"
|
119
|
+
subject.association(name).reflection.should be_collection
|
120
120
|
|
121
|
-
|
122
|
-
|
121
|
+
[items].flatten.each do |item_props|
|
122
|
+
item = "IB::Models::#{name.to_s.classify}".constantize.new item_props
|
123
|
+
#item = item_class.new item_props
|
124
|
+
association = subject.send name #, :reload
|
123
125
|
|
124
|
-
|
125
|
-
|
126
|
-
|
126
|
+
# Add item to collection
|
127
|
+
expect { association << item }.to_not raise_error
|
128
|
+
association.should include item
|
127
129
|
|
128
|
-
|
129
|
-
|
130
|
-
|
130
|
+
# Reverse association does NOT point to subject
|
131
|
+
reverse_association = association.first.send(subject_name)
|
132
|
+
#reverse_association.should be_nil # But not always!
|
131
133
|
|
132
|
-
|
133
|
-
|
134
|
+
#association.size.should == items.size # Not for Order, +1 OrderState
|
135
|
+
end
|
134
136
|
|
135
|
-
|
136
|
-
|
137
|
-
|
137
|
+
# Now let's save subject
|
138
|
+
if subject.valid?
|
139
|
+
subject.save
|
138
140
|
|
139
|
-
|
140
|
-
|
141
|
+
[items].flatten.each do |item_props|
|
142
|
+
item = "IB::Models::#{name.to_s.classify}".constantize.new item_props
|
143
|
+
association = subject.send name #, :reload
|
141
144
|
|
142
|
-
|
145
|
+
association.should include item
|
143
146
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
end
|
147
|
+
# Reverse association DOES point to subject now
|
148
|
+
reverse_association = association.first.send(subject_name)
|
149
|
+
reverse_association.should == subject
|
148
150
|
end
|
149
|
-
|
150
151
|
end
|
151
152
|
end
|
152
153
|
end
|
154
|
+
|
155
|
+
|
153
156
|
end
|
@@ -1,69 +1,56 @@
|
|
1
1
|
require 'model_helper'
|
2
2
|
|
3
|
-
describe IB::Models::Bag
|
4
|
-
|
5
|
-
let(:props) do
|
6
|
-
{:symbol => 'GOOG',
|
7
|
-
:exchange => 'SMART',
|
8
|
-
:currency => 'USD',
|
9
|
-
:legs => [IB::ComboLeg.new(:con_id => 81032967, :weight => 1),
|
10
|
-
IB::ComboLeg.new(:con_id => 81032968, :weight => -2),
|
11
|
-
IB::ComboLeg.new(:con_id => 81032973, :weight => 1)]
|
12
|
-
}
|
13
|
-
end
|
3
|
+
describe IB::Models::Bag,
|
14
4
|
|
15
|
-
|
16
|
-
|
17
|
-
|
5
|
+
:props =>
|
6
|
+
{:symbol => 'GOOG',
|
7
|
+
:exchange => 'SMART',
|
8
|
+
:currency => 'USD',
|
9
|
+
:legs => [IB::ComboLeg.new(:con_id => 81032967, :weight => 1),
|
10
|
+
IB::ComboLeg.new(:con_id => 81032968, :weight => -2),
|
11
|
+
IB::ComboLeg.new(:con_id => 81032973, :weight => 1)]
|
12
|
+
},
|
18
13
|
|
19
|
-
|
20
|
-
{:legs => ["legs cannot be empty"],
|
21
|
-
}
|
22
|
-
end
|
14
|
+
:human => "<Bag: GOOG SMART USD legs: 81032967|1,81032968|-2,81032973|1 >",
|
23
15
|
|
24
|
-
|
25
|
-
|
26
|
-
{[nil, ''] => '',
|
27
|
-
[20060913, '20060913', 200609, '200609', :foo, 2006, 42, 'bar'] =>
|
28
|
-
/should be blank/},
|
16
|
+
:errors =>
|
17
|
+
{:legs => ["legs cannot be empty"]},
|
29
18
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
19
|
+
:assigns =>
|
20
|
+
{:expiry =>
|
21
|
+
{[nil, ''] => '',
|
22
|
+
[20060913, '20060913', 200609, '200609', 2006, :foo, 'bar'] =>
|
23
|
+
/should be blank/},
|
34
24
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
25
|
+
:sec_type =>
|
26
|
+
{['BAG', :bag] => :bag,
|
27
|
+
IB::CODES[:sec_type].reject { |k, _| k == :bag }.to_a =>
|
28
|
+
/should be a bag/},
|
39
29
|
|
40
|
-
|
41
|
-
|
30
|
+
:right =>
|
31
|
+
{['?', :none, '', '0'] => :none,
|
32
|
+
["PUT", :put, "CALL", "C", :call, :foo, 'BAR', 42] =>
|
33
|
+
/should be none/},
|
42
34
|
|
43
|
-
|
44
|
-
|
35
|
+
:exchange => string_upcase_assigns.merge(
|
36
|
+
[:smart, 'SMART', 'smArt'] => 'SMART'),
|
45
37
|
|
46
|
-
|
38
|
+
:primary_exchange =>string_upcase_assigns.merge(
|
39
|
+
[:SMART, 'SMART'] => /should not be SMART/),
|
47
40
|
|
48
|
-
|
49
|
-
}
|
50
|
-
end
|
41
|
+
[:symbol, :local_symbol] => string_assigns,
|
51
42
|
|
52
|
-
|
53
|
-
|
54
|
-
bag.legs = []
|
55
|
-
bag.should be_invalid
|
56
|
-
bag.errors.messages[:legs].should include "legs cannot be empty"
|
57
|
-
end
|
43
|
+
:multiplier => to_i_assigns,
|
44
|
+
} do # AKA IB::Bag
|
58
45
|
|
59
|
-
|
60
|
-
subject { IB::Bag.new }
|
61
|
-
it_behaves_like 'Model instantiated empty'
|
62
|
-
end
|
63
|
-
|
64
|
-
it_behaves_like 'Model'
|
46
|
+
it_behaves_like 'Model with valid defaults'
|
65
47
|
it_behaves_like 'Self-equal Model'
|
66
48
|
|
49
|
+
it 'has class name shortcut' do
|
50
|
+
IB::Bag.should == IB::Models::Bag
|
51
|
+
IB::Bag.new.should == IB::Models::Bag.new
|
52
|
+
end
|
53
|
+
|
67
54
|
context 'properly initiated' do
|
68
55
|
subject { IB::Bag.new props }
|
69
56
|
|