From f5615d9580041b1cf6b3327e31520aca6939a55a Mon Sep 17 00:00:00 2001 From: fish Date: Mon, 11 May 2026 21:18:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=97=A5=E5=86=85=E6=96=B9?= =?UTF-8?q?=E5=90=91=E5=88=86=E6=9E=90=E5=8A=9F=E8=83=BD=EF=BC=9A=E5=9F=BA?= =?UTF-8?q?=E4=BA=8E=E4=B8=89=E5=B1=82=E6=89=93=E5=88=86=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E7=94=B1=20AI=20=E6=89=B9=E9=87=8F=E7=94=9F=E6=88=90=E4=B8=8B?= =?UTF-8?q?=E4=B8=80=E4=B8=AA=E4=BA=A4=E6=98=93=E6=97=A5=E6=96=B9=E5=90=91?= =?UTF-8?q?=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/handlers/daily_direction.go | 409 ++++++++++++++++++ web/backend/internal/router/router.go | 2 + web/backend/internal/store/futures.go | 120 +++++ web/backend/main.go | 3 + web/frontend/src/App.vue | 2 + web/frontend/src/api/daily.ts | 46 ++ web/frontend/src/router/index.ts | 5 + web/frontend/src/views/DailyDirectionView.vue | 159 +++++++ 8 files changed, 746 insertions(+) create mode 100644 web/backend/internal/handlers/daily_direction.go create mode 100644 web/frontend/src/api/daily.ts create mode 100644 web/frontend/src/views/DailyDirectionView.vue diff --git a/web/backend/internal/handlers/daily_direction.go b/web/backend/internal/handlers/daily_direction.go new file mode 100644 index 0000000..6a6bef0 --- /dev/null +++ b/web/backend/internal/handlers/daily_direction.go @@ -0,0 +1,409 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "strconv" + "strings" + "time" + + "trade/web/internal/store" +) + +// 所有已配置品种(与 tushare/src/contracts.py 保持一致)。 +var allSymbols = []string{"FG", "SA", "RB", "MA", "CF", "M"} + +type ddRunRequest struct { + TradeDate string `json:"trade_date,omitempty"` // YYYYMMDD, 默认今天 + Symbols []string `json:"symbols,omitempty"` // 默认全部 +} + +type ddRunResponse struct { + TradeDate string `json:"trade_date"` + Results []ddSymbolResult `json:"results"` + Errors []ddSymbolResult `json:"errors,omitempty"` +} + +type ddSymbolResult struct { + Symbol string `json:"symbol"` + Direction string `json:"direction"` + Confidence float64 `json:"confidence"` + Error string `json:"error,omitempty"` +} + +// DailyDirectionRun 对多个品种批量执行 AI 方向分析。 +func (d *Deps) DailyDirectionRun(w http.ResponseWriter, r *http.Request) { + var req ddRunRequest + if r.Body != nil { + _ = json.NewDecoder(r.Body).Decode(&req) + } + if req.TradeDate == "" { + req.TradeDate = time.Now().Format("20060102") + } + if len(req.Symbols) == 0 { + req.Symbols = allSymbols + } + + llmCfg := d.resolveLLMConfig() + if llmCfg.APIKey == "" { + writeErr(w, http.StatusServiceUnavailable, "LLM API Key 未配置,请在管理后台设置") + return + } + + resp := ddRunResponse{TradeDate: req.TradeDate} + + for _, sym := range req.Symbols { + result, err := d.analyzeOneDirection(llmCfg, sym, req.TradeDate) + if err != nil { + resp.Errors = append(resp.Errors, ddSymbolResult{Symbol: sym, Error: err.Error()}) + continue + } + resp.Results = append(resp.Results, *result) + } + + writeJSON(w, http.StatusOK, resp) +} + +// analyzeOneDirection 对单个品种做方向分析并持久化。 +func (d *Deps) analyzeOneDirection(llmCfg *AIConfig, symbol, tradeDate string) (*ddSymbolResult, error) { + // 1. 找到活跃合约 + tsCode, err := d.Futures.GetActiveTsCode(symbol, tradeDate) + if err != nil { + return nil, fmt.Errorf("查找活跃合约: %w", err) + } + + // 2. 拉取分析上下文 + ctx, err := d.Futures.GetAnalysisContext(tsCode, tradeDate) + if err != nil { + return nil, fmt.Errorf("获取分析数据: %w", err) + } + + // 3. 构建方向分析 prompt + prompt := buildDirectionPrompt(ctx, symbol) + promptSnapshot := prompt[len(prompt)-1]["content"] + + // 4. 调 LLM(非流式) + rawJSON, err := callLLM(llmCfg, prompt) + if err != nil { + return nil, fmt.Errorf("LLM 调用失败: %w", err) + } + + // 5. 解析 JSON + parsed, err := parseDirectionJSON(rawJSON) + if err != nil { + log.Printf("[daily-direction] %s JSON parse failed, raw: %s", symbol, rawJSON) + return nil, fmt.Errorf("AI 返回格式异常: %w", err) + } + + // 6. 计算 target_date(简化:+1天,周五 +3天) + targetDate := nextTradeDate(tradeDate) + + // 7. 持久化 + supportJSON, _ := json.Marshal(parsed.Support) + resistJSON, _ := json.Marshal(parsed.Resistance) + + dd := &store.DailyDirection{ + Symbol: symbol, + TradeDate: tradeDate, + TargetDate: targetDate, + Direction: parsed.Direction, + Confidence: parsed.Confidence, + Support: string(supportJSON), + Resistance: string(resistJSON), + Reasoning: parsed.Reasoning, + RiskNote: parsed.RiskNote, + PromptSnapshot: promptSnapshot, + } + if err := d.Futures.SaveDailyDirection(dd); err != nil { + return nil, fmt.Errorf("保存失败: %w", err) + } + + return &ddSymbolResult{ + Symbol: symbol, + Direction: parsed.Direction, + Confidence: parsed.Confidence, + }, nil +} + +// directionJSON LLM 返回的期望结构。 +type directionJSON struct { + Direction string `json:"direction"` + Confidence float64 `json:"confidence"` + Support []float64 `json:"support"` + Resistance []float64 `json:"resistance"` + Reasoning string `json:"reasoning"` + RiskNote string `json:"risk_note"` +} + +// parseDirectionJSON 从 LLM 原始响应中提取结构化结果。 +func parseDirectionJSON(raw string) (*directionJSON, error) { + raw = strings.TrimSpace(raw) + + // 尝试直接解析 + var d directionJSON + if err := json.Unmarshal([]byte(raw), &d); err == nil { + return &d, d.validate() + } + + // 尝试提取 markdown code block 中的 JSON + if idx := strings.Index(raw, "```json"); idx >= 0 { + rest := raw[idx+7:] + if end := strings.Index(rest, "```"); end > 0 { + raw = strings.TrimSpace(rest[:end]) + if err := json.Unmarshal([]byte(raw), &d); err == nil { + return &d, d.validate() + } + } + } + if idx := strings.Index(raw, "```"); idx >= 0 { + rest := raw[idx+3:] + if end := strings.Index(rest, "```"); end > 0 { + raw = strings.TrimSpace(rest[:end]) + if err := json.Unmarshal([]byte(raw), &d); err == nil { + return &d, d.validate() + } + } + } + + return nil, fmt.Errorf("无法解析 AI 返回的 JSON") +} + +func (d *directionJSON) validate() error { + d.Direction = strings.TrimSpace(strings.ToLower(d.Direction)) + switch d.Direction { + case "bullish", "bearish", "neutral": + default: + return fmt.Errorf("无效的 direction 值: %s", d.Direction) + } + if d.Confidence < 0 || d.Confidence > 100 { + d.Confidence = max(0, min(100, d.Confidence)) + } + return nil +} + +// callLLM 非流式调用 LLM,返回 content 文本。 +func callLLM(cfg *AIConfig, prompt []map[string]string) (string, error) { + body := map[string]any{ + "model": cfg.Model, + "messages": prompt, + "stream": false, + } + payload, err := json.Marshal(body) + if err != nil { + return "", err + } + + client := &http.Client{Timeout: 120 * time.Second} + req, err := http.NewRequest("POST", strings.TrimRight(cfg.BaseURL, "/")+"/chat/completions", bytes.NewReader(payload)) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+cfg.APIKey) + + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("llm request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + b, _ := io.ReadAll(io.LimitReader(resp.Body, 2048)) + return "", fmt.Errorf("llm status %d: %s", resp.StatusCode, string(b)) + } + + var result struct { + Choices []struct { + Message struct { + Content string `json:"content"` + } `json:"message"` + } `json:"choices"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return "", fmt.Errorf("decode llm response: %w", err) + } + if len(result.Choices) == 0 { + return "", fmt.Errorf("llm returned empty choices") + } + return result.Choices[0].Message.Content, nil +} + +// buildDirectionPrompt 构建方向分析专用 prompt。 +func buildDirectionPrompt(ctx *store.AnalysisContext, symbol string) []map[string]string { + s := ctx.Score + + var detail struct { + ShortDetails []map[string]any `json:"short_details"` + MediumDetail map[string]any `json:"medium_detail"` + LongDetail map[string]any `json:"long_detail"` + Volatility map[string]any `json:"volatility"` + AdaptiveW map[string]any `json:"adaptive_weights"` + VolPenalty float64 `json:"vol_penalty"` + Delta1D *float64 `json:"composite_delta"` + Delta5D *float64 `json:"composite_delta_5d"` + } + if s.Detail != nil { + _ = json.Unmarshal(s.Detail, &detail) + } + + symbolNames := map[string]string{ + "FG": "玻璃", "SA": "纯碱", "RB": "螺纹钢", "MA": "甲醇", "CF": "棉花", "M": "豆粕", + } + symbolName := symbolNames[symbol] + if symbolName == "" { + symbolName = symbol + } + + var sb strings.Builder + + // ── 综合概况 ── + sb.WriteString(fmt.Sprintf("品种:%s(%s)\n", symbolName, symbol)) + sb.WriteString(fmt.Sprintf("合约:%s 日期:%s\n", s.TsCode, s.TradeDate)) + sb.WriteString(fmt.Sprintf("收盘:%.2f 持仓:%.0f (日变动 %+.0f)\n", s.Close, s.OI, s.OIChg)) + sb.WriteString(fmt.Sprintf("综合分:%.1f/100 信号:%s\n", s.Composite, s.Signal)) + sb.WriteString(fmt.Sprintf("分层:短期 %.1f 中期 %.1f 长期 %.1f\n", s.ShortTerm, s.MediumTerm, s.LongTerm)) + + // 波动率 + if v, ok := detail.Volatility["vol_penalty"]; ok { + risk := 0.0 + if vr, ok := detail.Volatility["vol_risk"]; ok { + if vv, ok := vr.(float64); ok { + risk = vv + } + } + sb.WriteString(fmt.Sprintf("波动率惩罚:%.3f (风险 %.2f%%)\n", v, risk*100)) + } + + // 自适应权重 + if ts, ok := detail.AdaptiveW["trend_strength"]; ok { + sb.WriteString(fmt.Sprintf("趋势强度:%.2f → 权重 短期%.0f%%/中期%.0f%%/长期%.0f%%\n", + ts, valPct(detail.AdaptiveW, "w_short"), valPct(detail.AdaptiveW, "w_medium"), valPct(detail.AdaptiveW, "w_long"))) + } + + // 分数动量 + if detail.Delta1D != nil { + sb.WriteString(fmt.Sprintf("分数日变化:%+.1f", *detail.Delta1D)) + } + if detail.Delta5D != nil { + sb.WriteString(fmt.Sprintf(" 周变化:%+.1f", *detail.Delta5D)) + } + if detail.Delta1D != nil || detail.Delta5D != nil { + sb.WriteString("\n") + } + + // ── 短期动力 7 日 ── + sb.WriteString("\n近7日逐日打分:\n") + for _, d := range detail.ShortDetails { + q := fmt.Sprint(d["quadrant"]) + qcn := map[string]string{ + "accumulation": "增仓涨", "distribution": "增仓跌", + "covering": "减仓涨", "liquidation": "减仓跌", "flat": "持平", + }[q] + if qcn == "" { + qcn = q + } + sb.WriteString(fmt.Sprintf(" %s %s 涨跌%+.2f%% OI%+.2f%% 量比%.2f 得分%.1f\n", + d["trade_date"], qcn, floatVal(d, "price_chg_pct")*100, floatVal(d, "oi_chg_pct")*100, floatVal(d, "vol_ratio"), floatVal(d, "score"))) + } + + // ── 中期趋势 15 日 ── + md := detail.MediumDetail + sb.WriteString(fmt.Sprintf("\n中期趋势(15日):价格信号 %.1f (收益率 %+.2f%%) 资金意愿 %.1f\n", + floatVal(md, "price_signal"), floatVal(md, "price_return_pct"), floatVal(md, "fund_signal"))) + a, d_, c, l := intVal(md, "accumulation_days"), intVal(md, "distribution_days"), intVal(md, "covering_days"), intVal(md, "liquidation_days") + sb.WriteString(fmt.Sprintf("象限分布:增仓涨 %d天 增仓跌 %d天 减仓涨 %d天 减仓跌 %d天\n", a, d_, c, l)) + + // ── 长期结构 30 日 ── + ld := detail.LongDetail + sb.WriteString(fmt.Sprintf("\n长期结构(30日):OI趋势分 %.1f (变化 %+.2f%%) 价格趋势分 %.1f (收益 %+.2f%%)\n", + floatVal(ld, "oi_score"), floatVal(ld, "oi_change_pct"), floatVal(ld, "price_score"), floatVal(ld, "price_return_30d_pct"))) + + // ── 近 5 日分数趋势 ── + if len(ctx.RecentScores) > 0 { + sb.WriteString("\n近5日综合分趋势:") + for i, rs := range ctx.RecentScores { + if i >= 5 { + break + } + sb.WriteString(fmt.Sprintf("%s=%.1f ", rs.TradeDate, rs.Composite)) + } + sb.WriteString("\n") + } + + // ── 近 30 日 K 线(支撑阻力依据)── + if len(ctx.Candles) > 0 { + start := 0 + if len(ctx.Candles) > 30 { + start = len(ctx.Candles) - 30 + } + sb.WriteString("\n近30日K线(开/高/低/收):\n") + for _, c := range ctx.Candles[start:] { + sb.WriteString(fmt.Sprintf(" %s O%.1f H%.1f L%.1f C%.1f\n", c.TradeDate, c.Open, c.High, c.Low, c.Close)) + } + } + + systemPrompt := strings.Join([]string{ + "你是一位期货日内交易分析师。基于量化打分数据,给出下一个交易日的方向判断。", + "", + "你必须输出严格 JSON,不要有任何额外文字:", + "{", + ` "direction": "bullish" | "bearish" | "neutral",`, + ` "confidence": <0-100 的整数,表示对方向的确定程度>`, + ` "support": [<关键支撑位1>, <关键支撑位2>]`, + ` "resistance": [<关键阻力位1>, <关键阻力位2>]`, + ` "reasoning": "<3-5句话,说明核心逻辑:三层信号是共振还是背离?资金在干什么?>",`, + ` "risk_note": "<1-2句话,最可能打破当前判断的风险>"`, + "}", + "", + "分析要点:", + "1. 短期(7日)+中期(15日)+长期(30日) ≥2 层指向同一方向 = 有效方向信号", + "2. 关注持仓变化:增仓方向才是真方向,减仓方向可能是离场", + "3. 分数动量:Δ1d 和 Δ5d 连续同向 = 趋势加速,反向 = 可能转折", + "4. 支撑阻力从 K 线数据中找:近期高低点、密集成交区", + "5. 波动率惩罚系数 < 0.95 表示行情不稳定,降低 confidence", + }, "\n") + + return []map[string]string{ + {"role": "system", "content": systemPrompt}, + {"role": "user", "content": sb.String()}, + } +} + +// nextTradeDate 计算下一个交易日(简化规则:周六→周一,周日→周一,周五→周一,其余+1天)。 +func nextTradeDate(dateStr string) string { + t, err := time.Parse("20060102", dateStr) + if err != nil { + return dateStr + } + next := t.AddDate(0, 0, 1) + switch next.Weekday() { + case time.Saturday: + next = next.AddDate(0, 0, 2) + case time.Sunday: + next = next.AddDate(0, 0, 1) + } + return next.Format("20060102") +} + +// ListDailyDirections 查询方向分析列表。 +func (d *Deps) ListDailyDirections(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + symbol := q.Get("symbol") + start := q.Get("start") + end := q.Get("end") + limit := 50 + if l, err := strconv.Atoi(q.Get("limit")); err == nil { + limit = l + } + + items, err := d.Futures.ListDailyDirections(symbol, start, end, limit) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, items) +} diff --git a/web/backend/internal/router/router.go b/web/backend/internal/router/router.go index 587a81f..2f6892f 100644 --- a/web/backend/internal/router/router.go +++ b/web/backend/internal/router/router.go @@ -35,6 +35,8 @@ func New(d *handlers.Deps, dist fs.FS) http.Handler { r.Post("/run/range", d.RunRange) r.Post("/run/full", d.RunFull) r.Get("/ai/analyze", d.Analyze) + r.Post("/ai/daily-direction", d.DailyDirectionRun) + r.Get("/ai/daily-direction", d.ListDailyDirections) r.Group(func(r chi.Router) { r.Use(mw.RequireAdmin) diff --git a/web/backend/internal/store/futures.go b/web/backend/internal/store/futures.go index 8fb4480..0989143 100644 --- a/web/backend/internal/store/futures.go +++ b/web/backend/internal/store/futures.go @@ -144,6 +144,126 @@ type Candle struct { PreClose float64 `json:"pre_close"` } +// ── Daily Direction ──────────────────────────────────────────────── + +// DailyDirection 日内方向分析结果。 +type DailyDirection struct { + ID string `json:"id"` + Symbol string `json:"symbol"` + TradeDate string `json:"trade_date"` + TargetDate string `json:"target_date"` + Direction string `json:"direction"` + Confidence float64 `json:"confidence"` + Support string `json:"support"` // JSONB → string + Resistance string `json:"resistance"` // JSONB → string + Reasoning string `json:"reasoning"` + RiskNote string `json:"risk_note"` + PromptSnapshot string `json:"prompt_snapshot,omitempty"` + CreatedAt string `json:"created_at"` +} + +// EnsureDailyDirectionTable 建 daily_direction 表(幂等)。 +func (s *FuturesStore) EnsureDailyDirectionTable() error { + _, err := s.db.Exec(` + CREATE TABLE IF NOT EXISTS daily_direction ( + id UUID DEFAULT uuidv7() PRIMARY KEY, + symbol TEXT NOT NULL, + trade_date TEXT NOT NULL, + target_date TEXT NOT NULL, + direction TEXT NOT NULL, + confidence REAL, + support JSONB, + resistance JSONB, + reasoning TEXT, + risk_note TEXT, + prompt_snapshot TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE (symbol, trade_date) + ) + `) + return err +} + +// SaveDailyDirection 写入(upsert)一条方向分析。 +func (s *FuturesStore) SaveDailyDirection(dd *DailyDirection) error { + _, err := s.db.Exec(` + INSERT INTO daily_direction + (symbol, trade_date, target_date, direction, confidence, support, resistance, reasoning, risk_note, prompt_snapshot) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + ON CONFLICT (symbol, trade_date) DO UPDATE SET + target_date = EXCLUDED.target_date, + direction = EXCLUDED.direction, + confidence = EXCLUDED.confidence, + support = EXCLUDED.support, + resistance = EXCLUDED.resistance, + reasoning = EXCLUDED.reasoning, + risk_note = EXCLUDED.risk_note, + prompt_snapshot = EXCLUDED.prompt_snapshot, + created_at = CURRENT_TIMESTAMP + `, dd.Symbol, dd.TradeDate, dd.TargetDate, dd.Direction, dd.Confidence, + dd.Support, dd.Resistance, dd.Reasoning, dd.RiskNote, dd.PromptSnapshot) + return err +} + +// ListDailyDirections 查询方向分析列表。 +func (s *FuturesStore) ListDailyDirections(symbol, start, end string, limit int) ([]DailyDirection, error) { + if limit <= 0 || limit > 500 { + limit = 50 + } + q := `SELECT id, symbol, trade_date, target_date, direction, confidence, + COALESCE(support::text, '[]'), COALESCE(resistance::text, '[]'), + reasoning, risk_note, COALESCE(prompt_snapshot, ''), + COALESCE(created_at::text, '') + FROM daily_direction WHERE 1=1` + args := []any{} + n := 0 + next := func() string { n++; return fmt.Sprintf("$%d", n) } + if symbol != "" { + q += " AND symbol = " + next() + args = append(args, symbol) + } + if start != "" { + q += " AND trade_date >= " + next() + args = append(args, start) + } + if end != "" { + q += " AND trade_date <= " + next() + args = append(args, end) + } + q += " ORDER BY trade_date DESC, symbol ASC LIMIT " + next() + args = append(args, limit) + + rows, err := s.db.Query(q, args...) + if err != nil { + return nil, err + } + defer rows.Close() + out := []DailyDirection{} + for rows.Next() { + var dd DailyDirection + if err := rows.Scan(&dd.ID, &dd.Symbol, &dd.TradeDate, &dd.TargetDate, + &dd.Direction, &dd.Confidence, &dd.Support, &dd.Resistance, + &dd.Reasoning, &dd.RiskNote, &dd.PromptSnapshot, &dd.CreatedAt); err != nil { + return nil, err + } + out = append(out, dd) + } + return out, rows.Err() +} + +// GetActiveTsCode 通过 scores 表查找某品种在指定日期的活跃合约代码。 +func (s *FuturesStore) GetActiveTsCode(symbol, tradeDate string) (string, error) { + var tsCode string + err := s.db.QueryRow( + `SELECT ts_code FROM scores WHERE trade_date = $1 AND ts_code LIKE $2 || '%' ORDER BY ts_code DESC LIMIT 1`, + tradeDate, symbol, + ).Scan(&tsCode) + if err != nil { + return "", fmt.Errorf("no active contract for %s on %s: %w", symbol, tradeDate, err) + } + return tsCode, nil +} + func (s *FuturesStore) ListCandles(tsCode, start, end string) ([]Candle, error) { if tsCode == "" { return nil, ErrMissingTsCode diff --git a/web/backend/main.go b/web/backend/main.go index 3c1d998..7007f9a 100644 --- a/web/backend/main.go +++ b/web/backend/main.go @@ -43,6 +43,9 @@ func main() { if err := auth.BootstrapLLMConfig(futures); err != nil { log.Fatalf("bootstrap llm config: %v", err) } + if err := futures.EnsureDailyDirectionTable(); err != nil { + log.Fatalf("bootstrap daily_direction: %v", err) + } deps := &handlers.Deps{ Auth: authDB, diff --git a/web/frontend/src/App.vue b/web/frontend/src/App.vue index 170a54b..7ca94b0 100644 --- a/web/frontend/src/App.vue +++ b/web/frontend/src/App.vue @@ -76,6 +76,7 @@ async function handleReset() { K 线 / 持仓 合约全景 手动打分 + 日内方向 用户管理 数据重置 @@ -104,6 +105,7 @@ async function handleReset() { K 线 / 持仓 合约全景 手动打分 + 日内方向 用户管理 diff --git a/web/frontend/src/api/daily.ts b/web/frontend/src/api/daily.ts new file mode 100644 index 0000000..94671dd --- /dev/null +++ b/web/frontend/src/api/daily.ts @@ -0,0 +1,46 @@ +import client from './client' + +export interface DailyDirection { + id: string + symbol: string + trade_date: string + target_date: string + direction: string + confidence: number + support: string // JSON string of number[] + resistance: string // JSON string of number[] + reasoning: string + risk_note: string + created_at: string +} + +export interface DailyDirectionRunRequest { + trade_date?: string + symbols?: string[] +} + +export interface DailyDirectionRunResult { + symbol: string + direction: string + confidence: number + error?: string +} + +export interface DailyDirectionRunResponse { + trade_date: string + results: DailyDirectionRunResult[] + errors?: DailyDirectionRunResult[] +} + +export function runDailyDirection(req?: DailyDirectionRunRequest) { + return client.post('/ai/daily-direction', req ?? {}, { timeout: 300_000 }).then((r) => r.data) +} + +export function listDailyDirections(params?: { + symbol?: string + start?: string + end?: string + limit?: number +}) { + return client.get('/ai/daily-direction', { params }).then((r) => r.data) +} diff --git a/web/frontend/src/router/index.ts b/web/frontend/src/router/index.ts index 9cceca4..6ba84b0 100644 --- a/web/frontend/src/router/index.ts +++ b/web/frontend/src/router/index.ts @@ -35,6 +35,11 @@ const routes: RouteRecordRaw[] = [ name: 'run', component: () => import('@/views/RunView.vue'), }, + { + path: '/daily-direction', + name: 'daily-direction', + component: () => import('@/views/DailyDirectionView.vue'), + }, { path: '/admin/users', name: 'admin-users', diff --git a/web/frontend/src/views/DailyDirectionView.vue b/web/frontend/src/views/DailyDirectionView.vue new file mode 100644 index 0000000..7402e5c --- /dev/null +++ b/web/frontend/src/views/DailyDirectionView.vue @@ -0,0 +1,159 @@ + + + + +