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 read — realtime_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¶
sell_price > buy_pricefor every commodity at every station (the 15% spread plus the floor rule).- Station stock is never negative:
0 ≤ quantity ≤ capacity. - Commodity prices clamp inside
COMMODITY_PRICE_RANGES[*]even at extremes. - A transaction either fully settles or fully rolls back — credits and cargo move together.
Player.credits ≥ 0at all times.Station.commoditiesJSONB is mutated throughflag_modifiedso SQLAlchemy persists the diff.MarketPrice.previous_*always reflects the immediately prior tick (no skipping).- 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 |
Related¶
- DATA_MODELS:
../DATA_MODELS/economy.md. - FEATURES:
../FEATURES/economy/trading.md. - SYSTEMS: realtime-bus.md, aria-dialogue.md (market intelligence).
- REST API: trading routes auto-published at
<api-host>/docs. - OPERATIONS:
../OPERATIONS/aria.md— ARIA market intelligence consumer.