rebuild(stage-9): deliver deterministic intelligent screening

This commit is contained in:
leefer
2026-07-30 05:15:17 +08:00
parent 6cb52e864a
commit 158257ebb8
46 changed files with 7322 additions and 34 deletions
+110
View File
@@ -0,0 +1,110 @@
import { api } from "./client";
export type FormulaCondition = { field: string; op: string; value: unknown };
export type FormulaScore = { field: string; weight: number; direction: "asc" | "desc" };
export type ScreenerFormula = {
universe: { exclude_st: boolean; listed_days_min: number };
filters: FormulaCondition[];
score: FormulaScore[];
limit: number;
min_score: number;
meta?: Record<string, string>;
};
export type ScreenerStrategy = {
id: string;
version: number;
kind: "stage" | "curated";
name: string;
display_name: string;
description: string;
regimes: string[];
formula: ScreenerFormula;
};
export type Candidate = {
identifier: string;
code: string;
name: string;
sector: string;
close: number | null;
pct_chg: number | null;
amount_billion: number | null;
score_display: number;
reason: string;
risk_flags: string[];
};
export type ScreenerRun = {
id: number;
mode: "stage" | "curated" | "custom";
strategy_id: string;
strategy_name: string;
selection_date: string;
status: "pending" | "running" | "completed" | "no_signal" | "data_incomplete" | "failed";
coverage: number;
missing_fields: string[];
items: Candidate[];
error_message: string;
};
export type CustomStrategy = {
id: number;
name: string;
version: number;
formula: ScreenerFormula;
};
export type ScreenerCatalog = {
factor_groups: Record<string, string[]>;
factors: Record<string, string>;
stage: ScreenerStrategy[];
curated: ScreenerStrategy[];
};
export type ScreenerWorkspace = {
trade_date: string | null;
message: string;
catalog: ScreenerCatalog;
stage_runs: ScreenerRun[];
curated_runs: ScreenerRun[];
custom_strategies: CustomStrategy[];
custom_runs: ScreenerRun[];
};
export type StrategyTrack = {
id: number;
code: string;
name: string;
sector: string;
selection_date: string;
strategy_name: string;
entry_price: number;
t1_open_return: number | null;
t1_return: number | null;
t3_return: number | null;
t5_return: number | null;
max_gain: number | null;
max_drawdown: number | null;
observed_days: number;
};
export const screenerApi = {
catalog(): Promise<ScreenerCatalog> {
return api.get("/screener/catalog");
},
workspace(date: string): Promise<ScreenerWorkspace> {
return api.get(`/screener?date=${encodeURIComponent(date)}`);
},
saveCustom(name: string, formula: ScreenerFormula): Promise<CustomStrategy> {
return api.put("/screener/custom", { name, formula });
},
deleteCustom(id: number): Promise<{ message: string }> {
return api.delete(`/screener/custom/${id}`);
},
runCustom(id: number, date: string): Promise<ScreenerRun> {
return api.post(`/screener/custom/${id}/run?date=${encodeURIComponent(date)}`);
},
tracks(): Promise<StrategyTrack[]> {
return api.get("/screener/tracks");
},
addTrack(runId: number, identifier: string): Promise<{ id: number }> {
return api.post("/screener/tracks", { run_id: runId, identifier });
},
removeTrack(id: number): Promise<{ message: string }> {
return api.delete(`/screener/tracks/${id}`);
},
};
@@ -0,0 +1,102 @@
.screener-page,
.tracking-page { display: grid; align-content: start; gap: var(--layout-gap); overflow-y: auto; }
.screener-page .page-header { margin-bottom: 0; }
.screener-page .page-actions { display: flex; align-items: center; gap: var(--s-10); }
.tracking-entry { min-height: var(--s-36); display: flex; align-items: center; gap: var(--s-8); padding: var(--s-4) var(--s-10); border: var(--s-1) solid var(--color-warning); border-radius: var(--control-radius); color: var(--color-warning); background: var(--color-warning-soft); }
.tracking-entry span { display: grid; place-items: center; width: var(--s-24); height: var(--s-24); border-radius: var(--radius-round); color: var(--color-surface); background: var(--color-warning); font-size: var(--font-10-5); }
.tracking-entry strong { font-size: var(--font-12); }
.stage-overview { overflow: hidden; }
.stage-heading { display: grid; grid-template-columns: var(--s-260) minmax(0, 1fr) auto; align-items: center; gap: var(--s-20); padding: var(--s-16) var(--s-20); }
.stage-heading > div { display: grid; gap: var(--s-4); }
.stage-heading > div span { color: var(--color-text-secondary); font-size: var(--font-11); }
.stage-heading > div strong { font-size: var(--font-18); }
.stage-heading p { color: var(--color-text-secondary); line-height: var(--s-20); }
.stage-flow { display: flex; align-items: center; justify-content: center; gap: var(--s-8); padding: var(--s-10) var(--s-20); border-top: var(--s-1) solid var(--color-divider); background: var(--color-surface-muted); }
.stage-flow span { color: var(--color-text-secondary); font-size: var(--font-11); }
.stage-flow span.done { color: var(--color-down); }
.stage-flow i { width: var(--s-44); height: var(--s-1); background: var(--color-border); }
.stage-run-tabs { display: flex; gap: var(--s-6); overflow-x: auto; padding: var(--s-8) var(--s-12); border-top: var(--s-1) solid var(--color-divider); }
.stage-run-tabs button { padding: var(--s-6) var(--s-10); border-radius: var(--control-radius); color: var(--color-text-secondary); background: var(--color-surface); }
.stage-run-tabs button.active { color: var(--color-primary); background: var(--color-primary-soft); }
.strategy-layout { display: grid; grid-template-columns: var(--s-360) minmax(0, 1fr); gap: var(--layout-gap); min-height: var(--s-360); }
.strategy-library { min-width: 0; overflow: hidden; }
.strategy-library .card-header .tag { margin-left: auto; }
.strategy-tools { display: grid; grid-template-columns: minmax(0, 1fr) var(--s-200) auto; gap: var(--s-6); padding: var(--s-8); border-bottom: var(--s-1) solid var(--color-divider); }
.strategy-tools .input,
.strategy-tools .select { min-height: var(--s-32); padding: var(--s-4) var(--s-8); }
.strategy-items { max-height: var(--s-320); overflow-y: auto; }
.strategy-items.is-list { display: grid; }
.strategy-items.is-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-6); padding: var(--s-6); }
.strategy-items > button { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--s-2) var(--s-8); padding: var(--s-8) var(--s-10); border-bottom: var(--s-1) solid var(--color-divider); background: var(--color-surface); text-align: left; }
.strategy-items.is-grid > button { border: var(--s-1) solid var(--color-border); border-radius: var(--control-radius); }
.strategy-items > button:hover,
.strategy-items > button.active { background: var(--color-primary-soft); }
.strategy-items strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.strategy-items small { grid-column: 1; color: var(--color-text-faint); }
.strategy-status { grid-column: 2; grid-row: 1 / span 2; align-self: center; color: var(--color-text-faint); font-size: var(--font-10-5); }
.strategy-status.completed { color: var(--color-up); }
.strategy-status.data_incomplete { color: var(--color-warning); }
.strategy-detail { min-width: 0; padding: var(--s-20); }
.strategy-detail > header { display: flex; justify-content: space-between; gap: var(--s-12); }
.strategy-detail > header div { display: grid; gap: var(--s-6); }
.strategy-detail > header span { color: var(--color-text-faint); font-size: var(--font-11); }
.strategy-detail h2 { font-size: var(--font-18); }
.strategy-detail > p { max-width: var(--s-dialog-wide); margin-top: var(--s-16); color: var(--color-text-secondary); line-height: var(--s-20); }
.strategy-detail dl { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s-10); margin: var(--s-20) 0; }
.strategy-detail dl div { display: grid; gap: var(--s-6); padding: var(--s-10); border: var(--s-1) solid var(--color-border); border-radius: var(--control-radius); background: var(--color-surface-muted); }
.strategy-detail dt { color: var(--color-text-faint); font-size: var(--font-11); }
.strategy-detail dd { margin: 0; }
.strategy-conditions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-8); }
.strategy-conditions h3 { width: 100%; font-size: var(--font-12); }
.strategy-conditions span { padding: var(--s-6) var(--s-8); border-radius: var(--tag-radius); color: var(--color-text-secondary); background: var(--color-surface-muted); font-size: var(--font-11); }
.screener-results { min-width: 0; overflow: hidden; }
.screener-results .card-header > div { display: flex; align-items: baseline; gap: var(--s-10); }
.screener-results .card-header .tag { margin-left: auto; }
.screener-results .notice { margin: var(--s-12); }
.screener-empty { min-height: var(--s-64); display: grid; place-content: center; gap: var(--s-4); padding: var(--s-20); color: var(--color-text-secondary); text-align: center; }
.screener-empty strong { color: var(--color-text); }
.custom-builder { display: grid; grid-template-columns: minmax(0, 1fr) var(--s-320); overflow: hidden; }
.custom-factors { min-width: 0; border-right: var(--s-1) solid var(--color-divider); }
.custom-factors .card-header .tag { margin-left: auto; }
.factor-add { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--s-8); padding: var(--s-10); }
.factor-list { display: grid; gap: var(--s-1); background: var(--color-divider); }
.factor-list > div { display: grid; grid-template-columns: minmax(0, 1fr) var(--s-64) var(--s-200) var(--s-64) var(--s-32); align-items: center; gap: var(--s-10); padding: var(--s-8) var(--s-10); background: var(--color-surface); }
.factor-list .input { min-height: var(--s-32); padding: var(--s-4) var(--s-6); }
.factor-direction { min-height: var(--s-32); padding: var(--s-4); }
.custom-filters { min-width: 0; }
.filter-list { display: grid; gap: var(--s-6); padding: var(--s-10); border-bottom: var(--s-1) solid var(--color-divider); }
.filter-list > div { display: grid; grid-template-columns: minmax(0, 1fr) var(--s-64) var(--s-64) var(--s-32); gap: var(--s-6); }
.filter-list .input,
.filter-list .select { min-height: var(--s-32); padding: var(--s-4) var(--s-6); }
.filter-list > .btn { width: fit-content; }
.custom-options { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s-8); padding: var(--s-10); }
.switch-field { grid-column: 1 / -1; display: flex; align-items: center; gap: var(--s-8); color: var(--color-text-secondary); font-size: var(--font-12); }
.custom-save { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: var(--s-8); padding: 0 var(--s-10) var(--s-10); }
.custom-save .btn-primary { width: fit-content; }
.custom-library { overflow: hidden; }
.custom-strategy-list { display: flex; gap: var(--s-8); overflow-x: auto; padding: var(--s-10); }
.custom-strategy-list > button { min-width: var(--s-200); display: flex; justify-content: space-between; gap: var(--s-8); padding: var(--s-10); border: var(--s-1) solid var(--color-border); border-radius: var(--control-radius); background: var(--color-surface); }
.custom-strategy-list > button.active { border-color: var(--color-primary); background: var(--color-primary-soft); }
.custom-strategy-list span { color: var(--color-text-faint); font-size: var(--font-11); }
.custom-actions { display: flex; gap: var(--s-8); padding: 0 var(--s-10) var(--s-10); }
.tracking-page .page-header > div { display: grid; gap: var(--s-6); }
.tracking-page .page-header .btn { width: fit-content; padding-left: 0; }
.tracking-table { overflow: hidden; }
@media (max-width: 1023px) {
.screener-page .page-actions { width: 100%; align-items: stretch; flex-direction: column; }
.screener-page .seg-control { width: 100%; overflow-x: auto; }
.screener-page .seg-control button { flex: 1; }
.stage-heading,
.strategy-layout,
.custom-builder { grid-template-columns: minmax(0, 1fr); }
.stage-heading { gap: var(--s-8); }
.stage-flow { justify-content: flex-start; overflow-x: auto; }
.strategy-library { max-height: var(--s-400); }
.strategy-detail dl { grid-template-columns: minmax(0, 1fr); }
.custom-factors { border-right: 0; border-bottom: var(--s-1) solid var(--color-divider); }
.factor-list > div { grid-template-columns: minmax(0, 1fr) var(--s-64) var(--s-64) var(--s-44); }
.factor-list > div input[type="range"] { grid-column: 1 / -1; grid-row: 2; }
.custom-options { grid-template-columns: minmax(0, 1fr); }
.custom-save { grid-template-columns: minmax(0, 1fr); }
}