Developers

Implementing a new AbstractBroker

We will use AlpacaBroker as an example for how to implement the AbstractBroker interface.

Base.@kwdef mutable struct AlpacaBroker <: AbstractBroker
    key_id::String
    secret_key::String
    cache::DataCache = DataCache()
    rate::Int = 200
    last::TimeDate = current_time()
    @atomic nrequests::Int

    function AlpacaBroker(key_id, secret_key, cache, rate, last, nrequests)
        try
            header = ["APCA-API-KEY-ID" => key_id, "APCA-API-SECRET-KEY" => secret_key]
            testurl = URI("https://paper-api.alpaca.markets/v2/clock")
            resp = HTTP.get(testurl, header)
        catch e
            throw(AuthenticationException(e))
        end
        return new(key_id, secret_key, cache, rate, last, nrequests)
    end
end

key_id and secret_key are used in the constructor for authentication verification, and later on for making api calls. However, these are not assumed to be present and are thus not part of the interface.

.cache DataCache

The functions:

  • bars(broker)
  • quotes(broker)
  • trades(broker)

are defined for an AbstractBroker, but assume that the AbstractBroker has a field .cache which is a DataCache so it is advised for any AbstractBroker to follow the convention that it has a .cache field.

Wrapper Broker

If you would want to construct a "wrapper" broker, i.e. wrapping the broker which supplies the actual API to alter its default behavior (HistoricalBroker is an example of this), you should overload the broker(broker) function to return the "actual" broker:

broker(b::HistoricalBroker) = b.broker
broker(b::AlpacaBroker) = b

data_query

Next up is the data_query(broker, asset, start, stop, args...; section = ["bars" or "quotes" or"trades"], [timeframe=DateTime if section == "bars"]) function, which performs the actual api calls to retrieve either bars, quotes or trades from the broker. What to retrieve is communicated through the section kwarg, and in the case of bars it will be called using the timeframe kwarg. This should return a TimeFrame.

Bars

The bars(broker, msg::Vector) function should be overloaded to be used in DataStream to parse incoming bar updates. It should return a Vector of (asset, (datetime, (bar_open, bar_high, bar_low, bar_close, bar_volume))):

function bars(::Union{AlpacaBroker,MockBroker}, msg::AbstractVector)
    return map(filter(x -> x[:T] == "b", msg)) do bar
        asset = bar[:S]
        return asset, (parse_time(bar[:t]), (bar[:o], bar[:h], bar[:l], bar[:c], bar[:v]))
    end
end

The function subscribe_bars(broker, asset, websocket) should be overloaded in order to communicate to a DataStream which assets we want to receive updates for.

data_stream_url(broker) should point to a url from which we can stream bar update data. authenticate_data(broker) is called just after opening the stream.

Orders

The first function to overload is submit_order(broker, order::EntityState{Purchase or Sale}) which actually submites the orders. I think the AlpacaBroker one can be used with other Brokers, given that a couple of overloads are defined, but I'm not sure if the structure would always be the same.

Next up is receive_order(broker, websocket) which is called by the trading_task to listen to Order updates and thus potentially portfolio updates (when filled). It should return an Order.

To be able to open the TradingStream trading_stream_url(broker) should be overloaded and authenticate_trading(broker, websocket) which also sends the right message to start listening to the updates.

Account Details

Upon construction a Trader will fill out the current portfolio by calling account_details(broker) which should return (cash, positions) where positions = [(asset, quantity), (asset, quantity), ...].

References