update
This commit is contained in:
parent
ffd6f73427
commit
0e8cf51f64
@ -140,6 +140,7 @@ AlphaX 是一个以 `Python + FastAPI + PostgreSQL + Docker + 静态 HTML` 组
|
|||||||
|
|
||||||
- 链上功能当前已下线,不再有 `onchain` CLI、调度任务、Web 页面、API route、NodeReal/Alchemy client 或链上因子评分。
|
- 链上功能当前已下线,不再有 `onchain` CLI、调度任务、Web 页面、API route、NodeReal/Alchemy client 或链上因子评分。
|
||||||
- 历史 PostgreSQL migration 中的 `onchain_*` 表可暂时保留,避免破坏已部署库的迁移链;当前业务代码不应读取或写入这些表。
|
- 历史 PostgreSQL migration 中的 `onchain_*` 表可暂时保留,避免破坏已部署库的迁移链;当前业务代码不应读取或写入这些表。
|
||||||
|
- 下线运行模块时不能只删 CLI/route/service,还必须在 `app/db/scheduler_db.py#RETIRED_JOBS` 登记退役任务,初始化时自动禁用旧 `scheduler_job_config`、跳过旧手动触发,并从 scheduler/API/运行大屏过滤,避免线上数据库残留任务继续执行已删除命令。
|
||||||
- 后续如需重启链上方向,必须先重新设计数据源、可读事件模型、映射机制、策略接入边界和复盘评价,不能直接恢复旧实现。
|
- 后续如需重启链上方向,必须先重新设计数据源、可读事件模型、映射机制、策略接入边界和复盘评价,不能直接恢复旧实现。
|
||||||
|
|
||||||
### 4.2 Web/API
|
### 4.2 Web/API
|
||||||
|
|||||||
@ -7,6 +7,9 @@ from app.db import altcoin_db
|
|||||||
from app.db.postgres_connection import connect as pg_connect, ensure_migrations_once
|
from app.db.postgres_connection import connect as pg_connect, ensure_migrations_once
|
||||||
|
|
||||||
_SCHEDULER_INIT_DONE = False
|
_SCHEDULER_INIT_DONE = False
|
||||||
|
RETIRED_JOBS = {
|
||||||
|
"onchain": "链上采集/API 模块已下线,当前系统聚焦 CEX 机会捕捉",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_scheduler_conn():
|
def get_scheduler_conn():
|
||||||
@ -165,6 +168,43 @@ def _seed_scheduler_tables(conn):
|
|||||||
""",
|
""",
|
||||||
(job["job_name"], now),
|
(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():
|
def init_scheduler_tables():
|
||||||
@ -183,7 +223,7 @@ def init_scheduler_tables():
|
|||||||
def get_job_configs():
|
def get_job_configs():
|
||||||
init_scheduler_tables()
|
init_scheduler_tables()
|
||||||
conn = get_scheduler_conn()
|
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()
|
conn.close()
|
||||||
jobs = []
|
jobs = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
@ -201,6 +241,8 @@ def get_job_config(job_name):
|
|||||||
conn.close()
|
conn.close()
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
|
if row["job_name"] in RETIRED_JOBS:
|
||||||
|
return None
|
||||||
item = dict(row)
|
item = dict(row)
|
||||||
item["args"] = _load(item.pop("args_json", "[]"), [])
|
item["args"] = _load(item.pop("args_json", "[]"), [])
|
||||||
item["enabled"] = bool(item.get("enabled"))
|
item["enabled"] = bool(item.get("enabled"))
|
||||||
@ -211,10 +253,13 @@ def set_job_enabled(job_name, enabled):
|
|||||||
init_scheduler_tables()
|
init_scheduler_tables()
|
||||||
now = _now()
|
now = _now()
|
||||||
conn = get_scheduler_conn()
|
conn = get_scheduler_conn()
|
||||||
cur = conn.execute(
|
if job_name in RETIRED_JOBS:
|
||||||
"UPDATE scheduler_job_config SET enabled=%s, updated_at=%s WHERE job_name=%s",
|
cur = conn.execute("UPDATE scheduler_job_config SET enabled=0, updated_at=%s WHERE job_name=%s", (now, job_name))
|
||||||
(1 if enabled else 0, 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.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return cur.rowcount > 0
|
return cur.rowcount > 0
|
||||||
@ -225,10 +270,13 @@ def set_job_interval(job_name, every_seconds):
|
|||||||
init_scheduler_tables()
|
init_scheduler_tables()
|
||||||
now = _now()
|
now = _now()
|
||||||
conn = get_scheduler_conn()
|
conn = get_scheduler_conn()
|
||||||
cur = conn.execute(
|
if job_name in RETIRED_JOBS:
|
||||||
"UPDATE scheduler_job_config SET every_seconds=%s, updated_at=%s WHERE job_name=%s",
|
cur = conn.execute("UPDATE scheduler_job_config SET enabled=0, updated_at=%s WHERE job_name=%s", (now, job_name))
|
||||||
(seconds, 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.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return cur.rowcount > 0
|
return cur.rowcount > 0
|
||||||
@ -335,7 +383,7 @@ def list_manual_triggers(limit=30):
|
|||||||
def get_scheduler_overview():
|
def get_scheduler_overview():
|
||||||
init_scheduler_tables()
|
init_scheduler_tables()
|
||||||
conn = get_scheduler_conn()
|
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()
|
runtime_rows = conn.execute("SELECT * FROM scheduler_runtime_status").fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -47,29 +47,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="note" id="paperNote">策略交易只统计已经进入交易账本的信号。页面用账户余额、持仓价值、累计杠杆和实际盈亏展示策略表现,不再把观察池或推荐归档当作收益。</div>
|
<div class="note" id="paperNote">策略交易只统计已经进入交易账本的信号。页面用账户余额、持仓价值、累计杠杆和实际盈亏展示策略表现,不再把观察池或推荐归档当作收益。</div>
|
||||||
<div class="note" id="reportNote" style="display:none"></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>
|
<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="策略交易视图切换">
|
<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 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>
|
<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>
|
<div class="pagination" id="eventPager"></div>
|
||||||
</section>
|
</section>
|
||||||
</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>
|
||||||
|
<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>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block extra_script %}
|
{% block extra_script %}
|
||||||
|
|||||||
@ -32,6 +32,34 @@ def test_scheduler_tables_seed_defaults(monkeypatch, tmp_path):
|
|||||||
assert "onchain" not in jobs
|
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):
|
def test_scheduler_control_api_and_page(monkeypatch, tmp_path):
|
||||||
db_path = tmp_path / "altcoin_monitor.db"
|
db_path = tmp_path / "altcoin_monitor.db"
|
||||||
sched_path = tmp_path / "scheduler_state.db"
|
sched_path = tmp_path / "scheduler_state.db"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user