xtb 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/CHANGELOG.md +4 -0
  4. data/README.md +13 -8
  5. data/lib/xtb/config.rb +27 -6
  6. data/lib/xtb/error.rb +88 -0
  7. data/lib/xtb/error_factory.rb +101 -0
  8. data/lib/xtb/http/all_symbols.rb +1 -1
  9. data/lib/xtb/http/calendar.rb +1 -1
  10. data/lib/xtb/http/chart_last_request.rb +3 -1
  11. data/lib/xtb/http/chart_range_request.rb +3 -1
  12. data/lib/xtb/http/client.rb +49 -19
  13. data/lib/xtb/http/command.rb +27 -19
  14. data/lib/xtb/http/commission_def.rb +3 -1
  15. data/lib/xtb/http/current_user_data.rb +1 -1
  16. data/lib/xtb/http/login.rb +33 -0
  17. data/lib/xtb/http/logout.rb +12 -0
  18. data/lib/xtb/http/margin_level.rb +1 -1
  19. data/lib/xtb/http/margin_trade.rb +3 -1
  20. data/lib/xtb/http/news.rb +3 -1
  21. data/lib/xtb/http/ping.rb +12 -0
  22. data/lib/xtb/http/profit_calculation.rb +3 -1
  23. data/lib/xtb/http/server_time.rb +1 -1
  24. data/lib/xtb/http/step_rules.rb +1 -1
  25. data/lib/xtb/http/symbol.rb +3 -1
  26. data/lib/xtb/http/tick_prices.rb +3 -1
  27. data/lib/xtb/http/trade_records.rb +3 -1
  28. data/lib/xtb/http/trade_transaction.rb +5 -3
  29. data/lib/xtb/http/trades.rb +3 -1
  30. data/lib/xtb/http/trades_history.rb +3 -1
  31. data/lib/xtb/http/trading_hours.rb +6 -4
  32. data/lib/xtb/http/version.rb +1 -1
  33. data/lib/xtb/http.rb +5 -1
  34. data/lib/xtb/request_queue.rb +14 -15
  35. data/lib/xtb/version.rb +1 -1
  36. data/lib/xtb/web_socket/client.rb +1 -0
  37. data/lib/xtb/web_socket.rb +1 -1
  38. data/lib/xtb.rb +2 -1
  39. metadata +80 -9
  40. data/lib/xtb/errors.rb +0 -8
  41. data/lib/xtb/http/response.rb +0 -63
  42. data/lib/xtb/http/ssl_client.rb +0 -43
  43. data/sig/xtb.rbs +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c368d7138b3a90d013565212661d39e0dd339c57c8a4c235eaa30ad786e4601
4
- data.tar.gz: c1613af477c8de260af049d7fc1ac42df8ead3d327967ecc2b575c8599cdbe19
3
+ metadata.gz: 7d9a9468ded9f433834ca0a96a2fafa64f18b39ae8dd11fb71a6ad4b93522477
4
+ data.tar.gz: '091e39d9c4fc4e36a738f8c2e2bc6d177e13a8cee71cea3d641788f69fecaf4a'
5
5
  SHA512:
6
- metadata.gz: 98a5f73fb940d41c98f338c14bbb7a11f5b546214d1b9a7249bc3594797f4968216fbc449118a39a7cb5ba5bf8a0837560334521cf758d8eb50cc52ad3678e9a
7
- data.tar.gz: '046729d8d9a8efad3203bfef09ce870fbb2086b196343460114232f052b8ef194a5063e56d119324595d7ed53db27f3bd9a3217b0ce161c44ec5efe16728ffba'
6
+ metadata.gz: b367e84969900e94217dfa3c982989cd02ce5e41c8bbd5928118e2f8409a2a7de433d55b441ce8da1800e0bd97c2dccfdd1bbd064b6e3fdf3ae8fc527bacb379
7
+ data.tar.gz: 8c30d8723954a1aa85e871d2884457b3675929a384999869e6f2f5a5046c85fc74dd02880bd23eeea24b2d1d0d407a39b5e81ee6713f68df6d143b38621f013d
data/.rubocop.yml CHANGED
@@ -1,3 +1,5 @@
1
+ require: rubocop-rspec
2
+
1
3
  AllCops:
2
4
  TargetRubyVersion: 3.1
3
5
 
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
1
  ## 0.1.0
2
2
 
3
3
  - Initial release
4
+
5
+ ## 0.1.1
6
+
7
+ - Add support for connection pooling
data/README.md CHANGED
@@ -6,33 +6,38 @@ This gem allows you to connect to the XTB broker and execute trades, get account
6
6
 
7
7
  ## Installation
8
8
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
-
11
9
  Install the gem and add to the application's Gemfile by executing:
12
10
 
13
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
11
+ $ bundle add xtb
14
12
 
15
13
  If bundler is not being used to manage dependencies, install the gem by executing:
16
14
 
17
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
15
+ $ gem install xtb
18
16
 
19
17
  ## Usage
20
18
 
21
19
  ### Configuration
22
20
 
23
- Before you can connect to the XTB API, you need to configure the connection.
24
- You can do this by creating a configuration object and passing it to the client.
21
+ Before you can connect to the XTB API, you need to configure the connection.
25
22
 
26
- ```ruby
27
-
23
+ ```shell
24
+ XTB__USER_ID=your_user_id
25
+ XTB__PASSWORD=your_password
28
26
  ```
29
27
 
30
28
  ### Connect to the XTB API
31
29
 
32
30
  ```ruby
31
+ require 'xtb'
33
32
 
