Spaces:
Running
Running
fix dashboard bugs and add resolution progress
Browse files- api/dashboard.py +171 -1
- database/init_db.py +16 -0
- database/job_runs_repository.py +21 -0
- database/metrics_repository.py +5 -4
- database/resolution_repository.py +39 -18
- database/schema.sql +4 -0
- jobs/resolve_predictions.py +16 -3
api/dashboard.py
CHANGED
|
@@ -14,6 +14,71 @@ from evaluation.metrics import calculate_metrics
|
|
| 14 |
|
| 15 |
router = APIRouter(tags=["dashboard"])
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
PREDICTION_COLUMNS = """
|
| 18 |
id,
|
| 19 |
prediction_date,
|
|
@@ -1122,6 +1187,45 @@ a:hover {{
|
|
| 1122 |
font-weight: 650;
|
| 1123 |
}}
|
| 1124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1125 |
.table-card {{
|
| 1126 |
background: var(--surface-1);
|
| 1127 |
border: 1px solid var(--border);
|
|
@@ -1236,6 +1340,21 @@ Predictions, five-session outcomes, calibration,
|
|
| 1236 |
and currently unresolved signals.
|
| 1237 |
</p>
|
| 1238 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1239 |
{metrics_html}
|
| 1240 |
|
| 1241 |
<h2>
|
|
@@ -1263,7 +1382,58 @@ to a higher probability of hitting the target?
|
|
| 1263 |
|
| 1264 |
{pending_table}
|
| 1265 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1266 |
</body>
|
| 1267 |
</html>
|
| 1268 |
"""
|
| 1269 |
-
)
|
|
|
|
| 14 |
|
| 15 |
router = APIRouter(tags=["dashboard"])
|
| 16 |
|
| 17 |
+
|
| 18 |
+
@router.get("/resolution-status")
|
| 19 |
+
def resolution_status():
|
| 20 |
+
"""Return the latest durable resolution progress for dashboard polling."""
|
| 21 |
+
with connect(settings.DATABASE_URL) as conn, conn.cursor() as cur:
|
| 22 |
+
cur.execute(
|
| 23 |
+
"""
|
| 24 |
+
SELECT COUNT(*) AS total,
|
| 25 |
+
SUM(CASE WHEN resolved_at IS NOT NULL THEN 1 ELSE 0 END) AS resolved,
|
| 26 |
+
SUM(CASE WHEN resolved_at IS NULL THEN 1 ELSE 0 END) AS pending
|
| 27 |
+
FROM predictions
|
| 28 |
+
"""
|
| 29 |
+
)
|
| 30 |
+
counts = cur.fetchone()
|
| 31 |
+
cur.execute(
|
| 32 |
+
"""
|
| 33 |
+
SELECT status, started_at, finished_at, last_updated_at,
|
| 34 |
+
total_rows, rows_processed, resolved_rows, failed_rows
|
| 35 |
+
FROM job_runs
|
| 36 |
+
WHERE job_name = 'resolution'
|
| 37 |
+
ORDER BY id DESC
|
| 38 |
+
LIMIT 1
|
| 39 |
+
"""
|
| 40 |
+
)
|
| 41 |
+
run = cur.fetchone()
|
| 42 |
+
cur.execute(
|
| 43 |
+
"""
|
| 44 |
+
SELECT MAX(value) AS revision FROM (
|
| 45 |
+
SELECT MAX(prediction_timestamp) AS value FROM predictions
|
| 46 |
+
UNION ALL SELECT MAX(resolved_at) FROM predictions
|
| 47 |
+
UNION ALL SELECT MAX(calculated_at) FROM prediction_metrics
|
| 48 |
+
UNION ALL SELECT MAX(COALESCE(last_updated_at, finished_at, started_at)) FROM job_runs
|
| 49 |
+
)
|
| 50 |
+
"""
|
| 51 |
+
)
|
| 52 |
+
revision = cur.fetchone()["revision"]
|
| 53 |
+
|
| 54 |
+
total = int(counts["total"] or 0)
|
| 55 |
+
resolved = int(counts["resolved"] or 0)
|
| 56 |
+
pending = int(counts["pending"] or 0)
|
| 57 |
+
if run is None:
|
| 58 |
+
return {
|
| 59 |
+
"status": "IDLE",
|
| 60 |
+
"processed": 0,
|
| 61 |
+
"total": total,
|
| 62 |
+
"resolved": resolved,
|
| 63 |
+
"pending": pending,
|
| 64 |
+
"failed": 0,
|
| 65 |
+
"started_at": None,
|
| 66 |
+
"last_updated_at": None,
|
| 67 |
+
"revision": revision,
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
return {
|
| 71 |
+
"status": run["status"],
|
| 72 |
+
"processed": int(run["rows_processed"] or 0),
|
| 73 |
+
"total": int(run["total_rows"] or total),
|
| 74 |
+
"resolved": resolved,
|
| 75 |
+
"pending": pending,
|
| 76 |
+
"failed": int(run["failed_rows"] or 0),
|
| 77 |
+
"started_at": run["started_at"],
|
| 78 |
+
"last_updated_at": run["last_updated_at"] or run["finished_at"] or run["started_at"],
|
| 79 |
+
"revision": revision,
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
PREDICTION_COLUMNS = """
|
| 83 |
id,
|
| 84 |
prediction_date,
|
|
|
|
| 1187 |
font-weight: 650;
|
| 1188 |
}}
|
| 1189 |
|
| 1190 |
+
.resolution-card {{
|
| 1191 |
+
background: var(--surface-1);
|
| 1192 |
+
border: 1px solid var(--border);
|
| 1193 |
+
border-radius: 8px;
|
| 1194 |
+
padding: 1rem;
|
| 1195 |
+
margin-bottom: 1.5rem;
|
| 1196 |
+
max-width: 520px;
|
| 1197 |
+
}}
|
| 1198 |
+
|
| 1199 |
+
.resolution-status {{
|
| 1200 |
+
color: var(--series-1);
|
| 1201 |
+
font-size: 1.15rem;
|
| 1202 |
+
font-weight: 700;
|
| 1203 |
+
letter-spacing: .04em;
|
| 1204 |
+
}}
|
| 1205 |
+
|
| 1206 |
+
.resolution-progress {{
|
| 1207 |
+
height: 10px;
|
| 1208 |
+
margin: .75rem 0;
|
| 1209 |
+
border-radius: 99px;
|
| 1210 |
+
overflow: hidden;
|
| 1211 |
+
background: rgba(128,128,128,.18);
|
| 1212 |
+
}}
|
| 1213 |
+
|
| 1214 |
+
.resolution-progress > div {{
|
| 1215 |
+
height: 100%;
|
| 1216 |
+
width: 0;
|
| 1217 |
+
background: var(--series-1);
|
| 1218 |
+
transition: width .25s ease;
|
| 1219 |
+
}}
|
| 1220 |
+
|
| 1221 |
+
.resolution-details {{
|
| 1222 |
+
display: grid;
|
| 1223 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 1224 |
+
gap: .35rem 1rem;
|
| 1225 |
+
color: var(--text-secondary);
|
| 1226 |
+
font-variant-numeric: tabular-nums;
|
| 1227 |
+
}}
|
| 1228 |
+
|
| 1229 |
.table-card {{
|
| 1230 |
background: var(--surface-1);
|
| 1231 |
border: 1px solid var(--border);
|
|
|
|
| 1340 |
and currently unresolved signals.
|
| 1341 |
</p>
|
| 1342 |
|
| 1343 |
+
<section class="resolution-card" aria-live="polite">
|
| 1344 |
+
<h2>Prediction resolution</h2>
|
| 1345 |
+
<div class="resolution-status" id="resolution-status">Loading</div>
|
| 1346 |
+
<div id="resolution-count">0 / 0 processed</div>
|
| 1347 |
+
<div class="resolution-progress"><div id="resolution-progress-bar"></div></div>
|
| 1348 |
+
<div id="resolution-percent">0.00%</div>
|
| 1349 |
+
<div class="resolution-details">
|
| 1350 |
+
<div>Resolved <strong id="resolution-resolved">0</strong></div>
|
| 1351 |
+
<div>Pending <strong id="resolution-pending">0</strong></div>
|
| 1352 |
+
<div>Failed <strong id="resolution-failed">0</strong></div>
|
| 1353 |
+
<div>Started <strong id="resolution-started">—</strong></div>
|
| 1354 |
+
<div>Last update <strong id="resolution-updated">—</strong></div>
|
| 1355 |
+
</div>
|
| 1356 |
+
</section>
|
| 1357 |
+
|
| 1358 |
{metrics_html}
|
| 1359 |
|
| 1360 |
<h2>
|
|
|
|
| 1382 |
|
| 1383 |
{pending_table}
|
| 1384 |
|
| 1385 |
+
<script>
|
| 1386 |
+
let dashboardRevision = null;
|
| 1387 |
+
|
| 1388 |
+
function formatResolutionTime(value) {{
|
| 1389 |
+
if (!value) return "—";
|
| 1390 |
+
return new Intl.DateTimeFormat("en-IN", {{
|
| 1391 |
+
timeZone: "Asia/Kolkata",
|
| 1392 |
+
hour: "2-digit",
|
| 1393 |
+
minute: "2-digit",
|
| 1394 |
+
second: "2-digit",
|
| 1395 |
+
hour12: false,
|
| 1396 |
+
}}).format(new Date(value));
|
| 1397 |
+
}}
|
| 1398 |
+
|
| 1399 |
+
function setResolutionText(id, value) {{
|
| 1400 |
+
document.getElementById(id).textContent = value;
|
| 1401 |
+
}}
|
| 1402 |
+
|
| 1403 |
+
async function refreshDashboardState() {{
|
| 1404 |
+
try {{
|
| 1405 |
+
const response = await fetch("/resolution-status", {{ cache: "no-store" }});
|
| 1406 |
+
if (!response.ok) return;
|
| 1407 |
+
const state = await response.json();
|
| 1408 |
+
const total = Math.max(Number(state.total) || 0, 0);
|
| 1409 |
+
const processed = Math.min(Math.max(Number(state.processed) || 0, 0), total);
|
| 1410 |
+
const percent = total ? (processed / total) * 100 : 0;
|
| 1411 |
+
|
| 1412 |
+
setResolutionText("resolution-status", state.status);
|
| 1413 |
+
setResolutionText("resolution-count", `${{processed.toLocaleString()}} / ${{total.toLocaleString()}} processed`);
|
| 1414 |
+
setResolutionText("resolution-percent", `${{percent.toFixed(2)}}%`);
|
| 1415 |
+
setResolutionText("resolution-resolved", Number(state.resolved || 0).toLocaleString());
|
| 1416 |
+
setResolutionText("resolution-pending", Number(state.pending || 0).toLocaleString());
|
| 1417 |
+
setResolutionText("resolution-failed", Number(state.failed || 0).toLocaleString());
|
| 1418 |
+
setResolutionText("resolution-started", formatResolutionTime(state.started_at));
|
| 1419 |
+
setResolutionText("resolution-updated", formatResolutionTime(state.last_updated_at));
|
| 1420 |
+
document.getElementById("resolution-progress-bar").style.width = `${{percent}}%`;
|
| 1421 |
+
|
| 1422 |
+
if (dashboardRevision !== null && state.revision && state.revision !== dashboardRevision) {{
|
| 1423 |
+
window.location.reload();
|
| 1424 |
+
return;
|
| 1425 |
+
}}
|
| 1426 |
+
dashboardRevision = state.revision;
|
| 1427 |
+
}} catch (_) {{
|
| 1428 |
+
// Keep the last rendered data visible while a transient poll fails.
|
| 1429 |
+
}}
|
| 1430 |
+
}}
|
| 1431 |
+
|
| 1432 |
+
refreshDashboardState();
|
| 1433 |
+
setInterval(refreshDashboardState, 3000);
|
| 1434 |
+
</script>
|
| 1435 |
+
|
| 1436 |
</body>
|
| 1437 |
</html>
|
| 1438 |
"""
|
| 1439 |
+
)
|
database/init_db.py
CHANGED
|
@@ -13,6 +13,21 @@ from database.connection import connect
|
|
| 13 |
|
| 14 |
SCHEMA_PATH = Path(__file__).parent / "schema.sql"
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
def init_db(database_url: str | None = None) -> None:
|
| 18 |
from app.config import settings
|
|
@@ -20,6 +35,7 @@ def init_db(database_url: str | None = None) -> None:
|
|
| 20 |
conn = connect(database_url or settings.DATABASE_URL)
|
| 21 |
try:
|
| 22 |
conn.executescript(SCHEMA_PATH.read_text())
|
|
|
|
| 23 |
conn.commit()
|
| 24 |
finally:
|
| 25 |
conn.close()
|
|
|
|
| 13 |
|
| 14 |
SCHEMA_PATH = Path(__file__).parent / "schema.sql"
|
| 15 |
|
| 16 |
+
JOB_RUN_PROGRESS_COLUMNS = {
|
| 17 |
+
"total_rows": "INTEGER NOT NULL DEFAULT 0",
|
| 18 |
+
"resolved_rows": "INTEGER NOT NULL DEFAULT 0",
|
| 19 |
+
"failed_rows": "INTEGER NOT NULL DEFAULT 0",
|
| 20 |
+
"last_updated_at": "TIMESTAMP",
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _migrate_job_runs(conn) -> None:
|
| 25 |
+
"""Add progress columns for databases created before resolution tracking."""
|
| 26 |
+
existing = {row["name"] for row in conn.execute("PRAGMA table_info(job_runs)")}
|
| 27 |
+
for name, definition in JOB_RUN_PROGRESS_COLUMNS.items():
|
| 28 |
+
if name not in existing:
|
| 29 |
+
conn.execute(f"ALTER TABLE job_runs ADD COLUMN {name} {definition}")
|
| 30 |
+
|
| 31 |
|
| 32 |
def init_db(database_url: str | None = None) -> None:
|
| 33 |
from app.config import settings
|
|
|
|
| 35 |
conn = connect(database_url or settings.DATABASE_URL)
|
| 36 |
try:
|
| 37 |
conn.executescript(SCHEMA_PATH.read_text())
|
| 38 |
+
_migrate_job_runs(conn)
|
| 39 |
conn.commit()
|
| 40 |
finally:
|
| 41 |
conn.close()
|
database/job_runs_repository.py
CHANGED
|
@@ -22,3 +22,24 @@ def finish_job_run(database_url: str, job_run_id: int, *, success: bool, error_m
|
|
| 22 |
"UPDATE job_runs SET status = ?, finished_at = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now'), error_message = ? WHERE id = ?",
|
| 23 |
("SUCCESS" if success else "FAILED", error_message, job_run_id),
|
| 24 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
"UPDATE job_runs SET status = ?, finished_at = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now'), error_message = ? WHERE id = ?",
|
| 23 |
("SUCCESS" if success else "FAILED", error_message, job_run_id),
|
| 24 |
)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def update_job_progress(
|
| 28 |
+
database_url: str,
|
| 29 |
+
job_run_id: int,
|
| 30 |
+
*,
|
| 31 |
+
total_rows: int,
|
| 32 |
+
rows_processed: int,
|
| 33 |
+
resolved_rows: int,
|
| 34 |
+
failed_rows: int,
|
| 35 |
+
) -> None:
|
| 36 |
+
with connect(database_url) as conn, conn.cursor() as cur:
|
| 37 |
+
cur.execute(
|
| 38 |
+
"""
|
| 39 |
+
UPDATE job_runs
|
| 40 |
+
SET total_rows = ?, rows_processed = ?, resolved_rows = ?,
|
| 41 |
+
failed_rows = ?, last_updated_at = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now')
|
| 42 |
+
WHERE id = ?
|
| 43 |
+
""",
|
| 44 |
+
(total_rows, rows_processed, resolved_rows, failed_rows, job_run_id),
|
| 45 |
+
)
|
database/metrics_repository.py
CHANGED
|
@@ -11,8 +11,9 @@ def get_database_url() -> str:
|
|
| 11 |
return value
|
| 12 |
|
| 13 |
|
| 14 |
-
def fetch_resolved_rows(conn, days: int = 20):
|
| 15 |
-
|
|
|
|
| 16 |
WITH latest_model AS (
|
| 17 |
SELECT model_version FROM predictions
|
| 18 |
WHERE resolved_at IS NOT NULL
|
|
@@ -20,7 +21,7 @@ def fetch_resolved_rows(conn, days: int = 20):
|
|
| 20 |
), latest_dates AS (
|
| 21 |
SELECT prediction_date FROM predictions p JOIN latest_model m USING (model_version)
|
| 22 |
WHERE p.actual_label IS NOT NULL AND p.actual_return IS NOT NULL
|
| 23 |
-
GROUP BY prediction_date ORDER BY prediction_date DESC
|
| 24 |
)
|
| 25 |
SELECT p.prediction_date, p.symbol, p.model_version, p.predicted_probability,
|
| 26 |
p.rank, p.actual_return, p.actual_label
|
|
@@ -30,7 +31,7 @@ def fetch_resolved_rows(conn, days: int = 20):
|
|
| 30 |
ORDER BY p.prediction_date, p.predicted_probability DESC, p.symbol
|
| 31 |
"""
|
| 32 |
with conn.cursor() as cur:
|
| 33 |
-
cur.execute(query, {"days": days})
|
| 34 |
return cur.fetchall()
|
| 35 |
|
| 36 |
|
|
|
|
| 11 |
return value
|
| 12 |
|
| 13 |
|
| 14 |
+
def fetch_resolved_rows(conn, days: int | None = 20):
|
| 15 |
+
limit_clause = "" if days is None else "LIMIT :days"
|
| 16 |
+
query = f"""
|
| 17 |
WITH latest_model AS (
|
| 18 |
SELECT model_version FROM predictions
|
| 19 |
WHERE resolved_at IS NOT NULL
|
|
|
|
| 21 |
), latest_dates AS (
|
| 22 |
SELECT prediction_date FROM predictions p JOIN latest_model m USING (model_version)
|
| 23 |
WHERE p.actual_label IS NOT NULL AND p.actual_return IS NOT NULL
|
| 24 |
+
GROUP BY prediction_date ORDER BY prediction_date DESC {limit_clause}
|
| 25 |
)
|
| 26 |
SELECT p.prediction_date, p.symbol, p.model_version, p.predicted_probability,
|
| 27 |
p.rank, p.actual_return, p.actual_label
|
|
|
|
| 31 |
ORDER BY p.prediction_date, p.predicted_probability DESC, p.symbol
|
| 32 |
"""
|
| 33 |
with conn.cursor() as cur:
|
| 34 |
+
cur.execute(query, {} if days is None else {"days": days})
|
| 35 |
return cur.fetchall()
|
| 36 |
|
| 37 |
|
database/resolution_repository.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
from datetime import date, datetime, timezone
|
| 6 |
-
from typing import Any
|
| 7 |
|
| 8 |
from jobs.resolver import resolve_prediction_rows
|
| 9 |
|
|
@@ -28,25 +28,46 @@ def fetch_future_ohlcv(conn, symbol: str, prediction_date: date, forward_days: i
|
|
| 28 |
return cur.fetchall()
|
| 29 |
|
| 30 |
|
| 31 |
-
def resolve_pending_predictions(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
resolved_count = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
for prediction in fetch_unresolved_predictions(conn):
|
| 34 |
rows = fetch_future_ohlcv(conn, prediction["symbol"], prediction["prediction_date"], prediction["target_horizon_days"])
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
conn.commit()
|
|
|
|
|
|
|
| 52 |
return resolved_count
|
|
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
from datetime import date, datetime, timezone
|
| 6 |
+
from typing import Any, Callable
|
| 7 |
|
| 8 |
from jobs.resolver import resolve_prediction_rows
|
| 9 |
|
|
|
|
| 28 |
return cur.fetchall()
|
| 29 |
|
| 30 |
|
| 31 |
+
def resolve_pending_predictions(
|
| 32 |
+
conn,
|
| 33 |
+
*,
|
| 34 |
+
on_progress: Callable[[int, int, int], None] | None = None,
|
| 35 |
+
commit_interval: int = 25,
|
| 36 |
+
) -> int:
|
| 37 |
resolved_count = 0
|
| 38 |
+
failed_count = 0
|
| 39 |
+
processed_count = 0
|
| 40 |
+
|
| 41 |
+
def checkpoint() -> None:
|
| 42 |
+
if processed_count % commit_interval == 0:
|
| 43 |
+
conn.commit()
|
| 44 |
+
if on_progress is not None:
|
| 45 |
+
on_progress(processed_count, resolved_count, failed_count)
|
| 46 |
+
|
| 47 |
for prediction in fetch_unresolved_predictions(conn):
|
| 48 |
rows = fetch_future_ohlcv(conn, prediction["symbol"], prediction["prediction_date"], prediction["target_horizon_days"])
|
| 49 |
+
try:
|
| 50 |
+
resolution = resolve_prediction_rows(
|
| 51 |
+
prediction_date=prediction["prediction_date"], symbol=prediction["symbol"],
|
| 52 |
+
future_rows=[{"date": row["timestamp"], "open": row["open"], "close": row["close"]} for row in rows],
|
| 53 |
+
target_return=prediction["target_threshold"], forward_days=prediction["target_horizon_days"],
|
| 54 |
+
)
|
| 55 |
+
except ValueError:
|
| 56 |
+
failed_count += 1
|
| 57 |
+
else:
|
| 58 |
+
if resolution is not None:
|
| 59 |
+
with conn.cursor() as cur:
|
| 60 |
+
cur.execute("""
|
| 61 |
+
UPDATE predictions SET entry_date = ?, entry_open = ?, max_close_5d = ?,
|
| 62 |
+
evaluation_end_date = ?, actual_return = ?, actual_label = ?, resolved_at = ?
|
| 63 |
+
WHERE id = ? AND resolved_at IS NULL
|
| 64 |
+
""", (resolution.entry_date, resolution.entry_open, resolution.max_close,
|
| 65 |
+
resolution.evaluation_dates[-1], resolution.actual_return, resolution.actual_label,
|
| 66 |
+
datetime.now(timezone.utc), prediction["id"]))
|
| 67 |
+
resolved_count += cur.rowcount
|
| 68 |
+
processed_count += 1
|
| 69 |
+
checkpoint()
|
| 70 |
conn.commit()
|
| 71 |
+
if on_progress is not None:
|
| 72 |
+
on_progress(processed_count, resolved_count, failed_count)
|
| 73 |
return resolved_count
|
database/schema.sql
CHANGED
|
@@ -121,6 +121,10 @@ CREATE TABLE IF NOT EXISTS job_runs (
|
|
| 121 |
),
|
| 122 |
|
| 123 |
rows_processed INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
error_message TEXT
|
| 126 |
);
|
|
|
|
| 121 |
),
|
| 122 |
|
| 123 |
rows_processed INTEGER NOT NULL DEFAULT 0,
|
| 124 |
+
total_rows INTEGER NOT NULL DEFAULT 0,
|
| 125 |
+
resolved_rows INTEGER NOT NULL DEFAULT 0,
|
| 126 |
+
failed_rows INTEGER NOT NULL DEFAULT 0,
|
| 127 |
+
last_updated_at TIMESTAMP,
|
| 128 |
|
| 129 |
error_message TEXT
|
| 130 |
);
|
jobs/resolve_predictions.py
CHANGED
|
@@ -11,8 +11,8 @@ from dotenv import load_dotenv
|
|
| 11 |
from app.config import settings
|
| 12 |
from database.connection import connect
|
| 13 |
from database.job_lock_repository import acquire_job_lock, release_job_lock
|
| 14 |
-
from database.job_runs_repository import finish_job_run, start_job_run
|
| 15 |
-
from database.resolution_repository import resolve_pending_predictions
|
| 16 |
|
| 17 |
load_dotenv()
|
| 18 |
|
|
@@ -35,7 +35,20 @@ def run_resolution_job() -> int | None:
|
|
| 35 |
settings.DATABASE_URL, "resolution", datetime.now(IST).date()
|
| 36 |
)
|
| 37 |
with connect(settings.DATABASE_URL) as conn:
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
finish_job_run(settings.DATABASE_URL, job_run_id, success=True)
|
| 40 |
log.info("Resolution complete: %s predictions resolved", resolved)
|
| 41 |
return resolved
|
|
|
|
| 11 |
from app.config import settings
|
| 12 |
from database.connection import connect
|
| 13 |
from database.job_lock_repository import acquire_job_lock, release_job_lock
|
| 14 |
+
from database.job_runs_repository import finish_job_run, start_job_run, update_job_progress
|
| 15 |
+
from database.resolution_repository import fetch_unresolved_predictions, resolve_pending_predictions
|
| 16 |
|
| 17 |
load_dotenv()
|
| 18 |
|
|
|
|
| 35 |
settings.DATABASE_URL, "resolution", datetime.now(IST).date()
|
| 36 |
)
|
| 37 |
with connect(settings.DATABASE_URL) as conn:
|
| 38 |
+
total_rows = len(fetch_unresolved_predictions(conn))
|
| 39 |
+
|
| 40 |
+
def report_progress(processed: int, resolved: int, failed: int) -> None:
|
| 41 |
+
update_job_progress(
|
| 42 |
+
settings.DATABASE_URL,
|
| 43 |
+
job_run_id,
|
| 44 |
+
total_rows=total_rows,
|
| 45 |
+
rows_processed=processed,
|
| 46 |
+
resolved_rows=resolved,
|
| 47 |
+
failed_rows=failed,
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
report_progress(0, 0, 0)
|
| 51 |
+
resolved = resolve_pending_predictions(conn, on_progress=report_progress)
|
| 52 |
finish_job_run(settings.DATABASE_URL, job_run_id, success=True)
|
| 53 |
log.info("Resolution complete: %s predictions resolved", resolved)
|
| 54 |
return resolved
|