em-nordnet 0.0.1
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/.gitignore +18 -0
- data/.rspec +2 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +65 -0
- data/LICENSE +22 -0
- data/README.md +66 -0
- data/Rakefile +2 -0
- data/em-nordnet.gemspec +28 -0
- data/lib/em-nordnet.rb +46 -0
- data/lib/em-nordnet/account.rb +27 -0
- data/lib/em-nordnet/api.rb +234 -0
- data/lib/em-nordnet/config.rb +16 -0
- data/lib/em-nordnet/feed/connection.rb +53 -0
- data/lib/em-nordnet/feed/dsl.rb +56 -0
- data/lib/em-nordnet/feed/private.rb +27 -0
- data/lib/em-nordnet/feed/public.rb +43 -0
- data/lib/em-nordnet/goliath/feed_plugin.rb +25 -0
- data/lib/em-nordnet/goliath/private_feed.rb +8 -0
- data/lib/em-nordnet/goliath/public_feed.rb +18 -0
- data/lib/em-nordnet/instrument.rb +27 -0
- data/lib/em-nordnet/order.rb +48 -0
- data/lib/em-nordnet/version.rb +5 -0
- data/spec/em-nordnet/account_spec.rb +34 -0
- data/spec/em-nordnet/api_spec.rb +179 -0
- data/spec/em-nordnet/config_spec.rb +14 -0
- data/spec/em-nordnet/goliath/public_feed_spec.rb +11 -0
- data/spec/em-nordnet/instrument_spec.rb +35 -0
- data/spec/em-nordnet/order_spec.rb +27 -0
- data/spec/em-nordnet_spec.rb +46 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/api/headers.rb +15 -0
- data/spec/support/api/results.rb +205 -0
- data/spec/support/api/stubs.rb +137 -0
- data/spec/support/api/urls.rb +112 -0
- data/spec/support/api_helper.rb +4 -0
- data/spec/support/configure_helper.rb +19 -0
- metadata +208 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
module Em
|
2
|
+
module Nordnet
|
3
|
+
class Config < Struct.new :url, :user, :password, :pem, :service
|
4
|
+
def credentials
|
5
|
+
@credentials ||= generate_credentials
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
def generate_credentials
|
10
|
+
auth = [user, password, Time.now.to_i*1000]
|
11
|
+
encoded = auth.map { |val| Base64.encode64 val.to_s.strip }.join(':')
|
12
|
+
Base64.encode64 OpenSSL::PKey::RSA.new(pem).public_encrypt encoded
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Em
|
2
|
+
module Nordnet
|
3
|
+
module Feed
|
4
|
+
module Connection
|
5
|
+
attr_reader :session, :callbacks
|
6
|
+
|
7
|
+
def initialize(session)
|
8
|
+
@session = session
|
9
|
+
@callbacks = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def on_tick &block
|
13
|
+
@callbacks << block
|
14
|
+
end
|
15
|
+
|
16
|
+
def subscribe args
|
17
|
+
send_data generate(:subscribe, args)
|
18
|
+
end
|
19
|
+
|
20
|
+
def connection_completed
|
21
|
+
start_tls
|
22
|
+
end
|
23
|
+
|
24
|
+
def ssl_handshake_completed
|
25
|
+
params = { session_key: session, service: Nordnet.config.service }
|
26
|
+
send_data generate(:login, params)
|
27
|
+
end
|
28
|
+
|
29
|
+
def receive_data json
|
30
|
+
callbacks.each { |callback| callback.call parse(json) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def unbind
|
34
|
+
EventMachine::stop_event_loop
|
35
|
+
end
|
36
|
+
|
37
|
+
def send_data data
|
38
|
+
super "%s\n" % data # All writes must end with linefeed
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def generate command, args ={}
|
44
|
+
MultiJson.dump cmd: command.to_s, args: args
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse json
|
48
|
+
MultiJson.load(json, symbolize_keys: true)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Em
|
2
|
+
module Nordnet
|
3
|
+
module Feed
|
4
|
+
class DSL
|
5
|
+
attr_reader :socket, :session, :callbacks
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@callbacks = initialize_callbacks
|
9
|
+
|
10
|
+
socket.on_tick { |data| run_callback data }
|
11
|
+
end
|
12
|
+
|
13
|
+
def socket
|
14
|
+
@socket = EventMachine::connect hostname, port, Connection, session
|
15
|
+
end
|
16
|
+
|
17
|
+
def session
|
18
|
+
@session ||= Nordnet.api.session
|
19
|
+
end
|
20
|
+
|
21
|
+
def on_heartbeat &block
|
22
|
+
add_callback :heartbeat, block
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_callback type, block
|
26
|
+
@callbacks[type] << block
|
27
|
+
end
|
28
|
+
|
29
|
+
def types
|
30
|
+
raise NotImplementedError
|
31
|
+
end
|
32
|
+
|
33
|
+
def hostname
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
|
37
|
+
def port
|
38
|
+
raise NotImplementedError
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def initialize_callbacks
|
44
|
+
types.inject heartbeat: [] do |callbacks, type|
|
45
|
+
callbacks[type] = []
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def run_callback data
|
50
|
+
type, args = data[:cmd], data[:args]
|
51
|
+
callbacks[type].each { |callback| callback.call args }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Em
|
2
|
+
module Nordnet
|
3
|
+
module Feed
|
4
|
+
class Private < Em::Nordnet::Feed::DSL
|
5
|
+
def types
|
6
|
+
[:trade, :order]
|
7
|
+
end
|
8
|
+
|
9
|
+
def hostname
|
10
|
+
Nordnet.api.private_feed[:hostname]
|
11
|
+
end
|
12
|
+
|
13
|
+
def port
|
14
|
+
Nordnet.api.private_feed[:port]
|
15
|
+
end
|
16
|
+
|
17
|
+
def on_trade &block
|
18
|
+
add_callback :trade, block
|
19
|
+
end
|
20
|
+
|
21
|
+
def on_order &block
|
22
|
+
add_callback :order, block
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Em
|
2
|
+
module Nordnet
|
3
|
+
module Feed
|
4
|
+
class Public < Em::Nordnet::Feed::DSL
|
5
|
+
def types
|
6
|
+
[:price, :depth, :trade, :index, :news, :trading_status]
|
7
|
+
end
|
8
|
+
|
9
|
+
def hostname
|
10
|
+
Nordnet.api.public_feed[:hostname]
|
11
|
+
end
|
12
|
+
|
13
|
+
def port
|
14
|
+
Nordnet.api.public_feed[:port]
|
15
|
+
end
|
16
|
+
|
17
|
+
def on_price &block
|
18
|
+
add_callback :price, block
|
19
|
+
end
|
20
|
+
|
21
|
+
def on_trade &block
|
22
|
+
add_callback :trade, block
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_depth &block
|
26
|
+
add_callback :depth, block
|
27
|
+
end
|
28
|
+
|
29
|
+
def on_index &block
|
30
|
+
add_callback :index, block
|
31
|
+
end
|
32
|
+
|
33
|
+
def on_news &block
|
34
|
+
add_callback :news, block
|
35
|
+
end
|
36
|
+
|
37
|
+
def on_trading_status &block
|
38
|
+
add_callback :trading_status, block
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Em::Nordnet::Goliath
|
2
|
+
class FeedPlugin
|
3
|
+
@@connection = nil
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def connection
|
7
|
+
@@connection
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize port, config, status, logger
|
12
|
+
@port, @config, @status, @logger = port, config, status, logger
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
@@connection = feed_connection
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def feed_connection
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Em::Nordnet::Goliath
|
2
|
+
class PublicFeed < FeedPlugin
|
3
|
+
class << self
|
4
|
+
def subscribe type, instrument, &block
|
5
|
+
instrument.subscribe_params(type).tap do |args|
|
6
|
+
connection.add_callback type, &block
|
7
|
+
connection.subscribe args
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def feed_connection
|
15
|
+
Nordnet::Feed::Public.new
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Em
|
2
|
+
module Nordnet
|
3
|
+
class Instrument
|
4
|
+
attr_accessor :id, :market_id, :currency, :shortname, :is_in_code, :longname, :ticksize_id
|
5
|
+
|
6
|
+
def self.search symbol, options={}
|
7
|
+
search_options = {query: symbol, type: 'A', country: ['SE','US']}.merge options
|
8
|
+
results = Nordnet.api.instrument_search(search_options)
|
9
|
+
results.map { |attributes| new attributes }
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize attributes={}
|
13
|
+
self.id = attributes[:id] || attributes[:identifier]
|
14
|
+
self.market_id = attributes[:market_id] || attributes[:marketID]
|
15
|
+
self.currency = attributes[:currency]
|
16
|
+
self.shortname = attributes[:shortname]
|
17
|
+
self.longname = attributes[:longname]
|
18
|
+
self.is_in_code = attributes[:is_in_code] || attributes[:isinCode]
|
19
|
+
self.ticksize_id = attributes[:ticksize_id] || attributes[:ticksizeID]
|
20
|
+
end
|
21
|
+
|
22
|
+
def subscribe_params type
|
23
|
+
{ 't' => type.to_s, 'i' => id, 'm' => market_id }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Em
|
2
|
+
module Nordnet
|
3
|
+
class Order
|
4
|
+
BUY = 'BUY'.freeze
|
5
|
+
SELL = 'SELL'.freeze
|
6
|
+
|
7
|
+
attr_accessor :id, :account, :instrument, :price, :volume, :status
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def all account_id
|
11
|
+
orders = Nordnet.api.orders(account_id)
|
12
|
+
orders.map { |attributes| new attributes }
|
13
|
+
end
|
14
|
+
|
15
|
+
def buy attributes={}
|
16
|
+
symbol, price, volume = attributes[:symbol], attributes[:price], attributes[:volume]
|
17
|
+
|
18
|
+
instrument = Instrument.search(symbol).first
|
19
|
+
account = attributes[:account] || Account.default
|
20
|
+
|
21
|
+
order = Order.new account: account, instrument: instrument, price: price, volume: volume
|
22
|
+
order.buy!
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize attributes={}
|
27
|
+
self.id = attributes[:id] || attributes[:orderID]
|
28
|
+
self.account = attributes[:account]
|
29
|
+
self.instrument = attributes[:instrument]
|
30
|
+
self.price = attributes[:price]
|
31
|
+
self.volume = attributes[:volume]
|
32
|
+
self.status = attributes[:status]
|
33
|
+
end
|
34
|
+
|
35
|
+
def buy!
|
36
|
+
self.status = Nordnet.api.create_order account.id,
|
37
|
+
identifier: instrument.id,
|
38
|
+
marketID: instrument.market_id,
|
39
|
+
price: price,
|
40
|
+
volume: volume,
|
41
|
+
side: BUY,
|
42
|
+
currency: instrument.currency
|
43
|
+
|
44
|
+
status
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Em::Nordnet::Account do
|
4
|
+
let(:attributes) { { id: 123, alias: 'alias', default: true } }
|
5
|
+
before { Em::Nordnet.stub(:api).and_return stub(accounts: [attributes]) }
|
6
|
+
|
7
|
+
subject { Em::Nordnet::Account.new attributes }
|
8
|
+
|
9
|
+
describe 'initialization' do
|
10
|
+
its(:id) { should == 123 }
|
11
|
+
its(:alias) { should == 'alias' }
|
12
|
+
its(:default) { should be_true }
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '.all' do
|
16
|
+
it 'should returns a collection of accounts fetched with the API' do
|
17
|
+
Em::Nordnet::Account.all.first.should be_instance_of(Em::Nordnet::Account)
|
18
|
+
end
|
19
|
+
it 'should be memoized' do
|
20
|
+
Em::Nordnet::Account.all.should be Em::Nordnet::Account.all
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it '.default', 'should return the account that is set to default' do
|
25
|
+
account = Em::Nordnet::Account.default
|
26
|
+
account.should be_instance_of Em::Nordnet::Account
|
27
|
+
account.default.should be_true
|
28
|
+
end
|
29
|
+
|
30
|
+
it '#orders', 'should return a collection of orders connected to the account' do
|
31
|
+
Em::Nordnet::Order.should_receive(:all).with(123)
|
32
|
+
subject.orders
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Em::Nordnet::Api, type: 'api authenticated' do
|
4
|
+
let(:api) { Em::Nordnet::Api.new }
|
5
|
+
subject { api }
|
6
|
+
|
7
|
+
let(:account_id) { 4711 }
|
8
|
+
|
9
|
+
describe 'initialization' do
|
10
|
+
its(:credentials) { should_not be_nil }
|
11
|
+
its(:session) { should_not be_nil }
|
12
|
+
its(:expires_in) { should_not be_nil }
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '#login' do
|
16
|
+
its(:session) { should == 'cf147c690f65fe1068f40ef23f23648ebf8c7f02' }
|
17
|
+
its(:expires_in) { should == 300 }
|
18
|
+
it { api.login.should == login_result }
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#touch' do
|
22
|
+
before { stub_touch(api.session) }
|
23
|
+
it { api.touch.should == touch_result }
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#realtime_access' do
|
27
|
+
before { stub_realtime_access }
|
28
|
+
it { api.realtime_access.should == realtime_access_result }
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#news_sources' do
|
32
|
+
before { stub_news_sources }
|
33
|
+
it { api.news_sources.should == news_sources_result }
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#news_items' do
|
37
|
+
let(:params) { {query: 'ERIC'} }
|
38
|
+
before { stub_news_items params }
|
39
|
+
it { api.news_items.should == news_items_result }
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#news_item' do
|
43
|
+
let(:news_item_id) { 213 }
|
44
|
+
before { stub_news_item news_item_id }
|
45
|
+
it { api.news_item(news_item_id).should == news_item_result }
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '#accounts' do
|
49
|
+
before { stub_accounts }
|
50
|
+
it { api.accounts.should == accounts_result }
|
51
|
+
end
|
52
|
+
|
53
|
+
describe '#account' do
|
54
|
+
before { stub_account account_id }
|
55
|
+
it { api.account(account_id).should == account_result }
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#ledgers' do
|
59
|
+
before { stub_ledgers account_id }
|
60
|
+
it { api.ledgers(account_id).should == ledgers_result }
|
61
|
+
end
|
62
|
+
|
63
|
+
describe '#positions' do
|
64
|
+
before { stub_positions account_id }
|
65
|
+
it { api.positions(account_id).should == positions_result }
|
66
|
+
end
|
67
|
+
|
68
|
+
describe '#orders' do
|
69
|
+
before { stub_orders account_id }
|
70
|
+
it { api.orders(account_id).should == orders_result }
|
71
|
+
end
|
72
|
+
|
73
|
+
describe '#trades' do
|
74
|
+
before { stub_trades account_id }
|
75
|
+
it { api.trades(account_id).should == trades_result }
|
76
|
+
end
|
77
|
+
|
78
|
+
describe '#instrument_search' do
|
79
|
+
let(:params) { {query: 'abb', type: 'A', country: 'SE'} }
|
80
|
+
before { stub_instrument_search params }
|
81
|
+
it { api.instrument_search(params).should == instrument_search_result }
|
82
|
+
end
|
83
|
+
|
84
|
+
describe '#instrument_lookup' do
|
85
|
+
let(:params) { {identifier: 101, marketID: 11} }
|
86
|
+
before { stub_instrument_lookup params }
|
87
|
+
it { api.instrument_lookup(params).should == instrument_lookup_result }
|
88
|
+
end
|
89
|
+
|
90
|
+
describe '#multiple_instrument_lookup' do
|
91
|
+
let(:list) { "101,11;102,11;" }
|
92
|
+
before { stub_multiple_instrument_lookup list }
|
93
|
+
it { api.multiple_instrument_lookup(list).should == multiple_instrument_lookup_result }
|
94
|
+
end
|
95
|
+
|
96
|
+
describe '#chart_data' do
|
97
|
+
let(:params) { {identifier: 101, marketID: 11} }
|
98
|
+
before { stub_chart_data params }
|
99
|
+
it { api.chart_data(params).should == chart_data_result }
|
100
|
+
end
|
101
|
+
|
102
|
+
describe '#lists' do
|
103
|
+
before { stub_lists }
|
104
|
+
it { api.lists.should == lists_result }
|
105
|
+
end
|
106
|
+
|
107
|
+
describe '#list_items' do
|
108
|
+
let(:list_id) { 123 }
|
109
|
+
before { stub_list_items list_id }
|
110
|
+
it { api.list_items.should == list_items_result }
|
111
|
+
end
|
112
|
+
|
113
|
+
describe '#markets' do
|
114
|
+
before { stub_markets }
|
115
|
+
it { api.markets.should == markets_result }
|
116
|
+
end
|
117
|
+
|
118
|
+
describe '#trading_days' do
|
119
|
+
let(:market_id) { 32 }
|
120
|
+
before { stub_trading_days market_id }
|
121
|
+
it { api.trading_days(market_id).should == trading_days_result }
|
122
|
+
end
|
123
|
+
|
124
|
+
describe '#indices' do
|
125
|
+
before { stub_indices }
|
126
|
+
it { api.indices.should == indices_result }
|
127
|
+
end
|
128
|
+
|
129
|
+
describe '#ticksizes' do
|
130
|
+
let(:instrument_id) { 11002 }
|
131
|
+
before { stub_ticksizes instrument_id }
|
132
|
+
it { api.ticksizes(instrument_id).should == ticksizes_result }
|
133
|
+
end
|
134
|
+
|
135
|
+
describe '#countries' do
|
136
|
+
let(:derivative_type) { 'WNT' }
|
137
|
+
before { stub_countries derivative_type }
|
138
|
+
it { api.countries(derivative_type).should == countries_result }
|
139
|
+
end
|
140
|
+
|
141
|
+
describe '#underlyings' do
|
142
|
+
let(:derivative_type) { 'WNT' }
|
143
|
+
let(:country) { 'SE' }
|
144
|
+
before { stub_underlyings derivative_type, country }
|
145
|
+
it { api.underlyings(derivative_type, country).should == underlyings_result }
|
146
|
+
end
|
147
|
+
|
148
|
+
describe '#derivatives' do
|
149
|
+
let(:derivative_type) { 'WNT' }
|
150
|
+
let(:params) { {identifier: 101, marketID: 11} }
|
151
|
+
before { stub_derivatives derivative_type, params }
|
152
|
+
it { api.derivatives(derivative_type, params).should == derivatives_result }
|
153
|
+
end
|
154
|
+
|
155
|
+
describe '#related_markets' do
|
156
|
+
let(:params) { {identifier: 101, marketID: 11} }
|
157
|
+
before { stub_related_markets params }
|
158
|
+
it { api.related_markets(params).should == related_markets_result }
|
159
|
+
end
|
160
|
+
|
161
|
+
describe '#create_order' do
|
162
|
+
let(:params) { { identifier: 101, marketID: 11, price: 60, volume: 200, side: 'BUY', currency: 'SEK' } }
|
163
|
+
before { stub_create_order account_id, params }
|
164
|
+
it { api.create_order(account_id, params).should == create_order_result }
|
165
|
+
end
|
166
|
+
|
167
|
+
describe '#modify_order' do
|
168
|
+
let(:order_id) { 6670 }
|
169
|
+
let(:params) { { identifier: 101, marketID: 11, price: 100, volume: 200, side: 'BUY', currency: 'SEK' } }
|
170
|
+
before { stub_modify_order account_id, order_id, params }
|
171
|
+
it { api.modify_order(account_id, order_id, params).should == modify_order_result }
|
172
|
+
end
|
173
|
+
|
174
|
+
describe '#destroy_order' do
|
175
|
+
let(:order_id) { 6670 }
|
176
|
+
before { stub_destroy_order account_id, order_id }
|
177
|
+
it { api.destroy_order(account_id, order_id).should == destroy_order_result }
|
178
|
+
end
|
179
|
+
end
|