This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
# Alpha 管理候选PPAC
|
||||||
|
Type: task
|
||||||
|
Status: ready-for-agent
|
||||||
|
|
||||||
|
用户要求新增固定候选清单,并将对应检查结果显示为黄色“候选PPAC”Tag。
|
||||||
|
|
||||||
|
## 实现范围
|
||||||
|
|
||||||
|
- 仅有一项 Alpha FAIL 且名称为 PURE_POWER_POOL_THEME 时,检查结果派生为 PPAC_CANDIDATE;REGULAR_SUBMISSION 沿用既有排除口径,WARNING/PENDING 不计失败。
|
||||||
|
- 固定“候选PPAC”Tab 通过 ppac_candidate=true 在数据库分页前筛选未提交候选;叠加筛选、分页、导出、保存/恢复视图及 AI 页面上下文保持一致。
|
||||||
|
- 列表和详情显示黄色候选状态,保留原始 FAIL 检查证据与本地研究记录。候选身份不表示当前可正式提交。
|
||||||
|
- 迁移 0022 复用已有 check_type 索引,仅重分类匹配的历史缓存。同步和主动检查沿用 snapshot_columns 自动刷新分类。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
分类边界、组合筛选/分页/导出、同步与主动检查后的状态迁移、隔离历史迁移、后端静态检查、前端构建和浏览器固定 Tab/黄色 Tag/保存恢复/窄屏验证。
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
已完成本地实现与验证:95 项后端回归通过,包含分类边界、真实业务执行器对模拟 /check 的状态刷新、筛选/分页/导出/保存视图、503 条历史缓存的升级/降级/再升级及迁移 schema check;Ruff、TypeScript 与生产构建通过。
|
||||||
|
|
||||||
|
原有 Alpha 管理、提交受阻和本地相关性筛选 3 项浏览器回归通过。另在隔离临时库使用 56 条合成 PPAC Alpha 验证:固定 Tab、列表/详情黄色 Tag、原始 FAIL 保留、跨页勾选、搜索后回第一页及清空选择、保存/恢复视图、空结果和重置保持候选范围。1920px 桌面与 720px 窄屏完成截图检查,行高为 40px,窄屏分页底部 887px 位于 900px 视口内,页面未横向溢出,表体可独立横向滚动。
|
||||||
|
|
||||||
|
截图位于忽略目录 output/playwright/ppac-candidate-{desktop,detail,narrow}.png。构建保留已有 lottie eval 和大包提示;浏览器仅有登录前 auth/me 的 401 与 favicon 404。未部署、未迁移实际业务数据库、未请求真实平台。
|
||||||
+13
-4
@@ -31,16 +31,20 @@ def snapshot_columns(settings, metrics, checks, *, checked=False):
|
|||||||
Sync snapshots with no failures are PRE_CHECK; a completed explicit /check
|
Sync snapshots with no failures are PRE_CHECK; a completed explicit /check
|
||||||
with no failures is PASS. WARNING/PENDING do not count as failures, matching
|
with no failures is PASS. WARNING/PENDING do not count as failures, matching
|
||||||
the legacy workflow. Empty, malformed or unknown results remain PENDING.
|
the legacy workflow. Empty, malformed or unknown results remain PENDING.
|
||||||
No submission eligibility or activity eligibility is inferred here.
|
A sole PURE_POWER_POOL_THEME failure is a PPAC candidate, not confirmation
|
||||||
|
of current submission or activity eligibility. Raw failures stay available.
|
||||||
"""
|
"""
|
||||||
settings = settings if isinstance(settings, dict) else {}
|
settings = settings if isinstance(settings, dict) else {}
|
||||||
metrics = metrics if isinstance(metrics, dict) else {}
|
metrics = metrics if isinstance(metrics, dict) else {}
|
||||||
blocked = submission_limits(checks)["status"] == "blocked"
|
blocked = submission_limits(checks)["status"] == "blocked"
|
||||||
checks, _ = split_checks(checks)
|
checks, _ = split_checks(checks)
|
||||||
valid = [check for check in checks if isinstance(check, dict)]
|
valid = [check for check in checks if isinstance(check, dict)]
|
||||||
failures = len(failed_checks(checks))
|
failed_names = failed_checks(checks)
|
||||||
|
failures = len(failed_names)
|
||||||
by_name = {check["name"]: check for check in valid if isinstance(check.get("name"), str)}
|
by_name = {check["name"]: check for check in valid if isinstance(check.get("name"), str)}
|
||||||
if failures:
|
if failed_names == ["PURE_POWER_POOL_THEME"]:
|
||||||
|
check_type = "PPAC_CANDIDATE"
|
||||||
|
elif failures:
|
||||||
check_type = "FAIL_1" if failures == 1 else "FAIL_2"
|
check_type = "FAIL_1" if failures == 1 else "FAIL_2"
|
||||||
elif not checks or len(valid) != len(checks) or any(check_result(check) not in ("PASS", "WARNING", "PENDING") for check in valid):
|
elif not checks or len(valid) != len(checks) or any(check_result(check) not in ("PASS", "WARNING", "PENDING") for check in valid):
|
||||||
check_type = "PENDING"
|
check_type = "PENDING"
|
||||||
@@ -75,7 +79,7 @@ def check_summary(checks, *, check_type):
|
|||||||
"check_type": check_type,
|
"check_type": check_type,
|
||||||
"failed_checks": failed_checks(checks),
|
"failed_checks": failed_checks(checks),
|
||||||
"submission_limits": submission_limits(checks),
|
"submission_limits": submission_limits(checks),
|
||||||
"meaning": "PRE_CHECK 为同步无失败项;PASS 为主动检查完成且无失败项。PENDING/WARNING 不算失败,不代表全部检查项 PASS 或当前可提交",
|
"meaning": "PRE_CHECK 为同步无失败项;PASS 为主动检查完成且无失败项;PPAC_CANDIDATE 为唯一失败项是 PURE_POWER_POOL_THEME 的候选。PENDING/WARNING 不算失败,不代表全部检查项 PASS 或当前可提交",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -199,6 +203,11 @@ def list_statement(filters):
|
|||||||
query = query.where(Alpha.submission_blocked == filters.submission_blocked)
|
query = query.where(Alpha.submission_blocked == filters.submission_blocked)
|
||||||
if filters.submission_blocked:
|
if filters.submission_blocked:
|
||||||
query = query.where(submission_condition("UNSUBMITTED"))
|
query = query.where(submission_condition("UNSUBMITTED"))
|
||||||
|
if filters.ppac_candidate is not None:
|
||||||
|
candidate = Alpha.check_type == "PPAC_CANDIDATE"
|
||||||
|
query = query.where(candidate if filters.ppac_candidate else ~candidate)
|
||||||
|
if filters.ppac_candidate:
|
||||||
|
query = query.where(submission_condition("UNSUBMITTED"))
|
||||||
if (filters.local_correlation_status is not None or filters.local_correlation_min is not None
|
if (filters.local_correlation_status is not None or filters.local_correlation_min is not None
|
||||||
or filters.local_correlation_max is not None):
|
or filters.local_correlation_max is not None):
|
||||||
# One cache row per Alpha keeps totals/export stable; stale overrides the displayed status.
|
# One cache row per Alpha keeps totals/export stable; stale overrides the displayed status.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_valida
|
|||||||
|
|
||||||
ResearchState = Literal["inbox", "candidate", "optimizing", "archived"]
|
ResearchState = Literal["inbox", "candidate", "optimizing", "archived"]
|
||||||
Submission = Literal["UNSUBMITTED", "SUBMITTED"]
|
Submission = Literal["UNSUBMITTED", "SUBMITTED"]
|
||||||
CheckType = Literal["PENDING", "PRE_CHECK", "PASS", "FAIL_1", "FAIL_2"]
|
CheckType = Literal["PENDING", "PRE_CHECK", "PASS", "PPAC_CANDIDATE", "FAIL_1", "FAIL_2"]
|
||||||
SortField = Literal[
|
SortField = Literal[
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
@@ -74,6 +74,7 @@ class AlphaFilters(Contract):
|
|||||||
local_correlation_min: float | None = Field(default=None, ge=-1, le=1)
|
local_correlation_min: float | None = Field(default=None, ge=-1, le=1)
|
||||||
local_correlation_max: float | None = Field(default=None, ge=-1, le=1)
|
local_correlation_max: float | None = Field(default=None, ge=-1, le=1)
|
||||||
submission_blocked: bool | None = None
|
submission_blocked: bool | None = None
|
||||||
|
ppac_candidate: bool | None = None
|
||||||
submission: Submission | None = None
|
submission: Submission | None = None
|
||||||
source: str | None = Field(default=None, max_length=100)
|
source: str | None = Field(default=None, max_length=100)
|
||||||
source_reference: str | None = Field(default=None, max_length=200)
|
source_reference: str | None = Field(default=None, max_length=200)
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"""Classify snapshots whose sole Alpha failure is the unopened PPAC theme."""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = "0022"
|
||||||
|
down_revision = "0021"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# Reuse the indexed check_type column; preserve all raw evidence and check stages.
|
||||||
|
table = sa.table("alphas", sa.column("id", sa.String()), sa.column("checks", sa.JSON()),
|
||||||
|
sa.column("check_type", sa.String(20)))
|
||||||
|
connection = op.get_bind()
|
||||||
|
last_id = None
|
||||||
|
while True:
|
||||||
|
query = sa.select(table.c.id, table.c.checks).order_by(table.c.id).limit(500)
|
||||||
|
if last_id is not None:
|
||||||
|
query = query.where(table.c.id > last_id)
|
||||||
|
rows = connection.execute(query).mappings().all()
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
candidates = []
|
||||||
|
for row in rows:
|
||||||
|
# Freeze the current interpretation instead of importing application code.
|
||||||
|
checks = row["checks"] if isinstance(row["checks"], list) else []
|
||||||
|
failures = [check for check in checks if isinstance(check, dict)
|
||||||
|
and check.get("name") != "REGULAR_SUBMISSION"
|
||||||
|
and isinstance(check.get("result"), str) and check["result"].upper() == "FAIL"]
|
||||||
|
if len(failures) == 1 and failures[0].get("name") == "PURE_POWER_POOL_THEME":
|
||||||
|
candidates.append(row["id"])
|
||||||
|
if candidates:
|
||||||
|
connection.execute(table.update().where(table.c.id.in_(candidates)).values(check_type="PPAC_CANDIDATE"))
|
||||||
|
last_id = rows[-1]["id"]
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
table = sa.table("alphas", sa.column("check_type", sa.String(20)))
|
||||||
|
op.execute(table.update().where(table.c.check_type == "PPAC_CANDIDATE").values(check_type="FAIL_1"))
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
"""PPAC candidates retain their theme failure and wait for platform eligibility."""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.alphas import check_summary, snapshot_columns, upsert_alpha
|
||||||
|
from tests import test_submission
|
||||||
|
from tests.conftest import alpha
|
||||||
|
|
||||||
|
THEME = {"name": "PURE_POWER_POOL_THEME", "result": "FAIL"}
|
||||||
|
LIMIT = {"name": "REGULAR_SUBMISSION", "result": "FAIL"}
|
||||||
|
OTHER = {"name": "LOW_SHARPE", "result": "FAIL"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("checked", [False, True])
|
||||||
|
@pytest.mark.parametrize("checks,expected", [
|
||||||
|
([THEME], "PPAC_CANDIDATE"),
|
||||||
|
([{**THEME, "result": "fail"}], "PPAC_CANDIDATE"),
|
||||||
|
([THEME, LIMIT, {"name": "MATCHES_THEMES", "result": "WARNING"},
|
||||||
|
{"name": "PROD_CORRELATION", "result": "PENDING"}], "PPAC_CANDIDATE"),
|
||||||
|
([THEME, OTHER], "FAIL_2"),
|
||||||
|
([THEME, THEME], "FAIL_2"),
|
||||||
|
([THEME, {"result": "FAIL"}], "FAIL_2"),
|
||||||
|
([OTHER], "FAIL_1"),
|
||||||
|
([{**THEME, "result": "PASS"}, OTHER], "FAIL_1"),
|
||||||
|
([{**THEME, "result": False}], "PENDING"),
|
||||||
|
([{**THEME, "value": False}], "PPAC_CANDIDATE"),
|
||||||
|
([], "PENDING"),
|
||||||
|
([{}], "PENDING"),
|
||||||
|
])
|
||||||
|
def test_only_a_single_explicit_theme_failure_is_a_candidate(checks, expected, checked):
|
||||||
|
original = copy.deepcopy(checks)
|
||||||
|
columns = snapshot_columns({}, {}, checks, checked=checked)
|
||||||
|
assert columns["check_type"] == expected
|
||||||
|
assert checks == original
|
||||||
|
if expected == "PPAC_CANDIDATE":
|
||||||
|
assert check_summary(checks, check_type=expected)["failed_checks"] == ["PURE_POWER_POOL_THEME"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ppac_scope_filters_before_pagination_and_export(app, logged_in):
|
||||||
|
async with app.state.sessions.begin() as db:
|
||||||
|
for name, checks, status, region, hidden in [
|
||||||
|
("candidate1", [THEME], "UNSUBMITTED", "USA", False),
|
||||||
|
("candidate2", [THEME, LIMIT], "UNSUBMITTED", "USA", True),
|
||||||
|
("other_region", [THEME], "UNSUBMITTED", "CHN", False),
|
||||||
|
("submitted", [THEME], "ACTIVE", "USA", False),
|
||||||
|
("missing_status", [THEME], None, "USA", False),
|
||||||
|
("two_failures", [THEME, OTHER], "UNSUBMITTED", "USA", False),
|
||||||
|
("other_failure", [OTHER], "UNSUBMITTED", "USA", False),
|
||||||
|
("passed", [{**THEME, "result": "PASS"}], "UNSUBMITTED", "USA", False),
|
||||||
|
("missing", [], "UNSUBMITTED", "USA", False),
|
||||||
|
]:
|
||||||
|
await upsert_alpha(db, alpha(name, status=status, hidden=hidden, settings={"region": region},
|
||||||
|
**{"is": {"checks": checks}}))
|
||||||
|
query = "ppac_candidate=true®ion=USA&sort=id&direction=asc&limit=1&offset=1"
|
||||||
|
response = await logged_in.get(f"/api/v1/alphas?{query}")
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
result = response.json()
|
||||||
|
assert result["total"] == 2
|
||||||
|
assert [row["id"] for row in result["items"]] == ["candidate2"]
|
||||||
|
assert result["items"][0]["check_type"] == "PPAC_CANDIDATE"
|
||||||
|
assert result["items"][0]["failed_checks"] == ["PURE_POWER_POOL_THEME"]
|
||||||
|
export = await logged_in.get(f"/api/v1/alphas/export?{query}")
|
||||||
|
rows = list(csv.DictReader(io.StringIO(export.text.lstrip("\ufeff"))))
|
||||||
|
assert [row["id"] for row in rows] == ["candidate1", "candidate2"]
|
||||||
|
assert {row["check_type"] for row in rows} == {"PPAC_CANDIDATE"}
|
||||||
|
assert (await logged_in.get("/api/v1/alphas?ppac_candidate=true&submission=SUBMITTED")).json()["total"] == 0
|
||||||
|
assert (await logged_in.get("/api/v1/alphas?ppac_candidate=true&check_type=FAIL_1")).json()["total"] == 0
|
||||||
|
assert (await logged_in.get("/api/v1/alphas?ppac_candidate=true&submission_blocked=true")).json()["total"] == 1
|
||||||
|
assert (await logged_in.get("/api/v1/alphas?ppac_candidate=true&q=candidate1")).json()["total"] == 1
|
||||||
|
assert (await logged_in.get("/api/v1/alphas?check_type=PPAC_CANDIDATE&submission=UNSUBMITTED")).json()["total"] == 3
|
||||||
|
assert (await logged_in.get("/api/v1/alphas?ppac_candidate=false")).json()["total"] == 4
|
||||||
|
|
||||||
|
saved = await logged_in.post("/api/v1/research/assets", json={
|
||||||
|
"kind": "view", "content": {"name": "候选PPAC", "filters": {
|
||||||
|
"submission": "UNSUBMITTED", "ppac_candidate": True, "region": "USA",
|
||||||
|
}, "columns": ["name", "check_type"]},
|
||||||
|
})
|
||||||
|
assert saved.status_code in (200, 201), saved.text
|
||||||
|
assert saved.json()["content"]["filters"]["ppac_candidate"] is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_check_and_sync_refresh_candidate_status_without_losing_evidence(app, logged_in, monkeypatch):
|
||||||
|
await test_submission.setup(app, alpha(**{"is": {"checks": [OTHER]}}))
|
||||||
|
for checks, expected in [([THEME, LIMIT], "PPAC_CANDIDATE"), ([{**THEME, "result": "PASS"}], "PASS")]:
|
||||||
|
monkeypatch.setattr(test_submission, "CHECKS", checks)
|
||||||
|
response = await test_submission.enqueue(logged_in)
|
||||||
|
assert response.status_code == 202, response.text
|
||||||
|
await app.state.runner.execute(response.json()["id"])
|
||||||
|
state = (await logged_in.get("/api/v1/alphas/alpha1/submission")).json()
|
||||||
|
assert state["job"]["status"] == "completed"
|
||||||
|
assert state["check_summary"]["check_type"] == expected
|
||||||
|
detail = (await logged_in.get("/api/v1/alphas/alpha1")).json()
|
||||||
|
assert detail["checks"] == checks
|
||||||
|
assert detail["check_type"] == expected
|
||||||
|
assert detail["research"]["note"] == "preserve local research"
|
||||||
|
assert (await logged_in.get("/api/v1/alphas?ppac_candidate=true")).json()["total"] == (expected == "PPAC_CANDIDATE")
|
||||||
|
for checks, expected in [([THEME], "PPAC_CANDIDATE"), ([THEME, OTHER], "FAIL_2")]:
|
||||||
|
async with app.state.sessions.begin() as db:
|
||||||
|
await upsert_alpha(db, alpha(**{"is": {"checks": checks}}))
|
||||||
|
detail = (await logged_in.get("/api/v1/alphas/alpha1")).json()
|
||||||
|
assert detail["check_type"] == expected
|
||||||
|
assert detail["checks"] == checks
|
||||||
|
assert detail["research"]["note"] == "preserve local research"
|
||||||
|
assert (await logged_in.get("/api/v1/alphas?ppac_candidate=true")).json()["total"] == (expected == "PPAC_CANDIDATE")
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
"""Historical PPAC classification changes only matching cached check types."""
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import command
|
||||||
|
from alembic.config import Config
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
|
||||||
|
|
||||||
|
def test_ppac_backfill_preserves_evidence_across_batches_and_downgrade(tmp_path, monkeypatch):
|
||||||
|
database = tmp_path / "ppac.db"
|
||||||
|
monkeypatch.setenv("DATABASE_URL", f"sqlite+aiosqlite:///{database}")
|
||||||
|
monkeypatch.setenv("ADMIN_PASSWORD", "migration-test-only")
|
||||||
|
monkeypatch.setenv("ENCRYPTION_KEY", Fernet.generate_key().decode())
|
||||||
|
monkeypatch.setenv("WQ_EMAIL", "")
|
||||||
|
monkeypatch.setenv("WQ_PASSWORD", "")
|
||||||
|
root = Path(__file__).resolve().parents[1]
|
||||||
|
config = Config(str(root / "alembic.ini"))
|
||||||
|
config.set_main_option("script_location", str(root / "migrations"))
|
||||||
|
command.upgrade(config, "0021")
|
||||||
|
engine = sa.create_engine(f"sqlite:///{database}")
|
||||||
|
alphas = sa.Table("alphas", sa.MetaData(), autoload_with=engine)
|
||||||
|
theme = {"name": "PURE_POWER_POOL_THEME", "result": "FAIL"}
|
||||||
|
other = {"name": "LOW_SHARPE", "result": "FAIL"}
|
||||||
|
patterns = [
|
||||||
|
([theme], "FAIL_1", "PPAC_CANDIDATE"),
|
||||||
|
([{**theme, "result": "fail"}, {"name": "REGULAR_SUBMISSION", "result": "FAIL"}], "FAIL_1", "PPAC_CANDIDATE"),
|
||||||
|
([theme, other], "FAIL_2", "FAIL_2"),
|
||||||
|
([theme, theme], "FAIL_2", "FAIL_2"),
|
||||||
|
([other], "FAIL_1", "FAIL_1"),
|
||||||
|
([{**theme, "result": "PASS"}], "PASS", "PASS"),
|
||||||
|
([{**theme, "result": "WARNING"}], "PRE_CHECK", "PRE_CHECK"),
|
||||||
|
([{}], "PENDING", "PENDING"),
|
||||||
|
(None, "PENDING", "PENDING"),
|
||||||
|
]
|
||||||
|
with engine.begin() as db:
|
||||||
|
db.execute(alphas.insert(), [
|
||||||
|
{"id": f"ppac{i:04}", "status": "UNSUBMITTED", "hidden": False, "settings": {}, "os_metrics": {},
|
||||||
|
"is_metrics": {"checks": patterns[i % len(patterns)][0]}, "checks": patterns[i % len(patterns)][0],
|
||||||
|
"check_type": patterns[i % len(patterns)][1], "synced_at": datetime(2026, 9, 13, tzinfo=timezone.utc),
|
||||||
|
"raw": {"is": {"checks": patterns[i % len(patterns)][0]}}}
|
||||||
|
for i in range(503)
|
||||||
|
])
|
||||||
|
original = db.execute(sa.select(alphas).order_by(alphas.c.id)).mappings().all()
|
||||||
|
for target, position in [("0022", 2), ("0021", 1), ("0022", 2)]:
|
||||||
|
(command.upgrade if target == "0022" else command.downgrade)(config, target)
|
||||||
|
with engine.connect() as db:
|
||||||
|
rows = db.execute(sa.select(alphas).order_by(alphas.c.id)).mappings().all()
|
||||||
|
assert len(rows) == 503
|
||||||
|
for i, row in enumerate(rows):
|
||||||
|
assert dict(row) == {**original[i], "check_type": patterns[i % len(patterns)][position]}
|
||||||
|
command.check(config)
|
||||||
|
engine.dispose()
|
||||||
@@ -230,6 +230,11 @@ export function AlphaDetail({
|
|||||||
<Tag>{detail.alpha_type ?? "类型未提供"}</Tag>
|
<Tag>{detail.alpha_type ?? "类型未提供"}</Tag>
|
||||||
<Tag>{detail.language ?? "语言未提供"}</Tag>
|
<Tag>{detail.language ?? "语言未提供"}</Tag>
|
||||||
<Tag>{detail.status ?? "状态未提供"}</Tag>
|
<Tag>{detail.status ?? "状态未提供"}</Tag>
|
||||||
|
{detail.check_type === "PPAC_CANDIDATE" && (
|
||||||
|
<Tag color="yellow" size="small" type="light">
|
||||||
|
候选PPAC
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
@@ -240,6 +245,12 @@ export function AlphaDetail({
|
|||||||
<Button>在 BRAIN 中打开 ↗</Button>
|
<Button>在 BRAIN 中打开 ↗</Button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
{detail.check_type === "PPAC_CANDIDATE" && (
|
||||||
|
<p className="muted">
|
||||||
|
唯一失败项为 PURE_POWER_POOL_THEME,等待平台开放 PPAC
|
||||||
|
主题后可重新检查提交资格。
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{detail.alpha_type === "SUPER" && (
|
{detail.alpha_type === "SUPER" && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ const checkLabels = {
|
|||||||
PENDING: "待检查",
|
PENDING: "待检查",
|
||||||
PRE_CHECK: "预检通过",
|
PRE_CHECK: "预检通过",
|
||||||
PASS: "检查通过",
|
PASS: "检查通过",
|
||||||
|
PPAC_CANDIDATE: "候选PPAC",
|
||||||
FAIL_1: "FAIL=1",
|
FAIL_1: "FAIL=1",
|
||||||
FAIL_2: "FAIL≥2",
|
FAIL_2: "FAIL≥2",
|
||||||
};
|
};
|
||||||
@@ -205,6 +206,7 @@ export function AlphaPage({
|
|||||||
const [filterOpen, setFilterOpen] = useState(false);
|
const [filterOpen, setFilterOpen] = useState(false);
|
||||||
const [selectedViewId, setSelectedViewId] = useState<string | null>(null);
|
const [selectedViewId, setSelectedViewId] = useState<string | null>(null);
|
||||||
const [submissionBlocked, setSubmissionBlocked] = useState(false);
|
const [submissionBlocked, setSubmissionBlocked] = useState(false);
|
||||||
|
const [ppacCandidate, setPpacCandidate] = useState(false);
|
||||||
const [submission, setSubmission] = useState<Submission>("UNSUBMITTED");
|
const [submission, setSubmission] = useState<Submission>("UNSUBMITTED");
|
||||||
const [syncingScope, setSyncingScope] = useState<Submission | null>(null);
|
const [syncingScope, setSyncingScope] = useState<Submission | null>(null);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
@@ -274,6 +276,7 @@ export function AlphaPage({
|
|||||||
management_scope: managementScope,
|
management_scope: managementScope,
|
||||||
submission,
|
submission,
|
||||||
submission_blocked: submissionBlocked || undefined,
|
submission_blocked: submissionBlocked || undefined,
|
||||||
|
ppac_candidate: ppacCandidate || undefined,
|
||||||
sort,
|
sort,
|
||||||
direction,
|
direction,
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
@@ -283,6 +286,7 @@ export function AlphaPage({
|
|||||||
filters,
|
filters,
|
||||||
submission,
|
submission,
|
||||||
submissionBlocked,
|
submissionBlocked,
|
||||||
|
ppacCandidate,
|
||||||
sort,
|
sort,
|
||||||
direction,
|
direction,
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -320,6 +324,7 @@ export function AlphaPage({
|
|||||||
offset: _offset,
|
offset: _offset,
|
||||||
submission: nextSubmission,
|
submission: nextSubmission,
|
||||||
submission_blocked: nextBlocked,
|
submission_blocked: nextBlocked,
|
||||||
|
ppac_candidate: nextPpacCandidate,
|
||||||
...values
|
...values
|
||||||
} = action.filters;
|
} = action.filters;
|
||||||
const next = Object.fromEntries(
|
const next = Object.fromEntries(
|
||||||
@@ -334,6 +339,9 @@ export function AlphaPage({
|
|||||||
if (nextSubmission === "SUBMITTED" || nextSubmission === "UNSUBMITTED")
|
if (nextSubmission === "SUBMITTED" || nextSubmission === "UNSUBMITTED")
|
||||||
setSubmission(nextSubmission);
|
setSubmission(nextSubmission);
|
||||||
setSubmissionBlocked(nextBlocked === true || nextBlocked === "true");
|
setSubmissionBlocked(nextBlocked === true || nextBlocked === "true");
|
||||||
|
setPpacCandidate(
|
||||||
|
nextPpacCandidate === true || nextPpacCandidate === "true",
|
||||||
|
);
|
||||||
if (nextSort) setSort(String(nextSort));
|
if (nextSort) setSort(String(nextSort));
|
||||||
if (nextDirection) setDirection(String(nextDirection));
|
if (nextDirection) setDirection(String(nextDirection));
|
||||||
}
|
}
|
||||||
@@ -367,7 +375,7 @@ export function AlphaPage({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelected([]);
|
setSelected([]);
|
||||||
selectionVersions.current = {};
|
selectionVersions.current = {};
|
||||||
}, [filters, submission, submissionBlocked, managementScope]);
|
}, [filters, submission, submissionBlocked, ppacCandidate, managementScope]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
tablePanel.current?.querySelector(".semi-table-body")?.scrollTo({ top: 0 });
|
tablePanel.current?.querySelector(".semi-table-body")?.scrollTo({ top: 0 });
|
||||||
}, [query]);
|
}, [query]);
|
||||||
@@ -394,6 +402,7 @@ export function AlphaPage({
|
|||||||
setDirection("desc");
|
setDirection("desc");
|
||||||
setSubmission(value === "SUBMITTED" ? "SUBMITTED" : "UNSUBMITTED");
|
setSubmission(value === "SUBMITTED" ? "SUBMITTED" : "UNSUBMITTED");
|
||||||
setSubmissionBlocked(value === "SUBMISSION_BLOCKED");
|
setSubmissionBlocked(value === "SUBMISSION_BLOCKED");
|
||||||
|
setPpacCandidate(value === "PPAC_CANDIDATE");
|
||||||
setDraft({});
|
setDraft({});
|
||||||
setFilters({});
|
setFilters({});
|
||||||
setPage(1);
|
setPage(1);
|
||||||
@@ -582,8 +591,12 @@ export function AlphaPage({
|
|||||||
width: 125,
|
width: 125,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Tag
|
<Tag
|
||||||
|
size="small"
|
||||||
|
type="light"
|
||||||
color={
|
color={
|
||||||
row!.check_type?.startsWith("FAIL")
|
row!.check_type === "PPAC_CANDIDATE"
|
||||||
|
? "yellow"
|
||||||
|
: row!.check_type?.startsWith("FAIL")
|
||||||
? "red"
|
? "red"
|
||||||
: row!.check_type === "PASS"
|
: row!.check_type === "PASS"
|
||||||
? "green"
|
? "green"
|
||||||
@@ -1010,6 +1023,7 @@ export function AlphaPage({
|
|||||||
managementScope={managementScope}
|
managementScope={managementScope}
|
||||||
submission={submission}
|
submission={submission}
|
||||||
submissionBlocked={submissionBlocked}
|
submissionBlocked={submissionBlocked}
|
||||||
|
ppacCandidate={ppacCandidate}
|
||||||
selectedId={selectedViewId}
|
selectedId={selectedViewId}
|
||||||
onSelect={setSelectedViewId}
|
onSelect={setSelectedViewId}
|
||||||
onSubmissionChange={changeSubmission}
|
onSubmissionChange={changeSubmission}
|
||||||
@@ -1019,6 +1033,7 @@ export function AlphaPage({
|
|||||||
...filters,
|
...filters,
|
||||||
submission,
|
submission,
|
||||||
submission_blocked: submissionBlocked || undefined,
|
submission_blocked: submissionBlocked || undefined,
|
||||||
|
ppac_candidate: ppacCandidate || undefined,
|
||||||
sort,
|
sort,
|
||||||
direction,
|
direction,
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
@@ -1029,6 +1044,7 @@ export function AlphaPage({
|
|||||||
const {
|
const {
|
||||||
submission: savedSubmission,
|
submission: savedSubmission,
|
||||||
submission_blocked: savedBlocked,
|
submission_blocked: savedBlocked,
|
||||||
|
ppac_candidate: savedPpacCandidate,
|
||||||
sort: savedSort,
|
sort: savedSort,
|
||||||
direction: savedDirection,
|
direction: savedDirection,
|
||||||
limit,
|
limit,
|
||||||
@@ -1050,6 +1066,9 @@ export function AlphaPage({
|
|||||||
setSubmissionBlocked(
|
setSubmissionBlocked(
|
||||||
savedBlocked === true || savedBlocked === "true",
|
savedBlocked === true || savedBlocked === "true",
|
||||||
);
|
);
|
||||||
|
setPpacCandidate(
|
||||||
|
savedPpacCandidate === true || savedPpacCandidate === "true",
|
||||||
|
);
|
||||||
setSort(String(savedSort || "date_created"));
|
setSort(String(savedSort || "date_created"));
|
||||||
setDirection(String(savedDirection || "desc"));
|
setDirection(String(savedDirection || "desc"));
|
||||||
if (typeof limit === "number") setPageSize(limit);
|
if (typeof limit === "number") setPageSize(limit);
|
||||||
@@ -1336,12 +1355,16 @@ export function AlphaPage({
|
|||||||
<Empty
|
<Empty
|
||||||
className="alpha-empty"
|
className="alpha-empty"
|
||||||
title={
|
title={
|
||||||
activeFilterCount
|
ppacCandidate
|
||||||
|
? "没有符合条件的候选PPAC"
|
||||||
|
: activeFilterCount
|
||||||
? "没有符合条件的 Alpha"
|
? "没有符合条件的 Alpha"
|
||||||
: "从第一个 Alpha 开始"
|
: "从第一个 Alpha 开始"
|
||||||
}
|
}
|
||||||
description={
|
description={
|
||||||
activeFilterCount
|
ppacCandidate
|
||||||
|
? "仅收录未提交且唯一失败项为 PURE_POWER_POOL_THEME 的 Alpha,等待平台开放 PPAC 主题。"
|
||||||
|
: activeFilterCount
|
||||||
? "调整筛选条件,探索更多信号。"
|
? "调整筛选条件,探索更多信号。"
|
||||||
: "连接 WorldQuant 并同步数据,或通过 Alpha ID 导入已有研究。"
|
: "连接 WorldQuant 并同步数据,或通过 Alpha ID 导入已有研究。"
|
||||||
}
|
}
|
||||||
@@ -1351,7 +1374,9 @@ export function AlphaPage({
|
|||||||
<footer className="table-pagination" aria-label="Alpha 分页">
|
<footer className="table-pagination" aria-label="Alpha 分页">
|
||||||
<div className="alpha-table-meta">
|
<div className="alpha-table-meta">
|
||||||
<span className="muted">
|
<span className="muted">
|
||||||
{submissionBlocked
|
{ppacCandidate
|
||||||
|
? "候选PPAC"
|
||||||
|
: submissionBlocked
|
||||||
? "提交受阻"
|
? "提交受阻"
|
||||||
: submission === "UNSUBMITTED"
|
: submission === "UNSUBMITTED"
|
||||||
? "待提交"
|
? "待提交"
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export function SavedViews({
|
|||||||
columns,
|
columns,
|
||||||
submission,
|
submission,
|
||||||
submissionBlocked,
|
submissionBlocked,
|
||||||
|
ppacCandidate,
|
||||||
selectedId,
|
selectedId,
|
||||||
onSelect,
|
onSelect,
|
||||||
onSubmissionChange,
|
onSubmissionChange,
|
||||||
@@ -61,6 +62,7 @@ export function SavedViews({
|
|||||||
columns: string[];
|
columns: string[];
|
||||||
submission: Submission;
|
submission: Submission;
|
||||||
submissionBlocked: boolean;
|
submissionBlocked: boolean;
|
||||||
|
ppacCandidate: boolean;
|
||||||
selectedId: string | null;
|
selectedId: string | null;
|
||||||
onSelect: (id: string | null) => void;
|
onSelect: (id: string | null) => void;
|
||||||
onSubmissionChange: (submission: string) => void;
|
onSubmissionChange: (submission: string) => void;
|
||||||
@@ -75,6 +77,11 @@ export function SavedViews({
|
|||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [revision, setRevision] = useState(0);
|
const [revision, setRevision] = useState(0);
|
||||||
const current = items.find((item) => item.id === selectedId);
|
const current = items.find((item) => item.id === selectedId);
|
||||||
|
const fixedTab = ppacCandidate
|
||||||
|
? "PPAC_CANDIDATE"
|
||||||
|
: submissionBlocked
|
||||||
|
? "SUBMISSION_BLOCKED"
|
||||||
|
: submission;
|
||||||
const modified =
|
const modified =
|
||||||
current &&
|
current &&
|
||||||
(filterSignature(filters) !== filterSignature(current.content.filters) ||
|
(filterSignature(filters) !== filterSignature(current.content.filters) ||
|
||||||
@@ -165,10 +172,7 @@ export function SavedViews({
|
|||||||
setItems((previous) =>
|
setItems((previous) =>
|
||||||
previous.filter((item) => item.id !== deleting.id),
|
previous.filter((item) => item.id !== deleting.id),
|
||||||
);
|
);
|
||||||
if (selectedId === deleting.id)
|
if (selectedId === deleting.id) onSubmissionChange(fixedTab);
|
||||||
onSubmissionChange(
|
|
||||||
submissionBlocked ? "SUBMISSION_BLOCKED" : submission,
|
|
||||||
);
|
|
||||||
setDeleting(null);
|
setDeleting(null);
|
||||||
Toast.success("视图已删除");
|
Toast.success("视图已删除");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -185,10 +189,7 @@ export function SavedViews({
|
|||||||
type="card"
|
type="card"
|
||||||
collapsible="auto"
|
collapsible="auto"
|
||||||
tabPaneMotion={false}
|
tabPaneMotion={false}
|
||||||
activeKey={
|
activeKey={selectedId ?? fixedTab}
|
||||||
selectedId ??
|
|
||||||
(submissionBlocked ? "SUBMISSION_BLOCKED" : submission)
|
|
||||||
}
|
|
||||||
onChange={(key) => {
|
onChange={(key) => {
|
||||||
const view = items.find((item) => item.id === key);
|
const view = items.find((item) => item.id === key);
|
||||||
if (view) {
|
if (view) {
|
||||||
@@ -212,6 +213,11 @@ export function SavedViews({
|
|||||||
tab: "提交受阻",
|
tab: "提交受阻",
|
||||||
icon: <IconList aria-hidden="true" />,
|
icon: <IconList aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
itemKey: "PPAC_CANDIDATE",
|
||||||
|
tab: "候选PPAC",
|
||||||
|
icon: <IconList aria-hidden="true" />,
|
||||||
|
},
|
||||||
...items.map((view) => ({
|
...items.map((view) => ({
|
||||||
itemKey: view.id,
|
itemKey: view.id,
|
||||||
icon: <IconList aria-hidden="true" />,
|
icon: <IconList aria-hidden="true" />,
|
||||||
|
|||||||
@@ -58,7 +58,13 @@ export type Alpha = {
|
|||||||
date_created: string | null;
|
date_created: string | null;
|
||||||
date_submitted: string | null;
|
date_submitted: string | null;
|
||||||
synced_at: string;
|
synced_at: string;
|
||||||
check_type: "PENDING" | "PRE_CHECK" | "PASS" | "FAIL_1" | "FAIL_2";
|
check_type:
|
||||||
|
| "PENDING"
|
||||||
|
| "PRE_CHECK"
|
||||||
|
| "PASS"
|
||||||
|
| "PPAC_CANDIDATE"
|
||||||
|
| "FAIL_1"
|
||||||
|
| "FAIL_2";
|
||||||
failed_checks: string[];
|
failed_checks: string[];
|
||||||
neutralization: string | null;
|
neutralization: string | null;
|
||||||
sub_universe_sharpe: number | null;
|
sub_universe_sharpe: number | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user