Stock market simulation with Cellular automaton

In my previous post I have created cellular automaton framework and implemented Conway’s game of life with it. In this post I will show you how you can use cellular automaton for one very interesting simulation – stock market. What I am going to do is a very simplified model of market micro structure. However it still can be fascinating to watch! And of course source code is provided and explained so you can try various things yourself! Even though it’s not necessary it could be helpful to read previous article first if you are going to really dive into the source code of this stock market simulation as I’m not going to explain application design here.

First of all you can download jar file and simply try out application by running this command (you will need java8 installed):

java -jar simulation.jar

Here is screenshot of how it looks like:

stockmarketautomaton

So how is it implemented? Firstly I have created 2d cellular world where each cell represents market participant. And as in the real stock market every participant affects other participants. To make it simple each cell in our stock market simulation has certain amount of money and number of shares on their balance. Before each iteration it also has action set – will it buy or sell shares next. As you can see and guess from the screenshot red cells are selling, green cells are buying and grey cells are neutral.

For market to function it must support two types of orders: limit and market. For example lets say you want to sell your phone. You put an ad that the price is 500$. That’s your offer and your limit order. Some other guy puts an ad that he is buying the phone but will only pay 450$. Now we have best bid 450$ and best offer 500$. Market order in this case would be simply to execute transaction at price offered or bid (in this case buying right away for 500$ or selling for 450$). For simplicity I made that 20% of participants are using limit orders to buy or sell and all neutral participants also put their bids and offers at much wider spreads. Other participants use market orders to buy/sell from those who did put their limit orders. The rules how market participant decides of his next action is simple: if he has 3 to 7 neighbors which are bullish – his next action is to buy. Same the other way around. However if all 8 of his neighbors are bullish or bearish – he takes contrarian view and does the opposite. Now once he has an open position he calculates his profit or loss and exit that position if loss exceeds 2% of the position size or profit is more than 6% of the position size. Participant with an open position also exits if neighbors become bullish and he is short or neighbors become bearish and he is long. And that’s it. As can you see rules are pretty simple. Now Lets walk you through the implementation and source code.

As in Conway’s game of life main class is very simple:

public class Main {
    public static void main(final String[] args) throws IOException, InterruptedException {
        final WorldGui<Action> gui = new SwingStockMarketGui();
        final World<Action> world = new StockMarketWorld(10, 10, new StockMarketAutomatonRule());
        gui.showWorld(world);
        for (int i = 0; i < 100; i++) {
            Thread.sleep(1000);
            world.nextState();
            gui.showWorld(world);
            System.out.println("Iteration " + i);
        }
    }
}

We iterate 100 times through world states and show results in GUI (which shows us final candlestick chart with each candle representing one iteration). Now obviously all the magic happens behind the scenes. First of all lets see how StockMarketWorld is implemented:

public class StockMarketWorld extends SimpleTwoDimensionalGrid<Action> {

    private static final int INITIAL_PRICE = 100;
    private final StockExchangeLevel2Book level2;
    private List<Candle> candles = new ArrayList<>(Arrays.asList(new Candle(INITIAL_PRICE, INITIAL_PRICE, INITIAL_PRICE, INITIAL_PRICE)));
    private Integer currentOpen = INITIAL_PRICE;
    private Integer currentHigh = INITIAL_PRICE;
    private Integer currentLow = INITIAL_PRICE;
    private Integer currentClose = INITIAL_PRICE;

    @Override
    protected Cell<Action> createNewCell(final XYCoordinates coordinates, final CellularAutomatonRule<Action> rule) {
        final Random random = new Random();
        // 33% chance to buy, sell, or neutral initial state
        int randomInt = random.nextInt(3);
        final Action action = randomInt == 0 ? Action.NEUTRAL : randomInt == 1 ? Action.BUY : Action.SELL;
        return new MarketParticipant(action, rule, coordinates, this);
    }

