Skip to main content

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}