C56DKOEPHH4TMIFTWDWYTHCIR2FVGOS2A5J5LQYKWJGQ5TK6OJ6AC ExUnit.start()
defmodule Ibex.MessageTest douse ExUnit.Casedoctest Ibexrequire Ibex.Messagetest "adds nul to field" doresult = Ibex.Message.make_field(<<71>>)assert(result == <<71, 0>>)endtest "prefixes length to message" domsg_code = <<71>>version = <<2>>uhh = <<2>>last = <<0>>msg_code = Ibex.Message.make_field(msg_code)assert(msg_code == <<71, 0>>)version = Ibex.Message.make_field(version)assert(version == <<2, 0>>)uhh = Ibex.Message.make_field(uhh)assert(uhh == <<2, 0>>)last = Ibex.Message.make_field(last)assert(last == <<0, 0>>)msg = << msg_code::binary, version::binary, uhh::binary, last::binary >>l = Ibex.Message.get_msg_length(msg)made_msg = Ibex.Message.make_msg(msg, l)assert(made_msg == <<0, 0, 0, 8, 71, 0, 2, 0, 2, 0, 0, 0, 0>>)endtest "convert to IB API format" dotest_msg = Ibex.Messages.Inbound.ReqIds.t()converted = IBMessage.to_ib_api(test_msg)assert(converted = <<0, 0, 0, 4, 8, 0, 50, 0, 0>>)endend
defmodule IbexTest douse ExUnit.Casedoctest Ibextest "greets the world" doassert Ibex.hello() == :worldendend
defmodule Ibex.MixProject douse Mix.Projectdef project do[app: :ibex,version: "0.1.0",elixir: "~> 1.15",start_permanent: Mix.env() == :prod,deps: deps()]end# Run "mix help compile.app" to learn about applications.def application do[extra_applications: [:logger]]end# Run "mix help deps" to learn about dependencies.defp deps do[# {:dep_from_hexpm, "~> 0.3.0"},# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}]endend
defmodule Ibex.Messages.Types do@type message_type :: atom()@type code_map :: map()@type market_data_type :: :bid | :ask | :trades@type ticker_id :: integer()defmodule Inbound do@type message_type :: Ibex.Messages.Types.message_type()@codes %{1 => :tick_price,2 => :tick_size,3 => :order_status,4 => :err_msg,5 => :open_order,6 => :acct_value,7 => :portfolio_value,8 => :acct_update,9 => :next_valid,10 => :contract_data,11 => :execution_data,12 => :market_depth,13 => :market_depth,14 => :news_bulletins,15 => :managed_accts,16 => :receive_fa,17 => :historical_data,18 => :bond_contract,19 => :scanner_parameters,20 => :scanner_data,21 => :tick_option,45 => :tick_generic,46 => :tick_string,47 => :tick_efp,49 => :current_time,50 => :real_time_bars,51 => :fundamental_data,52 => :contract_data_end,53 => :open_order_end,54 => :acct_download_end,55 => :execution_data_end,56 => :delta_neutral_validation,57 => :tick_snapshot_end,58 => :market_data_type,59 => :commission_report,61 => :position_data,62 => :position_end,63 => :account_summary,64 => :account_summary_end,65 => :verify_message_api,66 => :verify_completed,67 => :display_group_list,68 => :display_group_updated,69 => :verify_and_auth_message_api,70 => :verify_and_auth_completed,71 => :position_multi,72 => :position_multi_end,73 => :account_update_multi,74 => :account_update_multi_end,75 => :security_definition_option_parameter,76 => :security_definition_option_parameter_end,77 => :soft_dollar_tiers,78 => :family_codes,79 => :symbol_samples,80 => :mkt_depth_exchanges,81 => :tick_req_params,82 => :smart_components,83 => :news_article,84 => :tick_news,85 => :news_providers,86 => :historical_news,87 => :historical_news_end,88 => :head_timestamp,89 => :histogram_data,90 => :historical_data_update,91 => :reroute_mkt_data_req,92 => :reroute_mkt_depth_req,93 => :market_rule,94 => :pnl,95 => :pnl_single,96 => :historical_ticks,97 => :historical_ticks_bid_ask,98 => :historical_ticks_last,99 => :tick_by_tick,100 => :order_bound,101 => :completed_order,102 => :completed_orders_end,103 => :replace_fa_end,104 => :wsh_meta_data,105 => :wsh_event_data,106 => :historical_schedule,107 => :user_info}@spec parse(integer()) :: message_type()def parse(code) doMap.get(@codes, code, nil)endenddefmodule Outbound do@type message_type :: Ibex.Messages.Types.message_type()@codes %{:req_mkt_data => 1,:cancel_mkt_data => 2,:place_order => 3,:cancel_order => 4,:req_open_orders => 5,:req_acct_data => 6,:req_executions => 7,:req_ids => 8,:req_contract_data => 9,:req_mkt_depth => 10,:cancel_mkt_depth => 11,:req_news_bulletins => 12,:cancel_news_bulletins => 13,:set_server_loglevel => 14,:req_auto_open_orders => 15,:req_all_open_orders => 16,:req_managed_accts => 17,:req_fa => 18,:replace_fa => 19,:req_historical_data => 20,:exercise_options => 21,:req_scanner_subscription => 22,:cancel_scanner_subscription => 23,:req_scanner_parameters => 24,:cancel_historical_data => 25,:req_current_time => 49,:req_real_time_bars => 50,:cancel_real_time_bars => 51,:req_fundamental_data => 52,:cancel_fundamental_data => 53,:req_calc_implied_volat => 54,:req_calc_option_price => 55,:cancel_calc_implied_volat => 56,:cancel_calc_option_price => 57,:req_global_cancel => 58,:req_market_data_type => 59,:req_positions => 61,:req_account_summary => 62,:cancel_account_summary => 63,:cancel_positions => 64,:verify_request => 65,:verify_message => 66,:query_display_groups => 67,:subscribe_to_group_events => 68,:update_display_group => 69,:unsubscribe_from_group_events => 70,:start_api => 71,:verify_and_auth_request => 72,:verify_and_auth_message => 73,:req_positions_multi => 74,:cancel_positions_multi => 75,:req_account_updates_multi => 76,:cancel_account_updates_multi => 77,:req_sec_def_opt_params => 78,:req_soft_dollar_tiers => 79,:req_family_codes => 80,:req_matching_symbols => 81,:req_mkt_depth_exchanges => 82,:req_smart_components => 83,:req_news_article => 84,:req_news_providers => 85,:req_historical_news => 86,:req_head_timestamp => 87,:req_histogram_data => 88,:cancel_histogram_data => 89,:cancel_head_timestamp => 90,:req_market_rule => 91,:req_pnl => 92,:cancel_pnl => 93,:req_pnl_single => 94,:cancel_pnl_single => 95,:req_historical_ticks => 96,:req_tick_by_tick_data => 97,:cancel_tick_by_tick_data => 98,:req_completed_orders => 99,:req_wsh_meta_data => 100,:cancel_wsh_meta_data => 101,:req_wsh_event_data => 102,:cancel_wsh_event_data => 103,:req_user_info => 104}@spec parse(message_type()) :: integer()def parse(mtype) doMap.get(@codes, mtype, nil)endendend
defmodule Ibex.Message dorequire Loggerdef make_field(f) dof <> <<0>>enddef make_msg(fields, l) do<<l::size(32)>> <> fields <> <<0>>enddef get_msg_length(msg) dobyte_size(msg) + 1enddef message_version() do<<2>>endenddefprotocol IBMessage dodef to_ib_api(msg)enddefimpl IBMessage, for: List dorequire Loggerdef to_ib_api(msg) dofields =Enum.reduce(msg, <<>>, fn f, acc ->acc <> Ibex.Message.make_field(f)end)length = Ibex.Message.get_msg_length(to_string(fields))Ibex.Message.make_msg(fields, length)endenddefmodule Ibex.Messages.Inbound dorequire Loggerdef parse(m) do<<l::size(32), rest::binary>> = mfields = String.split(rest, <<0>>)code = Enum.at(fields, 0)Ibex.Messages.Types.Inbound.parse(String.to_integer(code)){l, fields}endenddefmodule Ibex.Messages.Outbound dorequire Loggerdefmodule ReqIds doalias Ibex.Messages.Types.Outbound, as: Outboundimport Ibex.Message, only: [message_version: 0]def t() docode = to_string(Outbound.parse(:req_ids))Logger.debug("Code is: #{code}")[code,<<"1">>,<<"1">>]endendend
defmodule Ibex.Messages.Historical dodefmodule EarliestAvailableData dodefstruct [:instrument, :contract, :what_to_show, :use_rth, :format_date]endend
defmodule Ibex do@moduledoc """Documentation for `Ibex`."""@doc """Hello world.## Examplesiex> Ibex.hello():world"""def hello do:worldendend
defmodule IBClient dorequire Loggeruse GenServer@ip {127, 0, 0, 1}@port 4001@type connection_state() :: :disconnected | :connecting | :connecteddef send_message(pid, message) doGenServer.cast(pid, {:message, message})enddef start doLogger.debug("Starting IBClient...")GenServer.start(__MODULE__, %{socket: nil,conn_state: :disconnected,server_api: nil,server_connect_time: nil,next_id: 0})enddef init(state) dosend(self(), :tick){:ok, state}enddef handle_info(:tick, state) dostate =case state[:conn_state] do:disconnected ->connect_to_server(state):send_api_version ->send_version(state):waiting_on_version_resp ->wait_for_api_version_response(state):sending_start_api ->start_api(state):waiting_on_start_api_resp ->wait_for_start_api_resp(state):transition_to_non_blocking ->transition_to_nonblocking(state):connected ->stateendif state[:conn_state] != :connected doLogger.info("Sending tick...")Process.send_after(self(), :tick, 1000)end{:noreply, state}enddef handle_info(:start_api, state) doLogger.debug("Starting API..."){:noreply, state}enddef handle_info(:req_account_summary, state) doLogger.debug("Requesting account summary")msg = <<0, 0, 0, 8, "62", 0, 2, 0, 2, 0, 0, 0, 0>>:ok = :gen_tcp.send(state[:socket], msg){:noreply, state}enddef handle_info(:req_ids, state) doLogger.info("Requesting next valid IDs...")msg =Ibex.Messages.Outbound.ReqIds.t()|> IBMessage.to_ib_api()Logger.debug("Sending: #{inspect(msg)}"):ok = :gen_tcp.send(state[:socket], msg){:noreply, state}enddef handle_info({:tcp, _, data}, state) doLogger.debug("Received: #{inspect(data)}"){:noreply, state}enddef handle_info({:tcp_closed, _}, state), do: {:stop, :normal, state}def handle_info({:tcp_error, _}, state), do: {:stop, :normal, state}def handle_info({other, _}, state) doLogger.debug("Received #{inspect(other)}"){:noreply, state}enddef handle_cast({:message, message}, %{socket: socket} = state) do:ok = :gen_tcp.send(socket, message){:noreply, state}enddef handle({:disconnect, _}, state) dodisconnect(state, :user_requested){:noreply, state}enddef disconnect(state, reason) doLogger.info("Disconnected: #{reason}"){:stop, :normal, state}enddefp connect_to_server(state) docase :gen_tcp.connect(@ip, @port, [:binary, active: false]) do{:ok, socket} ->%{state | conn_state: :send_api_version, socket: socket}{:error, reason} ->Logger.error("Failed to connect to server: #{inspect(reason)}")stateendenddefp send_version(state) doLogger.info("Sending API version to server...")case :gen_tcp.send(state[:socket], <<"API", 0, 0, 0, 0, 9, "v100..177">>) do:ok ->%{state | conn_state: :waiting_on_version_resp}{:error, reason} ->Logger.error("Failed to send API version string to server: #{inspect(reason)}")stateendenddefp start_api(state) doLogger.info("Sending START_API message...")case :gen_tcp.send(state[:socket], <<0, 0, 0, 8, 55, 49, 0, 50, 0, 50, 0, 0>>) do:ok ->%{state | conn_state: :waiting_on_start_api_resp}{:error, reason} ->Logger.error("Failed to send StartApi message: #{inspect(reason)}")stateendenddefp wait_for_api_version_response(state) doLogger.info("Waiting for API version response..."){_length, data} = wait_for_data(state)state =case length(data) do3 ->Logger.info("Extracting Server API version from fields. Version: #{Enum.at(data, 0)}. Time: #{Enum.at(data, 1)}")state = %{ state | server_api: Enum.at(data, 0)}%{ state | server_connect_time: Enum.at(data, 1)}_ ->stateend%{state | conn_state: :sending_start_api}enddefp wait_for_start_api_resp(state) doLogger.info("Waiting for START_API response...")wait_for_data(state)%{state | conn_state: :transition_to_non_blocking}enddefp wait_for_data(state) do{:ok, data} = :gen_tcp.recv(state[:socket], 0)Ibex.Messages.Inbound.parse(data)enddefp transition_to_nonblocking(state) doLogger.info("Transitioning to non-blocking mode..."):inet.setopts(state[:socket], active: true)%{ state | conn_state: :connected }endend
# Ibex**TODO: Add description**## InstallationIf [available in Hex](https://hex.pm/docs/publish), the package can be installedby adding `ibex` to your list of dependencies in `mix.exs`:```elixirdef deps do[{:ibex, "~> 0.1.0"}]end```Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)and published on [HexDocs](https://hexdocs.pm). Once published, the docs canbe found at <https://hexdocs.pm/ibex>.