33
+ # You're ready to use the API
34
+ Xtb::Http::CurrentUserData.call
34
35
  ```
36
+ Note that there is no need to log in first or log out afterwards. The gem handles the connection for you.
37
+
38
+ ### Subscribing to the XTB API streaming commands
35
39
 
40
+ 🚧 The streaming API is not yet supported.
36
41
 
37
42
  ## Development
38
43
 
data/lib/xtb/config.rb CHANGED
@@ -3,25 +3,34 @@
3
3
  module Xtb
4
4
  # Configuration mapping for XTB API.
5
5
  class Config
6
+ DEFAULT_HTTP_HOST = 'xapi.xtb.com'
7
+ DEFAULT_HTTP_PORT = 5124
8
+ DEFAULT_WEB_SOCKET_HOST = 'ws.xtb.com'
9
+ DEFAULT_WEB_SOCKET_PORT = 5125
10
+ DEFAULT_WEB_SOCKET_PATH = 'demo'
11
+ DEFAULT_CONNECTION_POOL_SIZE = 5
12
+ MAX_CONNECTION_POOL_SIZE = 50
13
+ MIN_REQUEST_INTERVAL = 200 # in milliseconds
14
+
6
15
  class << self
7
16
  def https_host
8
- ENV.fetch('XTB__HTTPS_HOST', 'xapi.xtb.com')
17
+ ENV.fetch('XTB__HTTPS_HOST', DEFAULT_HTTP_HOST)
9
18
  end
10
19
 
11
20
  def https_port
12
- ENV.fetch('XTB__HTTPS_PORT', 5124).to_i
21
+ ENV.fetch('XTB__HTTPS_PORT', DEFAULT_HTTP_PORT).to_i
13
22
  end
14
23
 
15
24
  def wss_host
16
- ENV.fetch('XTB__WSS_HOST', 'ws.xtb.com')
25
+ ENV.fetch('XTB__WSS_HOST', DEFAULT_WEB_SOCKET_HOST)
17
26
  end
18
27
 
19
28
  def wss_path
20
- ENV.fetch('XTB__WSS_PATH', 'demo')
29
+ ENV.fetch('XTB__WSS_PATH', DEFAULT_WEB_SOCKET_PATH)
21
30
  end
22
31
 
23
32
  def wss_port
24
- ENV.fetch('XTB__WSS_PORT', 5125).to_i
33
+ ENV.fetch('XTB__WSS_PORT', DEFAULT_WEB_SOCKET_PORT).to_i
25
34
  end
26
35
 
27
36
  def user_id
@@ -32,8 +41,20 @@ module Xtb
32
41
  ENV.fetch('XTB__PASSWORD') { raise 'XTB__PASSWORD is required' }
33
42
  end
34
43
 
44
+ def connection_pool_size
45
+ size = ENV.fetch('XTB__CONNECTION_POOL_SIZE', DEFAULT_CONNECTION_POOL_SIZE).to_i
46
+ raise "Max connection pool size is #{MAX_CONNECTION_POOL_SIZE}" if size > MAX_CONNECTION_POOL_SIZE
47
+
48
+ size
49
+ end
50
+
35
51
  def min_request_interval
36
- ENV.fetch('XTB__MIN_REQUEST_INTERVAL', 0.2).to_f
52
+ requests_interval = ENV.fetch('XTB__MIN_REQUEST_INTERVAL', MIN_REQUEST_INTERVAL).to_i
53
+ if requests_interval < MIN_REQUEST_INTERVAL
54
+ raise "Minimum request interval must be greater than or equal to #{MIN_REQUEST_INTERVAL}"
55
+ end
56
+
57
+ requests_interval
37
58
  end
38
59
  end
39
60
  end
data/lib/xtb/error.rb ADDED
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xtb
4
+ # http://developers.xstore.pro/documentation/#error-messages
5
+ class Error < StandardError; end
6
+
7
+ class InvalidPriceError < Error; end
8
+
9
+ class InvalidStopLossOrTakeProfitError < Error; end
10
+
11
+ class InvalidVolumeError < Error; end
12
+
13
+ class LoginDisabledError < Error; end
14
+
15
+ class InvalidLoginOrPasswordError < Error; end
16
+
17
+ class MarketClosedError < Error; end
18
+
19
+ class MismatchedParametersError < Error; end
20
+
21
+ class ModificationDeniedError < Error; end
22
+
23
+ class NotEnoughMoneyError < Error; end
24
+
25
+ class OffQuotesError < Error; end
26
+
27
+ class OppositePositionsProhibitedError < Error; end
28
+
29
+ class ShortPositionsProhibitedError < Error; end
30
+
31
+ class PriceChangedError < Error; end
32
+
33
+ class RequestTooFrequentError < Error; end
34
+
35
+ class TooManyTradeRequestsError < Error; end
36
+
37
+ class TradingOnInstrumentDisabledError < Error; end
38
+
39
+ class TradingTimeoutError < Error; end
40
+
41
+ class OtherError < Error; end
42
+
43
+ class SymbolDoesNotExistForAccountError < Error; end
44
+
45
+ class AccountCannotTradeOnSymbolError < Error; end
46
+
47
+ class PendingOrderCannotBeClosedError < Error; end
48
+
49
+ class CannotCloseOrderError < Error; end
50
+
51
+ class NoSuchTransactionError < Error; end
52
+
53
+ class UnknownInstrumentSymbolError < Error; end
54
+
55
+ class UnknownTransactionTypeError < Error; end
56
+
57
+ class NotLoggedInError < Error; end
58
+
59
+ class MethodDoesNotExistError < Error; end
60
+
61
+ class IncorrectPeriodError < Error; end
62
+
63
+ class MissingDataError < Error; end
64
+
65
+ class IncorrectCommandFormatError < Error; end
66
+
67
+ class SymbolDoesNotExistError < Error; end
68
+
69
+ class InvalidTokenError < Error; end
70
+
71
+ class AlreadyLoggedInError < Error; end
72
+
73
+ class SessionTimedOutError < Error; end
74
+
75
+ class InvalidParametersError < Error; end
76
+
77
+ class InternalError < Error; end
78
+
79
+ class NoAccessError < Error; end
80
+
81
+ class ConnectionLimitError < Error; end
82
+
83
+ class DataLimitExceededError < Error; end
84
+
85
+ class BlacklistedError < Error; end
86
+
87
+ class CommandNotAllowedError < Error; end
88
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xtb
4
+ # Factory for creating error objects based on error codes.
5
+ class ErrorFactory
6
+ ERROR_CODES = {
7
+ 'BE001': { klass: 'InvalidPriceError', description: 'Invalid price' },
8
+ 'BE002': { klass: 'InvalidStopLossOrTakeProfitError', description: 'Invalid StopLoss or TakeProfit' },
9
+ 'BE003': { klass: 'InvalidVolumeError', description: 'Invalid volume' },
10
+ 'BE004': { klass: 'LoginDisabledError', description: 'Login disabled' },
11
+ 'BE005': { klass: 'InvalidLoginOrPasswordError', description: 'Invalid login or password' },
12
+ 'BE006': { klass: 'MarketClosedError', description: 'Market for instrument is closed' },
13
+ 'BE007': { klass: 'MismatchedParametersError', description: 'Mismatched parameters' },
14
+ 'BE008': { klass: 'ModificationDeniedError', description: 'Modification is denied' },
15
+ 'BE009': { klass: 'NotEnoughMoneyError', description: 'Not enough money on account to perform trade' },
16
+ 'BE010': { klass: 'OffQuotesError', description: 'Off quotes' },
17
+ 'BE011': { klass: 'OppositePositionsProhibitedError', description: 'Opposite positions prohibited' },
18
+ 'BE012': { klass: 'ShortPositionsProhibitedError', description: 'Short positions prohibited' },
19
+ 'BE013': { klass: 'PriceChangedError', description: 'Price has changed' },
20
+ 'BE014': { klass: 'RequestTooFrequentError', description: 'Request too frequent' },
21
+ 'BE016': { klass: 'TooManyTradeRequestsError', description: 'Too many trade requests' },
22
+ 'BE018': { klass: 'TradingOnInstrumentDisabledError', description: 'Trading on instrument disabled' },
23
+ 'BE019': { klass: 'TradingTimeoutError', description: 'Trading timeout' },
24
+ 'BE020': { klass: 'OtherError', description: 'Other error' },
25
+ 'BE094': { klass: 'SymbolDoesNotExistForAccountError', description: 'Symbol does not exist for given account' },
26
+ 'BE095': { klass: 'AccountCannotTradeOnSymbolError', description: 'Account cannot trade on given symbol' },
27
+ 'BE096': { klass: 'PendingOrderCannotBeClosedError',
28
+ description: 'Pending order cannot be closed. Pending order must be deleted' },
29
+ 'BE097': { klass: 'CannotCloseOrderError', description: 'Cannot close already closed order' },
30
+ 'BE098': { klass: 'NoSuchTransactionError', description: 'No such transaction' },
31
+ 'BE101': { klass: 'UnknownInstrumentSymbolError', description: 'Unknown instrument symbol' },
32
+ 'BE102': { klass: 'UnknownTransactionTypeError', description: 'Unknown transaction type' },
33
+ 'BE103': { klass: 'NotLoggedInError', description: 'User is not logged' },
34
+ 'BE104': { klass: 'MethodDoesNotExistError', description: 'Method does not exist' },
35
+ 'BE105': { klass: 'IncorrectPeriodError', description: 'Incorrect period given' },
36
+ 'BE106': { klass: 'MissingDataError', description: 'Missing data' },
37
+ 'BE110': { klass: 'IncorrectCommandFormatError', description: 'Incorrect command format' },
38
+ 'BE115': { klass: 'SymbolDoesNotExistError', description: 'Symbol does not exist' },
39
+ 'BE116': { klass: 'SymbolDoesNotExistError', description: 'Symbol does not exist' },
40
+ 'BE117': { klass: 'InvalidTokenError', description: 'Invalid token' },
41
+ 'BE118': { klass: 'AlreadyLoggedInError', description: 'User already logged' },
42
+ 'BE200': { klass: 'SessionTimedOutError', description: 'Session timed out' },
43
+ 'EX000': { klass: 'InvalidParametersError', description: 'Invalid parameters' },
44
+ 'EX001': { klass: 'InternalError', description: 'Internal error, in case of such error, please contact support' },
45
+ 'EX003': { klass: 'InternalError', description: 'Internal error, request timed out' },
46
+ 'EX004': { klass: 'InvalidVolumeError',
47
+ description: 'Login credentials are incorrect or this login is not allowed to use an application \
48
+ with this appId' },
49
+ 'EX005': { klass: 'InvalidVolumeError', description: 'Internal error' },
50
+ 'EX006': { klass: 'NoAccessError', description: 'No access' },
51
+ 'EX007': { klass: 'InvalidVolumeError',
52
+ description: 'Invalid login or password. This login/password is disabled for 10 minutes' },
53
+ 'EX008': { klass: 'ConnectionLimitError', description: 'You have reached the connection limit' },
54
+ 'EX009': { klass: 'DataLimitExceededError', description: 'Data limit potentially exceeded' },
55
+ 'EX010': { klass: 'BlacklistedError', description: 'Your login is on the black list' },
56
+ 'EX011': { klass: 'CommandNotAllowedError', description: 'You are not allowed to execute this command' }
57
+ }.freeze
58
+
59
+ ERROR_CODE_DUPLICATES = {
60
+ 'BE016': ['BE017'],
61
+ 'BE020': ('BE021'..'BE037').to_a + ['BE099'],
62
+ 'BE115': ['BE116'],
63
+ 'EX001': %w[BE000 EX002]
64
+ }.freeze
65
+
66
+ # Create an error object based on the error code. The API specifies multiple error codes can be
67
+ # matched to the same error, so a list of duplicates is provided, plus a mapping for codes
68
+ # matching 'SExxx' to 'EX001'.
69
+ # @param error_code [String]
70
+ # @param error_description [String] (optional)
71
+ # @return [Xtb::Error]
72
+ def self.create(error_code, error_description = '')
73
+ error_code = error_code.to_sym
74
+ mapped_code = ERROR_CODES.keys.include?(error_code) ? error_code : error_code_for_duplicate(error_code)
75
+ error = ERROR_CODES.fetch(mapped_code, nil)
76
+ return default_error(error_code, error_description) if error.nil?
77
+
78
+ error_description = error_description.empty? ? error[:description] : error_description
79
+ message = "(#{error_code}) #{error_description}"
80
+ Xtb.const_get(error[:klass]).new(message)
81
+ end
82
+
83
+ def self.default_error(error_code, error_description)
84
+ Xtb::Error.new("(#{error_code}) #{error_description}")
85
+ end
86
+
87
+ def self.error_code_for_duplicate(error_code_duplicate)
88
+ code = nil
89
+
90
+ if error_code_duplicate.start_with?('SE')
91
+ code = :EX001
92
+ else
93
+ ERROR_CODE_DUPLICATES.each do |error_code, duplicates|
94
+ code = error_code if duplicates.include?(error_code_duplicate)
95
+ end
96
+ end
97
+
98
+ code
99
+ end
100
+ end
101
+ end
@@ -5,7 +5,7 @@ module Xtb
5
5
  # http://developers.xstore.pro/documentation/2.5.0#getAllSymbols
6
6
  class AllSymbols < Command
7
7
  def call
8
- super.map { |symbol| Xtb::SymbolRecord.new(**symbol) }
8
+ super.return_data.map { |symbol| Xtb::SymbolRecord.new(**symbol) }
9
9
  end
10
10
 
11
11
  private
@@ -17,7 +17,7 @@ module Xtb
17
17
  end
18
18
 
19
19
  def call
20
- super.map { |record| CalendarRecord.new(**record) }
20
+ super.return_data.map { |record| CalendarRecord.new(**record) }
21
21
  end
22
22
 
23
23
  private
@@ -14,10 +14,12 @@ module Xtb
14
14
  @period = period
15
15
  @start = start
16
16
  @symbol = symbol
17
+
18
+ super()
17
19
  end
18
20
 
19
21
  def call
20
- digits, rate_infos = super.values_at(:digits, :rate_infos)
22
+ digits, rate_infos = super.return_data.values_at(:digits, :rate_infos)
21
23
  rate_infos = rate_infos.map { |record| RateInfoRecord.new(**record) }
22
24
  ChartLastRequestResponse.new(digits:, rate_infos:)
23
25
  end
@@ -18,10 +18,12 @@ module Xtb
18
18
  @start_time = start_time
19
19
  @symbol = symbol
20
20
  @ticks = ticks
21
+
22
+ super()
21
23
  end
22
24
 
23
25
  def call
24
- digits, rate_infos = super.values_at(:digits, :rate_infos)
26
+ digits, rate_infos = super.return_data.values_at(:digits, :rate_infos)
25
27
  rate_infos = rate_infos.map { |record| RateInfoRecord.new(**record) }
26
28
  ChartLastRequestResponse.new(digits:, rate_infos:)
27
29
  end
@@ -1,39 +1,69 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'ssl_client'
4
- require_relative '../errors'
3
+ require 'connection_pool'
4
+
5
+ require_relative '../error'
6
+ require_relative '../config'
5
7
  require_relative '../request_queue'
6
8
 
7
9
  module Xtb
8
10
  module Http
11
+ # Client for XTB API.
9
12
  class Client
10
13
  include RequestQueue
11
14
 
12
- class << self
13
- def post(payload)
15
+ def initialize
16
+ @ssl_socket = new_ssl_socket
17
+ end
18
+
19
+ # Sends a request to the broker server. It utilizes a connection pool to manage connections.
20
+ def self.post
21
+ connection_pool.with do |connection|
14
22
  with_request_queue do
15
- Xtb::Http::SslClient.request(payload)
23
+ yield connection
16
24
  end
17
- # rescue NotLoggedInError => e
18
- # return if command == :logout
19
- #
20
- # Rails.logger.debug(e)
21
- #
22
- # BrokerClients::Xtb::Login.call
23
- # retry
24
- # rescue AlreadyLoggedInError => _e
25
- # # noop
26
25
  end
26
+ end
27
+
28
+ def request(payload)
29
+ ssl_socket.puts(payload)
30
+ response
31
+ end
32
+
33
+ def stream_session_id
34
+ @stream_session_id ||= BrokerClients::Xtb::Login.call.stream_session_id
35
+ end
27
36
 
28
- def stream_session_id
29
- @stream_session_id ||= BrokerClients::Xtb::Login.call.stream_session_id
37
+ private
38
+
39
+ attr_reader :ssl_socket
40
+
41
+ def self.connection_pool
42
+ @connection_pool ||= ConnectionPool.new(size: Config.connection_pool_size, timeout: 30) do
43
+ connection = new
44
+ Xtb::Http::Login.call(connection:)
45
+
46
+ connection
30
47
  end
48
+ end
31
49
 
32
- private
50
+ def response
51
+ buffer = []
33
52
 
34
- def ssl_client
35
- @ssl_client ||= Xtb::Http::SslClient.new
53
+ while (line = ssl_socket.gets)
54
+ break if line.strip.empty?
55
+
56
+ buffer << line
36
57
  end
58
+
59
+ buffer.join.strip
60
+ end
61
+
62
+ def new_ssl_socket
63
+ OpenSSL::SSL::SSLSocket.open(Config.https_host, Config.https_port).tap do |socket|
64
+ socket.hostname = Config.https_host
65
+ socket.sync_close = true
66
+ end.connect
37
67
  end
38
68
  end
39
69
  end
@@ -2,7 +2,8 @@
2
2
 
3
3
  require 'json'
4
4
  require 'active_support/inflector'
5
- require_relative '../errors'
5
+ require_relative '../error'
6
+ require_relative '../error_factory'
6
7
 
7
8
  module Xtb
8
9
  module Http
@@ -35,14 +36,20 @@ module Xtb
35
36
  @response = parse
36
37
  end
37
38
 
38
- def success?
39
+ def status
39
40
  response[:status]
40
41
  end
41
42
 
43
+ alias_method :success?, :status
44
+
42
45
  def return_data
43
46
  response[:return_data]
44
47
  end
45
48
 
49
+ def stream_session_id
50
+ response[:stream_session_id]
51
+ end
52
+
46
53
  def error_code
47
54
  response[:error_code]
48
55
  end
@@ -73,41 +80,42 @@ module Xtb
73
80
  end
74
81
  end
75
82
 
76
- def initialize(**args); end
83
+ def initialize(**args)
84
+ @args = args
85
+ @request_data = Request.new(command:, arguments:).to_json
86
+ end
77
87
 
78
88
  def self.call(**args)
79
89
  new(**args).call
80
90
  end
81
91
 
82
92
  def call
83
- request_data = Request.new(command:, arguments:).to_json
84
- raw_response = Client.post(request_data)
93
+ raw_response = if args[:connection]
94
+ args.fetch(:connection).request(request_data)
95
+ else
96
+ Xtb::Http::Client.post do |connection|
97
+ connection.request(request_data)
98
+ end
99
+ end
100
+
85
101
  response = Response.new(command:, raw_response:)
86
- raise_error(response.error_code, response.error_description) unless response.success?
87
102
 
88
- response.return_data
103
+ raise ErrorFactory.create(response.error_code, response.error_description) unless response.success?
104
+
105
+ response
89
106
  end
90
107
 
91
108
  private
92
109
 
110
+ attr_reader :args, :request_data
111
+
93
112
  def command
94
- raise NotImplementedError
113
+ raise NotImplementedError('You must implement the command method')
95
114
  end
96
115
 
97
116
  def arguments
98
117
  nil
99
118
  end
100
-
101
- def raise_error(error_code, error_description)
102
- case error_code
103
- when 'BE103'
104
- raise NotLoggedInError, "(#{error_code}) #{error_description}"
105
- when 'BE118'
106
- raise AlreadyLoggedInError, "(#{error_code}) #{error_description}"
107
- else
108
- raise "(#{error_code}) #{error_description}"
109
- end
110
- end
111
119
  end
112
120
  end
113
121
  end
@@ -11,10 +11,12 @@ module Xtb
11
11
  def initialize(symbol, volume)
12
12
  @symbol = symbol
13
13
  @volume = volume
14
+
15
+ super()
14
16
  end
15
17
 
16
18
  def call
17
- CommissionDefResponse.new(**super)
19
+ CommissionDefResponse.new(**super.return_data)
18
20
  end
19
21
 
20
22
  private
@@ -8,7 +8,7 @@ module Xtb
8
8
  :leverage_multiplier, :spread_type, :trailing_stop)
9
9
 
10
10
  def call
11
- CurrentUserDataResponse.new(**super)
11
+ CurrentUserDataResponse.new(**super.return_data)
12
12
  end
13
13
 
14
14
  private
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xtb
4
+ module Http
5
+ # http://developers.xstore.pro/documentation/2.5.0#login
6
+ class Login < Command
7
+ LoginResponse = Data.define(:stream_session_id)
8
+
9
+ def call
10
+ LoginResponse.new(super.stream_session_id)
11
+ end
12
+
13
+ private
14
+
15
+ def command = :login
16
+
17
+ def arguments
18
+ {
19
+ user_id:,
20
+ password:
21
+ }
22
+ end
23
+
24
+ def user_id
25
+ Xtb::Config.user_id
26
+ end
27
+
28
+ def password
29
+ Xtb::Config.password
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xtb
4
+ module Http
5
+ # http://developers.xstore.pro/documentation/2.5.0#logout
6
+ class Logout < Command
7
+ private
8
+
9
+ def command = :logout
10
+ end
11
+ end
12
+ end
@@ -7,7 +7,7 @@ module Xtb
7
7
  MarginLevelResponse = Data.define(:balance, :credit, :currency, :equity, :margin, :margin_free, :margin_level)
8
8
 
9
9
  def call
10
- MarginLevelResponse.new(**super)
10
+ MarginLevelResponse.new(**super.return_data)
11
11
  end
12
12
 
13
13
  private
@@ -11,10 +11,12 @@ module Xtb
11
11
  def initialize(symbol, volume)
12
12
  @symbol = symbol
13
13
  @volume = volume
14
+
15
+ super()
14
16
  end
15
17
 
16
18
  def call
17
- MarginTradeResponse.new(**super)
19
+ MarginTradeResponse.new(**super.return_data)
18
20
  end
19
21
 
20
22
  private
data/lib/xtb/http/news.rb CHANGED
@@ -11,10 +11,12 @@ module Xtb
11
11
  def initialize(end_time, start_time)
12
12
  @end_time = end_time
13
13
  @start_time = start_time
14
+
15
+ super()
14
16
  end
15
17
 
16
18
  def call
17
- super.map { |record| NewsTopicRecord.new(**record) }
19
+ super.return_data.map { |record| NewsTopicRecord.new(**record) }
18
20
  end
19
21
 
20
22
  private
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xtb
4
+ module Http
5
+ # http://developers.xstore.pro/documentation/2.5.0#ping
6
+ class Ping < Command
7
+ private
8
+
9
+ def command = :ping
10
+ end
11
+ end
12
+ end
@@ -17,10 +17,12 @@ module Xtb
17
17
  @open_price = open_price
18
18
  @symbol = symbol
19
19
  @volume = volume
20
+
21
+ super()
20
22
  end
21
23
 
22
24
  def call
23
- ProfitCalculationResponse.new(**super)
25
+ ProfitCalculationResponse.new(**super.return_data)
24
26
  end
25
27
 
26
28
  private
@@ -7,7 +7,7 @@ module Xtb
7
7
  ServerTimeResponse = Data.define(:time, :time_string)
8
8
 
9
9
  def call
10
- ServerTimeResponse.new(**super)
10
+ ServerTimeResponse.new(**super.return_data)
11
11
  end
12
12
 
13
13
  private
@@ -7,7 +7,7 @@ module Xtb
7
7
  StepRecord = Data.define(:from_value, :step)
8
8
 
9
9
  def call
10
- super.map { |record| StepRecord.new(**record) }
10
+ super.return_data.map { |record| StepRecord.new(**record) }
11
11
  end
12
12
 
13
13
  private
@@ -7,10 +7,12 @@ module Xtb
7
7
  # @param symbol [String|Symbol]
8
8
  def initialize(symbol)
9
9
  @symbol = symbol
10
+
11
+ super()
10
12
  end
11
13
 
12
14
  def call
13
- Xtb::SymbolRecord.new(**super)
15
+ Xtb::SymbolRecord.new(**super.return_data)
14
16
  end
15
17
 
16
18
  private
@@ -11,10 +11,12 @@ module Xtb
11
11
  @level = level
12
12
  @symbols = symbols
13
13
  @timestamp = timestamp
14
+
15
+ super()
14
16
  end
15
17
 
16
18
  def call
17
- super[:quotations].map { |record| TickRecord.new(**record) }
19
+ super.return_data[:quotations].map { |record| TickRecord.new(**record) }
18
20
  end
19
21
 
20
22
  private
@@ -7,10 +7,12 @@ module Xtb
7
7
  # @param orders [Array<Integer>] list of order ids
8
8
  def initialize(orders)
9
9
  @orders = orders
10
+
11
+ super()
10
12
  end
11
13
 
12
14
  def call
13
- super.map { |record| Xtb::TradeRecord.new(**record) }
15
+ super.return_data.map { |record| Xtb::TradeRecord.new(**record) }
14
16
  end
15
17
 
16
18
  private
@@ -6,7 +6,7 @@ module Xtb
6
6
  class TradeTransaction < Command
7
7
  TradeTransactionResponse = Data.define(:order)
8
8
 
9
- # @param cmd [Symbol]
9
+ # @param cmd [Symbol] One of :buy, :sell, :buy_limit, :sell_limit, :buy_stop, :sell_stop, :balance, or :credit
10
10
  # @param custom_comment [String]
11
11
  # @param expiration [Integer]
12
12
  # @param offset [Integer]
@@ -15,7 +15,7 @@ module Xtb
15
15
  # @param stop_loss [Float]
16
16
  # @param symbol [String]
17
17
  # @param take_profit [Float]
18
- # @param type [Symbol]
18
+ # @param type [Symbol] One of :open, :pending, :close, :modify, or :delete
19
19
  # @param volume [Float]
20
20
  def initialize(cmd:, custom_comment:, expiration:, offset:, order:, price:, stop_loss:, symbol:, take_profit:,
21
21
  type:, volume:)
@@ -30,10 +30,12 @@ module Xtb
30
30
  @take_profit = take_profit
31
31
  @type = type
32
32
  @volume = volume
33
+
34
+ super()
33
35
  end
34
36
 
35
37
  def call
36
- TradeTransactionResponse.new(**super)
38
+ TradeTransactionResponse.new(**super.return_data)
37
39
  end
38
40
 
39
41
  private
@@ -7,10 +7,12 @@ module Xtb
7
7
  # @param opened_only [Boolean]
8
8
  def initialize(opened_only)
9
9
  @opened_only = opened_only
10
+
11
+ super()
10
12
  end
11
13
 
12
14
  def call
13
- super.map { |record| Xtb::TradeRecord.new(**record) }
15
+ super.return_data.map { |record| Xtb::TradeRecord.new(**record) }
14
16
  end
15
17
 
16
18
  private
@@ -9,10 +9,12 @@ module Xtb
9
9
  def initialize(end_time, start_time)
10
10
  @end_time = end_time
11
11
  @start_time = start_time
12
+
13
+ super()
12
14
  end
13
15
 
14
16
  def call
15
- super.map { |record| Xtb::TradeRecord.new(**record) }
17
+ super.return_data.map { |record| Xtb::TradeRecord.new(**record) }
16
18
  end
17
19
 
18
20
  private
@@ -4,10 +4,10 @@ module Xtb
4
4
  module Http
5
5
  # http://developers.xstore.pro/documentation/2.5.0#getTradingHours
6
6
  class TradingHours < Command
7
- TradingHoursRecord = Data.define(:quotes, :symbol, :trading) do
8
- # Represents both quotes and trading times records.
9
- TimesRecord = Data.define(:day, :from_t, :to_t)
7
+ # Represents both quotes and trading times records.
8
+ TimesRecord = Data.define(:day, :from_t, :to_t)
10
9
 
10
+ TradingHoursRecord = Data.define(:quotes, :symbol, :trading) do
11
11
  def initialize(symbol:, quotes: TimesRecord.new, trading: TimesRecord.new)
12
12
  super(quotes:, symbol:, trading:)
13
13
  end
@@ -16,10 +16,12 @@ module Xtb
16
16
  # @param symbols [Array<String|Symbol>]
17
17
  def initialize(symbols)
18
18
  @symbols = symbols
19
+
20
+ super()
19
21
  end
20
22
 
21
23
  def call
22
- super.map { |record| TradingHoursRecord.new(**record) }
24
+ super.return_data.map { |record| TradingHoursRecord.new(**record) }
23
25
  end
24
26
 
25
27
  private
@@ -7,7 +7,7 @@ module Xtb
7
7
  VersionResponse = Data.define(:version)
8
8
 
9
9
  def call
10
- VersionResponse.new(**super)
10
+ VersionResponse.new(**super.return_data)
11
11
  end
12
12
 
13
13
  private
data/lib/xtb/http.rb CHANGED
@@ -1,24 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'http/client'
4
+ require_relative 'http/command'
4
5
 
5
6
  # commands
6
- require_relative 'http/command'
7
7
  require_relative 'http/all_symbols'
8
8
  require_relative 'http/calendar'
9
9
  require_relative 'http/chart_last_request'
10
10
  require_relative 'http/chart_range_request'
11
11
  require_relative 'http/commission_def'
12
12
  require_relative 'http/current_user_data'
13
+ require_relative 'http/login'
14
+ require_relative 'http/logout'
13
15
  require_relative 'http/margin_level'
14
16
  require_relative 'http/margin_trade'
15
17
  require_relative 'http/news'
18
+ require_relative 'http/ping'
16
19
  require_relative 'http/profit_calculation'
17
20
  require_relative 'http/server_time'
18
21
  require_relative 'http/step_rules'
19
22
  require_relative 'http/symbol'
20
23
  require_relative 'http/tick_prices'
21
24
  require_relative 'http/trade_records'
25
+ require_relative 'http/trade_transaction'
22
26
  require_relative 'http/trades'
23
27
  require_relative 'http/trades_history'
24
28
  require_relative 'http/trading_hours'
@@ -10,29 +10,28 @@ module Xtb
10
10
  base.extend(ClassMethods)
11
11
  end
12
12
 
13
- module ClassMethods
14
- private
15
-
16
- def with_request_queue(&block)
17
- queue << block
18
- queue << wait_proc
19
-
20
- result = queue.pop.call
21
- queue.pop.call
13
+ module ClassMethods # :nodoc:
14
+ def with_request_queue
15
+ sleep(time_to_next_request / 1000) unless @last_request_time.nil?
22
16
 
23
- result
17
+ yield
18
+ ensure
19
+ @last_request_time = Time.now
24
20
  end
25
21
 
26
- def queue
27
- @queue ||= Queue.new
22
+ private
23
+
24
+ def time_to_next_request
25
+ [0, min_request_interval - time_since_last_request].max.to_f
28
26
  end
29
27
 
30
- def wait_proc
31
- proc { sleep(min_request_interval) }
28
+ # @return [Integer] time elapsed since the last request in milliseconds
29
+ def time_since_last_request
30
+ (1000 * (@last_request_time - Time.now)).to_i.abs
32
31
  end
33
32
 
34
33
  def min_request_interval
35
- Config.min_request_interval
34
+ @min_request_interval ||= Config.min_request_interval
36
35
  end
37
36
  end
38
37
  end
data/lib/xtb/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Xtb
4
- VERSION = '0.1.0'
4
+ VERSION = '0.1.1'
5
5
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Xtb
4
4
  module WebSocket
5
+ # Client for WebSocket connection.
5
6
  class Client
6
7
  include RequestQueue
7
8
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Xtb
4
- module WebSocket
4
+ module WebSocket # :nodoc:
5
5
  end
6
6
  end
data/lib/xtb.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'xtb/version'
4
+ require_relative 'xtb/error'
4
5
  require_relative 'xtb/http'
5
6
  require_relative 'xtb/web_socket'
6
7
  require_relative 'xtb/config'
@@ -14,7 +15,7 @@ module Xtb
14
15
  m30: 30,
15
16
  h1: 60,
16
17
  h4: 240,
17
- d1: 1440,
18
+ d1: 1_440,
18
19
  w1: 10_080,
19
20
  mn1: 43_200
20
21
  }.freeze
metadata CHANGED
@@ -1,15 +1,85 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xtb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
- - jacekmaciag
7
+ - Jacek Maciag
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-11-12 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2024-11-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: connection_pool
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: openssl
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 3.1.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 3.1.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.21'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.21'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop-rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
13
83
  description: 'The XTB API (xAPI) client for Ruby provides a simple and easy to use
14
84
  interface to interact with XTB API.
15
85
 
@@ -28,7 +98,8 @@ files:
28
98
  - Rakefile
29
99
  - lib/xtb.rb
30
100
  - lib/xtb/config.rb
31
- - lib/xtb/errors.rb
101
+ - lib/xtb/error.rb
102
+ - lib/xtb/error_factory.rb
32
103
  - lib/xtb/http.rb
33
104
  - lib/xtb/http/all_symbols.rb
34
105
  - lib/xtb/http/calendar.rb
@@ -38,13 +109,14 @@ files:
38
109
  - lib/xtb/http/command.rb
39
110
  - lib/xtb/http/commission_def.rb
40
111
  - lib/xtb/http/current_user_data.rb
112
+ - lib/xtb/http/login.rb
113
+ - lib/xtb/http/logout.rb
41
114
  - lib/xtb/http/margin_level.rb
42
115
  - lib/xtb/http/margin_trade.rb
43
116
  - lib/xtb/http/news.rb
117
+ - lib/xtb/http/ping.rb
44
118
  - lib/xtb/http/profit_calculation.rb
45
- - lib/xtb/http/response.rb
46
119
  - lib/xtb/http/server_time.rb
47
- - lib/xtb/http/ssl_client.rb
48
120
  - lib/xtb/http/step_rules.rb
49
121
  - lib/xtb/http/symbol.rb
50
122
  - lib/xtb/http/tick_prices.rb
@@ -58,7 +130,6 @@ files:
58
130
  - lib/xtb/version.rb
59
131
  - lib/xtb/web_socket.rb
60
132
  - lib/xtb/web_socket/client.rb
61
- - sig/xtb.rbs
62
133
  homepage: https://github.com/jacekmaciag/xtb
63
134
  licenses:
64
135
  - MIT
@@ -66,7 +137,7 @@ metadata:
66
137
  allowed_push_host: https://rubygems.org
67
138
  homepage_uri: https://github.com/jacekmaciag/xtb
68
139
  source_code_uri: https://github.com/jacekmaciag/xtb
69
- changelog_uri: https://github.com/jacekmaciag/xtb/blob/main/CHANGELOG.md
140
+ changelog_uri: https://github.com/jacekmaciag/xtb/blob/master/CHANGELOG.md
70
141
  post_install_message:
71
142
  rdoc_options: []
72
143
  require_paths:
data/lib/xtb/errors.rb DELETED
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Xtb
4
- class Error < StandardError; end
5
-
6
- class NotLoggedInError < Error; end
7
- class AlreadyLoggedInError < Error; end
8
- end
@@ -1,63 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Xtb
4
- module Http
5
- class Response
6
- def initialize(raw_response, command)
7
- @response = parse_json(raw_response)
8
- @command = command
9
- end
10
-
11
- def self.parse(raw_response, command)
12
- new(raw_response, command).parse
13
- end
14
-
15
- def parse
16
- return data if success?
17
-
18
- raise_error
19
- end
20
-
21
- private
22
-
23
- attr_reader :response, :command
24
-
25
- def parse_json(raw_response)
26
- JSON.parse(raw_response).deep_transform_keys { |key| key.underscore.to_sym }
27
- end
28
-
29
- def success?
30
- response[:status]
31
- end
32
-
33
- def data
34
- return response if command == :login
35
-
36
- response[:return_data]
37
- end
38
-
39
- def error_code
40
- response[:error_code]
41
- end
42
-
43
- def error_description
44
- response[:error_descr]
45
- end
46
-
47
- def error_string
48
- "(#{error_code}) #{error_description}"
49
- end
50
-
51
- def raise_error
52
- case error_code
53
- when 'BE103'
54
- raise NotLoggedInError, error_string
55
- when 'BE118'
56
- raise AlreadyLoggedInError, error_string
57
- else
58
- raise BrokerError, error_string
59
- end
60
- end
61
- end
62
- end
63
- end
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'openssl'
4
- require_relative '../config'
5
-
6
- module Xtb
7
- module Http
8
- class SslClient
9
- class << self
10
- def request(payload)
11
- ssl_socket.connect
12
- ssl_socket.puts(payload)
13
- next_line
14
- end
15
-
16
- private
17
-
18
- def next_line
19
- line = ssl_socket.gets.chomp
20
- return line unless line.empty?
21
-
22
- next_line
23
- end
24
-
25
- def ssl_context
26
- @ssl_context ||= OpenSSL::SSL::SSLContext.new
27
- end
28
-
29
- def ssl_socket
30
- @ssl_socket ||= OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context) do |socket|
31
- socket.hostname = Config.https_host
32
- socket.sync_close = true
33
- socket
34
- end
35
- end
36
-
37
- def tcp_socket
38
- TCPSocket.new(Config.https_host, Config.https_port)
39
- end
40
- end
41
- end
42
- end
43
- end
data/sig/xtb.rbs DELETED
@@ -1,4 +0,0 @@
1
- module Xtb
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end