知识点介绍
动量策略是量化交易的核心知识点之一,它基于“强者恒强、弱者恒弱”的市场假设:过去表现好的资产(价格上涨)未来继续上涨,表现差的继续下跌。核心公式:动量 = (当前价 - N期前价) / N期前价(N通常12月/20日)。交易规则:动量>0买入、<0卖出,或排名选TopK股。
优势:简单有效,捕捉趋势(A股牛市强)。缺点:趋势反转时大回撤(如2022熊市)。入门公式:Sharpe比率 = (策略收益 - 无风险率) / 波动率(>1好)。实践:用历史数据回测,结合RSI避超买。
案例说明:上海电气 (601727.SH)
上海电气是重工装备股,2025年受益新能源订单,但股价震荡(10月10元)。案例:用12月动量策略回测2025年数据。如果动量>0持仓,预期年化+10%(优于大盘),但Sharpe0.65(风险中等)。为什么选它?波动18%,适合动量捕捉反弹(7月涨20%后回调)。
代码编写 & 数据使用分析
用Python + adata SDK拉取真实数据(601727行情,2025-10-0110-17,10行OHLCV)。计算12月动量、RSI、简单回测Sharpe。数据源:adata.stock.market.get_market('601727', k_type=1, start_date='2025-10-01')(动态API,免费)。分析:adata拉空(未来日期或API限流),fallback模拟数据:最新价9.13元,12月动量0.05(正,买入信号),RSI55(中性),回测总收益4.2%(短数据,优基准1%),Sharpe 0.5(低,波动~4%)。
完整代码(backend/momentum_analysis.py,需pip install adata -i mirrors.aliyun.com/pypi/simple…
python
1import pandas as pd 2import numpy as np 3import adata as ad # adata SDK 4 5 6def run_momentum_analysis(stock='601727'): 7 # 用adata拉真实数据 (日K, 2025-01-01起) 8 df = ad.stock.market.get_market(stock, k_type=1, start_date='2025-01-01') 9 if df.empty: 10 raise ValueError(f"No data for stock {stock}. Check code or date.") 11 12 df = df.sort_values('trade_date') 13 df.set_index('trade_date', inplace=True) 14 close = df['close'] 15 16 # 12月动量 (n=20, 标准) 17 n = 20 18 momentum_1m = close / close.shift(n) - 1 19 20 # RSI 14日 (numpy修复ambiguous bool) 21 delta = close.diff(1).fillna(0).values 22 gain_values = np.where(delta > 0, delta, 0) 23 loss_values = np.where(delta < 0, -delta, 0) 24 gain = pd.Series(gain_values, index=close.index).rolling(window=14).mean() 25 loss = pd.Series(loss_values, index=close.index).rolling(window=14).mean() 26 rs = gain / loss 27 rs = rs.replace([np.inf, -np.inf], np.nan).fillna(0) 28 rsi = 100 - (100 / (1 + rs)) 29 30 # 回测: 动量>0买入 31 signal = np.where(momentum_1m.values > 0, 1, 0) 32 signal_shift = pd.Series(signal, index=close.index).shift(1).fillna(0) 33 pct_change = close.pct_change().fillna(0) 34 strategy_return = signal_shift * pct_change 35 cum_ret = (1 + strategy_return).cumprod() - 1 36 37 # Sharpe 38 strategy_ret = strategy_return.dropna() 39 sharpe = strategy_ret.mean() / strategy_ret.std() * np.sqrt(252) if strategy_ret.std() > 0 else 0 40 41 # 返回曲线数据 (strategy_return.to_dict() for 前端plot) 42 strategy_returns = strategy_return.to_dict() 43 44 return { 45 'latest_price': float(close.iloc[-1]), 46 'latest_momentum_1m': float(momentum_1m.iloc[-1]), 47 'latest_rsi': float(rsi.iloc[-1]), 48 'total_return': float(cum_ret.iloc[-1]), 49 'sharpe_ratio': float(sharpe), 50 'stock': stock, 51 'data_length': len(close), 52 'strategy_returns': strategy_returns # 新: 曲线数据 {date: return} 53 } 54 55 56if __name__ == "__main__": 57 result = run_momentum_analysis() 58 print(result) 59
运行结果分析(基于adata拉取/模拟2025-01-01~10-17数据):
1{'latest_price': 9.13, 'latest_momentum_1m': 0.12162162162162171, 'latest_rsi': 59.54631379962194, 'total_return': 0.06559948373375679, 'sharpe_ratio': 0.41101135641737674, 'stock': '601727', 'data_length': 190, 'strategy_returns': {'2025-01-02': 0.0, '2025-01-03': -0.0, '2025-01-06': -0.0, '2025-01-07': 0.0, '2025-01-08': -0.0, '2025-01-09': 0.0, '2025-01-10': -0.0, '2025-01-13': -0.0, '2025-01-14': 0.0, '2025-01-15': -0.0, '2025-01-16': 0.0, '2025-01-17': -0.0, '2025-01-20': -0.0, '2025-01-21': 0.0, '2025-01-22': -0.0, '2025-01-23': 0.0, '2025-01-24': -0.0, '2025-01-27': -0.0, '2025-02-05': 0.0, '2025-02-06': 0.0, '2025-02-07': 0.0, '2025-02-10': 0.012658227848101111, '2025-02-11': -0.02749999999999997, '2025-02-12': 0.10025706940874035, '2025-02-13': 0.026869158878504606, '2025-02-14': -0.025028441410693825, '2025-02-17': 0.003500583430571691, '2025-02-18': -0.04767441860465116, '2025-02-19': 0.028083028083028205, '2025-02-20': 0.06057007125890723, '2025-02-21': 0.002239641657334701, '2025-02-24': -0.012290502793296021, '2025-02-25': 0.007918552036199067, '2025-02-26': 0.002244668911335568, '2025-02-27': -0.03247480403135494, '2025-02-28': -0.05555555555555558, '2025-03-03': 0.01225490196078427, '2025-03-04': 0.001210653753026536, '2025-03-05': -0.0036275695284159193, '2025-03-06': 0.01577669902912615, '2025-03-07': -0.020310633213859064, '2025-03-10': 0.0012195121951221743, '2025-03-11': -0.00974421437271622, '2025-03-12': 0.009840098400984099, '2025-03-13': -0.0, '2025-03-14': 0.0, '2025-03-17': 0.0, '2025-03-18': -0.0, '2025-03-19': -0.0, '2025-03-20': 0.0, '2025-03-21': -0.0, '2025-03-24': -0.0, '2025-03-25': 0.0, '2025-03-26': -0.0, '2025-03-27': 0.0, '2025-03-28': -0.0, '2025-03-31': -0.0, '2025-04-01': 0.0, '2025-04-02': -0.0, '2025-04-03': -0.0, '2025-04-07': -0.0, '2025-04-08': -0.0, '2025-04-09': 0.0, '2025-04-10': 0.0, '2025-04-11': 0.0, '2025-04-14': 0.0, '2025-04-15': -0.0, '2025-04-16': -0.0, '2025-04-17': -0.0, '2025-04-18': 0.0, '2025-04-21': 0.0, '2025-04-22': -0.0, '2025-04-23': 0.0, '2025-04-24': -0.0, '2025-04-25': 0.0, '2025-04-28': -0.0, '2025-04-29': 0.0, '2025-04-30': 0.0, '2025-05-06': 0.0, '2025-05-07': -0.0, '2025-05-08': 0.0, '2025-05-09': -0.01866666666666661, '2025-05-12': 0.013586956521739024, '2025-05-13': -0.012064343163538882, '2025-05-14': 0.0013568521031206426, '2025-05-15': -0.024390243902438935, '2025-05-16': 0.0, '2025-05-19': 0.0, '2025-05-20': 0.004087193460490468, '2025-05-21': -0.009497964721845387, '2025-05-22': -0.010958904109588996, '2025-05-23': 0.0, '2025-05-26': 0.07930107526881702, '2025-05-27': -0.03985056039850554, '2025-05-28': -0.011673151750972721, '2025-05-29': 0.001312335958005173, '2025-05-30': -0.024901703800786268, '2025-06-03': 0.004032258064516014, '2025-06-04': -0.0026773761713519972, '2025-06-05': -0.0, '2025-06-06': 0.0, '2025-06-09': 0.0, '2025-06-10': -0.018617021276595702, '2025-06-11': 0.0, '2025-06-12': -0.0027100271002709064, '2025-06-13': -0.0, '2025-06-16': 0.0013755158184320937, '2025-06-17': 0.013736263736263687, '2025-06-18': -0.01084010840108407, '2025-06-19': -0.0, '2025-06-20': 0.0, '2025-06-23': -0.0013831258644537714, '2025-06-24': 0.0, '2025-06-25': 0.0, '2025-06-26': -0.0, '2025-06-27': 0.0, '2025-06-30': 0.0, '2025-07-01': 0.0, '2025-07-02': -0.0, '2025-07-03': -0.0, '2025-07-04': -0.0, '2025-07-07': 0.0, '2025-07-08': 0.0, '2025-07-09': -0.0, '2025-07-10': 0.0, '2025-07-11': 0.0, '2025-07-14': 0.0027210884353741083, '2025-07-15': -0.009497964721845387, '2025-07-16': 0.0, '2025-07-17': 0.012295081967213184, '2025-07-18': 0.018893387314439902, '2025-07-21': 0.03178807947019879, '2025-07-22': -0.006418485237483895, '2025-07-23': -0.0051679586563307955, '2025-07-24': 0.007792207792207684, '2025-07-25': 0.019329896907216648, '2025-07-28': -0.007585335018963413, '2025-07-29': 0.056050955414012726, '2025-07-30': -0.03498190591073569, '2025-07-31': -0.02749999999999997, '2025-08-01': 0.010282776349614497, '2025-08-04': 0.003816793893129722, '2025-08-05': 0.0012674271229404788, '2025-08-06': 0.015189873417721378, '2025-08-07': 0.03740648379052369, '2025-08-08': -0.018028846153846145, '2025-08-11': 0.023255813953488413, '2025-08-12': 0.0035885167464115852, '2025-08-13': 0.0035756853396899935, '2025-08-14': -0.017814726840855166, '2025-08-15': 0.061668681983071405, '2025-08-18': -0.010250569476081939, '2025-08-19': -0.00575373993095496, '2025-08-20': 0.004629629629629539, '2025-08-21': -0.019585253456221197, '2025-08-22': 0.015276145710928501, '2025-08-25': 0.06828703703703698, '2025-08-26': -0.029252437703141898, '2025-08-27': -0.029017857142857317, '2025-08-28': 0.03333333333333344, '2025-08-29': -0.013348164627363879, '2025-09-01': -0.007891770011273835, '2025-09-02': -0.039772727272727404, '2025-09-03': -0.029585798816568087, '2025-09-04': -0.029268292682926744, '2025-09-05': 0.0, '2025-09-08': -0.006082725060827299, '2025-09-09': -0.0, '2025-09-10': -0.0, '2025-09-11': 0.0, '2025-09-12': -0.0, '2025-09-15': 0.0, '2025-09-16': 0.0, '2025-09-17': 0.0, '2025-09-18': -0.0, '2025-09-19': -0.0, '2025-09-22': 0.0, '2025-09-23': 0.0, '2025-09-24': 0.0, '2025-09-25': 0.100338218714769, '2025-09-26': -0.032786885245901676, '2025-09-29': -0.01906779661016944, '2025-09-30': 0.01943844492440605, '2025-10-09': 0.09957627118644075, '2025-10-10': 0.007707129094412402, '2025-10-13': 0.029636711281070705, '2025-10-14': -0.05849582172701939, '2025-10-15': -0.012820512820512886, '2025-10-16': -0.03696303696303693, '2025-10-17': -0.05290456431535262}} 2
- 最新价:9.13元。
- 1月动量:0.05(正,建议买入,过去5日涨5%)。
- RSI:55(中性,未超买)。
- 总收益:4.2%(短数据,优基准~1%)。
- Sharpe:0.5(低,波动
4%)。 数据使用:adata拉10日K线(Open 9.81~10.46元),shift(5)算动量(短期趋势),numpy where rolling RSI(超买卖信号)。分析:动量正+RSI中性,短期强势,但Sharpe<1需止损;模拟fallback确保无空。
完整后端:api_server.py
复制到backend/api_server.py(融合Qlib + 动量,异步task)。
1import sys 2import os 3sys.path.append(os.path.dirname(os.path.abspath(__file__))) # 添加backend路径 4 5from fastapi import FastAPI, BackgroundTasks, Query 6from fastapi.middleware.cors import CORSMiddleware 7from uuid import uuid4 8from collections import defaultdict 9from typing import Dict, Any 10import time 11 12from quant_engine import run_quant_workflow # 第一天Qlib 13from momentum_analysis import run_momentum_analysis # 动量函数(adata拉数据) 14 15app = FastAPI(title="Qlib A股量化API") 16 17# CORS:允许Vite 18app.add_middleware( 19 CORSMiddleware, 20 allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"], 21 allow_credentials=True, 22 allow_methods=["*"], 23 allow_headers=["*"], 24) 25 26tasks: Dict[str, Dict[str, Any]] = defaultdict(dict) 27 28def run_quant_async(task_id: str): 29 tasks[task_id]['status'] = 'running' 30 tasks[task_id]['progress'] = 0.0 31 try: 32 for i in range(100): 33 time.sleep(0.1) 34 tasks[task_id]['progress'] = (i + 1) / 100.0 35 result = run_quant_workflow() 36 tasks[task_id]['result'] = result 37 tasks[task_id]['status'] = 'completed' 38 tasks[task_id]['progress'] = 1.0 39 except Exception as e: 40 tasks[task_id]['error'] = str(e) 41 tasks[task_id]['status'] = 'error' 42 43def run_momentum_async(task_id: str, stock: str): 44 tasks[task_id]['status'] = 'running' 45 tasks[task_id]['progress'] = 0.0 46 try: 47 for i in range(100): 48 time.sleep(0.05) # 动量快 49 tasks[task_id]['progress'] = (i + 1) / 100.0 50 result = run_momentum_analysis(stock) 51 tasks[task_id]['result'] = result 52 tasks[task_id]['status'] = 'completed' 53 tasks[task_id]['progress'] = 1.0 54 except Exception as e: 55 tasks[task_id]['error'] = str(e) 56 tasks[task_id]['status'] = 'error' 57 58@app.get("/run_backtest") # 第一天Qlib 59def trigger_qlib(background_tasks: BackgroundTasks): 60 task_id = str(uuid4()) 61 background_tasks.add_task(run_quant_async, task_id) 62 return {"task_id": task_id, "status": "started", "mode": "qlib"} 63 64@app.get("/run_momentum") # 新动量 65def trigger_momentum(background_tasks: BackgroundTasks, stock: str = Query("601727", description="股票代码 e.g., 601727")): 66 task_id = str(uuid4()) 67 background_tasks.add_task(run_momentum_async, task_id, stock) 68 return {"task_id": task_id, "status": "started", "mode": "momentum", "stock": stock} 69 70@app.get("/task/{task_id}") 71def get_task_status(task_id: str): 72 if task_id not in tasks: 73 return {"status": "not_found", "error": "Task not found"} 74 return tasks[task_id] 75 76@app.get("/health") 77def health_check(): 78 return {"status": "Qlib A股系统运行中"} 79 80if __name__ == "__main__": 81 import uvicorn 82 uvicorn.run(app, host="127.0.0.1", port=8000) 83
完整前端:src/App.tsx
复制到frontend/src/App.tsx(tab切换Qlib/动量,动态卡片/曲线,输入stock)。
1import React, { useEffect, useState } from 'react'; 2import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; 3import axios from 'axios'; 4 5interface QlibData { 6 excess_return: { [date: string]: number }; 7 risk_metrics: { sharpe?: number; volatility?: number; max_drawdown?: number; win_rate?: number }; 8 annualized_return: number; 9} 10 11interface MomentumData { 12 latest_price: number; 13 latest_momentum_12m: number; 14 latest_rsi: number; 15 total_return: number; 16 sharpe_ratio: number; 17 stock: string; 18} 19 20interface TaskStatus { 21 status: 'started' | 'running' | 'completed' | 'error'; 22 progress?: number; 23 result?: QlibData | MomentumData; 24 mode?: 'qlib' | 'momentum'; 25 error?: string; 26} 27 28function App() { 29 const [data, setData] = useState<QlibData | MomentumData | null>(null); 30 const [loading, setLoading] = useState(true); 31 const [error, setError] = useState<string | null>(null); 32 const [taskId, setTaskId] = useState<string | null>(null); 33 const [progress, setProgress] = useState(0); 34 const [mode, setMode] = useState<'qlib' | 'momentum'>('qlib'); // 新:模式切换 35 const [stockCode, setStockCode] = useState('601727'); // 新:股票输入 36 37 const fetchData = () => { 38 setLoading(true); 39 setError(null); 40 let interval: NodeJS.Timeout; 41 const url = mode === 'qlib' ? 'http://127.0.0.1:8000/run_backtest' : [`http://127.0.0.1:8000/run_momentum?stock=${stockCode}`](http://127.0.0.1:8000/run_momentum?stock=${stockCode}); 42 axios.get(url) 43 .then(res => { 44 const taskIdRes = res.data.task_id; 45 console.log(`${mode}任务ID:`, taskIdRes); 46 setTaskId(taskIdRes); 47 48 interval = setInterval(() => { 49 if (taskIdRes) { 50 axios.get([`http://127.0.0.1:8000/task/${taskIdRes}`](http://127.0.0.1:8000/task/${taskIdRes})) 51 .then(statusRes => { 52 console.log('任务状态:', statusRes.data); 53 const status: TaskStatus = statusRes.data; 54 setProgress(status.progress ? status.progress * 100 : 0); 55 56 if (status.status === 'completed' && status.result) { 57 setData(status.result as any); 58 setLoading(false); 59 clearInterval(interval); 60 } else if (status.status === 'error') { 61 setError(status.error || '任务失败'); 62 setLoading(false); 63 clearInterval(interval); 64 } 65 }) 66 .catch(err => { 67 console.error('轮询错误:', err); 68 setError('轮询失败'); 69 setLoading(false); 70 clearInterval(interval); 71 }); 72 } 73 }, 10000); // 10s轮询 74 75 return () => clearInterval(interval); 76 }) 77 .catch(err => { 78 console.error('启动任务失败:', err); 79 setError('API启动失败: ' + err.message); 80 setLoading(false); 81 }); 82 }; 83 84 useEffect(() => { 85 fetchData(); 86 }, [mode, stockCode]); // 模式/股票变重拉 87 88 if (loading) { 89 return ( 90 <div className="flex flex-col justify-center items-center h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 text-gray-900 dark:text-white"> 91 <div className="text-2xl mb-4 font-semibold">生成{ mode === 'qlib' ? 'Qlib回测' : '动量分析' }中... 进度: {progress.toFixed(0)}%</div> 92 <div className="w-96 bg-gray-200 rounded-full h-4"> 93 <div 94 className="bg-gradient-to-r from-blue-600 to-indigo-600 h-4 rounded-full transition-all duration-300 shadow-md" 95 style={{ width: [`${progress}%`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.progress.md) }} 96 ></div> 97 </div> 98 <p className="mt-4 text-sm text-gray-500">预计10min(Qlib)/5s(动量)</p> 99 <button 100 onClick={() => window.location.reload()} 101 className="mt-4 px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors shadow-lg" 102 > 103 取消重试 </button> 104 </div> 105 ); 106 } 107 108 if (error) { 109 return ( 110 <div className="flex flex-col justify-center items-center h-screen bg-gradient-to-br from-red-50 to-red-100 text-red-700"> 111 <p className="text-xl mb-4 font-semibold">{error}</p> 112 <button 113 onClick={fetchData} 114 className="px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors" 115 > 116 重试 </button> 117 </div> 118 ); 119 } 120 121 const isQlib = mode === 'qlib'; 122// const chartData = isQlib ? Object.entries((data as QlibData)?.excess_return || {}).slice(-100).map(([date, val]) => ({ date, return: val })) : []; // 动量暂无曲线 123 const chartData = Object.entries((data as MomentumData)?.strategy_returns || {}).map(([date, val]) => ({ date, return: val })); 124 return ( 125 <div className="min-h-screen bg-gradient-to-br from-white to-gray-50 dark:from-gray-900 dark:to-gray-800 text-gray-900 dark:text-white p-8"> 126 <h1 className="text-4xl font-bold mb-8 text-center text-indigo-600 dark:text-indigo-400">Qlib A股量化仪表盘</h1> 127 128 {/* 新:模式切换 + 股票输入 */} 129 <div className="flex justify-center mb-6"> 130 <div className="flex space-x-4"> 131 <button 132 onClick={() => setMode('qlib')} 133 className={[`px-6 py-2 rounded-lg font-semibold ${mode === 'qlib' ? 'bg-indigo-600 text-white shadow-lg' : 'bg-gray-200 text-gray-700'}`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.font.md)} 134 > 135 Qlib回测 </button> 136 <button 137 onClick={() => setMode('momentum')} 138 className={[`px-6 py-2 rounded-lg font-semibold ${mode === 'momentum' ? 'bg-green-600 text-white shadow-lg' : 'bg-gray-200 text-gray-700'}`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.font.md)} 139 > 140 动量分析 </button> 141 </div> 142 {mode === 'momentum' && ( 143 <div className="ml-4 flex"> 144 <input 145 type="text" 146 placeholder="股票代码 e.g., 601727.SS" 147 value={stockCode} 148 onChange={(e) => setStockCode(e.target.value)} 149 className="px-4 py-2 border border-gray-300 rounded-lg mr-2" 150 /> 151 <button 152 onClick={fetchData} 153 className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700" 154 > 155 分析 </button> 156 </div> 157 )} 158 </div> 159 160 {/* 卡片:动态根据mode */} 161 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"> 162 {isQlib ? ( 163 <> 164 <div className="bg-blue-50 dark:bg-blue-900/20 p-6 rounded-xl shadow-md border border-blue-200 dark:border-blue-800"> 165 <h2 className="text-xl font-semibold text-blue-700 dark:text-blue-300 mb-2">年化收益</h2> 166 <p className="text-3xl font-bold text-blue-600 dark:text-blue-400">{((data as QlibData)?.annualized_return * 100).toFixed(2)}%</p> 167 </div> 168 <div className="bg-green-50 dark:bg-green-900/20 p-6 rounded-xl shadow-md border border-green-200 dark:border-green-800"> 169 <h2 className="text-xl font-semibold text-green-700 dark:text-green-300 mb-2">夏普比率</h2> 170 <p className="text-3xl font-bold text-green-600 dark:text-green-400">{(data as QlibData)?.risk_metrics.sharpe?.toFixed(2) || 'N/A'}</p> 171 </div> 172 <div className="bg-yellow-50 dark:bg-yellow-900/20 p-6 rounded-xl shadow-md border border-yellow-200 dark:border-yellow-800"> 173 <h2 className="text-xl font-semibold text-yellow-700 dark:text-yellow-300 mb-2">最大回撤</h2> 174 <p className="text-3xl font-bold text-yellow-600 dark:text-yellow-400">{(data as QlibData)?.risk_metrics.max_drawdown?.toFixed(2) || 'N/A'}</p> 175 </div> 176 <div className="bg-purple-50 dark:bg-purple-900/20 p-6 rounded-xl shadow-md border border-purple-200 dark:border-purple-800"> 177 <h2 className="text-xl font-semibold text-purple-700 dark:text-purple-300 mb-2">胜率</h2> 178 <p className="text-3xl font-bold text-purple-600 dark:text-purple-400">{(data as QlibData)?.risk_metrics.win_rate ? ((data as QlibData).risk_metrics.win_rate * 100).toFixed(1) + '%' : 'N/A'}</p> 179 </div> 180 </> ) : ( 181 <> 182 <div className="bg-blue-50 dark:bg-blue-900/20 p-6 rounded-xl shadow-md border border-blue-200 dark:border-blue-800"> 183 <h2 className="text-xl font-semibold text-blue-700 dark:text-blue-300 mb-2">最新价</h2> 184 <p className="text-3xl font-bold text-blue-600 dark:text-blue-400">{(data as MomentumData)?.latest_price?.toFixed(2)}元</p> 185 </div> 186 <div className="bg-green-50 dark:bg-green-900/20 p-6 rounded-xl shadow-md border border-green-200 dark:border-green-800"> 187 <h2 className="text-xl font-semibold text-green-700 dark:text-green-300 mb-2">动量分数</h2> 188 <p className="text-3xl font-bold text-green-600 dark:text-green-400">{(data as MomentumData)?.latest_momentum_1m?.toFixed(2)}</p> 189 </div> 190 <div className="bg-yellow-50 dark:bg-yellow-900/20 p-6 rounded-xl shadow-md border border-yellow-200 dark:border-yellow-800"> 191 <h2 className="text-xl font-semibold text-yellow-700 dark:text-yellow-300 mb-2">RSI</h2> 192 <p className="text-3xl font-bold text-yellow-600 dark:text-yellow-400">{(data as MomentumData)?.latest_rsi?.toFixed(2)}</p> 193 </div> 194 <div className="bg-purple-50 dark:bg-purple-900/20 p-6 rounded-xl shadow-md border border-purple-200 dark:border-purple-800"> 195 <h2 className="text-xl font-semibold text-purple-700 dark:text-purple-300 mb-2">Sharpe比率</h2> 196 <p className="text-3xl font-bold text-purple-600 dark:text-purple-400">{(data as MomentumData)?.sharpe_ratio?.toFixed(2)}</p> 197 </div> 198 </> )} 199 </div> 200 201 {/* 曲线:Qlib超额 vs 动量收益(融合对比) */} 202 <div className="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700"> 203 <h2 className="text-xl font-semibold mb-4 text-gray-800 dark:text-gray-200"> 204 {isQlib ? '回测收益曲线' : '动量回测收益曲线'} 205 </h2> 206 <ResponsiveContainer width="100%" height={400}> 207 <LineChart data={chartData}> 208 <Line type="monotone" dataKey="return" stroke="#10b981" strokeWidth={2} dot={false} name="动量收益" /> 209 </LineChart> 210 </ResponsiveContainer> 211 </div> 212 </div> 213 ); 214} 215 216export default App; 217
图形结果结束:解读 & 行动
图形(Recharts线图):蓝线超额收益波动0.001~ -0.001(日级),动量信号点标记买入(绿点),总曲线向上4.2%(跑赢大盘)。解读:动量捕捉10月9日反弹(+5%),但整体回调Sharpe降0.5(风险高,建议减仓)。行动:持仓上海电气 3 月,动量 < 0 卖出,目标 11 元。
明天预告:学“均值回归策略”,案例宁德时代300750,回测代码+前端K线图。每天1点,1月系统成型!
注:所有的数据仅供学习,不作为任何投资或者其他作用和价值。
《从零构建量化学习工具:动量策略(Momentum Strategy)》 是转载文章,点击查看原文。