Skip to content
Draft
4 changes: 2 additions & 2 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ It is not an automated money maker, promises no profitability, and does not prov
```mermaid
flowchart LR
Op["Operator / External Agent<br/>Web · ht CLI · TUI · Desktop"] --> API["FastAPI Control Plane<br/>Mission · TokenManager · Probes (/livez, /readyz, /healthz)"]
API --> DB[("PostgreSQL + pgvector<br/>50 migrations · Mission projections · HET tree · Memory")]
API --> DB[("PostgreSQL + pgvector<br/>51 migrations · Mission projections · HET tree · Memory")]
Worker["Worker Loops (13 Loops)<br/>Mission · AVO research · evolution · auto-review · meta-tuning"] --> DB
Worker --> Research["ARC Autonomous Research Loop (AVO + RD-Agent)<br/>HET tree → Co-STEER synthesis → AST gatekeeper → sandbox → baseline"]
Worker --> Evolution["Evolution Engine (default on)<br/>decay scan → budget admission → re-research → race duel → ledger"]
Expand Down Expand Up @@ -213,7 +213,7 @@ Single command executes all quality gates: Frontend lint, Vitest, and production
| **Control Plane** | FastAPI 0.122+ / Uvicorn, scoped security tokens, `/livez`, `/readyz`, `/healthz` container probes, cursor SSE |
| **Credential Security**| `TokenRotationService` (SHA-256 single-way hashing, zero plaintext storage, 24h grace period hot rotation, 7-day expiration audit) |
| **Domain & Runtime** | Python 3.12+, Pydantic v2 strict models, modular monolith + ports-and-adapters |
| **Storage** | SQLAlchemy 2.0 + Alembic (50 migrations), PostgreSQL 14+ with `pgvector` |
| **Storage** | SQLAlchemy 2.0 + Alembic (51 migrations), PostgreSQL 14+ with `pgvector` |
| **Workers** | asyncio + PostgreSQL SQL lease locking (13 concurrent background loops) |
| **Research Core** | ARC + RD-Agent Hypothesis Evolution Tree (HET) + Co-STEER synthesizer + `ASTGatekeeper` |
| **Target Adapters** | `market_target.v1` and `market-evolution.v1` MCP contracts; BitPro (Crypto) + QuantLab (A-Shares) |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ It is not an automated money maker, promises no profitability, and does not prov
```mermaid
flowchart LR
Op["Operator / External Agent<br/>Web · ht CLI · TUI · Desktop"] --> API["FastAPI Control Plane<br/>Mission · TokenManager · Probes (/livez, /readyz, /healthz)"]
API --> DB[("PostgreSQL + pgvector<br/>50 migrations · Mission projections · HET tree · Memory")]
API --> DB[("PostgreSQL + pgvector<br/>51 migrations · Mission projections · HET tree · Memory")]
Worker["Worker Loops (13 Loops)<br/>Mission · AVO research · evolution · auto-review · meta-tuning"] --> DB
Worker --> Research["ARC Autonomous Research Loop (AVO + RD-Agent)<br/>HET tree → Co-STEER synthesis → AST gatekeeper → sandbox → baseline"]
Worker --> Evolution["Evolution Engine (default on)<br/>decay scan → budget admission → re-research → race duel → ledger"]
Expand Down
4 changes: 2 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
```mermaid
flowchart LR
Op["操作员 / 外部 Agent<br/>Web · ht CLI · TUI · Desktop"] --> API["FastAPI 控制面<br/>Mission · TokenManager · Probes (/livez, /readyz, /healthz)"]
API --> DB[("PostgreSQL + pgvector<br/>50 项迁移 · Mission 投影 · 演进树 · 记忆库")]
API --> DB[("PostgreSQL + pgvector<br/>51 项迁移 · Mission 投影 · 演进树 · 记忆库")]
Worker["Worker 循环 (13 个 Loop)<br/>Mission · AVO 研究 · 进化扫描 · 自动评审 · 元学习"] --> DB
Worker --> Research["ARC 自主研究循环 (AVO + RD-Agent)<br/>HET 假设树 → Co-STEER 合成 → AST 门禁 → 沙箱自测 → 基线对比"]
Worker --> Evolution["自主进化引擎(默认开启)<br/>退化扫描 → 预算准入 → 再研究 → 赛马对决 → 效果账本"]
Expand Down Expand Up @@ -213,7 +213,7 @@ curl http://localhost:3334/api/portfolio/relay/handovers
| **控制面** | FastAPI 0.122+ / Uvicorn,作用域安全令牌,`/livez`, `/readyz`, `/healthz` 容器探活矩阵,游标可续传 SSE |
| **凭据安全** | `TokenRotationService`(SHA-256 单向散列,零明文存储,24h 宽限期热轮换,7天临期审计) |
| **运行时与领域** | Python 3.12+、Pydantic v2 严格合同,模块化单体 + ports-and-adapters |
| **存储** | SQLAlchemy 2.0 + Alembic(50 个迁移),PostgreSQL 14+ 带 `pgvector` |
| **存储** | SQLAlchemy 2.0 + Alembic(51 个迁移),PostgreSQL 14+ 带 `pgvector` |
| **Worker 集群** | asyncio + PostgreSQL SQL lease 租约(13 个并发后台循环) |
| **研究与演进中枢** | ARC + RD-Agent 假设演进树 (HET) + Co-STEER 代码合成器 + `ASTGatekeeper` 语法守卫 |
| **多市场目标适配** | `market_target.v1` 与 `market-evolution.v1` MCP 契约;BitPro (Crypto) + QuantLab (A股) |
Expand Down
118 changes: 118 additions & 0 deletions backend/alembic/versions/0051_arc_ablation_runs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""Persist per-change ablation of forward losers (development-window backtests only).

Ablation records are observations, not causal verdicts. Neither table holds a
foreign key to ``arc_forward_outcomes``: correcting a ledger row must not rewrite
or delete the backtest evidence already produced for it.
"""

from collections.abc import Sequence

import sqlalchemy as sa
from alembic import op

revision: str = "0051_arc_ablation_runs"
down_revision: str | None = "0050_arc_forward_outcomes"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def _timestamps() -> tuple[sa.Column[object], sa.Column[object]]:
return (
sa.Column(
"created_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.func.now(),
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.func.now(),
),
)


def _tables() -> set[str]:
return set(sa.inspect(op.get_bind()).get_table_names())


def _indexes(table: str) -> set[str]:
return {item["name"] for item in sa.inspect(op.get_bind()).get_indexes(table)}


def upgrade() -> None:
# Idempotent on purpose: developer databases already got these tables from
# ``create_all``, and a partially applied migration must be safe to re-run.
if "arc_ablation_runs" not in _tables():
op.create_table(
"arc_ablation_runs",
sa.Column("id", sa.String(64), primary_key=True),
sa.Column("outcome_id", sa.String(64), nullable=False),
sa.Column("target_id", sa.String(32), nullable=False),
sa.Column("parent_strategy_id", sa.String(64), nullable=False),
sa.Column("challenger_strategy_id", sa.String(64), nullable=False),
sa.Column("status", sa.String(16), nullable=False, server_default="pending"),
sa.Column("reason", sa.String(64), nullable=False, server_default=""),
sa.Column("message", sa.Text(), nullable=False, server_default=""),
sa.Column("research_id", sa.String(64), nullable=False, server_default=""),
sa.Column("window_json", sa.JSON(), nullable=False, server_default="{}"),
sa.Column("plan_json", sa.JSON(), nullable=False, server_default="{}"),
sa.Column("result_json", sa.JSON(), nullable=False, server_default="{}"),
sa.Column("next_check_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("finished_at", sa.DateTime(timezone=True), nullable=True),
*_timestamps(),
sa.UniqueConstraint(
"target_id",
"challenger_strategy_id",
name="uq_arc_ablation_run_challenger",
),
sa.UniqueConstraint("outcome_id", name="uq_arc_ablation_run_outcome"),
)

run_indexes = _indexes("arc_ablation_runs")
for name, columns in (
("ix_arc_ablation_runs_parent_strategy_id", ["parent_strategy_id"]),
("ix_arc_ablation_runs_status", ["status"]),
("ix_arc_ablation_runs_next_check_at", ["next_check_at"]),
):
if name not in run_indexes:
op.create_index(name, "arc_ablation_runs", columns)

if "arc_ablation_steps" not in _tables():
op.create_table(
"arc_ablation_steps",
sa.Column("id", sa.String(64), primary_key=True),
sa.Column("run_id", sa.String(64), nullable=False),
sa.Column("step_key", sa.String(160), nullable=False),
sa.Column("ordinal", sa.Integer(), nullable=False, server_default="0"),
sa.Column("kind", sa.String(16), nullable=False),
sa.Column("change_json", sa.JSON(), nullable=False, server_default="{}"),
sa.Column("status", sa.String(16), nullable=False, server_default="pending"),
sa.Column("reason", sa.String(128), nullable=False, server_default=""),
sa.Column("request_hash", sa.String(64), nullable=False, server_default=""),
sa.Column("checkpoint_json", sa.JSON(), nullable=False, server_default="{}"),
sa.Column("result_json", sa.JSON(), nullable=False, server_default="{}"),
sa.Column("charged_at", sa.DateTime(timezone=True), nullable=True),
*_timestamps(),
sa.UniqueConstraint("run_id", "step_key", name="uq_arc_ablation_step"),
)

step_indexes = _indexes("arc_ablation_steps")
for name, columns in (
("ix_arc_ablation_steps_run_id", ["run_id"]),
("ix_arc_ablation_steps_status", ["status"]),
("ix_arc_ablation_steps_request_hash", ["request_hash"]),
("ix_arc_ablation_steps_charged_at", ["charged_at"]),
):
if name not in step_indexes:
op.create_index(name, "arc_ablation_steps", columns)


def downgrade() -> None:
tables = _tables()
if "arc_ablation_steps" in tables:
op.drop_table("arc_ablation_steps")
if "arc_ablation_runs" in tables:
op.drop_table("arc_ablation_runs")
Loading