自动排队生成量化因子挖掘任务,心跳派发后将高 IC 因子入库。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+187
-3
@@ -21,7 +21,7 @@ os.environ["ADMIN_PASSWORD"] = "test-admin-pass"
|
||||
os.environ["ADMIN_WALLET"] = "admin-root"
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import create_engine, select, text
|
||||
|
||||
from app.clock import utcnow
|
||||
from app.database import Base, SessionLocal, engine
|
||||
@@ -29,6 +29,7 @@ from app.main import app
|
||||
from app.jobs.daily_yield import run_daily_yield_cron
|
||||
from app.money import money
|
||||
from app.models import (
|
||||
AlphaFactor,
|
||||
ComputeNode,
|
||||
ComputeTask,
|
||||
IPWhitelist,
|
||||
@@ -42,7 +43,9 @@ from app.models import (
|
||||
from app.services.heartbeat import mark_stale_nodes
|
||||
from app.services.datacenter import FINGERPRINT_CLASH
|
||||
from app.services.ip_guard import DATACENTER_BATCH_MESSAGE
|
||||
from app.services.tasks import dispatch_random_tasks
|
||||
from app.schema_upgrade import _relax_task_node_id
|
||||
from app.services.alpha_queue import maybe_generate_alpha_batch
|
||||
from app.services.tasks import dispatch_random_tasks, expire_overdue_tasks
|
||||
|
||||
|
||||
class CoreFlowTest(unittest.TestCase):
|
||||
@@ -197,7 +200,7 @@ class CoreFlowTest(unittest.TestCase):
|
||||
|
||||
pending = self.client.get("/api/v1/node/NODE-T/tasks")
|
||||
self.assertEqual(pending.status_code, 200, pending.text)
|
||||
self.assertEqual(pending.json()[0]["task_id"], task_id)
|
||||
self.assertTrue(any(row["task_id"] == task_id for row in pending.json()))
|
||||
|
||||
rejected = self.client.post(
|
||||
"/api/v1/node/task/report",
|
||||
@@ -763,6 +766,187 @@ class CoreFlowTest(unittest.TestCase):
|
||||
self.assertEqual(logged.json()["role"], "admin")
|
||||
return {"Authorization": f"Bearer {logged.json()['access_token']}"}
|
||||
|
||||
def test_alpha_queue_generates_assigns_and_keeps_high_ic(self) -> None:
|
||||
self._register("0xalpha", "NODE-ALPHA", "100")
|
||||
start = utcnow()
|
||||
with SessionLocal() as session:
|
||||
first = maybe_generate_alpha_batch(session, now=start)
|
||||
session.commit()
|
||||
self.assertEqual(len(first), 3)
|
||||
self.assertEqual(len({task.payload["batch_id"] for task in first}), 1)
|
||||
for task in first:
|
||||
self.assertIsNone(task.node_id)
|
||||
self.assertEqual(task.status, TaskStatus.PENDING)
|
||||
self.assertEqual(task.task_type, "alpha_mining")
|
||||
self.assertIsInstance(task.payload["seed"], int)
|
||||
self.assertTrue(task.payload["operators"])
|
||||
self.assertTrue(task.payload["symbol"])
|
||||
self.assertGreaterEqual(len(task.payload["nonce"]), 8)
|
||||
quiet = maybe_generate_alpha_batch(session, now=start + timedelta(hours=1))
|
||||
self.assertEqual(quiet, [])
|
||||
later = maybe_generate_alpha_batch(session, now=start + timedelta(hours=2))
|
||||
session.commit()
|
||||
self.assertEqual(len(later), 3)
|
||||
self.assertNotEqual(later[0].payload["batch_id"], first[0].payload["batch_id"])
|
||||
queued = session.get(ComputeTask, first[0].id)
|
||||
queued.deadline_at = start - timedelta(minutes=1)
|
||||
session.commit()
|
||||
self.assertEqual(expire_overdue_tasks(session, start), 0)
|
||||
self.assertEqual(session.get(ComputeTask, first[0].id).status, TaskStatus.PENDING)
|
||||
|
||||
listed = self.client.get("/api/v1/admin/tasks/list", headers=self._admin_headers())
|
||||
self.assertEqual(listed.status_code, 200, listed.text)
|
||||
queued_row = next(row for row in listed.json() if row["task_id"] == first[0].id)
|
||||
self.assertEqual(queued_row["target_node_sn"], "")
|
||||
self.assertEqual(queued_row["display_node_id"], 0)
|
||||
|
||||
beat = self.client.post("/api/v1/node/heartbeat", json={"sn": "NODE-ALPHA"})
|
||||
self.assertEqual(beat.status_code, 200, beat.text)
|
||||
assigned = beat.json()["assigned_task"]
|
||||
self.assertIsNotNone(assigned)
|
||||
self.assertEqual(assigned["status"], "processing")
|
||||
self.assertEqual(assigned["task_type"], "alpha_mining")
|
||||
again = self.client.post("/api/v1/node/heartbeat", json={"sn": "NODE-ALPHA"})
|
||||
self.assertIsNone(again.json()["assigned_task"])
|
||||
with SessionLocal() as session:
|
||||
node = session.scalar(select(ComputeNode).where(ComputeNode.sn == "NODE-ALPHA"))
|
||||
holding = session.scalars(
|
||||
select(ComputeTask).where(
|
||||
ComputeTask.node_id == node.id,
|
||||
ComputeTask.task_type == "alpha_mining",
|
||||
ComputeTask.status == TaskStatus.PROCESSING,
|
||||
)
|
||||
).all()
|
||||
self.assertEqual(len(holding), 1)
|
||||
|
||||
high = self.client.post(
|
||||
"/api/v1/node/task/report",
|
||||
json={
|
||||
"sn": "NODE-ALPHA",
|
||||
"task_id": assigned["task_id"],
|
||||
"success": True,
|
||||
"nonce": assigned["payload"]["nonce"],
|
||||
"result": {
|
||||
"factors": [
|
||||
{"formula": "rank(delta(close, 1))", "ic": 0.08},
|
||||
{"formula": "scale(ts_std(close, 5))", "ic": 0.01},
|
||||
{"alpha": "", "ic": 0.2},
|
||||
]
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertEqual(high.status_code, 200, high.text)
|
||||
self.assertEqual(high.json()["status"], "completed")
|
||||
with SessionLocal() as session:
|
||||
rows = session.scalars(select(AlphaFactor)).all()
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertEqual(rows[0].formula, "rank(delta(close, 1))")
|
||||
self.assertAlmostEqual(rows[0].ic, 0.08)
|
||||
self.assertEqual(rows[0].symbol, assigned["payload"]["symbol"])
|
||||
|
||||
follow = self.client.post("/api/v1/node/heartbeat", json={"sn": "NODE-ALPHA"})
|
||||
second = follow.json()["assigned_task"]
|
||||
self.assertIsNotNone(second)
|
||||
self.assertNotEqual(second["task_id"], assigned["task_id"])
|
||||
low = self.client.post(
|
||||
"/api/v1/node/task/report",
|
||||
json={
|
||||
"sn": "NODE-ALPHA",
|
||||
"task_id": second["task_id"],
|
||||
"success": True,
|
||||
"nonce": second["payload"]["nonce"],
|
||||
"result": {"formula": "ts_mean(close, 3)", "ic": 0.01},
|
||||
},
|
||||
)
|
||||
self.assertEqual(low.status_code, 200, low.text)
|
||||
self.assertEqual(low.json()["status"], "completed")
|
||||
with SessionLocal() as session:
|
||||
self.assertEqual(len(session.scalars(select(AlphaFactor)).all()), 1)
|
||||
self.assertEqual(session.get(ComputeTask, second["task_id"]).status, TaskStatus.COMPLETED)
|
||||
|
||||
follow_again = self.client.post("/api/v1/node/heartbeat", json={"sn": "NODE-ALPHA"})
|
||||
third = follow_again.json()["assigned_task"]
|
||||
self.assertIsNotNone(third)
|
||||
visible = self.client.get("/api/v1/node/NODE-ALPHA/tasks")
|
||||
self.assertEqual(
|
||||
[row for row in visible.json() if row["task_id"] == third["task_id"]][0]["status"],
|
||||
"processing",
|
||||
)
|
||||
failed = self.client.post(
|
||||
"/api/v1/node/task/report",
|
||||
json={
|
||||
"sn": "NODE-ALPHA",
|
||||
"task_id": third["task_id"],
|
||||
"success": False,
|
||||
"nonce": third["payload"]["nonce"],
|
||||
"result": {"formula": "rank(close)", "ic": 0.2},
|
||||
},
|
||||
)
|
||||
self.assertEqual(failed.status_code, 200, failed.text)
|
||||
self.assertEqual(failed.json()["status"], "failed")
|
||||
with SessionLocal() as session:
|
||||
self.assertEqual(len(session.scalars(select(AlphaFactor)).all()), 1)
|
||||
|
||||
def test_old_task_table_accepts_unassigned_rows(self) -> None:
|
||||
path = Path(tempfile.gettempdir()) / "hypernode_relax_node.db"
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
other = create_engine("sqlite:///" + path.as_posix())
|
||||
with other.begin() as conn:
|
||||
conn.execute(text("CREATE TABLE compute_nodes (id INTEGER PRIMARY KEY)"))
|
||||
conn.execute(text("INSERT INTO compute_nodes (id) VALUES (1)"))
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
CREATE TABLE compute_tasks (
|
||||
id VARCHAR(36) NOT NULL PRIMARY KEY,
|
||||
node_id INTEGER NOT NULL,
|
||||
task_type VARCHAR(32) NOT NULL,
|
||||
payload JSON NOT NULL,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
push_status VARCHAR(16) NOT NULL,
|
||||
push_error VARCHAR(255),
|
||||
dispatched_at DATETIME NOT NULL,
|
||||
deadline_at DATETIME NOT NULL,
|
||||
responded_at DATETIME,
|
||||
success BOOLEAN,
|
||||
result JSON
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO compute_tasks (
|
||||
id, node_id, task_type, payload, status, push_status, dispatched_at, deadline_at
|
||||
) VALUES (
|
||||
'kept-task', 1, 'simulated_compute', '{}', 'pending', 'queued',
|
||||
'2026-10-01 00:00:00', '2026-10-01 00:10:00'
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
_relax_task_node_id(other)
|
||||
with other.begin() as conn:
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO compute_tasks (
|
||||
id, node_id, task_type, payload, status, push_status, dispatched_at, deadline_at
|
||||
) VALUES (
|
||||
'queued-task', NULL, 'alpha_mining', '{}', 'pending', 'queued',
|
||||
'2026-10-01 00:00:00', '2026-10-08 00:00:00'
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
kept = conn.execute(text("SELECT node_id FROM compute_tasks WHERE id = 'kept-task'")).scalar()
|
||||
queued = conn.execute(text("SELECT node_id FROM compute_tasks WHERE id = 'queued-task'")).scalar()
|
||||
other.dispose()
|
||||
self.assertEqual(kept, 1)
|
||||
self.assertIsNone(queued)
|
||||
|
||||
def _register(self, wallet: str, sn: str, amount: str, headers: dict[str, str] | None = None) -> None:
|
||||
user = self.client.post("/api/v1/users", json={"wallet_address": wallet})
|
||||
self.assertEqual(user.status_code, 200, user.text)
|
||||
|
||||
Reference in New Issue
Block a user