Blog Post

Why Some Software Will Resist AI's Advance: The Case of Trading Systems

December 4, 2024 - Finance, GenAI
17 mins

In 1891, Keuffel & Esser began manufacturing slide rules in Hoboken, New Jersey. By 1975, they had completely ceased production—electronic calculators had won. The transition from slide rules to calculators exemplified a common pattern in technological adoption: new tools enter at the high end, become progressively more accessible, and eventually democratize the entire field. Computer-aided design (CAD) followed a similar path, starting with aerospace giants like Boeing before eventually reaching individual practitioners.

Today’s artificial intelligence revolution appears different. Instead of starting with the most complex applications, AI is taking a bottom-up approach, beginning with widely-used technologies and common development patterns. This approach works well for many domains but creates a significant challenge for specialized fields—particularly low-latency trading systems.

The Hidden World of Trading Software

Trading systems represent a unique corner of software development, one that operates far from the public repositories and open-source communities that fuel AI’s training data. These systems are built on a foundation of specialized technologies from the likes of Adaptive, Chronicle, CoralBlocks and others.

What makes these systems particularly resistant to AI assistance isn’t just their complexity—it’s their rarity. While millions of developers contribute to public repositories using React, Spring Boot, or PostgreSQL, only a tiny fraction work with trading-specific technologies. Even when these specialized tools appear in public code, it’s often in basic examples rather than the sophisticated implementations that trading firms actually use.

The Training Data Dilemma

Current AI coding assistants face a fundamental limitation: they can’t learn from code they’ve never seen. This creates a particular challenge for trading systems:

  • Public Repositories: Trading firms don't open-source their core technologies
  • Documentation: Specialized trading libraries often have minimal public documentation, and certainly stay away from sharing their most valuable optimizations and patterns.
  • Usage Patterns: The most valuable optimizations and patterns remain closely guarded
  • Hardware Integration: Custom hardware interfaces and optimizations are proprietary
  • Performance Tricks: Critical low-latency techniques are treated as trade secrets

This isn’t a temporary limitation that better algorithms will solve. It’s a structural issue rooted in the competitive nature of trading. Firms deliberately keep their most valuable code private, creating a perpetual blind spot in AI training data.

The Widening Development Gap

As AI tools become more sophisticated, we’re likely to see a growing divide in software development:

AI-Accelerated DevelopmentHuman-Dependent Development
Web applications and servicesCore trading algorithms
Non-latency sensitive business logicUltra-low latency systems
Common database operationsCustom hardware interfaces
User interface developmentExchange-specific optimizations
API integrationsProprietary protocol implementations
Basic system monitoringPerformance-critical components

This divide may arguably lead trading firms toward a two-speed IT model. Standard business systems can leverage AI tools for rapid development, while core trading systems remain dependent on specialized human expertise.

Trading systems are likely to get isolated and specialized even more so than they are today, with their development remaining in the hands of a small group of experts. And around the edges, we will see more use of AI tools to accelerate development of standard components.

AI-Assisted Examples

FX Trading UI Component

Given the following prompt:

create a react UI component for an FX trading tile. I want the tile to show the buy and sell prices, with a subtle line chart behind them showing the last 15 minutes of prices, along with an indicator of the last traded direction (up/down). This tile will be for a single currency pair; for example use cable. There will be many FX tiles displayed in a configurable grid to a user. the line chart should be below the prices, and should be updating as well. Also, the buy/sell prices should be ticking

Claude 3.5 Sonnet generated the following React component:

GBP/USD
Sell
1.2645
Buy
1.2648
Spread: 3.015m Change: NaN

This tile worked first time, with no further prompts. I did make a few minor color changes to incorporate it into this blog post.

Aeron IPC Connection

Given the following prompt:

build, using Java, an application that sends data via Aeron IPC and prints the received data to the console. Have it send “hello world”

Claude 3.5 Sonnet generated the following Java code:

