Skip to content

Market Pricing

Purpose

Every station (port) maintains independent buy/sell prices for the commodities it trades. Prices are derived from local supply and demand and are recomputed on three events: a transaction, a production tick, and an admin/ARIA query for a fresh snapshot. The system also emits price-history snapshots (so trends, volatility, and price-alert evaluation can run downstream) and is the structural source of all arbitrage profit in the game.

Inputs

The pricing engine reads: - Station.commodities — JSONB map of {commodity_name: {quantity, capacity, base_price, production_rate, price_variance, current_price}}. - Station.station_class and Station.type — define which commodities the station buys vs sells (see ../FEATURES/economy/trading.md). - Player.faction_reputation (via FactionService) — modifies the player-facing price. - Player.personal_reputation (via PersonalReputationService) — small markup/discount for extreme alignments. - Player.military_rank — adds a flat trading bonus (rank.trading_bonus percent off). - Region.tax_rate — regional tax applied at transaction time. - Existing MarketPrice row (if any) — for trend calculation.

The system fires on: - Transaction (buy/sell) — triggers pricing recompute and history update for the affected commodity. - Production tick — periodic regen of Station.commodities[*].quantity; price recompute follows. - Snapshot readrealtime_market_service.get_market_snapshot(commodity) and ARIA market intelligence trigger a refresh + cache. - Price-alert evaluation — periodic; reads the latest MarketPrice and matches against player-defined alerts.

Process

Pricing formula

1. supply_ratio = clamp(quantity / capacity, 0.0, 1.0)
2. midpoint     = base_price × (1.5 - supply_ratio)
                  # supply 0.0 → 1.5× base (scarcity)
                  # supply 1.0 → 0.5× base (surplus)
3. sell_price   = midpoint × 1.15      # station sells TO player
   buy_price    = midpoint × 0.85      # station buys FROM player
4. clamp both to COMMODITY_PRICE_RANGES[commodity]
5. if buy_price >= sell_price: buy_price = sell_price - 1   # spread floor

Per-commodity price clamps

Commodity min max
ore 15 45
organics 8 25
gourmet_food 30 70
fuel 20 60
equipment 50 120
exotic_technology 150 300
luxury_goods 75 200
colonists 30 80

Player-facing price modifiers (applied at transaction time, on top of station price)

final_price = station_price
  × faction_reputation_multiplier   (0.85 to 1.50 — Exalted to Hated)
  × personal_reputation_multiplier  (0.90 to 1.20 — Legendary to Villain)
  × (1 - rank.trading_bonus / 100)  (Recruit 0% → Fleet Admiral 50%)
  × (1 + region.tax_rate)

Direction of personal_reputation_multiplier flips for sells (a Legendary player gets 10% better sell price too).

Transaction settlement

buy (station → player):
  validate player is docked at this station, in same sector
  recompute final_price as above
  total = final_price * units
  if player.credits < total: 400 Insufficient credits
  if station.commodities[c].quantity < units: 400 Insufficient stock
  player.credits -= total
  player.cargo[c] += units
  station.commodities[c].quantity -= units
  emit MarketTransaction row
  trigger update_market_prices(station_id)
  emit "market_update" event for that station+commodity

sell (player → station):
  symmetric: cargo decremented, station stock incremented, credits credited.

update_market_prices(station_id)