    @Override
    public void nextState() {
        currentOpen = getLastCandle().getClose();
        currentHigh = currentOpen;
        currentLow = currentOpen;
        currentClose = currentOpen;
        
        List<MarketParticipant> participants = new ArrayList<>();
        
        for (final Cell<Action>[] cellArray : worldGrid) {
            for (final Cell<Action> cell : cellArray) {
                final MarketParticipant participant = (MarketParticipant) cell;
                participants.add(participant);
            }
        }
        
        // make participants to act in random order during each iteration
        Collections.shuffle(participants);
        boolean useLimitOrders = true;
        int i = 0;
        for (MarketParticipant participant : participants) {
            participant.act(useLimitOrders);
            if (useLimitOrders && (i++ > participants.size() / 5)) { // 20% participants use limit orders
                useLimitOrders = false;
            }
        }
        
        candles.add(new Candle(currentOpen, currentHigh, currentLow, currentClose));
        
        // CellularAutomatonRule sets new values for next round
        super.nextState();
    }
}

Method createNewCell() is used to initialize world and create new cells which in this case is of MarketParticipant type. NextState() method randomly shuffle market participants and invoke their act() methods (this way they are acting in random order). First 20% of them uses limit orders for their actions. I left out other methods like placing orders on the market and returning candles or order book as I am concentrating on the behaviour of the market participants and want to save some space. You can check out full source from github.

Now lets see how Cell implementation is done. I called it MarketParticipant:

public class MarketParticipant extends SimpleCell<Action> {

    private int totalCapital = 1000000;
    private int sharesHold = 0;
    private int positionSize = 0;
    
    public MarketParticipant(final Action value, final CellularAutomatonRule<Action> rule, final Coordinates cellCoordinates,
            final World<Action> world) {
        super(value, rule, cellCoordinates, world);
    }

    public void act(boolean withLimitOrder) {
        final StockMarketWorld stockMarket = (StockMarketWorld) world;
        final int lastPrice = stockMarket.getLastCandle().getClose();
        final Random random = new Random(System.nanoTime());
        
        // buy or sell random amount from 0 to 400 shares
        final int randomNumberOfShares = (random.nextInt(4) + 1) * 100;
        final int numberOfShares = sharesHold != 0 ? Math.abs(sharesHold) : randomNumberOfShares;
        
        if (getValue() == Action.NEUTRAL) {
            int spread = random.nextInt(10) + 4;
            // if market maker undecided he simply bids and offers with big spread
            stockMarket.placeBid(this, randomNumberOfShares, lastPrice - spread);
            stockMarket.placeOffer(this, randomNumberOfShares, lastPrice + spread);
        } else if (withLimitOrder && (getValue() == Action.BUY || getValue() == Action.SELL)) {
            if (getValue() == Action.BUY) {
                int spread = random.nextInt(3) + 1; // first offer @ random spread no more than 4
                stockMarket.placeBid(this, numberOfShares, lastPrice - spread);
            } else if (getValue() == Action.SELL) {
                int spread = random.nextInt(3) + 1; // first offer @ random spread no more than 4
                stockMarket.placeOffer(this, numberOfShares, lastPrice + spread);
            }
        } else if (!withLimitOrder && (getValue() == Action.BUY || getValue() == Action.SELL)) {
            // if there is open position then size is of current position size otherwise its random number of shares
            if (getValue() == Action.BUY) {
                stockMarket.buyMarket(this, numberOfShares);
            } else if (getValue() == Action.SELL) {
                stockMarket.sellMarket(this, numberOfShares);
            }
        }
    }
}

public enum Action {
    BUY,
    SELL, 
    NEUTRAL
}

Market participant action depends on the cell value which in this case is of type Action. It is set by StockMarketAutomatonRule after the iteration for the next iteration. As you can see from the code if action is set to be NEUTRAL, market participant puts limit orders with wide spread, otherwise depending on withLimitOrder flag it puts limit or market orders to the market. And that’s about it.

The last important class is StockMarketAutomatonRule which decides what each cell is going to do during next iteration. In this case – buy, sell or stay neutral.

public class StockMarketAutomatonRule implements CellularAutomatonRule<Action> {