import io.aeron.Aeron;
import io.aeron.Publication;
import io.aeron.driver.MediaDriver;
import io.aeron.driver.ThreadingMode;
import org.agrona.concurrent.IdleStrategy;
import org.agrona.concurrent.SleepingIdleStrategy;
import org.agrona.concurrent.UnsafeBuffer;
import org.agrona.DirectBuffer;

import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicBoolean;

public class AeronIPCExample {
    private static final String CHANNEL = "aeron:ipc";
    private static final int STREAM_ID = 10;
    private static final IdleStrategy IDLE_STRATEGY = new SleepingIdleStrategy(100);
    private static final UnsafeBuffer BUFFER = new UnsafeBuffer(ByteBuffer.allocate(256));
    private static final AtomicBoolean RUNNING = new AtomicBoolean(true);

    public static void main(final String[] args) {
        // Configure media driver for IPC mode
        final MediaDriver.Context mediaDriverContext = new MediaDriver.Context()
            .threadingMode(ThreadingMode.SHARED)
            .dirDeleteOnStart(true)
            .dirDeleteOnShutdown(true);

        // Start the media driver
        try (MediaDriver mediaDriver = MediaDriver.launch(mediaDriverContext);
             Aeron aeron = Aeron.connect();
             Publication publication = aeron.addPublication(CHANNEL, STREAM_ID)) {

            System.out.println("Publishing to " + CHANNEL + " on stream id " + STREAM_ID);

            // Start the message sender thread
            Thread senderThread = new Thread(() -> {
                int messageCount = 0;
                while (RUNNING.get()) {
                    final String message = "Hello " + messageCount++;
                    final byte[] messageBytes = message.getBytes();
                    BUFFER.putBytes(0, messageBytes);

                    long result = publication.offer(BUFFER, 0, messageBytes.length);
                    if (result > 0) {
                        System.out.println("Sent: " + message);
                    } else {
                        System.out.println("Failed to send: " + result);
                    }

                }
            });

            // Add shutdown hook
            Runtime.getRuntime().addShutdownHook(new Thread(() -> {
                System.out.println("Shutting down...");
                RUNNING.set(false);
                try {
                    senderThread.join();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }));

            // Start sending messages
            senderThread.start();

            // Keep the main thread alive
            while (RUNNING.get()) {
                IDLE_STRATEGY.idle();
            }
        }
    }
}

Running this code did not work, and further prompts did not help. Output was:

Publishing to aeron:ipc on stream id 10
Failed to send: -1
Failed to send: -1
Failed to send: -1
Failed to send: -1

The code will never work because the Aeron Subscriber is never created.

This contrasts with OpenAI's o1-pro, which produced working code after a single prompt:

import io.aeron.Aeron;
import io.aeron.Publication;
import io.aeron.Subscription;
import io.aeron.driver.MediaDriver;
import io.aeron.driver.ThreadingMode;
import io.aeron.logbuffer.FragmentHandler;
import io.aeron.logbuffer.Header;
import org.agrona.BufferUtil;
import org.agrona.DirectBuffer;
import org.agrona.concurrent.BusySpinIdleStrategy;
import org.agrona.concurrent.ShutdownSignalBarrier;
import org.agrona.concurrent.UnsafeBuffer;

import java.nio.charset.StandardCharsets;

public class Test {

    private static final String CHANNEL = "aeron:ipc";
    private static final int STREAM_ID = 10;

