ig_markets 0.19 → 0.20
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +25 -3
- data/lib/ig_markets.rb +16 -1
- data/lib/ig_markets/cli/commands/self_test_command.rb +78 -59
- data/lib/ig_markets/cli/commands/stream_command.rb +69 -121
- data/lib/ig_markets/cli/curses_window.rb +96 -0
- data/lib/ig_markets/cli/main.rb +3 -0
- data/lib/ig_markets/cli/tables/table.rb +7 -0
- data/lib/ig_markets/deal_confirmation.rb +2 -1
- data/lib/ig_markets/dealing_platform.rb +20 -13
- data/lib/ig_markets/dealing_platform/streaming_methods.rb +173 -0
- data/lib/ig_markets/model/typecasters.rb +1 -0
- data/lib/ig_markets/position.rb +1 -0
- data/lib/ig_markets/streaming/account_state.rb +190 -0
- data/lib/ig_markets/streaming/account_update.rb +20 -0
- data/lib/ig_markets/streaming/chart_tick_update.rb +19 -0
- data/lib/ig_markets/streaming/consolidated_chart_data_update.rb +32 -0
- data/lib/ig_markets/streaming/market_subscription_manager.rb +86 -0
- data/lib/ig_markets/streaming/market_update.rb +21 -0
- data/lib/ig_markets/streaming/position_update.rb +23 -0
- data/lib/ig_markets/streaming/subscription.rb +122 -0
- data/lib/ig_markets/streaming/working_order_update.rb +23 -0
- data/lib/ig_markets/version.rb +1 -1
- data/lib/ig_markets/watchlist.rb +1 -1
- metadata +29 -4
@@ -0,0 +1,96 @@
|
|
1
|
+
module IGMarkets
|
2
|
+
module CLI
|
3
|
+
# This helper class supports the display of text in a fullscreen curses window.
|
4
|
+
#
|
5
|
+
# @private
|
6
|
+
class CursesWindow
|
7
|
+
# Initializes this fullscreen curses window.
|
8
|
+
def initialize
|
9
|
+
self.class.prepare
|
10
|
+
|
11
|
+
@window = Curses::Window.new 0, 0, 0, 0
|
12
|
+
@position = [0, 0]
|
13
|
+
end
|
14
|
+
|
15
|
+
# Clears the contents of this curses window and resets the cursor to the top left.
|
16
|
+
def clear
|
17
|
+
@window.clear
|
18
|
+
@position = [0, 0]
|
19
|
+
end
|
20
|
+
|
21
|
+
# Refreshes this curses window so its content is updated on the screen.
|
22
|
+
def refresh
|
23
|
+
@window.refresh
|
24
|
+
end
|
25
|
+
|
26
|
+
# Prints the specified lines in this fullscreen curses window.
|
27
|
+
#
|
28
|
+
# @param [Array<String>] lines The lines to print.
|
29
|
+
def print_lines(*lines)
|
30
|
+
change_foreground_color nil
|
31
|
+
|
32
|
+
lines.flatten.each do |line|
|
33
|
+
print_next_line_segment line until line.empty?
|
34
|
+
|
35
|
+
@position[0] += 1
|
36
|
+
@position[1] = 0
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
class << self
|
43
|
+
# Returns whether curses support is available. On Windows the 'curses' gem is optional and so this class may not
|
44
|
+
# be usable.
|
45
|
+
def available?
|
46
|
+
Kernel.require 'curses'
|
47
|
+
true
|
48
|
+
rescue LoadError
|
49
|
+
false
|
50
|
+
end
|
51
|
+
|
52
|
+
def prepare
|
53
|
+
raise IGMarketsError, 'curses gem is not installed' unless available?
|
54
|
+
|
55
|
+
return if @prepared
|
56
|
+
|
57
|
+
Curses.noecho
|
58
|
+
Curses.nonl
|
59
|
+
Curses.stdscr.nodelay = 1
|
60
|
+
Curses.init_screen
|
61
|
+
Curses.start_color
|
62
|
+
|
63
|
+
8.times { |color| Curses.init_pair (30 + color), color, 0 }
|
64
|
+
|
65
|
+
@prepared = true
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
COLORIZE_REGEXP = /^\e\[0(?:;(\d+);49)?m/
|
70
|
+
|
71
|
+
def print_next_line_segment(line)
|
72
|
+
match = line.match COLORIZE_REGEXP
|
73
|
+
|
74
|
+
if match
|
75
|
+
change_foreground_color match.captures.first
|
76
|
+
line[/^[^m]+m/] = ''
|
77
|
+
else
|
78
|
+
print_character line[0]
|
79
|
+
line[0] = ''
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def change_foreground_color(colorize_number)
|
84
|
+
colorize_number ||= 37
|
85
|
+
|
86
|
+
@window.attron Curses.color_pair(colorize_number.to_i) | Curses::A_NORMAL
|
87
|
+
end
|
88
|
+
|
89
|
+
def print_character(character)
|
90
|
+
@window.setpos @position[0], @position[1]
|
91
|
+
@window << character
|
92
|
+
@position[1] += 1
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/ig_markets/cli/main.rb
CHANGED
@@ -19,6 +19,9 @@ module IGMarkets
|
|
19
19
|
desc 'sprints [SUBCOMAND=list ...]', 'Command for working with sprint market positions'
|
20
20
|
subcommand 'sprints', Sprints
|
21
21
|
|
22
|
+
desc 'stream [SUBCOMAND=dashboard ...]', 'Command for working with display of streaming data'
|
23
|
+
subcommand 'stream', Stream
|
24
|
+
|
22
25
|
desc 'watchlists [SUBCOMAND=list ...]', 'Command for working with watchlists'
|
23
26
|
subcommand 'watchlists', Watchlists
|
24
27
|
|
@@ -7,13 +7,14 @@ module IGMarkets
|
|
7
7
|
attribute :status, Symbol, allowed_values: [:amended, :deleted, :fully_closed, :opened, :partially_closed]
|
8
8
|
end
|
9
9
|
|
10
|
+
attribute :account_id
|
10
11
|
attribute :affected_deals, AffectedDeal
|
11
12
|
attribute :date, Time, format: '%FT%T.%L'
|
12
13
|
attribute :deal_id
|
13
14
|
attribute :deal_reference
|
14
15
|
attribute :deal_status, Symbol, allowed_values: [:accepted, :fund_account, :rejected]
|
15
16
|
attribute :direction, Symbol, allowed_values: [:buy, :sell]
|
16
|
-
attribute :epic
|
17
|
+
attribute :epic, String, regex: Regex::EPIC
|
17
18
|
attribute :expiry, Date, nil_if: %w(- DFB), format: ['%d-%b-%y', '%b-%y']
|
18
19
|
attribute :guaranteed_stop, Boolean
|
19
20
|
attribute :level, Float
|
@@ -9,6 +9,7 @@ module IGMarkets
|
|
9
9
|
# - {#sprint_market_positions}
|
10
10
|
# - {#watchlists}
|
11
11
|
# - {#working_orders}
|
12
|
+
# - {#streaming}
|
12
13
|
#
|
13
14
|
# See `README.md` for examples.
|
14
15
|
#
|
@@ -59,6 +60,11 @@ module IGMarkets
|
|
59
60
|
# @return [WorkingOrderMethods]
|
60
61
|
attr_reader :working_orders
|
61
62
|
|
63
|
+
# Methods for working with live streaming of IG Markets data.
|
64
|
+
#
|
65
|
+
# @return [StreamingMethods]
|
66
|
+
attr_reader :streaming
|
67
|
+
|
62
68
|
def initialize
|
63
69
|
@session = Session.new
|
64
70
|
|
@@ -69,6 +75,7 @@ module IGMarkets
|
|
69
75
|
@sprint_market_positions = SprintMarketPositionMethods.new self
|
70
76
|
@watchlists = WatchlistMethods.new self
|
71
77
|
@working_orders = WorkingOrderMethods.new self
|
78
|
+
@streaming = StreamingMethods.new self
|
72
79
|
end
|
73
80
|
|
74
81
|
# Signs in to the IG Markets Dealing Platform, either the live platform or the demo platform.
|
@@ -93,6 +100,7 @@ module IGMarkets
|
|
93
100
|
|
94
101
|
# Signs out of the IG Markets Dealing Platform, ending any current session.
|
95
102
|
def sign_out
|
103
|
+
streaming.disconnect
|
96
104
|
session.sign_out
|
97
105
|
end
|
98
106
|
|
@@ -119,19 +127,6 @@ module IGMarkets
|
|
119
127
|
instantiate_models Application, session.put('operations/application/disable')
|
120
128
|
end
|
121
129
|
|
122
|
-
# Creates a new Lightstreamer session instance from this dealing platform's {#session} that is ready to connect
|
123
|
-
# and start streaming account and market data.
|
124
|
-
#
|
125
|
-
# @return [Lightstreamer::Session, nil] The new Lightstreamer session instance, or `nil` if there is no active
|
126
|
-
# IG Markets session to connect through.
|
127
|
-
def lightstreamer_session
|
128
|
-
return nil unless session.alive?
|
129
|
-
|
130
|
-
Lightstreamer::Session.new server_url: client_account_summary.lightstreamer_endpoint,
|
131
|
-
username: client_account_summary.client_id,
|
132
|
-
password: "CST-#{session.client_security_token}|XST-#{session.x_security_token}"
|
133
|
-
end
|
134
|
-
|
135
130
|
# This method is used to instantiate the various `Model` subclasses from data returned by the IG Markets API. It
|
136
131
|
# recurses through arrays and sub-hashes present in `source`, instantiating the required models based on the types
|
137
132
|
# of each attribute as defined on the models. All model instances returned by this method will have their
|
@@ -161,6 +156,18 @@ module IGMarkets
|
|
161
156
|
end
|
162
157
|
end
|
163
158
|
|
159
|
+
# This method is the same as {#instantiate_models} but takes an unparsed JSON string as its input.
|
160
|
+
#
|
161
|
+
# @param [Class] model_class The top-level model class to create from `json`.
|
162
|
+
# @param [String] json The JSON string to parse.
|
163
|
+
#
|
164
|
+
# @return [nil, `model_class`] The resulting instantiated model.
|
165
|
+
#
|
166
|
+
# @private
|
167
|
+
def instantiate_models_from_json(model_class, json)
|
168
|
+
instantiate_models model_class, ResponseParser.parse(JSON.parse(json))
|
169
|
+
end
|
170
|
+
|
164
171
|
private
|
165
172
|
|
166
173
|
# This method is a helper for {#instantiate_models} that prepares a source object for instantiation.
|
@@ -0,0 +1,173 @@
|
|
1
|
+
module IGMarkets
|
2
|
+
class DealingPlatform
|
3
|
+
# Provides methods for working with streaming of IG Markets data. Returned by {DealingPlatform#streaming}.
|
4
|
+
class StreamingMethods
|
5
|
+
# Initializes this class with the specified dealing platform.
|
6
|
+
#
|
7
|
+
# @param [DealingPlatform] dealing_platform The dealing platform.
|
8
|
+
def initialize(dealing_platform)
|
9
|
+
@dealing_platform = dealing_platform
|
10
|
+
@queue = Queue.new
|
11
|
+
@on_error_callbacks = []
|
12
|
+
end
|
13
|
+
|
14
|
+
# Connects the streaming session. Raises a `Lightstreamer::LightstreamerError` if an error occurs.
|
15
|
+
def connect
|
16
|
+
@lightstreamer.disconnect if @lightstreamer
|
17
|
+
@queue.clear
|
18
|
+
|
19
|
+
@lightstreamer = Lightstreamer::Session.new username: username, password: password, server_url: server_url
|
20
|
+
@lightstreamer.on_error { |error| @on_error_callbacks.each { |callback| callback.call error } }
|
21
|
+
@lightstreamer.connect
|
22
|
+
end
|
23
|
+
|
24
|
+
# Disconnects the streaming session.
|
25
|
+
def disconnect
|
26
|
+
@lightstreamer.disconnect if @lightstreamer
|
27
|
+
@lightstreamer = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
# Adds the passed block to the list of callbacks that will be run when the streaming session encounters an error.
|
31
|
+
# The block will be called on a worker thread and so the code that is run by the block must be thread-safe.
|
32
|
+
# The argument passed to the block is `|error|`, which will be a `Lightstreamer::LightstreamerError` subclass
|
33
|
+
# detailing the error that occurred.
|
34
|
+
#
|
35
|
+
# @param [Proc] callback The callback that is to be run.
|
36
|
+
def on_error(&callback)
|
37
|
+
@on_error_callbacks << callback
|
38
|
+
end
|
39
|
+
|
40
|
+
# Creates a new subscription for balance updates on the specified account(s). The returned
|
41
|
+
# {Streaming::Subscription} must be passed to {#start_subscriptions} in order to actually start streaming its
|
42
|
+
# data.
|
43
|
+
#
|
44
|
+
# @param [Array<#account_id>, nil] accounts The accounts to create a subscription for. If this is `nil` then the
|
45
|
+
# new subscription will apply to all the accounts for the active client.
|
46
|
+
#
|
47
|
+
# @return [Streaming::Subscription]
|
48
|
+
def build_accounts_subscription(accounts = nil)
|
49
|
+
accounts ||= @dealing_platform.client_account_summary.accounts
|
50
|
+
|
51
|
+
items = Array(accounts).map { |account| "ACCOUNT:#{account.account_id}" }
|
52
|
+
fields = [:available_cash, :available_to_deal, :deposit, :equity, :equity_used, :funds, :margin, :margin_lr,
|
53
|
+
:margin_nlr, :pnl, :pnl_lr, :pnl_nlr]
|
54
|
+
|
55
|
+
build_subscription items: items, fields: fields, mode: :merge
|
56
|
+
end
|
57
|
+
|
58
|
+
# Creates a new Lightstreamer subscription for updates to the specified market(s). The returned
|
59
|
+
# {Streaming::Subscription} must be passed to {#start_subscriptions} in order to actually start streaming its
|
60
|
+
# data.
|
61
|
+
#
|
62
|
+
# @param [Array<String>] epics The EPICs of the markets to create a subscription for.
|
63
|
+
#
|
64
|
+
# @return [Streaming::Subscription]
|
65
|
+
def build_markets_subscription(epics)
|
66
|
+
items = Array(epics).map { |epic| "MARKET:#{epic}" }
|
67
|
+
fields = [:bid, :change, :change_pct, :high, :low, :market_delay, :market_state, :mid_open, :odds, :offer,
|
68
|
+
:strike_price, :update_time]
|
69
|
+
|
70
|
+
build_subscription items: items, fields: fields, mode: :merge
|
71
|
+
end
|
72
|
+
|
73
|
+
# Creates a new Lightstreamer subscription for trade, position and working order updates on the specified
|
74
|
+
# account(s). The returned {Streaming::Subscription} must be passed to {#start_subscriptions} in order to
|
75
|
+
# actually start streaming its data.
|
76
|
+
#
|
77
|
+
# @param [Array<#account_id>, nil] accounts The accounts to create a subscription for. If this is `nil` then the
|
78
|
+
# new subscription will apply to all the accounts for the active client.
|
79
|
+
#
|
80
|
+
# @return [Streaming::Subscription]
|
81
|
+
def build_trades_subscription(accounts = nil)
|
82
|
+
accounts ||= @dealing_platform.client_account_summary.accounts
|
83
|
+
|
84
|
+
items = Array(accounts).map { |account| "TRADE:#{account.account_id}" }
|
85
|
+
fields = [:confirms, :opu, :wou]
|
86
|
+
|
87
|
+
build_subscription items: items, fields: fields, mode: :distinct
|
88
|
+
end
|
89
|
+
|
90
|
+
# Creates a new Lightstreamer subscription for chart tick data for the specified EPICs. The returned
|
91
|
+
# {Streaming::Subscription} must be passed to {#start_subscriptions} in order to actually start streaming its
|
92
|
+
# data.
|
93
|
+
#
|
94
|
+
# @param [Array<String>] epics The EPICs of the markets to create a chart tick data subscription for.
|
95
|
+
#
|
96
|
+
# @return [Streaming::Subscription]
|
97
|
+
def build_chart_ticks_subscription(epics)
|
98
|
+
items = Array(epics).map { |epic| "CHART:#{epic}:TICK" }
|
99
|
+
fields = [:bid, :day_high, :day_low, :day_net_chg_mid, :day_open_mid, :day_perc_chg_mid, :ltp, :ltv, :ofr, :ttv,
|
100
|
+
:utm]
|
101
|
+
|
102
|
+
build_subscription items: items, fields: fields, mode: :distinct
|
103
|
+
end
|
104
|
+
|
105
|
+
# Creates a new Lightstreamer subscription for consolidated chart data for the specified EPIC and scale. The
|
106
|
+
# returned {Streaming::Subscription} must be passed to {#start_subscriptions} in order to actually start streaming
|
107
|
+
# its data.
|
108
|
+
#
|
109
|
+
# @param [String] epic The EPIC of the market to create a consolidated chart data subscription for.
|
110
|
+
# @param [:one_second, :one_minute, :five_minutes, :one_hour] scale The scale of the consolidated data.
|
111
|
+
#
|
112
|
+
# @return [Streaming::Subscription]
|
113
|
+
def build_consolidated_chart_data_subscription(epic, scale)
|
114
|
+
scale = { one_second: 'SECOND', one_minute: '1MINUTE', five_minutes: '5MINUTE', one_hour: 'HOUR' }.fetch scale
|
115
|
+
items = ["CHART:#{epic}:#{scale}"]
|
116
|
+
|
117
|
+
fields = [:bid_close, :bid_high, :bid_low, :bid_open, :cons_end, :cons_tick_count, :day_high, :day_low,
|
118
|
+
:day_net_chg_mid, :day_open_mid, :day_perc_chg_mid, :ltp_close, :ltp_high, :ltp_low, :ltp_open, :ltv,
|
119
|
+
:ofr_close, :ofr_high, :ofr_low, :ofr_open, :ttv, :utm]
|
120
|
+
|
121
|
+
build_subscription items: items, fields: fields, mode: :merge
|
122
|
+
end
|
123
|
+
|
124
|
+
# Starts streaming data from the passed Lightstreamer subscription(s). The return value indicates the error state,
|
125
|
+
# if any, for each subscription.
|
126
|
+
#
|
127
|
+
# @param [Array<Streaming::Subscription>] subscriptions
|
128
|
+
# @param [Hash] options The options to start the subscriptions with. See the documentation for
|
129
|
+
# `Lightstreamer::Subscription#start` for details of the accepted options.
|
130
|
+
#
|
131
|
+
# @return [Array<Lightstreamer::LightstreamerError, nil>] An array with one entry per subscription which indicates
|
132
|
+
# the error state returned for that subscription's start request, or `nil` if no error occurred.
|
133
|
+
def start_subscriptions(subscriptions, options = {})
|
134
|
+
lightstreamer_subscriptions = Array(subscriptions).compact.map(&:lightstreamer_subscription)
|
135
|
+
|
136
|
+
return if lightstreamer_subscriptions.empty?
|
137
|
+
|
138
|
+
@lightstreamer.start_subscriptions lightstreamer_subscriptions, options
|
139
|
+
end
|
140
|
+
|
141
|
+
# Stops streaming data for the specified subscription(s) and removes them from the streaming session.
|
142
|
+
#
|
143
|
+
# @param [Array<Lightstreamer::Subscription>] subscriptions The subscriptions to stop.
|
144
|
+
def remove_subscriptions(subscriptions)
|
145
|
+
lightstreamer_subscriptions = Array(subscriptions).compact.map(&:lightstreamer_subscription)
|
146
|
+
|
147
|
+
return if lightstreamer_subscriptions.empty?
|
148
|
+
|
149
|
+
@lightstreamer.remove_subscriptions lightstreamer_subscriptions
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
def username
|
155
|
+
@dealing_platform.client_account_summary.client_id
|
156
|
+
end
|
157
|
+
|
158
|
+
def password
|
159
|
+
"CST-#{@dealing_platform.session.client_security_token}|XST-#{@dealing_platform.session.x_security_token}"
|
160
|
+
end
|
161
|
+
|
162
|
+
def server_url
|
163
|
+
@dealing_platform.client_account_summary.lightstreamer_endpoint
|
164
|
+
end
|
165
|
+
|
166
|
+
def build_subscription(options)
|
167
|
+
subscription = @lightstreamer.build_subscription options
|
168
|
+
|
169
|
+
Streaming::Subscription.new @dealing_platform, subscription
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
data/lib/ig_markets/position.rb
CHANGED
@@ -7,6 +7,7 @@ module IGMarkets
|
|
7
7
|
attribute :created_date_utc, Time, format: '%FT%T'
|
8
8
|
attribute :currency, String, regex: Regex::CURRENCY
|
9
9
|
attribute :deal_id
|
10
|
+
attribute :deal_reference
|
10
11
|
attribute :direction, Symbol, allowed_values: [:buy, :sell]
|
11
12
|
attribute :level, Float
|
12
13
|
attribute :limit_level, Float
|
@@ -0,0 +1,190 @@
|
|
1
|
+
module IGMarkets
|
2
|
+
module Streaming
|
3
|
+
# This class tracks the current state of a dealing platform's accounts, positions and working orders by subscribing
|
4
|
+
# to the relevant streaming data feeds supported by the IG Markets streaming API. This is much more efficient than
|
5
|
+
# making repeated calls to the REST API to get current account, position or working order details, and also avoids
|
6
|
+
# running into API traffic allowance limits.
|
7
|
+
class AccountState
|
8
|
+
# The current state of the accounts. The balances will be updated automatically as new streaming data arrives.
|
9
|
+
#
|
10
|
+
# @return [Array<Account>]
|
11
|
+
attr_accessor :accounts
|
12
|
+
|
13
|
+
# The current positions for this account. This set of positions will be updated automatically as new streaming
|
14
|
+
# data arrives.
|
15
|
+
#
|
16
|
+
# @return [Array<Position>]
|
17
|
+
attr_accessor :positions
|
18
|
+
|
19
|
+
# The current working orders for this account. This set of working orders will be updated automatically as new
|
20
|
+
# streaming data arrives.
|
21
|
+
#
|
22
|
+
# @return [Array<WorkingOrder>]
|
23
|
+
attr_accessor :working_orders
|
24
|
+
|
25
|
+
# Initializes this account state with the specified dealing platform.
|
26
|
+
#
|
27
|
+
# @param [DealingPlatform] dealing_platform The dealing platform.
|
28
|
+
def initialize(dealing_platform)
|
29
|
+
@dealing_platform = dealing_platform
|
30
|
+
|
31
|
+
@data_queue = Queue.new
|
32
|
+
|
33
|
+
@market_subscriptions_manager = MarketSubscriptionManager.new dealing_platform
|
34
|
+
@market_subscriptions_manager.on_data { |_data, merged_data| @data_queue << [:on_market_update, merged_data] }
|
35
|
+
end
|
36
|
+
|
37
|
+
# Starts all the relevant streaming subscriptions that are needed to keep this account state up to date using live
|
38
|
+
# streaming updates. After this account state has been started the {#process_queued_data} method must be called
|
39
|
+
# repeatedly to process the queue of streaming updates and apply them to {#accounts}, {#positions} and
|
40
|
+
# {#working_orders} as appropriate.
|
41
|
+
def start
|
42
|
+
start_accounts_subscription
|
43
|
+
start_trades_subscription
|
44
|
+
|
45
|
+
@accounts = @dealing_platform.account.all
|
46
|
+
@positions = @dealing_platform.positions.all
|
47
|
+
@working_orders = @dealing_platform.working_orders.all
|
48
|
+
|
49
|
+
@trades_subscription.unsilence
|
50
|
+
|
51
|
+
update_market_subscriptions
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns whether there is any data waiting to be processed by a call to {#process_queued_data}
|
55
|
+
#
|
56
|
+
# @return [Boolean]
|
57
|
+
def data_to_process?
|
58
|
+
!@data_queue.empty?
|
59
|
+
end
|
60
|
+
|
61
|
+
# Processes all queued data and updates {#accounts}, {#positions} and {#working_orders} accordingly. If there are
|
62
|
+
# no queued data updates then this method blocks until at least one data update is available.
|
63
|
+
def process_queued_data
|
64
|
+
loop do
|
65
|
+
send(*@data_queue.pop)
|
66
|
+
|
67
|
+
break if @data_queue.empty?
|
68
|
+
end
|
69
|
+
|
70
|
+
update_market_subscriptions
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def start_accounts_subscription
|
76
|
+
@accounts_subscription = @dealing_platform.streaming.build_accounts_subscription
|
77
|
+
|
78
|
+
@accounts_subscription.on_data do |_data, merged_data|
|
79
|
+
@data_queue << [:on_account_update, merged_data]
|
80
|
+
end
|
81
|
+
|
82
|
+
@dealing_platform.streaming.start_subscriptions @accounts_subscription, snapshot: true
|
83
|
+
end
|
84
|
+
|
85
|
+
def start_trades_subscription
|
86
|
+
@trades_subscription = @dealing_platform.streaming.build_trades_subscription
|
87
|
+
|
88
|
+
@trades_subscription.on_data do |data|
|
89
|
+
@data_queue << [:on_position_update, data] if data.is_a? PositionUpdate
|
90
|
+
@data_queue << [:on_working_order_update, data] if data.is_a? WorkingOrderUpdate
|
91
|
+
end
|
92
|
+
|
93
|
+
@dealing_platform.streaming.start_subscriptions @trades_subscription, silent: true
|
94
|
+
end
|
95
|
+
|
96
|
+
def market_models
|
97
|
+
@positions + @working_orders
|
98
|
+
end
|
99
|
+
|
100
|
+
def update_market_subscriptions
|
101
|
+
@market_subscriptions_manager.epics = market_models.map { |model| model.market.epic }
|
102
|
+
end
|
103
|
+
|
104
|
+
def on_account_update(account_update)
|
105
|
+
@accounts.each do |account|
|
106
|
+
next unless account.account_id == account_update.account_id
|
107
|
+
|
108
|
+
account.balance.available = account_update.available_to_deal
|
109
|
+
account.balance.balance = account_update.equity
|
110
|
+
account.balance.deposit = account_update.margin
|
111
|
+
account.balance.profit_loss = account_update.pnl
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def on_market_update(market_update)
|
116
|
+
market_models.each do |model|
|
117
|
+
next unless model.market.epic == market_update.epic
|
118
|
+
|
119
|
+
attributes_to_copy = { bid: :bid, high: :high, low: :low, net_change: :change, offer: :offer,
|
120
|
+
percentage_change: :change_pct }
|
121
|
+
|
122
|
+
attributes_to_copy.each do |target_attribute, update_attribute|
|
123
|
+
model.market.send "#{target_attribute}=", market_update.send(update_attribute)
|
124
|
+
end
|
125
|
+
|
126
|
+
model.market.market_status = convert_market_status market_update.market_state if market_update.market_state
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def convert_market_status(status)
|
131
|
+
{ auction: :on_auction, auction_no_edit: :on_auction_no_edits, closed: :closed, edit: :edits_only,
|
132
|
+
offline: :offline, suspended: :suspended, tradeable: :tradeable }.fetch status
|
133
|
+
end
|
134
|
+
|
135
|
+
def on_position_update(position_update)
|
136
|
+
send "on_position_#{position_update.status}", position_update
|
137
|
+
end
|
138
|
+
|
139
|
+
def on_position_open(position_update)
|
140
|
+
return if @positions.any? { |position| position.deal_id == position_update.deal_id }
|
141
|
+
|
142
|
+
new_position = @dealing_platform.positions[position_update.deal_id]
|
143
|
+
|
144
|
+
@positions << new_position if new_position
|
145
|
+
end
|
146
|
+
|
147
|
+
def on_position_updated(position_update)
|
148
|
+
@positions.each do |position|
|
149
|
+
next unless position.deal_id == position_update.deal_id
|
150
|
+
|
151
|
+
position.limit_level = position_update.limit_level
|
152
|
+
position.stop_level = position_update.stop_level
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def on_position_deleted(position_update)
|
157
|
+
@positions.delete_if do |position|
|
158
|
+
position.deal_id == position_update.deal_id
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def on_working_order_update(working_order_update)
|
163
|
+
send "on_working_order_#{working_order_update.status}", working_order_update
|
164
|
+
end
|
165
|
+
|
166
|
+
def on_working_order_open(working_order_update)
|
167
|
+
return if @working_orders.any? { |working_order| working_order.deal_id == working_order_update.deal_id }
|
168
|
+
|
169
|
+
new_working_order = @dealing_platform.working_orders[working_order_update.deal_id]
|
170
|
+
|
171
|
+
@working_orders << new_working_order if new_working_order
|
172
|
+
end
|
173
|
+
|
174
|
+
def on_working_order_updated(working_order_update)
|
175
|
+
@working_orders.each do |working_order|
|
176
|
+
next unless working_order.deal_id == working_order_update.deal_id
|
177
|
+
|
178
|
+
working_order.limit_distance = working_order_update.limit_distance
|
179
|
+
working_order.stop_distance = working_order_update.stop_distance
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def on_working_order_deleted(working_order_update)
|
184
|
+
@working_orders.delete_if do |working_order|
|
185
|
+
working_order.deal_id == working_order_update.deal_id
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|