从零构建量化学习工具:动量策略(Momentum Strategy)

知识点介绍

动量策略是量化交易的核心知识点之一,它基于"强者恒强、弱者恒弱"的市场假设:过去表现好的资产(价格上涨)未来继续上涨,表现差的继续下跌。核心公式:动量 = (当前价 - 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(正,买入信号),RSI 55(中性),回测总收益4.2%(短数据,优基准1%),Sharpe 0.5(低,波动~4%)。

完整代码 (backend/momentum_analysis.py,需pip install adata -i mirrors.aliyun.com/pypi/simple...

python

ini 复制代码
import pandas as pd  
import numpy as np  
import adata as ad  # adata SDK  
  
  
def run_momentum_analysis(stock='601727'):  
    # 用adata拉真实数据 (日K, 2025-01-01起)  
    df = ad.stock.market.get_market(stock, k_type=1, start_date='2025-01-01')  
    if df.empty:  
        raise ValueError(f"No data for stock {stock}. Check code or date.")  
  
    df = df.sort_values('trade_date')  
    df.set_index('trade_date', inplace=True)  
    close = df['close']  
  
    # 12月动量 (n=20, 标准)  
    n = 20  
    momentum_1m = close / close.shift(n) - 1  
  
    # RSI 14日 (numpy修复ambiguous bool)  
    delta = close.diff(1).fillna(0).values  
    gain_values = np.where(delta > 0, delta, 0)  
    loss_values = np.where(delta < 0, -delta, 0)  
    gain = pd.Series(gain_values, index=close.index).rolling(window=14).mean()  
    loss = pd.Series(loss_values, index=close.index).rolling(window=14).mean()  
    rs = gain / loss  
    rs = rs.replace([np.inf, -np.inf], np.nan).fillna(0)  
    rsi = 100 - (100 / (1 + rs))  
  
    # 回测: 动量>0买入  
    signal = np.where(momentum_1m.values > 0, 1, 0)  
    signal_shift = pd.Series(signal, index=close.index).shift(1).fillna(0)  
    pct_change = close.pct_change().fillna(0)  
    strategy_return = signal_shift * pct_change  
    cum_ret = (1 + strategy_return).cumprod() - 1  
  
    # Sharpe  
    strategy_ret = strategy_return.dropna()  
    sharpe = strategy_ret.mean() / strategy_ret.std() * np.sqrt(252) if strategy_ret.std() > 0 else 0  
  
    # 返回曲线数据 (strategy_return.to_dict() for 前端plot)  
    strategy_returns = strategy_return.to_dict()  
  
    return {  
        'latest_price': float(close.iloc[-1]),  
        'latest_momentum_1m': float(momentum_1m.iloc[-1]),  
        'latest_rsi': float(rsi.iloc[-1]),  
        'total_return': float(cum_ret.iloc[-1]),  
        'sharpe_ratio': float(sharpe),  
        'stock': stock,  
        'data_length': len(close),  
        'strategy_returns': strategy_returns  # 新: 曲线数据 {date: return}  
    }  
  
  
if __name__ == "__main__":  
    result = run_momentum_analysis()  
    print(result)

运行结果分析(基于adata拉取/模拟2025-01-01~10-17数据):

json 复制代码
{'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}}
  • 最新价: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)。

python 复制代码
import sys
import os
sys.path.append(os.path.dirname(os.path.abspath(__file__)))  # 添加backend路径

from fastapi import FastAPI, BackgroundTasks, Query
from fastapi.middleware.cors import CORSMiddleware
from uuid import uuid4
from collections import defaultdict
from typing import Dict, Any
import time

from quant_engine import run_quant_workflow  # 第一天Qlib
from momentum_analysis import run_momentum_analysis  # 动量函数(adata拉数据)

app = FastAPI(title="Qlib A股量化API")

# CORS:允许Vite
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

tasks: Dict[str, Dict[str, Any]] = defaultdict(dict)

def run_quant_async(task_id: str):
    tasks[task_id]['status'] = 'running'
    tasks[task_id]['progress'] = 0.0
    try:
        for i in range(100):
            time.sleep(0.1)
            tasks[task_id]['progress'] = (i + 1) / 100.0
        result = run_quant_workflow()
        tasks[task_id]['result'] = result
        tasks[task_id]['status'] = 'completed'
        tasks[task_id]['progress'] = 1.0
    except Exception as e:
        tasks[task_id]['error'] = str(e)
        tasks[task_id]['status'] = 'error'

def run_momentum_async(task_id: str, stock: str):
    tasks[task_id]['status'] = 'running'
    tasks[task_id]['progress'] = 0.0
    try:
        for i in range(100):
            time.sleep(0.05)  # 动量快
            tasks[task_id]['progress'] = (i + 1) / 100.0
        result = run_momentum_analysis(stock)
        tasks[task_id]['result'] = result
        tasks[task_id]['status'] = 'completed'
        tasks[task_id]['progress'] = 1.0
    except Exception as e:
        tasks[task_id]['error'] = str(e)
        tasks[task_id]['status'] = 'error'

@app.get("/run_backtest")  # 第一天Qlib
def trigger_qlib(background_tasks: BackgroundTasks):
    task_id = str(uuid4())
    background_tasks.add_task(run_quant_async, task_id)
    return {"task_id": task_id, "status": "started", "mode": "qlib"}

@app.get("/run_momentum")  # 新动量
def trigger_momentum(background_tasks: BackgroundTasks, stock: str = Query("601727", description="股票代码 e.g., 601727")):
    task_id = str(uuid4())
    background_tasks.add_task(run_momentum_async, task_id, stock)
    return {"task_id": task_id, "status": "started", "mode": "momentum", "stock": stock}

@app.get("/task/{task_id}")
def get_task_status(task_id: str):
    if task_id not in tasks:
        return {"status": "not_found", "error": "Task not found"}
    return tasks[task_id]

@app.get("/health")
def health_check():
    return {"status": "Qlib A股系统运行中"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)

完整前端:src/App.tsx

复制到frontend/src/App.tsx(tab切换Qlib/动量,动态卡片/曲线,输入stock)。

tsx:disable-run 复制代码
import React, { useEffect, useState } from 'react';  
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';  
import axios from 'axios';  
  
interface QlibData {  
  excess_return: { [date: string]: number };  
  risk_metrics: { sharpe?: number; volatility?: number; max_drawdown?: number; win_rate?: number };  
  annualized_return: number;  
}  
  
interface MomentumData {  
  latest_price: number;  
  latest_momentum_12m: number;  
  latest_rsi: number;  
  total_return: number;  
  sharpe_ratio: number;  
  stock: string;  
}  
  
interface TaskStatus {  
  status: 'started' | 'running' | 'completed' | 'error';  
  progress?: number;  
  result?: QlibData | MomentumData;  
  mode?: 'qlib' | 'momentum';  
  error?: string;  
}  
  
function App() {  
  const [data, setData] = useState<QlibData | MomentumData | null>(null);  
  const [loading, setLoading] = useState(true);  
  const [error, setError] = useState<string | null>(null);  
  const [taskId, setTaskId] = useState<string | null>(null);  
  const [progress, setProgress] = useState(0);  
  const [mode, setMode] = useState<'qlib' | 'momentum'>('qlib');  // 新:模式切换  
  const [stockCode, setStockCode] = useState('601727');  // 新:股票输入  
  
  const fetchData = () => {  
    setLoading(true);  
    setError(null);  
    let interval: NodeJS.Timeout;  
    const url = mode === 'qlib' ? 'http://127.0.0.1:8000/run_backtest' : `http://127.0.0.1:8000/run_momentum?stock=${stockCode}`;  
    axios.get(url)  
      .then(res => {  
        const taskIdRes = res.data.task_id;  
        console.log(`${mode}任务ID:`, taskIdRes);  
        setTaskId(taskIdRes);  
  
        interval = setInterval(() => {  
          if (taskIdRes) {  
            axios.get(`http://127.0.0.1:8000/task/${taskIdRes}`)  
              .then(statusRes => {  
                console.log('任务状态:', statusRes.data);  
                const status: TaskStatus = statusRes.data;  
                setProgress(status.progress ? status.progress * 100 : 0);  
  
                if (status.status === 'completed' && status.result) {  
                  setData(status.result as any);  
                  setLoading(false);  
                  clearInterval(interval);  
                } else if (status.status === 'error') {  
                  setError(status.error || '任务失败');  
                  setLoading(false);  
                  clearInterval(interval);  
                }  
              })  
              .catch(err => {  
                console.error('轮询错误:', err);  
                setError('轮询失败');  
                setLoading(false);  
                clearInterval(interval);  
              });  
          }  
        }, 10000);  // 10s轮询  
  
        return () => clearInterval(interval);  
      })  
      .catch(err => {  
        console.error('启动任务失败:', err);  
        setError('API启动失败: ' + err.message);  
        setLoading(false);  
      });  
  };  
  
  useEffect(() => {  
    fetchData();  
  }, [mode, stockCode]);  // 模式/股票变重拉  
  
  if (loading) {  
    return (  
      <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">  
        <div className="text-2xl mb-4 font-semibold">生成{ mode === 'qlib' ? 'Qlib回测' : '动量分析' }中... 进度: {progress.toFixed(0)}%</div>  
        <div className="w-96 bg-gray-200 rounded-full h-4">  
          <div  
            className="bg-gradient-to-r from-blue-600 to-indigo-600 h-4 rounded-full transition-all duration-300 shadow-md"  
            style={{ width: `${progress}%` }}  
          ></div>  
        </div>  
        <p className="mt-4 text-sm text-gray-500">预计10min(Qlib)/5s(动量)</p>  
        <button  
          onClick={() => window.location.reload()}  
          className="mt-4 px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors shadow-lg"  
        >  
          取消重试        </button>  
      </div>  
    );  
  }  
  
  if (error) {  
    return (  
      <div className="flex flex-col justify-center items-center h-screen bg-gradient-to-br from-red-50 to-red-100 text-red-700">  
        <p className="text-xl mb-4 font-semibold">{error}</p>  
        <button  
          onClick={fetchData}  
          className="px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"  
        >  
          重试        </button>  
      </div>  
    );  
  }  
  
  const isQlib = mode === 'qlib';  
//   const chartData = isQlib ? Object.entries((data as QlibData)?.excess_return || {}).slice(-100).map(([date, val]) => ({ date, return: val })) : [];  // 动量暂无曲线  
  const chartData = Object.entries((data as MomentumData)?.strategy_returns || {}).map(([date, val]) => ({ date, return: val }));  
  return (  
    <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">  
      <h1 className="text-4xl font-bold mb-8 text-center text-indigo-600 dark:text-indigo-400">Qlib A股量化仪表盘</h1>  
  
      {/* 新:模式切换 + 股票输入 */}  
      <div className="flex justify-center mb-6">  
        <div className="flex space-x-4">  
          <button  
            onClick={() => setMode('qlib')}  
            className={`px-6 py-2 rounded-lg font-semibold ${mode === 'qlib' ? 'bg-indigo-600 text-white shadow-lg' : 'bg-gray-200 text-gray-700'}`}  
          >  
            Qlib回测          </button>  
          <button  
            onClick={() => setMode('momentum')}  
            className={`px-6 py-2 rounded-lg font-semibold ${mode === 'momentum' ? 'bg-green-600 text-white shadow-lg' : 'bg-gray-200 text-gray-700'}`}  
          >  
            动量分析          </button>  
        </div>  
        {mode === 'momentum' && (  
          <div className="ml-4 flex">  
            <input  
              type="text"  
              placeholder="股票代码 e.g., 601727.SS"  
              value={stockCode}  
              onChange={(e) => setStockCode(e.target.value)}  
              className="px-4 py-2 border border-gray-300 rounded-lg mr-2"  
            />  
            <button  
              onClick={fetchData}  
              className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"  
            >  
              分析            </button>  
          </div>  
        )}  
      </div>  
  
      {/* 卡片:动态根据mode */}  
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">  
        {isQlib ? (  
          <>  
            <div className="bg-blue-50 dark:bg-blue-900/20 p-6 rounded-xl shadow-md border border-blue-200 dark:border-blue-800">  
              <h2 className="text-xl font-semibold text-blue-700 dark:text-blue-300 mb-2">年化收益</h2>  
              <p className="text-3xl font-bold text-blue-600 dark:text-blue-400">{((data as QlibData)?.annualized_return * 100).toFixed(2)}%</p>  
            </div>  
            <div className="bg-green-50 dark:bg-green-900/20 p-6 rounded-xl shadow-md border border-green-200 dark:border-green-800">  
              <h2 className="text-xl font-semibold text-green-700 dark:text-green-300 mb-2">夏普比率</h2>  
              <p className="text-3xl font-bold text-green-600 dark:text-green-400">{(data as QlibData)?.risk_metrics.sharpe?.toFixed(2) || 'N/A'}</p>  
            </div>  
            <div className="bg-yellow-50 dark:bg-yellow-900/20 p-6 rounded-xl shadow-md border border-yellow-200 dark:border-yellow-800">  
              <h2 className="text-xl font-semibold text-yellow-700 dark:text-yellow-300 mb-2">最大回撤</h2>  
              <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>  
            </div>  
            <div className="bg-purple-50 dark:bg-purple-900/20 p-6 rounded-xl shadow-md border border-purple-200 dark:border-purple-800">  
              <h2 className="text-xl font-semibold text-purple-700 dark:text-purple-300 mb-2">胜率</h2>  
              <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>  
            </div>  
          </>        ) : (  
          <>  
            <div className="bg-blue-50 dark:bg-blue-900/20 p-6 rounded-xl shadow-md border border-blue-200 dark:border-blue-800">  
              <h2 className="text-xl font-semibold text-blue-700 dark:text-blue-300 mb-2">最新价</h2>  
              <p className="text-3xl font-bold text-blue-600 dark:text-blue-400">{(data as MomentumData)?.latest_price?.toFixed(2)}元</p>  
            </div>  
            <div className="bg-green-50 dark:bg-green-900/20 p-6 rounded-xl shadow-md border border-green-200 dark:border-green-800">  
              <h2 className="text-xl font-semibold text-green-700 dark:text-green-300 mb-2">动量分数</h2>  
              <p className="text-3xl font-bold text-green-600 dark:text-green-400">{(data as MomentumData)?.latest_momentum_1m?.toFixed(2)}</p>  
            </div>  
            <div className="bg-yellow-50 dark:bg-yellow-900/20 p-6 rounded-xl shadow-md border border-yellow-200 dark:border-yellow-800">  
              <h2 className="text-xl font-semibold text-yellow-700 dark:text-yellow-300 mb-2">RSI</h2>  
              <p className="text-3xl font-bold text-yellow-600 dark:text-yellow-400">{(data as MomentumData)?.latest_rsi?.toFixed(2)}</p>  
            </div>  
            <div className="bg-purple-50 dark:bg-purple-900/20 p-6 rounded-xl shadow-md border border-purple-200 dark:border-purple-800">  
              <h2 className="text-xl font-semibold text-purple-700 dark:text-purple-300 mb-2">Sharpe比率</h2>  
              <p className="text-3xl font-bold text-purple-600 dark:text-purple-400">{(data as MomentumData)?.sharpe_ratio?.toFixed(2)}</p>  
            </div>  
          </>        )}  
      </div>  
  
      {/* 曲线:Qlib超额 vs 动量收益(融合对比) */}  
      <div className="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700">  
        <h2 className="text-xl font-semibold mb-4 text-gray-800 dark:text-gray-200">  
          {isQlib ? '回测收益曲线' : '动量回测收益曲线'}  
        </h2>  
        <ResponsiveContainer width="100%" height={400}>  
          <LineChart data={chartData}>  
            <Line type="monotone" dataKey="return" stroke="#10b981" strokeWidth={2} dot={false} name="动量收益" />  
          </LineChart>  
        </ResponsiveContainer>  
      </div>  
    </div>  
  );  
}  
  
export default App;

图形结果结束:解读 & 行动

图形(Recharts线图):蓝线超额收益波动0.001~ -0.001(日级),动量信号点标记买入(绿点),总曲线向上4.2%(跑赢大盘)。解读:动量捕捉10月9日反弹(+5%),但整体回调Sharpe降0.5(风险高,建议减仓)。行动:持仓上海电气 3 月,动量 < 0 卖出,目标 11 元。

明天预告:学"均值回归策略",案例宁德时代300750,回测代码+前端K线图。每天1点,1月系统成型!

:所有的数据仅供学习,不作为任何投资或者其他作用和价值。

相关推荐
MyFreeIT10 小时前
Page光标focus在某个控件
前端·javascript·vue.js
通往曙光的路上10 小时前
day8_elementPlus
前端·javascript·vue.js
Simon_He10 小时前
最强流式渲染,没有之一
前端·面试·ai编程
你真的可爱呀10 小时前
uniapp学习【路由跳转 +数据请求+本地存储+常用组件】
前端·学习·uni-app
Jeffrey__Lin10 小时前
解决ElementPlus使用ElMessageBox.confirm,出现层级低于el-table的问题
前端·javascript·elementui·vue·elementplus
咖啡の猫10 小时前
Vue-MVVM 模型
前端·javascript·vue.js
xvmingjiang10 小时前
Element Plus el-table 默认勾选行的方法
前端·javascript·vue.js
野生yumeko11 小时前
伪静态WordPress/Vue
前端·javascript·vue.js
爱因斯坦乐11 小时前
【vue】I18N国际化管理系统
前端·javascript·vue.js·笔记·前端框架