Skip to main content

proof_engine/economy/
market.rs

1//! Dynamic market simulation.
2//!
3//! Provides commodity price formation through supply and demand, price
4//! elasticity, an order book, three auction types, trade-history ring buffer,
5//! market-manipulation detection, and arbitrage scanning.
6
7use std::collections::{HashMap, VecDeque};
8
9// ---------------------------------------------------------------------------
10// Primitive IDs
11// ---------------------------------------------------------------------------
12
13/// Opaque handle for a registered commodity.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub struct CommodityId(pub u32);
16
17/// Opaque handle for an open order in the order book.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub struct OrderId(pub u64);
20
21/// Opaque handle for a running auction.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub struct AuctionId(pub u32);
24
25/// Opaque handle for a market participant (player, faction, AI trader).
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct ParticipantId(pub u32);
28
29// ---------------------------------------------------------------------------
30// Commodity
31// ---------------------------------------------------------------------------
32
33/// Intrinsic properties of a tradeable commodity.
34#[derive(Debug, Clone)]
35pub struct Commodity {
36    pub id: CommodityId,
37    pub name: String,
38    /// Base price under neutral supply/demand conditions.
39    pub base_price: f64,
40    /// How many units are available per tick at baseline production.
41    pub natural_supply: f64,
42    /// Price elasticity coefficient: higher = price reacts more strongly to
43    /// supply/demand imbalance (0.1 = inelastic, 1.0+ = very elastic).
44    pub elasticity: f64,
45    /// Current spot price.
46    pub spot_price: f64,
47    /// Accumulated supply this tick (reset each tick after matching).
48    pub supply: f64,
49    /// Accumulated demand this tick.
50    pub demand: f64,
51    /// Running 7-tick exponential moving average of price.
52    pub ema_price: f64,
53    /// Volatility (standard deviation of recent log returns).
54    pub volatility: f64,
55    /// Recent log returns used to compute volatility.
56    recent_log_returns: VecDeque<f64>,
57    /// Whether this commodity is currently embargoed (no trades allowed).
58    pub embargoed: bool,
59}
60
61impl Commodity {
62    fn new(id: CommodityId, name: &str, base_price: f64, natural_supply: f64, elasticity: f64) -> Self {
63        Self {
64            id,
65            name: name.to_string(),
66            base_price,
67            natural_supply,
68            elasticity,
69            spot_price: base_price,
70            supply: natural_supply,
71            demand: natural_supply,
72            ema_price: base_price,
73            volatility: 0.0,
74            recent_log_returns: VecDeque::with_capacity(20),
75            embargoed: false,
76        }
77    }
78
79    /// Update the EMA and volatility after a price change.
80    fn record_price(&mut self, new_price: f64) {
81        let old = self.spot_price.max(1e-9);
82        let log_ret = (new_price / old).ln();
83        self.recent_log_returns.push_back(log_ret);
84        if self.recent_log_returns.len() > 20 {
85            self.recent_log_returns.pop_front();
86        }
87        // EMA alpha = 2 / (7 + 1) ≈ 0.25
88        let alpha = 0.25;
89        self.ema_price = alpha * new_price + (1.0 - alpha) * self.ema_price;
90        // volatility = std-dev of log returns
91        let n = self.recent_log_returns.len() as f64;
92        if n >= 2.0 {
93            let mean = self.recent_log_returns.iter().sum::<f64>() / n;
94            let var = self.recent_log_returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / (n - 1.0);
95            self.volatility = var.sqrt();
96        }
97        self.spot_price = new_price;
98    }
99
100    /// Compute a new price given the current supply/demand imbalance.
101    fn recompute_price(&mut self) {
102        let effective_supply = self.supply.max(1.0);
103        let effective_demand = self.demand.max(1.0);
104        // Ratio: > 1 means more demand than supply -> price up
105        let ratio = effective_demand / effective_supply;
106        // log-ratio scaled by elasticity
107        let pressure = ratio.ln() * self.elasticity;
108        let raw = self.base_price * pressure.exp();
109        // Clamp to [base * 0.01, base * 100]
110        let clamped = raw.clamp(self.base_price * 0.01, self.base_price * 100.0);
111        self.record_price(clamped);
112    }
113}
114
115// ---------------------------------------------------------------------------
116// Order Book
117// ---------------------------------------------------------------------------
118
119/// Which side of the market this order is on.
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum OrderSide {
122    Buy,
123    Sell,
124}
125
126/// Order status.
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub enum OrderStatus {
129    Open,
130    PartiallyFilled,
131    Filled,
132    Cancelled,
133    Expired,
134}
135
136/// A single limit order in the order book.
137#[derive(Debug, Clone)]
138pub struct Order {
139    pub id: OrderId,
140    pub participant: ParticipantId,
141    pub commodity: CommodityId,
142    pub side: OrderSide,
143    /// Limit price. Buy orders fill at <= limit; sell orders fill at >= limit.
144    pub limit_price: f64,
145    pub quantity: f64,
146    pub filled: f64,
147    pub status: OrderStatus,
148    /// Tick on which this order was placed.
149    pub placed_tick: u64,
150    /// Expire if not filled within this many ticks (0 = good-till-cancelled).
151    pub ttl: u64,
152}
153
154impl Order {
155    pub fn remaining(&self) -> f64 {
156        self.quantity - self.filled
157    }
158}
159
160// ---------------------------------------------------------------------------
161// Trade History Ring Buffer
162// ---------------------------------------------------------------------------
163
164/// A completed trade record stored in the ring buffer.
165#[derive(Debug, Clone)]
166pub struct TradeRecord {
167    pub tick: u64,
168    pub commodity: CommodityId,
169    pub price: f64,
170    pub quantity: f64,
171    pub buyer: ParticipantId,
172    pub seller: ParticipantId,
173}
174
175/// Fixed-capacity ring buffer for trade history.
176pub struct TradeHistory {
177    buf: VecDeque<TradeRecord>,
178    capacity: usize,
179}
180
181impl TradeHistory {
182    pub fn new(capacity: usize) -> Self {
183        Self { buf: VecDeque::with_capacity(capacity), capacity }
184    }
185
186    pub fn push(&mut self, record: TradeRecord) {
187        if self.buf.len() == self.capacity {
188            self.buf.pop_front();
189        }
190        self.buf.push_back(record);
191    }
192
193    pub fn iter(&self) -> impl Iterator<Item = &TradeRecord> {
194        self.buf.iter()
195    }
196
197    /// Last N trades for a specific commodity.
198    pub fn recent_for(&self, commodity: CommodityId, n: usize) -> Vec<&TradeRecord> {
199        self.buf.iter().rev().filter(|r| r.commodity == commodity).take(n).collect()
200    }
201
202    /// Volume-weighted average price over the last `n` trades for a commodity.
203    pub fn vwap(&self, commodity: CommodityId, n: usize) -> Option<f64> {
204        let records: Vec<_> = self.recent_for(commodity, n);
205        if records.is_empty() { return None; }
206        let total_val: f64 = records.iter().map(|r| r.price * r.quantity).sum();
207        let total_qty: f64 = records.iter().map(|r| r.quantity).sum();
208        if total_qty < 1e-9 { return None; }
209        Some(total_val / total_qty)
210    }
211}
212
213// ---------------------------------------------------------------------------
214// Price chart point
215// ---------------------------------------------------------------------------
216
217/// A single data point in a commodity's price history.
218#[derive(Debug, Clone)]
219pub struct PricePoint {
220    pub tick: u64,
221    pub open: f64,
222    pub high: f64,
223    pub low: f64,
224    pub close: f64,
225    pub volume: f64,
226}
227
228// ---------------------------------------------------------------------------
229// Auction System
230// ---------------------------------------------------------------------------
231
232/// The three supported auction formats.
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234pub enum AuctionType {
235    /// Ascending open-cry auction; highest bidder wins at their bid price.
236    English,
237    /// Price starts high and drops until a bidder accepts.
238    Dutch,
239    /// All bids sealed; highest bidder wins, pays second-highest price.
240    SealedBid,
241}
242
243/// Lifecycle state of an auction.
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub enum AuctionState {
246    Open,
247    Closing,
248    Settled,
249    Cancelled,
250}
251
252/// A single bid in an auction.
253#[derive(Debug, Clone)]
254pub struct AuctionBid {
255    pub bidder: ParticipantId,
256    pub amount: f64,
257    pub tick: u64,
258}
259
260/// An active or completed auction.
261#[derive(Debug, Clone)]
262pub struct Auction {
263    pub id: AuctionId,
264    pub auction_type: AuctionType,
265    pub commodity: CommodityId,
266    pub quantity: f64,
267    pub seller: ParticipantId,
268    /// Minimum acceptable price (reserve price).
269    pub reserve_price: f64,
270    /// Dutch: starting price.
271    pub start_price: f64,
272    /// Dutch: price decrement per tick.
273    pub dutch_decrement: f64,
274    /// Current Dutch clock price.
275    pub current_dutch_price: f64,
276    pub state: AuctionState,
277    pub bids: Vec<AuctionBid>,
278    pub opened_tick: u64,
279    /// Auction closes after this many ticks with no activity (English), or
280    /// when Dutch clock hits reserve, or at a fixed end tick.
281    pub close_tick: u64,
282    pub winner: Option<ParticipantId>,
283    pub winning_price: Option<f64>,
284}
285
286impl Auction {
287    fn highest_bid(&self) -> Option<&AuctionBid> {
288        self.bids.iter().max_by(|a, b| a.amount.partial_cmp(&b.amount).unwrap())
289    }
290
291    fn second_highest_bid(&self) -> Option<&AuctionBid> {
292        if self.bids.len() < 2 { return None; }
293        let mut sorted: Vec<f64> = self.bids.iter().map(|b| b.amount).collect();
294        sorted.sort_by(|a, b| b.partial_cmp(a).unwrap());
295        let second = sorted[1];
296        self.bids.iter().find(|b| (b.amount - second).abs() < 1e-9)
297    }
298}
299
300// ---------------------------------------------------------------------------
301// Market Manipulation Detection
302// ---------------------------------------------------------------------------
303
304/// Evidence of potential market manipulation by a participant.
305#[derive(Debug, Clone)]
306pub struct ManipulationAlert {
307    pub participant: ParticipantId,
308    pub commodity: CommodityId,
309    pub alert_type: ManipulationKind,
310    pub confidence: f64,
311    pub detected_tick: u64,
312    pub details: String,
313}
314
315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316pub enum ManipulationKind {
317    /// Placing large orders and cancelling before fill (spoofing).
318    Spoofing,
319    /// Washing trades: participant appears on both sides.
320    WashTrading,
321    /// Ramping price artificially with a series of small buys.
322    PriceRamping,
323    /// Cornering the market: participant holds dominant supply.
324    Cornering,
325}
326
327// ---------------------------------------------------------------------------
328// Arbitrage
329// ---------------------------------------------------------------------------
330
331/// An identified arbitrage opportunity between two commodities or two
332/// markets (for multi-market expansion).
333#[derive(Debug, Clone)]
334pub struct ArbitrageOpportunity {
335    pub buy_commodity: CommodityId,
336    pub sell_commodity: CommodityId,
337    /// Conversion ratio: 1 unit buy_commodity converts to `ratio` units sell_commodity.
338    pub conversion_ratio: f64,
339    pub profit_per_unit: f64,
340    pub confidence: f64,
341    pub detected_tick: u64,
342}
343
344// ---------------------------------------------------------------------------
345// Participant Activity Tracker (used for manipulation detection)
346// ---------------------------------------------------------------------------
347
348#[derive(Debug, Default, Clone)]
349struct ParticipantActivity {
350    orders_placed: u32,
351    orders_cancelled: u32,
352    buy_volume: f64,
353    sell_volume: f64,
354    /// Ticks on which large cancel events occurred.
355    cancel_spikes: VecDeque<u64>,
356    /// Prices at which wash patterns were suspected.
357    wash_prices: VecDeque<f64>,
358}
359
360// ---------------------------------------------------------------------------
361// The Market
362// ---------------------------------------------------------------------------
363
364/// Central market simulation. Owns all commodities, the order book, auctions,
365/// trade history, and analytics.
366pub struct Market {
367    next_commodity_id: u32,
368    next_order_id: u64,
369    next_auction_id: u32,
370    pub current_tick: u64,
371
372    pub commodities: HashMap<CommodityId, Commodity>,
373    /// Named index: name -> CommodityId
374    commodity_names: HashMap<String, CommodityId>,
375
376    /// Order book: per commodity, sorted buy (desc) and sell (asc) lists.
377    buy_orders: HashMap<CommodityId, Vec<Order>>,
378    sell_orders: HashMap<CommodityId, Vec<Order>>,
379
380    pub trade_history: TradeHistory,
381    /// OHLCV candles per commodity, most recent last.
382    pub price_history: HashMap<CommodityId, VecDeque<PricePoint>>,
383
384    pub auctions: HashMap<AuctionId, Auction>,
385    pub settled_auctions: Vec<Auction>,
386
387    pub manipulation_alerts: Vec<ManipulationAlert>,
388    pub arbitrage_opportunities: Vec<ArbitrageOpportunity>,
389
390    /// Per-participant activity for manipulation detection.
391    participant_activity: HashMap<ParticipantId, HashMap<CommodityId, ParticipantActivity>>,
392
393    /// Commodity exchange rate relationships for arbitrage (a -> b conversion factor).
394    conversion_graph: HashMap<(CommodityId, CommodityId), f64>,
395
396    /// Maximum price history candles kept per commodity.
397    max_price_history: usize,
398}
399
400impl Market {
401    /// Create a new empty market.
402    pub fn new() -> Self {
403        Self {
404            next_commodity_id: 1,
405            next_order_id: 1,
406            next_auction_id: 1,
407            current_tick: 0,
408            commodities: HashMap::new(),
409            commodity_names: HashMap::new(),
410            buy_orders: HashMap::new(),
411            sell_orders: HashMap::new(),
412            trade_history: TradeHistory::new(4096),
413            price_history: HashMap::new(),
414            auctions: HashMap::new(),
415            settled_auctions: Vec::new(),
416            manipulation_alerts: Vec::new(),
417            arbitrage_opportunities: Vec::new(),
418            participant_activity: HashMap::new(),
419            conversion_graph: HashMap::new(),
420            max_price_history: 512,
421        }
422    }
423
424    // -----------------------------------------------------------------------
425    // Commodity Registration
426    // -----------------------------------------------------------------------
427
428    /// Register a new tradeable commodity. Returns its ID.
429    pub fn register_commodity(
430        &mut self,
431        name: &str,
432        base_price: f64,
433        natural_supply: f64,
434        elasticity: f64,
435    ) -> CommodityId {
436        let id = CommodityId(self.next_commodity_id);
437        self.next_commodity_id += 1;
438        let c = Commodity::new(id, name, base_price, natural_supply, elasticity);
439        self.commodity_names.insert(name.to_string(), id);
440        self.commodities.insert(id, c);
441        self.buy_orders.insert(id, Vec::new());
442        self.sell_orders.insert(id, Vec::new());
443        self.price_history.insert(id, VecDeque::with_capacity(self.max_price_history));
444        id
445    }
446
447    /// Look up a commodity by name.
448    pub fn commodity_by_name(&self, name: &str) -> Option<CommodityId> {
449        self.commodity_names.get(name).copied()
450    }
451
452    /// Register a commodity conversion relationship for arbitrage scanning.
453    pub fn register_conversion(&mut self, from: CommodityId, to: CommodityId, ratio: f64) {
454        self.conversion_graph.insert((from, to), ratio);
455    }
456
457    /// Embargo or lift embargo on a commodity.
458    pub fn set_embargo(&mut self, id: CommodityId, embargoed: bool) {
459        if let Some(c) = self.commodities.get_mut(&id) {
460            c.embargoed = embargoed;
461        }
462    }
463
464    // -----------------------------------------------------------------------
465    // Supply / Demand Injection
466    // -----------------------------------------------------------------------
467
468    /// Inject external supply (from production, imports, etc.).
469    pub fn inject_supply(&mut self, commodity: CommodityId, amount: f64) {
470        if let Some(c) = self.commodities.get_mut(&commodity) {
471            c.supply += amount;
472        }
473    }
474
475    /// Inject external demand (from consumption, export contracts, etc.).
476    pub fn inject_demand(&mut self, commodity: CommodityId, amount: f64) {
477        if let Some(c) = self.commodities.get_mut(&commodity) {
478            c.demand += amount;
479        }
480    }
481
482    // -----------------------------------------------------------------------
483    // Order Book
484    // -----------------------------------------------------------------------
485
486    /// Place a limit order. Returns the order ID.
487    pub fn place_order(
488        &mut self,
489        participant: ParticipantId,
490        commodity: CommodityId,
491        side: OrderSide,
492        limit_price: f64,
493        quantity: f64,
494        ttl: u64,
495    ) -> Option<OrderId> {
496        if self.commodities.get(&commodity)?.embargoed { return None; }
497        let id = OrderId(self.next_order_id);
498        self.next_order_id += 1;
499        let order = Order {
500            id,
501            participant,
502            commodity,
503            side,
504            limit_price,
505            quantity,
506            filled: 0.0,
507            status: OrderStatus::Open,
508            placed_tick: self.current_tick,
509            ttl,
510        };
511        // Track activity
512        let act = self.participant_activity
513            .entry(participant)
514            .or_default()
515            .entry(commodity)
516            .or_default();
517        act.orders_placed += 1;
518        match side {
519            OrderSide::Buy => {
520                let book = self.buy_orders.entry(commodity).or_default();
521                book.push(order);
522                // Keep sorted descending by limit price
523                book.sort_by(|a, b| b.limit_price.partial_cmp(&a.limit_price).unwrap());
524            }
525            OrderSide::Sell => {
526                let book = self.sell_orders.entry(commodity).or_default();
527                book.push(order);
528                // Keep sorted ascending by limit price
529                book.sort_by(|a, b| a.limit_price.partial_cmp(&b.limit_price).unwrap());
530            }
531        }
532        Some(id)
533    }
534
535    /// Cancel an open order. Returns true if found and cancelled.
536    pub fn cancel_order(&mut self, order_id: OrderId) -> bool {
537        for orders in self.buy_orders.values_mut().chain(self.sell_orders.values_mut()) {
538            if let Some(o) = orders.iter_mut().find(|o| o.id == order_id) {
539                if o.status == OrderStatus::Open || o.status == OrderStatus::PartiallyFilled {
540                    // Track cancellation for manipulation detection
541                    let commodity = o.commodity;
542                    let participant = o.participant;
543                    let act = self.participant_activity
544                        .entry(participant)
545                        .or_default()
546                        .entry(commodity)
547                        .or_default();
548                    act.orders_cancelled += 1;
549                    act.cancel_spikes.push_back(self.current_tick);
550                    if act.cancel_spikes.len() > 20 { act.cancel_spikes.pop_front(); }
551                    o.status = OrderStatus::Cancelled;
552                    return true;
553                }
554            }
555        }
556        false
557    }
558
559    /// Get all open orders for a participant.
560    pub fn orders_for_participant(&self, participant: ParticipantId) -> Vec<&Order> {
561        let mut result = Vec::new();
562        for orders in self.buy_orders.values().chain(self.sell_orders.values()) {
563            for o in orders {
564                if o.participant == participant
565                    && (o.status == OrderStatus::Open || o.status == OrderStatus::PartiallyFilled)
566                {
567                    result.push(o);
568                }
569            }
570        }
571        result
572    }
573
574    /// Best bid (highest buy limit) for a commodity.
575    pub fn best_bid(&self, commodity: CommodityId) -> Option<f64> {
576        self.buy_orders.get(&commodity)?
577            .iter()
578            .filter(|o| o.status == OrderStatus::Open || o.status == OrderStatus::PartiallyFilled)
579            .map(|o| o.limit_price)
580            .reduce(f64::max)
581    }
582
583    /// Best ask (lowest sell limit) for a commodity.
584    pub fn best_ask(&self, commodity: CommodityId) -> Option<f64> {
585        self.sell_orders.get(&commodity)?
586            .iter()
587            .filter(|o| o.status == OrderStatus::Open || o.status == OrderStatus::PartiallyFilled)
588            .map(|o| o.limit_price)
589            .reduce(f64::min)
590    }
591
592    /// Bid-ask spread.
593    pub fn spread(&self, commodity: CommodityId) -> Option<f64> {
594        Some(self.best_ask(commodity)? - self.best_bid(commodity)?)
595    }
596
597    // -----------------------------------------------------------------------
598    // Auction System
599    // -----------------------------------------------------------------------
600
601    /// Open a new auction. Returns its ID.
602    pub fn open_auction(
603        &mut self,
604        auction_type: AuctionType,
605        commodity: CommodityId,
606        quantity: f64,
607        seller: ParticipantId,
608        reserve_price: f64,
609        start_price: f64,
610        dutch_decrement: f64,
611        duration_ticks: u64,
612    ) -> AuctionId {
613        let id = AuctionId(self.next_auction_id);
614        self.next_auction_id += 1;
615        let auction = Auction {
616            id,
617            auction_type,
618            commodity,
619            quantity,
620            seller,
621            reserve_price,
622            start_price,
623            dutch_decrement,
624            current_dutch_price: start_price,
625            state: AuctionState::Open,
626            bids: Vec::new(),
627            opened_tick: self.current_tick,
628            close_tick: self.current_tick + duration_ticks,
629            winner: None,
630            winning_price: None,
631        };
632        self.auctions.insert(id, auction);
633        id
634    }
635
636    /// Place a bid in an auction.
637    pub fn bid_auction(
638        &mut self,
639        auction_id: AuctionId,
640        bidder: ParticipantId,
641        amount: f64,
642    ) -> bool {
643        let tick = self.current_tick;
644        let auction = match self.auctions.get_mut(&auction_id) {
645            Some(a) if a.state == AuctionState::Open => a,
646            _ => return false,
647        };
648        match auction.auction_type {
649            AuctionType::English => {
650                let current_high = auction.bids.iter().map(|b| b.amount).fold(0.0_f64, f64::max);
651                if amount <= current_high.max(auction.reserve_price) { return false; }
652                auction.bids.push(AuctionBid { bidder, amount, tick });
653                true
654            }
655            AuctionType::Dutch => {
656                // Accept the current clock price
657                if amount >= auction.current_dutch_price {
658                    auction.bids.push(AuctionBid { bidder, amount: auction.current_dutch_price, tick });
659                    auction.state = AuctionState::Closing;
660                    true
661                } else {
662                    false
663                }
664            }
665            AuctionType::SealedBid => {
666                // One bid per participant; blind
667                if auction.bids.iter().any(|b| b.bidder == bidder) { return false; }
668                auction.bids.push(AuctionBid { bidder, amount, tick });
669                true
670            }
671        }
672    }
673
674    /// Settle an auction that has closed. Returns winning price if any.
675    fn settle_auction(&mut self, auction_id: AuctionId) -> Option<f64> {
676        let tick = self.current_tick;
677        let auction = self.auctions.get_mut(&auction_id)?;
678        if auction.state == AuctionState::Settled || auction.state == AuctionState::Cancelled {
679            return None;
680        }
681        let (winner, winning_price) = match auction.auction_type {
682            AuctionType::English | AuctionType::Dutch => {
683                let best = auction.highest_bid()?;
684                if best.amount < auction.reserve_price { return None; }
685                (best.bidder, best.amount)
686            }
687            AuctionType::SealedBid => {
688                // Vickrey: highest bid wins, pays second-highest price
689                let best = auction.highest_bid()?;
690                if best.amount < auction.reserve_price { return None; }
691                let winner = best.bidder;
692                let price = auction.second_highest_bid()
693                    .map(|b| b.amount)
694                    .unwrap_or(best.amount);
695                (winner, price)
696            }
697        };
698        auction.winner = Some(winner);
699        auction.winning_price = Some(winning_price);
700        auction.state = AuctionState::Settled;
701
702        // Record in trade history
703        let commodity = auction.commodity;
704        let quantity = auction.quantity;
705        let seller = auction.seller;
706        self.trade_history.push(TradeRecord {
707            tick,
708            commodity,
709            price: winning_price,
710            quantity,
711            buyer: winner,
712            seller,
713        });
714        // Influence supply/demand
715        if let Some(c) = self.commodities.get_mut(&commodity) {
716            c.demand += quantity;
717        }
718        Some(winning_price)
719    }
720
721    // -----------------------------------------------------------------------
722    // Order Matching Engine
723    // -----------------------------------------------------------------------
724
725    fn match_orders_for(&mut self, commodity: CommodityId) {
726        let tick = self.current_tick;
727        let mut new_trades: Vec<TradeRecord> = Vec::new();
728        loop {
729            let best_buy = {
730                let buys = self.buy_orders.get(&commodity);
731                buys.and_then(|b| b.iter().find(|o| {
732                    o.status == OrderStatus::Open || o.status == OrderStatus::PartiallyFilled
733                }).map(|o| (o.id, o.limit_price, o.participant, o.remaining())))
734            };
735            let best_sell = {
736                let sells = self.sell_orders.get(&commodity);
737                sells.and_then(|s| s.iter().find(|o| {
738                    o.status == OrderStatus::Open || o.status == OrderStatus::PartiallyFilled
739                }).map(|o| (o.id, o.limit_price, o.participant, o.remaining())))
740            };
741            match (best_buy, best_sell) {
742                (Some((bid_id, bid_price, buyer, bid_rem)),
743                 Some((ask_id, ask_price, seller, ask_rem))) => {
744                    if bid_price < ask_price { break; }
745                    // Price: midpoint between bid and ask
746                    let exec_price = (bid_price + ask_price) * 0.5;
747                    let fill_qty = bid_rem.min(ask_rem);
748                    new_trades.push(TradeRecord {
749                        tick,
750                        commodity,
751                        price: exec_price,
752                        quantity: fill_qty,
753                        buyer,
754                        seller,
755                    });
756                    // Update buy order
757                    if let Some(orders) = self.buy_orders.get_mut(&commodity) {
758                        if let Some(o) = orders.iter_mut().find(|o| o.id == bid_id) {
759                            o.filled += fill_qty;
760                            o.status = if o.remaining() < 1e-9 { OrderStatus::Filled } else { OrderStatus::PartiallyFilled };
761                        }
762                    }
763                    // Update sell order
764                    if let Some(orders) = self.sell_orders.get_mut(&commodity) {
765                        if let Some(o) = orders.iter_mut().find(|o| o.id == ask_id) {
766                            o.filled += fill_qty;
767                            o.status = if o.remaining() < 1e-9 { OrderStatus::Filled } else { OrderStatus::PartiallyFilled };
768                        }
769                    }
770                    // Update supply/demand
771                    if let Some(c) = self.commodities.get_mut(&commodity) {
772                        c.supply += fill_qty;
773                        c.demand += fill_qty;
774                    }
775                    // Track wash trading
776                    if buyer == seller {
777                        let act = self.participant_activity.entry(buyer).or_default().entry(commodity).or_default();
778                        act.wash_prices.push_back(exec_price);
779                        if act.wash_prices.len() > 10 { act.wash_prices.pop_front(); }
780                    }
781                    // Update activity volumes
782                    {
783                        let act_buy = self.participant_activity.entry(buyer).or_default().entry(commodity).or_default();
784                        act_buy.buy_volume += fill_qty;
785                    }
786                    {
787                        let act_sell = self.participant_activity.entry(seller).or_default().entry(commodity).or_default();
788                        act_sell.sell_volume += fill_qty;
789                    }
790                }
791                _ => break,
792            }
793        }
794        for trade in new_trades {
795            if let Some(c) = self.commodities.get_mut(&trade.commodity) {
796                c.record_price(trade.price);
797            }
798            self.trade_history.push(trade);
799        }
800    }
801
802    // -----------------------------------------------------------------------
803    // Candle / Price History
804    // -----------------------------------------------------------------------
805
806    fn open_candle(&self, commodity: CommodityId) -> PricePoint {
807        let price = self.commodities.get(&commodity).map(|c| c.spot_price).unwrap_or(0.0);
808        PricePoint {
809            tick: self.current_tick,
810            open: price,
811            high: price,
812            low: price,
813            close: price,
814            volume: 0.0,
815        }
816    }
817
818    fn update_candle_from_trades(&mut self) {
819        // For each commodity, find trades this tick and update the open candle.
820        let ids: Vec<CommodityId> = self.commodities.keys().copied().collect();
821        for id in ids {
822            let trades: Vec<(f64, f64)> = self.trade_history.iter()
823                .filter(|t| t.commodity == id && t.tick == self.current_tick)
824                .map(|t| (t.price, t.quantity))
825                .collect();
826            if trades.is_empty() { continue; }
827            // Compute the candle seed before taking a mutable borrow on price_history.
828            let seed_price = self.commodities.get(&id).map(|c| c.spot_price).unwrap_or(0.0);
829            let current_tick = self.current_tick;
830            let hist = self.price_history.entry(id).or_default();
831            let needs_new = hist.back().map(|c| c.tick != current_tick).unwrap_or(true);
832            if needs_new {
833                hist.push_back(PricePoint {
834                    tick: current_tick,
835                    open: seed_price,
836                    high: seed_price,
837                    low: seed_price,
838                    close: seed_price,
839                    volume: 0.0,
840                });
841            }
842            let candle = hist.back_mut().unwrap();
843            for (price, qty) in trades {
844                if price > candle.high { candle.high = price; }
845                if price < candle.low { candle.low = price; }
846                candle.close = price;
847                candle.volume += qty;
848            }
849        }
850    }
851
852    // -----------------------------------------------------------------------
853    // Manipulation Detection
854    // -----------------------------------------------------------------------
855
856    fn detect_manipulation(&mut self) {
857        let tick = self.current_tick;
858        let mut alerts: Vec<ManipulationAlert> = Vec::new();
859
860        for (&participant, commodity_map) in &self.participant_activity {
861            for (&commodity, act) in commodity_map {
862                // Spoofing: cancel rate > 80% of placed orders and recent cancel spikes
863                if act.orders_placed >= 5 {
864                    let cancel_rate = act.orders_cancelled as f64 / act.orders_placed as f64;
865                    if cancel_rate >= 0.80 {
866                        let recent_cancels = act.cancel_spikes.iter()
867                            .filter(|&&t| tick.saturating_sub(t) <= 10)
868                            .count();
869                        if recent_cancels >= 3 {
870                            alerts.push(ManipulationAlert {
871                                participant,
872                                commodity,
873                                alert_type: ManipulationKind::Spoofing,
874                                confidence: (cancel_rate * 100.0).min(100.0),
875                                detected_tick: tick,
876                                details: format!(
877                                    "cancel_rate={:.0}% orders_placed={} recent_cancels={}",
878                                    cancel_rate * 100.0, act.orders_placed, recent_cancels
879                                ),
880                            });
881                        }
882                    }
883                }
884                // Wash trading: wash_prices non-empty
885                if !act.wash_prices.is_empty() {
886                    alerts.push(ManipulationAlert {
887                        participant,
888                        commodity,
889                        alert_type: ManipulationKind::WashTrading,
890                        confidence: (act.wash_prices.len() as f64 * 20.0).min(100.0),
891                        detected_tick: tick,
892                        details: format!("wash_trade_events={}", act.wash_prices.len()),
893                    });
894                }
895                // Price ramping: buy_volume >> sell_volume and price moved > 20%
896                if act.buy_volume > act.sell_volume * 5.0 && act.buy_volume > 100.0 {
897                    if let Some(c) = self.commodities.get(&commodity) {
898                        let price_move = (c.spot_price - c.base_price) / c.base_price.max(1e-9);
899                        if price_move > 0.20 {
900                            alerts.push(ManipulationAlert {
901                                participant,
902                                commodity,
903                                alert_type: ManipulationKind::PriceRamping,
904                                confidence: (price_move * 200.0).min(100.0),
905                                detected_tick: tick,
906                                details: format!(
907                                    "buy_vol={:.1} sell_vol={:.1} price_move={:.1}%",
908                                    act.buy_volume, act.sell_volume, price_move * 100.0
909                                ),
910                            });
911                        }
912                    }
913                }
914                // Cornering: single participant holds > 70% of buy-side volume
915                let total_buy_vol: f64 = self.participant_activity.values()
916                    .filter_map(|cm| cm.get(&commodity))
917                    .map(|a| a.buy_volume)
918                    .sum();
919                if total_buy_vol > 0.0 {
920                    let share = act.buy_volume / total_buy_vol;
921                    if share > 0.70 && act.buy_volume > 500.0 {
922                        alerts.push(ManipulationAlert {
923                            participant,
924                            commodity,
925                            alert_type: ManipulationKind::Cornering,
926                            confidence: (share * 100.0).min(100.0),
927                            detected_tick: tick,
928                            details: format!("market_share={:.1}%", share * 100.0),
929                        });
930                    }
931                }
932            }
933        }
934        self.manipulation_alerts.extend(alerts);
935        // Keep only last 256 alerts
936        if self.manipulation_alerts.len() > 256 {
937            let drain_count = self.manipulation_alerts.len() - 256;
938            self.manipulation_alerts.drain(0..drain_count);
939        }
940    }
941
942    // -----------------------------------------------------------------------
943    // Arbitrage Scanning
944    // -----------------------------------------------------------------------
945
946    fn scan_arbitrage(&mut self) {
947        let tick = self.current_tick;
948        let mut opportunities: Vec<ArbitrageOpportunity> = Vec::new();
949        let conversions: Vec<((CommodityId, CommodityId), f64)> =
950            self.conversion_graph.iter().map(|(&k, &v)| (k, v)).collect();
951
952        for ((from, to), ratio) in conversions {
953            let buy_price = match self.commodities.get(&from) {
954                Some(c) if !c.embargoed => c.spot_price,
955                _ => continue,
956            };
957            let sell_price = match self.commodities.get(&to) {
958                Some(c) if !c.embargoed => c.spot_price,
959                _ => continue,
960            };
961            // Cost to buy 1 unit of `from`, convert to `ratio` units of `to`, sell
962            let revenue = sell_price * ratio;
963            let profit_per_unit = revenue - buy_price;
964            if profit_per_unit > buy_price * 0.02 {
965                // > 2% profit margin
966                let confidence = (profit_per_unit / buy_price * 10.0).min(1.0);
967                opportunities.push(ArbitrageOpportunity {
968                    buy_commodity: from,
969                    sell_commodity: to,
970                    conversion_ratio: ratio,
971                    profit_per_unit,
972                    confidence,
973                    detected_tick: tick,
974                });
975            }
976        }
977        self.arbitrage_opportunities = opportunities;
978    }
979
980    // -----------------------------------------------------------------------
981    // Expire Stale Orders
982    // -----------------------------------------------------------------------
983
984    fn expire_orders(&mut self) {
985        let tick = self.current_tick;
986        for orders in self.buy_orders.values_mut().chain(self.sell_orders.values_mut()) {
987            for o in orders.iter_mut() {
988                if o.ttl > 0 && (o.status == OrderStatus::Open || o.status == OrderStatus::PartiallyFilled) {
989                    if tick.saturating_sub(o.placed_tick) >= o.ttl {
990                        o.status = OrderStatus::Expired;
991                    }
992                }
993            }
994        }
995    }
996
997    // -----------------------------------------------------------------------
998    // Auction Tick
999    // -----------------------------------------------------------------------
1000
1001    fn tick_auctions(&mut self) {
1002        let tick = self.current_tick;
1003        let ids: Vec<AuctionId> = self.auctions.keys().copied().collect();
1004        let mut to_settle: Vec<AuctionId> = Vec::new();
1005        let mut to_cancel: Vec<AuctionId> = Vec::new();
1006
1007        for &id in &ids {
1008            let auction = match self.auctions.get_mut(&id) {
1009                Some(a) if a.state == AuctionState::Open || a.state == AuctionState::Closing => a,
1010                _ => continue,
1011            };
1012            match auction.auction_type {
1013                AuctionType::Dutch => {
1014                    // Advance the Dutch clock
1015                    auction.current_dutch_price -= auction.dutch_decrement;
1016                    if auction.current_dutch_price <= auction.reserve_price {
1017                        auction.current_dutch_price = auction.reserve_price;
1018                        // No bidder -> cancel
1019                        if auction.bids.is_empty() {
1020                            to_cancel.push(id);
1021                        } else {
1022                            to_settle.push(id);
1023                        }
1024                    } else if auction.state == AuctionState::Closing {
1025                        to_settle.push(id);
1026                    }
1027                }
1028                AuctionType::English | AuctionType::SealedBid => {
1029                    if tick >= auction.close_tick {
1030                        if auction.bids.is_empty() {
1031                            to_cancel.push(id);
1032                        } else {
1033                            to_settle.push(id);
1034                        }
1035                    }
1036                }
1037            }
1038        }
1039        for id in to_cancel {
1040            if let Some(a) = self.auctions.get_mut(&id) {
1041                a.state = AuctionState::Cancelled;
1042            }
1043            if let Some(a) = self.auctions.remove(&id) {
1044                self.settled_auctions.push(a);
1045            }
1046        }
1047        for id in to_settle {
1048            self.settle_auction(id);
1049            if let Some(a) = self.auctions.remove(&id) {
1050                self.settled_auctions.push(a);
1051            }
1052        }
1053    }
1054
1055    // -----------------------------------------------------------------------
1056    // Main Tick
1057    // -----------------------------------------------------------------------
1058
1059    /// Advance the market by one simulation tick.
1060    ///
1061    /// This:
1062    /// 1. Expires stale orders.
1063    /// 2. Matches the order book for every commodity.
1064    /// 3. Recomputes supply/demand prices.
1065    /// 4. Updates OHLCV candles.
1066    /// 5. Ticks all running auctions.
1067    /// 6. Scans for manipulation and arbitrage.
1068    /// 7. Resets per-tick supply/demand accumulators.
1069    pub fn tick(&mut self) {
1070        self.current_tick += 1;
1071        self.expire_orders();
1072        let ids: Vec<CommodityId> = self.commodities.keys().copied().collect();
1073        for id in &ids {
1074            self.match_orders_for(*id);
1075        }
1076        for id in &ids {
1077            if let Some(c) = self.commodities.get_mut(id) {
1078                c.recompute_price();
1079            }
1080        }
1081        self.update_candle_from_trades();
1082        // Trim price histories
1083        for hist in self.price_history.values_mut() {
1084            while hist.len() > self.max_price_history {
1085                hist.pop_front();
1086            }
1087        }
1088        self.tick_auctions();
1089        self.detect_manipulation();
1090        self.scan_arbitrage();
1091        // Reset per-tick supply/demand to natural levels
1092        for c in self.commodities.values_mut() {
1093            c.supply = c.natural_supply;
1094            c.demand = c.natural_supply;
1095        }
1096    }
1097
1098    // -----------------------------------------------------------------------
1099    // Query Helpers
1100    // -----------------------------------------------------------------------
1101
1102    /// Current spot price for a commodity.
1103    pub fn spot_price(&self, commodity: CommodityId) -> Option<f64> {
1104        self.commodities.get(&commodity).map(|c| c.spot_price)
1105    }
1106
1107    /// Volume-weighted average price over last n trades.
1108    pub fn vwap(&self, commodity: CommodityId, n: usize) -> Option<f64> {
1109        self.trade_history.vwap(commodity, n)
1110    }
1111
1112    /// Price history (OHLCV candles), most recent last.
1113    pub fn price_history(&self, commodity: CommodityId) -> Option<&VecDeque<PricePoint>> {
1114        self.price_history.get(&commodity)
1115    }
1116
1117    /// Current open auctions.
1118    pub fn open_auctions(&self) -> impl Iterator<Item = &Auction> {
1119        self.auctions.values()
1120    }
1121
1122    /// All alerts raised this market's lifetime.
1123    pub fn all_alerts(&self) -> &[ManipulationAlert] {
1124        &self.manipulation_alerts
1125    }
1126
1127    /// Arbitrage opportunities detected last tick.
1128    pub fn arbitrage(&self) -> &[ArbitrageOpportunity] {
1129        &self.arbitrage_opportunities
1130    }
1131
1132    /// Summary statistics for a commodity.
1133    pub fn commodity_stats(&self, commodity: CommodityId) -> Option<CommodityStats> {
1134        let c = self.commodities.get(&commodity)?;
1135        Some(CommodityStats {
1136            id: commodity,
1137            name: c.name.clone(),
1138            spot_price: c.spot_price,
1139            base_price: c.base_price,
1140            ema_price: c.ema_price,
1141            volatility: c.volatility,
1142            supply: c.supply,
1143            demand: c.demand,
1144            bid: self.best_bid(commodity),
1145            ask: self.best_ask(commodity),
1146            spread: self.spread(commodity),
1147            embargoed: c.embargoed,
1148        })
1149    }
1150
1151    /// Depth of book: how many open buy/sell orders exist.
1152    pub fn book_depth(&self, commodity: CommodityId) -> (usize, usize) {
1153        let buys = self.buy_orders.get(&commodity).map(|b| {
1154            b.iter().filter(|o| o.status == OrderStatus::Open || o.status == OrderStatus::PartiallyFilled).count()
1155        }).unwrap_or(0);
1156        let sells = self.sell_orders.get(&commodity).map(|s| {
1157            s.iter().filter(|o| o.status == OrderStatus::Open || o.status == OrderStatus::PartiallyFilled).count()
1158        }).unwrap_or(0);
1159        (buys, sells)
1160    }
1161}
1162
1163/// Snapshot of a commodity's current market state.
1164#[derive(Debug, Clone)]
1165pub struct CommodityStats {
1166    pub id: CommodityId,
1167    pub name: String,
1168    pub spot_price: f64,
1169    pub base_price: f64,
1170    pub ema_price: f64,
1171    pub volatility: f64,
1172    pub supply: f64,
1173    pub demand: f64,
1174    pub bid: Option<f64>,
1175    pub ask: Option<f64>,
1176    pub spread: Option<f64>,
1177    pub embargoed: bool,
1178}
1179
1180impl Default for Market {
1181    fn default() -> Self {
1182        Self::new()
1183    }
1184}
1185
1186// ---------------------------------------------------------------------------
1187// Tests
1188// ---------------------------------------------------------------------------
1189
1190#[cfg(test)]
1191mod tests {
1192    use super::*;
1193
1194    #[test]
1195    fn test_register_and_price() {
1196        let mut m = Market::new();
1197        let id = m.register_commodity("Gold", 100.0, 500.0, 0.5);
1198        assert_eq!(m.spot_price(id), Some(100.0));
1199        m.inject_demand(id, 2000.0);
1200        m.tick();
1201        let price = m.spot_price(id).unwrap();
1202        assert!(price > 100.0, "price should rise with excess demand: {}", price);
1203    }
1204
1205    #[test]
1206    fn test_order_matching() {
1207        let mut m = Market::new();
1208        let id = m.register_commodity("Iron", 50.0, 100.0, 0.3);
1209        let buyer = ParticipantId(1);
1210        let seller = ParticipantId(2);
1211        m.place_order(buyer, id, OrderSide::Buy, 55.0, 10.0, 0);
1212        m.place_order(seller, id, OrderSide::Sell, 45.0, 10.0, 0);
1213        m.tick();
1214        // A trade should have occurred
1215        let trades: Vec<_> = m.trade_history.iter().collect();
1216        assert!(!trades.is_empty());
1217    }
1218
1219    #[test]
1220    fn test_english_auction() {
1221        let mut m = Market::new();
1222        let id = m.register_commodity("Silk", 200.0, 50.0, 0.4);
1223        let seller = ParticipantId(10);
1224        let bidder1 = ParticipantId(11);
1225        let bidder2 = ParticipantId(12);
1226        let aid = m.open_auction(AuctionType::English, id, 100.0, seller, 150.0, 150.0, 0.0, 10);
1227        m.bid_auction(aid, bidder1, 160.0);
1228        m.bid_auction(aid, bidder2, 175.0);
1229        // Advance past close_tick
1230        for _ in 0..11 { m.tick(); }
1231        let settled: Vec<_> = m.settled_auctions.iter().filter(|a| a.id == aid).collect();
1232        assert!(!settled.is_empty());
1233        let auction = &settled[0];
1234        assert_eq!(auction.winner, Some(bidder2));
1235        assert!((auction.winning_price.unwrap() - 175.0).abs() < 1e-9);
1236    }
1237
1238    #[test]
1239    fn test_sealed_bid_vickrey() {
1240        let mut m = Market::new();
1241        let id = m.register_commodity("Gems", 500.0, 10.0, 0.6);
1242        let seller = ParticipantId(20);
1243        let bidder1 = ParticipantId(21);
1244        let bidder2 = ParticipantId(22);
1245        let aid = m.open_auction(AuctionType::SealedBid, id, 5.0, seller, 400.0, 400.0, 0.0, 5);
1246        m.bid_auction(aid, bidder1, 600.0);
1247        m.bid_auction(aid, bidder2, 550.0);
1248        for _ in 0..6 { m.tick(); }
1249        let settled: Vec<_> = m.settled_auctions.iter().filter(|a| a.id == aid).collect();
1250        let auction = &settled[0];
1251        assert_eq!(auction.winner, Some(bidder1));
1252        // Vickrey: pays second-highest price
1253        assert!((auction.winning_price.unwrap() - 550.0).abs() < 1e-9);
1254    }
1255
1256    #[test]
1257    fn test_embargo() {
1258        let mut m = Market::new();
1259        let id = m.register_commodity("Spice", 75.0, 200.0, 0.4);
1260        m.set_embargo(id, true);
1261        let buyer = ParticipantId(30);
1262        let result = m.place_order(buyer, id, OrderSide::Buy, 80.0, 10.0, 0);
1263        assert!(result.is_none(), "orders on embargoed commodity should be rejected");
1264    }
1265
1266    #[test]
1267    fn test_arbitrage_detection() {
1268        let mut m = Market::new();
1269        let wheat = m.register_commodity("Wheat", 10.0, 1000.0, 0.3);
1270        let bread = m.register_commodity("Bread", 35.0, 200.0, 0.5);
1271        // 1 wheat -> 3 bread, cost 10, revenue 105 => massive profit
1272        m.register_conversion(wheat, bread, 3.0);
1273        m.tick();
1274        assert!(!m.arbitrage_opportunities.is_empty());
1275    }
1276
1277    #[test]
1278    fn test_vwap() {
1279        let mut m = Market::new();
1280        let id = m.register_commodity("Wood", 20.0, 500.0, 0.3);
1281        let b = ParticipantId(1);
1282        let s = ParticipantId(2);
1283        m.place_order(b, id, OrderSide::Buy, 25.0, 100.0, 0);
1284        m.place_order(s, id, OrderSide::Sell, 15.0, 100.0, 0);
1285        m.tick();
1286        assert!(m.vwap(id, 10).is_some());
1287    }
1288}