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
Bids (YES orders)
Asks (NO orders)
Market depth
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:
Price (highest first)
Time (earliest first at same price)
NO orders represent buyers willing to purchase NO positions: interface Order {
price : number ;
quantity : number ;
filled : number ;
orderId : string ;
side : "no" ;
userId : string ;
}
Asks are sorted by:
Price (lowest first)
Time (earliest first at same price)
Market depth shows aggregated orders at each price level: {
bids : [
[ "7.50" , "120" ], // 120 units at ₹7.50
[ "7.00" , "85" ], // 85 units at ₹7.00
[ "6.50" , "200" ] // 200 units at ₹6.50
],
asks : [
[ "2.50" , "150" ], // 150 units at ₹2.50
[ "3.00" , "100" ], // 100 units at ₹3.00
[ "3.50" , "75" ] // 75 units at ₹3.50
]
}
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:
Choose event
Navigate to an event’s trading page at /events/[id].
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 >
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.
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"
/>
Review calculation
The panel displays:
You put : price × quantity (your cost)
You get : Potential payout if you win (₹10 × quantity)
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
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 ;
}
}
Create order object
const order : Order = {
price: Number ( price ),
quantity: Number ( quantity ),
orderId: uuidv4 (),
filled: 0 ,
side ,
userId
};
Attempt matching
The orderbook tries to match the order: const { fills , executedQty } = orderbook . addOrder ( order );
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 ;
});
}
}
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 ()
}
});
});
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