FixAlchemy.Portfolio behaviour (FIXAlchemy v0.3.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 account in force.

Accounts

A session holds every account the venue names on the messages it sends, in the order they were first seen. list_accounts/2 returns them; get_account/2 returns the one in force — the account set_active_account/3 selected, else the first reported, else the :account given at start, which applies only while the venue has reported none. set_active_account/3 accepts an account drawn from list_accounts/2 and rejects any other with {:error, :unknown_account}.

An adapter records an account the base does not see with observe_account/2, and files a CollateralReport under the account it belongs to with put_collateral/3; collateral/1 reads back the report for the account in force.

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.

Functions

The accounts state holds, in the order they were first seen.

The account state's orders and account reads use.

Publish message on topic_prefix <> connection_id.

The CollateralReport for the account in force.

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

The account this session's orders and account reads use.

Adapter-owned state the base does not model.

The accounts this session holds, in the order the venue first named them.

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

Record account as one the venue has named.

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

File a CollateralReport under the account it reports on.

Select the account this session's orders and account reads use.

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

accounts(state)

@spec accounts(term()) :: [binary()]

The accounts state holds, in the order they were first seen.

The account state was started with stands in while the venue has named none.

active_account(state)

@spec active_account(term()) :: binary() | nil

The account state's orders and account reads use.

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.

collateral(state)

@spec collateral(term()) :: map() | nil

The CollateralReport for the account in force.

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

The account this session's orders and account reads use.

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()]

list_accounts(connection_id, session_name)

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

The accounts this session holds, in the order the venue first named them.

Falls back to the :account the session was started with while the venue has named none, and is empty when neither is known.

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.

observe_account(state, account)

@spec observe_account(term(), binary() | nil) :: term()

Record account as one the venue has named.

Blank and already-known accounts leave state as it is.

parse_float(value)

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

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

put_collateral(state, account, report)

@spec put_collateral(term(), binary() | nil, map()) :: term()

File a CollateralReport under the account it reports on.

A report naming no account is filed as the session's own, read back when the account in force has no report of its own.

set_active_account(connection_id, session_name, account)

@spec set_active_account(binary(), FixAlchemy.SessionConfig.name(), binary()) ::
  :ok | {:error, :unknown_account}

Select the account this session's orders and account reads use.

account must be one of list_accounts/2.