ExperimentalAgents - abides-sim/abides GitHub Wiki

Create your own Experimental Agent

In this tutorial we will go through how to create an Experimental Agent in ABIDES. We will assume access to some reference configuration (here we will be using RMCS03). We will then go through the methods that need to be implemented for the agent to function and design a simple trading strategy. Then we can use tools provided in the ABIDES codebase to analyse the effectiveness of the strategy.

To keep the focus on the experimental agent design, we have provided a config housing the agent we'll create in this tutorial in the file config/exp_agent_demo.py (for more tips on config design check out this wiki page). This config is a variant of our Reference Market Simulation Configuration config/rmsc03.py with the option to house one of two possible experimental agents, ExampleExperimentalAgentTemplate and ExampleExperimentalAgent, controlled via a command-line switch. Both of these agents are found in the file agent/examples/ExampleExperimentAgent.py.

The agent ExampleExperimentalAgentTemplate is a simple agent to demonstrate the minimal set of methods an experimental agent will need to implement to interact with the market. Indeed, this agent has no affect on the market as such. It merely sits and receives market data at some subscription frequency (in the config we set this to every second but this can be changed, along with the number of orderbook levels to subscribe to). The agent also wakes up at a fixed frequency, but takes no action. The agent ExampleExperimentalAgent inherits from the ExampleExperimentalAgentTemplate and implements a simple mean-reversion strategy. We'll see that it takes a relatively small amount of code to implement a new strategy due to the abstractions defined in the ABIDES API.

Let's take a look at the construction of the ExampleExperimentalAgent, to implement your own strategy you can copy/edit this class.

def __init__(self, *args, wake_freq, order_size, short_window, long_window, **kwargs):
   super().__init__(*args, **kwargs)
   self.wake_freq = wake_freq
   self.order_size = order_size
   self.short_window = short_window
   self.long_window = long_window
   self.mid_price_history = pd.DataFrame(columns=['mid_price'], index=pd.to_datetime([]))

In the constructor we need to initialise with the superclass *args and **kwargs to ensure that the agent has all relevant attributes defined. We also define the following arguments unique to this strategy

  • wake_freq : this will define how often the agent will trade.
  • order_size: this will be the size of market order placed.
  • short_window: this defines the length of time the trading algo considers "short-term".
  • long_window: this defines the length of time the trading algo considers "long-term".

We also initialise the attribute mid_price_history: this variable will hold all of the mid-prices received from the market data subscription. The agents strategy will be as follows:

Compute the "short-term" and "long-term" rolling averages of the observed market mid-price. If short_term > long_term we assume the mid-price will decrease, so sell order_size shares. If short_term < long_term we assume the mid-price will increase, so buy order_size shares.

We now define the agent's behaviour when it recieves a message from the exchange.

def receiveMessage(self, currentTime, msg):
    super().receiveMessage(currentTime, msg)  # receives subscription market data
    self.mid_price_history = self.mid_price_history.append(
        pd.Series({'mid_price': self.getCurrentMidPrice()}, name=currentTime))
    self.mid_price_history.dropna(inplace=True)

We need to call the superclass' receiveMessage method, which collects the subscription market data and processes meassages from the exchange such as when the market opens and closes. We also append the current mid price to the agent's price history using the getCurrentMidPrice method, defined as follows.

def getCurrentMidPrice(self):
    try:
        best_bid = self.current_bids[0][0]
        best_ask = self.current_asks[0][0]
        return round((best_ask + best_bid) / 2)
    except (TypeError, IndexError):
        return None

The self.current_bids and self.current_asks attributes are populated with the orderbook subscription data by the receiveMessage method. They are each a list, where each element is a tuple (price, quantity).

We now need to define the wakeup behaviour of the agent. First we overload the getWakeFrequency method to set the next wakeup as self.wake_freq. Note that getWakeFrequency can return a next wakeup using randomness or some deterministic formula, it need not be a constant. Here for simplicity we set a constant wakeup.

def getWakeFrequency(self):
    return pd.Timedelta(self.wake_freq)

We also need to override the wakeup method, implementing the mean-reversion strategy.

def wakeup(self, currentTime):
    super().wakeup(currentTime)
    short_moving_avg, long_moving_avg = self.computeMidPriceMovingAverages()
    if short_moving_avg is not None and long_moving_avg is not None:
        if short_moving_avg > long_moving_avg:
            self.placeMarketOrder(self.order_size, 0)
        elif short_moving_avg < long_moving_avg:
            self.placeMarketOrder(self.order_size, 1)

First we must call the superclass wakeup method. We then compute moving averages with the computeMidPriceMovingAverages method (defined below). Notice we then implement the placeMarketOrder method. This method is defined in ExampleExperimentalAgentTemplate along with placeLimitOrder and cancelAllOrders, which can be used for other user-defined strategies.

def computeMidPriceMovingAverages(self): 
    try:
        short_moving_avg = self.mid_price_history.rolling(self.short_window).mean().iloc[-1]['mid_price']
        long_moving_avg = self.mid_price_history.rolling(self.long_window).mean().iloc[-1]['mid_price']
        return short_moving_avg, long_moving_avg
    except IndexError:
        return None, None

This method computes the short and long term moving average mid-prices from the market data using a pandas function.

That is all the code needed to define the mean-reversion agent, we can test how profitable this family of strategies is for a number of different parameter values. Let us try two settings:

  • (I) Short-term window of 1 second, long-term window of 30 seconds.
  • (II) Short-term window of 2 minutes, long-term window of 5 minutes.

We can run each of these settings for 30 seeds like so:

Note that you'll need the free command-line utility GNU parallel to run the following commands.

NUM_JOBS=48  # number of jobs to run in parallel, may need to reduce to satisfy computational constraints

for seed in $(seq 100 130); do
      sem -j${NUM_JOBS} --line-buffer python -u abides.py -c exp_agent_demo -t ABM -d 20200603 -s ${seed} \
       -l experimental_agent_demo_short_1s_long_30s_${seed} -e --ea-short-window '1s' --ea-long-window '30s'
done

for seed in $(seq 100 130); do
      sem -j${NUM_JOBS} --line-buffer python -u abides.py -c exp_agent_demo -t ABM -d 20200603 -s ${seed} \
       -l experimental_agent_demo_short_2min_long_5min_${seed} -e --ea-short-window '2min' --ea-long-window '5min'
done

Once these experiments are complete we can compare profitability. For config I:

python -u cli/read_agent_logs.py log/experimental_agent_demo_short_1s_long_30s_*
                                 Return       Surplus
AgentType
ExampleExperimentalAgent -992551.806452 -2.696557e+08

and for config II:

python -u cli/read_agent_logs.py log/experimental_agent_demo_short_2min_long_5min_*
                                 Return       Surplus
AgentType
ExampleExperimentalAgent -768427.709677 -6.968747e+08

The return is the difference between the initial and final cash positions of the agent. The surplus is the difference of the agent's initial cash position from its final holdings (cash + shares marked to the final market price). So it appears that both of these strategies are unprofitable! However, config I does better for surplus and config II does better for return (cash).

This experiment code sits in the script scripts/experimental_agent_demo.sh for inconvenience.

For a thorough introduction to the ABIDES platform please see the paper ABIDES: Towards High-Fidelity Multi-Agent Market Simulation, for details on the individual agent behaviours see the paper Explaining Agent-Based Financial Market Simulation.

If you'd like more details on the RMSC03 configuration in which we tested this configuration, please see this tutorial. Thanks for following along and happy simulating!