    public static void main(String[] args) throws InterruptedException {
        // Start an embedded Media Driver
        final MediaDriver.Context mdCtx = new MediaDriver.Context()
            .threadingMode(ThreadingMode.SHARED)
            .dirDeleteOnStart(true);
        final MediaDriver mediaDriver = MediaDriver.launchEmbedded(mdCtx);

        final Aeron.Context ctx = new Aeron.Context()
            .aeronDirectoryName(mediaDriver.aeronDirectoryName());

        final ShutdownSignalBarrier barrier = new ShutdownSignalBarrier();

        try (Aeron aeron = Aeron.connect(ctx)) {

            // Create a Subscription to listen for messages.
            try (Subscription subscription = aeron.addSubscription(CHANNEL, STREAM_ID)) {

                Runnable subscriberTask = () -> {
                    final FragmentHandler fragmentHandler = (DirectBuffer buffer, int offset, int length, Header header) -> {
                        byte[] data = new byte[length];
                        buffer.getBytes(offset, data);
                        String receivedMessage = new String(data, StandardCharsets.UTF_8);
                        System.out.println("Subscriber received message: " + receivedMessage);

                        // Signal that a message was received.
                        barrier.signal();
                    };

                    System.out.println("Subscriber listening on " + CHANNEL + " for stream " + STREAM_ID);

                    // Idle strategy for polling loop
                    final BusySpinIdleStrategy idleStrategy = new BusySpinIdleStrategy();

                    // Poll until barrier is signaled.
                    // We'll rely on barrier.await() in the main thread, and once signaled, we can break out.
                    // But since this thread does not await on barrier, we need another approach:
                    // We'll poll until barrier is signaled. Since we don't have isSignalled(), we can do:
                    // Actually, we do not need to loop after signaling. Once the message is received and signal() is called,
                    // we can just exit the thread.
                    while (true) {
                        int fragmentsRead = subscription.poll(fragmentHandler, 10);
                        if (fragmentsRead == 0) {
                            idleStrategy.idle(fragmentsRead);
                        } else {
                            // We received something and signaled the barrier, exit the loop
                            break;
                        }
                    }

                    System.out.println("Subscriber shutting down...");
                };

                Thread subscriberThread = new Thread(subscriberTask);
                subscriberThread.start();

                // Wait a moment to ensure subscriber is ready
                Thread.sleep(100);

                // Create a Publication to send a message
                try (Publication publication = aeron.addPublication(CHANNEL, STREAM_ID)) {
                    String message = "hello world";
                    byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);

                    UnsafeBuffer buffer = new UnsafeBuffer(BufferUtil.allocateDirectAligned(messageBytes.length, 64));
                    buffer.putBytes(0, messageBytes);

                    long result;
                    do {
                        result = publication.offer(buffer, 0, messageBytes.length);
                        if (result < 0L) {
                            // If back-pressured or not connected, retry
                            Thread.yield();
                        }
                    } while (result < 0L);

                    System.out.println("Publisher sent message: " + message);

                    // Wait until subscriber signals that message was received
                    barrier.await();
                }

                // Join the subscriber thread
                subscriberThread.join();
            }
        }

        mediaDriver.close();
    }
}

After running, the subscriber received the message and printed it to the console:

Subscriber listening on aeron:ipc for stream 10
Publisher sent message: hello world
Subscriber received message: hello world
Subscriber shutting down...

OpenAI's o1-pro is already able to generate correct code for this problem, but Claude is not. Pushing o1-pro further, it became a bit more unreliable—and this, I believe, is down to the underlying lack of available training data.

The Path Forward

Unlike CAD, which eventually democratized complex design work, some aspects of trading system development may remain resistant to AI assistance. This isn’t a limitation of current AI technology - it’s a fundamental characteristic of the domain. The very features that make trading systems valuable (their proprietary nature, specialized optimizations, and unique implementations) also make them difficult for AI to learn.

Today, the Keuffel & Esser building in Hoboken stands as a residential complex, a reminder of how thoroughly some technologies can be displaced. But while calculators completely replaced slide rules, the future of trading software development is likely to be more nuanced—a careful balance between AI-assisted development for standard systems and human expertise for core trading components.

This pattern may extend beyond trading to other specialized domains where proprietary technology and competitive advantage intersect. In these areas, AI might enhance development but is unlikely to replace the deep expertise that makes these systems unique.


Last updated: December 7, 2024budding

Changelog

  • December 7, 2024Updated with claude and o1-pro examples
  • December 4, 2024Initial version