surge_network/market/virtual_bid.rs
1// SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0
2//! Virtual energy bids for day-ahead market clearing.
3//!
4//! A **virtual bid** (also called an "inc/dec bid" or "convergence bid") is a
5//! financial position that injects or withdraws virtual MW at a bus to influence
6//! LMPs. They are a day-ahead market construct — RTOs (ERCOT, PJM, ISO-NE) use
7//! them for convergence bidding between day-ahead and real-time markets.
8//!
9//! # Economics
10//!
11//! - **Inc (increment) bid**: Injects MW at a bus, competing against physical
12//! generators. Clears when the bus LMP ≥ bid price; drives LMP down at that bus.
13//! - **Dec (decrement) bid**: Withdraws MW at a bus, competing against physical
14//! loads. Clears when the bus LMP ≤ bid price; drives LMP up at that bus.
15//!
16//! # LP formulation
17//!
18//! Each in-service bid adds one variable `v_k ∈ [0, mw_limit / base_mva]`.
19//!
20//! **Objective** (minimise total production cost + virtual bid cost):
21//! - Inc bid: `+price_per_mwh * base_mva * v_k` (paying for energy)
22//! - Dec bid: `-price_per_mwh * base_mva * v_k` (receiving payment)
23//!
24//! **Power balance at bus** (LP constraint matrix coefficient for variable `v_k`):
25//! - Inc bid: `-1.0` — injects power at the bus (same sign as a generator in the
26//! B-theta formulation: `B·θ - Σ Pg + Σ Pd = 0`).
27//! - Dec bid: `+1.0` — withdraws power from the bus (same sign as a load).
28//!
29//! Uneconomic bids clear at zero naturally: the LP will not award an Inc bid at
30//! a price above the equilibrium LMP, nor a Dec bid at a price below it.
31
32use serde::{Deserialize, Serialize};
33
34/// Direction of a virtual bid.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub enum VirtualBidDirection {
38 /// Increment (virtual injection): adds MW supply at this bus.
39 Inc,
40 /// Decrement (virtual withdrawal): adds MW demand at this bus.
41 Dec,
42}
43
44/// A virtual energy bid for day-ahead market clearing.
45///
46/// Virtual bids participate in DC-OPF / SCED / SCUC as purely financial
47/// positions: they affect LMPs but do not represent physical generation or load.
48/// They are a day-ahead instrument — do not use in real-time dispatch.
49///
50/// Each bid targets exactly one dispatch period (hour). A trader bidding the
51/// same price/quantity at the same location for hours 14–18 submits five
52/// separate positions, matching real ISO practice (ERCOT, PJM, ISO-NE).
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct VirtualBid {
55 /// Market-layer position ID that originated this bid.
56 ///
57 /// When a single position maps to multiple buses (e.g. via a settlement
58 /// location), each constituent bus-level bid carries the same position_id
59 /// so that cleared MW can be aggregated back to the originating position.
60 pub position_id: String,
61 /// External bus number where the virtual injection/withdrawal occurs.
62 pub bus: u32,
63 /// Target dispatch period (0-indexed hour).
64 #[serde(default)]
65 pub period: usize,
66 /// Maximum cleared MW (≥ 0). The LP variable is bounded `[0, mw_limit/base]`.
67 pub mw_limit: f64,
68 /// Offer/bid price ($/MWh).
69 ///
70 /// - Inc bid: offer price — cleared if bus LMP ≥ price (cheaper than LMP).
71 /// - Dec bid: bid price — cleared if bus LMP ≤ price (more expensive than LMP).
72 pub price_per_mwh: f64,
73 /// Inc (virtual injection) or Dec (virtual withdrawal).
74 pub direction: VirtualBidDirection,
75 /// Whether this bid participates in the current solve.
76 pub in_service: bool,
77}
78
79/// Result for a single virtual bid after market clearing.
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct VirtualBidResult {
82 /// Market-layer position ID that originated this bid.
83 pub position_id: String,
84 /// External bus number.
85 pub bus: u32,
86 /// Bid direction.
87 pub direction: VirtualBidDirection,
88 /// Cleared MW (≥ 0). Zero means the bid was not awarded.
89 pub cleared_mw: f64,
90 /// Submitted offer/bid price ($/MWh).
91 pub price_per_mwh: f64,
92 /// Bus LMP at the optimal solution ($/MWh).
93 pub lmp: f64,
94}