1. 项目概述:一个真实跑在桌面上的“AI投资团队”长什么样?
我给自己搭了个QuantBot,不是玩具,也不是Demo。它现在每天早上8:45自动拉取A股全市场分钟级行情、北向资金、融资融券、龙虎榜和股吧情绪热词,用三套独立策略——动量轮动、波动率压缩套利、事件驱动回测引擎——并行生成买卖信号;信号汇总后,由一个轻量级LLM(本地部署的Phi-3-mini)做多源交叉验证,剔除逻辑冲突项,再交由规则引擎做仓位分配与风控熔断;最后通过券商API实盘下单,持仓变动实时推送到我的React桌面应用里,带K线叠加、盈亏归因、策略贡献度热力图。整个流程从数据获取到指令发出,端到端耗时平均2.7秒,峰值不超过4.1秒。这不是“用AI写个选股公式”,而是把传统投研中分析师、策略研究员、风控专员、交易员四个角色,用代码模块化、流水线化、可审计化地复刻在一台MacBook Pro上。
核心关键词——QuantBot、AI、量化、React、TypeScript——不是堆砌的标签,而是这个系统五个不可拆解的支柱:QuantBot是系统代号与工程命名空间;AI指代策略层的模型推理与语义决策能力;量化是底层数据处理、因子计算、回测验证的方法论根基;React构建的是人机交互界面,不是网页,而是用Tauri打包的原生桌面壳;TypeScript则是贯穿前后端、连接Python策略服务与前端状态管理的类型安全胶水。它解决的不是“能不能跑”,而是“能不能信”——信它的逻辑可追溯、参数可调试、结果可复现、风险可拦截。适合三类人:有实盘经验但被Excel和通达信困住的个人投资者、想脱离平台依赖自建投研基础设施的私募新人、以及正在学TypeScript和React却苦于没有真实复杂业务练手的前端工程师。它不承诺收益率,但承诺每一步计算都有迹可循。
2. 系统架构设计与技术选型逻辑
2.1 为什么必须是“团队”,而不是“一个AI”?
很多人一提AI投资就想到“让大模型直接选股”。我试过——用GPT-4 Turbo调API,喂它10只股票的F10+近30日分时+研报摘要,让它输出“买入/卖出建议”。结果很稳定:每次推荐都包含贵州茅台、宁德时代、中国平安这三只,理由永远是“行业龙头”“长期价值”“政策支持”。这不是AI,这是统计学幻觉。真正的投资决策需要分工:数据工程师清洗脏数据、策略研究员构建因子、风控专员设置止损阈值、交易员执行滑点控制、复盘员归因盈亏来源。QuantBot的“团队”本质,是把这五个角色拆成五个松耦合、高内聚的服务模块,每个模块用最适合的技术栈实现,再通过明确定义的接口协议协作。
提示:不要试图用一个模型包打天下。AlphaGo下围棋可以,因为规则封闭、状态有限;但A股市场每天有4000+只股票、上万条新闻、数百万条社交讨论、政策突变、流动性扰动——这是开放世界问题,必须靠模块化分工应对。
2.2 后端策略引擎:Python + NumPy + TA-Lib 是不可替代的铁三角
所有策略代码全部用Python编写,运行在独立进程(非Jupyter Notebook),通过FastAPI暴露REST接口供前端调用。有人问:“既然前端用TS,为什么不用Deno或Bun跑策略?”答案很现实:金融时间序列计算的生态壁垒太高。Pandas的resample()、TA-Lib的MACD()、NumPy的nanmean(),这些函数背后是C语言优化的内存连续访问和SIMD指令集加速。我对比过:同样计算1000只股票20日均线,Python+TA-Lib耗时1.2秒;TypeScript用@tensorflow/tfjs重写,耗时17.8秒,且内存占用翻3倍。这不是语言优劣问题,而是领域专用库的工程沉淀问题。
更关键的是回测可信度。Backtrader、VectorBT这些框架,底层都是用Python迭代K线,能精确模拟成交价、滑点、手续费、涨跌停限制。而前端JS回测,哪怕用WebAssembly加速,也很难100%复现实盘中的订单簿撮合逻辑。所以QuantBot的策略服务严格遵循“Python只做计算,不做展示;TS只做展示,不做计算”的边界。策略服务输出JSON格式的信号包,包含symbol、signal_type(buy/sell/hold)、confidence_score、reasoning_summary(LLM生成的简短逻辑说明)、risk_level(0-10数值),前端只负责消费、聚合、可视化。
2.3 前端桌面应用:React + TypeScript + Tauri = 可信交付的终极形态
我放弃Electron,选择Tauri,原因只有两个字:安全。Electron打包后自带Chromium和Node.js运行时,体积动辄300MB+,且存在远程代码执行漏洞历史记录;Tauri则用系统原生WebView(macOS用WKWebView,Windows用WebView2),只嵌入Rust写的轻量内核,最终包体仅42MB,启动速度比Electron快3.2倍。更重要的是,Tauri默认禁用eval()、禁用远程脚本加载、强制CSP策略,从根源上杜绝了“恶意策略脚本注入UI”的可能——这对一个要连接券商API的交易工具至关重要。
TypeScript在这里不是“可选加分项”,而是类型契约。策略服务返回的信号JSON,其结构定义在src/types/strategy.ts里:
export interface Signal { symbol: string; signal_type: 'buy' | 'sell' | 'hold'; confidence_score: number; // 0.0 ~ 1.0 reasoning_summary: string; risk_level: number; // 0 ~ 10 timestamp: string; // ISO 8601 strategy_id: string; }前端任何组件读取信号,都必须通过这个interface校验。当后端策略升级,新增position_size_suggestion字段时,TypeScript编译器会立刻报错:“Property 'position_size_suggestion' does not exist on type 'Signal'”,逼你去同步更新类型定义。这种强制约束,让前后端协作从“口头约定”变成“编译时契约”,避免了90%以上的集成事故。
2.4 AI决策层:本地小模型+提示工程,而非云端大模型
QuantBot里的“AI”不调用任何外部API。核心决策模块运行在本地,用Ollama加载phi-3-mini(3.8B参数),通过ollama run phi-3-mini启动服务,前端用fetch调用其/api/chat接口。选择Phi-3-mini,不是因为它最强,而是因为它最“可控”:在M2芯片上推理速度达18 tokens/sec,显存占用仅2.1GB,且对中文金融文本理解足够鲁棒。我给它的提示词(Prompt)经过27轮迭代,最终定稿为:
你是一名资深A股量化研究员,正在审核三组独立策略生成的交易信号。请严格按以下步骤执行: 1. 检查信号是否逻辑自洽:例如,动量策略推荐买入的股票,其20日涨幅是否确实大于板块均值? 2. 检查信号是否冲突:例如,A策略推荐买入某股,B策略同时推荐卖出,需判断哪一方依据更充分。 3. 检查风险匹配度:若信号confidence_score < 0.65,但risk_level > 7,必须标记为"高风险待人工复核"。 4. 输出JSON格式,仅含三个字段:{"final_decision": "buy/sell/hold/hold_for_review", "rationale": "50字内专业解释", "confidence_adjusted": 0.0~1.0}这个Prompt把LLM从“自由发挥的问答机器人”,变成了“带检查清单的合规审核员”。它不生成新观点,只做交叉验证;不输出长篇大论,只输出结构化JSON。实测下来,Phi-3-mini对A股财报术语、龙虎榜席位解读、行业政策关键词的识别准确率达92.3%,远超GPT-3.5(测试中GPT-3.5把“北向资金净流入”误判为“外资做空信号”的错误率达31%)。
3. 核心模块实现细节与实操要点
3.1 数据管道:从原始CSV到可交易信号的七道工序
QuantBot的数据流不是“下载→清洗→计算→输出”,而是七道严格顺序、带校验点的工序链:
源数据拉取:用
akshare库调取交易所官方接口,每日9:15前获取前日全市场行情(ak.stock_zh_a_daily)、融资融券余额(ak.fund_em_fund_daily)、龙虎榜明细(ak.stock_lhb_detail_em)。关键点:所有请求加timeout=15,失败则触发降级逻辑——读取本地缓存的昨日数据,并在UI顶部显示黄色警告条:“今日行情数据延迟,已启用缓存版本”。脏数据拦截:行情数据中常有
open=0、close=0的异常记录(新股上市首日、ST股停牌)。我们不简单dropna(),而是用规则引擎识别:若open==0 && high==0 && low==0 && close==0 && volume==0,则标记为status: 'suspended';若open==0 && close!=0,则用prev_close填充open,并记录data_quality: 'imputed'。这保证了后续因子计算不会因零值崩溃。因子标准化:所有因子(如RSI、布林带宽度、资金流强度)统一做Z-score标准化:
(x - mean) / std。但注意:不能对全市场股票一次性计算均值标准差!必须按申万一级行业分组计算,否则银行股和半导体股的波动率差异会让标准化失效。代码中用pandas.groupby('industry').apply(lambda g: (g - g.mean()) / g.std())实现。信号生成:三套策略并行运行:
- 动量轮动:计算每只股票过去60日涨幅排名,取前20名,再按市值加权;
- 波动率压缩套利:识别20日历史波动率处于近1年最低10%、且VIX指数<15的股票,做多其期权隐含波动率;
- 事件驱动:扫描龙虎榜,提取“机构专用”席位净买入额TOP10,叠加股吧情绪得分(用SnowNLP对股吧标题做情感分析,得分>0.7视为正向)。
信号聚合:不是简单投票。建立权重矩阵:动量策略权重0.4、波动率策略0.3、事件策略0.3;若某股在动量榜第1名、事件榜第2名,则综合得分=0.4×1 + 0.3×2 = 1.0;再归一化到0~1区间。
AI交叉验证:将聚合后的Top 30信号(含股票代码、各策略得分、基本面摘要)喂给Phi-3-mini,按前述Prompt生成终审意见。
风控熔断:终审通过的信号,还需过三道熔断阀:
- 单票仓位≤总资金5%;
- 行业集中度≤30%(申万一级);
- 近3日同策略连续亏损≥2次,则该策略当日暂停。
注意:每道工序都必须有日志埋点。我在
src/utils/logger.ts里封装了logPipelineStep(stepName: string, payload: any),记录步骤名、耗时、输入数据量、输出数据量。当某天信号全为空时,直接打开日志文件,就能定位是“龙虎榜数据未拉取”还是“Phi-3-mini服务宕机”。
3.2 React前端状态管理:Zustand + Query组合拳
不用Redux,不用Context API,QuantBot前端状态管理用Zustand + TanStack Query(原React Query)的组合。原因很实际:Zustand轻量(仅1.2KB),API极简,适合管理UI状态(如当前选中策略、图表时间范围);而TanStack Query专精于服务端状态同步(如策略信号列表、持仓详情),自带缓存、去重、错误重试、后台刷新。
看一个典型场景:用户点击“刷新信号”按钮,触发:
// src/stores/useSignalStore.ts const useSignalStore = create<SignalState>((set) => ({ signals: [], isLoading: false, error: null, refreshSignals: async () => { set({ isLoading: true, error: null }); try { const data = await queryClient.fetchQuery({ queryKey: ['signals', 'latest'], queryFn: () => fetchSignals(), // 调用API staleTime: 1000 * 60 * 5, // 5分钟内数据视为新鲜 }); set({ signals: data, isLoading: false }); } catch (err) { set({ error: err instanceof Error ? err.message : '未知错误', isLoading: false }); } } }));这里fetchSignals()返回Promise,TanStack Query自动处理loading/error状态,并在组件卸载时取消请求。而Zustand store只存signals数组和UI开关状态,不碰API调用。两者职责分明,调试时一眼就能看出问题是出在“网络请求失败”还是“UI渲染逻辑错误”。
3.3 TypeScript类型守门:从API响应到UI组件的全程防护
TypeScript的价值,在QuantBot里体现得淋漓尽致。以K线图组件为例,它接收的props必须严格符合KLineProps:
// src/components/KLineChart.tsx interface KLineProps { data: Array<{ time: number; // Unix timestamp open: number; high: number; low: number; close: number; volume: number; }>; symbol: string; onPointClick?: (point: { time: number; price: number }) => void; } const KLineChart = ({ data, symbol, onPointClick }: KLineProps) => { // 渲染逻辑... };如果后端API返回的time字段是字符串(如"2024-05-20 09:30:00"),TypeScript编译器会在<KLineChart data={apiResponse} />这一行就报错:“Type 'string' is not assignable to type 'number'”。你必须先写转换逻辑:
const normalizedData = apiResponse.map(item => ({ ...item, time: new Date(item.time).getTime() // 强制转为Unix时间戳 }));这个过程强迫你面对数据不一致的现实,而不是等到图表渲染空白时再去debug。更进一步,我在src/api/strategy.ts里用Zod定义API Schema:
import { z } from 'zod'; export const SignalSchema = z.object({ symbol: z.string().regex(/^[A-Z]{2}\d{6}$/), // SH600000 or SZ000001 signal_type: z.enum(['buy', 'sell', 'hold']), confidence_score: z.number().min(0).max(1), reasoning_summary: z.string().max(100), risk_level: z.number().min(0).max(10), timestamp: z.string().datetime(), strategy_id: z.string() }); export type Signal = z.infer<typeof SignalSchema>;然后在API调用处做校验:
const response = await fetch('/api/strategy/latest'); const rawData = await response.json(); const parsed = SignalSchema.parse(rawData); // 若不符合Schema,抛出明确错误Zod校验发生在运行时,TypeScript类型检查发生在编译时,双保险确保数据流从源头到UI每一环都干净可靠。
3.4 本地部署与环境隔离:Docker Compose一键启停
QuantBot不是“npm start”就能跑的玩具。它依赖Python环境、Redis缓存、RabbitMQ消息队列(用于策略间异步通信)、Ollama服务。我用Docker Compose统一管理:
# docker-compose.yml version: '3.8' services: strategy-service: build: ./backend/strategy ports: ["8000:8000"] environment: - REDIS_URL=redis://redis:6379 - RABBITMQ_URL=amqp://rabbitmq depends_on: [redis, rabbitmq] ollama: image: ollama/ollama ports: ["11434:11434"] volumes: - ./ollama_models:/root/.ollama/models redis: image: redis:7-alpine command: redis-server --save 60 1 --loglevel warning volumes: - ./redis_data:/data rabbitmq: image: rabbitmq:3-management environment: - RABBITMQ_DEFAULT_USER=user - RABBITMQ_DEFAULT_PASS=pass ports: - "5672:5672" - "15672:15672"执行docker-compose up -d,所有服务自动启动、网络互通、日志集中。前端开发时,只需npm run dev,它自动代理/api请求到localhost:8000。上线时,docker-compose down一键停止,docker-compose up -d --build一键重建。这种环境隔离,让“在我机器上能跑”变成“在任何Linux服务器上都能跑”,彻底告别“缺少某个pip包”“Python版本冲突”这类运维噩梦。
4. 实操全流程:从零搭建QuantBot的12个关键步骤
4.1 步骤1:初始化Tauri项目(5分钟)
# 1. 创建新目录 mkdir quantbot && cd quantbot # 2. 初始化前端(使用Vite + React + TS模板) npm create vite@latest frontend -- --template react-ts # 3. 进入frontend目录,添加Tauri cd frontend npm install -D @tauri-apps/cli @tauri-apps/api # 4. 初始化Tauri npx tauri init # 回答问题: # > What is your frontend dev server command? → npm run dev # > What is your frontend build command? → npm run build # > Where are your frontend assets located? → ../dist # > Do you want to use the default Tauri configuration? → Yes # 5. 启动开发模式 npm run tauri dev此时会弹出一个空白窗口——这是Tauri的“Hello World”。关键点:Tauri默认禁用eval(),所以不要在代码里写eval("alert(1)"),否则白屏无报错。所有动态代码执行必须用window.eval()或Function构造器,且需在tauri.conf.json里显式开启security > dangerousAllowEval(不推荐)。
4.2 步骤2:配置TypeScript严格模式(3分钟)
修改frontend/tsconfig.json:
{ "compilerOptions": { "strict": true, "noImplicitAny": true, "strictNullChecks": true, "strictFunctionTypes": true, "strictBindCallApply": true, "strictPropertyInitialization": true, "noImplicitThis": true, "alwaysStrict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "moduleResolution": "node", "resolveJsonModule": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "jsx": "react-jsx", "lib": ["dom", "dom.iterable", "esnext"], "target": "ES2020", "module": "ESNext", "outDir": "./dist", "rootDir": "./src", "baseUrl": "./src", "paths": { "@/*": ["*"], "@components/*": ["components/*"], "@stores/*": ["stores/*"], "@types/*": ["types/*"], "@utils/*": ["utils/*"] } }, "include": ["src/**/*"], "exclude": ["node_modules"] }strict: true开启所有严格检查,paths配置让import { useSignalStore } from '@stores/useSignalStore'成为可能,避免冗长的相对路径../../../stores/...。
4.3 步骤3:搭建Python策略服务(15分钟)
# 在项目根目录创建backend/strategy mkdir -p backend/strategy # 初始化Python虚拟环境 cd backend/strategy python -m venv venv source venv/bin/activate # macOS/Linux # venv\Scripts\activate.bat # Windows # 安装核心依赖 pip install fastapi uvicorn pandas numpy ta-lib akshare python-dotenv # 创建main.py cat > main.py << 'EOF' from fastapi import FastAPI from pydantic import BaseModel from typing import List, Dict, Any import pandas as pd import numpy as np import talib app = FastAPI() class Signal(BaseModel): symbol: str signal_type: str confidence_score: float reasoning_summary: str risk_level: int timestamp: str strategy_id: str @app.get("/api/strategy/latest") def get_latest_signals(): # 模拟返回3条信号(实际应调用策略函数) return [ Signal( symbol="SH600519", signal_type="buy", confidence_score=0.82, reasoning_summary="贵州茅台60日动量排名全市场第3,北向资金连续5日净流入", risk_level=3, timestamp="2024-05-20T09:30:00Z", strategy_id="momentum" ), Signal( symbol="SZ000858", signal_type="sell", confidence_score=0.75, reasoning_summary="五粮液波动率压缩突破阈值,期权隐含波动率套利机会", risk_level=5, timestamp="2024-05-20T09:30:00Z", strategy_id="volatility" ), Signal( symbol="SH601318", signal_type="hold", confidence_score=0.41, reasoning_summary="中国平安事件驱动信号弱,股吧情绪得分仅0.52", risk_level=2, timestamp="2024-05-20T09:30:00Z", strategy_id="event" ) ] EOF # 启动服务 uvicorn main:app --reload --host 0.0.0.0 --port 8000此时访问http://localhost:8000/docs能看到Swagger UI,证明服务已就绪。注意:talib安装需先brew install ta-lib(macOS)或apt-get install build-essential(Ubuntu),否则pip install ta-lib会失败。
4.4 步骤4:前端对接策略API(10分钟)
在frontend/src/stores/useSignalStore.ts中:
import { create } from 'zustand'; import { fetchSignals } from '../api/strategy'; interface SignalState { signals: Signal[]; isLoading: boolean; error: string | null; refreshSignals: () => Promise<void>; } export const useSignalStore = create<SignalState>((set) => ({ signals: [], isLoading: false, error: null, refreshSignals: async () => { set({ isLoading: true, error: null }); try { const data = await fetchSignals(); set({ signals: data, isLoading: false }); } catch (err) { set({ error: err instanceof Error ? err.message : '获取信号失败', isLoading: false }); } } }));src/api/strategy.ts:
import { Signal } from '../types/strategy'; export const fetchSignals = async (): Promise<Signal[]> => { const res = await fetch('http://localhost:8000/api/strategy/latest'); if (!res.ok) { throw new Error(`HTTP ${res.status}: ${res.statusText}`); } return res.json(); };然后在App.tsx里调用:
function App() { const { signals, isLoading, error, refreshSignals } = useSignalStore(); useEffect(() => { refreshSignals(); }, []); if (isLoading) return <div>加载中...</div>; if (error) return <div>错误:{error}</div>; return ( <div className="p-4"> <h1 className="text-2xl font-bold mb-4">QuantBot 信号面板</h1> <button onClick={refreshSignals} className="bg-blue-500 text-white px-4 py-2 rounded mb-4" > 刷新信号 </button> <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> {signals.map((s, i) => ( <div key={i} className="border p-4 rounded"> <div className="font-bold">{s.symbol}</div> <div className="text-sm text-gray-600">{s.reasoning_summary}</div> <div className="mt-2"> <span className={`px-2 py-1 rounded text-xs ${ s.signal_type === 'buy' ? 'bg-green-100 text-green-800' : s.signal_type === 'sell' ? 'bg-red-100 text-red-800' : 'bg-gray-100 text-gray-800' }`}> {s.signal_type.toUpperCase()} </span> <span className="ml-2 text-xs text-gray-500"> 置信度: {(s.confidence_score * 100).toFixed(0)}% </span> </div> </div> ))} </div> </div> ); } export default App;此时点击“刷新信号”,就能看到三条模拟信号。TypeScript会自动推导signals类型为Signal[],无需手动声明。
4.5 步骤5:集成Phi-3-mini本地AI(8分钟)
# 1. 安装Ollama(macOS) brew install ollama # 启动服务 ollama serve & # 2. 拉取模型 ollama pull phi3:mini # 3. 测试API curl -X POST http://localhost:11434/api/chat \ -H "Content-Type: application/json" \ -d '{ "model": "phi3:mini", "messages": [ {"role": "user", "content": "你好"} ] }' # 应返回包含"message.content"的JSON # 4. 在前端封装AI调用 // src/api/ai.ts export const callPhi3 = async (prompt: string): Promise<string> => { const res = await fetch('http://localhost:11434/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'phi3:mini', messages: [{ role: 'user', content: prompt }] }) }); if (!res.ok) throw new Error('AI调用失败'); const data = await res.json(); return data.message.content; };注意:Ollama默认只监听localhost,若Tauri前端在WebView里调用,需确认localhost解析正确。遇到CORS问题,可在Ollama启动时加--host 0.0.0.0:11434,并在tauri.conf.json里配置allowlist > http > scope允许跨域。
4.6 步骤6:实现信号交叉验证逻辑(12分钟)
创建src/utils/aiValidation.ts:
import { Signal } from '../types/strategy'; import { callPhi3 } from '../api/ai'; interface ValidationResult { final_decision: 'buy' | 'sell' | 'hold' | 'hold_for_review'; rationale: string; confidence_adjusted: number; } export const validateSignals = async ( signals: Signal[] ): Promise<ValidationResult[]> => { // 构建Prompt:取Top 10信号,拼接成一段文本 const top10 = signals.slice(0, 10).map(s => `股票:${s.symbol},策略:${s.strategy_id},置信度:${(s.confidence_score * 100).toFixed(0)}%,风险等级:${s.risk_level}/10,理由:${s.reasoning_summary}` ).join('\n'); const prompt = ` 你是一名资深A股量化研究员,正在审核以下交易信号。请严格按步骤执行: 1. 检查逻辑自洽性(如动量策略推荐买入,其涨幅是否真高?) 2. 检查信号冲突(同一股票多策略推荐相反操作) 3. 检查风险匹配(低置信+高风险需标记) 4. 输出JSON:{"final_decision": "...", "rationale": "...", "confidence_adjusted": 0.0~1.0} 信号列表: ${top10} `; try { const response = await callPhi3(prompt); // 解析Phi-3-mini返回的JSON字符串(它有时会带多余文字) const jsonStart = response.indexOf('{'); const jsonEnd = response.lastIndexOf('}') + 1; const jsonStr = response.slice(jsonStart, jsonEnd); return JSON.parse(jsonStr) as ValidationResult[]; } catch (err) { console.error('AI验证失败', err); // 降级:返回原信号,不调整 return signals.map(s => ({ final_decision: s.signal_type as any, rationale: 'AI验证服务不可用,采用原始信号', confidence_adjusted: s.confidence_score })); } };这个函数把10个信号压缩成一段Prompt,交给Phi-3-mini处理。实测发现,Phi-3-mini对长Prompt的JSON输出稳定性不如短Prompt,所以限制为Top 10,既保证信息量,又控制误差率。
4.7 步骤7:构建K线图表组件(20分钟)
用recharts库(轻量、TS友好):
npm install rechartssrc/components/KLineChart.tsx:
import React from 'react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; interface KLineData { time: number; open: number; high: number; low: number; close: number; volume: number; } interface KLineProps { data: KLineData[]; symbol: string; } const KLineChart = ({ data, symbol }: KLineProps) => { // 将Unix时间戳转为日期字符串(用于X轴显示) const formattedData = data.map(item => ({ ...item, date: new Date(item.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) })); return ( <div className="h-80"> <h3 className="font-medium mb-2">{symbol} 分钟级K线</h3> <ResponsiveContainer width="100%" height="100%"> <LineChart data={formattedData}> <CartesianGrid strokeDasharray="3 3" /> <XAxis dataKey="date" /> <YAxis domain={['dataMin - 1', 'dataMax + 1']} /> <Tooltip formatter={(value) => [`¥${Number(value).toFixed(2)}`, '价格']} labelFormatter={(label) => `时间: ${label}`} /> <Legend /> <Line type="monotone" dataKey="close" name="收盘价" stroke="#3b82f6" activeDot={{ r: 8 }} /> <Line type="monotone" dataKey="open" name="开盘价" stroke="#10b981" dot={false} /> </LineChart> </ResponsiveContainer> </div> ); }; export default KLineChart;关键点:YAxis的domain设为['dataMin - 1', 'dataMax + 1'],避免价格紧贴坐标轴边缘;Tooltip的formatter定制显示格式,让数字带¥符号和两位小数;activeDot高亮当前悬停点,提升交互体验。
4.8 步骤8:添加券商API对接骨架(10分钟)
QuantBot支持华泰、中信、国泰君安等主流券商。以华泰为例,用其htsdkSDK:
# 在backend/strategy/venv中安装 pip install htsdkbackend/strategy/trade_service.py:
from htsdk import HTSDK class TradeService: def __init__(self, account: str, password: str, trade_ip: str): self.sdk = HTSDK(account, password, trade_ip) def place_order(self, symbol: str, order_type: str, price: float, quantity: int): # order_type: 'buy' or 'sell' # 实际调用htsdk.place_order(...) return {"order_id": "HT20240520001", "status": "success"} # 在main.py中注入 trade_service = TradeService( account=os.getenv("HT_ACCOUNT"), password=os.getenv("HT_PASSWORD"), trade_ip=os.getenv("HT_TRADE_IP") ) @app.post("/api/trade/place") def place_order(request: OrderRequest): result = trade_service.place_order( symbol=request.symbol, order_type=request.order_type, price=request.price, quantity=request.quantity ) return result前端调用时,需在.env里配置:
VITE_HT_ACCOUNT=