ib-ruby 0.7.6 → 0.7.8
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY +8 -0
- data/Rakefile +8 -0
- data/VERSION +1 -1
- data/bin/fundamental_data +6 -9
- data/lib/ib-ruby/connection.rb +16 -19
- data/lib/ib-ruby/constants.rb +3 -1
- data/lib/ib-ruby/extensions.rb +5 -0
- data/lib/ib-ruby/messages/incoming/contract_data.rb +46 -45
- data/lib/ib-ruby/messages/incoming/delta_neutral_validation.rb +8 -5
- data/lib/ib-ruby/messages/incoming/execution_data.rb +2 -2
- data/lib/ib-ruby/messages/incoming/next_valid_id.rb +18 -0
- data/lib/ib-ruby/messages/incoming/open_order.rb +23 -16
- data/lib/ib-ruby/messages/incoming/order_status.rb +5 -3
- data/lib/ib-ruby/messages/incoming/scanner_data.rb +15 -11
- data/lib/ib-ruby/messages/incoming.rb +1 -5
- data/lib/ib-ruby/messages/outgoing/abstract_message.rb +2 -1
- data/lib/ib-ruby/messages/outgoing/place_order.rb +1 -1
- data/lib/ib-ruby/messages/outgoing.rb +1 -1
- data/lib/ib-ruby/models/bag.rb +59 -0
- data/lib/ib-ruby/models/combo_leg.rb +10 -6
- data/lib/ib-ruby/models/contract.rb +278 -0
- data/lib/ib-ruby/models/contract_detail.rb +70 -0
- data/lib/ib-ruby/models/execution.rb +22 -16
- data/lib/ib-ruby/models/model.rb +75 -17
- data/lib/ib-ruby/models/model_properties.rb +40 -26
- data/lib/ib-ruby/models/option.rb +62 -0
- data/lib/ib-ruby/models/order.rb +122 -86
- data/lib/ib-ruby/models/order_state.rb +11 -12
- data/lib/ib-ruby/models/underlying.rb +36 -0
- data/lib/ib-ruby/models.rb +1 -4
- data/spec/account_helper.rb +2 -1
- data/spec/db.rb +1 -1
- data/spec/db_helper.rb +105 -0
- data/spec/ib-ruby/connection_spec.rb +3 -3
- data/spec/ib-ruby/messages/incoming/open_order_spec.rb +5 -5
- data/spec/ib-ruby/messages/incoming/order_status_spec.rb +3 -3
- data/spec/ib-ruby/models/bag_spec.rb +15 -23
- data/spec/ib-ruby/models/bar_spec.rb +0 -5
- data/spec/ib-ruby/models/combo_leg_spec.rb +18 -25
- data/spec/ib-ruby/models/contract_detail_spec.rb +54 -0
- data/spec/ib-ruby/models/contract_spec.rb +25 -37
- data/spec/ib-ruby/models/execution_spec.rb +64 -19
- data/spec/ib-ruby/models/option_spec.rb +12 -34
- data/spec/ib-ruby/models/order_spec.rb +107 -45
- data/spec/ib-ruby/models/order_state_spec.rb +12 -12
- data/spec/ib-ruby/models/underlying_spec.rb +36 -0
- data/spec/integration/contract_info_spec.rb +65 -55
- data/spec/integration/fundamental_data_spec.rb +2 -2
- data/spec/integration/orders/attached_spec.rb +3 -3
- data/spec/integration/orders/combo_spec.rb +3 -3
- data/spec/integration/orders/placement_spec.rb +8 -8
- data/spec/integration/orders/{execution_spec.rb → trades_spec.rb} +8 -12
- data/spec/integration/orders/valid_ids_spec.rb +3 -3
- data/spec/message_helper.rb +1 -1
- data/spec/model_helper.rb +150 -85
- data/spec/order_helper.rb +35 -18
- metadata +18 -10
- data/lib/ib-ruby/models/contracts/bag.rb +0 -62
- data/lib/ib-ruby/models/contracts/contract.rb +0 -320
- data/lib/ib-ruby/models/contracts/option.rb +0 -66
- data/lib/ib-ruby/models/contracts.rb +0 -27
@@ -0,0 +1,278 @@
|
|
1
|
+
require 'ib-ruby/models/contract_detail'
|
2
|
+
require 'ib-ruby/models/underlying'
|
3
|
+
|
4
|
+
module IB
|
5
|
+
module Models
|
6
|
+
class Contract < Model.for(:contract)
|
7
|
+
include ModelProperties
|
8
|
+
|
9
|
+
# Fields are Strings unless noted otherwise
|
10
|
+
prop :con_id, # int: The unique contract identifier.
|
11
|
+
:currency, # Only needed if there is an ambiguity, e.g. when SMART exchange
|
12
|
+
# and IBM is being requested (IBM can trade in GBP or USD).
|
13
|
+
|
14
|
+
:legs_description, # received in OpenOrder for all combos
|
15
|
+
|
16
|
+
:sec_type, # Security type. Valid values are: SECURITY_TYPES
|
17
|
+
|
18
|
+
:sec_id, # Unique identifier of the given secIdType.
|
19
|
+
|
20
|
+
:sec_id_type => :sup, # Security identifier, when querying contract details or
|
21
|
+
# when placing orders. Supported identifiers are:
|
22
|
+
# - ISIN (Example: Apple: US0378331005)
|
23
|
+
# - CUSIP (Example: Apple: 037833100)
|
24
|
+
# - SEDOL (6-AN + check digit. Example: BAE: 0263494)
|
25
|
+
# - RIC (exchange-independent RIC Root and exchange-
|
26
|
+
# identifying suffix. Ex: AAPL.O for Apple on NASDAQ.)
|
27
|
+
|
28
|
+
:symbol => :s, # This is the symbol of the underlying asset.
|
29
|
+
|
30
|
+
:local_symbol => :s, # Local exchange symbol of the underlying asset
|
31
|
+
|
32
|
+
# Future/option contract multiplier (only needed when multiple possibilities exist)
|
33
|
+
:multiplier => {:set => :i},
|
34
|
+
|
35
|
+
:strike => :f, # double: The strike price.
|
36
|
+
:expiry => :s, # The expiration date. Use the format YYYYMM or YYYYMMDD
|
37
|
+
:exchange => :sup, # The order destination, such as Smart.
|
38
|
+
:primary_exchange => :sup, # Non-SMART exchange where the contract trades.
|
39
|
+
:include_expired => :bool, # When true, contract details requests and historical
|
40
|
+
# data queries can be performed pertaining to expired contracts.
|
41
|
+
# Note: Historical data queries on expired contracts are
|
42
|
+
# limited to the last year of the contracts life, and are
|
43
|
+
# only supported for expired futures contracts.
|
44
|
+
# This field can NOT be set to true for orders.
|
45
|
+
|
46
|
+
|
47
|
+
# Specifies a Put or Call. Valid input values are: P, PUT, C, CALL
|
48
|
+
:right =>
|
49
|
+
{:set => proc { |val|
|
50
|
+
self[:right] =
|
51
|
+
case val.to_s.upcase
|
52
|
+
when 'NONE', '', '0', '?'
|
53
|
+
''
|
54
|
+
when 'PUT', 'P'
|
55
|
+
'P'
|
56
|
+
when 'CALL', 'C'
|
57
|
+
'C'
|
58
|
+
else
|
59
|
+
val
|
60
|
+
end },
|
61
|
+
:validate => {:format => {:with => /^put$|^call$|^none$/,
|
62
|
+
:message => "should be put, call or none"}}
|
63
|
+
}
|
64
|
+
|
65
|
+
attr_accessor :description # NB: local to ib-ruby, not part of TWS.
|
66
|
+
|
67
|
+
### Associations
|
68
|
+
|
69
|
+
has_one :contract_detail
|
70
|
+
|
71
|
+
has_one :underlying # for Delta-Neutral Combo contracts only!
|
72
|
+
alias under_comp underlying
|
73
|
+
alias under_comp= underlying=
|
74
|
+
|
75
|
+
has_many :combo_legs
|
76
|
+
alias legs combo_legs
|
77
|
+
alias legs= combo_legs=
|
78
|
+
alias combo_legs_description legs_description
|
79
|
+
alias combo_legs_description= legs_description=
|
80
|
+
|
81
|
+
|
82
|
+
### Extra validations
|
83
|
+
validates_inclusion_of :sec_type, :in => CODES[:sec_type].keys,
|
84
|
+
:message => "should be valid security type"
|
85
|
+
|
86
|
+
validates_format_of :expiry, :with => /^\d{6}$|^\d{8}$|^$/,
|
87
|
+
:message => "should be YYYYMM or YYYYMMDD"
|
88
|
+
|
89
|
+
validates_format_of :primary_exchange, :without => /SMART/,
|
90
|
+
:message => "should not be SMART"
|
91
|
+
|
92
|
+
validates_format_of :sec_id_type, :with => /ISIN|SEDOL|CUSIP|RIC|^$/,
|
93
|
+
:message => "should be valid security identifier"
|
94
|
+
|
95
|
+
validates_numericality_of :multiplier, :strike, :allow_nil => true
|
96
|
+
|
97
|
+
def default_attributes
|
98
|
+
{:con_id => 0,
|
99
|
+
:strike => 0.0,
|
100
|
+
:right => :none, # Not an option
|
101
|
+
:exchange => 'SMART',
|
102
|
+
:include_expired => false, }.merge super
|
103
|
+
end
|
104
|
+
|
105
|
+
# This returns an Array of data from the given contract.
|
106
|
+
# Different messages serialize contracts differently. Go figure.
|
107
|
+
# Note that it does NOT include the combo legs.
|
108
|
+
# serialize [:option, :con_id, :include_expired, :sec_id]
|
109
|
+
def serialize *fields
|
110
|
+
[(fields.include?(:con_id) ? [con_id] : []),
|
111
|
+
symbol,
|
112
|
+
self[:sec_type],
|
113
|
+
(fields.include?(:option) ?
|
114
|
+
[expiry,
|
115
|
+
strike,
|
116
|
+
self[:right],
|
117
|
+
multiplier] : []),
|
118
|
+
exchange,
|
119
|
+
(fields.include?(:primary_exchange) ? [primary_exchange] : []),
|
120
|
+
currency,
|
121
|
+
local_symbol,
|
122
|
+
(fields.include?(:sec_id) ? [sec_id_type, sec_id] : []),
|
123
|
+
(fields.include?(:include_expired) ? [include_expired] : []),
|
124
|
+
].flatten
|
125
|
+
end
|
126
|
+
|
127
|
+
def serialize_long *fields
|
128
|
+
serialize :option, :primary_exchange, *fields
|
129
|
+
end
|
130
|
+
|
131
|
+
def serialize_short *fields
|
132
|
+
serialize :option, *fields
|
133
|
+
end
|
134
|
+
|
135
|
+
# Serialize under_comp parameters: EClientSocket.java, line 471
|
136
|
+
def serialize_under_comp *args
|
137
|
+
under_comp ? under_comp.serialize : [false]
|
138
|
+
end
|
139
|
+
|
140
|
+
# Defined in Contract, not BAG subclass to keep code DRY
|
141
|
+
def serialize_legs *fields
|
142
|
+
case
|
143
|
+
when !bag?
|
144
|
+
[]
|
145
|
+
when legs.empty?
|
146
|
+
[0]
|
147
|
+
else
|
148
|
+
[legs.size, legs.map { |leg| leg.serialize *fields }].flatten
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# This produces a string uniquely identifying this contract, in the format used
|
153
|
+
# for command line arguments in the IB-Ruby examples. The format is:
|
154
|
+
#
|
155
|
+
# symbol:sec_type:expiry:strike:right:multiplier:exchange:primary_exchange:currency:local_symbol
|
156
|
+
#
|
157
|
+
# Fields not needed for a particular security should be left blank
|
158
|
+
# (e.g. strike and right are only relevant for options.)
|
159
|
+
#
|
160
|
+
# For example, to query the British pound futures contract trading on Globex
|
161
|
+
# expiring in September, 2008, the string is:
|
162
|
+
#
|
163
|
+
# GBP:FUT:200809:::62500:GLOBEX::USD:
|
164
|
+
def serialize_ib_ruby
|
165
|
+
serialize_long.join(":")
|
166
|
+
end
|
167
|
+
|
168
|
+
# Contract comparison
|
169
|
+
def == other
|
170
|
+
return false unless other.is_a?(self.class)
|
171
|
+
|
172
|
+
# Different sec_id_type
|
173
|
+
return false if sec_id_type && other.sec_id_type && sec_id_type != other.sec_id_type
|
174
|
+
|
175
|
+
# Different sec_id
|
176
|
+
return false if sec_id && other.sec_id && sec_id != other.sec_id
|
177
|
+
|
178
|
+
# Different symbols
|
179
|
+
return false if symbol && other.symbol && symbol != other.symbol
|
180
|
+
|
181
|
+
# Different currency
|
182
|
+
return false if currency && other.currency && currency != other.currency
|
183
|
+
|
184
|
+
# Same con_id for all Bags, but unknown for new Contracts...
|
185
|
+
# 0 or nil con_id matches any
|
186
|
+
return false if con_id != 0 && other.con_id != 0 &&
|
187
|
+
con_id && other.con_id && con_id != other.con_id
|
188
|
+
|
189
|
+
# SMART or nil exchange matches any
|
190
|
+
return false if exchange != 'SMART' && other.exchange != 'SMART' &&
|
191
|
+
exchange && other.exchange && exchange != other.exchange
|
192
|
+
|
193
|
+
# Comparison for Bonds and Options
|
194
|
+
if bond? || option?
|
195
|
+
return false if right != other.right || strike != other.strike
|
196
|
+
return false if multiplier && other.multiplier &&
|
197
|
+
multiplier != other.multiplier
|
198
|
+
return false if expiry && expiry[0..5] != other.expiry[0..5]
|
199
|
+
return false unless expiry && (expiry[6..7] == other.expiry[6..7] ||
|
200
|
+
expiry[6..7].empty? || other.expiry[6..7].empty?)
|
201
|
+
end
|
202
|
+
|
203
|
+
# All else being equal...
|
204
|
+
sec_type == other.sec_type
|
205
|
+
end
|
206
|
+
|
207
|
+
def to_s
|
208
|
+
"<Contract: " + instance_variables.map do |key|
|
209
|
+
value = send(key[1..-1])
|
210
|
+
" #{key}=#{value}" unless value.nil? || value == '' || value == 0
|
211
|
+
end.compact.join(',') + " >"
|
212
|
+
end
|
213
|
+
|
214
|
+
def to_human
|
215
|
+
"<Contract: " +
|
216
|
+
[symbol,
|
217
|
+
sec_type,
|
218
|
+
(expiry == '' ? nil : expiry),
|
219
|
+
(right == :none ? nil : right),
|
220
|
+
(strike == 0 ? nil : strike),
|
221
|
+
exchange,
|
222
|
+
currency
|
223
|
+
].compact.join(" ") + ">"
|
224
|
+
end
|
225
|
+
|
226
|
+
def to_short
|
227
|
+
"#{symbol}#{expiry}#{strike}#{right}#{exchange}#{currency}"
|
228
|
+
end
|
229
|
+
|
230
|
+
# Testing for type of contract:
|
231
|
+
|
232
|
+
def bag?
|
233
|
+
self[:sec_type] == 'BAG'
|
234
|
+
end
|
235
|
+
|
236
|
+
def bond?
|
237
|
+
self[:sec_type] == 'BOND'
|
238
|
+
end
|
239
|
+
|
240
|
+
def stock?
|
241
|
+
self[:sec_type] == 'STK'
|
242
|
+
end
|
243
|
+
|
244
|
+
def option?
|
245
|
+
self[:sec_type] == 'OPT'
|
246
|
+
end
|
247
|
+
|
248
|
+
end # class Contract
|
249
|
+
|
250
|
+
|
251
|
+
### Now let's deal with Contract subclasses
|
252
|
+
|
253
|
+
require 'ib-ruby/models/option'
|
254
|
+
require 'ib-ruby/models/bag'
|
255
|
+
|
256
|
+
class Contract
|
257
|
+
# Specialized Contract subclasses representing different security types
|
258
|
+
Subclasses = Hash.new(Contract)
|
259
|
+
Subclasses[:bag] = IB::Models::Bag
|
260
|
+
Subclasses[:option] = IB::Models::Option
|
261
|
+
|
262
|
+
# This returns a Contract initialized from the serialize_ib_ruby format string.
|
263
|
+
def self.build opts = {}
|
264
|
+
subclass = VALUES[:sec_type][opts[:sec_type]] || opts[:sec_type].to_sym
|
265
|
+
Contract::Subclasses[subclass].new opts
|
266
|
+
end
|
267
|
+
|
268
|
+
# This returns a Contract initialized from the serialize_ib_ruby format string.
|
269
|
+
def self.from_ib_ruby string
|
270
|
+
keys = [:symbol, :sec_type, :expiry, :strike, :right, :multiplier,
|
271
|
+
:exchange, :primary_exchange, :currency, :local_symbol]
|
272
|
+
props = Hash[keys.zip(string.split(":"))]
|
273
|
+
props.delete_if { |k, v| v.nil? || v.empty? }
|
274
|
+
Contract.build props
|
275
|
+
end
|
276
|
+
end # class Contract
|
277
|
+
end # module Models
|
278
|
+
end # module IB
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module IB
|
2
|
+
module Models
|
3
|
+
|
4
|
+
# Additional Contract properties (volatile, therefore extracted)
|
5
|
+
class ContractDetail < Model.for(:contract_detail)
|
6
|
+
include ModelProperties
|
7
|
+
|
8
|
+
belongs_to :contract
|
9
|
+
alias summary contract
|
10
|
+
alias summary= contract=
|
11
|
+
|
12
|
+
# All fields Strings, unless specified otherwise:
|
13
|
+
prop :market_name, # The market name for this contract.
|
14
|
+
:trading_class, # The trading class name for this contract.
|
15
|
+
:min_tick, # double: The minimum price tick.
|
16
|
+
:price_magnifier, # int: Allows execution and strike prices to be
|
17
|
+
# reported consistently with market data, historical data and the
|
18
|
+
# order price: Z on LIFFE is reported in index points, not GBP.
|
19
|
+
|
20
|
+
:order_types, # The list of valid order types for this contract.
|
21
|
+
:valid_exchanges, # The list of exchanges this contract is traded on.
|
22
|
+
:under_con_id, # int: The underlying contract ID.
|
23
|
+
:long_name, # Descriptive name of the asset.
|
24
|
+
:contract_month, # The contract month of the underlying futures contract.
|
25
|
+
|
26
|
+
# The industry classification of the underlying/product:
|
27
|
+
:industry, # Wide industry. For example, Financial.
|
28
|
+
:category, # Industry category. For example, InvestmentSvc.
|
29
|
+
:subcategory, # Subcategory. For example, Brokerage.
|
30
|
+
[:time_zone, :time_zone_id], # Time zone for the trading hours (e.g. EST)
|
31
|
+
:trading_hours, # The trading hours of the product. For example:
|
32
|
+
# 20090507:0700-1830,1830-2330;20090508:CLOSED.
|
33
|
+
:liquid_hours, # The liquid trading hours of the product. For example,
|
34
|
+
# 20090507:0930-1600;20090508:CLOSED.
|
35
|
+
|
36
|
+
# BOND values:
|
37
|
+
:cusip, # The nine-character bond CUSIP or the 12-character SEDOL.
|
38
|
+
:ratings, # Credit rating of the issuer. Higher rating is less risky investment.
|
39
|
+
# Bond ratings are from Moody's and S&P respectively.
|
40
|
+
:desc_append, # Additional descriptive information about the bond.
|
41
|
+
:bond_type, # The type of bond, such as "CORP."
|
42
|
+
:coupon_type, # The type of bond coupon.
|
43
|
+
:coupon, # double: The interest rate used to calculate the amount you
|
44
|
+
# will receive in interest payments over the year. default 0
|
45
|
+
:maturity, # The date on which the issuer must repay bond face value
|
46
|
+
:issue_date, # The date the bond was issued.
|
47
|
+
:next_option_date, # only if bond has embedded options.
|
48
|
+
:next_option_type, # only if bond has embedded options.
|
49
|
+
:notes, # Additional notes, if populated for the bond in IB's database
|
50
|
+
:callable => :bool, # Can be called by the issuer under certain conditions.
|
51
|
+
:puttable => :bool, # Can be sold back to the issuer under certain conditions
|
52
|
+
:convertible => :bool, # Can be converted to stock under certain conditions.
|
53
|
+
:next_option_partial => :bool # # only if bond has embedded options.
|
54
|
+
|
55
|
+
# Extra validations
|
56
|
+
validates_format_of :time_zone, :with => /^\w{3}$/, :message => 'should be XXX'
|
57
|
+
|
58
|
+
def default_attributes
|
59
|
+
{:coupon => 0.0,
|
60
|
+
:under_con_id => 0,
|
61
|
+
:min_tick => 0,
|
62
|
+
:callable => false,
|
63
|
+
:puttable => false,
|
64
|
+
:convertible => false,
|
65
|
+
:next_option_partial => false, }.merge super
|
66
|
+
end
|
67
|
+
|
68
|
+
end # class ContractDetail
|
69
|
+
end # module Models
|
70
|
+
end # module IB
|
@@ -5,7 +5,9 @@ module IB
|
|
5
5
|
class Execution < Model.for(:execution)
|
6
6
|
include ModelProperties
|
7
7
|
|
8
|
-
|
8
|
+
belongs_to :order
|
9
|
+
|
10
|
+
prop [:local_id, :order_id], # int: order id. TWS orders have a fixed order id of 0.
|
9
11
|
:client_id, # int: client id. TWS orders have a fixed client id of 0.
|
10
12
|
:perm_id, # int: TWS id used to identify orders over TWS sessions
|
11
13
|
:exec_id, # String: Unique order execution id over TWS sessions.
|
@@ -13,29 +15,33 @@ module IB
|
|
13
15
|
# String: The order execution time.
|
14
16
|
:exchange, # String: Exchange that executed the order.
|
15
17
|
:order_ref, # int: Same order_ref as in corresponding Order
|
16
|
-
[:account_name, :account_number], # String: The customer account number.
|
17
18
|
:price, # double: The order execution price.
|
18
19
|
:average_price, # double: Average price. Used in regular trades, combo
|
19
20
|
# trades and legs of the combo.
|
20
|
-
:shares, # int: The number of shares filled.
|
21
|
+
[:quantity, :shares], # int: The number of shares filled.
|
21
22
|
:cumulative_quantity, # int: Cumulative quantity. Used in regular
|
22
23
|
# trades, combo trades and legs of the combo
|
23
|
-
:liquidation => :bool, #
|
24
|
-
[:
|
24
|
+
:liquidation => :bool, # This position is liquidated last should the need arise.
|
25
|
+
[:account_name, :account_number] => :s, # The customer account number.
|
26
|
+
[:side, :action] => PROPS[:side] # Was the transaction a buy or a sale: BOT|SLD
|
25
27
|
|
26
28
|
# Extra validations
|
27
|
-
validates_numericality_of :
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
29
|
+
validates_numericality_of :quantity, :cumulative_quantity, :price, :average_price
|
30
|
+
validates_numericality_of :local_id, :client_id, :perm_id, :only_integer => true
|
31
|
+
|
32
|
+
def default_attributes
|
33
|
+
{:local_id => 0,
|
34
|
+
:client_id => 0,
|
35
|
+
:quantity => 0,
|
36
|
+
:price => 0,
|
37
|
+
:perm_id => 0,
|
38
|
+
:liquidation => false, }.merge super
|
39
|
+
end
|
40
|
+
|
35
41
|
# Comparison
|
36
42
|
def == other
|
37
43
|
perm_id == other.perm_id &&
|
38
|
-
|
44
|
+
local_id == other.local_id && # ((p __LINE__)||true) &&
|
39
45
|
client_id == other.client_id &&
|
40
46
|
exec_id == other.exec_id &&
|
41
47
|
time == other.time &&
|
@@ -46,9 +52,9 @@ module IB
|
|
46
52
|
end
|
47
53
|
|
48
54
|
def to_human
|
49
|
-
"<Execution: #{time} #{side} #{
|
55
|
+
"<Execution: #{time} #{side} #{quantity} at #{price} on #{exchange}, " +
|
50
56
|
"cumulative #{cumulative_quantity} at #{average_price}, " +
|
51
|
-
"ids #{
|
57
|
+
"ids #{local_id}/#{perm_id}/#{exec_id}>"
|
52
58
|
end
|
53
59
|
|
54
60
|
alias to_s to_human
|
data/lib/ib-ruby/models/model.rb
CHANGED
@@ -1,43 +1,101 @@
|
|
1
1
|
module IB
|
2
2
|
module Models
|
3
3
|
|
4
|
-
# Base
|
4
|
+
# Base class for tableless IB data Models extends ActiveModel API
|
5
5
|
class Model
|
6
|
+
extend ActiveModel::Naming
|
7
|
+
extend ActiveModel::Callbacks
|
8
|
+
include ActiveModel::Validations
|
9
|
+
include ActiveModel::Serialization
|
10
|
+
include ActiveModel::Serializers::Xml
|
11
|
+
include ActiveModel::Serializers::JSON
|
6
12
|
|
7
13
|
# IB Models can be either database-backed, or not
|
8
|
-
# require 'ib-ruby/db' # to make IB models database-backed
|
14
|
+
# require 'ib-ruby/db' # to make all IB models database-backed
|
15
|
+
# If you plan to persist only specific Models, select those subclasses here:
|
9
16
|
def self.for subclass
|
10
|
-
if DB
|
11
|
-
|
12
|
-
when :execution, :bar, :order_state
|
13
|
-
# Just a couple of AR models introduced for now...
|
14
|
-
ActiveRecord::Base
|
15
|
-
else
|
16
|
-
Model
|
17
|
-
end
|
17
|
+
if DB # && [:contract, :order, :order_state].include? subclass
|
18
|
+
ActiveRecord::Base
|
18
19
|
else
|
19
20
|
Model
|
20
21
|
end
|
21
22
|
end
|
22
23
|
|
24
|
+
attr_accessor :created_at, :updated_at, :attributes
|
25
|
+
|
23
26
|
# If a opts hash is given, keys are taken as attribute names, values as data.
|
24
27
|
# The model instance fields are then set automatically from the opts Hash.
|
25
|
-
def initialize
|
26
|
-
|
28
|
+
def initialize opts={}
|
29
|
+
run_callbacks :initialize do
|
30
|
+
error "Argument must be a Hash", :args unless opts.is_a?(Hash)
|
31
|
+
|
32
|
+
attrs = default_attributes.merge(opts)
|
33
|
+
attrs.keys.each { |key| self.send("#{key}=", attrs[key]) }
|
34
|
+
end
|
35
|
+
end
|
27
36
|
|
28
|
-
|
29
|
-
|
37
|
+
# ActiveModel API (for serialization)
|
38
|
+
|
39
|
+
def attributes
|
40
|
+
@attributes ||= HashWithIndifferentAccess.new
|
30
41
|
end
|
31
42
|
|
32
|
-
# ActiveModel-style
|
43
|
+
# ActiveModel-style read/write_attribute accessors
|
33
44
|
def [] key
|
34
|
-
|
45
|
+
attributes[key.to_sym]
|
35
46
|
end
|
36
47
|
|
37
48
|
def []= key, val
|
38
|
-
|
49
|
+
attributes[key.to_sym] = val
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_model
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
def new_record?
|
57
|
+
true
|
58
|
+
end
|
59
|
+
|
60
|
+
def save
|
61
|
+
valid?
|
62
|
+
end
|
63
|
+
|
64
|
+
alias save! save
|
65
|
+
|
66
|
+
### ActiveRecord::Base association API mocks
|
67
|
+
|
68
|
+
def self.belongs_to model, *args
|
69
|
+
attr_accessor model
|
39
70
|
end
|
40
71
|
|
72
|
+
def self.has_one model, *args
|
73
|
+
attr_accessor model
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.has_many models, *args
|
77
|
+
attr_accessor models
|
78
|
+
|
79
|
+
define_method(models) do
|
80
|
+
self.instance_variable_get("@#{models}") ||
|
81
|
+
self.instance_variable_set("@#{models}", [])
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.find *args
|
86
|
+
[]
|
87
|
+
end
|
88
|
+
|
89
|
+
### ActiveRecord::Base callback API mocks
|
90
|
+
|
91
|
+
define_model_callbacks :initialize, :only => :after
|
92
|
+
|
93
|
+
### ActiveRecord::Base misc
|
94
|
+
|
95
|
+
def self.serialize *properties
|
96
|
+
end
|
97
|
+
|
98
|
+
|
41
99
|
end # Model
|
42
100
|
end # module Models
|
43
101
|
end # module IB
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'active_model'
|
2
2
|
require 'active_support/concern'
|
3
|
+
require 'active_support/hash_with_indifferent_access'
|
3
4
|
|
4
5
|
module IB
|
5
6
|
module Models
|
@@ -8,25 +9,50 @@ module IB
|
|
8
9
|
module ModelProperties
|
9
10
|
extend ActiveSupport::Concern
|
10
11
|
|
11
|
-
|
12
|
+
def default_attributes
|
13
|
+
{:created_at => Time.now,
|
14
|
+
:updated_at => Time.now,
|
15
|
+
}
|
16
|
+
end
|
12
17
|
|
13
18
|
### Instance methods
|
14
19
|
|
15
|
-
|
20
|
+
# Default presentation
|
21
|
+
def to_human
|
22
|
+
"<#{self.class.to_s.demodulize}: " + attributes.map do |attr, value|
|
23
|
+
"#{attr}: #{value}" unless value.nil?
|
24
|
+
end.compact.sort.join(' ') + ">"
|
25
|
+
end
|
26
|
+
|
27
|
+
# Comparison support
|
28
|
+
def content_attributes
|
29
|
+
HashWithIndifferentAccess[attributes.reject do |(attr, _)|
|
30
|
+
attr.to_s =~ /(_id|_count)$/ ||
|
31
|
+
[:created_at, :updated_at, :type, :id].include?(attr.to_sym)
|
32
|
+
end]
|
33
|
+
end
|
16
34
|
|
17
|
-
|
18
|
-
|
19
|
-
|
35
|
+
# Default Model comparison
|
36
|
+
def == other
|
37
|
+
content_attributes.inject(true) { |res, (attr, value)| res && other.send(attr) == value } &&
|
38
|
+
other.content_attributes.inject(true) { |res, (attr, value)| res && send(attr) == value }
|
20
39
|
end
|
21
40
|
|
22
41
|
included do
|
23
42
|
|
43
|
+
# Extending AR-backed Model class with attribute defaults
|
44
|
+
if defined?(ActiveRecord::Base) && ancestors.include?(ActiveRecord::Base)
|
45
|
+
def initialize opts={}
|
46
|
+
super default_attributes.merge(opts)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
24
50
|
### Class macros
|
25
51
|
|
26
52
|
def self.prop *properties
|
27
53
|
prop_hash = properties.last.is_a?(Hash) ? properties.pop : {}
|
28
54
|
|
29
|
-
properties.each { |names| define_property names,
|
55
|
+
properties.each { |names| define_property names, nil }
|
30
56
|
prop_hash.each { |names, type| define_property names, type }
|
31
57
|
end
|
32
58
|
|
@@ -57,7 +83,6 @@ module IB
|
|
57
83
|
:validate => body[2]
|
58
84
|
|
59
85
|
when Hash # recursion base case
|
60
|
-
|
61
86
|
getter = case # Define getter
|
62
87
|
when body[:get].respond_to?(:call)
|
63
88
|
body[:get]
|
@@ -65,10 +90,14 @@ module IB
|
|
65
90
|
proc { self[name].send "to_#{body[:get]}" }
|
66
91
|
when VALUES[name] # property is encoded
|
67
92
|
proc { VALUES[name][self[name]] }
|
93
|
+
#when respond_to?(:column_names) && column_names.include?(name.to_s)
|
94
|
+
# # noop, ActiveRecord will take care of it...
|
95
|
+
# p "#{name} => get noop"
|
96
|
+
# p respond_to?(:column_names) && column_names
|
68
97
|
else
|
69
98
|
proc { self[name] }
|
70
99
|
end
|
71
|
-
define_method name, &getter
|
100
|
+
define_method name, &getter if getter
|
72
101
|
|
73
102
|
setter = case # Define setter
|
74
103
|
when body[:set].respond_to?(:call)
|
@@ -78,9 +107,9 @@ module IB
|
|
78
107
|
when CODES[name] # property is encoded
|
79
108
|
proc { |value| self[name] = CODES[name][value] || value }
|
80
109
|
else
|
81
|
-
proc { |value| self[name] = value }
|
110
|
+
proc { |value| self[name] = value } # p name, value;
|
82
111
|
end
|
83
|
-
define_method "#{name}=", &setter
|
112
|
+
define_method "#{name}=", &setter if setter
|
84
113
|
|
85
114
|
# Define validator(s)
|
86
115
|
[body[:validate]].flatten.compact.each do |validator|
|
@@ -95,22 +124,7 @@ module IB
|
|
95
124
|
# TODO define self[:name] accessors for :virtual and :flag properties
|
96
125
|
|
97
126
|
else # setter given
|
98
|
-
define_property_methods name, :set => body
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
# Extending lighweight (not DB-backed) Model class to mimic AR::Base
|
103
|
-
unless ancestors.include? ActiveModel::Validations
|
104
|
-
include ActiveModel::Validations
|
105
|
-
|
106
|
-
def save
|
107
|
-
false
|
108
|
-
end
|
109
|
-
|
110
|
-
alias save! save
|
111
|
-
|
112
|
-
def self.find *args
|
113
|
-
[]
|
127
|
+
define_property_methods name, :set => body, :get => body
|
114
128
|
end
|
115
129
|
end
|
116
130
|
|