This commit is contained in:
aaron 2026-06-07 21:50:36 +08:00
parent ffd6f73427
commit 0e8cf51f64
4 changed files with 109 additions and 32 deletions

View File

@ -140,6 +140,7 @@ AlphaX 是一个以 `Python + FastAPI + PostgreSQL + Docker + 静态 HTML` 组
- 链上功能当前已下线,不再有 `onchain` CLI、调度任务、Web 页面、API route、NodeReal/Alchemy client 或链上因子评分。
- 历史 PostgreSQL migration 中的 `onchain_*` 表可暂时保留,避免破坏已部署库的迁移链;当前业务代码不应读取或写入这些表。
- 下线运行模块时不能只删 CLI/route/service还必须在 `app/db/scheduler_db.py#RETIRED_JOBS` 登记退役任务,初始化时自动禁用旧 `scheduler_job_config`、跳过旧手动触发,并从 scheduler/API/运行大屏过滤,避免线上数据库残留任务继续执行已删除命令。
- 后续如需重启链上方向,必须先重新设计数据源、可读事件模型、映射机制、策略接入边界和复盘评价,不能直接恢复旧实现。
### 4.2 Web/API

View File

@ -7,6 +7,9 @@ from app.db import altcoin_db
from app.db.postgres_connection import connect as pg_connect, ensure_migrations_once
_SCHEDULER_INIT_DONE = False
RETIRED_JOBS = {
"onchain": "链上采集/API 模块已下线,当前系统聚焦 CEX 机会捕捉",
}
def get_scheduler_conn():
@ -165,6 +168,43 @@ def _seed_scheduler_tables(conn):
""",
(job["job_name"], now),
)
_retire_scheduler_jobs(conn, now)
def _retire_scheduler_jobs(conn, now: str | None = None):
now = now or _now()
for job_name, reason in RETIRED_JOBS.items():
conn.execute(
"""
UPDATE scheduler_job_config
SET enabled=0,
description=%s,
updated_at=%s
WHERE job_name=%s
""",
(f"已下线:{reason}", now, job_name),
)
conn.execute(
"""
INSERT INTO scheduler_runtime_status (job_name, status, last_error, updated_at)
VALUES (%s, 'disabled', %s, %s)
ON CONFLICT(job_name) DO UPDATE SET
status='disabled',
last_error=excluded.last_error,
updated_at=excluded.updated_at
""",
(job_name, reason, now),
)
conn.execute(
"""
UPDATE scheduler_manual_trigger
SET status='skipped',
finished_at=%s,
error_message=%s
WHERE job_name=%s AND status IN ('queued','pending','running')
""",
(now, reason, job_name),
)
def init_scheduler_tables():
@ -183,7 +223,7 @@ def init_scheduler_tables():
def get_job_configs():
init_scheduler_tables()
conn = get_scheduler_conn()
rows = conn.execute("SELECT * FROM scheduler_job_config ORDER BY sort_order ASC, job_name ASC").fetchall()
rows = conn.execute("SELECT * FROM scheduler_job_config WHERE job_name <> ALL(%s) ORDER BY sort_order ASC, job_name ASC", (list(RETIRED_JOBS.keys()),)).fetchall()
conn.close()
jobs = []
for row in rows:
@ -201,6 +241,8 @@ def get_job_config(job_name):
conn.close()
if not row:
return None
if row["job_name"] in RETIRED_JOBS:
return None
item = dict(row)
item["args"] = _load(item.pop("args_json", "[]"), [])
item["enabled"] = bool(item.get("enabled"))
@ -211,10 +253,13 @@ def set_job_enabled(job_name, enabled):
init_scheduler_tables()
now = _now()
conn = get_scheduler_conn()
cur = conn.execute(
"UPDATE scheduler_job_config SET enabled=%s, updated_at=%s WHERE job_name=%s",
(1 if enabled else 0, now, job_name),
)
if job_name in RETIRED_JOBS:
cur = conn.execute("UPDATE scheduler_job_config SET enabled=0, updated_at=%s WHERE job_name=%s", (now, job_name))
else:
cur = conn.execute(
"UPDATE scheduler_job_config SET enabled=%s, updated_at=%s WHERE job_name=%s",
(1 if enabled else 0, now, job_name),
)
conn.commit()
conn.close()
return cur.rowcount > 0
@ -225,10 +270,13 @@ def set_job_interval(job_name, every_seconds):
init_scheduler_tables()
now = _now()
conn = get_scheduler_conn()
cur = conn.execute(
"UPDATE scheduler_job_config SET every_seconds=%s, updated_at=%s WHERE job_name=%s",
(seconds, now, job_name),
)
if job_name in RETIRED_JOBS:
cur = conn.execute("UPDATE scheduler_job_config SET enabled=0, updated_at=%s WHERE job_name=%s", (now, job_name))
else:
cur = conn.execute(
"UPDATE scheduler_job_config SET every_seconds=%s, updated_at=%s WHERE job_name=%s",
(seconds, now, job_name),
)
conn.commit()
conn.close()
return cur.rowcount > 0
@ -335,7 +383,7 @@ def list_manual_triggers(limit=30):
def get_scheduler_overview():
init_scheduler_tables()
conn = get_scheduler_conn()
configs = conn.execute("SELECT * FROM scheduler_job_config ORDER BY sort_order ASC, job_name ASC").fetchall()
configs = conn.execute("SELECT * FROM scheduler_job_config WHERE job_name <> ALL(%s) ORDER BY sort_order ASC, job_name ASC", (list(RETIRED_JOBS.keys()),)).fetchall()
runtime_rows = conn.execute("SELECT * FROM scheduler_runtime_status").fetchall()
conn.close()
try:

View File

@ -47,29 +47,7 @@
</div>
<div class="note" id="paperNote">策略交易只统计已经进入交易账本的信号。页面用账户余额、持仓价值、累计杠杆和实际盈亏展示策略表现,不再把观察池或推荐归档当作收益。</div>
<div class="note" id="reportNote" style="display:none"></div>
<section class="strategy-board">
<div class="strategy-board-head">
<div>
<div class="strategy-board-title">运行策略看板</div>
<div class="strategy-board-copy">按策略独立看信号、机会、交易、胜率和收益;点击卡片可筛选下方持仓、挂单、完结持仓、取消订单和日志。</div>
</div>
<div class="actions">
<button class="btn" type="button" onclick="clearStrategyAndSide()">查看全部</button>
</div>
</div>
<div class="strategy-grid" id="strategyCards"><div class="strategy-empty">策略数据加载中...</div></div>
</section>
<div class="kpis" id="kpis"><div class="kpi"><span>状态</span><b>加载中</b></div></div>
<section class="perf-panel">
<div class="perf-head">
<div>
<div class="perf-title">每日收益与权益曲线</div>
<div class="panel-note">蓝线展示账户权益变化,绿色/红色柱展示每日实现收益,右侧同步最大回撤。</div>
</div>
<div class="perf-meta" id="perfMeta"><span class="perf-pill">加载中...</span></div>
</div>
<div class="perf-chart" id="performanceChart"><div class="loading">加载中...</div></div>
</section>
<div class="tabs" role="tablist" aria-label="策略交易视图切换">
<button class="tab-btn active" id="tab-open" type="button" onclick="setTradeTab('open')" role="tab" aria-selected="true">持仓中</button>
<button class="tab-btn" id="tab-orders" type="button" onclick="setTradeTab('orders')" role="tab" aria-selected="false">挂单中</button>
@ -142,6 +120,28 @@
<div class="pagination" id="eventPager"></div>
</section>
</div>
<section class="perf-panel">
<div class="perf-head">
<div>
<div class="perf-title">每日收益与权益曲线</div>
<div class="panel-note">蓝线展示账户权益变化,绿色/红色柱展示每日实现收益,右侧同步最大回撤。</div>
</div>
<div class="perf-meta" id="perfMeta"><span class="perf-pill">加载中...</span></div>
</div>
<div class="perf-chart" id="performanceChart"><div class="loading">加载中...</div></div>
</section>
<section class="strategy-board">
<div class="strategy-board-head">
<div>
<div class="strategy-board-title">运行策略看板</div>
<div class="strategy-board-copy">统计每个策略的信号、机会、交易、胜率和收益;点击卡片可筛选上方交易列表。</div>
</div>
<div class="actions">
<button class="btn" type="button" onclick="clearStrategyAndSide()">查看全部</button>
</div>
</div>
<div class="strategy-grid" id="strategyCards"><div class="strategy-empty">策略数据加载中...</div></div>
</section>
</div>
{% endblock %}
{% block extra_script %}

View File

@ -32,6 +32,34 @@ def test_scheduler_tables_seed_defaults(monkeypatch, tmp_path):
assert "onchain" not in jobs
def test_scheduler_init_retires_legacy_onchain_job(pg_conn):
pg_conn.execute(
"""
INSERT INTO scheduler_job_config (job_name, command, args_json, enabled, every_seconds, initial_delay, lock_group, description, sort_order, created_at, updated_at)
VALUES ('onchain', 'onchain', '[]', 1, 60, 0, 'onchain', 'legacy', 99, NOW(), NOW())
ON CONFLICT(job_name) DO UPDATE SET enabled=1, command='onchain', updated_at=NOW()
"""
)
pg_conn.execute(
"""
INSERT INTO scheduler_manual_trigger (job_name, force, status, requested_by, requested_at)
VALUES ('onchain', 1, 'queued', 'test', NOW())
"""
)
pg_conn.commit()
scheduler_db._SCHEDULER_INIT_DONE = False
scheduler_db.init_scheduler_tables()
assert scheduler_db.get_job_config("onchain") is None
assert "onchain" not in {item["job_name"] for item in scheduler_db.get_job_configs()}
row = pg_conn.execute("SELECT enabled, description FROM scheduler_job_config WHERE job_name='onchain'").fetchone()
trigger = pg_conn.execute("SELECT status, error_message FROM scheduler_manual_trigger WHERE job_name='onchain' ORDER BY id DESC LIMIT 1").fetchone()
assert row["enabled"] == 0
assert "已下线" in row["description"]
assert trigger["status"] == "skipped"
def test_scheduler_control_api_and_page(monkeypatch, tmp_path):
db_path = tmp_path / "altcoin_monitor.db"
sched_path = tmp_path / "scheduler_state.db"