diff --git a/algo-exercise/algo/src/main/java/codingblackfemales/sotw/SimpleAlgoState.java b/algo-exercise/algo/src/main/java/codingblackfemales/sotw/SimpleAlgoState.java index b27d0972..bec37374 100644 --- a/algo-exercise/algo/src/main/java/codingblackfemales/sotw/SimpleAlgoState.java +++ b/algo-exercise/algo/src/main/java/codingblackfemales/sotw/SimpleAlgoState.java @@ -20,4 +20,6 @@ public interface SimpleAlgoState { public List getActiveChildOrders(); public long getInstrumentId(); + + } diff --git a/algo-exercise/getting-started/README.md b/algo-exercise/getting-started/README.md new file mode 100644 index 00000000..a77a56cb --- /dev/null +++ b/algo-exercise/getting-started/README.md @@ -0,0 +1,43 @@ +# Coding Black Females - My Trading Algorithm Application + +### Overview +This project is a Java- based trading algorithm designed to automate decision making in the financial markets. The algorithm does this by analysing the order and executing buy and sell actions based on market conditions. The project aims to simplify trading operations by automatically managing orders, this helps users to make timely trading decisions without manual intervention. The main problem it addresses is the need for efficient and rule-based order management in fast-moving markets. It reduces human error by automating repetitive tasks like matching buy and sell orders, creating new orders based on specific conditions, and cancelling orders that no longer meet the criteria. This type of automation is particularly valuable for traders who want to capitalise on market opportunities quickly while adhering to a pre-defined strategy. + +This project is useful because it enhances trading efficiency by using predefined thresholds and actions to guide decision-making. The algorithm’s logging system also provides real-time insights into its operations, which helps in monitoring performance and refining trading strategies. My project offers an extendable framework starting point for developers and traders interested in creating or optimising algorithmic trading solutions. + +### How to Get Started + +#### Pre-requisites + +1. The project requires Java version 17 or higher + +##### Note +This project is configured for Java 17. If you have a later version installed, it will compile and run successfully, but you may see warnings in the log like this, which you can safely ignore: + +```sh +[WARNING] system modules path not set in conjunction with -source 17 +``` + +#### Opening the project + +1. Fork this repo in GitHub and clone it to your local machine +2. Open the project as a Maven project in your IDE (normally by opening the top level pom.xml file) +3. Click to expand the "getting-started" module + +##### Note +You will first need to run the Maven `install` task to make sure the binary encoders and decoders are installed and available for use. You can use the provided Maven wrapper or an installed instance of Maven, either in the command line or from the IDE integration. + +To get started, run the following command from the project root: `./mvnw clean install`. Once you've done this, you can compile or test specific projects using the `--projects` flag, e.g.: + +- Clean all projects: `./mvnw clean` +- Test all `algo-exercise` projects: `./mvnw test --projects algo-exercise` +- Compile the `getting-started` project only: `./mvnw compile --projects algo-exercise/getting-started` +- Then run the Algotest/ AlgoBacktest to see the output of my code + + ☺️ 💻 + + + + + + diff --git a/algo-exercise/getting-started/src/main/java/codingblackfemales/gettingstarted/MyAlgoLogic.java b/algo-exercise/getting-started/src/main/java/codingblackfemales/gettingstarted/MyAlgoLogic.java index f0259d0a..f0d5d4eb 100644 --- a/algo-exercise/getting-started/src/main/java/codingblackfemales/gettingstarted/MyAlgoLogic.java +++ b/algo-exercise/getting-started/src/main/java/codingblackfemales/gettingstarted/MyAlgoLogic.java @@ -1,30 +1,175 @@ package codingblackfemales.gettingstarted; import codingblackfemales.action.Action; +import codingblackfemales.action.CancelChildOrder; +import codingblackfemales.action.CreateChildOrder; import codingblackfemales.action.NoAction; import codingblackfemales.algo.AlgoLogic; +import codingblackfemales.sotw.ChildOrder; import codingblackfemales.sotw.SimpleAlgoState; +import codingblackfemales.sotw.marketdata.AskLevel; +import codingblackfemales.sotw.marketdata.BidLevel; import codingblackfemales.util.Util; +import messages.order.Side; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Iterator; + public class MyAlgoLogic implements AlgoLogic { private static final Logger logger = LoggerFactory.getLogger(MyAlgoLogic.class); + // Thresholds for buy and sell actions + private static long buyThreshold = 90; // Minimum acceptable bid price for a buy order + private static long sellThreshold = 115; // Minimum acceptable ask price for a sell order + private static long spreadThreshold = -3; // Minimum spread threshold for action + + @Override public Action evaluate(SimpleAlgoState state) { var orderBookAsString = Util.orderBookToString(state); +//shows current state of order book and current state of active orders logger.info("[MYALGO] The state of the order book is:\n" + orderBookAsString); + logger.info("Active Orders:" + state.getActiveChildOrders().toString()); + + var totalOrderCount = state.getChildOrders().size(); + + //make sure we have an exit condition... + if (totalOrderCount > 10) { + return NoAction.NoAction; + } + + + + final BidLevel bidlevel = state.getBidAt(0); + final long bestBidPrice = bidlevel.price; + final long bidQuantity = bidlevel.quantity; + + + + final AskLevel asklevel = state.getAskAt(0); + final long bestAskPrice = asklevel.price; + final long askQuantity = asklevel.quantity; + + + //calculate the spread + final long spread = bestAskPrice - bestBidPrice; + logger.info("[MYALGO] Spread between ask and bid " + spread); + + //calculate the midPrice + final long midPrice =(bestBidPrice + bestBidPrice)/2; + logger.info("[MYALGO] Mid-price calculated:" + midPrice); + + //Calculate the spread as a percentage of the mid-price + final double spreadPercentage = (double) spread / midPrice * 100; + logger.info("[MYALGO] Spread percentage: " + spreadPercentage + "%"); + + // Check for matching orders + matchOrders(state, bestBidPrice, bestAskPrice); + + + + // not to create or cancel orders because spread is small + if (spread < spreadThreshold) { + logger.info("[MYALGO] The spread is small " + spread + " No Action"); + return NoAction.NoAction; + + } + + +//count the buy and sell orders + long buyOrdersCount = state.getChildOrders().stream().filter(ChildOrder -> ChildOrder.getSide() == Side.BUY).count(); + long sellOrdersCount = state.getChildOrders().stream().filter(ChildOrder -> ChildOrder.getSide() == Side.SELL).count(); + + + + //create new buy orders if there are less than 5 orders + if (buyOrdersCount < 5 ) { + return createBuyOrder(state, buyOrdersCount, bidQuantity, bestBidPrice); + } - /******** - * - * Add your logic here.... - * - */ + //create new sell orders if there are less than 5 order + if (sellOrdersCount < 5 ) { + return createSellOrder(state, sellOrdersCount, askQuantity, bestAskPrice); + } + + //cancel orders that don't match the best price + + return cancelOrders(state, bestBidPrice, bestAskPrice); + } + + private void matchOrders(SimpleAlgoState state, long bestBidPrice, long bestAskPrice) { + Iterator iterator = state.getActiveChildOrders().iterator(); + + while (iterator.hasNext()) { + ChildOrder order = iterator.next(); + if (order.getSide() == Side.BUY && order.getPrice() >= bestAskPrice) { + logger.info("[MYALGO] Matched BUY order: Order ID: #" + order.getOrderId() +", Side: " + order.getSide() + ", Price: " + order.getPrice() + ", Quantity: " + order.getQuantity()); + iterator.remove(); // Remove matched order + } else if (order.getSide() == Side.SELL && order.getPrice() <= bestBidPrice) { + logger.info("[MYALGO] Matched SELL order: Order ID: #" + order.getOrderId() +", Side: " + order.getSide() + ", Price: " + order.getPrice() + ", Quantity: " + order.getQuantity()); + iterator.remove(); // Remove matched order + } + } + } + + + + + + //create method to create new buy order + public Action createBuyOrder(SimpleAlgoState state, long buyOrdersCount, long bidQuantity, long bestBidPrice) { + logger.info("[MYALGO] Creating new buy order: " + state.getChildOrders().size() + " orders and add to new buy order " + bidQuantity + " @ " + bestBidPrice); + logger.info(state.getActiveChildOrders().toString()); + logger.info("Buy order count is:" + buyOrdersCount); + //creates a new child order + return new CreateChildOrder(Side.BUY, bidQuantity, bestBidPrice); + + } + + // create a method to create a new sell order + public Action createSellOrder(SimpleAlgoState state, long sellOrdersCount, long askQuantity, long bestAskPrice) { + logger.info("[MYALGO] Creating new sell order:" + state.getChildOrders().size() + " orders and add to new sell order " + askQuantity + " @ " + bestAskPrice); + logger.info("Sell order count is:" + sellOrdersCount); + //creates a new child order + return new CreateChildOrder(Side.SELL, askQuantity, bestAskPrice); + } + + //create a method to cancel orders that don't match best price or fall below thresholds + public Action cancelOrders(SimpleAlgoState state, long bestBidPrice, long bestAskPrice){ + for (ChildOrder order : state.getActiveChildOrders()){ + logger.info("Order ID: #" + order.getOrderId() + ", Side: " + order.getSide() + ", Price: " + order.getPrice() + ", Quantity: " + order.getQuantity()); + + boolean buyOrder = order.getSide() ==Side.BUY; + boolean notBuyThreshold = order.getPrice() != buyThreshold; + boolean lessThanBuyThreshold = order.getPrice() < buyThreshold; + + // Cancel buy orders not matching the best price or below the buy threshold + if (buyOrder && (notBuyThreshold || lessThanBuyThreshold)) { + logger.info(String.format("The buy Threshold is %d", + buyThreshold )); + logger.info(String.format("[MYALGO] Cancel BUY order %d with price %d and quantity %d. The current best bid price is %d." ,+ order.getOrderId(), + order.getPrice(), + order.getQuantity(), + bestBidPrice)); + return new CancelChildOrder(order); + } + boolean sellTheOrder = order.getSide() ==Side.SELL; + boolean notSellThreshold = order.getPrice() != sellThreshold; + boolean lessThanSellThreshold = order.getPrice() < sellThreshold; + + + // Cancel sell orders not matching the best price or below the sell threshold + if (sellTheOrder && (notSellThreshold || lessThanSellThreshold)) { + logger.info(String.format("The sell Threshold is %d", + sellThreshold )); + logger.info(String.format("[MYALGO] Cancel SELL order %d with price %d and quantity %d. The current best ask price is %d." ,+ order.getOrderId(), + order.getPrice(), + order.getQuantity(), + bestAskPrice)); + return new CancelChildOrder(order); + } + } return NoAction.NoAction; } } + + + + diff --git a/algo-exercise/getting-started/src/test/java/codingblackfemales/gettingstarted/AbstractAlgoTest.java b/algo-exercise/getting-started/src/test/java/codingblackfemales/gettingstarted/AbstractAlgoTest.java index f57fef15..7db0e8f3 100644 --- a/algo-exercise/getting-started/src/test/java/codingblackfemales/gettingstarted/AbstractAlgoTest.java +++ b/algo-exercise/getting-started/src/test/java/codingblackfemales/gettingstarted/AbstractAlgoTest.java @@ -75,6 +75,73 @@ protected UnsafeBuffer createTick(){ return directBuffer; } + protected UnsafeBuffer createTickWithHighThreshold() { + final MessageHeaderEncoder headerEncoder = new MessageHeaderEncoder(); + final BookUpdateEncoder encoder = new BookUpdateEncoder(); + final ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024); + final UnsafeBuffer directBuffer = new UnsafeBuffer(byteBuffer); + + // Encode the message + encoder.wrapAndApplyHeader(directBuffer, 0, headerEncoder); + + // Set a high spread by configuring ask and bid levels + encoder.venue(Venue.XLON); + encoder.instrumentId(123L); + + // Here, we’ll set a very high ask and a much lower bid. + encoder.askBookCount(1).next().price(150L).size(300L); // High ask price + encoder.bidBookCount(1).next().price(100L).size(300L); // Low bid price + + encoder.instrumentStatus(InstrumentStatus.CONTINUOUS); + encoder.source(Source.STREAM); + + return directBuffer; + } + + protected UnsafeBuffer createTickWithLowThreshold() { + final MessageHeaderEncoder headerEncoder = new MessageHeaderEncoder(); + final BookUpdateEncoder encoder = new BookUpdateEncoder(); + final ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024); + final UnsafeBuffer directBuffer = new UnsafeBuffer(byteBuffer); + + encoder.wrapAndApplyHeader(directBuffer, 0, headerEncoder); + encoder.venue(Venue.XLON); + encoder.instrumentId(123L); + + encoder.askBookCount(1) + .next().price(100L).size(200L); // Ask price close to bid + + encoder.bidBookCount(1) + .next().price(90L).size(200L); // Bid price close to ask + + encoder.instrumentStatus(InstrumentStatus.CONTINUOUS); + encoder.source(Source.STREAM); + + return directBuffer; + } + + protected UnsafeBuffer createTickWithIncreasingVolume() { + final MessageHeaderEncoder headerEncoder = new MessageHeaderEncoder(); + final BookUpdateEncoder encoder = new BookUpdateEncoder(); + final ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024); + final UnsafeBuffer directBuffer = new UnsafeBuffer(byteBuffer); + + encoder.wrapAndApplyHeader(directBuffer, 0, headerEncoder); + encoder.venue(Venue.XLON); + encoder.instrumentId(123L); + + encoder.askBookCount(1) + .next().price(110L).size(1000L); // Higher ask quantity + + encoder.bidBookCount(1) + .next().price(108L).size(900L); // Higher bid quantity + + encoder.instrumentStatus(InstrumentStatus.CONTINUOUS); + encoder.source(Source.STREAM); + + return directBuffer; + } + } diff --git a/algo-exercise/getting-started/src/test/java/codingblackfemales/gettingstarted/MyAlgoBackTest.java b/algo-exercise/getting-started/src/test/java/codingblackfemales/gettingstarted/MyAlgoBackTest.java index 1c1b3fd7..7ff75c75 100644 --- a/algo-exercise/getting-started/src/test/java/codingblackfemales/gettingstarted/MyAlgoBackTest.java +++ b/algo-exercise/getting-started/src/test/java/codingblackfemales/gettingstarted/MyAlgoBackTest.java @@ -1,8 +1,13 @@ package codingblackfemales.gettingstarted; import codingblackfemales.algo.AlgoLogic; +import codingblackfemales.sotw.ChildOrder; +import messages.order.Side; +import org.junit.Before; import org.junit.Test; +import static org.junit.Assert.*; + /** * This test plugs together all of the infrastructure, including the order book (which you can trade against) * and the market data feed. @@ -22,6 +27,14 @@ public AlgoLogic createAlgoLogic() { return new MyAlgoLogic(); } + + @Before + + public void setup() { + container.getState().getChildOrders().clear(); + + } + @Test public void testExampleBackTest() throws Exception { //create a sample market data tick.... @@ -42,4 +55,25 @@ public void testExampleBackTest() throws Exception { //assertEquals(225, filledQuantity); } + @Test public void testForOrderManagement () throws Exception{ + send(createTick()); + send(createTick2()); + + //check algo has a max order limit of 10 + assertTrue("There should be at least 10 child orders", container.getState().getChildOrders().size() <=10 ); + } + +// @Test +// +// public void testCancelOrders () throws Exception{ +// for (int i = 0; i< 5; i++){ +// container.getState().getChildOrders().add(new ChildOrder(Side.BUY, 500L + i, 110, 115, i + 1)); +// send(createTick2()); +// assertEquals(10, container.getState().getChildOrders().size()); +// ChildOrder cancelOrders = container.getState().getChildOrders().stream().filter(order -> order.getState() == 2).findFirst().orElse(null); +// System.out.println("Check if there are cancelled orders. Found: " + (cancelOrders != null)); +// assertEquals(true, cancelOrders != null); +// +// } +// } } diff --git a/algo-exercise/getting-started/src/test/java/codingblackfemales/gettingstarted/MyAlgoTest.java b/algo-exercise/getting-started/src/test/java/codingblackfemales/gettingstarted/MyAlgoTest.java index c16427bb..68e42a04 100644 --- a/algo-exercise/getting-started/src/test/java/codingblackfemales/gettingstarted/MyAlgoTest.java +++ b/algo-exercise/getting-started/src/test/java/codingblackfemales/gettingstarted/MyAlgoTest.java @@ -1,7 +1,24 @@ package codingblackfemales.gettingstarted; +import codingblackfemales.action.Action; +import codingblackfemales.action.CreateChildOrder; +import codingblackfemales.action.NoAction; import codingblackfemales.algo.AlgoLogic; +import codingblackfemales.service.MarketDataService; +import codingblackfemales.service.OrderService; +import codingblackfemales.sotw.marketdata.BidLevel; +import org.agrona.concurrent.UnsafeBuffer; +import codingblackfemales.sotw.SimpleAlgoState; +import codingblackfemales.sotw.SimpleAlgoStateImpl; +import messages.order.Side; +import org.junit.Assert; +import org.junit.Before; import org.junit.Test; +import codingblackfemales.container.RunTrigger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + /** @@ -15,6 +32,10 @@ * */ public class MyAlgoTest extends AbstractAlgoTest { + private SimpleAlgoState algoState; + private MarketDataService marketDataService; + private OrderService orderService; + @Override public AlgoLogic createAlgoLogic() { @@ -22,6 +43,14 @@ public AlgoLogic createAlgoLogic() { return new MyAlgoLogic(); } + @Before + public void setUp() { + marketDataService = new MarketDataService(new RunTrigger()); + orderService = new OrderService(new RunTrigger()); + algoState = new SimpleAlgoStateImpl(marketDataService, orderService); + System.out.println(" MyAlgoTest"); + } + @Test public void testDispatchThroughSequencer() throws Exception { @@ -29,7 +58,51 @@ public void testDispatchThroughSequencer() throws Exception { //create a sample market data tick.... send(createTick()); - //simple assert to check we had 3 orders created - //assertEquals(container.getState().getChildOrders().size(), 3); + + SimpleAlgoState state = container.getState(); + Action action = createAlgoLogic().evaluate(state); + assertEquals("Do not action if spread is below threshold", NoAction.NoAction, action); + } + + @Test + public void testCancelOrderOnLowThreshold() throws Exception { + // Send tick with low spread + send(createTickWithLowThreshold()); + SimpleAlgoState state = container.getState(); + Action action = createAlgoLogic().evaluate(state); + + // Check that no action is taken if spread is too low for orders + assertTrue("Expected NoAction when spread is below threshold", action instanceof NoAction); + } + + + @Test + public void testHandleIncreasingVolume() throws Exception { + // Arrange: Set up a tick with increasing bid/ask volume + send(createTickWithIncreasingVolume()); + SimpleAlgoState state = container.getState(); + + // Act: Evaluate the state after the high-volume tick + Action action = createAlgoLogic().evaluate(state); + + // Assert: Check that algo reacts appropriately to the high-volume tick + assertTrue("Expected NoAction or appropriate order response on high volume", action instanceof Action); + } + + @Test + public void testCreateOrderWithHighThreshold() throws Exception { + // Arrange: Send a tick with a high spread + send(createTickWithHighThreshold()); + } + + @Test + public void testMaxOrders() throws Exception { + send(createTick()); + + //check algo has a max order limit of 10 + assertTrue("There should be at least 10 child orders", container.getState().getChildOrders().size() <= 10); } } + + +