Skip to Content
BackendTrading Module

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_balance

Position States

pending → open → closed ↘ liquidated pending → canceled
StateDescription
pendingCreated, waiting for entry condition (limit) or market open
openActive position, PnL being calculated
closedManually closed or SL/TP triggered
liquidatedMargin depleted, forced close
canceledUser 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: time

Key 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:

  1. Margin and entry price are finite and positive; leverage ≥ 1 (rejected before any DB/balance change)
  2. Match is open and inside its trading window (end_time)
  3. User is participant in the match
  4. Max positions not exceeded (counted per participant)
  5. Asset exists in market
  6. Stop-loss / target-point are on the correct side of the entry price
  7. Sufficient virtual balance
  8. 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:

  1. Opens pending positions when conditions met
  2. Calculates unrealized PnL
  3. Checks SL/TP triggers
  4. Checks liquidation
  5. 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:

EventNotification
Position openedNotificationEventPositionUpdated
PnL updateNotificationEventPositionUpdated (with pnl data)
Position closedNotificationEventPositionClosed
Position liquidatedNotificationEventPositionLiquidated
Position canceledNotificationEventPositionCanceled

Configuration

Slippage

const slippage = 0.1 // 10%

Free Leverage

const maximumFreeLeverage = 10 // >10x requires inventory item

Trade 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.

InvariantWhereWhat it prevents
margin > 0, finite (not NaN/Inf)CreatePositionA negative margin is AddVBalance(-margin) — i.e. free credit. NaN corrupts balances and ranking.
entry_price > 0, finiteCreatePositionZero/negative entry breaks the PnL math (size / entry).
leverage ≥ 1CreatePositionLeverage 0 yields an infinite/NaN liquidation price.
Market orders fill at the server priceCreatePositionA 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 mirrorCreatePosition, UpdatePositionA 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 participantCreatePositionThe 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_timeCreatePositionTrading 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 AddVBalancepartiallyClosePositionConcurrent 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_pct on Game)
  • Per-challenge leverage limits (max_leverage on Game)
  • Symbol restrictions (allowed_symbols on Game)
  • Trade direction limits (allowed_directions on Game)
  • Drawdown auto-disqualification (daily + total)
  • Trading lock (admin kill switch)
  • Trade audit logging (TradeAuditLog entity)
  • 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
Last updated on