1use std::collections::{HashMap, VecDeque};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub struct CommodityId(pub u32);
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub struct OrderId(pub u64);
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub struct AuctionId(pub u32);
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct ParticipantId(pub u32);
28
29#[derive(Debug, Clone)]
35pub struct Commodity {
36 pub id: CommodityId,
37 pub name: String,
38 pub base_price: f64,
40 pub natural_supply: f64,
42 pub elasticity: f64,
45 pub spot_price: f64,
47 pub supply: f64,
49 pub demand: f64,
51 pub ema_price: f64,
53 pub volatility: f64,
55 recent_log_returns: VecDeque<f64>,
57 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 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 let alpha = 0.25;
89 self.ema_price = alpha * new_price + (1.0 - alpha) * self.ema_price;
90 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 fn recompute_price(&mut self) {
102 let effective_supply = self.supply.max(1.0);
103 let effective_demand = self.demand.max(1.0);
104 let ratio = effective_demand / effective_supply;
106 let pressure = ratio.ln() * self.elasticity;
108 let raw = self.base_price * pressure.exp();
109 let clamped = raw.clamp(self.base_price * 0.01, self.base_price * 100.0);
111 self.record_price(clamped);
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum OrderSide {
122 Buy,
123 Sell,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub enum OrderStatus {
129 Open,
130 PartiallyFilled,
131 Filled,
132 Cancelled,
133 Expired,
134}
135
136#[derive(Debug, Clone)]
138pub struct Order {
139 pub id: OrderId,
140 pub participant: ParticipantId,
141 pub commodity: CommodityId,
142 pub side: OrderSide,
143 pub limit_price: f64,
145 pub quantity: f64,
146 pub filled: f64,
147 pub status: OrderStatus,
148 pub placed_tick: u64,
150 pub ttl: u64,
152}
153
154impl Order {
155 pub fn remaining(&self) -> f64 {
156 self.quantity - self.filled
157 }
158}
159
160#[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
175pub 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 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 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234pub enum AuctionType {
235 English,
237 Dutch,
239 SealedBid,
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub enum AuctionState {
246 Open,
247 Closing,
248 Settled,
249 Cancelled,
250}
251
252#[derive(Debug, Clone)]
254pub struct AuctionBid {
255 pub bidder: ParticipantId,
256 pub amount: f64,
257 pub tick: u64,
258}
259
260#[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 pub reserve_price: f64,
270 pub start_price: f64,
272 pub dutch_decrement: f64,
274 pub current_dutch_price: f64,
276 pub state: AuctionState,
277 pub bids: Vec<AuctionBid>,
278 pub opened_tick: u64,
279 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#[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 Spoofing,
319 WashTrading,
321 PriceRamping,
323 Cornering,
325}
326
327#[derive(Debug, Clone)]
334pub struct ArbitrageOpportunity {
335 pub buy_commodity: CommodityId,
336 pub sell_commodity: CommodityId,
337 pub conversion_ratio: f64,
339 pub profit_per_unit: f64,
340 pub confidence: f64,
341 pub detected_tick: u64,
342}
343
344#[derive(Debug, Default, Clone)]
349struct ParticipantActivity {
350 orders_placed: u32,
351 orders_cancelled: u32,
352 buy_volume: f64,
353 sell_volume: f64,
354 cancel_spikes: VecDeque<u64>,
356 wash_prices: VecDeque<f64>,
358}
359
360pub 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 commodity_names: HashMap<String, CommodityId>,
375
376 buy_orders: HashMap<CommodityId, Vec<Order>>,
378 sell_orders: HashMap<CommodityId, Vec<Order>>,
379
380 pub trade_history: TradeHistory,
381 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 participant_activity: HashMap<ParticipantId, HashMap<CommodityId, ParticipantActivity>>,
392
393 conversion_graph: HashMap<(CommodityId, CommodityId), f64>,
395
396 max_price_history: usize,
398}
399
400impl Market {
401 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 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 pub fn commodity_by_name(&self, name: &str) -> Option<CommodityId> {
449 self.commodity_names.get(name).copied()
450 }
451
452 pub fn register_conversion(&mut self, from: CommodityId, to: CommodityId, ratio: f64) {
454 self.conversion_graph.insert((from, to), ratio);
455 }
456
457 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 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 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 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 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 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 book.sort_by(|a, b| a.limit_price.partial_cmp(&b.limit_price).unwrap());
530 }
531 }
532 Some(id)
533 }
534
535 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 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 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 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 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 pub fn spread(&self, commodity: CommodityId) -> Option<f64> {
594 Some(self.best_ask(commodity)? - self.best_bid(commodity)?)
595 }
596
597 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 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 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 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 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 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 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 if let Some(c) = self.commodities.get_mut(&commodity) {
716 c.demand += quantity;
717 }
718 Some(winning_price)
719 }
720
721 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 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 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 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 if let Some(c) = self.commodities.get_mut(&commodity) {
772 c.supply += fill_qty;
773 c.demand += fill_qty;
774 }
775 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 {
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 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 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 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 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 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 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 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 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 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 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 let revenue = sell_price * ratio;
963 let profit_per_unit = revenue - buy_price;
964 if profit_per_unit > buy_price * 0.02 {
965 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 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 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 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 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 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 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 for c in self.commodities.values_mut() {
1093 c.supply = c.natural_supply;
1094 c.demand = c.natural_supply;
1095 }
1096 }
1097
1098 pub fn spot_price(&self, commodity: CommodityId) -> Option<f64> {
1104 self.commodities.get(&commodity).map(|c| c.spot_price)
1105 }
1106
1107 pub fn vwap(&self, commodity: CommodityId, n: usize) -> Option<f64> {
1109 self.trade_history.vwap(commodity, n)
1110 }
1111
1112 pub fn price_history(&self, commodity: CommodityId) -> Option<&VecDeque<PricePoint>> {
1114 self.price_history.get(&commodity)
1115 }
1116
1117 pub fn open_auctions(&self) -> impl Iterator<Item = &Auction> {
1119 self.auctions.values()
1120 }
1121
1122 pub fn all_alerts(&self) -> &[ManipulationAlert] {
1124 &self.manipulation_alerts
1125 }
1126
1127 pub fn arbitrage(&self) -> &[ArbitrageOpportunity] {
1129 &self.arbitrage_opportunities
1130 }
1131
1132 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 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#[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#[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 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 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 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 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}