677 lines
16 KiB
TypeScript
677 lines
16 KiB
TypeScript
const API_BASE = "";
|
|
|
|
function getAuthToken(): string | null {
|
|
if (typeof window === "undefined") return null;
|
|
return localStorage.getItem("auth_token");
|
|
}
|
|
|
|
function handleUnauthorized(): void {
|
|
if (typeof window === "undefined") return;
|
|
localStorage.removeItem("auth_token");
|
|
localStorage.removeItem("auth_user");
|
|
window.location.href = "/login";
|
|
}
|
|
|
|
export async function fetchAPI<T>(path: string): Promise<T> {
|
|
const token = getAuthToken();
|
|
const headers: Record<string, string> = {};
|
|
if (token) {
|
|
headers["Authorization"] = `Bearer ${token}`;
|
|
}
|
|
|
|
const res = await fetch(`${API_BASE}${path}`, { headers });
|
|
if (res.status === 401) {
|
|
handleUnauthorized();
|
|
throw new Error("Unauthorized");
|
|
}
|
|
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
|
return res.json();
|
|
}
|
|
|
|
export async function postAPI<T>(path: string, body?: unknown): Promise<T> {
|
|
const token = getAuthToken();
|
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
if (token) {
|
|
headers["Authorization"] = `Bearer ${token}`;
|
|
}
|
|
|
|
const res = await fetch(`${API_BASE}${path}`, {
|
|
method: "POST",
|
|
headers,
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
if (res.status === 401) {
|
|
handleUnauthorized();
|
|
throw new Error("Unauthorized");
|
|
}
|
|
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
|
return res.json();
|
|
}
|
|
|
|
export async function deleteAPI<T>(path: string): Promise<T> {
|
|
const token = getAuthToken();
|
|
const headers: Record<string, string> = {};
|
|
if (token) {
|
|
headers["Authorization"] = `Bearer ${token}`;
|
|
}
|
|
|
|
const res = await fetch(`${API_BASE}${path}`, {
|
|
method: "DELETE",
|
|
headers,
|
|
});
|
|
if (res.status === 401) {
|
|
handleUnauthorized();
|
|
throw new Error("Unauthorized");
|
|
}
|
|
if (!res.ok) {
|
|
const data = await res.json().catch(() => ({}));
|
|
throw new Error(data.detail || `API error: ${res.status}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
export async function patchAPI<T>(path: string, body?: unknown): Promise<T> {
|
|
const token = getAuthToken();
|
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
if (token) {
|
|
headers["Authorization"] = `Bearer ${token}`;
|
|
}
|
|
|
|
const res = await fetch(`${API_BASE}${path}`, {
|
|
method: "PATCH",
|
|
headers,
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
if (res.status === 401) {
|
|
handleUnauthorized();
|
|
throw new Error("Unauthorized");
|
|
}
|
|
if (!res.ok) {
|
|
const data = await res.json().catch(() => ({}));
|
|
throw new Error(data.detail || `API error: ${res.status}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
export interface MarketTemperatureData {
|
|
trade_date: string;
|
|
temperature: number;
|
|
up_count: number;
|
|
down_count: number;
|
|
limit_up_count: number;
|
|
limit_down_count?: number;
|
|
max_streak?: number;
|
|
broken_rate?: number;
|
|
index_above_ma20?: boolean;
|
|
}
|
|
|
|
export interface IndexOverview {
|
|
name: string;
|
|
code: string;
|
|
close: number;
|
|
pct_chg: number;
|
|
volume: number;
|
|
realtime: boolean;
|
|
}
|
|
|
|
export interface RecommendationData {
|
|
ts_code: string;
|
|
name: string;
|
|
sector: string;
|
|
score: number;
|
|
level: string;
|
|
signal: string;
|
|
market_temp_score: number;
|
|
sector_score: number;
|
|
capital_score: number;
|
|
technical_score: number;
|
|
supply_demand_score?: number;
|
|
price_action_score?: number;
|
|
position_score?: number;
|
|
valuation_score?: number;
|
|
entry_price: number | null;
|
|
target_price: number | null;
|
|
stop_loss: number | null;
|
|
reasons: string[];
|
|
risk_note: string;
|
|
strategy?: "momentum" | "potential" | "trend_breakout";
|
|
entry_signal_type?: "breakout" | "pullback" | "launch" | "none";
|
|
llm_analysis?: string;
|
|
llm_score?: number | null;
|
|
recall_tags?: string[];
|
|
prefilter_decision?: "priority" | "watch" | "ignore" | "";
|
|
prefilter_reason?: string;
|
|
focus_points?: string[];
|
|
scan_session: string;
|
|
created_at: string | null;
|
|
entry_timing?: string;
|
|
action_plan?: "可操作" | "重点关注" | "观察";
|
|
trigger_condition?: string;
|
|
invalidation_condition?: string;
|
|
suggested_position_pct?: number;
|
|
review_after_days?: number;
|
|
lifecycle_status?: string;
|
|
data_freshness?: string;
|
|
tracking?: RecommendationTrackingSummary | null;
|
|
}
|
|
|
|
export interface RecommendationTrackingSummary {
|
|
current_price: number | null;
|
|
pct_from_entry: number | null;
|
|
max_return_pct: number | null;
|
|
max_drawdown_pct: number | null;
|
|
days_since_recommendation: number | null;
|
|
close_reason: string;
|
|
review_note: string;
|
|
track_date: string;
|
|
}
|
|
|
|
export interface LeadingStock {
|
|
ts_code: string;
|
|
name: string;
|
|
pct_chg: number;
|
|
amount: number;
|
|
limit_times?: number;
|
|
}
|
|
|
|
export interface SectorData {
|
|
sector_code: string;
|
|
sector_name: string;
|
|
pct_change: number;
|
|
trade_date?: string;
|
|
capital_inflow: number;
|
|
limit_up_count: number;
|
|
days_continuous: number;
|
|
heat_score: number;
|
|
stage?: string;
|
|
member_count?: number;
|
|
leading_stocks?: LeadingStock[];
|
|
leading_stocks_realtime?: LeadingStock[] | null;
|
|
pct_trend?: number[];
|
|
turnover_avg?: number;
|
|
main_force_ratio?: number;
|
|
realtime_pct_change?: number | null;
|
|
realtime_limit_up_count?: number | null;
|
|
realtime_amount?: number | null;
|
|
realtime_turnover_rate?: number | null;
|
|
realtime_up_count?: number | null;
|
|
realtime_down_count?: number | null;
|
|
is_realtime?: boolean;
|
|
data_mode?: "realtime_overlay" | "daily_snapshot";
|
|
structure_trade_date?: string;
|
|
}
|
|
|
|
export interface LatestResult {
|
|
market_temperature: MarketTemperatureData | null;
|
|
recommendations: RecommendationData[];
|
|
strategy_profile?: {
|
|
strategy_id: string;
|
|
name: string;
|
|
description?: string;
|
|
buy_threshold?: number;
|
|
min_score?: number;
|
|
notes?: string[];
|
|
} | null;
|
|
}
|
|
|
|
export interface DayGroup {
|
|
date: string;
|
|
count: number;
|
|
buy_count: number;
|
|
avg_score: number;
|
|
recommendations: RecommendationData[];
|
|
}
|
|
|
|
// ---------- Performance Stats ----------
|
|
|
|
export interface PerformanceStats {
|
|
total_recommendations: number;
|
|
tracked: number;
|
|
winning: number;
|
|
win_rate: number;
|
|
avg_return: number;
|
|
avg_max_return: number;
|
|
avg_max_drawdown: number;
|
|
hit_target_count: number;
|
|
hit_stop_count: number;
|
|
lifecycle_counts: Record<string, number>;
|
|
route_breakdown?: Array<{ route: string; count: number; win_rate: number; avg_return: number }>;
|
|
prefilter_breakdown?: Array<{ decision: string; count: number; win_rate: number; avg_return: number }>;
|
|
details: TrackedRecommendation[];
|
|
}
|
|
|
|
export interface TrackedRecommendation {
|
|
recommendation_id: number;
|
|
ts_code: string;
|
|
name: string;
|
|
entry_price: number;
|
|
current_price: number;
|
|
pct_from_entry: number;
|
|
hit_target: boolean;
|
|
hit_stop_loss: boolean;
|
|
status: string;
|
|
action_plan?: string;
|
|
lifecycle_status?: string;
|
|
recall_tags?: string[];
|
|
prefilter_decision?: string;
|
|
max_return_pct?: number;
|
|
max_drawdown_pct?: number;
|
|
days_since_recommendation?: number;
|
|
close_reason?: string;
|
|
review_note?: string;
|
|
track_date: string;
|
|
}
|
|
|
|
// ---------- Daily Review ----------
|
|
|
|
export interface DailyReview {
|
|
trade_date: string;
|
|
content: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface DailyReviewResponse {
|
|
reviews: DailyReview[];
|
|
}
|
|
|
|
// ---------- Strategy Board ----------
|
|
|
|
export interface StrategyFocus {
|
|
label: string;
|
|
description: string;
|
|
}
|
|
|
|
export interface StrategySectorFocus {
|
|
sector_name: string;
|
|
stage: string;
|
|
heat_score: number;
|
|
pct_change: number;
|
|
limit_up_count: number;
|
|
turnover_rate?: number;
|
|
up_count?: number;
|
|
down_count?: number;
|
|
data_mode?: string;
|
|
view: string;
|
|
}
|
|
|
|
export interface StrategyStat {
|
|
name: string;
|
|
count: number;
|
|
win_rate: number;
|
|
avg_return: number;
|
|
avg_max_return: number;
|
|
avg_max_drawdown: number;
|
|
hit_target: number;
|
|
hit_stop: number;
|
|
}
|
|
|
|
export interface StrategyAdjustment {
|
|
target: string;
|
|
action: string;
|
|
reason: string;
|
|
confidence: string;
|
|
}
|
|
|
|
export interface StrategyIterationReport {
|
|
generated_at: string;
|
|
sample_size: number;
|
|
summary: string;
|
|
strategy_stats: StrategyStat[];
|
|
signal_stats: StrategyStat[];
|
|
failure_patterns: string[];
|
|
adjustment_suggestions: StrategyAdjustment[];
|
|
ai_analysis: string;
|
|
generated_by: string;
|
|
}
|
|
|
|
export interface StrategyBoard {
|
|
trade_date: string;
|
|
data_mode?: string;
|
|
market_regime: string;
|
|
risk_level: string;
|
|
action_bias: string;
|
|
position_suggestion: string;
|
|
summary: string;
|
|
recommended_mode: string;
|
|
strategy_focus: StrategyFocus[];
|
|
watch_sectors: StrategySectorFocus[];
|
|
avoid_rules: string[];
|
|
iteration_notes: string[];
|
|
iteration_report?: StrategyIterationReport;
|
|
metrics: {
|
|
temperature?: number;
|
|
recommendation_count?: number;
|
|
actionable_count?: number;
|
|
watch_count?: number;
|
|
avg_score?: number;
|
|
win_rate?: number;
|
|
avg_return?: number;
|
|
tracked?: number;
|
|
};
|
|
ai_review: string;
|
|
generated_by: string;
|
|
}
|
|
|
|
export interface StockThesisTracking {
|
|
track_date: string;
|
|
current_price: number | null;
|
|
pct_from_entry: number | null;
|
|
max_return_pct: number | null;
|
|
max_drawdown_pct: number | null;
|
|
days_since_recommendation: number | null;
|
|
hit_target: boolean;
|
|
hit_stop_loss: boolean;
|
|
close_reason: string;
|
|
review_note: string;
|
|
status: string;
|
|
}
|
|
|
|
export interface StockThesisDiagnosis {
|
|
id: number;
|
|
diagnosis: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface StockThesisPoint {
|
|
label: string;
|
|
value: string;
|
|
}
|
|
|
|
export interface StockThesisResponse {
|
|
ts_code: string;
|
|
name: string;
|
|
has_recommendation: boolean;
|
|
recommendation: RecommendationData | null;
|
|
latest_tracking: StockThesisTracking | null;
|
|
tracking_history: StockThesisTracking[];
|
|
diagnoses: StockThesisDiagnosis[];
|
|
decision_points: StockThesisPoint[];
|
|
data_freshness: {
|
|
recommendation_created_at: string;
|
|
tracking_date: string;
|
|
status: string;
|
|
message: string;
|
|
};
|
|
}
|
|
|
|
export interface OpsStatusResponse {
|
|
scan_running: boolean;
|
|
scan_mode: string;
|
|
is_trading: boolean;
|
|
data_freshness: {
|
|
market_trade_date: string;
|
|
sector_trade_date: string;
|
|
tracking_trade_date: string;
|
|
last_recommendation_created_at: string;
|
|
last_tracking_created_at: string;
|
|
last_market_created_at: string;
|
|
last_sector_created_at: string;
|
|
last_review_created_at: string;
|
|
status: string;
|
|
message: string;
|
|
generated_at: string;
|
|
};
|
|
actions: {
|
|
key: string;
|
|
label: string;
|
|
admin_only: boolean;
|
|
}[];
|
|
}
|
|
|
|
export interface WatchlistItem {
|
|
id: number;
|
|
ts_code: string;
|
|
name: string;
|
|
note: string;
|
|
watch_group?: "observe" | "focus" | "candidate" | "holding";
|
|
cost_price?: number | null;
|
|
created_at: string;
|
|
conclusion?: string;
|
|
advice?: string;
|
|
trigger_condition?: string;
|
|
risk_note?: string;
|
|
summary?: string;
|
|
analysis_created_at?: string;
|
|
}
|
|
|
|
export interface WatchlistHistoryItem {
|
|
id: number;
|
|
user_id: number;
|
|
watchlist_id: number;
|
|
ts_code: string;
|
|
name: string;
|
|
conclusion: string;
|
|
advice: string;
|
|
trigger_condition: string;
|
|
risk_note: string;
|
|
summary: string;
|
|
full_analysis: string;
|
|
score_reference: number;
|
|
analysis_mode: string;
|
|
created_at: string;
|
|
}
|
|
|
|
// ---------- Sector Rotation ----------
|
|
|
|
export interface SectorRotationData {
|
|
trade_date: string;
|
|
dates: string[];
|
|
sectors: {
|
|
sector_code: string;
|
|
sector_name: string;
|
|
daily_data: { trade_date: string; pct_change: number; net_amount: number }[];
|
|
}[];
|
|
}
|
|
|
|
// ---------- AI Diagnosis ----------
|
|
|
|
export interface DiagnosisResult {
|
|
status: string;
|
|
ts_code?: string;
|
|
diagnosis?: string;
|
|
message?: string;
|
|
}
|
|
|
|
export interface ChatMessage {
|
|
role: "user" | "assistant";
|
|
content: string;
|
|
}
|
|
|
|
export interface StreamEvent {
|
|
type: "content" | "status";
|
|
content: string;
|
|
}
|
|
|
|
export async function* streamChat(
|
|
messages: ChatMessage[]
|
|
): AsyncGenerator<StreamEvent, void, undefined> {
|
|
const token = getAuthToken();
|
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
if (token) {
|
|
headers["Authorization"] = `Bearer ${token}`;
|
|
}
|
|
|
|
const res = await fetch(`${API_BASE}/api/chat/stream`, {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify({ messages }),
|
|
});
|
|
|
|
if (res.status === 401) {
|
|
handleUnauthorized();
|
|
throw new Error("Unauthorized");
|
|
}
|
|
if (!res.ok) throw new Error(`Chat API error: ${res.status}`);
|
|
if (!res.body) throw new Error("No response body");
|
|
|
|
const reader = res.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buffer = "";
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
const lines = buffer.split("\n");
|
|
buffer = lines.pop() || "";
|
|
|
|
for (const line of lines) {
|
|
if (line.startsWith("data: ")) {
|
|
const data = line.slice(6).trim();
|
|
if (data === "[DONE]") return;
|
|
try {
|
|
const parsed = JSON.parse(data) as StreamEvent;
|
|
yield parsed;
|
|
} catch {
|
|
// ignore malformed lines
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export interface AuthUser {
|
|
id: number;
|
|
username: string;
|
|
role: "admin" | "user";
|
|
}
|
|
|
|
export interface LoginResponse {
|
|
token: string;
|
|
user: AuthUser;
|
|
}
|
|
|
|
export async function loginAPI(username: string, password: string): Promise<LoginResponse> {
|
|
const res = await fetch(`${API_BASE}/api/auth/login`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ username, password }),
|
|
});
|
|
if (!res.ok) {
|
|
const data = await res.json().catch(() => ({}));
|
|
throw new Error(data.detail || `Login failed: ${res.status}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
// ---------- User Management ----------
|
|
|
|
export interface UserItem {
|
|
id: number;
|
|
username: string;
|
|
role: "admin" | "user";
|
|
is_active: boolean;
|
|
created_at: string | null;
|
|
}
|
|
|
|
export interface CreateUserResult {
|
|
username: string;
|
|
password: string;
|
|
role: string;
|
|
message: string;
|
|
}
|
|
|
|
export interface ResetPasswordResult {
|
|
username: string;
|
|
password: string;
|
|
message: string;
|
|
}
|
|
|
|
export async function listUsersAPI(): Promise<UserItem[]> {
|
|
return fetchAPI<UserItem[]>("/api/auth/users");
|
|
}
|
|
|
|
export async function createUserAPI(username: string, role: string): Promise<CreateUserResult> {
|
|
return postAPI<CreateUserResult>("/api/auth/users", { username, role });
|
|
}
|
|
|
|
export async function disableUserAPI(userId: number): Promise<{ message: string }> {
|
|
return deleteAPI<{ message: string }>(`/api/auth/users/${userId}`);
|
|
}
|
|
|
|
export async function resetPasswordAPI(userId: number): Promise<ResetPasswordResult> {
|
|
return postAPI<ResetPasswordResult>(`/api/auth/users/${userId}/reset-password`);
|
|
}
|
|
|
|
export async function changePasswordAPI(oldPassword: string, newPassword: string): Promise<{ message: string }> {
|
|
return postAPI<{ message: string }>("/api/auth/change-password", {
|
|
old_password: oldPassword,
|
|
new_password: newPassword,
|
|
});
|
|
}
|
|
|
|
// ---------- Data Reset (Admin) ----------
|
|
|
|
export interface DataStats {
|
|
recommendations: number;
|
|
tracking: number;
|
|
sector_heat: number;
|
|
market_temperature: number;
|
|
daily_reviews: number;
|
|
low_score_count: number;
|
|
latest_date: string;
|
|
earliest_date: string;
|
|
}
|
|
|
|
export interface DataResetResult {
|
|
status: string;
|
|
mode: string;
|
|
deleted: Record<string, number>;
|
|
}
|
|
|
|
export async function getDataStatsAPI(): Promise<DataStats> {
|
|
return fetchAPI<DataStats>("/api/auth/data-stats");
|
|
}
|
|
|
|
export async function dataResetAPI(mode: string, beforeDate?: string, minScore?: number): Promise<DataResetResult> {
|
|
return postAPI<DataResetResult>("/api/auth/data-reset", {
|
|
mode,
|
|
before_date: beforeDate,
|
|
min_score: minScore,
|
|
});
|
|
}
|
|
|
|
// ---------- Debug Logs (Admin) ----------
|
|
|
|
export interface ErrorLog {
|
|
id: number;
|
|
source: string;
|
|
level: string;
|
|
message: string;
|
|
detail: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ErrorLogsResult {
|
|
total: number;
|
|
errors: ErrorLog[];
|
|
sources: string[];
|
|
levels: string[];
|
|
}
|
|
|
|
export interface SystemStatus {
|
|
is_trading: boolean;
|
|
scan_running: boolean;
|
|
scan_locked: boolean;
|
|
recent_errors: number;
|
|
last_errors: { source: string; message: string; created_at: string }[];
|
|
tables_counts: Record<string, number>;
|
|
db_size_mb: number;
|
|
}
|
|
|
|
export async function getErrorLogsAPI(limit: number = 50, source?: string, level?: string, days: number = 7): Promise<ErrorLogsResult> {
|
|
const params = new URLSearchParams({ limit: String(limit), days: String(days) });
|
|
if (source) params.set("source", source);
|
|
if (level) params.set("level", level);
|
|
return fetchAPI<ErrorLogsResult>(`/api/debug/errors?${params}`);
|
|
}
|
|
|
|
export async function clearErrorLogsAPI(days: number = 30): Promise<{ status: string; deleted: number }> {
|
|
return deleteAPI<{ status: string; deleted: number }>(`/api/debug/errors?days=${days}`);
|
|
}
|
|
|
|
export async function getSystemStatusAPI(): Promise<SystemStatus> {
|
|
return fetchAPI<SystemStatus>("/api/debug/system");
|
|
}
|