C56DKOEPHH4TMIFTWDWYTHCIR2FVGOS2A5J5LQYKWJGQ5TK6OJ6AC
ExUnit.start()
defmodule Ibex.MessageTest do
use ExUnit.Case
doctest Ibex
require Ibex.Message
test "adds nul to field" do
result = Ibex.Message.make_field(<<71>>)
assert(result == <<71, 0>>)
end
test "prefixes length to message" do
msg_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>>)
end
test "convert to IB API format" do
test_msg = Ibex.Messages.Inbound.ReqIds.t()
converted = IBMessage.to_ib_api(test_msg)
assert(converted = <<0, 0, 0, 4, 8, 0, 50, 0, 0>>)
end
end
defmodule IbexTest do
use ExUnit.Case
doctest Ibex
test "greets the world" do
assert Ibex.hello() == :world
end
end
defmodule Ibex.MixProject do
use Mix.Project
def 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"}
]
end
end
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) do
Map.get(@codes, code, nil)
end
end
defmodule 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) do
Map.get(@codes, mtype, nil)
end
end
end
defmodule Ibex.Message do
require Logger
def make_field(f) do
f <> <<0>>
end
def make_msg(fields, l) do
<<l::size(32)>> <> fields <> <<0>>
end
def get_msg_length(msg) do
byte_size(msg) + 1
end
def message_version() do
<<2>>
end
end
defprotocol IBMessage do
def to_ib_api(msg)
end
defimpl IBMessage, for: List do
require Logger
def to_ib_api(msg) do
fields =
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)
end
end
defmodule Ibex.Messages.Inbound do
require Logger
def parse(m) do
<<l::size(32), rest::binary>> = m
fields = String.split(rest, <<0>>)
code = Enum.at(fields, 0)
Ibex.Messages.Types.Inbound.parse(String.to_integer(code))
{l, fields}
end
end
defmodule Ibex.Messages.Outbound do
require Logger
defmodule ReqIds do
alias Ibex.Messages.Types.Outbound, as: Outbound
import Ibex.Message, only: [message_version: 0]
def t() do
code = to_string(Outbound.parse(:req_ids))
Logger.debug("Code is: #{code}")
[
code,
<<"1">>,
<<"1">>
]
end
end
end
defmodule Ibex.Messages.Historical do
defmodule EarliestAvailableData do
defstruct [:instrument, :contract, :what_to_show, :use_rth, :format_date]
end
end
defmodule Ibex do
@moduledoc """
Documentation for `Ibex`.
"""
@doc """
Hello world.
## Examples
iex> Ibex.hello()
:world
"""
def hello do
:world
end
end
defmodule IBClient do
require Logger
use GenServer
@ip {127, 0, 0, 1}
@port 4001
@type connection_state() :: :disconnected | :connecting | :connected
def send_message(pid, message) do
GenServer.cast(pid, {:message, message})
end
def start do
Logger.debug("Starting IBClient...")
GenServer.start(__MODULE__, %{
socket: nil,
conn_state: :disconnected,
server_api: nil,
server_connect_time: nil,
next_id: 0
})
end
def init(state) do
send(self(), :tick)
{:ok, state}
end
def handle_info(:tick, state) do
state =
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 ->
state
end
if state[:conn_state] != :connected do
Logger.info("Sending tick...")
Process.send_after(self(), :tick, 1000)
end
{:noreply, state}
end
def handle_info(:start_api, state) do
Logger.debug("Starting API...")
{:noreply, state}
end
def handle_info(:req_account_summary, state) do
Logger.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}
end
def handle_info(:req_ids, state) do
Logger.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}
end
def handle_info({:tcp, _, data}, state) do
Logger.debug("Received: #{inspect(data)}")
{:noreply, state}
end
def handle_info({:tcp_closed, _}, state), do: {:stop, :normal, state}
def handle_info({:tcp_error, _}, state), do: {:stop, :normal, state}
def handle_info({other, _}, state) do
Logger.debug("Received #{inspect(other)}")
{:noreply, state}
end
def handle_cast({:message, message}, %{socket: socket} = state) do
:ok = :gen_tcp.send(socket, message)
{:noreply, state}
end
def handle({:disconnect, _}, state) do
disconnect(state, :user_requested)
{:noreply, state}
end
def disconnect(state, reason) do
Logger.info("Disconnected: #{reason}")
{:stop, :normal, state}
end
defp connect_to_server(state) do
case :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)}")
state
end
end
defp send_version(state) do
Logger.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)}")
state
end
end
defp start_api(state) do
Logger.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)}")
state
end
end
defp wait_for_api_version_response(state) do
Logger.info("Waiting for API version response...")
{_length, data} = wait_for_data(state)
state =
case length(data) do
3 ->
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)}
_ ->
state
end
%{state | conn_state: :sending_start_api}
end
defp wait_for_start_api_resp(state) do
Logger.info("Waiting for START_API response...")
wait_for_data(state)
%{state | conn_state: :transition_to_non_blocking}
end
defp wait_for_data(state) do
{:ok, data} = :gen_tcp.recv(state[:socket], 0)
Ibex.Messages.Inbound.parse(data)
end
defp transition_to_nonblocking(state) do
Logger.info("Transitioning to non-blocking mode...")
:inet.setopts(state[:socket], active: true)
%{ state | conn_state: :connected }
end
end
# Ibex
**TODO: Add description**
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `ibex` to your list of dependencies in `mix.exs`:
```elixir
def 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 can
be found at <https://hexdocs.pm/ibex>.