Per commodity on the station: 1. Recompute sell_price, buy_price via the dynamic formula. 2. Read existing MarketPrice row. 3. Roll old → previous: previous_buy_price ← buy_price, previous_sell_price ← sell_price. 4. Compute price_trend = (new_mid - old_mid) / old_mid (signed ratio). 5. Update quantity, supply_level = q/cap, demand_level = 1 - q/cap, volatility = price_variance / 100. 6. Persist station JSONB (current_price = (buy+sell)//2) and MarketPrice row. 7. Set Station.last_market_update = now.

Production tick

for each commodity in station.commodities:
  rate = commodity.production_rate
  if rate <= 0 or quantity >= capacity: continue
  new_qty = min(capacity, quantity + rate)
  commodity.quantity = new_qty

if any change: update_market_prices(station_id)

Tick cadence: hourly per station, scheduled by the realtime market service or an external scheduler.

Price-alert evaluation cycle

Every alert row references (player_id, commodity, station_id?, condition): - condition = {op: "<="|">=", price_kind: "buy"|"sell", threshold: int}. - On each update_market_prices call, after persistence, the alert evaluator: 1. Loads alerts matching (commodity, station_id_or_any). 2. For each alert, compares the new price against threshold. 3. On match, emits a price_alert_triggered realtime event to the player; marks alert last_triggered_at; honors the alert's cooldown so a flapping price does not spam.

Snapshot stream

realtime_market_service aggregates the latest MarketPrice rows per commodity into a MarketSnapshot (best buy / best sell / depth / trend / volatility / AI prediction). Snapshots are cached in Redis (TTL 10 s) and pushed on the market topic of the realtime bus.

Outputs / state changes

Per transaction: - Player.credits adjusted. - Player.current_ship.cargo adjusted (cargo JSONB). - Station.commodities[c].quantity adjusted. - Station.commodities[c].current_price adjusted (display midpoint). - Station.last_market_update set. - MarketPrice row upserted. - MarketTransaction row inserted (full ledger). - Region.treasury (if station is region-owned) credited any tax.

Events emitted: - market_update — sector + market topic; carries new buy/sell, trend, supply/demand levels. - price_alert_triggered — unicast. - transaction_completed — unicast to the player's session.

Invariants

  1. sell_price > buy_price for every commodity at every station (the 15% spread plus the floor rule).
  2. Station stock is never negative: 0 ≤ quantity ≤ capacity.
  3. Commodity prices clamp inside COMMODITY_PRICE_RANGES[*] even at extremes.
  4. A transaction either fully settles or fully rolls back — credits and cargo move together.
  5. Player.credits ≥ 0 at all times.
  6. Station.commodities JSONB is mutated through flag_modified so SQLAlchemy persists the diff.
  7. MarketPrice.previous_* always reflects the immediately prior tick (no skipping).
  8. The pricing recompute runs after stock mutation in transaction settlement, so the next reader sees the new equilibrium.

Failure modes

Mode Target handling
Race: two players buy the last unit Pre-flight stock check inside a row-locked transaction; second transaction returns 400 Insufficient stock.
Station class not selling/buying this commodity Trade rejected at TradingService.can_player_trade; no price compute.
Player not docked or not in same sector Rejected at can_player_trade; no state changes.
Negative production_rate config Tick skips the commodity (treated as no production).
capacity = 0 / missing Sanitized to 1 to avoid div-by-zero; price clamps still apply.
Station JSONB drift (manual admin edit) update_market_prices re-derives prices from quantity / capacity; MarketPrice history gets a single anomalous tick — flagged via volatility.
Alert flap (price oscillating around threshold) Alert holds a cooldown_seconds field; re-fires only after cooldown expires.
Snapshot cache stale during outage Redis miss falls through to live DB read; latency spikes but correctness preserved.
Region tax misconfigured Default tax = 0; mis-set rate clamped to [0, 0.5].

Source map

Concern Path (target)
Pricing formula + spec clamps services/gameserver/src/services/trading_service.py:calculate_dynamic_price
Market price write + history services/gameserver/src/services/trading_service.py:update_market_prices
Production tick services/gameserver/src/services/trading_service.py:tick_production
Trade eligibility services/gameserver/src/services/trading_service.py:can_player_trade
Realtime market snapshot services/gameserver/src/services/realtime_market_service.py
Market prediction services/gameserver/src/services/market_prediction_engine.py
Faction modifier services/gameserver/src/services/faction_service.py
Personal-reputation modifier services/gameserver/src/services/personal_reputation_service.py
Trade API services/gameserver/src/api/routes/trading.py
Models services/gameserver/src/models/station.py, market_transaction.py, resource.py
Price-alert evaluator (target) services/gameserver/src/services/price_alert_service.py