Skip to main content

Overview

Opinix Trade uses a sophisticated order matching engine that processes trades asynchronously via Redis queues. When you place an order, it’s instantly matched against existing orders or added to the order book for future matching.
All order book updates are broadcast in real-time via WebSocket, ensuring you see market changes the instant they happen.

How trading works

Opinix uses a prediction market model where you trade on event outcomes:

YES position

You believe the event will occur. If it does, each unit pays ₹10.

NO position

You believe the event won’t occur. If it doesn’t, each unit pays ₹10.

Pricing dynamics

Prices are determined by supply and demand:
  • YES price: What buyers pay for YES positions
  • NO price: What buyers pay for NO positions
  • Prices fluctuate based on market sentiment
  • Total YES + NO price typically ≈ ₹10 (but can vary)
Example:
  • Event: “Bitcoin to reach ₹60,00,000 by midnight”
  • YES trading at ₹7.50 → Market believes 75% probability
  • NO trading at ₹2.50 → Market believes 25% won’t happen

Order book structure

Each event has its own order book containing all pending YES and NO orders.

Order book components

YES orders represent buyers willing to purchase YES positions:
interface Order {
  price: number;        // Price per unit
  quantity: number;     // Number of units
  filled: number;       // Units already matched
  orderId: string;      // Unique identifier
  side: "yes";         // Order type
  userId: string;      // Trader ID
}
Bids are sorted by:
  1. Price (highest first)
  2. Time (earliest first at same price)

Viewing the order book

The order book UI displays:
<Table>
  <TableHeader>
    <TableRow>
      <TableHead>PRICE</TableHead>
      <TableHead>QTY AT YES</TableHead>
      <TableHead>PRICE</TableHead>
      <TableHead>QTY AT NO</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    {orderBook.map((row) => (
      <TableRow>
        <TableCell className="text-blue-500">{row.yesPrice}</TableCell>
        <TableCell>
          <div className="relative">
            <div className="bg-blue-700 opacity-20" 
                 style={{ width: getBarWidth(row.yesQty, maxQty) }} />
            <div className="text-blue-500">{row.yesQty}</div>
          </div>
        </TableCell>
        <TableCell className="text-red-500">{row.noPrice}</TableCell>
        <TableCell>
          <div className="relative">
            <div className="bg-red-700 opacity-20"
                 style={{ width: getBarWidth(row.noQty, maxQty) }} />
            <div className="text-red-500">{row.noQty}</div>
          </div>
        </TableCell>
      </TableRow>
    ))}
  </TableBody>
</Table>
Bar widths visualize relative quantities at each price level.

Placing orders

Here’s how to place an order through the UI:
1

Choose event

Navigate to an event’s trading page at /events/[id].
2

Select position

In the “Place Order” card, click YES or NO:
const [side, setSide] = useState<"yes" | "no">("yes");

<Button
  variant={side === "yes" ? "default" : "outline"}
  onClick={() => setSide("yes")}
  className="bg-blue-500"
>
  Yes ₹{currentYesPrice}
</Button>
3

Set price

Enter your desired price per unit (₹0.00 - ₹10.00):
const [tradePrice, setTradePrice] = useState("");

<Input
  type="number"
  value={tradePrice}
  onChange={(e) => setTradePrice(e.target.value)}
  placeholder="Enter price"
/>
The UI shows available quantity at your chosen price from the order book.
4

Enter quantity

Specify how many units to buy:
const [tradeQuantity, setTradeQuantity] = useState("");

<Input
  type="number"
  value={tradeQuantity}
  onChange={(e) => setTradeQuantity(e.target.value)}
  placeholder="Enter quantity"
/>
5

Review calculation

The panel displays:
  • You put: price × quantity (your cost)
  • You get: Potential payout if you win (₹10 × quantity)
6

Submit order

Click “Place order” to submit:
async function handleTrade() {
  const response = await fetch('/api/placeorder', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      market: eventId,
      price: Number(tradePrice),
      quantity: Number(tradeQuantity),
      side: side,
      userId: session.user.id
    })
  });
  
  const result = await response.json();
  // Handle success/error
}
Ensure you have sufficient available balance. Required amount = price × quantity for YES orders, or quantity for NO orders.

Order matching engine

Opinix’s matching engine is built with TypeScript and processes orders asynchronously.

Engine architecture

export class Engine {
  private balances: Map<string, UserBalance> = new Map();
  private orderbooks: Orderbook[] = [];
  
