Market Simulator

Last updated on 16th July 2024

This is an experimental feature!

Experimental features are early versions for users to test before final release. We work hard to ensure that every available Simudyne SDK feature is thoroughly tested, but these experimental features may have minor bugs we still need to work on.

If you have any comments or find any bugs, please share with support@simudyne.com.

Introduction

The simudyne.core.abm.finkit.market.simulator provides an API, that sits on top of the core of the SDK, which affords us the tools to create market simulations tersely. This enables us to generate market simulations quickly, leaving you free to develop and deploy trading algorithms in a controlled environment. All this is done while speaking the semantics of markets.

Market simulation models comprise of two distinct types of Agents, Traders and an AuctionMarket. Traders allow us to submit bids and asks to an AuctionMarket which process the requests and send responses. For this functionality to execute, we must run our models in a MarketSimulator.

Trader

Traders interact with a central market by sending requests. These requests can take on one of three forms.

  • NewOrder requests allow traders to make bids or asks on the market.
  • Cancel requests enable traders to cancel bids that are currently on the market.
  • Amend requests allow traders to amend orders currently on the market.

Traders also have internal state and expose methods for dealing with current bids on the market and getting the current market information.

To create an agent with this functionality simply extend Trader.

Creating a trader BasicTrader.java

public class BasicTrader extends Trader<GlobalState>{}

Requests

Requests are the way Traders communicate with the market. Traders expose three methods for sending each of the requests.

New Request - sendNewRequest()

A Trader can send requests to the market by using specific methods:

Sending a new bid request

// sending an order to the market to buy 12 units at a price of 2 per unit or less
buyer.sendNewRequest(buyer.bid()
                  .forPrice(2)
                  .forQuantity(12)
);

Sending a new ask request

// sending an order to the market to sell 10 units at a price of 3 per unit or more
seller.sendNewRequest(seller.ask()
                    .forPrice(3)
                    .forQuantity(10)
);

Amend Request - sendAmendRequest()

Once a trader has orders on the market, it can send requests to amend them. Orders currently on the market are accessed via Trader#getOrdersOnMarket() method, which will return a List containing orders. The orders can, in turn, be passed to the amend request like so.

// we begin by getting the order we want to amend
ImmutableOrder order = getOrdersOnMarket().get(0);

// here, we are amending this order with a new quantity, but the price per unit remains the same
trader.sendAmendRequest(trader.amend(order).samePrice().newQuantity(9));

Cancel Request - sendCancelRequest()

Cancelling orders is similar to amending them, except you simply need to pass the order to cancel via Trader#sendCancelRequest

// we begin by getting the order we want to cancel
ImmutableOrder order = getOrdersOnMarket().get(0);
// here, we are cancelling the order
trader.sendCancelRequest(order);

Price must be a multiple of the market's tick

You can access the tick of the market with the method `getMarketConfig().getTick()`. If the specified price does not conform to the tick, the price will be floored (if bid order) or ceiled (if ask order). You should avoid entering bids/asks with prices many orders of magnitude larger than tick size to avoid rounding errors.

Internal state

Each trader has its own wealth and quantity of shares. Every time a trader sells or buys on the market, its wealth and quantity of shares will automatically update.

Traders have access to some limited information about the market. This information will be automatically updated at each step(). A trader will know about its own orders on the market. Accessing its own orders can be done using the method Trader#getOrdersOnMarket(). This method will return an immutable List containing the orders.

A trader is also able to access the 10 best bids and asks on the market. These can be found inside the MarketInfo using Trader#getMarketInfo.

Custom handles

Once the orders have been sent to the market, the trader will receive its response during the next phase. There are several types of responses, and the trader will automatically update its knowledge of the market, wealth and quantity.

Moreover, a trader can also have user defined behaviours when receiving a certain type of response from the market. These behaviours can be defined in the following methods.

public class BasicTrader extends Trader<GlobalState>{

    @Override
    public void handleCancels(Cancelled response) {
      //define the behaviour when receiving a confirmation that an order has been cancelled.
    }

    @Override
    public void handleAcknowledgements(Acknowledge response) {
      //define the behaviour when receiving a confirmation that an order has been amended.
    }

    @Override
    public void handleCreation(OrderCreation response) {
      //define the behaviour when receiving a confirmation that an order has been created.
    }

    @Override
    public void handleReject(Reject response) {
      //define the behaviour when receiving a confirmation that an order has been rejected.
    }

    @Override
    public void handleFilledOrder(Filled response) {
      //define the behaviour when receiving an information that one of the trader's order has been filled.
    }

    @Override
    public void handlePartialFill(PartiallyFilled response) {
      //define the behaviour when receiving an information that one of the trader's order has been filled.
    }
  }

