{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "Hbol",
   "metadata": {},
   "outputs": [],
   "source": [
    "import marimo as mo"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "MJUe",
   "metadata": {},
   "outputs": [],
   "source": [
    "import urllib\n",
    "import sqlalchemy\n",
    "\n",
    "host = \"81.71.3.24\"\n",
    "port = 6785\n",
    "username = \"leopard\"\n",
    "password = urllib.parse.quote_plus(\"9NEzFzovnddf@PyEP?e*AYAWnCyd7UhYwQK$pJf>7?ccFiN^x4$eKEZ5~E<7<+~X\")\n",
    "database = \"leopard_dev\"\n",
    "engine = sqlalchemy.create_engine(f\"postgresql://{username}:{password}@{host}:{port}/{database}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "vblA",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 173108 characters]"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "dailies_df = mo.sql(\n",
    "    f\"\"\"\n",
    "    select trade_date,\n",
    "           open * factor         as Open,\n",
    "           close * factor        as Close,\n",
    "           high * factor         as High,\n",
    "           low * factor          as Low,\n",
    "           volume                as Volume,\n",
    "           coalesce(factor, 1.0) as factor\n",
    "    from leopard_daily daily\n",
    "             left join leopard_stock stock on stock.id = daily.stock_id\n",
    "    where stock.code = '000001.SZ'\n",
    "      and daily.trade_date between '2024-01-01 00:00:00' and '2025-12-31 23:59:59'\n",
    "    order by daily.trade_date\n",
    "    \"\"\",\n",
    "    engine=engine\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bkHC",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 196528 characters]"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import pandas as pd\n",
    "\n",
    "dailies_df.rename(\n",
    "    columns={'open': 'Open', 'close': 'Close', 'high': 'High', 'low': 'Low', 'volume': 'Volume'}, inplace=True\n",
    ")\n",
    "dailies_df['trade_date'] = pd.to_datetime(dailies_df['trade_date'], format='%Y-%m-%d')\n",
    "dailies_df.set_index('trade_date', inplace=True)\n",
    "dailies_df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "lEQa",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 231616 characters]"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import talib\n",
    "\n",
    "dailies_df['sma10'] = talib.SMA(dailies_df['Close'], timeperiod=10)\n",
    "dailies_df['sma30'] = talib.SMA(dailies_df['Close'], timeperiod=30)\n",
    "dailies_df['sma60'] = talib.SMA(dailies_df['Close'], timeperiod=60)\n",
    "dailies_df['sma120'] = talib.SMA(dailies_df['Close'], timeperiod=120)\n",
    "dailies_df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "PKri",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 255088 characters]"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# 指标计算完成后截取后面指标的完整的部份使用\n",
    "target_dailies_df = dailies_df.loc['2025-01-01':'2025-12-31']\n",
    "target_dailies_df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "Xref",
   "metadata": {},
   "outputs": [],
   "source": [
    "from backtesting import Strategy\n",
    "from backtesting.lib import crossover\n",
    "\n",
    "class SmaCross(Strategy):\n",
    "    def init(self):\n",
    "        self.sma10 = self.I(lambda x: x, self.data.sma10)\n",
    "        self.sma30 = self.I(lambda x: x, self.data.sma30)\n",
    "        self.sma60 = self.I(lambda x: x, self.data.sma60)\n",
    "        # self.sma120 = self.I(lambda x: x, self.data.sma120)\n",
    "\n",
    "    def next(self):\n",
    "        if self.sma60 > 0 and crossover(self.data.sma10, self.data.sma30):\n",
    "            self.buy()\n",
    "        elif self.position.size > 0 and crossover(self.data.sma30, self.data.sma10):\n",
    "            self.position.close()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "SFPL",
   "metadata": {
    "marimo": {
     "name": "*stats_print"
    }
   },
   "outputs": [],
   "source": [
    "def stats_print(stats):\n",
    "    indicator_name_mapping = {\n",
    "        # 'Start': '回测开始时间',\n",
    "        # 'End': '回测结束时间',\n",
    "        # 'Duration': '回测持续时长',\n",
    "        # 'Exposure Time [%]': '持仓时间占比（%）',\n",
    "        'Equity Final [$]': '最终收益',\n",
    "        'Equity Peak [$]': '峰值收益',\n",
    "        'Return [%]': '总收益率（%）',\n",
    "        'Buy & Hold Return [%]': '买入并持有收益率（%）',\n",
    "        'Return (Ann.) [%]': '年化收益率（%）',\n",
    "        'Volatility (Ann.) [%]': '年化波动率（%）',\n",
    "        # 'CAGR [%]': '复合年均增长率（%）',\n",
    "        # 'Sharpe Ratio': '夏普比率',\n",
    "        'Sortino Ratio': '索提诺比率',\n",
    "        'Calmar Ratio': '卡尔玛比率',\n",
    "        # 'Alpha [%]': '阿尔法系数（%）',\n",
    "        # 'Beta': '贝塔系数',\n",
    "        'Max. Drawdown [%]': '最大回撤（%）',\n",
    "        'Avg. Drawdown [%]': '平均回撤（%）',\n",
    "        'Max. Drawdown Duration': '最大回撤持续时长',\n",
    "        'Avg. Drawdown Duration': '平均回撤持续时长',\n",
    "        '# Trades': '总交易次数',\n",
    "        'Win Rate [%]': '胜率（%）',\n",
    "        # 'Best Trade [%]': '最佳单笔交易收益率（%）',\n",
    "        # 'Worst Trade [%]': '最差单笔交易收益率（%）',\n",
    "        # 'Avg. Trade [%]': '平均单笔交易收益率（%）',\n",
    "        # 'Max. Trade Duration': '单笔交易最长持有时长',\n",
    "        # 'Avg. Trade Duration': '单笔交易平均持有时长',\n",
    "        # 'Profit Factor': '盈利因子',\n",
    "        # 'Expectancy [%]': '期望收益（%）',\n",
    "        'SQN': '系统质量数',\n",
    "        # 'Kelly Criterion': '凯利准则',\n",
    "    }\n",
    "    for k, v in stats.items():\n",
    "        if k in indicator_name_mapping:\n",
    "            cn_name = indicator_name_mapping.get(k, k)\n",
    "            if isinstance(v, (int, float)):\n",
    "                if \"%\" in cn_name or k in ['Sharpe Ratio', 'Sortino Ratio', 'Calmar Ratio', 'Profit Factor']:\n",
    "                    formatted_value = f\"{v:.2f}\"\n",
    "                elif \"$\" in cn_name:\n",
    "                    formatted_value = f\"{v:.2f}\"\n",
    "                elif \"次数\" in cn_name:\n",
    "                    formatted_value = f\"{v:.0f}\"\n",
    "                else:\n",
    "                    formatted_value = f\"{v:.4f}\"\n",
    "            else:\n",
    "                formatted_value = str(v)\n",
    "            print(f'{cn_name}: {formatted_value}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "BYtC",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\r",
      "Backtest.run:   0%|                                                                                                                                                       | 0/187 [00:00<?, ?bar/s]\r",
      "                                                                                                                                                                                                   \r"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "最终收益: 106401.5007\n",
      "峰值收益: 114790.5427\n",
      "总收益率（%）: 6.40\n",
      "买入并持有收益率（%）: 5.04\n",
      "年化收益率（%）: 8.67\n",
      "年化波动率（%）: 12.75\n",
      "索提诺比率: 1.11\n",
      "卡尔玛比率: 1.19\n",
      "最大回撤（%）: -7.31\n",
      "平均回撤（%）: -2.59\n",
      "最大回撤持续时长: 97 days 00:00:00\n",
      "平均回撤持续时长: 29 days 00:00:00\n",
      "总交易次数: 2\n",
      "胜率（%）: 50.00\n",
      "系统质量数: 0.4955\n"
     ]
    }
   ],
   "source": [
    "from backtesting import Backtest\n",
    "\n",
    "import backtesting._plotting as plotting\n",
    "from bokeh.colors.named import tomato, lime\n",
    "\n",
    "plotting.BULL_COLOR = tomato\n",
    "plotting.BEAR_COLOR = lime\n",
    "\n",
    "bt = Backtest(target_dailies_df, SmaCross, cash=100000, commission=.002, finalize_trades=True)\n",
    "stats = bt.run()\n",
    "stats_print(stats)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "RGSE",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 94148 characters]"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "stats._trades\n",
    "# stats._equity_curve"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "Kclp",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 78732 characters]",
      "text/html": [
       "<iframe src='./@file/70677-2264120-qvOTIySw.html' style='width: 100%' onload='__resizeIframe(this)' frameborder='0'></iframe>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "bt.plot()"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3"
  },
  "marimo": {
   "app_config": {
    "auto_download": [
     "ipynb"
    ],
    "sql_output": "pandas",
    "width": "full"
   },
   "marimo_version": "0.19.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