  processOrders({ message, clientId }: {
    message: MessageFromApi;
    clientId: string;
  }) {
    switch (message.type) {
      case CREATE_ORDER:
        const { executedQty, fills, orderId } = this.createOrders(
          message.data.market,
          message.data.price,
          message.data.quantity,
          message.data.side,
          message.data.userId
        );
        
        // Publish result to client via Redis
        RedisManager.getInstance().sendToApi(clientId, {
          type: "ORDER_PLACED",
          payload: { orderId, executedQty, fills }
        });
        break;
      
      case CANCEL_ORDER:
        // Handle cancellation...
        break;
    }
  }
}

Order creation flow

1

Validate and lock funds

Before processing, engine validates balance:
checkAndLockFunds(
  side: "yes" | "no",
  userId: string,
  price: number,
  quantity: number
) {
  if (side === "yes") {
    const required = quantity * price;
    if ((this.balances.get(userId)?.available || 0) < required) {
      throw new Error("Insufficient balance");
    }
    
    // Lock funds
    this.balances.get(userId).available -= required;
    this.balances.get(userId).locked += required;
  } else {
    if ((this.balances.get(userId)?.available || 0) < quantity) {
      throw new Error("Insufficient funds");
    }
    
    this.balances.get(userId).available -= quantity;
    this.balances.get(userId).locked += quantity;
  }
}
2

Create order object

const order: Order = {
  price: Number(price),
  quantity: Number(quantity),
  orderId: uuidv4(),
  filled: 0,
  side,
  userId
};
3

Attempt matching

The orderbook tries to match the order:
const { fills, executedQty } = orderbook.addOrder(order);
4

Update balances

For matched portions, transfer funds:
updateBalance(userId: string, side: "yes" | "no", fills: Fill[]) {
  if (side === "yes") {
    fills.forEach((fill) => {
      const makerBalance = this.balances.get(fill.otherUserId);
      const takerBalance = this.balances.get(userId);
      
      // Maker receives payment
      makerBalance.available += fill.qty * fill.price;
      takerBalance.locked -= fill.qty * fill.price;
      
      // Taker receives position
      makerBalance.locked -= fill.qty;
      takerBalance.available += fill.qty;
    });
  }
}
5

Create database trades

Log each fill to database via Redis:
fills.forEach((fill) => {
  RedisManager.getInstance().pushMessage({
    type: TRADE_ADDED,
    data: {
      market: market,
      id: fill.tradeId,
      isBuyerMaker: fill.otherUserId === userId,
      price: fill.price,
      quantity: fill.qty,
      timestamp: Date.now()
    }
  });
});
6

Broadcast updates

Send real-time updates via WebSocket:
publishWsTrades(fills: Fill[], userId: string, market: string) {
  fills.forEach((fill) => {
    RedisManager.getInstance().publishMessage(`trade@${market}`, {
      stream: `trade@${market}`,
      data: {
        e: "trade",
        t: fill.tradeId,
        m: fill.otherUserId === userId,
        p: fill.price,
        q: fill.qty.toString(),
        s: market
      }
    });
  });
}

Matching algorithm

For YES (bid) orders:
matchBid(order: Order): { fills: Fill[]; executedQty: number } {
  const fills: Fill[] = [];
  let executedQty = 0;
  
  // Iterate through asks (NO orders)
  for (let i = 0; i < this.asks.length; i++) {
    if (this.asks[i]?.price! <= order.price && executedQty < order.quantity) {
      const filledQty = Math.min(
        order.quantity - executedQty,
        this.asks[i]?.quantity!
      );
      
      executedQty += filledQty;
      this.asks[i].filled += filledQty;
      
      fills.push({
        price: this.asks[i]?.price!,
        qty: filledQty,
        tradeId: uuidv4(),
        otherUserId: this.asks[i]?.userId!,
        marketOrderId: this.asks[i]?.orderId!
      });
    }
  }
  
  // Remove fully filled asks
  this.asks = this.asks.filter(ask => ask.filled < ask.quantity);
  
  return { fills, executedQty };
}
For NO (ask) orders:
matchAsk(order: Order): { fills: Fill[]; executedQty: number } {
  const fills: Fill[] = [];
  let executedQty = 0;
  
  // Iterate through bids (YES orders)
  for (let i = 0; i < this.bids.length; i++) {
    if (this.bids[i]?.price! >= order.price && executedQty < order.quantity) {
      const filledQty = Math.min(
        order.quantity - executedQty,
        this.bids[i]?.quantity!
      );
      
      executedQty += filledQty;
      this.bids[i].filled += filledQty;
      
      fills.push({
        price: this.bids[i]?.price!,
        qty: filledQty,
        tradeId: uuidv4(),
        otherUserId: this.bids[i]?.userId!,
        marketOrderId: this.bids[i]?.orderId!
      });
    }
  }
  
  // Remove fully filled bids
  this.bids = this.bids.filter(bid => bid.filled < bid.quantity);
  
  return { fills, executedQty };
}
Orders are matched using price-time priority: best prices first, then earliest orders at the same price.

