The system aims to maximize: Revenue = Price × Expected_Bookings(Price)
The key insight: raising prices increases revenue per booking, but decreases the number of bookings. The optimal price depends on how price-sensitive (elastic) each customer segment is.
Location: Lines 50-51 in pricing.py
Starting point based purely on spot type:
- Standard: $10/hour (baseline)
- EV: $15/hour (premium for charging infrastructure)
- Motorcycle: $5/hour (smaller space, less valuable)
Reasoning: Different spot types have different inherent costs and value propositions. EV spots require expensive charging equipment, so they command a premium.
Location: Lines 53-60 in pricing.py
Five multipliers that adjust the base price based on market conditions:
# Breakpoints from settings.py:
(0%, 1.0x), (50%, 1.0x), (70%, 1.5x), (85%, 2.5x), (95%, 3.5x), (100%, 4.0x)How it works: Calculates current occupancy rate, then uses linear interpolation between breakpoints.
Reasoning:
- 0-50% occupancy: Flat 1.0x (plenty of availability, no scarcity premium)
- 50-70%: Gentle ramp to 1.5x (scarcity starting to matter)
- 70-100%: Aggressive non-linear surge (4x at full capacity)
- This creates dramatic price movements for demo effect
- Economically: scarcity increases willingness to pay
Real behavior: At 60% occupancy, price is between 1.0x and 1.5x (interpolated). At 95%, you're at 3.5x.
# Breakpoints (hours_before_game -> multiplier):
(13hr+, 0.5x), (8hr, 0.7x), (4hr, 1.0x), (2hr, 1.5x), (1hr, 2.0x), (0hr, 2.5x), (-1hr, 1.5x)How it works: Calculates hours_before_game = game_hour - current_time, interpolates multiplier.
Reasoning:
- Early morning (6 AM, 13hrs before): 0.5x discount (soft demand)
- 4 hours before (3 PM): 1.0x baseline
- 1 hour before (6 PM): 2.0x (time pressure, last-minute panic)
- Game time (7 PM): 2.5x peak
- After game starts: Drops to 1.5x then 0.8x (demand collapses)
Economic logic: Time pressure reduces price sensitivity. People arriving close to game time have fewer alternatives and will pay more.
# From DEMAND_FORECAST in settings.py:
# 6 AM: 0.05, 12 PM: 0.25, 4 PM: 0.60, 7 PM: 1.00, 11 PM: 0.10How it works: Looks up the demand factor for the current hour (with interpolation for fractional hours).
Reasoning: This represents predicted volume of people looking to park. High demand hours justify higher prices even if current occupancy is low (forward-looking pricing). At 18.5 (6:30 PM), demand is 0.95, meaning prices are already elevated even before occupancy hits capacity.
Why separate from time multiplier? Time multiplier captures time-pressure psychology. Demand multiplier captures volume/traffic patterns. They're correlated but conceptually different.
Zone A (near entrance): 1.3x
Zone B (middle): 1.0x
Zone C (far): 0.8xHow it works: Simple lookup based on space's zone assignment.
Reasoning: Convenience matters. Spots near the entrance are worth more because they save customers time walking. Zone C gets a 20% discount to compensate for the long walk.
event_multiplier: 2.0How it works: Flat 2.0x multiplier applied to all spots.
Reasoning: Special events command premium pricing. World Cup at MetLife Stadium creates extraordinary demand. This is a global multiplier for the entire demo scenario.
context_price = base_price × occ_mult × time_mult × demand_mult × location_mult × event_multAll five multipliers are multiplicative, creating compounding effects. A 100% full garage (4.0x) at game time (2.5x) for an EV spot ($15 base) in Zone A (1.3x) during World Cup (2.0x) with peak demand (1.0x):
$15 × 4.0 × 2.5 × 1.0 × 1.3 × 2.0 = $390/hour (before elasticity adjustment and guardrails)
Location: Lines 62-77 in pricing.py
This is where the revenue maximization magic happens.
Economic Theory: Price elasticity of demand (ε) measures how quantity demanded responds to price changes:
- ε < 1.0 (Inelastic): Demand is insensitive to price → raise prices (revenue goes up)
- ε > 1.0 (Elastic): Demand is sensitive to price → lower prices (volume compensates for lower price)
- ε = 1.0 (Unit elastic): No adjustment needed → optimal price
Function: _get_elasticity() (lines 172-203)
Three factors determine elasticity for a booking:
1. Base elasticity by spot type:
- Standard: 1.0 (neutral baseline)
- EV: 0.7 (inelastic - EV drivers have limited options, need charging)
- Motorcycle: 1.1 (slightly elastic)
2. Zone modifier:
- Zone A: 0.9× (near entrance = convenience premium = inelastic)
- Zone B: 1.0× (neutral)
- Zone C: 1.3× (far away = price-sensitive = elastic)
3. Timing modifier (optional, only if booking_lead_time provided):
- Last-minute (<1hr before game): 0.7× modifier → very inelastic (desperation)
- Advance (>4hr before game): 1.2× modifier → more elastic (can shop around)
Combined elasticity = type_elasticity × zone_modifier × timing_modifier
Example segments:
- EV Zone A, last-minute:
0.7 × 0.9 × 0.7 = 0.44(extremely inelastic) - Standard Zone C, advance:
0.0 × 1.3 × 1.2 = 1.56(quite elastic) - Standard Zone B, no timing:
1.0 × 1.0 = 1.0(unit elastic)
if elasticity < 1.0:
# Inelastic: push price UP
elasticity_adj = 1.0 + (1.0 - elasticity)
# Example: e=0.7 → adj = 1.0 + 0.3 = 1.3 → +30% price increase
elif elasticity > 1.0:
# Elastic: pull price DOWN for volume
elasticity_adj = 1.0 / elasticity
# Example: e=1.3 → adj = 0.769 → -23% price reduction
else:
elasticity_adj = 1.0 # No changeFinal price: context_price × elasticity_adjustment
Economic intuition:
- Inelastic segments (EV drivers, last-minute, Zone A): Can tolerate higher prices → extract maximum revenue
- Elastic segments (far spots, advance booking, motorcycles): Price-sensitive → reduce price to capture volume
Location: Lines 79-80 in pricing.py
final_price = max(5.0, min(50.0, final_price))Floor: $5/hour minimum (prevents absurdly low prices) Ceiling: $50/hour maximum (prevents gouging, keeps demo realistic)
Note: Original spec had ±20% smoothing (no sudden price jumps) but it was intentionally removed for dramatic demo effect. Prices can swing freely within $5-$50 range.
Let's price an EV spot in Zone A at 6 PM (18.0 hours), 70% occupancy:
# Layer 1: Base price
base_price = $15 # EV spot
# Layer 2: Context multipliers
occupancy_mult = 1.5 # 70% occupancy → 1.5x
time_mult = 2.0 # 1 hour before game → 2.0x
demand_mult = 0.90 # Hour 18 demand → 0.90
location_mult = 1.3 # Zone A → 1.3x
event_mult = 2.0 # World Cup → 2.0x
context_price = 15 × 1.5 × 2.0 × 0.90 × 1.3 × 2.0 = $105.30
# Layer 3: Elasticity
elasticity = 0.7 × 0.9 = 0.63 # EV × Zone A (assuming no timing modifier)
# Inelastic! Push price up
elasticity_adj = 1.0 + (1.0 - 0.63) = 1.37
optimized_price = 105.30 × 1.37 = $144.26
# Guardrails
final_price = min(50.0, 144.26) = $50.00 # Ceiling hit!Result: $50/hour (at ceiling). The system wanted to charge $144 but was capped.
- Economically rigorous: Based on real pricing theory (elasticity of demand)
- Revenue-maximizing: Different segments pay different prices based on willingness to pay
- Transparent: Every multiplier is visible in
PriceResultbreakdown - Tunable: All parameters (elasticities, multipliers, breakpoints) are configurable
- Dramatic for demo: Non-linear occupancy curve + no smoothing = exciting price swings
- Self-explanatory: Each layer has clear economic logic that reviewers can follow
The pricing engine is thoroughly tested in test_pricing.py:
- Interpolation tests: Verify linear interpolation logic for all curves
- Occupancy tests: Validate occupancy rate calculation and multiplier at various levels
- Time multiplier tests: Check correct pricing at different times relative to game
- Demand multiplier tests: Verify demand curve interpolation
- Elasticity tests: Test all combinations of type, zone, and timing modifiers
- Guardrail tests: Ensure floor and ceiling are enforced
- Full pipeline tests: End-to-end scenarios with inelastic, elastic, and unit elastic segments
- Edge cases: Zero occupancy, full occupancy, cancelled reservations, etc.
Run tests with:
python3 -m pytest backend/tests/test_pricing.py -vReturned by calculate_price(), contains:
final_price: The price after all adjustments and guardrailsbase_price: Layer 1 starting pricecontext_price: After Layer 2 multipliers, before elasticityoccupancy_multiplier,time_multiplier,demand_multiplier,location_multiplier,event_multiplier: Individual Layer 2 multiplierselasticity: Calculated price elasticity for this segmentelasticity_adjustment: Layer 3 adjustment factoroptimization_note: Human-readable explanation of elasticity adjustment
This breakdown enables full transparency for debugging and the System Panel UI.
The operator dashboard displays a projected end-of-day revenue estimate that updates in real-time as the simulation progresses. This helps operators understand potential daily earnings based on current performance.
The projection uses demand-curve-weighted extrapolation:
- Sum past demand: Add up demand forecast values for all hours before the current time
- Sum remaining demand: Add up demand forecast values for all hours from current time to end of day
- Calculate ratio:
remaining_demand / past_demand - Extrapolate:
projected_revenue = current_revenue + (current_revenue × ratio)
At 3 PM (hour 15) with $2,000 in revenue:
Past demand (6 AM - 2 PM): 0.05 + 0.08 + 0.10 + 0.12 + 0.15 + 0.20 + 0.25 + 0.30 + 0.40 = 1.65
Remaining demand (3 PM - 11 PM): 0.50 + 0.60 + 0.75 + 0.90 + 1.00 + 0.70 + 0.40 + 0.20 + 0.10 = 5.15
Ratio = 5.15 / 1.65 = 3.12
Projected additional = $2,000 × 3.12 = $6,240
Total projected = $2,000 + $6,240 = $8,240
This approach assumes that future revenue per demand unit will match past performance. The demand curve captures when people are expected to arrive, so if you've earned $X during low-demand hours, you'll earn proportionally more during high-demand hours.
Limitations:
- Early projections (before 9 AM) are less reliable due to small sample size
- Doesn't account for price changes from increasing occupancy
- Assumes consistent booking behavior throughout the day
The projection is shown below current revenue in the dashboard and only appears when there's sufficient data to make a meaningful estimate.
All pricing parameters live in backend/config/settings.py:
PricingConfig: Base prices, elasticity values, multiplier breakpoints, guardrailsGarageConfig: Garage dimensions, game time, simulation parametersDEMAND_FORECAST: Hourly demand curve (6 AM - 11 PM)
To tune the pricing behavior, modify these config values rather than the pricing engine code itself.