Trading Module
The trading module handles all position-related operations in BattlesBit. Since this is a trading game (not real trading), positions are simulated using real-time market data.
Architecture
User Request
↓
GraphQL API (web service)
↓
GameUseCase.CreatePosition()
↓
Validation (balance, max positions)
↓
Position Created (status: pending)
↓
Published to NATS JetStream
↓
Arena Worker picks up
↓
CalculatePosition() loop
↓
Real-time price updates via Binance WebSocket
↓
SL/TP/Liquidation checks
↓
Position closed → Update participant v_balancePosition States
pending → open → closed
↘ liquidated
pending → canceled| State | Description |
|---|---|
pending | Created, waiting for entry condition (limit) or market open |
open | Active position, PnL being calculated |
closed | Manually closed or SL/TP triggered |
liquidated | Margin depleted, forced close |
canceled | User canceled before fill |
Position Schema
// From: internal/ent/schema/game_match_position.go
Fields:
- id: UUID
- game_match_id: UUID
- game_match_participant_id: UUID
- base_token: string (e.g., "BTCUSDT")
- quote_token: string (default: "USDT")
- entry_price: float64
- close_price: float64 (nullable)
- margin: float64
- leverage: uint8
- side: enum (buy, sell)
- type: enum (market, limit)
- status: enum (open, pending, canceled, liquidated, closed)
- stop_loss: float64 (nullable)
- target_point: float64 (nullable)
- size: float64 (margin * leverage)
- liquidation_price: float64
- profit: float64
- opened_at: time
- closed_at: timeKey Operations
Create Position
File: game_usecase.go:310
func (u *gameUseCase) CreatePosition(
ctx context.Context,
userID uuid.UUID,
input *ent.CreateGameMatchPositionInput,
) (*ent.GameMatchPosition, error)Validations:
- Margin and entry price are finite and positive; leverage ≥ 1 (rejected before any DB/balance change)
- Match is
openand inside its trading window (end_time) - User is participant in the match
- Max positions not exceeded (counted per participant)
- Asset exists in market
- Stop-loss / target-point are on the correct side of the entry price
- Sufficient virtual balance
- Leverage item exists (if >10x)
For market orders the entry price is taken from the live server price, not from the client input. Only limit orders honor the client-supplied price.
Liquidation Price Calculation:
// Buy side
liquidationPrice = entryPrice * (1 - 1/leverage)
// Sell side
liquidationPrice = entryPrice * (1 + 1/leverage)Calculate Position (Arena Worker)
File: game_usecase_calc.go:32
The arena worker subscribes to market updates and:
- Opens pending positions when conditions met
- Calculates unrealized PnL
- Checks SL/TP triggers
- Checks liquidation
- Sends real-time notifications
Close Position
File: game_usecase.go:458
Supports partial close via percentage:
func (u *gameUseCase) ClosePosition(
ctx context.Context,
userID uuid.UUID,
positionID uuid.UUID,
percent float64, // 1-100
) (*ent.GameMatchPosition, error)PnL Calculation
File: game_usecase_calc.go:550
func calculatePositionProfit(pos *ent.GameMatchPosition, marketPrice float64) float64 {
baseAmount := pos.Size / pos.EntryPrice
if isBuySide {
profit = (marketPrice - pos.EntryPrice) * baseAmount
} else if isSellSide {
profit = (pos.EntryPrice - marketPrice) * baseAmount
}
return profit
}Market Data Integration
File: internal/adapters/clients/market/binance_client.go
Get Assets
REST API call to fetch current prices and exchange info.
Subscribe Assets
WebSocket connection for real-time price updates.
type AssetUpdate struct {
ID string // Symbol
Symbol string
PriceUsd float64
}Notifications
Position updates trigger real-time notifications:
| Event | Notification |
|---|---|
| Position opened | NotificationEventPositionUpdated |
| PnL update | NotificationEventPositionUpdated (with pnl data) |
| Position closed | NotificationEventPositionClosed |
| Position liquidated | NotificationEventPositionLiquidated |
| Position canceled | NotificationEventPositionCanceled |
Configuration
Slippage
const slippage = 0.1 // 10%Free Leverage
const maximumFreeLeverage = 10 // >10x requires inventory itemTrade Integrity / Anti-Cheat Invariants
The engine treats every numeric field on CreateGameMatchPositionInput as
untrusted client input. The following invariants are enforced server-side; each
one closes a way a player could otherwise fabricate profit or a balance.
| Invariant | Where | What it prevents |
|---|---|---|
margin > 0, finite (not NaN/Inf) | CreatePosition | A negative margin is AddVBalance(-margin) — i.e. free credit. NaN corrupts balances and ranking. |
entry_price > 0, finite | CreatePosition | Zero/negative entry breaks the PnL math (size / entry). |
leverage ≥ 1 | CreatePosition | Leverage 0 yields an infinite/NaN liquidation price. |
| Market orders fill at the server price | CreatePosition | A client entry price up to a slippage-width from market would bank a risk-free gap on close. |
| SL below entry (buy) / above entry (sell); TP mirror | CreatePosition, UpdatePosition | A wrong-side stop-loss “stops out” at a price better than market, booking arbitrary profit, because the close routine fills at the SL/TP price. |
| Max positions counted by participant | CreatePosition | The previous filter compared participant.id to userID and always matched zero rows, so the limit never applied. |
Trading only while match is open and before end_time | CreatePosition | Trading in a waiting room or after the match window (before the close cron runs) with extra hindsight. |
Full close stamps closed_at under a closed_at IS NULL guard; balance credited with atomic AddVBalance | partiallyClosePosition | Concurrent 100% closes each crediting profit (double-spend), and lost-update races on v_balance. |
Implemented Since Initial Docs
- Risk per trade limits (
max_loss_per_trade_pcton Game) - Per-challenge leverage limits (
max_leverageon Game) - Symbol restrictions (
allowed_symbolson Game) - Trade direction limits (
allowed_directionson Game) - Drawdown auto-disqualification (daily + total)
- Trading lock (admin kill switch)
- Trade audit logging (
TradeAuditLogentity) - Position update (SL/TP modification)
- Profit correctly applied to v_balance on partial close (bugfix)
- Trust-boundary validation of margin/entry/leverage (anti-cheat)
- Server-sourced entry price for market orders (anti-cheat)
- SL/TP side validation on create and update (anti-cheat)
- Max-positions limit correctly enforced per participant (bugfix)
- Double-close guard + atomic balance credit (bugfix)
Remaining TODOs
- Stop orders
- Stop-limit orders
- Reduce-only orders
- Max order size limits
- Cooldown between trades
- Equity calculation
- Free margin calculation