WebSocket integration

Real-time order book updates use WebSocket connections.

Connecting to WebSocket

const ws = new WebSocket('ws://server-url');

ws.onopen = () => {
  // Subscribe to event
  ws.send(JSON.stringify({
    eventId: "bitcoin-price-prediction"
  }));
};

ws.onmessage = (message) => {
  const data = JSON.parse(message.data);
  
  // Update order book in UI
  setOrderBook(data.orderBook);
};

ws.onclose = () => {
  console.log('WebSocket disconnected');
};

Server-side WebSocket

The server manages client connections and broadcasts:
import WebSocket from "ws";

let clients: { ws: WebSocket; eventId: string }[] = [];

export const setupwebsocket = (server: Server) => {
  const wss = new WebSocket.Server({ server });
  
  wss.on("connection", (ws: WebSocket) => {
    ws.on("message", async (message: string) => {
      const parsedMessage = JSON.parse(message);
      
      if (parsedMessage && parsedMessage.eventId) {
        // Subscribe client to event
        clients.push({ ws, eventId: parsedMessage.eventId });
        
        // Send initial order book
        ws.send(JSON.stringify(
          await getOrderBookForEvent(parsedMessage.eventId)
        ));
      }
    });
    
    ws.on("close", () => {
      clients = clients.filter((client) => client.ws !== ws);
    });
  });
  
  return wss;
};

// Broadcast updates to subscribed clients
export const WebsocketServer = {
  broadcast: (eventId: string, data: any) => {
    clients.forEach((client) => {
      if (client.eventId === eventId && 
          client.ws.readyState === WebSocket.OPEN) {
        client.ws.send(JSON.stringify(data));
      }
    });
  }
};

Broadcasting order book updates

After each trade, the server broadcasts to all connected clients:
export async function updateOrderBook() {
  const onGoingEvents = await prisma.event.findMany({
    where: { status: "ONGOING" },
    include: {
      orderBook: {
        include: { yes: true, no: true }
      }
    }
  });
  
  for (const event of onGoingEvents) {
    const orderBook = event.orderBook;
    
    // Broadcast to all clients watching this event
    WebsocketServer.broadcast(event.id, {
      orderBook: {
        yes: orderBook.yes,
        no: orderBook.no,
        topPriceYes: orderBook.topPriceYes,
        topPriceNo: orderBook.topPriceNo
      }
    });
  }
}

Canceling orders

You can cancel unmatched portions of your orders.

Cancellation flow

case CANCEL_ORDER:
  const orderId = message.data.orderId;
  const orderbook = this.orderbooks.find(o => o.market === cancelMarket);
  
  const order = 
    orderbook.asks.find(o => o.orderId === orderId) ||
    orderbook.bids.find(o => o.orderId === orderId);
  
  if (!order) {
    throw new Error("Order not found");
  }
  
  if (order.side === "yes") {
    // Remove from bids
    const price = orderbook.cancelBid(order);
    
    // Unlock funds
    const leftQuantity = (order.quantity - order.filled) * order.price;
    this.balances.get(order.userId).available += leftQuantity;
    this.balances.get(order.userId).locked -= leftQuantity;
  } else {
    // Remove from asks
    const price = orderbook.cancelAsk(order);
    
    // Unlock funds
    const leftQuantity = order.quantity - order.filled;
    this.balances.get(order.userId).available += leftQuantity;
    this.balances.get(order.userId).locked -= leftQuantity;
  }
  
  // Notify client
  RedisManager.getInstance().sendToApi(clientId, {
    type: "ORDER_CANCELLED",
    payload: { orderId, executedQty: 0, remainingQty: 0 }
  });
  break;

Trading strategies

Place both YES and NO orders at different prices to capture the spread:
  • Buy YES at ₹7.00
  • Sell YES at ₹7.50 (via NO order)
  • Profit: ₹0.50 per unit if both fill
Risk: Market moves against you before matching.
Follow price trends:
  • YES price rising? Buy YES to ride momentum
  • NO price falling? Buy YES as market sentiment shifts
Risk: Momentum can reverse quickly.
Bet against market consensus:
  • If YES at ₹9.00 (90% probability), buy NO at ₹1.00
  • Small investment, high reward if upset occurs
Risk: Market is usually right.
Exploit price inefficiencies:
  • If YES + NO < ₹10, buy both
  • Guaranteed profit when event settles
Reality: Rare due to efficient market.

Trading tips

Watch the order book

Large orders at specific prices indicate strong support/resistance levels.

Start small

Test your strategy with small positions before scaling up.

Use limit orders

Set your price instead of accepting market price to control costs.

Monitor events

Stay updated on event developments that might affect outcomes.

Next steps