TradingBot
Completed
Experiment · Backend

TradingBot

2024

Java CLI that opens a WebSocket to the WazirX API, subscribes to the BTC/INR market stream, and prepares a buy or sell order payload on every tick, on whichever side of a trigger price you typed in the last price landed — there is no position state, no cooldown and no notion of an order already being open. Payloads are printed rather than submitted, so it never places a real order.

Built with
JavaJava
MavenMaven
WebSocketWebSocket
WazirX APIWazirX API
Project Details
RESOURCES

STATUS
Completed
YEAR

2024

TYPE

Experiment · Backend

TAGS
Trading
WebSockets
CLI

Price-triggered trading is trivial arithmetic sitting behind an awkward piece of plumbing: you need a live tick stream that stays connected, and you need it decoupled from whatever decides to act on it. This is a Java CLI that builds that plumbing against the WazirX exchange and deliberately stops one step short of placing orders.

The naive version is a REST poll. Ask for the price every second, compare, act. It works until it does not: you are rate-limited, you are always a second behind, and the interesting moment — the tick that crosses your trigger — is exactly the one a poll interval is most likely to miss. A socket inverts that. The exchange pushes, and the problem becomes lifecycle rather than latency. Who owns the connection, who owns the decision, and what happens when the two disagree about how recent the price is.

The transport, in order

  • Opens a WebSocket to wss://stream.wazirx.com/stream through Java-WebSocket 1.5.2, subclassing the library's WebSocketClient so the four lifecycle hooks are the whole interface.
  • Subscribes on connect, sending {"event":"subscribe","streams":["btcinr@ticker"]} from onOpen rather than once at startup, so any fresh handshake re-establishes the subscription instead of leaving an open socket with nothing flowing through it.
  • Parses each frame with Jackson 2.12.3, testing the root node for a lastPrice field and ignoring every frame that does not carry one. Everything else the stream sends — order book deltas, trade prints, heartbeats — is dropped without a branch.
  • Reads a trigger price from stdin with a Scanner before the evaluation loop starts. connect() is non-blocking, so the prompt appears while the handshake is still in flight.
  • Runs evaluation on its own thread, polling the client's cached last price on a one-second interval so socket I/O is never blocked by decision logic, and treating the initial 0.0 as "no data yet".
  • Formats order payloads in PayloadPreparer — buy, sell and cancel, each a small JSON string with price to two decimals or an order id.
  • Logs through two paths: SLF4J with the simple binding inside the socket client, and java.util.logging in the driver, so socket-level and strategy-level output stay separable.
  • Builds and runs as one command via the Maven exec plugin with com.logicstics.TradingBot as the main class, over mvn clean install then mvn compile exec:java.

What a prepared order actually contains

{"type":"buy","price":123456.78}. That is the whole payload. No market symbol, no quantity, no timestamp, no nonce, no HMAC signature — none of the things WazirX's authenticated order endpoint would require. prepareCancelPayload takes an order id and is never called, because nothing in the program has ever received one.

That gap is the honest measure of how far the project got. The payload formatter is a placeholder for a signing step, not a near-complete client, and calling it a demonstration of the transport rather than of order placement is the accurate description.

Why it never places an order

Payloads are printed, not sent. That is the design decision the README leads with, and it is the right one for a first version: the interesting and fragile parts here are the stream handshake, the subscription round-trip and the payload shape, none of which need a funded account to exercise. The alternative — wiring live keys in early — buys you nothing except a class of bug where a logic error costs real money, on a codebase that has no position state to protect you.

Wiring the signed order endpoint in afterwards is a bounded change: a request signer, a REST client and a place to keep the secret. Getting the stream right is not.

Honest read of the logic

The comparison is lastPrice <= trigger then else if lastPrice >= trigger, which means every single tick produces either a buy or a sell payload. There is no position state, no cooldown, and no idea of whether an order is already open. As a demonstration of the transport it does the job; as a strategy it is one branch away from being a strategy at all.

The concurrency is the other soft spot. lastPrice is a plain double field written by the socket's read thread and read by the evaluation thread with no volatile and no lock, so there is nothing in the memory model guaranteeing the poll ever sees a fresh value. It works in practice on a hot loop; it is not correct by construction. And onClose only logs the reason. The library's own reconnect() is never called and no connection-lost timeout is configured, so a dropped socket leaves the evaluation thread happily comparing the last price it saw against the trigger forever.

A few loose ends are visible in the tree. App.java is still the untouched Maven archetype Hello World!, alongside the archetype's AppTest; the exec plugin quietly points somewhere else. The evaluation loop is a bare while (true) with no shutdown path, and main returns as soon as it starts that thread, so the only things keeping the JVM alive are a non-daemon thread and an open socket. The Scanner on stdin is never closed and will throw on anything that is not a number. All of which is fine for a single-market demonstrator run from a terminal, and all of which is the list you would work through before it touched a key.

Project Details
RESOURCES

STATUS
Completed
YEAR

2024

TYPE

Experiment · Backend

TAGS
Trading
WebSockets
CLI