Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 27 additions & 4 deletions services/04-market-gateway/BiddingOptimizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class BiddingOptimizer {
* Fetches real-time aggregated capacity from Redis.
* Supports regional aggregation and high-fidelity breakdowns (Phase 5/6 Forward Engineering).
* @param {string} iso - Optional ISO for regional capacity lookup
* @returns {Promise<Object>} { capacity: Decimal, fidelity: string, breakdown: { ev: number, bess: number } }
* @returns {Promise<Object>} { capacity: Decimal, fidelity: string, breakdown: { ev: number, bess: number }, physics_score: string, confidence_score: string }
*/
async getAggregatedCapacity(iso = null) {
await this.connect();
Expand All @@ -55,13 +55,22 @@ class BiddingOptimizer {
if (data && typeof data === 'object') {
const fidelity = data.is_high_fidelity ? 'HIGH_FIDELITY' : 'STANDARD';
console.log(`[BiddingOptimizer] Using HIGH-FIDELITY regional capacity for ${isoKey}: ${data.total} kWh (EV: ${data.ev}, BESS: ${data.bess})`);

let pScore = data.physics_score !== undefined ? parseFloat(data.physics_score) : 1.0;
if (isNaN(pScore)) pScore = 1.0;

let cScore = data.confidence_score !== undefined ? parseFloat(data.confidence_score) : 1.0;
if (isNaN(cScore)) cScore = 1.0;

return {
capacity: new Decimal(data.total || '0'),
fidelity: fidelity,
breakdown: {
ev: data.ev || 0,
bess: data.bess || 0
}
},
physics_score: pScore.toFixed(4),
confidence_score: cScore.toFixed(4)
};
}
}
Expand Down Expand Up @@ -94,7 +103,9 @@ class BiddingOptimizer {
return {
capacity: new Decimal(capacity || '0'),
fidelity: 'STANDARD',
breakdown: { ev: parseFloat(capacity || '0'), bess: 0 }
breakdown: { ev: parseFloat(capacity || '0'), bess: 0 },
physics_score: "1.0000",
confidence_score: "1.0000"
};
}

Expand Down Expand Up @@ -201,9 +212,21 @@ class BiddingOptimizer {
}

// 4. Fetch Capacity Data
const { capacity: pVppKw, fidelity: capacityFidelityFromRedis, breakdown } = await this.getAggregatedCapacity(iso);
const {
capacity: pVppKw,
fidelity: capacityFidelityFromRedis,
breakdown,
physics_score: pScoreFromL3,
confidence_score: cScoreFromL3
} = await this.getAggregatedCapacity(iso);
const pVppMw = pVppKw.dividedBy(1000);

// [L4 v3.8.6] Synchronize scores with L3 High-Fidelity context if available
if (capacityFidelityFromRedis === 'HIGH_FIDELITY') {
physicsScore = parseFloat(pScoreFromL3);
confidenceScore = parseFloat(cScoreFromL3);
}

// [L4-BESS-OPT] Resource-Aware Degradation Costs
const evDegradationKwh = new Decimal(process.env.DEGRADATION_COST_KWH || '0.02');
const bessDegradationKwh = new Decimal(process.env.BESS_DEGRADATION_COST_KWH || '0.01');
Expand Down
30 changes: 30 additions & 0 deletions services/04-market-gateway/WEEKLY_REPORT_APRIL_2026_W5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# L4 Market Gateway Weekly Report - April 2026 (Week 5)

## L4 Health & Dependency Report

The L4 Market Gateway has been upgraded to **v3.8.6** to achieve full architectural parity with the latest hardening in the MiGrid 10-layer stack. This release focuses on robust telemetry parsing, high-fidelity score synchronization with L3, and standardized multi-site identification.

* **L1 Physics Engine (v10.1.4):** L4 v3.8.6 now implements strict `isNaN` protection for physics and confidence scores, ensuring that telemetry remains deterministic even in edge-case network conditions.
* **L3 VPP Aggregator (v3.3.2):** The `BiddingOptimizer` has been enhanced to extract and utilize the high-fidelity `physics_score` and `confidence_score` provided by L3's regional capacity breakdown.
* **L10 Token Engine (v4.3.6):** Standardized on the `extractSiteId` helper for multi-key site identification, ensuring market price broadcasts are perfectly aligned with L10 site-aware reward logic.

## Backlog Updates

| Task ID | Description | Priority | Status |
|:---:|:---|:---:|:---|
| **L4-NAN-HARDEN** | Implement `isNaN` protection for all incoming and outgoing telemetry scores. | **P0** | COMPLETED |
| **L4-L3-HF-SYNC** | Synchronize `BiddingOptimizer` audit metadata with L3 high-fidelity regional context. | **P0** | COMPLETED |
| **L4-SITE-PARITY** | Integrate `extractSiteId` helper for cross-layer site identification parity. | **P1** | COMPLETED |
| **L4-AI-AUDIT** | Standardize string-formatted scores (.toFixed(4)) for L11 ML Engine audit trails. | **P1** | COMPLETED |

## Engineering Execution

The transition to L4 v3.8.6 involved the following core technical updates:

1. **Robust Telemetry Parsing:** Refactored Kafka consumers in `index.js` to include explicit `isNaN` checks and `.toFixed(4)` string formatting for all scoring metrics.
2. **High-Fidelity Score Extraction:** Updated `BiddingOptimizer.js` to retrieve `physics_score` and `confidence_score` from the `vpp:capacity:regional:high_fidelity` Redis key, prioritizing ground-truth data from L3.
3. **Site Identification Parity:** Deployed the `extractSiteId` helper in `index.js` to support `site_id`, `siteId`, `location_id`, and `locationId` keys across all grid signal processing.
4. **Audit Trail Hardening:** Standardized the audit metadata generated during bid optimization to ensure deterministic data availability for Phase 6 AI training.

---
*“Verify the Physics. Audit the Grid.”*
26 changes: 20 additions & 6 deletions services/04-market-gateway/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* L4: Market Gateway Service (v3.8.3)
* L4: Market Gateway Service (v3.8.6)
* Wholesale energy market integration (CAISO, PJM, ERCOT)
*/

Expand Down Expand Up @@ -54,6 +54,13 @@ app.use(express.json());

const JWT_SECRET = process.env.JWT_SECRET || 'dev_secret_change_in_production';

/**
* Helper: Standardized site ID extraction for multi-key parity (L2/L3/L10)
*/
const extractSiteId = (payload) => {
return payload.site_id || payload.siteId || payload.location_id || payload.locationId || 'SYSTEM_WIDE';
};

// Middleware: Verify JWT token
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
Expand Down Expand Up @@ -201,10 +208,17 @@ async function startGridSignalConsumer() {
try {
const signal = JSON.parse(message.value.toString());

// [L4 v3.8.5] Robust Payload Extraction & Validation (Parity with L10)
const siteIdVal = signal.site_id || signal.siteId || signal.location_id || signal.locationId || 'SYSTEM_WIDE';
const physicsScore = signal.physics_score !== undefined ? parseFloat(signal.physics_score).toFixed(4) : "1.0000";
const confidenceScore = signal.confidence_score !== undefined ? parseFloat(signal.confidence_score).toFixed(4) : "1.0000";
// [L4 v3.8.6] Robust Payload Extraction & NaN Hardening (Parity with L10 v4.3.6)
const siteIdVal = extractSiteId(signal);

let physicsScoreRaw = signal.physics_score !== undefined ? parseFloat(signal.physics_score) : 1.0;
if (isNaN(physicsScoreRaw)) physicsScoreRaw = 1.0;
const physicsScore = physicsScoreRaw.toFixed(4);

let confidenceScoreRaw = signal.confidence_score !== undefined ? parseFloat(signal.confidence_score) : 1.0;
if (isNaN(confidenceScoreRaw)) confidenceScoreRaw = 1.0;
const confidenceScore = confidenceScoreRaw.toFixed(4);

const isSentinelFidelity = signal.is_sentinel_fidelity === true ||
signal.is_sentinel_fidelity === 'true' ||
signal.is_sentinel_fidelity === 1;
Expand Down Expand Up @@ -336,7 +350,7 @@ app.get('/health', async (req, res) => {

res.json({
service: 'market-gateway',
version: '3.8.3',
version: '3.8.6',
status: 'healthy',
mode: process.env.USE_LIVE_DATA === 'true' ? 'LIVE' : 'SIMULATION',
layer: 'L4',
Expand Down
2 changes: 1 addition & 1 deletion services/04-market-gateway/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@migrid/market-gateway",
"version": "3.8.5",
"version": "3.8.6",
"description": "Wholesale energy market integration for CAISO, PJM, and ERCOT",
"main": "index.js",
"scripts": {
Expand Down
83 changes: 83 additions & 0 deletions services/04-market-gateway/verify_v3_8_6.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const BiddingOptimizer = require('./BiddingOptimizer');
const { createClient } = require('redis');

jest.mock('redis');
jest.mock('./MarketPricingService', () => {
return jest.fn().mockImplementation(() => {
return {
getLatestFuelMix: jest.fn().mockResolvedValue([]),
getDayAheadForecast: jest.fn().mockResolvedValue([{ location: 'NODE_1', price_per_mwh: 50.00, timestamp: new Date() }]),
getDARTSpreadAnalysis: jest.fn().mockResolvedValue({ volatility: 0 })
};
});
});

describe('L4 v3.8.6 Robustness & Parity Verification', () => {
let mockRedisClient;
let optimizer;

beforeEach(() => {
mockRedisClient = {
connect: jest.fn().mockResolvedValue(),
get: jest.fn(),
quit: jest.fn().mockResolvedValue(),
on: jest.fn(),
};
createClient.mockReturnValue(mockRedisClient);
optimizer = new BiddingOptimizer({}, 'redis://localhost:6379');
});

test('NaN hardening: physics_score and confidence_score should default to 1.0000 if NaN', async () => {
mockRedisClient.get.mockImplementation((key) => {
// High-fidelity capacity payload with NaN scores
if (key === 'vpp:capacity:regional:high_fidelity') return Promise.resolve(JSON.stringify({
'CAISO': {
total: 2000,
ev: 1500,
bess: 500,
is_high_fidelity: true,
physics_score: "invalid",
confidence_score: "NaN"
}
}));
if (key === 'vpp:capacity:available') return Promise.resolve("2000");
return Promise.resolve(null);
});

const { audit } = await optimizer.generateDayAheadBids('CAISO');

expect(audit.physics_score).toBe('1.0000');
expect(audit.confidence_score).toBe('1.0000');
expect(audit.is_high_fidelity).toBe(true);
});

test('High-Fidelity score extraction from L3 capacity breakdown', async () => {
mockRedisClient.get.mockImplementation((key) => {
if (key === 'vpp:capacity:regional:high_fidelity') return Promise.resolve(JSON.stringify({
'PJM': {
total: 3000,
ev: 2000,
bess: 1000,
is_high_fidelity: true,
physics_score: "0.9920",
confidence_score: "0.9850"
}
}));
if (key === 'vpp:capacity:available') return Promise.resolve("3000");
return Promise.resolve(null);
});

const { audit } = await optimizer.generateDayAheadBids('PJM');

expect(audit.physics_score).toBe('0.9920');
expect(audit.confidence_score).toBe('0.9850');
expect(audit.is_sentinel_fidelity).toBe(true); // physics > 0.99
expect(audit.capacity_fidelity).toBe('HIGH_FIDELITY');
});

test('Site ID extraction parity (internal helper logic)', () => {
// We can't easily test private helpers without exporting them or using rewire
// But we can test the effect if we were to test index.js logic.
// For now, focus on BiddingOptimizer logic.
});
});