dtn 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +21 -5
  3. data/.env.example +5 -0
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +7 -0
  6. data/CHANGELOG.md +5 -1
  7. data/Gemfile +3 -1
  8. data/README.md +288 -9
  9. data/Rakefile +2 -0
  10. data/docker-compose.yml +34 -0
  11. data/dtn.gemspec +1 -1
  12. data/lib/dtn.rb +18 -1
  13. data/lib/dtn/concerns/id.rb +36 -0
  14. data/lib/dtn/concerns/validation.rb +71 -0
  15. data/lib/dtn/helpers/catalog.rb +32 -0
  16. data/lib/dtn/lookups/catalog/listed_markets.rb +12 -0
  17. data/lib/dtn/lookups/catalog/naic_codes.rb +12 -0
  18. data/lib/dtn/lookups/catalog/security_types.rb +12 -0
  19. data/lib/dtn/lookups/catalog/sic_codes.rb +12 -0
  20. data/lib/dtn/lookups/catalog/trade_conditions.rb +12 -0
  21. data/lib/dtn/lookups/historical/base.rb +43 -0
  22. data/lib/dtn/lookups/historical/daily_datapoint.rb +13 -0
  23. data/lib/dtn/lookups/historical/daily_timeframe.rb +34 -0
  24. data/lib/dtn/lookups/historical/datapoint.rb +27 -0
  25. data/lib/dtn/lookups/historical/interval.rb +14 -0
  26. data/lib/dtn/lookups/historical/interval_datapoint.rb +31 -0
  27. data/lib/dtn/lookups/historical/interval_day.rb +32 -0
  28. data/lib/dtn/lookups/historical/interval_timeframe.rb +37 -0
  29. data/lib/dtn/lookups/historical/monthly_datapoint.rb +13 -0
  30. data/lib/dtn/lookups/historical/tick.rb +14 -0
  31. data/lib/dtn/lookups/historical/tick_datapoint.rb +24 -0
  32. data/lib/dtn/lookups/historical/tick_day.rb +34 -0
  33. data/lib/dtn/lookups/historical/tick_timeframe.rb +31 -0
  34. data/lib/dtn/lookups/historical/weekly_datapoint.rb +13 -0
  35. data/lib/dtn/lookups/news/base.rb +85 -0
  36. data/lib/dtn/lookups/news/config.rb +25 -0
  37. data/lib/dtn/lookups/news/headline.rb +40 -0
  38. data/lib/dtn/lookups/news/story.rb +40 -0
  39. data/lib/dtn/lookups/news/story_count.rb +36 -0
  40. data/lib/dtn/lookups/request.rb +92 -0
  41. data/lib/dtn/lookups/symbol/base.rb +11 -0
  42. data/lib/dtn/lookups/symbol/by_filter.rb +58 -0
  43. data/lib/dtn/lookups/symbol/by_naic.rb +26 -0
  44. data/lib/dtn/lookups/symbol/by_sic.rb +26 -0
  45. data/lib/dtn/message.rb +29 -0
  46. data/lib/dtn/messages/bar/base.rb +30 -0
  47. data/lib/dtn/messages/bar/current_bar.rb +11 -0
  48. data/lib/dtn/messages/bar/historical_bar.rb +11 -0
  49. data/lib/dtn/messages/bar/update_bar.rb +11 -0
  50. data/lib/dtn/messages/catalog/code.rb +22 -0
  51. data/lib/dtn/messages/catalog/listed_markets.rb +20 -0
  52. data/lib/dtn/messages/catalog/naic_codes.rb +10 -0
  53. data/lib/dtn/messages/catalog/security_types.rb +20 -0
  54. data/lib/dtn/messages/catalog/sic_codes.rb +10 -0
  55. data/lib/dtn/messages/catalog/trade_conditions.rb +20 -0
  56. data/lib/dtn/messages/historical/daily_weekly_monthly.rb +25 -0
  57. data/lib/dtn/messages/historical/interval.rb +28 -0
  58. data/lib/dtn/messages/historical/tick.rb +31 -0
  59. data/lib/dtn/messages/level2/level2_update.rb +38 -0
  60. data/lib/dtn/messages/level2/market_maker_name.rb +20 -0
  61. data/lib/dtn/messages/message_with_simple_parser.rb +38 -0
  62. data/lib/dtn/messages/news/base.rb +34 -0
  63. data/lib/dtn/messages/news/config.rb +24 -0
  64. data/lib/dtn/messages/news/headline.rb +25 -0
  65. data/lib/dtn/messages/news/story.rb +20 -0
  66. data/lib/dtn/messages/news/story_count.rb +21 -0
  67. data/lib/dtn/messages/quote/level1.rb +150 -0
  68. data/lib/dtn/messages/quote/level1_fundamental.rb +17 -0
  69. data/lib/dtn/messages/quote/level1_news.rb +27 -0
  70. data/lib/dtn/messages/quote/level1_regional.rb +31 -0
  71. data/lib/dtn/messages/quote/level1_summary.rb +19 -0
  72. data/lib/dtn/messages/quote/level1_update.rb +21 -0
  73. data/lib/dtn/messages/symbol/base.rb +35 -0
  74. data/lib/dtn/messages/symbol/by_filter.rb +11 -0
  75. data/lib/dtn/messages/symbol/by_naic.rb +22 -0
  76. data/lib/dtn/messages/symbol/by_sic.rb +22 -0
  77. data/lib/dtn/messages/system/client_stats.rb +46 -0
  78. data/lib/dtn/messages/system/customer_info.rb +30 -0
  79. data/lib/dtn/messages/system/end_of_message_characters.rb +22 -0
  80. data/lib/dtn/messages/system/error.rb +20 -0
  81. data/lib/dtn/messages/system/generic.rb +98 -0
  82. data/lib/dtn/messages/system/no_data_characters.rb +22 -0
  83. data/lib/dtn/messages/system/stats.rb +38 -0
  84. data/lib/dtn/messages/system/symbol_not_found.rb +19 -0
  85. data/lib/dtn/messages/system/timestamp.rb +16 -0
  86. data/lib/dtn/messages/unknown.rb +15 -0
  87. data/lib/dtn/registry.rb +57 -0
  88. data/lib/dtn/streaming/client.rb +105 -0
  89. data/lib/dtn/streaming/clients/admin.rb +20 -0
  90. data/lib/dtn/streaming/clients/bar.rb +49 -0
  91. data/lib/dtn/streaming/clients/level2.rb +25 -0
  92. data/lib/dtn/streaming/clients/quote.rb +57 -0
  93. data/lib/dtn/streaming/messages_recorder_observer.rb +26 -0
  94. data/lib/dtn/streaming/request.rb +27 -0
  95. data/lib/dtn/streaming/request_builder.rb +57 -0
  96. data/lib/dtn/streaming/requests/admin/register_client_app.rb +24 -0
  97. data/lib/dtn/streaming/requests/admin/remove_client_app.rb +22 -0
  98. data/lib/dtn/streaming/requests/admin/save_login_info.rb +18 -0
  99. data/lib/dtn/streaming/requests/admin/set_autoconnect.rb +18 -0
  100. data/lib/dtn/streaming/requests/admin/set_client_stats.rb +20 -0
  101. data/lib/dtn/streaming/requests/admin/set_loginid.rb +21 -0
  102. data/lib/dtn/streaming/requests/admin/set_password.rb +21 -0
  103. data/lib/dtn/streaming/requests/bar/unwatch.rb +18 -0
  104. data/lib/dtn/streaming/requests/bar/unwatch_all.rb +16 -0
  105. data/lib/dtn/streaming/requests/bar/watch.rb +81 -0
  106. data/lib/dtn/streaming/requests/bar/watches.rb +21 -0
  107. data/lib/dtn/streaming/requests/level2/connect.rb +16 -0
  108. data/lib/dtn/streaming/requests/level2/disconnect.rb +16 -0
  109. data/lib/dtn/streaming/requests/level2/market_maker_by_id.rb +18 -0
  110. data/lib/dtn/streaming/requests/level2/unwatch.rb +18 -0
  111. data/lib/dtn/streaming/requests/level2/watch.rb +18 -0
  112. data/lib/dtn/streaming/requests/quote/all_update_fieldnames.rb +16 -0
  113. data/lib/dtn/streaming/requests/quote/connect.rb +16 -0
  114. data/lib/dtn/streaming/requests/quote/current_update_fieldnames.rb +16 -0
  115. data/lib/dtn/streaming/requests/quote/fundamental_fieldnames.rb +16 -0
  116. data/lib/dtn/streaming/requests/quote/news_switch.rb +18 -0
  117. data/lib/dtn/streaming/requests/quote/refresh.rb +29 -0
  118. data/lib/dtn/streaming/requests/quote/regional_switch.rb +26 -0
  119. data/lib/dtn/streaming/requests/quote/set_client_name.rb +16 -0
  120. data/lib/dtn/streaming/requests/quote/set_protocol.rb +16 -0
  121. data/lib/dtn/streaming/requests/quote/timestamp.rb +21 -0
  122. data/lib/dtn/streaming/requests/quote/timestamp_switch.rb +18 -0
  123. data/lib/dtn/streaming/requests/quote/trades.rb +21 -0
  124. data/lib/dtn/streaming/requests/quote/unwatch.rb +22 -0
  125. data/lib/dtn/streaming/requests/quote/unwatch_all.rb +16 -0
  126. data/lib/dtn/streaming/requests/quote/update_fields.rb +40 -0
  127. data/lib/dtn/streaming/requests/quote/watch.rb +22 -0
  128. data/lib/dtn/streaming/requests/quote/watches.rb +21 -0
  129. data/lib/dtn/version.rb +1 -1
  130. data/lib/ext/business_day.rb +15 -0
  131. data/lib/tasks/spec_date.rake +13 -0
  132. metadata +126 -6
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Messages
5
+ module System
6
+ # Empty response abstraction
7
+ class NoDataCharacters < MessageWithSimpleParser
8
+ class << self
9
+ def fields
10
+ @fields ||= {
11
+ request_id: :to_i
12
+ }
13
+ end
14
+ end
15
+
16
+ def termination?
17
+ true
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Messages
5
+ module System
6
+ # All stats
7
+ class Stats < MessageWithSimpleParser
8
+ class << self
9
+ # rubocop:disable Metrics/MethodLength
10
+ def fields
11
+ @fields ||= {
12
+ server_ip: :to_s,
13
+ server_port: :to_i,
14
+ max_symbols: :to_i,
15
+ number_of_symbols: :to_i,
16
+ clients_connected: :to_i,
17
+ seconds_since_last_update: :to_i,
18
+ reconnections: :to_i,
19
+ attemptedReconnections: :to_i,
20
+ start_time: :to_datetime,
21
+ market_time: :to_datetime,
22
+ status: :to_s,
23
+ iq_feed_version: :to_s,
24
+ loginId: :to_s,
25
+ totalKBsRecv: :to_f,
26
+ kbsPerSecRecv: :to_f,
27
+ avgKBsPerSecRecv: :to_f,
28
+ totalKBsSent: :to_f,
29
+ kbsPerSecSent: :to_f,
30
+ avgKBsPerSecSent: :to_f
31
+ }
32
+ end
33
+ # rubocop:enable Metrics/MethodLength
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Messages
5
+ module System
6
+ # Not found symbol for streaming
7
+ class SymbolNotFound < MessageWithSimpleParser
8
+ class << self
9
+ def fields
10
+ @fields ||= {
11
+ _skip: :nil,
12
+ symbol: :to_s
13
+ }
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Messages
5
+ module System
6
+ # Parsed timestamp
7
+ class Timestamp < Message
8
+ class << self
9
+ def parse(line:, **)
10
+ new timestamp: line[2..].to_datetime
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Messages
5
+ # Just in case we got something unexpected.
6
+ # in the best world should be never executed.
7
+ class Unknown < Message
8
+ class << self
9
+ def parse(line:, **)
10
+ new(line: line)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ # Abstract thread safe registry
5
+ class Registry
6
+ include Enumerable
7
+ extend Forwardable
8
+
9
+ delegate delete: :@items,
10
+ size: :@items,
11
+ clear: :@items
12
+
13
+ attr_reader :name
14
+
15
+ def initialize(name:)
16
+ @name = name
17
+ @items = Concurrent::Map.new
18
+ end
19
+
20
+ def clear
21
+ @items.clear
22
+ end
23
+
24
+ def each(&block)
25
+ @items.values.uniq.each(&block)
26
+ end
27
+
28
+ def find(item_name)
29
+ @items.fetch(item_name)
30
+ rescue KeyError => e
31
+ raise key_error_with_custom_message(e, item_name)
32
+ end
33
+
34
+ alias [] find
35
+
36
+ def register(name, item)
37
+ return unless name
38
+
39
+ @items[name] = item
40
+ end
41
+
42
+ alias []= register
43
+
44
+ def registered?(name)
45
+ @items.key?(name)
46
+ end
47
+
48
+ private
49
+
50
+ def key_error_with_custom_message(key_error, item_name)
51
+ message = key_error.message.sub("key not found", %(#{@name} not registered: "#{item_name}"))
52
+ error = KeyError.new(message)
53
+ error.set_backtrace(key_error.backtrace)
54
+ error
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Streaming
5
+ # Top level client abstraction. Different streams are available on different
6
+ # ports, so we can use it and follow the same pattern
7
+ class Client
8
+ # Status helper methods
9
+ module Status
10
+ STATUSES = { run: :running, stop: :stopped, initializing: :initialized }.freeze
11
+
12
+ STATUSES.each do |k, v|
13
+ define_method(k) { self.status = v }
14
+ define_method("#{v}?") { status == v }
15
+ end
16
+
17
+ attr_reader :status
18
+
19
+ protected
20
+
21
+ attr_writer :status
22
+ end
23
+
24
+ include Status
25
+
26
+ PROTOCOL_VERSION = "6.1"
27
+
28
+ CLIENT_TERMINATION_SIGNALS = %w[TERM INT].freeze
29
+
30
+ COMMON_SUPPORTED_MESSAGES = {
31
+ "S" => Messages::System::Generic,
32
+ "T" => Messages::System::Timestamp,
33
+ "n" => Messages::System::SymbolNotFound,
34
+ "E" => Messages::System::Error
35
+ }.freeze
36
+
37
+ # @params name Specify name for this client
38
+ # @params start_engine auto start engine, which is processing messages
39
+ def initialize(name: nil, start_engine: true)
40
+ @name = name || SecureRandom.alphanumeric(10)
41
+
42
+ initializing
43
+
44
+ init_connection
45
+ setup_signals
46
+ engine if start_engine
47
+ end
48
+
49
+ attr_reader :name
50
+
51
+ # We are able to filer the incoming data with custom fields
52
+ # using
53
+ attr_accessor :quote_update_fields
54
+
55
+ def request
56
+ RequestBuilder.new(client: self)
57
+ end
58
+
59
+ def observers
60
+ @observers ||= Set.new
61
+ end
62
+
63
+ def to_s
64
+ "Client name: #{name}, status: #{status}"
65
+ end
66
+
67
+ def socket
68
+ @socket ||= TCPSocket.open(Dtn.host, self.class::PORT)
69
+ end
70
+
71
+ def engine
72
+ @engine ||= Thread.new do
73
+ run
74
+ while running? && (line = socket.gets)
75
+ process_line(line: line)
76
+ end
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def process_line(line:)
83
+ message_class(line: line).parse(line: line, client: self).tap do |message|
84
+ observers.each do |obs|
85
+ obs.public_send(message.callback_name, message: message) if obs.respond_to?(message.callback_name)
86
+ end
87
+ end
88
+ end
89
+
90
+ def message_class(line:)
91
+ self.class::SUPPORTED_MESSAGES[line[0]] || Messages::Unknown
92
+ end
93
+
94
+ def init_connection
95
+ socket
96
+ end
97
+
98
+ def setup_signals
99
+ CLIENT_TERMINATION_SIGNALS.each do |signal|
100
+ Signal.trap(signal) { stop }
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Streaming
5
+ module Clients
6
+ # Provides a connection to IQFeed's Administrative socket.
7
+ #
8
+ # Is used to find out the health of the feed, figure out the
9
+ # status of each connection made to IQFeed, and also set various parameters
10
+ # to the feed.
11
+ #
12
+ # See www.iqfeed.net/dev/api/docs/AdminviaTCPIP.cfm
13
+ class Admin < Client
14
+ PORT = 9300
15
+
16
+ SUPPORTED_MESSAGES = COMMON_SUPPORTED_MESSAGES
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Streaming
5
+ module Clients
6
+ # Let's you get live data as interval bar data.
7
+ #
8
+ # If you are using interval bars for trading, use this class if you want
9
+ # IQFeed to calculate the interval bars for you and send you interval bars
10
+ # instead of (or in addition to) receiving every tick. For example, you may
11
+ # want to get open, high, low, close data for each minute or every 50 trades.
12
+ #
13
+ # The length of the interval can be in time units, number of trades units
14
+ # or volume traded units as for bars from HistoryConn.
15
+ # If you want historical bars, use HistoryConn instead. This class
16
+ # allows you to get some history, for example if you want the past 5
17
+ # days's bars to fill in a data structure before you start getting live
18
+ # data updates. But if you want historical data for back-testing or some
19
+ # such, you are better off using HistoryConn instead.
20
+ #
21
+ # Since most historical data that IQFeed gives you is bar data, if you are
22
+ # just getting started, it may be a good idea to save some live tick-data and
23
+ # bar-data and compare them so you understand exactly how IQFeed is
24
+ # filtering ticks and generating it's bars. Different data providers tend to
25
+ # do this differently, dome better than others and the documentation usually
26
+ # doesn't get updated when things are changed.
27
+ #
28
+ # For more info, see:
29
+ # www.iqfeed.net/dev/api/docs/Derivatives_Overview.cfm
30
+ # and
31
+ # www.iqfeed.net/dev/api/docs/Derivatives_StreamingIntervalBars_TCPIP.cfm
32
+ class Bar < Client
33
+ PORT = 9400
34
+
35
+ SUPPORTED_MESSAGES = COMMON_SUPPORTED_MESSAGES.merge(
36
+ "BH" => Messages::Bar::HistoricalBar,
37
+ "BC" => Messages::Bar::CurrentBar,
38
+ "BU" => Messages::Bar::UpdateBar
39
+ ).freeze
40
+
41
+ def message_class(line:)
42
+ self.class::SUPPORTED_MESSAGES[line[0]] ||
43
+ ((line =~ /\d+,(\w+),.+/) && self.class::SUPPORTED_MESSAGES[Regexp.last_match(1)]) ||
44
+ Messages::Unknown
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Streaming
5
+ module Clients
6
+ # Provides a connection to IQFeed's Level2 socket.
7
+ class Level2 < Client
8
+ PORT = 9200
9
+
10
+ SUPPORTED_MESSAGES = COMMON_SUPPORTED_MESSAGES.merge(
11
+ "Z" => Messages::Level2::Level2Update, # Summary message, but it's actually the same
12
+ "2" => Messages::Level2::Level2Update,
13
+ "U" => Messages::Level2::Level2Update,
14
+ "M" => Messages::Level2::MarketMakerName # A Market Maker name OR order book level query response message.
15
+ ).freeze
16
+
17
+ private
18
+
19
+ def init_connection
20
+ request.level2.connect
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Streaming
5
+ module Clients
6
+ # Quote provides real-time Level 1 data and real-time news.
7
+ #
8
+ # Quote provides access to top-of-book quotes, regional quotes
9
+ # (quotes from a single exchange), fundamentals (which includes
10
+ # reference) data, streaming real-time news.
11
+ #
12
+ # Quotes, Regional, Trades and Fundamental data is provided as a messages
13
+ # since you are likely going to do something fancy with it.
14
+ #
15
+ #
16
+ # READ THIS CAREFULLY: For quote updates (provided when the top of book
17
+ # quote changes or a trade happens) IQFeed.exe can send dynamic fieldsets.
18
+ # This means that you can ask for any fields (subset of the set available)
19
+ # you want. This map to `Messages::Quote::Level1::ALL_FIELDS` which lists
20
+ # all available fields which will be used as a field name by DTN.
21
+ # The values corresponding to each
22
+ # key a tuple of (FieldName used by DTN, FieldName used in Structured Array,
23
+ # numpy scalar type used for that field).
24
+ #
25
+ # We start with a default set of fields (same as default in the IQFeed
26
+ # docs. It will be fetch once client starts. If you want a different
27
+ # set of fields, call
28
+ # `client.request.quote.update_fields list: %i[Bid Ask]` with the
29
+ # fieldnames you want. If you want a different set of fieldnames for options
30
+ # and stocks, create two clients. Use one for all stock subscriptions and
31
+ # one for all options subscriptions. They can both use the same observer if
32
+ # that is what you want.
33
+ #
34
+ # If you don't understand the above two paragraphs, look at the code, look
35
+ # at the examples, run the examples and then read the above again.
36
+ class Quote < Client
37
+ PORT = 5009
38
+
39
+ SUPPORTED_MESSAGES = COMMON_SUPPORTED_MESSAGES.merge(
40
+ "F" => Messages::Quote::Level1Fundamental,
41
+ "P" => Messages::Quote::Level1Summary,
42
+ "Q" => Messages::Quote::Level1Update,
43
+ "R" => Messages::Quote::Level1Regional,
44
+ "N" => Messages::Quote::Level1News
45
+ ).freeze
46
+
47
+ private
48
+
49
+ def init_connection
50
+ request.quote.set_client_name(name: name)
51
+ request.quote.current_update_fieldnames
52
+ request.quote.set_protocol
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Streaming
5
+ # A sample class, which will record all messages, which were
6
+ # invoked by the client.
7
+ # It will spy and record all invocations for simpler further analyses
8
+ #
9
+ # It's costly for production use, but very convenient for dev &
10
+ # testing purposes to quickly understand what you will get from the
11
+ # API
12
+ class MessagesRecorderObserver
13
+ def invoked_methods
14
+ @invoked_methods ||= Hash.new { |h, k| h[k] = [] }
15
+ end
16
+
17
+ def method_missing(method_name, **opts)
18
+ invoked_methods[method_name] << opts[:message]
19
+ end
20
+
21
+ def respond_to_missing?(*)
22
+ true
23
+ end
24
+ end
25
+ end
26
+ end