Files
NewQuant/data/ analysis/MA.ipynb

438 lines
331 KiB
Plaintext
Raw Normal View History

2025-09-24 23:14:14 +08:00
{
"cells": [
{
"metadata": {},
"cell_type": "raw",
"source": "# Please replace 'your_futures_data.csv' with the actual path to your CSV file",
"id": "fb1975346060eb6d"
},
{
"cell_type": "code",
"id": "initial_id",
"metadata": {
"collapsed": true,
"ExecuteTime": {
"end_time": "2026-01-01T12:13:25.245068Z",
"start_time": "2026-01-01T12:13:24.425778Z"
2025-09-24 23:14:14 +08:00
}
},
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"import seaborn as sns\n",
"import talib as ta # Make sure TA-Lib is installed: pip install TA-Lib\n",
"import statsmodels.api as sm\n",
"\n",
"import warnings\n",
"\n",
"# 忽略所有警告\n",
"warnings.filterwarnings(\"ignore\")\n",
"\n",
"# --- 0. Configure your file path ---\n",
"# Please replace 'your_futures_data.csv' with the actual path to your CSV file\n",
"file_path = 'D:/PyProject/NewQuant/data/data/KQ_m@CZCE_SA/KQ_m@CZCE_SA_min15.csv'\n",
2025-09-24 23:14:14 +08:00
"\n",
"sns.set(style='whitegrid')\n",
"plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签\n",
"plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号\n"
],
"outputs": [],
"execution_count": 1
2025-09-24 23:14:14 +08:00
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-01T12:13:25.293326Z",
"start_time": "2026-01-01T12:13:25.252980Z"
2025-09-24 23:14:14 +08:00
}
},
"cell_type": "code",
"source": [
"\n",
"# --- 1. Data Loading and Preprocessing ---\n",
"def load_and_preprocess_data(file_path):\n",
" \"\"\"\n",
" Loads historical futures data and performs basic preprocessing.\n",
" Assumes data contains 'datetime', 'open', 'high', 'low', 'close', 'volume' columns.\n",
" \"\"\"\n",
" try:\n",
" df = pd.read_csv(file_path, parse_dates=['datetime'], index_col='datetime')\n",
" # Ensure data is sorted by time\n",
" df = df.sort_index()\n",
" # Check and handle missing values\n",
" initial_rows = len(df)\n",
" df.dropna(inplace=True)\n",
" if len(df) < initial_rows:\n",
" print(f\"Warning: Missing values found in data, deleted {initial_rows - len(df)} rows.\")\n",
"\n",
" # Check if necessary columns exist\n",
" required_columns = ['open', 'high', 'low', 'close', 'volume']\n",
" if not all(col in df.columns for col in required_columns):\n",
" raise ValueError(f\"CSV file is missing required columns. Please ensure it contains: {required_columns}\")\n",
"\n",
" return df\n",
" except FileNotFoundError:\n",
" print(f\"Error: File '{file_path}' not found. Please check the path.\")\n",
" return None\n",
" except Exception as e:\n",
" print(f\"Error during data loading or preprocessing: {e}\")\n",
" return None\n",
"\n",
"\n",
"df_raw = load_and_preprocess_data(file_path)\n",
"df_raw = df_raw[df_raw.index >= '2024-01-01']\n",
"df_raw = df_raw.rename(columns={'underlying_symbol': 'symbol'})\n",
"print(df_raw.head())"
2025-09-24 23:14:14 +08:00
],
"id": "1638e05ca7ef1ac8",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
" open high low close volume open_oi \\\n",
"datetime \n",
"2024-01-02 09:00:00 2009.0 2047.0 2000.0 2041.0 79791.0 449616.0 \n",
"2024-01-02 09:15:00 2041.0 2045.0 2019.0 2019.0 32687.0 458070.0 \n",
"2024-01-02 09:30:00 2019.0 2028.0 2015.0 2026.0 17943.0 463813.0 \n",
"2024-01-02 09:45:00 2026.0 2027.0 2015.0 2016.0 11706.0 466468.0 \n",
"2024-01-02 10:00:00 2016.0 2019.0 2003.0 2016.0 35240.0 467551.0 \n",
2025-09-24 23:14:14 +08:00
"\n",
" close_oi symbol \n",
"datetime \n",
"2024-01-02 09:00:00 458070.0 CZCE.SA405 \n",
"2024-01-02 09:15:00 463813.0 CZCE.SA405 \n",
"2024-01-02 09:30:00 466468.0 CZCE.SA405 \n",
"2024-01-02 09:45:00 467551.0 CZCE.SA405 \n",
"2024-01-02 10:00:00 470145.0 CZCE.SA405 \n"
2025-09-24 23:14:14 +08:00
]
}
],
"execution_count": 2
2025-09-24 23:14:14 +08:00
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-01T12:13:25.575030Z",
"start_time": "2026-01-01T12:13:25.299843Z"
2025-09-24 23:14:14 +08:00
}
},
"cell_type": "code",
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"\n",
"def backtest_with_stats(raw, n=23, initial_capital=1000000):\n",
2025-09-24 23:14:14 +08:00
" \"\"\"\n",
" raw: pd.DataFrame, index为DatetimeIndex, columns=['symbol', 'close']\n",
" n: 趋势周期\n",
2025-09-24 23:14:14 +08:00
" \"\"\"\n",
" df = raw.copy().sort_index()\n",
"\n",
" # --- 1. 计算基础指标 (按品种分组) ---\n",
" def get_indicators(group):\n",
" # 价格均线\n",
" group['ema'] = group['close'].ewm(span=n, adjust=False).mean()\n",
" # 波动率 (价格变化绝对值的均值)\n",
" group['price_diff'] = group['close'].diff().abs()\n",
" group['vol'] = group['price_diff'].ewm(span=n, adjust=False).mean()\n",
" # 数据序号\n",
" group['count'] = range(len(group))\n",
" return group\n",
"\n",
" df = df.groupby('symbol', group_keys=False).apply(get_indicators)\n",
"\n",
" # --- 2. 生成信号与风险对齐 ---\n",
" # 使用 shift(1) 避免未来函数\n",
" df['p_prev'] = df.groupby('symbol')['close'].shift(1)\n",
" df['ema_prev'] = df.groupby('symbol')['ema'].shift(1)\n",
" df['vol_prev'] = df.groupby('symbol')['vol'].shift(1)\n",
"\n",
" # 计算信号 s_n (公式 20-1)\n",
" df['s_n'] = (df['p_prev'] - df['ema_prev']) / df['vol_prev']\n",
" df['direction'] = np.sign(df['s_n']).fillna(0)\n",
"\n",
" # 风险对齐:持仓量 = 1 / 波动率 (为了模拟方便,这里乘以一个常数系数)\n",
" # 在实战中,这个系数取决于你想在单个品种上分配多少风险金额\n",
" risk_multiplier = 1000\n",
" df['weight'] = (risk_multiplier / df['vol_prev']).fillna(0)\n",
"\n",
" # --- 3. 品种切换逻辑 (核心要求) ---\n",
" df['symbol_next'] = df['symbol'].shift(-1)\n",
" df['will_switch'] = df['symbol'] != df['symbol_next']\n",
" df['is_data_ready'] = df['count'] >= n\n",
"\n",
" # 确定最终持仓\n",
" df['final_pos'] = df['direction'] * df['weight']\n",
" df.loc[~df['is_data_ready'], 'final_pos'] = 0\n",
" df.loc[df['will_switch'], 'final_pos'] = 0 # 换品种时强平\n",
"\n",
" # --- 4. 计算盈亏 ---\n",
" df['price_change'] = df['close'].diff()\n",
" # 每日盈亏 (绝对金额)\n",
" df['pnl_daily'] = df['final_pos'].shift(1) * df['price_change']\n",
" df['pnl_daily'] = df['pnl_daily'].fillna(0)\n",
"\n",
" # 累计盈亏与净值曲线\n",
" df['cum_pnl'] = df['pnl_daily'].cumsum()\n",
" df['equity_curve'] = initial_capital + df['cum_pnl']\n",
" # 每日收益率 (用于计算指标)\n",
" df['strategy_return'] = df['pnl_daily'] / df['equity_curve'].shift(1)\n",
" df['strategy_return'] = df['strategy_return'].fillna(0)\n",
"\n",
" # --- 5. 计算统计数据 ---\n",
" days = (df.index[-1] - df.index[0]).days\n",
" annual_return = (df['equity_curve'][-1] / initial_capital) ** (365 / days) - 1\n",
" annual_vol = df['strategy_return'].std() * np.sqrt(252)\n",
" sharpe_ratio = annual_return / annual_vol if annual_vol != 0 else 0\n",
"\n",
" # 最大回撤计算\n",
" rolling_max = df['equity_curve'].cummax()\n",
" drawdown = (df['equity_curve'] - rolling_max) / rolling_max\n",
" max_drawdown = drawdown.min()\n",
"\n",
" stats = {\n",
" \"年化收益率\": f\"{annual_return:.2%}\",\n",
" \"年化波动率\": f\"{annual_vol:.2%}\",\n",
" \"夏普比率\": f\"{sharpe_ratio:.2f}\",\n",
" \"最大回撤\": f\"{max_drawdown:.2%}\",\n",
" \"累计收益\": f\"{(df['equity_curve'][-1]/initial_capital - 1):.2%}\"\n",
" }\n",
"\n",
" # --- 6. 绘图 ---\n",
" fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10), gridspec_kw={'height_ratios': [3, 1]})\n",
"\n",
" # 净值曲线图\n",
" ax1.plot(df['equity_curve'], label='Strategy Equity', color='#1f77b4')\n",
" ax1.set_title(f'Trend Following Strategy (n={n})', fontsize=14)\n",
" ax1.set_ylabel('Equity ($)')\n",
" ax1.grid(True, alpha=0.3)\n",
" ax1.legend()\n",
"\n",
" # 水下回撤图\n",
" ax2.fill_between(df.index, drawdown, 0, color='red', alpha=0.3)\n",
" ax2.set_ylabel('Drawdown')\n",
" ax2.set_title('Drawdown Area', fontsize=12)\n",
" ax2.grid(True, alpha=0.3)\n",
2025-09-24 23:14:14 +08:00
"\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
" return df, stats\n",
2025-09-24 23:14:14 +08:00
"\n",
"# --- 模拟运行 ---\n",
"# 提示:实际使用时请确保 raw 已经按照日期排序,且日期是 index\n",
"df_result, stats_result = backtest_with_stats(df_raw)\n",
"for k, v in stats_result.items(): print(f\"{k}: {v}\")"
2025-09-24 23:14:14 +08:00
],
"id": "d66b4e0fc23f3228",
2025-09-24 23:14:14 +08:00
"outputs": [
{
"data": {
"text/plain": [
"<Figure size 1200x1000 with 2 Axes>"
2025-09-24 23:14:14 +08:00
],
"image/png": "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
2025-09-24 23:14:14 +08:00
},
"metadata": {},
"output_type": "display_data",
"jetTransient": {
"display_id": null
}
2025-09-24 23:14:14 +08:00
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"年化收益率: 6.56%\n",
"年化波动率: 2.22%\n",
"夏普比率: 2.95\n",
"最大回撤: -8.56%\n",
"累计收益: 11.04%\n"
2025-09-24 23:14:14 +08:00
]
}
],
"execution_count": 3
2025-09-24 23:14:14 +08:00
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-01T12:13:25.590377Z",
"start_time": "2026-01-01T12:13:25.585660Z"
2025-09-24 23:14:14 +08:00
}
},
"cell_type": "code",
"source": "",
"id": "5756f4f24d702e02",
"outputs": [],
"execution_count": null
2025-09-24 23:14:14 +08:00
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-01T12:13:25.836181Z",
"start_time": "2026-01-01T12:13:25.596388Z"
2025-09-24 23:14:14 +08:00
}
},
"cell_type": "code",
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
2025-09-24 23:14:14 +08:00
"\n",
"def backtest_fixed_volume(raw, n=23, trade_volume=1, initial_capital=1000000):\n",
2025-09-24 23:14:14 +08:00
" \"\"\"\n",
" 固定手数、严格平仓逻辑的回测\n",
" raw: pd.DataFrame, columns=['symbol', 'close']\n",
" n: 趋势周期\n",
" trade_volume: 固定交易手数\n",
2025-09-24 23:14:14 +08:00
" \"\"\"\n",
" df = raw.copy().sort_index()\n",
"\n",
" # --- 1. 计算指标 ---\n",
" # 使用与策略类一致的 EMA\n",
" df['ema'] = df.groupby('symbol')['close'].transform(lambda x: x.ewm(span=n, adjust=False).mean())\n",
" df['vol'] = df.groupby('symbol')['close'].transform(lambda x: x.diff().abs().ewm(span=n, adjust=False).mean())\n",
"\n",
" # 计算信号 s_n (使用前一根Bar的数据)\n",
" df['s_n_prev'] = df.groupby('symbol').apply(lambda x: (x['close'].shift(1) - x['ema'].shift(1)) / x['vol'].shift(1)).reset_index(level=0, drop=True)\n",
"\n",
" # 基础信号方向\n",
" df['target_sig'] = np.sign(df['s_n_prev'].fillna(0))\n",
"\n",
" # --- 2. 严格状态机模拟 (平仓后再开仓) ---\n",
" # 由于涉及到“平仓后下一根Bar再开”的严格逻辑向量化shift无法完美表达此处使用循环计算位置\n",
" symbols = df['symbol'].unique()\n",
" all_pos = []\n",
"\n",
" for sym in symbols:\n",
" sym_df = df[df['symbol'] == sym]\n",
" sigs = sym_df['target_sig'].values\n",
" pos = np.zeros(len(sym_df))\n",
" current_pos = 0\n",
"\n",
" for i in range(1, len(sym_df)):\n",
" # 这里的逻辑严格对应策略类中的 on_open_bar\n",
" if current_pos > 0: # 当前持多头\n",
" if sigs[i] != 1: # 信号不是多头,平仓\n",
" current_pos = 0\n",
" elif current_pos < 0: # 当前持空头\n",
" if sigs[i] != -1: # 信号不是空头,平仓\n",
" current_pos = 0\n",
" else: # 当前无持仓\n",
" if sigs[i] == 1:\n",
" current_pos = trade_volume\n",
" elif sigs[i] == -1:\n",
" current_pos = -trade_volume\n",
"\n",
" pos[i] = current_pos\n",
" all_pos.extend(pos)\n",
"\n",
" df['pos'] = all_pos\n",
"\n",
" # --- 3. 盈亏计算 ---\n",
" # 盈亏计算公式:当日盈亏 = 前日持仓 * 今日价格变动\n",
" df['price_change'] = df.groupby('symbol')['close'].diff()\n",
" df['pnl_daily'] = df.groupby('symbol').apply(lambda x: x['pos'].shift(1) * x['price_change']).reset_index(level=0, drop=True)\n",
" df['pnl_daily'] = df['pnl_daily'].fillna(0)\n",
"\n",
" # 累计净值\n",
" df['cum_pnl'] = df['pnl_daily'].cumsum()\n",
" df['equity_curve'] = initial_capital + df['cum_pnl']\n",
"\n",
" # 计算收益率用于统计指标\n",
" df['returns'] = df['pnl_daily'] / initial_capital # 固定手数通常使用绝对值计算收益\n",
"\n",
" # --- 4. 统计数据 ---\n",
" days = (df.index[-1] - df.index[0]).days\n",
" total_return = df['cum_pnl'].iloc[-1] / initial_capital\n",
" # 简单年化\n",
" annual_return = total_return * (365 / max(days, 1))\n",
" # 波动率\n",
" annual_vol = df['returns'].std() * np.sqrt(252)\n",
" sharpe = annual_return / annual_vol if annual_vol != 0 else 0\n",
"\n",
" rolling_max = df['equity_curve'].cummax()\n",
" drawdown = (df['equity_curve'] - rolling_max) / rolling_max\n",
" max_dd = drawdown.min()\n",
"\n",
" stats = {\n",
" \"最终盈亏\": f\"{df['cum_pnl'].iloc[-1]:.2f}\",\n",
" \"年化收益率\": f\"{annual_return:.2%}\",\n",
" \"最大回撤\": f\"{max_dd:.2%}\",\n",
" \"夏普比率\": f\"{sharpe:.2f}\",\n",
" \"交易手数\": f\"{trade_volume}\"\n",
" }\n",
"\n",
" # --- 5. 绘图 ---\n",
" fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10), gridspec_kw={'height_ratios': [3, 1]})\n",
"\n",
" ax1.plot(df['equity_curve'], label='Equity (Fixed Volume)', color='blue')\n",
" ax1.set_title(f'Fixed Volume Trend Strategy (n={n}, Vol={trade_volume})', fontsize=14)\n",
" ax1.grid(True, alpha=0.3)\n",
" ax1.legend()\n",
"\n",
" ax2.fill_between(df.index, drawdown, 0, color='red', alpha=0.3)\n",
" ax2.set_title('Drawdown', fontsize=12)\n",
" ax2.grid(True, alpha=0.3)\n",
2025-09-24 23:14:14 +08:00
"\n",
" plt.tight_layout()\n",
" plt.show()\n",
2025-09-24 23:14:14 +08:00
"\n",
" return stats\n",
2025-09-24 23:14:14 +08:00
"\n",
"# 使用示例 (假设 df_raw 已经存在)\n",
"stats = backtest_fixed_volume(df_raw, n=23, trade_volume=10, initial_capital=1000000)\n",
"print(stats)"
2025-09-24 23:14:14 +08:00
],
"id": "72a5ce46efd6c337",
2025-09-24 23:14:14 +08:00
"outputs": [
{
"data": {
"text/plain": [
"<Figure size 1200x1000 with 2 Axes>"
],
"image/png": "iVBORw0KGgoAAAANSUhEUgAABKAAAAPYCAYAAADtj4GeAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3QXYHNXVwPETdyNBAiG4E5zgErQ4haJFixaHwoeV4rRIcYpbgRR3d3cPrgFCIBAs7vmes8PN3p2dmZ3ZndmR/f+eJ9l9V2dH75w599w2M2fOnCkAAAAAAABAQtom9cEAAAAAAAAAASgAAAAAAAAkjgwoAAAAAAAAJIoAFAAAAAAAABJFAAoAAAAAAACJIgAFAAAAAACARBGAAgAAAAAAQKIIQAEAAAAAACBRBKAAAABQSNOnT097EpAQli0A5A8BKAAAABTORRddJKeeemrak4GE6LK9+OKLmb8AkCMEoAAAAFAod999t1x77bXypz/9SbJg2rRpMnz4cPn+++/TnpTC2HbbbeWaa64pLWsAQD4QgAIApGbEiBGy2GKL+f7bZpttqt5z5513lp7T96ahnu/XE2F9j9eJ0k033VR67pZbbqlrel555ZXS+/U2b9kpQcte53PWNLLuffrpp7L//vvLKqusIssvv7z85S9/kY8++iiR6Wx1GuQ5+eST5d///rcsvfTSiX3PY489JltvvbUMGjRIll12Wdltt93kk08+8dz+V111Vdl4441lnXXWkS222ELef//9RKbptNNOK62jr732WtVzZ511Vum5559/vq7Pjnvfq8to11139X3+8ccflz/+8Y+leavz7MUXX6x4Xue7LuNTTjlFRo0aFcs0AQCS1T7hzwcAoCY9Kdtss82qHu/du3fVYyuvvHLpRGq22WbL1e/717/+VTqB0hNWmzmpWnfddaWVbLjhhjJw4MBZ80CDcxqgWXDBBUuPrbDCClIUGmjaYYcdZP7555eDDz5YxowZUwpKaBDq0Ucfle7du1cF5wYPHlwKVjWLCfh5BX3zRre19dZbT4YMGZLYdzzyyCNy6KGHloIjRx99tPz6669y9dVXlwIqDz300Kz90x133CFnnnmmHHjggaV1/quvvip1HfvrX/9aWvadO3eOfV9zww03lLYp3Vfa9LGuXbuW1q20XXnllTJ06FDfaXnwwQfliCOOKAW89FYDavvuu6/cfvvtsvjii896nS5j3XfqMUGDUQCAbCMABQBI3UILLSRbbbVVqNfOO++8pX95okEVDba89NJLVUV0X331VVliiSVkzjnnlFaiJ5HmRHLs2LGlANTqq6/e1KBLs1x11VWlZf3f//5XevXqVXpsySWXlP322690Qr3HHntUvF7r2hx00EFNnRd33XVXIQJQP/30Uykz6eGHH4783hkzZpQCSUG6dOki7dq1k9NPP1022GADufDCC6VtW6dDQf/+/eW4444rBU922WUXmTJlipx33nmy/fbblwKPStf5CRMmyDHHHCPDhg2rChI1StcZDTLpvkYDZMbPP/9cCoSuv/760rFjx1i/c+rUqaVtOIgGWfV79bUagNP1rU+fPp6v1fmjr9F9pmaI6ns1sKfz9IwzzihtR7bDDz9cNtlkE/n73//u+5kAgGwgAAUAQBOYzITPPvtMFl544dJjegKq2TA777wzy6DARo4cWQpc9OjRY9ZjGnjQDKi555471WkrGg0+abe7eoLUupw0QBNEA4N//vOfS8EQ7RZmgk9KA8nqxx9/nBXQ0i5x5nHDBIDs98ZFP1u7+z377LMybty4Wdl1GpCaOXNmIpmWb775Zqn7YZB//vOfpeDmW2+9Jc8880wpA/CCCy7wfK0G5TVgplliZvp1Xul+8qijjio9Z2fA6rLWgK4uew32AQCyixpQAIBcCapDYp7Tq+bG22+/Xco60DohNs0G0C5fK664YilrYJ999qmq3zJ58uTSFffVVlutVLfnb3/7W80r/X7MiZ9dx8Sv+53WaNEuW1rjZI011iidxOrJZFzzyn7c3NeuQhtttFFpfmg2kmZuaDc4zSz48ssvI823pOpFmRNpDdxo151vv/224nX6e7S7jk6XTp8GCV5//fWK12jXLM0++fzzz0ufo8t17bXXrhpNK85lv9RSS5UCjeeee24pK0Z169at1HVL57nSaTL1r5ROj/lbn/NbflpLSLt3adBBT9y95oeuQ/obtttuO3nuueeqPkf/6Xv1n/lb55ObdhnTot7LLLNMaZ094YQT5Jdffql4zaRJk0rr65prrllaRscee2wp2KCvP+CAA0qv2XTTTUvBGzfN2NF1TjNg6vXxxx97dt+0a6Vp1pnWY9Luc1pjSAMoavbZZy9Na9A/7UKrwQ/tDqYZT7Z33nmndGsy+7R7nf5uO7tx4sSJpUC0fldS9an0O7XouV0XzuxrdF233XfffbLllluWpkW7s2lQyKyjYenvrTXfdH0w2aD333+/rLTSSr6fZ/Yn7gxAnUYN6nnVTtPtnZpqAJB9ZEABAFKnJ616Vdum2SIdOnSI9Dl6hf2BBx4odYvRmlI9e/YsBZ40y0QDCIaetGsWg5446Qm6dgv53//+Vwr63HvvvbOyJ/Rqu9Z60e6BerKqJ2tPPfVUXb9RT8a1a4yeCJpsAQ2maJcR/WxDv+P//u//StOm3//NN9+UaqXoSbLexl0zxtATz7333rvUXUy7smjWhmZ7aP2am2++uRRICDvfkqLddo4//vjSvNTuTzo/7ewVDbD07du3lDmh644GWHRea0BSAzCGBq70d2hWmgaBNOBmglxapyfuZa/z8eWXXy7VvbnnnntK80unS9dPQx/TYJfS5a/TYabF1Mpy0/VH12/N/tCAUb9+/Sq2qT333LMU+Nprr71K36W1iTQIpL9F61GZemrqsssuK91qcNEEyGy67mnR6M0337w0rVro+7rrrisFeHX+abc0dc4555QK6mv3Qq3hpuuT/nadnyabRYNYul699957s4IwGtzT+asBHnu5RvXDDz8EdmvTdUEDMxp8bNOmjVx++eWl7nFPPPFEadvSbqD10G3h+uuvlznmmMMzy0iLZOu80CwdDfD85z//kU6dOkkSdL0264fJ6NL7up7YwbArrriiVDfJBAp1eeh06Xau60PYDC3tVhp2vtnrqJ/ffvutdOven5j3ugPPSuf7G2+8EWoaAADpIQAFAEidntzqP5uerLuv1oehJ+R6kqwBFQ2i6MmUDtVtn1BrkWI9mdFAVfv2zqFQT4Q1KHHbbbeVgit6NV0DEHpCrCfLSgMcmhE0fvz4urrGaIBBT341O0FPQrU7in6eOdHT7AjNHtHggJ7Em646euKoWTBa+0QzL5Kgn6+ZKZrFoTVsNANHT1Y1e8GcEIaZb0nS79eTfK8AgwaQNAigJ9Am0KFZF/qbbrzxxooAlGb6aNbQIYccUvpbs5DWWmut0km6Bn3iXvZ6gq4ZZpr5ogEQnVYN3mhAxxTf1+kz06gBKA2G1aqLphlaOn36G92GDx8uiyyySCnYY7KZ9HfqOvjCCy+U1jG7nppmBSmv79TsOw0s6bRqANDQAIoGMHS+mQwXXb81AKNBN6XdvnSd1gwtDQ4qna+aDaaBKxOA0ppNmnWm87kR+hlBgZ2nn366FMjUunPmN+h81FEKNeOwXpdeemkpU1DXUe1u6aZZXR9++KGMHj26lP2k23pS5pprrlKQ2GQ96bqgQRvNdDK+++670nas64PuH80+aJ555imtn7o8vNarZtAsJ+Wejyb4rkFVN12OGnQFAGQbAagaB0BNB1900UVnFY8MSw+C2ojTvuh65REA4O8Pf/hDKavC5q6bEpaeQB155JGlIsEaiNh2221LXZDs/bNeKdei0Bp08Ov+Ya6m2ydhGhDSrkN6slkPPTHXTIt33323lPGhARM7W0IDUloEWTN47ELBesKuv0dPnpMKQGk3MaWZQ3qCbDIlTBZa2PmWJP3tftkt2rVM56nJGgqaNv19JtNHaZcqfcx0/Upi2ev7tT2gBcc1S08DFSZg5zUCZBjafcwvSKABCA3GaUBGuyF
},
"metadata": {},
"output_type": "display_data",
"jetTransient": {
"display_id": null
}
},
2025-09-24 23:14:14 +08:00
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'最终盈亏': '4370.00', '年化收益率': '0.26%', '最大回撤': '-0.58%', '夏普比率': '2.38', '交易手数': '10'}\n"
2025-09-24 23:14:14 +08:00
]
}
],
"execution_count": 4
2025-09-24 23:14:14 +08:00
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}