Initial commit

[?]
EDe4uae8Qq8PE9CSEr1tKCsAFQ2XmsskNU3CkZx76JUS
Aug 20, 2023, 6:42 AM
C56DKOEPHH4TMIFTWDWYTHCIR2FVGOS2A5J5LQYKWJGQ5TK6OJ6AC

Dependencies

Change contents

  • file addition: test (d--r------)
    [2.1]
  • file addition: test_helper.exs (----------)
    [0.16]
    ExUnit.start()
  • file addition: message_test.exs (----------)
    [0.16]
    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
  • file addition: ibex_test.exs (----------)
    [0.16]
    defmodule IbexTest do
    use ExUnit.Case
    doctest Ibex
    test "greets the world" do
    assert Ibex.hello() == :world
    end
    end
  • file addition: mix.exs (----------)
    [2.1]
    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
  • file addition: lib (d--r------)
    [2.1]
  • file addition: messages (d--r------)
    [0.1952]
  • file addition: types.ex (----------)
    [0.1974]
    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
  • file addition: message.ex (----------)
    [0.1974]
    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
  • file addition: historical.ex (----------)
    [0.1974]
    defmodule Ibex.Messages.Historical do
    defmodule EarliestAvailableData do
    defstruct [:instrument, :contract, :what_to_show, :use_rth, :format_date]
    end
    end
  • file addition: ibex.ex (----------)
    [0.1952]
    defmodule Ibex do
    @moduledoc """
    Documentation for `Ibex`.
    """
    @doc """
    Hello world.
    ## Examples
    iex> Ibex.hello()
    :world
    """
    def hello do
    :world
    end
    end
  • file addition: ibclient.ex (----------)
    [0.1952]
    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
  • file addition: README.md (----------)
    [2.1]
    # 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>.