rebuild(stage-9): deliver deterministic intelligent screening
This commit is contained in:
@@ -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); }
|
||||
}
|
||||
Reference in New Issue
Block a user