mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
* feat: adapt AlphaSift hotspot topics * fix(review-feedback-1635): preserve cached hotspots when refresh returns no rows and Avoid * fix(review-feedback-1635): Reload detail when refreshing the selected hotspot and Fall back when * fix(review-feedback-1635): update by the requested topic or a request token * fix(review-feedback-1635): preserve analysis when optional serve startup fails and Fall back when * fix(review-feedback-1635): 当前 CI 状态为 failure,且 backend-gate 为 cancelled、docker-build 为 skipped * fix(review-feedback-1635): 让 backend-gate / docker-build 在当前 head 上完成并通过,或给出可接受的维护者级豁免说明 * fix(review-feedback-1635): 当前 CI 仍未闭环:结构化事实显示 backend-gate 被取消、docker-build skipped * fix(review-feedback-1635): 修复 hotspot detail 的上游 schema 容错问题,并让当前 head 的阻断 CI 重新跑通 * fix(review-feedback-1635): docs/CHANGELOG.md:当前 diff 不是在 [Unreleased] 扁平格式下追加本 PR * fix(review-feedback-1635): CI 闭环未完成:结构化事实显示当前 CI 状态为 failure,backend-gate cancelled、docker-build * fix(review-feedback-1635): 让当前 head 的阻断型 CI 完整通过,并修正文档中的 AlphaSift commit hash 不一致 * fix(review-feedback-1635): CI 仍未在当前 head 完整通过:结构化事实显示 * fix(review-feedback-1635): 修复 docs/CHANGELOG.md 对主线既有 [Unreleased] 条目的删除,并补齐 AlphaSift * fix(review-feedback-1635): Accept slash-containing hotspot topics * fix(review-feedback-1635): Try industry constituents for industry hotspots * fix(review-feedback-1635): Clear old hotspot details when loading a new topic and preserve the * test: isolate main schedule environment * chore: clean AlphaSift PR scope * fix: respect AlphaSift hotspot fallbacks
316 lines
9.2 KiB
TypeScript
316 lines
9.2 KiB
TypeScript
import apiClient from './index';
|
||
import { systemConfigApi } from './systemConfig';
|
||
import { toCamelCase } from './utils';
|
||
|
||
const ALPHASIFT_SCREEN_TIMEOUT_MS = 180000;
|
||
const ALPHASIFT_INSTALL_TIMEOUT_MS = 300000;
|
||
export const ALPHASIFT_CONFIG_CHANGED_EVENT = 'alphasift-config-changed';
|
||
export const SYSTEM_CONFIG_CHANGED_EVENT = 'dsa-system-config-changed';
|
||
|
||
export type AlphaSiftStatus = {
|
||
enabled: boolean;
|
||
available: boolean;
|
||
installSpecIsDefault: boolean;
|
||
contractVersion?: string | null;
|
||
version?: string | null;
|
||
strategyCount?: number | null;
|
||
diagnostics?: Record<string, string>;
|
||
};
|
||
|
||
export type AlphaSiftInstallResponse = {
|
||
installed: boolean;
|
||
alreadyInstalled: boolean;
|
||
installSpecIsDefault: boolean;
|
||
};
|
||
|
||
export type AlphaSiftCandidate = {
|
||
rank: number;
|
||
code: string;
|
||
name: string;
|
||
score?: number | null;
|
||
screenScore?: number | null;
|
||
reason: string;
|
||
riskLevel?: string;
|
||
riskFlags?: string[];
|
||
llmScore?: number | null;
|
||
llmConfidence?: number | null;
|
||
llmSector?: string;
|
||
llmTheme?: string;
|
||
llmTags?: string[];
|
||
llmThesis?: string;
|
||
llmCatalysts?: string[];
|
||
llmRisks?: string[];
|
||
llmWatchItems?: string[];
|
||
llmInvalidators?: string[];
|
||
llmStyleFit?: string;
|
||
price?: number | null;
|
||
changePct?: number | null;
|
||
amount?: number | null;
|
||
industry?: string;
|
||
factorScores?: Record<string, number>;
|
||
postAnalysisSummaries?: Record<string, string>;
|
||
postAnalysisTags?: string[];
|
||
dsaContext?: {
|
||
enriched?: boolean;
|
||
quote?: Record<string, unknown>;
|
||
fundamentals?: Record<string, unknown>;
|
||
news?: {
|
||
success?: boolean;
|
||
query?: string;
|
||
provider?: string;
|
||
results?: Array<Record<string, unknown>>;
|
||
error?: string | null;
|
||
};
|
||
warnings?: string[];
|
||
};
|
||
dsaNews?: Array<{
|
||
title?: string;
|
||
snippet?: string;
|
||
url?: string;
|
||
source?: string;
|
||
publishedDate?: string | null;
|
||
}>;
|
||
dsaAnalysisSummary?: string;
|
||
raw: Record<string, unknown>;
|
||
};
|
||
|
||
export type AlphaSiftStrategy = {
|
||
id: string;
|
||
name: string;
|
||
title?: string;
|
||
description: string;
|
||
version?: string;
|
||
category?: string;
|
||
tag?: string;
|
||
tags?: string[];
|
||
marketScope?: string[];
|
||
market?: string;
|
||
};
|
||
|
||
export type AlphaSiftStrategiesResponse = {
|
||
enabled: boolean;
|
||
strategies: AlphaSiftStrategy[];
|
||
strategyCount: number;
|
||
};
|
||
|
||
export type AlphaSiftHotspot = {
|
||
topic: string;
|
||
name?: string;
|
||
source?: string;
|
||
rank?: number | null;
|
||
changePct?: number | null;
|
||
heatScore?: number | null;
|
||
trendScore?: number | null;
|
||
persistenceScore?: number | null;
|
||
coolingScore?: number | null;
|
||
observations?: number | null;
|
||
state?: string;
|
||
stage?: string;
|
||
sampleStockCount?: number | null;
|
||
leaders?: string[];
|
||
providerUsed?: string;
|
||
fallbackUsed?: boolean;
|
||
cacheUsed?: boolean;
|
||
cachedAt?: string | null;
|
||
sourceErrors?: string[];
|
||
stale?: boolean;
|
||
staleAgeHours?: number | null;
|
||
};
|
||
|
||
export type AlphaSiftHotspotRouteItem = {
|
||
title: string;
|
||
description: string;
|
||
source?: string;
|
||
};
|
||
|
||
export type AlphaSiftHotspotStock = {
|
||
code?: string;
|
||
name?: string;
|
||
changePct?: number | null;
|
||
amount?: number | null;
|
||
turnoverRate?: number | null;
|
||
volumeRatio?: number | null;
|
||
role?: string;
|
||
hotStockScore?: number | null;
|
||
};
|
||
|
||
export type AlphaSiftHotspotDetail = {
|
||
enabled: boolean;
|
||
provider: string;
|
||
topic: string;
|
||
name?: string;
|
||
summary?: string;
|
||
route: AlphaSiftHotspotRouteItem[];
|
||
stocks: AlphaSiftHotspotStock[];
|
||
stockCount: number;
|
||
sourceErrors?: string[];
|
||
};
|
||
|
||
export type AlphaSiftHotspotsResponse = {
|
||
enabled: boolean;
|
||
provider: string;
|
||
providerUsed?: string;
|
||
fallbackUsed?: boolean;
|
||
cacheUsed?: boolean;
|
||
cachedAt?: string | null;
|
||
sourceErrors?: string[];
|
||
stale?: boolean;
|
||
staleAgeHours?: number | null;
|
||
hotspots: AlphaSiftHotspot[];
|
||
hotspotCount: number;
|
||
};
|
||
|
||
export type AlphaSiftScreenResponse = {
|
||
enabled: boolean;
|
||
candidates: AlphaSiftCandidate[];
|
||
candidateCount: number;
|
||
runId?: string;
|
||
strategy?: string;
|
||
market?: string;
|
||
snapshotCount?: number;
|
||
afterFilterCount?: number;
|
||
llmRanked?: boolean;
|
||
llmMarketView?: string;
|
||
llmSelectionLogic?: string;
|
||
llmPortfolioRisk?: string;
|
||
llmCoverage?: number | null;
|
||
llmParseErrors?: string[];
|
||
warnings?: string[];
|
||
sourceErrors?: string[];
|
||
dsaEnrichment?: {
|
||
enabled?: boolean;
|
||
maxCandidates?: number;
|
||
requestedCount?: number;
|
||
enrichedCount?: number;
|
||
warnings?: string[];
|
||
};
|
||
deepAnalysisRequested?: boolean | null;
|
||
postAnalyzers?: string[];
|
||
dailyEnriched?: boolean | null;
|
||
dailyEnrichCount?: number | null;
|
||
riskEnabled?: boolean | null;
|
||
portfolioDiversityEnabled?: boolean | null;
|
||
portfolioConcentrationNotes?: string[];
|
||
};
|
||
|
||
export type AlphaSiftScreenAccepted = {
|
||
taskId: string;
|
||
traceId?: string | null;
|
||
status: 'pending' | 'processing' | 'completed' | 'failed' | string;
|
||
message: string;
|
||
strategy: string;
|
||
market: string;
|
||
maxResults: number;
|
||
};
|
||
|
||
export type AlphaSiftScreenTaskStatus = {
|
||
taskId: string;
|
||
traceId?: string | null;
|
||
status: 'pending' | 'processing' | 'completed' | 'failed' | string;
|
||
progress?: number | null;
|
||
message?: string | null;
|
||
error?: string | null;
|
||
result?: AlphaSiftScreenResponse | null;
|
||
};
|
||
|
||
export function notifyAlphaSiftConfigChanged(): void {
|
||
window.dispatchEvent(new Event(ALPHASIFT_CONFIG_CHANGED_EVENT));
|
||
notifySystemConfigChanged();
|
||
}
|
||
|
||
export function notifySystemConfigChanged(): void {
|
||
window.dispatchEvent(new Event(SYSTEM_CONFIG_CHANGED_EVENT));
|
||
}
|
||
|
||
async function setAlphaSiftEnabled(value: 'true' | 'false'): Promise<void> {
|
||
const config = await systemConfigApi.getConfig(false);
|
||
await systemConfigApi.update({
|
||
configVersion: config.configVersion,
|
||
maskToken: config.maskToken,
|
||
reloadNow: true,
|
||
items: [{ key: 'ALPHASIFT_ENABLED', value }],
|
||
});
|
||
notifyAlphaSiftConfigChanged();
|
||
}
|
||
|
||
export const alphasiftApi = {
|
||
async getStatus(): Promise<AlphaSiftStatus> {
|
||
const response = await apiClient.get<Record<string, unknown>>('/api/v1/alphasift/status');
|
||
return toCamelCase<AlphaSiftStatus>(response.data);
|
||
},
|
||
|
||
async screen(payload: { market: string; strategy: string; maxResults: number }): Promise<AlphaSiftScreenResponse> {
|
||
const response = await apiClient.post<Record<string, unknown>>('/api/v1/alphasift/screen', {
|
||
market: payload.market,
|
||
strategy: payload.strategy,
|
||
max_results: payload.maxResults,
|
||
}, { timeout: ALPHASIFT_SCREEN_TIMEOUT_MS });
|
||
return toCamelCase<AlphaSiftScreenResponse>(response.data);
|
||
},
|
||
|
||
async startScreen(payload: { market: string; strategy: string; maxResults: number }): Promise<AlphaSiftScreenAccepted> {
|
||
const response = await apiClient.post<Record<string, unknown>>('/api/v1/alphasift/screen/tasks', {
|
||
market: payload.market,
|
||
strategy: payload.strategy,
|
||
max_results: payload.maxResults,
|
||
});
|
||
return toCamelCase<AlphaSiftScreenAccepted>(response.data);
|
||
},
|
||
|
||
async getScreenTask(taskId: string): Promise<AlphaSiftScreenTaskStatus> {
|
||
const response = await apiClient.get<Record<string, unknown>>(`/api/v1/alphasift/screen/tasks/${encodeURIComponent(taskId)}`);
|
||
return toCamelCase<AlphaSiftScreenTaskStatus>(response.data);
|
||
},
|
||
|
||
async getStrategies(): Promise<AlphaSiftStrategiesResponse> {
|
||
const response = await apiClient.get<Record<string, unknown>>('/api/v1/alphasift/strategies', { timeout: ALPHASIFT_INSTALL_TIMEOUT_MS });
|
||
return toCamelCase<AlphaSiftStrategiesResponse>(response.data);
|
||
},
|
||
|
||
async getHotspots(payload: { provider?: string; top?: number; refresh?: boolean } = {}): Promise<AlphaSiftHotspotsResponse> {
|
||
const response = await apiClient.get<Record<string, unknown>>('/api/v1/alphasift/hotspots', {
|
||
params: {
|
||
provider: payload.provider || 'akshare',
|
||
top: payload.top ?? 12,
|
||
refresh: payload.refresh ?? false,
|
||
},
|
||
timeout: ALPHASIFT_INSTALL_TIMEOUT_MS,
|
||
});
|
||
return toCamelCase<AlphaSiftHotspotsResponse>(response.data);
|
||
},
|
||
|
||
async getHotspotDetail(payload: { topic: string; provider?: string }): Promise<AlphaSiftHotspotDetail> {
|
||
const response = await apiClient.get<Record<string, unknown>>(
|
||
`/api/v1/alphasift/hotspots/${encodeURIComponent(payload.topic)}`,
|
||
{
|
||
params: { provider: payload.provider || 'akshare' },
|
||
timeout: ALPHASIFT_INSTALL_TIMEOUT_MS,
|
||
},
|
||
);
|
||
return toCamelCase<AlphaSiftHotspotDetail>(response.data);
|
||
},
|
||
|
||
async install(): Promise<AlphaSiftInstallResponse> {
|
||
const response = await apiClient.post<Record<string, unknown>>('/api/v1/alphasift/install', {}, { timeout: ALPHASIFT_INSTALL_TIMEOUT_MS });
|
||
return toCamelCase<AlphaSiftInstallResponse>(response.data);
|
||
},
|
||
|
||
async enable(): Promise<void> {
|
||
await setAlphaSiftEnabled('true');
|
||
try {
|
||
const status = await alphasiftApi.getStatus();
|
||
if (!status.available) {
|
||
const reason = status.diagnostics?.reason ? `(${status.diagnostics.reason})` : '';
|
||
throw new Error(`AlphaSift 适配层不可用${reason}。请确认后端已安装项目依赖,必要时执行 pip install -r requirements.txt 或重建 Docker/桌面后端。`);
|
||
}
|
||
} catch (error) {
|
||
try {
|
||
await setAlphaSiftEnabled('false');
|
||
} catch {
|
||
// Preserve the original install/status failure for the caller.
|
||
}
|
||
throw error;
|
||
}
|
||
},
|
||
};
|