Last updated on 16th July 2024
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
.
Traders
interact with a central market by sending requests. These requests can take on one of three forms.
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 are the way Traders
communicate with the market. Traders
expose three methods for sending each of the requests.
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)
);
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));
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);
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
.
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.
}
}
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> {}
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);
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.
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.
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 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
.
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.
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();
}
}
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;
}
}
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;
}
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);
}
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);
}
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();
}
});