    @Override
    public Action calculateNewValue(final Action currentValue, final World<Action> world, final Coordinates coordinates) {
        final XYCoordinates gridCoordinates = (XYCoordinates) coordinates;
        final StockMarketWorld marketWorld = (StockMarketWorld) world;

        final MarketParticipant marketParticipant = (MarketParticipant) world.getCell(new XYCoordinates(gridCoordinates.getX(),
                gridCoordinates.getY()));
        
        final int bullishNeighbors = getNumberOfBulishNeighbors(marketWorld, gridCoordinates);
        final int bearishNeighbors = getNumberOfBearishNeighbors(marketWorld, gridCoordinates);

        if (marketParticipant.getSharesHold() == 0) {
            return calculateActionFromNeighbors(marketWorld, gridCoordinates, bullishNeighbors, bearishNeighbors);
        } else {
            int currentPositionValue = marketParticipant.getSharesHold() * marketWorld.getLastCandle().getClose();
            int profitLoss = marketParticipant.getPositionSize() - currentPositionValue;
            double profitLossPrc = (double)(profitLoss * 100)/(double)currentPositionValue;
            boolean sameCloseTwice = marketWorld.getLastCandle().getClose() == marketWorld.getCandles().get(marketWorld.getCandles().size() - 2).getClose();
            if (marketParticipant.getSharesHold() > 0) {  // if already long
                if (sameCloseTwice) {
                    return Action.SELL; // if closed same twice in a row - exit position
                } else if (bearishNeighbors >= 4) {
                    return Action.SELL;
                } else if (profitLossPrc > 6) {
                    return Action.SELL;
                } else if (profitLossPrc < -2) {
                    return Action.SELL;
                }
                    
            } else { // if already short
                if (sameCloseTwice) {
                    return Action.BUY; // if closed same twice in a row - exit position
                } else if (bullishNeighbors >= 4) {
                    return Action.BUY;
                } else if (profitLossPrc > 6) {
                    return Action.BUY;
                } else if (profitLossPrc < -2) {
                    return Action.BUY;
                }
            }
        }
        
        return Action.NEUTRAL;
    }
    
    private Action calculateActionFromNeighbors(StockMarketWorld marketWorld, XYCoordinates gridCoordinates, int bullishNeighbors, int bearishNeighbors) {
        
        boolean between3and7Bulish = 3 <= bullishNeighbors && bullishNeighbors <= 7;
        boolean between3and7Bearish = 3 <= bearishNeighbors && bearishNeighbors <= 7;
        
        if (between3and7Bearish && between3and7Bulish) {// if have both 4 bullish and 4 bearish neighbors then be neutral
            return Action.NEUTRAL;
        } else if (between3and7Bulish || bearishNeighbors > 7) {// if more than 7 bearish - take contrarian view and buy
            return Action.BUY;
        } else if (between3and7Bearish || bullishNeighbors > 7) {// if more than 7 bullish - take contrarian view and sell
            return Action.SELL;
        } else {
            return Action.NEUTRAL;
        }
    }
}

Rules are pretty simple. If participant does not have open position then it checks how many of his neighbors are bullish and how many bearish. If 3 to 7 are bullish his next action is to buy and if 3 to 7 is bearish his next action is to sell. If all 8 neighbors are bullish he takes opposite view and sells. Same if all 8 neighbors are bearish – he takes opposite view and buys. If he has open position he takes profits if it’s more than 6% of his open position and cut losses if it exceeds 2% of open position. Also exit position if last 2 candles closes at the same price or neighbors become too bearish/bullish to the opposite side. And that’s it.

Summary

As you can see it is extremely simplified simulation. It can be a lot of fun to play with different parameters and add additional features. You can check out source code from github and do it yourself. Maybe it would make sense to separate market participants into two types like market makers who use only limit orders and speculators who use market orders. It would be interesting to add participants with more different and longer term strategies. For example some moving average or stochastic (or other indicator) strategies. Each cell could look not only at their own neighbors but market sentiment as a whole. Some market “news” feature could be added that changes perception of participants and it could be interesting to watch how they react to it. If you read good book about market microstructure I am sure there will be no shortage for ideas.

Can it be useful for real world trading? Well I am not sure. One possible use case could be to identify all types of real world market participants and build realistic market microstructure model based on that. Then by running a lot of simulations try to guess current market composition and then run simulation further which could provide a peek into the future under various conditions. Could it work? I don’t know but I hope to find out 🙂