FixAlchemy.Portfolio behaviour (FIXAlchemy v0.2.0)

View Source

Base for a per-session portfolio subscriber.

use FixAlchemy.Portfolio gives a GenServer that owns positions, orders, and account state for one FIX session. It subscribes to the session's execution, position, and reject message types on the FixAlchemy.Client dispatch bus and processes each off the socket loop, so trading state and the TCP session survive independently of any single message's decoding.

The defaults implement plain-FIX portfolio tracking: working orders from ExecutionReports, net-per-symbol positions from fills and PositionReports, and an account summary derived from the configured/discovered account id.

A broker adapter overrides the callbacks with @impl FixAlchemy.Portfolio to add proprietary identity (e.g. a position id tag), collateral handling, or extra message types, while reusing the get/broadcast machinery:

defmodule MyBroker.Portfolio do
  use FixAlchemy.Portfolio

  @impl FixAlchemy.Portfolio
  def subscribed_types, do: super() ++ ["BA"]

  @impl FixAlchemy.Portfolio
  def handle_message("BA", raw, meta, state), do: apply_collateral(raw, state)
  def handle_message(type, raw, meta, state), do: super(type, raw, meta, state)
end

Adapter state

State the base does not model — a trade blotter, per-asset-class bookkeeping — goes in the :extra map, read back with get_extra/2,3:

def handle_message("AE", raw, _meta, state) do
  trade = raw |> FixAlchemy.Portfolio.decode(state) |> Map.delete(:raw)
  update_in(state.extra, &Map.update(&1, :trades, [trade], fn ts -> [trade | ts] end))
end

:extra is deliberately outside the change-detection that broadcasts positions, orders, and the account summary, so holding state there costs nothing per message. An adapter that wants its own updates published calls FixAlchemy.Portfolio.broadcast/3.

Summary

Callbacks

Fold an ExecutionReport into the positions map.

Fold a PositionReport into the positions map.

Build the backend-neutral account summary from current state.

Handle a dispatched message, returning the new state.

Message types this portfolio subscribes to (default execution/position/reject).

Normalize the tracked orders map for broadcast.

Callbacks

apply_execution_report(positions, msg, order, order_id, state)

@callback apply_execution_report(
  positions :: map(),
  msg :: map(),
  order :: map(),
  order_id :: binary() | nil,
  state :: term()
) :: map()

Fold an ExecutionReport into the positions map.

apply_position_report(positions, raw_msg, state)

@callback apply_position_report(positions :: map(), raw_msg :: binary(), state :: term()) ::
  map()

Fold a PositionReport into the positions map.

build_account_summary(state)

@callback build_account_summary(state :: term()) :: map() | nil

Build the backend-neutral account summary from current state.

handle_message(type, raw, meta, state)

@callback handle_message(
  type :: binary(),
  raw :: binary(),
  meta :: map(),
  state :: term()
) :: term()

Handle a dispatched message, returning the new state.

subscribed_types()

@callback subscribed_types() :: [binary()]

Message types this portfolio subscribes to (default execution/position/reject).

summarize_orders(orders)

@callback summarize_orders(orders :: map()) :: [map()]

Normalize the tracked orders map for broadcast.

Functions

broadcast(state, topic_prefix, message)

@spec broadcast(term(), binary(), term()) :: :ok

Publish message on topic_prefix <> connection_id.

For adapter-owned updates the base does not detect; positions, orders, and the account summary are already broadcast on change. No-op without a PubSub module.

decode(raw_msg, state)

@spec decode(binary(), term()) :: map()

Decode a raw message into a field map using the session's dictionary.

get_account(connection_id, session_name)

@spec get_account(binary(), FixAlchemy.SessionConfig.name()) :: binary() | nil

get_account_summary(connection_id, session_name)

@spec get_account_summary(binary(), FixAlchemy.SessionConfig.name()) :: map() | nil

get_collateral(connection_id, session_name)

@spec get_collateral(binary(), FixAlchemy.SessionConfig.name()) :: map() | nil

get_extra(connection_id, session_name)

@spec get_extra(binary(), FixAlchemy.SessionConfig.name()) :: map()

Adapter-owned state the base does not model.

Returns the whole :extra map, or the value under key when given.

get_extra(connection_id, session_name, key)

@spec get_extra(binary(), FixAlchemy.SessionConfig.name(), term()) :: term()

get_order(connection_id, session_name, order_id)

@spec get_order(binary(), FixAlchemy.SessionConfig.name(), binary()) :: map() | nil

get_orders(connection_id, session_name)

@spec get_orders(binary(), FixAlchemy.SessionConfig.name()) :: map()

get_position(connection_id, session_name, position_id)

@spec get_position(binary(), FixAlchemy.SessionConfig.name(), binary()) :: map() | nil

get_positions(connection_id, session_name)

@spec get_positions(binary(), FixAlchemy.SessionConfig.name()) :: map()

get_positions_by_symbol(connection_id, session_name, symbol)

@spec get_positions_by_symbol(binary(), FixAlchemy.SessionConfig.name(), binary()) ::
  [map()]

net_quantity(long_qty, short_qty)

@spec net_quantity(binary() | integer() | nil, binary() | integer() | nil) ::
  integer()

Net signed quantity from a position report's long and short quantities.

parse_float(value)

@spec parse_float(binary() | number() | nil) :: float()

Parse a FIX numeric field into a float, defaulting to 0.0.