Continuous Auction Market

The auction market has one primary function. It takes requests from the Traders and matches bids to asks. As well as this, the market allows you to implement custom handles whenever a new request enters the market, has its own configurable parameters and distributes market information at the end of each round. Currently the tool kit supports a continuous auction market. In order to implement your own continuous auction market simply extend ContinuousAuctionMarket like so.

public class Market extends ContinuousAuctionMarket<GlobalState> {}

Continuous matching engine

The ContinuousAuctionMarket implements a continuous trading matching engine. Continuous trading involves the immediate execution of orders upon their receipt by the market. On entering the market new orders are exhaustively matched against all possible orders and it is only if they do not full match that they are placed on the orderbook. Cancel requests are also processed at the point they are received, removing the order from the order book before any other requests are processed. Amend requests are a combination of the two, firstly, the previous order is canceled and the new augmented order is added to the order book, attempting to match instantly.

This entire process is handled for you by the tool kit. All you need to do is create an Action and call ContinuousAuctionMarket#processRequests when you wish to process pending Trader requests.

static Action<Market> clear =
      Action.create(Market.class, ContinuousAuctionMarket::processRequests);

Shuffle orders

If a market receives orders in different runs but you want the market to process them as if they arrived at the same time, you can shuffle the orders before processing them. To do this call `ContinuousAuctionMarket#shuffleAndProcessRequests`.

Market configuration

Each market has an associated MarketConfig. This allows you to set the configurable parameters of your market. Firstly, you can set the tick size of the market, the minimum price movement of the trading instrument, which will be used as the resolution of which bids and asks are rounded to. Secondly, you can set the pricing policy of the market, this allows you to choose between pricing by the mean of the order prices or the earliest order price.

Market information

At the end of each round the market sends MarketInfo to Traders. This contains information of the best bid and ask orders left on the market at the end of bidding organised by price level.

Custom handles

As with traders you have the option of implementing custom handles which are called automatically whenever a market receives a certain request.

@Override
public void handleNewOrder(NewOrder order) {

}

@Override
public void handleAmend(Amend order) {

}

@Override
public void handleCancel(Cancel cancel) {

}

Market Simulator

Market simulator models must run inside a MarketSimulator. This provides you with familiar setup and step methods. It also provides a way of easily connecting your traders to the market.

public class DummyModel extends MarketSimulator<GlobalState> {

  public void step() {
    super.step();
    //the actions to run at each step
  }

  public void setup() {
    //creation of the different agents and of the topology
    super.setup();
  }

}

In order to connect traders to your market, simply execute the following at setup. Note that you can add as many trader groups to the market as you wish before calling build().

public void setup() {

  generateMarket(Market.class)
      .withTrader(Buyer.class, numbBuyers)
      .withTrader(Seller.class, numbSeller)
      .build();

  super.setup();
}

The method MarketSimulator#generateMarket will generate a market of the Type passed as argument, then link it to the various traders which are added using the MarketSimulator#withTrader method. This will create a fully connected topology, with all the traders being linked to the market and vice-versa. Once the desired traders have been added, the topology can be created by calling the MarketSimulator#build() method.

This will return a MarketTopology object containing the market's group (which only has one agent) and the trader's groups. You can access those group using the methods MarketTopology#getMarket() and MarketTopology#getTraders() if you want to create a more complex topology above the one created by the MarketTopology#generateMarket.

Example

The following example implements a market where zero intelligence traders place bids and asks on a continuous double auction in the spirit of Gode and Sunder (1993): http://people.brandeis.edu/~blebaron/classes/agentfin/GodeSunder.html.

Classes required

Let's start by creating all of the classes we need for this model. We will need two different Trader groups; one for the buyers and one for the sellers. We will also need a ContinousAuctionMarket to act as our market maker. Finally, our model will run in a MarketSimulator.

Lets create skeleton classes and fill in the blanks as we go.

Buyer.java

import simudyne.core.abm.GlobalState;
import simudyne.core.abm.finkit.market.simulator.Trader;

public class Buyer extends Trader<GlobalState> {}

Seller.java

import simudyne.core.abm.GlobalState;
import simudyne.core.abm.finkit.market.simulator.Trader;

public class Seller extends Trader<GlobalState> {}

Market.java

import simudyne.core.abm.finkit.market.simulator.ContinuousAuctionMarket;


public class Market extends ContinuousAuctionMarket<GlobalState> {}

MarketSim.java

import simudyne.core.abm.GlobalState;
import simudyne.core.abm.finkit.market.simulator.MarketSimulator;

public class MarketSim  extends MarketSimulator<GlobalState> {

