97 lines
2.6 KiB
JavaScript
97 lines
2.6 KiB
JavaScript
const axios = require('axios');
|
|
|
|
const EVOLUTION_SERVICE_URL = process.env.EVOLUTION_SERVICE_URL || 'http://localhost:5001';
|
|
const EVOLUTION_TIMEOUT = 5000;
|
|
|
|
const TRACKED_SIGNALS = [
|
|
'usage_rate',
|
|
'assist_rate',
|
|
'three_pt_attempt_rate',
|
|
'shot_location',
|
|
'aggression_score',
|
|
'minutes_trajectory',
|
|
];
|
|
|
|
/**
|
|
* Detect changepoints in a player metric time series via Python PELT microservice.
|
|
* @param {string} playerId
|
|
* @param {string} metric - Metric name
|
|
* @param {Array<number>} values - Time series values
|
|
* @param {Array<string>} timestamps - ISO timestamps
|
|
* @returns {object} Changepoint result or graceful degradation
|
|
*/
|
|
async function detectChangepoints(playerId, metric, values, timestamps) {
|
|
try {
|
|
const response = await axios.post(
|
|
`${EVOLUTION_SERVICE_URL}/detect`,
|
|
{ player_id: playerId, metric, values, timestamps },
|
|
{ timeout: EVOLUTION_TIMEOUT }
|
|
);
|
|
return response.data;
|
|
} catch (error) {
|
|
const reason = error.code === 'ECONNABORTED'
|
|
? 'timeout'
|
|
: error.response
|
|
? `HTTP ${error.response.status}`
|
|
: error.message;
|
|
return {
|
|
evolution_detected: false,
|
|
error: reason,
|
|
playerId,
|
|
metric,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if multiple signals are inflecting simultaneously.
|
|
* If 2+ signals inflecting above 70% confidence, evolution is detected.
|
|
* @param {object} playerMetrics - { signal_name: { values, timestamps, confidence } }
|
|
* @returns {object} { evolution_detected, confidence, signals }
|
|
*/
|
|
async function checkMultiSignalEvolution(playerMetrics) {
|
|
const results = [];
|
|
|
|
for (const signal of TRACKED_SIGNALS) {
|
|
if (playerMetrics[signal]) {
|
|
const { values, timestamps } = playerMetrics[signal];
|
|
const result = await detectChangepoints(
|
|
playerMetrics.playerId,
|
|
signal,
|
|
values,
|
|
timestamps
|
|
);
|
|
if (result && !result.error) {
|
|
results.push({ signal, ...result });
|
|
}
|
|
}
|
|
}
|
|
|
|
const inflecting = results.filter(r => r.confidence >= 0.70);
|
|
|
|
if (inflecting.length >= 2) {
|
|
const avgConfidence = inflecting.reduce((s, r) => s + r.confidence, 0) / inflecting.length;
|
|
return {
|
|
evolution_detected: true,
|
|
confidence: Math.round(avgConfidence * 100) / 100,
|
|
signals: inflecting.map(r => r.signal),
|
|
details: inflecting,
|
|
};
|
|
}
|
|
|
|
return {
|
|
evolution_detected: false,
|
|
confidence: 0,
|
|
signals: [],
|
|
checked: TRACKED_SIGNALS.filter(s => playerMetrics[s]),
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
EVOLUTION_SERVICE_URL,
|
|
EVOLUTION_TIMEOUT,
|
|
TRACKED_SIGNALS,
|
|
detectChangepoints,
|
|
checkMultiSignalEvolution,
|
|
};
|