Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Walk-forward validation: time-stratified only, no look-ahead bias.
|
||||
* @param {Array<{predicted: number, timestamp: string}>} predictions
|
||||
* @param {Array<{actual: number, timestamp: string}>} actuals
|
||||
* @returns {object} Accuracy metrics
|
||||
*/
|
||||
function walkForwardValidate(predictions, actuals) {
|
||||
if (!predictions || !actuals || predictions.length === 0 || actuals.length === 0) {
|
||||
return { accuracy: 0, mae: 0, rmse: 0, n: 0, hit_rate: 0 };
|
||||
}
|
||||
|
||||
const paired = predictions.map((pred, i) => {
|
||||
const actual = actuals[i];
|
||||
if (!actual) return null;
|
||||
return { predicted: pred.predicted, actual: actual.actual, timestamp: pred.timestamp };
|
||||
}).filter(Boolean);
|
||||
|
||||
// Sort by timestamp to enforce time-stratification
|
||||
paired.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
||||
|
||||
const n = paired.length;
|
||||
if (n === 0) return { accuracy: 0, mae: 0, rmse: 0, n: 0, hit_rate: 0 };
|
||||
|
||||
let totalError = 0;
|
||||
let totalSquaredError = 0;
|
||||
let hits = 0;
|
||||
|
||||
for (const p of paired) {
|
||||
const error = Math.abs(p.predicted - p.actual);
|
||||
totalError += error;
|
||||
totalSquaredError += error * error;
|
||||
// Hit = within 10% of actual or within 1 unit
|
||||
if (error <= Math.max(Math.abs(p.actual) * 0.1, 1)) hits++;
|
||||
}
|
||||
|
||||
return {
|
||||
accuracy: Math.round((hits / n) * 1000) / 1000,
|
||||
mae: Math.round((totalError / n) * 100) / 100,
|
||||
rmse: Math.round(Math.sqrt(totalSquaredError / n) * 100) / 100,
|
||||
n,
|
||||
hit_rate: Math.round((hits / n) * 1000) / 1000,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Closing Line Value at multiple checkpoints.
|
||||
* @param {number} predictionLine - Our predicted line at time of prediction
|
||||
* @param {number} lineAt24h - Market line 24 hours before tip
|
||||
* @param {number} lineAtTip - Market line at tip-off
|
||||
* @returns {object} { clv_at_prediction, clv_at_24hr, clv_at_tip }
|
||||
*/
|
||||
function calculateCLV(predictionLine, lineAt24h, lineAtTip) {
|
||||
return {
|
||||
clv_at_prediction: Math.round((lineAtTip - predictionLine) * 100) / 100,
|
||||
clv_at_24hr: Math.round((lineAtTip - lineAt24h) * 100) / 100,
|
||||
clv_at_tip: 0, // By definition, CLV at tip is 0 (reference point)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for model drift: 10 consecutive CLV below 0 triggers alert.
|
||||
* @param {Array<number>} clvHistory - Array of CLV values, most recent last
|
||||
* @returns {object} { drift_detected, consecutive_negative, alert }
|
||||
*/
|
||||
function checkDrift(clvHistory) {
|
||||
if (!clvHistory || clvHistory.length === 0) {
|
||||
return { drift_detected: false, consecutive_negative: 0, alert: false };
|
||||
}
|
||||
|
||||
let consecutiveNeg = 0;
|
||||
// Count from the end
|
||||
for (let i = clvHistory.length - 1; i >= 0; i--) {
|
||||
if (clvHistory[i] < 0) {
|
||||
consecutiveNeg++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
drift_detected: consecutiveNeg >= 10,
|
||||
consecutive_negative: consecutiveNeg,
|
||||
alert: consecutiveNeg >= 10,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cap weight changes to prevent overfitting.
|
||||
* @param {number} currentWeight
|
||||
* @param {number} proposedWeight
|
||||
* @param {number} maxDelta - Maximum allowed change per cycle (default 0.05)
|
||||
* @returns {number} Capped weight
|
||||
*/
|
||||
function applyLearningRateCap(currentWeight, proposedWeight, maxDelta = 0.05) {
|
||||
const delta = proposedWeight - currentWeight;
|
||||
const clampedDelta = Math.max(-maxDelta, Math.min(maxDelta, delta));
|
||||
return Math.round((currentWeight + clampedDelta) * 10000) / 10000;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
walkForwardValidate,
|
||||
calculateCLV,
|
||||
checkDrift,
|
||||
applyLearningRateCap,
|
||||
};
|
||||
Reference in New Issue
Block a user