    @Override
    public void setup() {
        super.setup();
    }

    @Override
    public void step() {
        super.step();
    }
}

Implement the Buyer

We will need to implement two actions for our Buyer. Firstly, each step the buyer will make trading decisions. If they have no currently placed bids, they have some probability of making a new bid request dependant on their activity. We can also call Trader#getOrdersOnMarket to make decisions on adjusting the price of any current bids on the market.

Secondly, following Gode and Sunder, our traders will stop once their order has been filled. We can implement a custom handle for the filled response by calling Trader#handleFilledOrder and reducing the activity of the trader to zero.

Buyer.java

import simudyne.core.abm.Action;
import simudyne.core.abm.GlobalState;
import simudyne.core.abm.finkit.market.simulator.Trader;
import simudyne.core.abm.finkit.market.simulator.requests.ImmutableOrder;
import simudyne.core.abm.finkit.market.simulator.responses.*;

public class Buyer extends Trader<GlobalState> {
  double quantity;
  double limitPrice;
  double activity = 0.1;

  public static Action<Buyer> step() {
    return Action.create(
        Buyer.class,
          double p = b.getPrng().uniform(0, b.limitPrice).sample();
          if (s.getPrng().uniform(0, 1).sample() < s.activity) {
            if (s.getOrdersOnMarket().isEmpty()) {
              s.sendNewRequest(s.bid().forPrice(p).forQuantity(s.quantity));
            } else {
              ImmutableOrder order = s.getOrdersOnMarket().get(0);
              if (p > order.getPrice()) {
                s.sendAmendRequest(s.amend(order).newPrice(p).sameQuantity());
              }
            }
          }
        });
  }

  @Override
  public void handleFilledOrder(Filled filled) {
    activity = 0;
  }
}

Implement the seller

The logic for the seller is much the same as the buyer. It has a trading decision to make each round and can update bids. It also stops participating in the market once its has been matched with a bid.

Seller.java

double quantity;
double limitPrice;
double activity = 0.1;

public static Action<Seller> step() {
  return Action.create(
      Seller.class,
      s -> {
        double p = s.getPrng().uniform(s.limitPrice, 400).sample();
        if (s.getPrng().uniform(0, 1).sample() < s.activity) {
          if (s.getOrdersOnMarket().isEmpty()) {
            s.sendNewRequest(s.ask().forPrice(p).forQuantity(s.quantity));
          } else {
            ImmutableOrder order = s.getOrdersOnMarket().get(0);
            if (p < order.getPrice()) {
              s.sendAmendRequest(s.amend(order).newPrice(p).sameQuantity());
            }
          }
        }
      });
}

@Override
public void handleFilledOrder(Filled filled) {
  activity = 0;
}

Implement the Market

For the market we simply need to create a single action that will shuffle our orders before processing them.

Market.java

public class Market extends ContinuousAuctionMarket<GlobalState> {

  public static Action<Market> clear =
      Action.create(Market.class, ContinuousAuctionMarket::shuffleAndProcessRequests);
}

Implement setup and step

We can generate our market/trader connections in setup. This is where where we call generateMarket, withTraders and build to construct the makret.

@Override
public void setup() {
  generateMarket(Market.class)
      .withTrader(
          Seller.class,
          500,
          s -> {
            s.quantity = 1;
            s.limitPrice = s.getPrng().uniform(100, 300).sample();
          })
      .withTrader(
          Buyer.class,
          500,
          b -> {
            b.quantity = 1;
            b.limitPrice = b.getPrng().uniform(100, 300).sample();
          })
      .build();

  super.setup();
}

In order to get our actions to execute we need to add them to separate runs in the model step First, our sellers and buyers will make their trading decision and at the end of each time step the market will clear.

@Override
public void step() {
  super.step();
  run(Seller.step());
  run(Buyer.step());
  run(Market.clear);
}

Report information

Finally let's go ahead and report the best bids and asks left on the market at the end of a time step. The Market exposes a getMarketInformation method which returns an object that has the best bids and best asks left on it, these are simply java treeMaps of price quantity, key value pairs. At each time step we can set the top values to a @Variable and report to the console.

@Variable double outstanding_bid = 0;
@Variable double outstanding_ask = 0;

public static Action<MarketX> bestOrders =
    Action.create(
        Market.class,
        m -> {
          if (!m.getMarketInformation().getBestBuys().isEmpty()) {
            m.outstanding_bid = m.getMarketInformation().getBestBuys().lastEntry().getKey();
          }
          if (!m.getMarketInformation().getBestSells().isEmpty()) {
            m.outstanding_ask = m.getMarketInformation().getBestSells().lastEntry().getKey();
          }
});