aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNavan Chauhan <navanchauhan@gmail.com>2025-04-27 20:44:14 -0600
committerNavan Chauhan <navanchauhan@gmail.com>2025-04-27 20:44:14 -0600
commit2bc0555caa595967a28593aec2abd52500c5ddf9 (patch)
treef64875ade75adb690e93bd5b5d47e25d9b0df46a
parentca784c19f48814094c91a2c1a5652c20ca381c13 (diff)
switch markets for market data
-rw-r--r--client/src/App.js71
-rw-r--r--client/src/MarketDataPage.jsx141
-rw-r--r--server/api/market.py73
3 files changed, 183 insertions, 102 deletions
diff --git a/client/src/App.js b/client/src/App.js
index 4e08a17..a83b628 100644
--- a/client/src/App.js
+++ b/client/src/App.js
@@ -1,10 +1,9 @@
-import React from 'react';
+import React, { useState, createContext } from 'react';
import { Routes, Route, Navigate, Link} from 'react-router-dom';
import MarketDataPage from './MarketDataPage';
import BidsPage from './BidsPage';
import SubmitBidPage from './SubmitBidPage';
-import { Menu } from '@arco-design/web-react';
-import { PageHeader, Typography } from '@arco-design/web-react';
+import { Menu, Select, PageHeader, Typography } from '@arco-design/web-react';
import '@arco-design/web-react/dist/css/arco.css';
const ghostBgStyle = {
@@ -13,13 +12,29 @@ const ghostBgStyle = {
padding: 20,
};
+export const MarketContext = createContext();
+export const MARKET_FULL_NAMES = {
+ CAISO: 'California ISO',
+ ERCOT: 'Electric Reliability Council of Texas',
+ ISONE: 'ISO New England',
+ MISO: 'Midcontinent ISO',
+ NYISO: 'New York ISO',
+ PJM: 'PJM Interconnection',
+};
+
function App() {
+ const [selectedMarket, setSelectedMarket] = useState('ISONE');
return (
+ <MarketContext.Provider value={{ selectedMarket, setSelectedMarket }}>
<div>
<style>
{`
@media (max-width: 600px) {
- .page-header-title {
+ .page-header-container {
+ flex-direction: column;
+ align-items: center;
+ }
+ .page-header-title {
font-size: 20px !important;
text-align: center;
}
@@ -27,22 +42,45 @@ function App() {
font-size: 14px !important;
text-align: center;
}
+ .market-select-container {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ margin-top: 10px;
+ }
+ .market-select {
+ width: 100% !important;
+ text-align: center;
+ }
}
`}
</style>
<div style={ghostBgStyle}>
- <PageHeader
- title={
- <Typography.Title heading={3} className="page-header-title">
- PolyEnergy
- </Typography.Title>
- }
- subTitle={
- <Typography.Title type="secondary" heading={4} className="page-header-subtitle">
- Virtual Energy Trading
- </Typography.Title>
- }
- />
+ <div className="page-header-container" style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-end', flexWrap: 'wrap' }}>
+ <div>
+ <PageHeader
+ title={
+ <Typography.Title heading={3} className="page-header-title">
+ PolyEnergy
+ </Typography.Title>
+ }
+ subTitle={
+ <Typography.Title type="secondary" heading={4} className="page-header-subtitle">
+ Virtual Energy Trading
+ </Typography.Title>
+ }
+ />
+ </div>
+ <div className="market-select-container">
+ <Select
+ className="market-select"
+ value={selectedMarket}
+ onChange={setSelectedMarket}
+ style={{ width: 200 }}
+ options={['ISONE', 'MISO', 'NYISO'].map(market => ({ label: market, value: market }))}
+ />
+ </div>
+ </div>
<Menu mode="horizontal" defaultSelectedKeys={['market-data']}>
<Menu.Item key="market-data">
<Link to="/market-data">Market Data</Link>
@@ -63,6 +101,7 @@ function App() {
<Route path="/bids" element={<BidsPage />} />
</Routes>
</div>
+ </MarketContext.Provider>
);
}
diff --git a/client/src/MarketDataPage.jsx b/client/src/MarketDataPage.jsx
index 3ffa099..76d8284 100644
--- a/client/src/MarketDataPage.jsx
+++ b/client/src/MarketDataPage.jsx
@@ -1,73 +1,99 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useContext } from 'react';
import { Typography, Spin, Message, Card, Grid } from '@arco-design/web-react';
import '@arco-design/web-react/dist/css/arco.css';
import { initVChartArcoTheme } from '@visactor/vchart-arco-theme';
import { VChart } from '@visactor/react-vchart';
import API_BASE_URL from './config';
+import { MarketContext, MARKET_FULL_NAMES } from './App';
+
const { Row, Col } = Grid;
-function MarketDataPage() {
- const [fiveMinData, setFiveMinData] = useState([]);
- const [dayAheadData, setDayAheadData] = useState([]);
- const [loading, setLoading] = useState(true);
- const [fiveMinLastUpdated, setFiveMinLastUpdated] = useState(null);
- const [dayAheadLastUpdated, setDayAheadLastUpdated] = useState(null);
+function TimeAgo({ timestamp, prefix }) {
const [now, setNow] = useState(new Date());
useEffect(() => {
- const fetchFiveMinData = async () => {
- try {
- const res = await fetch(`${API_BASE_URL}/market/real-time`);
- if (!res.ok) throw new Error('Failed to fetch real-time data');
- const json = await res.json();
- setFiveMinData(json);
- setFiveMinLastUpdated(new Date());
- } catch (err) {
- console.error(err);
- Message.error('Failed to load real-time market data.');
- }
- };
+ const timer = setInterval(() => setNow(new Date()), 1000);
+ return () => clearInterval(timer);
+ }, []);
- const fetchDayAheadData = async () => {
- try {
- const res = await fetch(`${API_BASE_URL}/market/day-ahead`);
- if (!res.ok) throw new Error('Failed to fetch day-ahead data');
- const json = await res.json();
- setDayAheadData(json);
- setDayAheadLastUpdated(new Date());
- } catch (err) {
- console.error(err);
- Message.error('Failed to load day-ahead market data.');
- }
- };
+ const formatTimeAgo = () => {
+ if (!timestamp) return '';
+ const seconds = Math.floor((now - timestamp) / 1000);
+ if (seconds < 60) return `${seconds} sec ago`;
+ const minutes = Math.floor(seconds / 60);
+ if (minutes < 60) return `${minutes} min ago`;
+ const hours = Math.floor(minutes / 60);
+ return `${hours} hr ago`;
+ };
- const initialize = async () => {
- setLoading(true);
- await Promise.all([fetchFiveMinData(), fetchDayAheadData()]);
- setLoading(false);
+ return (
+ <Typography.Text type="secondary" style={{ display: 'block' }}>
+ {prefix}: {formatTimeAgo()}
+ </Typography.Text>
+ );
+}
- initVChartArcoTheme({
- defaultMode: 'light',
- isWatchingMode: true
- });
- };
- initialize();
+function MarketDataPage() {
+ const { selectedMarket } = useContext(MarketContext);
+ const [fiveMinData, setFiveMinData] = useState([]);
+ const [dayAheadData, setDayAheadData] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [fiveMinLastUpdated, setFiveMinLastUpdated] = useState(null);
+ const [dayAheadLastUpdated, setDayAheadLastUpdated] = useState(null);
+ const [now, setNow] = useState(new Date());
+
+ useEffect(() => {
+ const fetchFiveMinData = async () => {
+ try {
+ const res = await fetch(`${API_BASE_URL}/market/real-time?market=${selectedMarket}`);
+ if (!res.ok) throw new Error('Failed to fetch real-time data');
+ const json = await res.json();
+ setFiveMinData(json);
+ setFiveMinLastUpdated(new Date());
+ } catch (err) {
+ console.error(err);
+ Message.error('Failed to load real-time market data.');
+ }
+ };
+
+ const fetchDayAheadData = async () => {
+ try {
+ const res = await fetch(`${API_BASE_URL}/market/day-ahead?market=${selectedMarket}`);
+ if (!res.ok) throw new Error('Failed to fetch day-ahead data');
+ const json = await res.json();
+ setDayAheadData(json);
+ setDayAheadLastUpdated(new Date());
+ } catch (err) {
+ console.error(err);
+ Message.error('Failed to load day-ahead market data.');
+ }
+ };
- const fiveMinInterval = setInterval(fetchFiveMinData, 5 * 60 * 1000); // 5 minutes
- const dayAheadInterval = setInterval(fetchDayAheadData, 60 * 60 * 1000); // 1 hour
+ const initialize = async () => {
+ setLoading(true);
+ await Promise.all([fetchFiveMinData(), fetchDayAheadData()]);
+ setLoading(false);
- const timer = setInterval(() => {
- setNow(new Date());
- }, 1000);
+ initVChartArcoTheme({
+ defaultMode: 'light',
+ isWatchingMode: true
+ });
+ };
- return () => {
- clearInterval(fiveMinInterval);
- clearInterval(dayAheadInterval);
- clearInterval(timer);
- };
- }, []);
+ initialize();
+
+ const fiveMinInterval = setInterval(fetchFiveMinData, 5 * 60 * 1000); // 5 minutes
+ const dayAheadInterval = setInterval(fetchDayAheadData, 60 * 60 * 1000); // 1 hour
+ const timer = setInterval(() => setNow(new Date()), 1000);
+
+ return () => {
+ clearInterval(fiveMinInterval);
+ clearInterval(dayAheadInterval);
+ clearInterval(timer);
+ };
+ }, [selectedMarket]);
const getChartSpec = (data, title) => ({
type: 'line',
@@ -151,18 +177,15 @@ function MarketDataPage() {
return (
<div style={{ padding: 20, backgroundColor: 'var(--color-fill-2)' }}>
- <Typography.Title heading={4}>Market Data Visualization</Typography.Title>
- <Typography.Text type="secondary" style={{ marginTop: 10, display: 'block' }}>
- Real-Time Data Last Updated: {formatTimeAgo(fiveMinLastUpdated)}
- </Typography.Text>
- <Typography.Text type="secondary" style={{ marginBottom: 20, display: 'block' }}>
- Day-Ahead Data Last Updated: {formatTimeAgo(dayAheadLastUpdated)}
- </Typography.Text>
+ <Typography.Title heading={4}>Market Data for {MARKET_FULL_NAMES[selectedMarket]}</Typography.Title>
+
{loading ? (
<Spin />
) : (
<>
+ <TimeAgo timestamp={fiveMinLastUpdated} prefix="Real-Time Data Last Updated" />
+ <TimeAgo timestamp={dayAheadLastUpdated} prefix="Day-Ahead Data Last Updated" />
<Row gutter={24} style={{ marginTop: 20, marginBottom: 20 }}>
<Col xs={12} md={6} style={{ marginTop: 10}}>
<Card style={{ textAlign: 'center' }} title="Avg Real-Time LMP ($/MWh)">
diff --git a/server/api/market.py b/server/api/market.py
index 5102f03..a1f25ec 100644
--- a/server/api/market.py
+++ b/server/api/market.py
@@ -1,30 +1,45 @@
-from fastapi import APIRouter
+from fastapi import APIRouter, Query, HTTPException
from models.market import MarketData
-from gridstatus import ISONE
+from gridstatus import ISONE, CAISO, Ercot, MISO, NYISO, PJM
from datetime import datetime, timedelta
-from typing import List
+from typing import List, Dict, Type
router = APIRouter()
-# Keeping the scope of this api to just one market right now
-iso = ISONE()
+MARKET_CLASSES: Dict[str, Type] = {
+ "ISONE": ISONE,
+ # "CAISO": CAISO,
+ # "ERCOT": Ercot,
+ "MISO": MISO,
+ "NYISO": NYISO,
+ # "PJM": PJM,
+}
# In-memory cache
-_cached_day_ahead: List[MarketData] = []
-_cache_timestamp: datetime | None = None
-_cached_real_time: List[MarketData] = []
-_cache_real_time_timestamp: datetime | None = None
+_cached_day_ahead: Dict[str, List[MarketData]] = {}
+_cached_day_ahead_timestamp: Dict[str, datetime] = {}
+_cached_real_time: Dict[str, List[MarketData]] = {}
+_cached_real_time_timestamp: Dict[str, datetime] = {}
-# TODO: Error Handling
-@router.get("/day-ahead", response_model=list[MarketData])
-def get_day_ahead_data():
- global _cached_day_ahead, _cache_timestamp
+def get_iso_instance(market: str):
+ market = market.upper()
+ if market not in MARKET_CLASSES:
+ raise HTTPException(status_code=400, detail=f"Unsupported market '{market}'. Supported: {list(MARKET_CLASSES.keys())}")
+ return MARKET_CLASSES[market]()
+@router.get("/day-ahead", response_model=List[MarketData])
+def get_day_ahead_data(market: str = Query("ISONE")):
now = datetime.utcnow()
- if _cache_timestamp is None or now - _cache_timestamp > timedelta(hours=1):
+ market = market.upper()
+
+ if (market not in _cached_day_ahead_timestamp or
+ now - _cached_day_ahead_timestamp[market] > timedelta(hours=1)):
+
+ iso = get_iso_instance(market)
df = iso.get_lmp(date=datetime.now().date(), market="DAY_AHEAD_HOURLY", locations="ALL")
- grouped = (df.groupby("Interval Start")[["LMP", "Energy", "Congestion", "Loss"]].mean().reset_index())
- _cached_day_ahead = [
+ grouped = df.groupby("Interval Start")[["LMP", "Energy", "Congestion", "Loss"]].mean().reset_index()
+
+ _cached_day_ahead[market] = [
MarketData(
timestamp=row["Interval Start"],
lmp=row["LMP"],
@@ -34,19 +49,23 @@ def get_day_ahead_data():
)
for _, row in grouped.iterrows()
]
- _cache_timestamp = now
-
- return _cached_day_ahead
+ _cached_day_ahead_timestamp[market] = now
-@router.get("/real-time", response_model=list[MarketData])
-def get_real_time_data():
- global _cached_real_time, _cache_real_time_timestamp
+ return _cached_day_ahead[market]
+@router.get("/real-time", response_model=List[MarketData])
+def get_real_time_data(market: str = Query("ISONE")):
now = datetime.utcnow()
- if _cache_real_time_timestamp is None or now - _cache_real_time_timestamp > timedelta(minutes=5):
+ market = market.upper()
+
+ if (market not in _cached_real_time_timestamp or
+ now - _cached_real_time_timestamp[market] > timedelta(minutes=5)):
+
+ iso = get_iso_instance(market)
df = iso.get_lmp(date="today", market="REAL_TIME_5_MIN", locations="ALL")
- grouped = (df.groupby("Interval Start")[["LMP", "Energy", "Congestion", "Loss"]].mean().reset_index())
- _cached_real_time = [
+ grouped = df.groupby("Interval Start")[["LMP", "Energy", "Congestion", "Loss"]].mean().reset_index()
+
+ _cached_real_time[market] = [
MarketData(
timestamp=row["Interval Start"],
lmp=row["LMP"],
@@ -56,6 +75,6 @@ def get_real_time_data():
)
for _, row in grouped.iterrows()
]
- _cache_real_time_timestamp = now
+ _cached_real_time_timestamp[market] = now
- return _cached_real_time
+ return _cached_real_time[market]