7 Revize f531ef20eb ... 34557b794c

Autor SHA1 Zpráva Datum
  maxfeng 34557b794c 完成了一版商品期货T型报价的代码 před 6 měsíci
  maxfeng a42e41ad9d 更新了一个分仓策略 před 7 měsíci
  maxfeng 3d715c5e86 增加了一个回测代码 před 7 měsíci
  maxfeng 559d01c05b 期权相关策略的初始化,更新了聚宽平台上找到的期权策略,更新了第一版牛差的卖购策略但是还没有完成 před 7 měsíci
  maxfeng abec30b077 动态基金的小改动 před 7 měsíci
  maxfeng ae4e340c6f 初始化用模型来预测 před 7 měsíci
  maxfeng 7e8f0aeda3 更新多均线穿越突破策略,添加跟踪均线止损机制及相关配置,完善文档说明,增加跳空检查功能,优化交易逻辑 před 7 měsíci
38 změnil soubory, kde provedl 8751 přidání a 161 odebrání
  1. 3 1
      .gitignore
  2. 1 0
      .python-version
  3. 148 0
      CLAUDE.md
  4. 135 0
      Lib/Options/BS_WhalleyWilmott.py
  5. 157 0
      Lib/Options/Jiabailie/01_聚宽平台期权数据获取与绘图.py
  6. 57 0
      Lib/Options/Jiabailie/02_获取期权数据列出符合要求的合约.py
  7. 310 0
      Lib/Options/Jiabailie/03_绘制期权损益分析图.py
  8. 206 0
      Lib/Options/Jiabailie/04_股指ETF期权T型报价.py
  9. 159 0
      Lib/Options/Jiabailie/05_商品期权T型报价代码.py
  10. 226 0
      Lib/Options/Jiabailie/06_50ETF期权备兑认购策略.py
  11. 318 0
      Lib/Options/Jiabailie/07_50ETF备兑认购策略改进版.py
  12. 232 0
      Lib/Options/Jiabailie/08_豆粕备兑认购策略.py
  13. 234 0
      Lib/Options/Jiabailie/09_商品主力合约备兑认购策略.py
  14. 238 0
      Lib/Options/Jiabailie/10_商品主力合约备兑认沽策略.py
  15. 309 0
      Lib/Options/Jiabailie/11_领口认购策略商品主力合约.py
  16. 273 0
      Lib/Options/Jiabailie/12_领口认购策略50ETF.py
  17. 296 0
      Lib/Options/Jiabailie/13_卖出跨式策略50ETF.py
  18. 324 0
      Lib/Options/Jiabailie/14_卖出跨式策略商品主力合约.py
  19. 377 0
      Lib/Options/Jiabailie/15_买入日历价差策略商品期货.py
  20. 355 0
      Lib/Options/Jiabailie/16_买入日历价差策略50ETF.py
  21. 354 0
      Lib/Options/README.md
  22. 195 0
      Lib/Options/README_STRATEGY_TEST.md
  23. binární
      Lib/Options/__pycache__/analysis_chart.cpython-310.pyc
  24. 60 5
      Lib/Options/analysis_chart.ipynb
  25. 305 0
      Lib/Options/analysis_chart.py
  26. 1248 0
      Lib/Options/deep_itm_bull_spread_strategy.py
  27. 219 0
      Lib/Options/product_T_price.py
  28. 1 1
      Lib/fund/FundPremium_DynamicPosition.py
  29. 530 147
      Lib/future/MultiMABreakoutStrategy_v001.py
  30. 27 7
      Lib/future/README.md
  31. binární
      Lib/future/__pycache__/MultiMABreakoutStrategy_v001.cpython-38.pyc
  32. 252 0
      Lib/future/with_model_anaylsis.ipynb
  33. 476 0
      Lib/stock/machine_learning.py
  34. 204 0
      Lib/stock/separate_warehouse.py
  35. 6 0
      main.py
  36. 10 0
      pyproject.toml
  37. 39 0
      quick_test.py
  38. 467 0
      uv.lock

+ 3 - 1
.gitignore

@@ -2,4 +2,6 @@
 
 resources/API新 - JoinQuant_files
 resources/API新 - JoinQuant.htm
-resources/part_api.html
+resources/part_api.html
+
+*.csv

+ 1 - 0
.python-version

@@ -0,0 +1 @@
+3.11

+ 148 - 0
CLAUDE.md

@@ -0,0 +1,148 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+This is a quantitative trading strategies repository focused on JoinQuant platform implementations. The codebase contains trading strategies across multiple asset classes including options, futures, ETFs, and stocks, primarily for the Chinese A-share market.
+
+## Architecture & Structure
+
+### High-Level Organization
+- **API/**: Documentation and resources for JoinQuant API usage
+- **Lib/**: Core strategy implementations organized by asset class
+  - **Options/**: Options trading strategies (bull spreads, covered calls, straddles, collars)
+  - **Fund/**: ETF/LOF fund premium/discount strategies
+  - **Future/**: Futures trading strategies (trend-following, breakout, mean-reversion)
+  - **Stock/**: Equity strategies and stock selection algorithms
+  - **Research/**: Data analysis and research notebooks
+  - **Utils/**: Utility functions and data processing tools
+- **Data/**: Sample data files and datasets
+- **Log/**: Strategy execution logs
+- **Resources/**: API documentation and external resources
+
+### Key Dependencies
+- Python 3.11+
+- pandas (data manipulation)
+- matplotlib (visualization)
+- jqdata (JoinQuant data access)
+
+## Common Development Commands
+
+### Setup & Installation
+```bash
+# Install dependencies
+pip install -r requirements.txt
+# or
+uv sync
+
+# Run basic strategy test
+python quick_test.py
+```
+
+### Strategy Development & Testing
+```bash
+# Run a specific strategy
+python Lib/Options/deep_itm_bull_spread_strategy.py
+
+# Analyze strategy performance
+python Lib/utils/read_log.py
+
+# Generate research reports
+jupyter notebook Lib/research/
+```
+
+### Common Operations
+- **Strategy Testing**: Use JoinQuant's backtesting framework
+- **Data Analysis**: Leverage pandas for financial data processing
+- **Visualization**: matplotlib for strategy performance charts
+- **Logging**: Built-in logging to /log/ directory
+
+## Strategy Categories
+
+### Options Strategies
+- **Bull Spreads**: Deep ITM call spreads with systematic roll-over
+- **Covered Calls**: ETF-based covered call strategies with dynamic adjustment
+- **Straddles/Strangles**: Volatility-based strategies for range-bound markets
+- **Collar Strategies**: Downside protection with upside cap
+- **Calendar Spreads**: Time decay arbitrage strategies
+
+### Fund Strategies
+- **Premium/Discount Arbitrage**: ETF/LOF fund discount strategies with dynamic position sizing
+- **Momentum Rotation**: ETF momentum-based sector rotation
+- **Grid Trading**: Systematic grid-based trading on fund premiums
+
+### Futures Strategies
+- **Trend Breakout**: Multi-MA crossover and breakout systems
+- **Spider Web**: Futures positioning-based sentiment analysis
+- **K-line Pattern**: Candlestick pattern recognition with trend filtering
+- **Mean Reversion**: Counter-trend strategies with strict risk controls
+
+### Risk Management Framework
+- **Dynamic Position Sizing**: Position sizing based on volatility and market conditions
+- **Multi-level Stop Loss**: Individual position and portfolio-level stops
+- **Holiday Risk Management**: Automatic position reduction before holidays
+- **Market State Filtering**: Adaptive strategies based on market regime detection
+
+## Key Code Patterns
+
+### Strategy Structure
+Most strategies follow this pattern:
+1. **Initialize**: Set up parameters, universe, and global variables
+2. **Signal Generation**: Technical analysis or fundamental filtering
+3. **Position Management**: Dynamic sizing and risk controls
+4. **Execution**: Order management with slippage and cost considerations
+5. **Monitoring**: Real-time P&L tracking and risk alerts
+
+### Common Parameters
+- `g.max_position`: Maximum number of positions
+- `g.loss_limit`: Stop loss threshold (typically 10%)
+- `g.proportion_cash`: Cash reserve for drawdowns
+- `g.holiday`: Holiday calendar for risk management
+
+### Data Sources
+- **JoinQuant API**: Real-time and historical market data
+- **Fundamental Data**: Financial statements and company metrics
+- **Options Data**: Greeks, implied volatility, and option chains
+- **Futures Data**: Positioning data, volume, and open interest
+
+## File Naming Conventions
+- Strategy files use descriptive names: `asset_class_strategy_type.py`
+- Research notebooks: `analysis_*.ipynb`
+- Test files: `test_*.py` or `*_test.py`
+- Log files: `strategy_name_YYYYMMDD.log`
+
+## Development Guidelines
+
+### Strategy Implementation
+1. Use JoinQuant's API functions (`get_price`, `order_target_value`, etc.)
+2. Implement proper error handling for API failures
+3. Include detailed logging for strategy execution
+4. Follow the existing parameter naming conventions
+
+### Testing Approach
+- Backtest using JoinQuant's framework
+- Validate with out-of-sample data
+- Check edge cases (holidays, limit moves, etc.)
+- Monitor transaction costs and slippage impact
+
+### Code Organization
+- Group related strategies in appropriate directories
+- Use consistent parameter naming across strategies
+- Implement shared utilities in `/Lib/utils/`
+- Document strategies in corresponding README.md files
+
+## Quick Start for New Strategies
+
+1. **Choose Strategy Type**: Options/Futures/Fund/Stock
+2. **Create File**: Follow naming convention in appropriate directory
+3. **Implement Framework**: Use existing strategy templates
+4. **Test Locally**: Use `quick_test.py` for basic validation
+5. **Backtest**: Upload to JoinQuant for full backtesting
+6. **Document**: Add description to relevant README.md
+
+## Important Notes
+- All strategies assume JoinQuant platform API availability
+- Some strategies may have hardcoded parameters for Chinese markets
+- Transaction costs and slippage assumptions vary by strategy
+- Holiday schedules follow Chinese market calendar

+ 135 - 0
Lib/Options/BS_WhalleyWilmott.py

@@ -0,0 +1,135 @@
+# 克隆自聚宽文章:https://www.joinquant.com/post/14348
+# 标题:场外期权对冲策略回测框架-(以Whally-Wilmott为例)
+# 作者:颖硕
+
+import datetime
+import time
+from scipy.stats import norm
+import jqdata
+import numpy
+
+def initialize(context):
+    set_options(context)
+    set_params()
+    
+# 设置期权参数
+def set_options(context):  
+    g.securities='002724.XSHE'
+    g.K=1
+    g.T=30
+    g.rf=0.09
+    g.sigma=secstd(context)
+    g.S0=secinitialprice(context)
+    g.startdate=context.run_params.start_date
+    g.maturity=maturity(context)
+    g.NP=context.portfolio.cash/1.05
+    g.YearDay=float(365)
+    g.secname=get_security_info(g.securities).display_name
+    log.info('##################################基本信息####################################')
+    log.info('标的代码->',g.securities,'标的名称->',g.secname,'名义本金->',g.NP)
+    log.info('起始日期->',g.startdate,'到期日期->',g.maturity)
+    log.info('期初价格->',g.S0,'执行价格->',g.K,'合约期限->',g.T,\
+    '无风险利率->',g.rf,'波动率->',round(g.sigma,2))
+    log.info('##############################################################################')
+
+# 设置参数    
+def set_params():
+    set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, \
+    close_commission=0.0003, min_commission=5), type='stock') 
+    set_option('use_real_price', True)
+    set_benchmark(g.securities)
+    set_slippage(FixedSlippage(0))
+
+def before_trading_start(context):
+# 获取当日日期
+    current_date=context.current_dt.date()
+# 第一天建仓
+    if current_date==g.startdate:
+        delta=DeltaCalculator(context,g.S0)
+        DeltaPosition=round(g.NP/g.S0*delta/100)*100
+        g.lastdelta=delta
+        order_target(g.securities,DeltaPosition)
+# 第二天到倒数第二天判断分红拆股
+    if current_date>g.startdate and current_date<g.maturity:
+       checkadj=get_price(g.securities,end_date=current_date,\
+       fields=['close','factor'],fq='pre',count=2)
+       if checkadj.iloc[0,1]!=checkadj.iloc[1,1]:
+          temp=g.S0
+          g.S0=round(g.S0*checkadj.iloc[0,1]/checkadj.iloc[1,1],2)
+          log.info('期初价格调整',temp,'-->',g.S0)
+# 到期日清仓
+    if current_date==g.maturity:
+       order_target(g.securities,0)
+       log.info('今天清仓日->',str(current_date))
+# 到期日之后就没事了
+    if current_date>g.maturity:
+       pass
+  
+def handle_data(context, data):
+# 获取时间并判断
+    current_time=context.current_dt
+    current_date=current_time.date()
+
+# 只有第二天到倒数第二天才会有对冲
+# 9点30会取到昨天的收盘价,所以我们跳过这一分钟。
+    if current_date>g.startdate and current_date<g.maturity and \
+       (current_time.hour>9 or current_time.minute>30): 
+
+# 中间交易日的触发对冲条件
+       price=history(1,'1m', field='close',security_list=g.securities,\
+          fq=None)
+       currentdelta=DeltaCalculator(context,price.iloc[0,0])
+       threshold=WhalleyWilmottThrehold(context,price.iloc[0,0])
+       
+       if abs(g.lastdelta-currentdelta)>threshold:
+          delta=DeltaCalculator(context,price.iloc[0,0])
+          DeltaPosition=round(g.NP/g.S0*delta/100)*100
+          g.lastdelta=delta
+          order_target(g.securities,DeltaPosition)
+
+def secstd(context):
+    end_date=context.current_dt.date()
+    start_date=end_date-datetime.timedelta(days=365)
+    price=get_price(g.securities,start_date=start_date,end_date=end_date,frequency='daily',fields='close',skip_paused=False,fq='pre')
+    rets=np.diff(np.log(price),axis=0)
+    std=rets.std()*sqrt(250)
+    return std
+
+def secinitialprice(context):
+    start_date=context.current_dt.date()
+    S0=get_price(g.securities,start_date=start_date,end_date=start_date,frequency='daily',fields='open',fq=None)
+    return S0.values[0][0]
+
+def maturity(context):
+    matday=g.startdate+datetime.timedelta(days=g.T)
+    array=jqdata.get_all_trade_days()
+    index=numpy.where(array>=matday)[0][0]  #datetime.date(matday.year,matday.month,matday.day)
+    truematday=array[index]
+    return truematday
+
+def DeltaCalculator(context,S):
+    current_date=context.current_dt.date()
+    Tau=(g.maturity-current_date).days+1
+    d1=(np.log(S/(g.S0*g.K))+(g.rf+g.sigma**2/2)*(Tau/g.YearDay))\
+    /(g.sigma*np.sqrt(Tau/g.YearDay))
+    delta=norm.cdf(d1)
+    return delta
+    
+def GammaCalculator(context,S):
+    current_date=context.current_dt.date()
+    Tau=(g.maturity-current_date).days+1
+    d1=(np.log(S/(g.S0*g.K))+(g.rf+g.sigma**2/2)*(Tau/g.YearDay))\
+    /(g.sigma*np.sqrt(Tau/g.YearDay))
+    gamma=norm.pdf(d1)/(S*g.sigma*np.sqrt(Tau/g.YearDay))
+    return gamma
+
+def WhalleyWilmottThrehold(context,S):
+    current_date=context.current_dt.date()
+    risktolerance=5
+    tradingcost=0.00055
+    Tau=(g.maturity-current_date).days+1
+    gamma=GammaCalculator(context,S)
+    a=exp(-g.rf*Tau/g.YearDay)*tradingcost*S*gamma**2
+    wwt=(3.0/2.0*a/risktolerance)**(1.0/3.0)
+    return wwt
+    

+ 157 - 0
Lib/Options/Jiabailie/01_聚宽平台期权数据获取与绘图.py

@@ -0,0 +1,157 @@
+# 聚宽平台期权数据获取与绘图
+# 参考资料:
+# - 原始策略来源: https://www.joinquant.com/view/community/detail/aa77127d7eccdaa699de7e87977f35dc
+# - 研究网址: https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E6%9C%9F%E6%9D%83%E6%95%B0%E6%8D%AE%E8%8E%B7%E5%8F%96%E4%B8%8E%E7%BB%98%E5%9B%BE.ipynb
+
+# TODO: 添加期权数据获取与绘图相关代码
+
+from jqdata import *
+import pandas as pd
+import numpy as np
+
+import matplotlib.pyplot as plt
+from matplotlib import rc
+rc("mathtext", default="regular")
+
+import seaborn as sns
+sns.set_style("white")
+
+from matplotlib import dates
+from pandas import Series,DataFrame,concat
+import matplotlib.dates as mdates
+from datetime import datetime
+
+warnings.filterwarnings('ignore')
+
+# 通过 get_all_securities 获得期权合约信息
+data = get_all_securities(types=['options'])
+
+print(data.head(10))
+
+print("\n")
+
+print(data.tail(10))
+
+# 通过 OPT_CONTRACT_INFO 表获取 50ETF 的所有合约 
+
+start = "2025-01-01"
+end   = "2025-03-01"
+
+q = query(opt.OPT_CONTRACT_INFO).filter(
+          opt.OPT_CONTRACT_INFO.underlying_symbol=='510050.XSHG',
+          opt.OPT_CONTRACT_INFO.list_date > start,
+          opt.OPT_CONTRACT_INFO.list_date < end)
+
+ins = opt.run_query(q)
+
+# cal应该是认购,put应该是认沽
+callOption = ins[ins["contract_type"]=="CO"].code.tolist()
+putOption = ins[ins["contract_type"]=="PO"].code.tolist()
+
+#早期可以使用这个函数获取期权行情数据。近期已失效
+#使用 get_price 获取多个期权的日行情
+# price = get_price(["10001151.XSHG","10001152.XSHG"],start_date='2018-01-02',panel = False)
+# price
+
+for c in callOption[:5]:
+    q_price = query(opt.OPT_DAILY_PRICE.code, opt.OPT_DAILY_PRICE.date, opt.OPT_DAILY_PRICE.close,
+                     ).filter(opt.OPT_DAILY_PRICE.code.in_([c]),
+                             opt.OPT_DAILY_PRICE.date>start)
+
+    price = opt.run_query(q_price)
+
+    print(f"c: {c}, price: {price.head()}")
+
+# 获取 10006932 在 2024-03-08 的 tick 数据
+get_ticks("10006932.XSHG", end_dt='2024-03-08 15:30:00', start_dt='2024-03-08 09:00:00')
+
+#获取所有 SR2405 合约的合约资料
+info_df = opt.run_query(query(opt.OPT_CONTRACT_INFO).filter(opt.OPT_CONTRACT_INFO.underlying_symbol=="SR2405.XZCE"))
+info_df.tail()
+
+#获取 SR405P7800.XZCE 在 2024-03-22 这天及之前10个交易日的行情数据
+
+q = query(opt.OPT_DAILY_PRICE).filter(
+    opt.OPT_DAILY_PRICE.code=='SR405P7800.XZCE',# 选择标的,多个合约用in_操作,详细查看query教程
+    opt.OPT_DAILY_PRICE.date<='2024-03-22'      # 过滤掉date大于 2024-03-22 的数据
+    ).order_by(opt.OPT_DAILY_PRICE.date.desc()  # 根据日期排序
+              ).limit(10)
+
+opt.run_query(q)
+
+#获取 20240321 这天所有 CU 合约的日行情数据
+q = query(opt.OPT_DAILY_PRICE).filter(
+    opt.OPT_DAILY_PRICE.code.like('CU%'),       # 选择code中以CU开头的标的,详细查看query教程
+    opt.OPT_DAILY_PRICE.date=='2024-03-21'      # 指定date等于2024-03-21
+    ).order_by(opt.OPT_DAILY_PRICE.date.desc()  # 根据日期排序
+              ).limit(10)
+
+opt.run_query(q)
+
+# 分别获取etf50的认购以及认沽期权合约
+start = "2025-01-01"
+end   = "2025-03-01"
+
+q = query(opt.OPT_CONTRACT_INFO).filter(
+    opt.OPT_CONTRACT_INFO.underlying_symbol=='510050.XSHG',
+    opt.OPT_CONTRACT_INFO.list_date>start,
+    opt.OPT_CONTRACT_INFO.list_date<end)
+
+ins = opt.run_query(q)
+
+callOption = ins[ins["contract_type"]=="CO"].code.tolist()
+putOption = ins[ins["contract_type"]=="PO"].code.tolist()
+
+# 获取价格数据序列
+q= query(opt.OPT_DAILY_PRICE.date,opt.OPT_DAILY_PRICE.volume,opt.OPT_DAILY_PRICE.money).filter(
+    opt.OPT_DAILY_PRICE.code.in_(callOption),      
+    opt.OPT_DAILY_PRICE.date > start,
+    opt.OPT_DAILY_PRICE.date < end,
+    )
+
+codf = opt.run_query(q)
+
+
+q= query(opt.OPT_DAILY_PRICE.date,opt.OPT_DAILY_PRICE.volume,opt.OPT_DAILY_PRICE.money).filter(
+    opt.OPT_DAILY_PRICE.code.in_(putOption),      
+    opt.OPT_DAILY_PRICE.date > start,
+    opt.OPT_DAILY_PRICE.date < end,
+    )
+
+podf = opt.run_query(q)
+
+# codf = get_price(callOption,start_date=start, end_date=end, frequency='daily', fields=["volume","money"], skip_paused=False, fq='pre', count=None, panel=False)
+# podf = get_price(putOption,start_date=start, end_date=end, frequency='daily', fields=["volume","money"], skip_paused=False, fq='pre', count=None, panel=False)
+
+# 转变数据格式
+callvol = pd.pivot_table(codf, values=["money", "volume"], index="date", aggfunc=[np.sum], dropna=True)
+putvol  = pd.pivot_table(podf, values=["money", "volume"], index="date", aggfunc=[np.sum], dropna=True)
+
+# 认购于认沽对比
+
+fig = plt.figure(figsize=(10,13))
+ax = fig.add_subplot(211)
+
+import matplotlib.dates as mdates
+ax.xaxis.set_major_formatter(mdates.DateFormatter('%y-%m-%d'))
+ax.plot(putvol.index, putvol["sum", "volume"], '--gs',linewidth=1, label = u'Put Volume')
+ax.plot(callvol.index, callvol["sum", "volume"], '--rs',linewidth=1, label = u'Call Volume')
+
+ax.grid()
+ax.set_xlabel(u"trade Date")
+ax.set_ylabel(r"Turnover Volume")
+
+ax1 = fig.add_subplot(212)
+ax1.plot(putvol.index, putvol["sum", "money"], '--gs',linewidth=1, label = u'Put Money')
+ax1.plot(callvol.index, callvol["sum", "money"], '--rs',linewidth=1, label = u'Put Money')
+ax1.grid()
+ax1.set_xlabel(u"trade Date")
+ax1.set_ylabel(r"Turnover Money")
+
+ax.legend(loc="best")
+ax1.legend(loc="best")
+plt.title('50ETF Option TurnoverVolume/ TurnoverMoney')
+
+plt.show()
+
+

+ 57 - 0
Lib/Options/Jiabailie/02_获取期权数据列出符合要求的合约.py

@@ -0,0 +1,57 @@
+# 获取期权数据,列出符合要求的合约
+# 参考资料:
+# - 原始策略来源: https://www.joinquant.com/view/community/detail/a8f4ad443448f4246260ea221c3d77ea
+# - 研究网址: https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E8%8E%B7%E5%8F%96%E6%9C%9F%E6%9D%83%E6%95%B0%E6%8D%AE%EF%BC%8C%E5%88%97%E5%87%BA%E7%AC%A6%E5%90%88%E8%A6%81%E6%B1%82%E7%9A%84%E5%90%88%E7%BA%A6.ipynb
+
+# TODO: 添加期权数据获取和合约筛选相关代码
+
+from   jqdata import *
+import pandas as pd
+import numpy as np
+
+## 日期数据处理
+trade_days = pd.Series(index=get_trade_days('2024-2-1','2024-3-26'))
+
+trade_days.index = pd.to_datetime(trade_days.index)
+
+month_split = list(trade_days.resample('M',label='left').mean().index) + [pd.to_datetime('20240201')]
+
+holding_contract2 = pd.Series(index=trade_days.index)
+
+
+# 获取期权合约数据
+q_contract_info = query(opt.OPT_CONTRACT_INFO.code, opt.OPT_CONTRACT_INFO.trading_code, opt.OPT_CONTRACT_INFO.name, #合约代码,合约交易代码,合约简称
+          opt.OPT_CONTRACT_INFO.exercise_price, opt.OPT_CONTRACT_INFO.last_trade_date, 
+          opt.OPT_CONTRACT_INFO.list_date
+         ).filter(opt.OPT_CONTRACT_INFO.contract_type == 'CO',              # CO认购期权、PO认沽期权
+                  opt.OPT_CONTRACT_INFO.exchange_code == 'XSHG',            # XSHG 上交所。XZCE 郑商所
+                  opt.OPT_CONTRACT_INFO.last_trade_date > month_split[0],   # 时间-到期月开始
+                  opt.OPT_CONTRACT_INFO.last_trade_date < month_split[1],   # 时间-到期月结束
+                  opt.OPT_CONTRACT_INFO.list_date < trade_days.index[0])    # 在交易前上市
+
+
+contract_info = opt.run_query(q_contract_info)
+
+etf_cls = get_price('510050.XSHG',trade_days.index[0],trade_days.index[0],fields=['close']).values[0][0]
+
+contract_info['price_spread'] = contract_info['exercise_price'] - etf_cls
+
+
+if contract_info['price_spread'].max() > 0:
+    
+    contract_info = contract_info[contract_info['price_spread'] > 0]  #选出虚值期权
+    contract_info = contract_info.sort_values('exercise_price')
+    
+else:  #全是实值期权
+    
+    contract_info = contract_info.sort_values('exercise_price',ascending=False)    
+
+    
+holding_contract2[trade_days.index[0]] = contract_info['code'].iloc[0]
+
+newest_exercise_price = contract_info['exercise_price'].iloc[0]
+
+newest_expire_date = contract_info['last_trade_date'].iloc[0]
+
+contract_info
+

+ 310 - 0
Lib/Options/Jiabailie/03_绘制期权损益分析图.py

@@ -0,0 +1,310 @@
+# 绘制期权损益分析图
+# 参考资料:
+# - 原始策略来源: https://www.joinquant.com/view/community/detail/ed821dbf617e69a9e9568b4b34bae458
+# - 研究网址: https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E7%BB%98%E5%88%B6%E6%9C%9F%E6%9D%83%E6%8D%9F%E7%9B%8A%E5%88%86%E6%9E%90%E5%9B%BE.ipynb
+
+# TODO: 添加期权损益分析图绘制相关代码
+
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+from statistics import mean
+
+def analyze_options(*options):
+    """
+    统一的期权分析方法
+
+    参数:
+    *options: 一个或多个期权,每个期权格式为 (direction, option_type, premium, strike_price, quantity)
+             例如: ('buy', 'call', 0.0456, 2.75, 1)
+
+    示例:
+    # 单个期权
+    analyze_options(('buy', 'call', 0.05, 3.0, 1))
+
+    # 期权组合
+    analyze_options(('buy', 'call', 0.08, 2.9, 1), ('sell', 'call', 0.03, 3.1, 1))
+    """
+
+    if not options:
+        raise ValueError("请至少提供一个期权")
+
+    # 解析期权数据
+    option_list = []
+    all_strikes = []
+
+    for i, opt in enumerate(options):
+        if len(opt) != 5:
+            raise ValueError(f"期权{i+1}格式错误,应为(direction, option_type, premium, strike_price, quantity)")
+
+        direction, option_type, premium, strike_price, quantity = opt
+        option_list.append({
+            'direction': direction,
+            'option_type': option_type,
+            'premium': premium,
+            'strike_price': strike_price,
+            'quantity': quantity
+        })
+        all_strikes.append(strike_price)
+
+    # 确定价格分析区间
+    min_strike = min(all_strikes)
+    max_strike = max(all_strikes)
+    price_min = min_strike * 0.7
+    price_max = max_strike * 1.3
+
+    # 生成价格序列
+    gap = (price_max - price_min) / 1000
+    prices = np.arange(price_min, price_max + gap, gap)
+
+    # 计算每个期权的收益
+    results = {'price': prices}
+
+    for i, opt in enumerate(option_list):
+        profits = []
+        for price in prices:
+            profit = _calculate_profit(opt, price)
+            profits.append(profit)
+        results[f'opt{i+1}'] = profits
+
+    # 计算组合收益
+    if len(option_list) > 1:
+        combined_profits = []
+        for j in range(len(prices)):
+            total = sum(results[f'opt{i+1}'][j] for i in range(len(option_list)))
+            combined_profits.append(total)
+        results['combined'] = combined_profits
+
+    # 绘制图表
+    _plot_results(results, option_list, prices)
+
+    # 打印分析报告
+    _print_report(results, option_list, prices)
+
+    return pd.DataFrame(results)
+
+
+def _calculate_profit(option, price):
+    """计算单个期权在特定价格下的收益"""
+    direction = option['direction']
+    option_type = option['option_type']
+    premium = option['premium']
+    strike_price = option['strike_price']
+    quantity = option['quantity']
+
+    if direction == 'buy' and option_type == 'call':
+        # 买入认购
+        if price > strike_price:
+            return (price - strike_price - premium) * quantity
+        else:
+            return -premium * quantity
+
+    elif direction == 'sell' and option_type == 'call':
+        # 卖出认购
+        if price > strike_price:
+            return -(price - strike_price - premium) * quantity
+        else:
+            return premium * quantity
+
+    elif direction == 'buy' and option_type == 'put':
+        # 买入认沽
+        if price < strike_price:
+            return (strike_price - price - premium) * quantity
+        else:
+            return -premium * quantity
+
+    elif direction == 'sell' and option_type == 'put':
+        # 卖出认沽
+        if price < strike_price:
+            return -(strike_price - price - premium) * quantity
+        else:
+            return premium * quantity
+
+    return 0
+
+
+def _plot_results(results, option_list, prices):
+    """绘制分析图表"""
+    plt.figure(figsize=(14, 10))
+    plt.rcParams['axes.unicode_minus'] = False
+
+    colors = ['blue', 'green', 'orange', 'purple', 'brown']
+
+    # 绘制单个期权曲线
+    for i in range(len(option_list)):
+        opt = option_list[i]
+        opt_name = f'opt{i+1}'
+        strategy_name = f"{opt['direction'].upper()} {opt['option_type'].upper()}"
+        color = colors[i % len(colors)]
+
+        plt.plot(prices, results[opt_name], '--', color=color, linewidth=2, alpha=0.7,
+                label=f'{opt_name}: {strategy_name} (行权价:{opt["strike_price"]})')
+
+    # 绘制组合曲线
+    if 'combined' in results:
+        plt.plot(prices, results['combined'], 'r-', linewidth=3, label='组合收益')
+
+    # 添加零线和行权价线
+    plt.axhline(0, color='gray', linestyle='-', alpha=0.5)
+    for opt in option_list:
+        plt.axvline(opt['strike_price'], color='gray', linestyle='--', alpha=0.3)
+
+    # 找到并标注关键点(盈亏平衡点、最大收益/损失边界点)
+    if 'combined' in results:
+        _mark_key_points(results['combined'], prices, '组合')
+    elif len(option_list) == 1:
+        _mark_key_points(results['opt1'], prices, '期权')
+
+    plt.xlabel('标的资产价格', fontsize=12)
+    plt.ylabel('收益/损失', fontsize=12)
+
+    if len(option_list) == 1:
+        opt = option_list[0]
+        title = f'{opt["direction"].upper()} {opt["option_type"].upper()} 期权分析'
+    else:
+        title = f'期权组合分析 ({len(option_list)}个期权)'
+
+    plt.title(title, fontsize=14, weight='bold')
+    plt.grid(True, alpha=0.3)
+    plt.legend()
+    plt.tight_layout()
+    plt.show()
+
+
+def _mark_key_points(profits, prices, label_prefix):
+    """标注关键点:盈亏平衡点、最大收益/损失边界点"""
+
+    # 1. 标注盈亏平衡点
+    for i in range(len(profits) - 1):
+        if profits[i] * profits[i + 1] <= 0:  # 符号改变
+            # 线性插值找到精确平衡点
+            p1, profit1 = prices[i], profits[i]
+            p2, profit2 = prices[i + 1], profits[i + 1]
+            if profit2 != profit1:
+                breakeven_price = p1 - profit1 * (p2 - p1) / (profit2 - profit1)
+                plt.plot(breakeven_price, 0, 'ro', markersize=10)
+                plt.annotate(f'平衡点: {breakeven_price:.3f}',
+                            xy=(breakeven_price, 0),
+                            xytext=(breakeven_price + (prices.max() - prices.min()) * 0.05, max(profits) * 0.1),
+                            arrowprops=dict(arrowstyle='->', color='red'),
+                            fontsize=11, color='red', weight='bold')
+
+    # 2. 找到最大收益和最大损失点
+    max_profit = max(profits)
+    min_profit = min(profits)
+
+    # 3. 检查是否存在最大收益/损失的边界点
+    # 最大收益边界点:收益达到最大值后不再增长的点
+    max_boundary_points = _find_boundary_points(profits, prices, max_profit, 'max')
+
+    # 最大损失边界点:损失达到最大值后不再增长的点
+    min_boundary_points = _find_boundary_points(profits, prices, min_profit, 'min')
+
+    # 4. 标注最大收益边界点
+    for bp in max_boundary_points:
+        plt.plot(bp, max_profit, 'go', markersize=10)
+        plt.annotate(f'最大收益边界: ({bp:.3f}, {max_profit:.3f})',
+                    xy=(bp, max_profit),
+                    xytext=(bp + (prices.max() - prices.min()) * 0.05, max_profit + (max_profit - min_profit) * 0.1),
+                    arrowprops=dict(arrowstyle='->', color='green'),
+                    fontsize=10, color='green', weight='bold')
+
+    # 5. 标注最大损失边界点
+    for bp in min_boundary_points:
+        plt.plot(bp, min_profit, 'mo', markersize=10)
+        plt.annotate(f'最大损失边界: ({bp:.3f}, {min_profit:.3f})',
+                    xy=(bp, min_profit),
+                    xytext=(bp + (prices.max() - prices.min()) * 0.05, min_profit - (max_profit - min_profit) * 0.1),
+                    arrowprops=dict(arrowstyle='->', color='magenta'),
+                    fontsize=10, color='magenta', weight='bold')
+
+
+def _find_boundary_points(profits, prices, extreme_value, _extreme_type):
+    """找到最大收益或最大损失的边界点"""
+    boundary_points = []
+    tolerance = abs(extreme_value) * 0.001  # 允许的误差范围
+
+    # 找到所有接近极值的点
+    extreme_indices = []
+    for i, profit in enumerate(profits):
+        if abs(profit - extreme_value) <= tolerance:
+            extreme_indices.append(i)
+
+    if not extreme_indices:
+        return boundary_points
+
+    # 找到连续区间的边界点
+    if len(extreme_indices) > 1:
+        # 检查是否是连续的区间
+        continuous_regions = []
+        current_region = [extreme_indices[0]]
+
+        for i in range(1, len(extreme_indices)):
+            if extreme_indices[i] - extreme_indices[i-1] <= 2:  # 允许小的间隔
+                current_region.append(extreme_indices[i])
+            else:
+                continuous_regions.append(current_region)
+                current_region = [extreme_indices[i]]
+        continuous_regions.append(current_region)
+
+        # 对于每个连续区间,标注边界点
+        for region in continuous_regions:
+            if len(region) > 10:  # 只有当区间足够长时才标注边界点
+                # 左边界点
+                left_boundary = prices[region[0]]
+                boundary_points.append(left_boundary)
+
+                # 右边界点
+                right_boundary = prices[region[-1]]
+                boundary_points.append(right_boundary)
+
+    return boundary_points
+
+
+def _print_report(results, option_list, prices):
+    """打印分析报告"""
+    print("=" * 60)
+    print("期权分析报告")
+    print("=" * 60)
+
+    # 期权基本信息
+    for i, opt in enumerate(option_list):
+        print(f"期权{i+1}: {opt['direction'].upper()} {opt['option_type'].upper()}")
+        print(f"  行权价: {opt['strike_price']}")
+        print(f"  权利金: {opt['premium']}")
+        print(f"  数量: {opt['quantity']}手")
+
+    # 分析关键指标
+    if 'combined' in results:
+        profits = results['combined']
+        print(f"\n【组合分析】")
+    else:
+        profits = results['opt1']
+        print(f"\n【单期权分析】")
+
+    max_profit = max(profits)
+    min_profit = min(profits)
+    max_idx = profits.index(max_profit)
+    min_idx = profits.index(min_profit)
+
+    print(f"最大收益: {max_profit:.4f} (标的价格: {prices[max_idx]:.4f})")
+    print(f"最大损失: {min_profit:.4f} (标的价格: {prices[min_idx]:.4f})")
+    print(f"一单最大收益: {max_profit * 10000:.2f}元")
+    print(f"一单最大亏损: {abs(min_profit) * 10000:.2f}元")
+
+    # 找盈亏平衡点
+    breakeven_points = []
+    for i in range(len(profits) - 1):
+        if profits[i] * profits[i + 1] <= 0:
+            p1, profit1 = prices[i], profits[i]
+            p2, profit2 = prices[i + 1], profits[i + 1]
+            if profit2 != profit1:
+                bp = p1 - profit1 * (p2 - p1) / (profit2 - profit1)
+                breakeven_points.append(bp)
+
+    if breakeven_points:
+        print(f"盈亏平衡点: {[f'{bp:.4f}' for bp in breakeven_points]}")
+    else:
+        print("无盈亏平衡点")
+
+    print("=" * 60)

+ 206 - 0
Lib/Options/Jiabailie/04_股指ETF期权T型报价.py

@@ -0,0 +1,206 @@
+# 股指ETF期权T型报价
+# 参考资料:
+# - 原始策略来源: https://www.joinquant.com/view/community/detail/a5a968ed72f2b827c051d337b0d74d04
+# - 研究网址: https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E8%82%A1%E6%8C%87ETF%E6%9C%9F%E6%9D%83T%E5%9E%8B%E6%8A%A5%E4%BB%B7.ipynb
+
+# TODO: 添加股指ETF期权T型报价相关代码
+
+import pandas as pd
+import numpy as np
+import talib as tb
+from   pandas.plotting import  table
+from   jqdata import *
+
+#期权合约信息
+class OptionContract:
+    
+    preClose: 0     #前一天的收盘价
+    marginUnit: 0   #单位保证金
+    prePosition: 0  #前一天收盘的持仓量
+    close: 0        #现价
+    volume: 0       #成交量
+    position: 0     #当前持仓量
+    priceChangePct: 0 #收盘价涨跌幅
+    positionChange: 0 #日增仓
+    positionChangePct: 0 #日增仓比例
+    rewardPct: 0         #报酬率(权利金/保证金)
+        
+    def __init__(self, preClose, marginUnit, 
+                 prePosition, close, volume, 
+                 position, priceChangePct, 
+                 positionChange, 
+                 positionChangePct, 
+                 rewardPct):
+        
+        self.preClose = preClose
+        self.marginUnit = marginUnit
+        self.prePosition = prePosition
+        self.close = close
+        self.volume = volume
+        self.position = position
+        self.priceChangePct = priceChangePct
+        self.positionChange = positionChange
+        self.positionChangePct = positionChangePct
+        self.rewardPct = rewardPct
+
+        
+# 同一行权价的一组期权
+class OptionGroup:
+    
+    exercisePrice: 0 ##行权价
+    call: None       ##认购的类型
+    put: None        ##认沽的类型
+        
+    def __init__(self, exercisePrice, call, put):
+        self.exercisePrice = exercisePrice
+        self.call = call
+        self.put = put
+        
+    def __init__(self, exercisePrice):
+        self.exercisePrice = exercisePrice
+
+#数据实体
+
+CONTENT = [
+    'close', 
+    'volume', 
+    'position', 
+    'priceChangePct', 
+    'positionChange', 
+    'positionChangePct',
+    'preClose', 
+    'marginUnit', 
+    'rewardPct'
+]
+
+
+##目前支持的域及对应的抬头
+CONTENT_HEADER_MAP = {
+    
+    'rewardPct': '报酬率', #计算方式:权利金/卖出一手所需的保证金*100
+    'marginUnit': '保证金', #开盘前一手该期权的保证金额,没有随盘中价格变化更新
+    'preClose': '昨收',
+    'prePosition': '昨日持仓',
+    'close': '现价', 
+    'volume': '成交量', 
+    'position': '持仓量', 
+    'priceChangePct': '涨跌幅', 
+    'positionChange': '日增仓', 
+    'positionChangePct': '日增仓率', 
+    
+}
+
+
+##期权合约,以50ETF为例 
+SUBJECT_MATTER = '510050.XSHG'
+
+##期权合约,以300ETF为例 
+#SUBJECT_MATTER = '510300.XSHG'
+
+##查询数据日期,以2024-03-22为例
+DATA_DATE = '2024-03-22'
+
+#合约到期日,以50ETF2403为例
+EXPIRE_MONTH = '2024-03-27'
+
+#查询相关的合约
+qy = query(opt.OPT_CONTRACT_INFO).filter(
+    opt.OPT_CONTRACT_INFO.underlying_symbol == SUBJECT_MATTER, ##期权标的物
+    opt.OPT_CONTRACT_INFO.exercise_date == EXPIRE_MONTH,##期权到期日
+).order_by(opt.OPT_CONTRACT_INFO.exercise_price, opt.OPT_CONTRACT_INFO.contract_type)
+optList = opt.run_query(qy)
+
+print(optList)
+
+optionGroups = {}
+
+for index, row in optList.iterrows():
+    
+    code = row['code'] #key - 期权代码
+    
+    #查询具体合约的信息,商品需要去掉开盘前的静态信息
+    optionDailyQuery = query(opt.OPT_DAILY_PREOPEN).filter(
+                             opt.OPT_DAILY_PREOPEN.code==code).order_by(
+                             opt.OPT_DAILY_PREOPEN.date.desc()).limit(1)
+    
+    dailyData = opt.run_query(optionDailyQuery)
+    realTimeQuery = query(opt.OPT_DAILY_PRICE).filter(
+                          opt.OPT_DAILY_PRICE.code==code, 
+                          opt.OPT_DAILY_PRICE.date==DATA_DATE).order_by(
+                          opt.OPT_DAILY_PRICE.date.desc()).limit(1)
+    
+    realTimeData = opt.run_query(realTimeQuery)
+   
+    #期权基本信息
+    exercisePrice = row['exercise_price'] #行权价
+    contractType = row['contract_type'] #合约类型。CO-认购期权,PO-认沽期权
+    
+    #盘前静态表查询
+    preClose = dailyData.loc[0].at['pre_close'] #前一天的收盘价
+    marginUnit = int(dailyData.loc[0].at['margin_unit']) #单位保证金
+    prePosition = dailyData.loc[0].at['position'] #前一天收盘的持仓量
+    
+    #实时表查询
+    close = realTimeData.loc[0].at['close'] #现价
+    volume = int(realTimeData.loc[0].at['volume']) #成交量
+    position = realTimeData.loc[0].at['position'] #当前持仓量
+    priceChangePct = str(round(realTimeData.loc[0].at['change_pct_close'], 2)) + '%' #收盘价涨跌幅
+    
+    #计算
+    positionChange = position - prePosition #日增仓
+    positionChangePct = str(round(abs(positionChange / prePosition * 100), 2)) + '%' #日增仓比例
+    rewardPct = round(close * 10000 / marginUnit * 100, 2) #报酬率(权利金/保证金)
+    
+    #去除非标准的带A合约
+    if(exercisePrice * 10000 % 500 != 0):
+        continue
+    optionContract = OptionContract(preClose, marginUnit, 
+                                    prePosition, close, 
+                                    volume, position, 
+                                    priceChangePct, positionChange, 
+                                    positionChangePct, rewardPct)
+    
+    if(exercisePrice in optionGroups):
+        
+        if(contractType == 'CO'):
+            optionGroups[exercisePrice].call = optionContract
+        else:
+            optionGroups[exercisePrice].put = optionContract
+            
+    else:
+        
+        optionGroup = OptionGroup(exercisePrice)
+        
+        if(contractType == 'CO'):
+            optionGroup.call = optionContract
+        else:
+            optionGroup.put = optionContract
+            
+        optionGroups[exercisePrice] = optionGroup
+
+list = optionGroups.values()
+
+rtitle = CONTENT.copy()
+rtitle.reverse()
+data = {}
+
+for key in rtitle:
+    data['C-'+CONTENT_HEADER_MAP[key]] = []
+data['行权价'] = []
+
+for key in CONTENT:
+    data['P-'+CONTENT_HEADER_MAP[key]] = []
+    
+for option in list:
+    
+    for key in CONTENT:
+        data['C-'+CONTENT_HEADER_MAP[key]].append(getattr(option.call, key))
+    data['行权价'].append(option.exercisePrice)
+    
+    for key in rtitle:
+        data['P-'+CONTENT_HEADER_MAP[key]].append(getattr(option.put, key))
+
+df = pd.DataFrame(data)
+
+df
+

+ 159 - 0
Lib/Options/Jiabailie/05_商品期权T型报价代码.py

@@ -0,0 +1,159 @@
+# 商品期权T型报价代码
+# 参考资料:
+# - 原始策略来源: https://www.joinquant.com/view/community/detail/625edad0050315dcc2df540cd462df60
+# - 研究网址: https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E5%95%86%E5%93%81%E6%9C%9F%E6%9D%83T%E5%9E%8B%E6%8A%A5%E4%BB%B7.ipynb
+
+# TODO: 添加商品期权T型报价相关代码
+
+import pandas as pd
+import numpy as np
+import talib as tb
+from pandas.plotting import  table
+from jqdata import *
+
+# 期权合约信息
+class OptionContract:
+    
+    close: 0 #现价
+    volume: 0 #成交量
+    position: 0 #当前持仓量
+    priceChangePct: 0 #收盘价涨跌幅
+        
+        
+    def __init__(self,close, volume, position, priceChangePct):
+        self.close = close
+        self.volume = volume
+        self.position = position
+        self.priceChangePct = priceChangePct
+
+
+        
+# 同一行权价的一组期权
+class OptionGroup:
+    
+    exercisePrice: 0 ##行权价
+    call: None ##提醒的品种
+    put: None  ##提醒的类型
+        
+    def __init__(self, exercisePrice, call, put):
+        self.exercisePrice = exercisePrice
+        self.call = call
+        self.put = put
+        
+    def __init__(self, exercisePrice):
+        self.exercisePrice = exercisePrice
+
+#数据内容
+
+CONTENT = [
+    'close', 
+    'volume', 
+    'position', 
+    'priceChangePct', 
+]
+
+
+##数据域
+CONTENT_HEADER_MAP = {
+    'prePosition': '昨日持仓',
+    'close': '现价', 
+    'volume': '成交量', 
+    'position': '持仓量', 
+    'priceChangePct': '涨跌幅'
+}
+
+
+# 豆粕期权
+SUBJECT_MATTER = 'M2407.XDCE'
+
+##查询数据日期,以2024-03-22为例
+DATA_DATE = '2024-03-22'
+EXPIRE_MONTH = '2024-03-27'
+
+#m2407 匹配 2024-04-19
+EXPIRE_MONTH = '2024-06-07'
+
+## 行权价间隔 比如3800 3750 每50元一档
+gap = 50
+
+#查询相关的合约,适用于商品
+
+qy = query(opt.OPT_CONTRACT_INFO).filter(
+    opt.OPT_CONTRACT_INFO.underlying_symbol == SUBJECT_MATTER, ##期权标的物
+    opt.OPT_CONTRACT_INFO.expire_date == EXPIRE_MONTH,##期权到期日
+).order_by(opt.OPT_CONTRACT_INFO.exercise_price, opt.OPT_CONTRACT_INFO.contract_type)
+
+optList = opt.run_query(qy)
+
+optList
+
+
+optionGroups = {}
+
+for index, row in optList.iterrows():
+    
+    code = row['code'] #key - 期权代码
+    
+    #查询具体合约的信息,商品需要去掉开盘前的静态信息
+    realTimeQuery = query(opt.OPT_DAILY_PRICE).filter(opt.OPT_DAILY_PRICE.code==code, opt.OPT_DAILY_PRICE.date==DATA_DATE).order_by(opt.OPT_DAILY_PRICE.date.desc()).limit(1)
+    realTimeData = opt.run_query(realTimeQuery)
+    
+    #期权基本信息
+    exercisePrice = row['exercise_price'] #行权价
+    contractType = row['contract_type'] #合约类型。CO-认购期权,PO-认沽期权
+    
+    #实时表查询
+    close = realTimeData.loc[0].at['close'] #现价
+    volume = int(realTimeData.loc[0].at['volume']) #成交量
+    position = realTimeData.loc[0].at['position'] #当前持仓量
+    priceChangePct = str(round(realTimeData.loc[0].at['change_pct_close'], 2)) + '%' #收盘价涨跌幅
+        
+    #去除非标准的带A合约
+    if(exercisePrice  % gap != 0):
+        continue
+    optionContract = OptionContract(close, volume, position, priceChangePct)
+    
+    if(exercisePrice in optionGroups):
+        
+        if(contractType == 'CO'):
+            optionGroups[exercisePrice].call = optionContract
+        else:
+            optionGroups[exercisePrice].put = optionContract
+            
+    else:
+        
+        optionGroup = OptionGroup(exercisePrice)
+        
+        if(contractType == 'CO'):
+            optionGroup.call = optionContract
+        else:
+            optionGroup.put = optionContract
+            
+        optionGroups[exercisePrice] = optionGroup
+
+list = optionGroups.values()
+
+rtitle = CONTENT.copy()
+rtitle.reverse()
+data = {}
+
+for key in rtitle:
+    data['C-'+CONTENT_HEADER_MAP[key]] = []
+data['行权价'] = []
+
+for key in CONTENT:
+    data['P-'+CONTENT_HEADER_MAP[key]] = []
+    
+for option in list:
+    
+    for key in CONTENT:
+        data['C-'+CONTENT_HEADER_MAP[key]].append(getattr(option.call, key))
+    data['行权价'].append(option.exercisePrice)
+    
+    for key in rtitle:
+        data['P-'+CONTENT_HEADER_MAP[key]].append(getattr(option.put, key))
+
+df = pd.DataFrame(data)
+
+df
+

+ 226 - 0
Lib/Options/Jiabailie/06_50ETF期权备兑认购策略.py

@@ -0,0 +1,226 @@
+# 50ETF-期权-备兑看涨策略
+# 参考资料:
+# - 原始策略来源: https://www.joinquant.com/view/community/detail/2b40f724dcea54aaa06419a46517f3db
+# - 研究网址: https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/50ETF-%E6%9C%9F%E6%9D%83-%E5%A4%87%E5%85%91%E7%9C%8B%E6%B6%A8%E7%AD%96%E7%95%A5.ipynb
+
+# TODO: 添加50ETF期权备兑看涨策略相关代码
+
+import jqdata
+from   jqdata import *
+import pandas as pd
+import numpy as np
+import datetime
+import matplotlib.pyplot as plt
+
+plt.rcParams['font.sans-serif']=['SimHei']
+plt.rcParams['axes.unicode_minus'] = False
+
+#获取交易时间和时间间隔(频率:月)
+trade_days = pd.Series(index=jqdata.get_trade_days('2025-03-01','2025-06-10'))
+trade_days.index = pd.to_datetime(trade_days.index)
+
+month_split = list(trade_days.resample('M',label='left').mean().index) + [pd.to_datetime('20250610')]
+
+month_split
+
+##传统备兑开仓策略
+holding_contract2 = pd.Series(index=trade_days.index)
+
+#获取首个持仓合约
+q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                        opt.OPT_CONTRACT_INFO.trading_code, 
+                        opt.OPT_CONTRACT_INFO.name, #合约代码,合约交易代码,合约简称
+          opt.OPT_CONTRACT_INFO.exercise_price, opt.OPT_CONTRACT_INFO.last_trade_date, 
+          opt.OPT_CONTRACT_INFO.list_date
+         ).filter(opt.OPT_CONTRACT_INFO.contract_type == 'CO',     #看涨期权
+                  opt.OPT_CONTRACT_INFO.exchange_code == 'XSHG',   #上交所
+                 opt.OPT_CONTRACT_INFO.last_trade_date > month_split[0],   #时间-到期月开始
+                 opt.OPT_CONTRACT_INFO.last_trade_date < month_split[1],   #时间-到期月结束
+                 opt.OPT_CONTRACT_INFO.list_date < trade_days.index[0])    #在交易前上市
+
+# 对应的期权列表
+contract_info = opt.run_query(q_contract_info)
+
+# 要先使用trading_code前6位为'510050'过滤
+contract_info = contract_info[contract_info['trading_code'].str[:6] == '510050']
+
+# 获取etf第一个交易日的收盘价etf_cls,price_spread是行权价exercise_price和etf价格的差
+etf_cls = get_price('510050.XSHG',trade_days.index[0],trade_days.index[0],fields=['close']).values[0][0]
+contract_info['price_spread'] = contract_info['exercise_price'] - etf_cls
+
+if contract_info['price_spread'].max() > 0:
+    
+    #选出认购虚值期权
+    contract_info = contract_info[contract_info['price_spread'] > 0]  
+    contract_info = contract_info.sort_values('exercise_price')
+    
+else:  #全是认购实值期权
+    contract_info = contract_info.sort_values('exercise_price',ascending=False)    
+
+holding_contract2[trade_days.index[0]] = contract_info['code'].iloc[0]
+
+newest_exercise_price = contract_info['exercise_price'].iloc[0]
+newest_expire_date = contract_info['last_trade_date'].iloc[0]
+
+print(f"newest_exercise_price:{newest_exercise_price}")
+print(f"newest_expire_date: {newest_expire_date}")
+print(f"first trading day: {trade_days.index[0]}")
+print(f"etf_cls: {etf_cls}")
+
+# 循环访问每一个交易日,判断交易情况
+# 规则:持有略虚值看涨期权,待行权价低于现价的 95% 时,平仓原期权合约
+# 重新开仓略虚值看涨期权;到期前1天移仓换月至次月合约
+
+for t in trade_days.index[1:]:
+    the_date = pd.to_datetime(get_trade_days(end_date=pd.to_datetime(newest_expire_date),count=2)[0])
+    print(f"processing day: {t} data, newest_expire_date: {newest_expire_date}, the_date: {the_date}")
+    
+    #到期前一天
+    if t >= the_date:
+        print(f"{t} 晚于 the_date: {the_date} 日期")
+        
+        #寻找month_idx
+        for month_idx in range(len(month_split)):
+            if month_split[month_idx] >= t:
+                break  
+                
+        q_contract_info = query(opt.OPT_CONTRACT_INFO.code, opt.OPT_CONTRACT_INFO.trading_code, opt.OPT_CONTRACT_INFO.name, 
+                  opt.OPT_CONTRACT_INFO.exercise_price, opt.OPT_CONTRACT_INFO.last_trade_date,  
+                  opt.OPT_CONTRACT_INFO.list_date
+                 ).filter(opt.OPT_CONTRACT_INFO.contract_type == 'CO',  #看涨期权
+                          opt.OPT_CONTRACT_INFO.exchange_code == 'XSHG',   #上交所
+                         opt.OPT_CONTRACT_INFO.last_trade_date > month_split[month_idx],  #时间-到期月开始
+                         opt.OPT_CONTRACT_INFO.last_trade_date <= month_split[month_idx+1],  #时间-到期月结束
+                         opt.OPT_CONTRACT_INFO.list_date < t)    #在交易前上市
+        
+        contract_info = opt.run_query(q_contract_info)
+        print(f"contract_info in the 1st step: {contract_info.head()}")
+        
+        etf_cls = get_price('510050.XSHG',t,t,fields=['close']).values[0][0]   #现货收盘价
+        contract_info['price_spread'] = contract_info['exercise_price'] - etf_cls
+        print(f"contract_info in the 2nd step: {contract_info.head()}")
+        
+        if contract_info['price_spread'].max() > 0:
+            contract_info = contract_info[contract_info['price_spread'] > 0]  #选出虚值期权
+            contract_info = contract_info.sort_values('exercise_price')
+        else:  #全是实值期权
+            contract_info = contract_info.sort_values('exercise_price',ascending=False) 
+            
+        print(f"contract_info in the 3rd step: {contract_info.head()}")    
+        if contract_info['last_trade_date'].iloc[0] >= newest_expire_date:
+            holding_contract2[t] = contract_info['code'].iloc[0]
+            newest_exercise_price = contract_info['exercise_price'].iloc[0]
+            newest_expire_date = contract_info['last_trade_date'].iloc[0]
+    else:
+        print(f"{t} 早于 newest_expire_date: {newest_expire_date} 日期")
+        #获取昨日50etf收盘价
+        pre_cls = get_price('510050.XSHG',t,t,fields=['pre_close']).values[0][0]
+        
+        if pre_cls*0.95 >= newest_exercise_price:  #原虚值变为实值,重新开仓略虚值期权
+            
+            #寻找 month_idx
+            for month_idx in range(len(month_split)):
+                if month_split[month_idx] >= t:
+                    break
+                    
+            q_contract_info = query(opt.OPT_CONTRACT_INFO.code, opt.OPT_CONTRACT_INFO.trading_code, opt.OPT_CONTRACT_INFO.name, 
+                      opt.OPT_CONTRACT_INFO.exercise_price, opt.OPT_CONTRACT_INFO.last_trade_date,  #行权价格,最后交易日
+                      opt.OPT_CONTRACT_INFO.list_date
+                     ).filter(opt.OPT_CONTRACT_INFO.contract_type == 'CO',  #看涨期权
+                              opt.OPT_CONTRACT_INFO.exchange_code == 'XSHG',   #上交所
+                             opt.OPT_CONTRACT_INFO.last_trade_date > month_split[month_idx-1],  #时间-到期月开始
+                             opt.OPT_CONTRACT_INFO.last_trade_date <= month_split[month_idx],  #时间-到期月结束
+                             opt.OPT_CONTRACT_INFO.list_date < t)    #在交易前上市
+            
+            contract_info = opt.run_query(q_contract_info)
+            etf_cls = get_price('510050.XSHG',t,t,fields=['close']).values[0][0]
+            contract_info['price_spread'] = contract_info['exercise_price'] - etf_cls
+            
+            
+            if contract_info['price_spread'].max() > 0: 
+                contract_info = contract_info[contract_info['price_spread'] > 0]  #选出虚值期权
+                contract_info = contract_info.sort_values('exercise_price')
+            else:  #全是实值期权
+                contract_info = contract_info.sort_values('exercise_price',ascending=False) 
+                 
+            if contract_info['last_trade_date'].iloc[0] >= newest_expire_date:
+                holding_contract2[t] = contract_info['code'].iloc[0]
+                newest_exercise_price = contract_info['exercise_price'].iloc[0]  
+                newest_expire_date = contract_info['last_trade_date'].iloc[0]
+
+holding_contract2 = holding_contract2.fillna(method='ffill')
+holding_contract2
+
+data2 = pd.DataFrame(holding_contract2)
+data2.columns = ['holding_contract']
+data2 = data2.reindex(columns=['holding_contract','close','last_close'])
+
+last_contract = holding_contract2.iloc[0]   #记录上个持仓
+
+for t in data2.index:
+    
+    if last_contract == data2.loc[t,'holding_contract']:  #期权未换仓
+        
+        #收盘价
+        q_price = query(opt.OPT_DAILY_PRICE.code, opt.OPT_DAILY_PRICE.date, opt.OPT_DAILY_PRICE.close,
+                 ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
+                         opt.OPT_DAILY_PRICE.date==t)
+        price = opt.run_query(q_price)['close'][0]
+        data2.loc[t,'close'] = price
+        
+    else:
+        
+        #收盘价,新
+        q_price = query(opt.OPT_DAILY_PRICE.code, opt.OPT_DAILY_PRICE.date, opt.OPT_DAILY_PRICE.close,
+                 ).filter(opt.OPT_DAILY_PRICE.code==data2.loc[t,'holding_contract'],
+                         opt.OPT_DAILY_PRICE.date==t)
+        price = opt.run_query(q_price)['close'][0]
+        data2.loc[t,'close'] = price
+        #收盘价,旧
+        q_price = query(opt.OPT_DAILY_PRICE.code, opt.OPT_DAILY_PRICE.date, opt.OPT_DAILY_PRICE.close,
+                 ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
+                         opt.OPT_DAILY_PRICE.date==t)
+        price = opt.run_query(q_price)['close'][0]
+        data2.loc[t,'last_close'] = price
+
+        last_contract = data2.loc[t,'holding_contract']
+        
+data2
+
+#计算卖出期权的收益
+
+opt_ret2 = pd.Series(0,index=data2.index)
+
+pre_close2 = data2['close'].iloc[0]
+
+for t in data2.index[1:]:
+    
+    if data2.isna().loc[t,'last_close']:   #未换仓,last为空
+        opt_ret2[t] = -10000*(data2.loc[t,'close'] - pre_close2)
+    else:
+        opt_ret2[t] = -10000*(data2.loc[t,'last_close'] - pre_close2) - 5  #手续费5元
+    pre_close2 = data2.loc[t,'close']
+    
+opt_ret2
+
+#计算持仓收益
+etf_price = get_price('510050.XSHG',trade_days.index[0],trade_days.index[-1],fields=['close'])['close']
+etf_ret = 10000*etf_price.diff(1).fillna(0)
+etf_ret
+
+#计算净值
+init_asset2 = etf_price.iloc[0]*10000
+ass2 = init_asset2 + (etf_ret + opt_ret2).cumsum()
+pfl_ret2 = (ass2/ass2.shift(1) - 1).fillna(0)
+pfl_nv2 = (1 + pfl_ret2).cumprod()
+pfl_nv2
+
+#绘制净值图
+plt.figure(figsize=(8, 5))
+plt.plot(etf_price/etf_price.iloc[0], label='50ETF现货净值')
+plt.plot(pfl_nv2, label='备兑看涨策略净值')
+plt.legend(loc='upper left', fontsize='large')
+plt.xlabel('时间',size=12)
+plt.ylabel('净值',size=12)
+plt.show()
+

+ 318 - 0
Lib/Options/Jiabailie/07_50ETF备兑认购策略改进版.py

@@ -0,0 +1,318 @@
+# 50ETF-备兑看涨策略-改进版
+# 参考资料:
+# - 原始策略来源: https://www.joinquant.com/view/community/detail/22955d161ec0a36d836a0e4f13fe66d7
+# - 研究网址: https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/50ETF-%E5%A4%87%E5%85%91%E7%9C%8B%E6%B6%A8%E7%AD%96%E7%95%A5-%E6%94%B9%E8%BF%9B%E7%89%88.ipynb
+
+# TODO: 添加50ETF备兑看涨策略改进版相关代码
+
+import jqdata
+from   jqdata import *
+import pandas as pd
+import numpy as np
+import datetime
+import matplotlib.pyplot as plt
+
+plt.rcParams['font.sans-serif']=['SimHei']
+plt.rcParams['axes.unicode_minus'] = False
+
+from datetime import datetime, timedelta
+
+
+def is_last_day_of_month(date_str):
+    
+    # 将字符串转换为日期对象
+    date_obj = datetime.strptime(date_str, '%Y-%m-%d')
+    
+    # 获取下一个日期对象
+    next_date_obj = date_obj + timedelta(days=1)
+    
+    # 判断是否为下个月的第一天,如果是,则当前日期为月末
+    return date_obj.month != next_date_obj.month
+
+
+
+def get_last_day_of_month(date_str):
+    
+    # 将字符串转换为日期对象
+    date_obj = datetime.strptime(date_str, '%Y-%m-%d')
+    
+    # 获取下个月的第一天日期对象
+    next_month_first_day = datetime(date_obj.year, date_obj.month + 1, 1)
+    
+    # 从下个月的第一天减去一天,得到当前月的月末日期对象
+    last_day_of_month = next_month_first_day - timedelta(days=1)
+    
+    # 返回月末日期的字符串形式
+    return last_day_of_month.strftime('%Y-%m-%d')
+
+#获取交易时间和时间间隔(频率:月)
+#根据不同交易日分割月份
+#指定回测的起始时间
+
+starttime = '2020-01-01'
+endtime   = '2024-04-10'
+
+trade_days = pd.Series(index=jqdata.get_trade_days(starttime,endtime))
+trade_days.index = pd.to_datetime(trade_days.index)
+last_day = get_last_day_of_month(endtime)
+
+month_split = list(trade_days.resample('M',label='left').mean().index) + [pd.to_datetime(last_day)]
+month_split
+
+##持仓情况
+holding_contract2 = pd.Series(index=trade_days.index)
+
+
+#获取首个持仓合约
+q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                        opt.OPT_CONTRACT_INFO.trading_code, 
+                        opt.OPT_CONTRACT_INFO.name,         #合约代码,合约交易代码,合约简称
+                        opt.OPT_CONTRACT_INFO.exercise_price, 
+                        opt.OPT_CONTRACT_INFO.last_trade_date, 
+                        opt.OPT_CONTRACT_INFO.list_date
+         ).filter(opt.OPT_CONTRACT_INFO.contract_type == 'CO',             # 看涨期权
+                  opt.OPT_CONTRACT_INFO.exchange_code == 'XSHG',           # 上交所
+                  opt.OPT_CONTRACT_INFO.last_trade_date > month_split[0],  # 时间-到期月开始
+                  opt.OPT_CONTRACT_INFO.last_trade_date < month_split[1],  # 时间-到期月结束
+                  opt.OPT_CONTRACT_INFO.list_date < trade_days.index[0])   # 在交易前上市
+
+
+contract_info = opt.run_query(q_contract_info)
+
+contract_info
+
+etf_cls = get_price('510050.XSHG',trade_days.index[0],trade_days.index[0],fields=['close']).values[0][0]
+etf_cls
+
+contract_info['price_spread'] = contract_info['exercise_price'] - etf_cls
+contract_info
+
+etf_cls = get_price('510050.XSHG',trade_days.index[0],trade_days.index[0],fields=['close']).values[0][0]
+
+contract_info['price_spread'] = contract_info['exercise_price'] - etf_cls
+
+
+if contract_info['price_spread'].max() > 0:
+    
+    contract_info = contract_info[contract_info['price_spread'] > 0]  #选出虚值期权
+    contract_info = contract_info.sort_values('exercise_price')
+    
+else:  #全是实值期权
+    
+    contract_info = contract_info.sort_values('exercise_price',ascending=False)    
+    
+contract_info
+
+holding_contract2[trade_days.index[0]] = contract_info['code'].iloc[0]
+
+newest_exercise_price = contract_info['exercise_price'].iloc[0]
+
+newest_expire_date = contract_info['last_trade_date'].iloc[0]
+
+
+print(newest_exercise_price)
+print(newest_expire_date)
+
+# 循环访问每一个交易日,判断交易情况
+ 
+# 规则:持有略虚值看涨期权
+# 待行权价低于现价的95%时,平仓原期权合约,重新开仓略虚值看涨期权
+# 到期前 1 天移仓换月至次月合约
+
+for t in trade_days.index[1:]:
+    
+    #到期前一天
+    if t >= pd.to_datetime(get_trade_days(end_date=pd.to_datetime(newest_expire_date),count=2)[0]):
+        
+        #寻找month_idx
+        for month_idx in range(len(month_split)):
+            if month_split[month_idx] >= t:
+                break
+                
+        if month_idx == len(month_split) -1:
+            lasttradeday = t
+            break
+        else:
+            lasttradeday = ''
+            
+        q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                                opt.OPT_CONTRACT_INFO.trading_code, 
+                                opt.OPT_CONTRACT_INFO.name, 
+                                opt.OPT_CONTRACT_INFO.exercise_price,
+                                opt.OPT_CONTRACT_INFO.last_trade_date,  
+                                opt.OPT_CONTRACT_INFO.list_date
+                 ).filter(opt.OPT_CONTRACT_INFO.contract_type == 'CO',     # 看涨期权
+                          opt.OPT_CONTRACT_INFO.exchange_code == 'XSHG',   # 上交所
+                          opt.OPT_CONTRACT_INFO.last_trade_date > month_split[month_idx],    # 时间-到期月开始
+                          opt.OPT_CONTRACT_INFO.last_trade_date <= month_split[month_idx+1], # 时间-到期月结束
+                          opt.OPT_CONTRACT_INFO.list_date < t)    # 在交易前上市
+        
+        contract_info = opt.run_query(q_contract_info)
+        
+        etf_cls = get_price('510050.XSHG',t,t,fields=['close']).values[0][0]   #现货收盘价
+        
+        contract_info['price_spread'] = contract_info['exercise_price'] - etf_cls
+        
+        if contract_info['price_spread'].max() > 0:
+            
+            contract_info = contract_info[contract_info['price_spread'] > 0]  #选出虚值期权
+            contract_info = contract_info.sort_values('exercise_price')
+            
+        else:  #全是实值期权
+            contract_info = contract_info.sort_values('exercise_price',ascending=False) 
+            
+            
+        if contract_info['last_trade_date'].iloc[0] >= newest_expire_date:
+            
+            holding_contract2[t] = contract_info['code'].iloc[0]
+            newest_exercise_price = contract_info['exercise_price'].iloc[0]
+            newest_expire_date = contract_info['last_trade_date'].iloc[0]
+            
+    else:
+        
+        #获取昨日50etf收盘价
+        pre_cls = get_price('510050.XSHG',t,t,fields=['pre_close']).values[0][0]
+        
+        if pre_cls*0.95 >= newest_exercise_price:  #原虚值变为实值,重新开仓略虚值期权
+            
+            #寻找month_idx
+            for month_idx in range(len(month_split)):
+                if month_split[month_idx] >= t:
+                    break
+                    
+            q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                                    opt.OPT_CONTRACT_INFO.trading_code, 
+                                    opt.OPT_CONTRACT_INFO.name, 
+                                    opt.OPT_CONTRACT_INFO.exercise_price, 
+                                    opt.OPT_CONTRACT_INFO.last_trade_date,  #行权价格,最后交易日
+                                    opt.OPT_CONTRACT_INFO.list_date
+                     ).filter(opt.OPT_CONTRACT_INFO.contract_type == 'CO',  #看涨期权
+                              opt.OPT_CONTRACT_INFO.exchange_code == 'XSHG',   #上交所
+                              opt.OPT_CONTRACT_INFO.last_trade_date > month_split[month_idx-1],  #时间-到期月开始
+                              opt.OPT_CONTRACT_INFO.last_trade_date <= month_split[month_idx],  #时间-到期月结束
+                              opt.OPT_CONTRACT_INFO.list_date < t)    #在交易前上市
+            
+            contract_info = opt.run_query(q_contract_info)
+            etf_cls = get_price('510050.XSHG',t,t,fields=['close']).values[0][0]
+            contract_info['price_spread'] = contract_info['exercise_price'] - etf_cls
+            
+            if contract_info['price_spread'].max() > 0: 
+                
+                contract_info = contract_info[contract_info['price_spread'] > 0]  #选出虚值期权
+                contract_info = contract_info.sort_values('exercise_price')
+                
+            else:  #全是实值期权
+                
+                contract_info = contract_info.sort_values('exercise_price',ascending=False) 
+                
+                
+            if contract_info['last_trade_date'].iloc[0] >= newest_expire_date:
+                
+                holding_contract2[t] = contract_info['code'].iloc[0]
+                newest_exercise_price = contract_info['exercise_price'].iloc[0]  
+                newest_expire_date = contract_info['last_trade_date'].iloc[0]
+
+                
+holding_contract2 = holding_contract2.fillna(method='ffill')
+
+holding_contract2
+
+if lasttradeday != '':
+    holding_contract2 = holding_contract2[holding_contract2.index < lasttradeday]
+
+data2 = pd.DataFrame(holding_contract2)
+data2.columns = ['holding_contract']
+data2 = data2.reindex(columns=['holding_contract','close','last_close'])
+
+
+last_contract = holding_contract2.iloc[0]   #记录上个持仓
+
+for t in data2.index:
+    
+    if last_contract == data2.loc[t,'holding_contract']:  #期权未换仓
+        
+        #收盘价
+        q_price = query(opt.OPT_DAILY_PRICE.code, 
+                        opt.OPT_DAILY_PRICE.date, 
+                        opt.OPT_DAILY_PRICE.close,
+                 ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
+                          opt.OPT_DAILY_PRICE.date==t)
+        
+        price = opt.run_query(q_price)['close'][0]
+        data2.loc[t,'close'] = price
+        
+    else:
+        
+        #收盘价,新
+        q_price = query(opt.OPT_DAILY_PRICE.code, 
+                        opt.OPT_DAILY_PRICE.date, 
+                        opt.OPT_DAILY_PRICE.close,
+                 ).filter(opt.OPT_DAILY_PRICE.code==data2.loc[t,'holding_contract'],
+                          opt.OPT_DAILY_PRICE.date==t)
+        
+        price = opt.run_query(q_price)['close'][0]
+        data2.loc[t,'close'] = price
+        
+        #收盘价,旧
+        q_price = query(opt.OPT_DAILY_PRICE.code, 
+                        opt.OPT_DAILY_PRICE.date, 
+                        opt.OPT_DAILY_PRICE.close,
+                 ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
+                          opt.OPT_DAILY_PRICE.date==t)
+        
+        price = opt.run_query(q_price)['close'][0]
+        
+        data2.loc[t,'last_close'] = price
+
+        last_contract = data2.loc[t,'holding_contract']
+        
+data2
+
+#计算卖出期权的收益
+opt_ret2 = pd.Series(0,index=data2.index)
+
+pre_close2 = data2['close'].iloc[0]
+
+for t in data2.index[1:]:
+    
+    if data2.isna().loc[t,'last_close']:   #未换仓,last为空
+        
+        opt_ret2[t] = -10000*(data2.loc[t,'close'] - pre_close2)
+        
+    else:
+        
+        opt_ret2[t] = -10000*(data2.loc[t,'last_close'] - pre_close2) - 5  #手续费5元
+        
+    pre_close2 = data2.loc[t,'close']
+    
+opt_ret2
+
+#计算持仓收益
+etf_price = get_price('510050.XSHG',trade_days.index[0],trade_days.index[-1],fields=['close'])['close']
+etf_ret = 10000*etf_price.diff(1).fillna(0)
+etf_ret
+
+#计算净值
+init_asset2 = etf_price.iloc[0]*10000
+
+ass2 = init_asset2 + (etf_ret + opt_ret2).cumsum()
+
+pfl_ret2 = (ass2/ass2.shift(1) - 1).fillna(0)
+
+pfl_nv2 = (1 + pfl_ret2).cumprod()
+
+pfl_nv2
+
+### 绘制净值图
+
+plt.figure(figsize=(30, 20))
+
+plt.plot(etf_price/etf_price.iloc[0], label='50ETF现货净值')
+plt.plot(pfl_nv2, label='备兑看涨策略净值')
+
+plt.legend(loc='upper left', fontsize='large')
+
+plt.xlabel('时间',size=12)
+plt.ylabel('净值',size=12)
+
+plt.show() 

+ 232 - 0
Lib/Options/Jiabailie/08_豆粕备兑认购策略.py

@@ -0,0 +1,232 @@
+# 豆粕-备兑看涨策略
+# 参考资料:
+# - 原始策略来源: https://www.joinquant.com/view/community/detail/4bf820b677d7d774f54c122460533b2e
+# - 研究网址: https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E8%B1%86%E7%B2%95-%E5%A4%87%E5%85%91%E7%9C%8B%E6%B6%A8%E7%AD%96%E7%95%A5.ipynb
+
+# TODO: 添加豆粕备兑看涨策略相关代码
+
+import jqdata
+from   jqdata import *
+import pandas as pd
+import numpy as np
+import datetime
+import matplotlib.pyplot as plt
+
+plt.rcParams['font.sans-serif']=['SimHei']
+plt.rcParams['axes.unicode_minus'] = False
+
+from datetime import datetime, timedelta
+
+def is_last_day_of_month(date_str):
+    
+    # 将字符串转换为日期对象
+    date_obj = datetime.strptime(date_str, '%Y-%m-%d')
+    
+    # 获取下一个日期对象
+    next_date_obj = date_obj + timedelta(days=1)
+    
+    # 判断是否为下个月的第一天,如果是,则当前日期为月末
+    return date_obj.month != next_date_obj.month
+
+def get_last_day_of_month(date_str):
+    
+    # 将字符串转换为日期对象
+    date_obj = datetime.strptime(date_str, '%Y-%m-%d')
+    
+    # 获取下个月的第一天日期对象
+    next_month_first_day = datetime(date_obj.year, date_obj.month + 1, 1)
+    
+    # 从下个月的第一天减去一天,得到当前月的月末日期对象
+    last_day_of_month = next_month_first_day - timedelta(days=1)
+    
+    # 返回月末日期的字符串形式
+    return last_day_of_month.strftime('%Y-%m-%d')
+
+# 豆粕合约
+SUBJECT_MATTER = 'M2409.XDCE'
+info = get_security_info(SUBJECT_MATTER)
+starttime = str(info.start_date)
+endtime = str(info.end_date)
+print(starttime)
+print(endtime)
+
+#查询相关的合约,适用于商品
+qy = query(opt.OPT_CONTRACT_INFO).filter(
+    opt.OPT_CONTRACT_INFO.underlying_symbol == SUBJECT_MATTER, ##期权标的物
+    opt.OPT_CONTRACT_INFO.contract_type == 'CO'
+).order_by(opt.OPT_CONTRACT_INFO.exercise_price)
+
+#合约价差
+price_gap = 10
+optList = opt.run_query(qy)
+optList
+
+#获取交易时间和时间间隔(频率:月)
+#根据不同交易日分割月份
+#指定回测的起始时间
+
+trade_days = pd.Series(index=jqdata.get_trade_days(starttime,endtime))
+trade_days.index = pd.to_datetime(trade_days.index)
+
+last_day = get_last_day_of_month(endtime)
+
+month_split = list(trade_days.resample('M',label='left').mean().index) + [pd.to_datetime(last_day)]
+month_split
+
+##持仓情况
+holding_contract2 = pd.Series(index=trade_days.index)
+
+#获取首个持仓合约
+q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                        opt.OPT_CONTRACT_INFO.trading_code, 
+                        opt.OPT_CONTRACT_INFO.name, #合约代码,合约交易代码,合约简称
+                        opt.OPT_CONTRACT_INFO.exercise_price, opt.OPT_CONTRACT_INFO.last_trade_date, 
+                        opt.OPT_CONTRACT_INFO.list_date
+         ).filter(opt.OPT_CONTRACT_INFO.underlying_symbol == SUBJECT_MATTER,
+                 opt.OPT_CONTRACT_INFO.contract_type == 'CO') 
+
+contract_info = opt.run_query(q_contract_info)
+contract_info
+
+commodity_cls = get_price(SUBJECT_MATTER,trade_days.index[0],trade_days.index[0],fields=['close']).values[0][0]
+contract_info['price_spread'] = contract_info['exercise_price'] - commodity_cls
+
+if contract_info['price_spread'].max() > 0:
+    
+    contract_info = contract_info[contract_info['price_spread'] > 0]  #选出虚值期权
+    contract_info = contract_info.sort_values('exercise_price')
+    
+else:  #全是实值期权
+    
+    contract_info = contract_info.sort_values('exercise_price',ascending=False) 
+    
+contract_info
+
+holding_contract2[trade_days.index[0]] = contract_info['code'].iloc[0]
+newest_exercise_price = contract_info['exercise_price'].iloc[0]
+newest_expire_date = contract_info['last_trade_date'].iloc[0]
+
+print(newest_exercise_price)
+print(newest_expire_date)
+
+# 循环访问每一个交易日,判断交易情况
+# 规则:持有略虚值看涨期权,待行权价低于现价的 95% 时,平仓原期权合约
+# 重新开仓略虚值看涨期权;到期前1天移仓换月至次月合约
+
+for t in trade_days.index[1:]:
+    
+    # 获取昨日收盘价
+    pre_cls = get_price(SUBJECT_MATTER, t, t, fields=['pre_close']).values[0][0]
+    
+    if pre_cls * 0.95 >= newest_exercise_price:  # 原虚值变为实值,重新开仓略虚值期权
+        
+        # 寻找month_idx
+        for month_idx in range(len(month_split)):
+            if month_split[month_idx] >= t:
+                break
+        q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                                opt.OPT_CONTRACT_INFO.trading_code,
+                                opt.OPT_CONTRACT_INFO.name,
+                                opt.OPT_CONTRACT_INFO.exercise_price, 
+                                opt.OPT_CONTRACT_INFO.last_trade_date,
+                                # 行权价格,最后交易日
+                                opt.OPT_CONTRACT_INFO.list_date
+                                ).filter(opt.OPT_CONTRACT_INFO.underlying_symbol == SUBJECT_MATTER,
+                                         opt.OPT_CONTRACT_INFO.contract_type == 'CO',  # 看涨期权
+                                         )  # 在交易前上市
+        
+        contract_info = opt.run_query(q_contract_info)
+        commodity_cls = get_price(SUBJECT_MATTER, t, t, fields=['close']).values[0][0]
+        contract_info['price_spread'] = contract_info['exercise_price'] - commodity_cls
+        
+        if contract_info['price_spread'].max() > 0:
+            
+            contract_info = contract_info[contract_info['price_spread'] > 0]  # 选出虚值期权
+            contract_info = contract_info.sort_values('exercise_price')
+            
+        else:  # 全是实值期权
+            
+            contract_info = contract_info.sort_values('exercise_price', ascending=False)
+            
+        holding_contract2[t] = contract_info['code'].iloc[0]
+        newest_exercise_price = contract_info['exercise_price'].iloc[0]
+        newest_expire_date = contract_info['last_trade_date'].iloc[0]
+
+
+holding_contract2 = holding_contract2.fillna(method='ffill')
+holding_contract2
+
+data2 = pd.DataFrame(holding_contract2)
+data2.columns = ['holding_contract']
+data2 = data2.reindex(columns=['holding_contract','close','last_close'])
+
+last_contract = holding_contract2.iloc[0]   #记录上个持仓
+
+for t in data2.index:
+        #收盘价
+    q_price = query(opt.OPT_DAILY_PRICE.code, 
+                    opt.OPT_DAILY_PRICE.date, 
+                    opt.OPT_DAILY_PRICE.close,
+                 ).filter(opt.OPT_DAILY_PRICE.code==data2.loc[t,'holding_contract'],
+                         opt.OPT_DAILY_PRICE.date==t)
+    
+    price = opt.run_query(q_price)
+    
+    if price.empty:
+        continue
+    else:
+        price = price['close'][0]
+        data2.loc[t,'close'] = price
+     
+data2
+
+t = data2.index[2]
+
+data2.loc[t,'holding_contract']
+
+q_price = query(opt.OPT_DAILY_PRICE.code, 
+                opt.OPT_DAILY_PRICE.date, 
+                opt.OPT_DAILY_PRICE.close,
+                 ).filter(opt.OPT_DAILY_PRICE.code==data2.loc[t,'holding_contract'],
+                          opt.OPT_DAILY_PRICE.date==t)
+
+price = opt.run_query(q_price)
+price
+
+#计算卖出期权的收益
+opt_ret2 = pd.Series(0,index=data2.index)
+
+pre_close2 = data2['close'].iloc[0]
+
+for t in data2.index[1:]:
+    
+    if data2.isna().loc[t,'last_close']:   #未换仓,last为空
+        opt_ret2[t] = -price_gap*(data2.loc[t,'close'] - pre_close2)
+    else:
+        opt_ret2[t] = -price_gap*(data2.loc[t,'last_close'] - pre_close2) - 5  #手续费5元
+        
+    pre_close2 = data2.loc[t,'close']
+    
+opt_ret2
+
+#计算持仓收益
+commodity_price = get_price(SUBJECT_MATTER,trade_days.index[0],trade_days.index[-1],fields=['close'])['close']
+commodity_ret = commodity_price.diff(1).fillna(0)
+commodity_ret
+
+#计算净值
+init_asset2 = commodity_price.iloc[0]*price_gap
+ass2 = init_asset2 + (commodity_ret + opt_ret2).cumsum()
+pfl_ret2 = (ass2/ass2.shift(1) - 1).fillna(0)
+pfl_nv2 = (1 + pfl_ret2).cumprod()
+pfl_nv2
+
+#绘制净值图
+plt.figure(figsize=(30, 20))
+plt.plot(commodity_price/commodity_price.iloc[0], label='现货净值')
+plt.plot(pfl_nv2, label=SUBJECT_MATTER+'备兑看涨策略净值')
+plt.legend(loc='upper left', fontsize='large')
+plt.xlabel('时间',size=12)
+plt.ylabel('净值',size=12)
+plt.show()
+

+ 234 - 0
Lib/Options/Jiabailie/09_商品主力合约备兑认购策略.py

@@ -0,0 +1,234 @@
+# 商品主力合约-备兑看涨策略
+# 参考资料:
+# - 原始策略来源: https://www.joinquant.com/view/community/detail/e306e04ca7a0c557f759487e8d252c65
+# - 研究网址: https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E5%95%86%E5%93%81%E4%B8%BB%E5%8A%9B%E5%90%88%E7%BA%A6-%E5%A4%87%E5%85%91%E7%9C%8B%E6%B6%A8%E7%AD%96%E7%95%A5.ipynb
+
+# TODO: 添加商品主力合约备兑看涨策略相关代码
+
+import jqdata
+from   jqdata import *
+import pandas as pd
+import numpy as np
+import datetime
+import matplotlib.pyplot as plt
+from   datetime import datetime, timedelta
+
+plt.rcParams['font.sans-serif']=['SimHei']
+plt.rcParams['axes.unicode_minus'] = False
+
+def is_last_day_of_month(date_str):
+    
+    # 将字符串转换为日期对象
+    date_obj = datetime.strptime(date_str, '%Y-%m-%d')
+    
+    # 获取下一个日期对象
+    next_date_obj = date_obj + timedelta(days=1)
+    
+    # 判断是否为下个月的第一天,如果是,则当前日期为月末
+    return date_obj.month != next_date_obj.month
+
+
+def get_last_day_of_month(date_str):
+    
+    # 将字符串转换为日期对象
+    date_obj = datetime.strptime(date_str, '%Y-%m-%d')
+    
+    # 获取下个月的第一天日期对象
+    next_month_first_day = datetime(date_obj.year, date_obj.month + 1, 1)
+    
+    # 从下个月的第一天减去一天,得到当前月的月末日期对象
+    last_day_of_month = next_month_first_day - timedelta(days=1)
+    
+    # 返回月末日期的字符串形式
+    return last_day_of_month.strftime('%Y-%m-%d')
+
+
+#获取看涨期权
+def getContract(code,date):
+    
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                            opt.OPT_CONTRACT_INFO.trading_code,
+                            opt.OPT_CONTRACT_INFO.name,
+                            opt.OPT_CONTRACT_INFO.exercise_price,
+                            opt.OPT_CONTRACT_INFO.last_trade_date,
+                            
+                            # 行权价格,最后交易日
+                            opt.OPT_CONTRACT_INFO.list_date
+                            ).filter(opt.OPT_CONTRACT_INFO.underlying_symbol == code,
+                                     opt.OPT_CONTRACT_INFO.contract_type == 'CO',  # 看涨期权
+                                    )  
+    
+    contract_info = opt.run_query(q_contract_info)
+    
+    commodity_cls = get_price(code, date, date, fields=['close']).values[0][0]
+    
+    contract_info['price_spread'] = contract_info['exercise_price'] - commodity_cls
+    
+    
+    if contract_info['price_spread'].max() > 0:
+        
+        contract_info = contract_info[contract_info['price_spread'] > 0]  # 选出虚值期权
+        contract_info = contract_info.sort_values('exercise_price')
+        
+    else:  # 全是实值期权
+        
+        contract_info = contract_info.sort_values('exercise_price', ascending=False)
+        
+    return(contract_info['code'].iloc[0]) 
+
+# 合约代码
+symbol = 'AU'
+
+#合约价差
+price_gap = 10
+
+#起始时间
+starttime =  '2025-01-01'
+endtime = '2025-06-01'
+
+SUBJECT_MATTER = get_dominant_future(symbol,date = starttime)
+
+#查询相关的合约,适用于商品
+qy = query(opt.OPT_CONTRACT_INFO).filter(
+           opt.OPT_CONTRACT_INFO.underlying_symbol == SUBJECT_MATTER, ##期权标的物
+           opt.OPT_CONTRACT_INFO.contract_type == 'CO'
+          ).order_by(opt.OPT_CONTRACT_INFO.exercise_price)
+
+optList = opt.run_query(qy)
+optList[:2] 
+
+#获取交易时间和时间间隔(频率:月)
+#根据不同交易日分割月份
+#指定回测的起始时间
+
+trade_days = pd.Series(index=jqdata.get_trade_days(starttime,endtime))
+trade_days.index = pd.to_datetime(trade_days.index)
+
+##持仓情况
+main_list = pd.Series(index=trade_days.index)
+main_list[trade_days.index[0]] = SUBJECT_MATTER
+holding_contract2 = pd.Series(index=trade_days.index)
+
+
+#获取首个持仓合约
+contract = getContract(SUBJECT_MATTER,trade_days.index[0])
+holding_contract2[trade_days.index[0]] = contract
+holding_contract2[:3]
+
+#循环访问每一个交易日,判断交易情况
+#规则:判断当前主力合约对应的期权,持有略虚值看涨期权,待行权价低于现价的95%时,平仓原期权合约,重新开仓略虚值看涨期权
+error_date =[]
+pre_hold = holding_contract2[0]
+
+for i in range(1,len(trade_days)):
+    
+    pre_day = trade_days.index[i-1]
+    cur_day = trade_days.index[i]
+    cur_main = get_dominant_future(symbol,date = cur_day) #当前主力合约
+    pre_main = get_dominant_future(symbol,date = pre_day) #上一个交易日的主力合约
+    main_list[cur_day] = cur_main
+    
+    if cur_main != pre_main: #主力合约切换
+        contract = getContract(cur_main,cur_day)
+    else:
+        pre_cls = get_price(cur_main, cur_day, cur_day, fields=['pre_close']).values[0][0]
+        q_contract_info = query(opt.OPT_CONTRACT_INFO.code, opt.OPT_CONTRACT_INFO.trading_code,
+                                opt.OPT_CONTRACT_INFO.name,
+                                opt.OPT_CONTRACT_INFO.exercise_price, opt.OPT_CONTRACT_INFO.last_trade_date,
+                                # 行权价格,最后交易日
+                                opt.OPT_CONTRACT_INFO.list_date
+                                ).filter(opt.OPT_CONTRACT_INFO.code == pre_hold,
+                                         opt.OPT_CONTRACT_INFO.contract_type == 'CO',  # 看涨期权
+                                         opt.OPT_CONTRACT_INFO.last_trade_date >= cur_day
+                                         ) 
+        
+        contract_info = opt.run_query(q_contract_info)
+        pre_exercise_price = contract_info
+        
+        
+        if pre_exercise_price.empty:
+            error_date.append(cur_day)
+            continue
+        else:
+            pre_exercise_price = pre_exercise_price['exercise_price'][0]
+            if pre_cls * 0.95 >= pre_exercise_price:
+                contract = getContract(pre_main,cur_day)
+            else:
+                contract = pre_hold
+                
+    holding_contract2[cur_day] = contract
+    
+    pre_hold = contract
+    
+holding_contract2 = holding_contract2.fillna(method='ffill')
+
+data2 = pd.DataFrame(holding_contract2)
+data2.columns = ['holding_contract']
+data2 = data2.reindex(columns=['holding_contract','close','last_close'])
+data2 = data2.drop(error_date)
+main_list = main_list.drop(error_date)
+
+last_contract = holding_contract2.iloc[0]   #记录上个持仓
+
+for i in range(0,len(data2.index)):
+    
+    t = data2.index[i]
+    
+    if last_contract == data2.loc[t,'holding_contract']:  #期权未换仓
+        q_price = query(opt.OPT_DAILY_PRICE.code, opt.OPT_DAILY_PRICE.date, opt.OPT_DAILY_PRICE.close,
+                 ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
+                         opt.OPT_DAILY_PRICE.date==t)
+        price = opt.run_query(q_price)['close'][0]
+        data2.loc[t,'close'] = price
+    else: #合约换仓
+        q_price = query(opt.OPT_DAILY_PRICE.code, opt.OPT_DAILY_PRICE.date, opt.OPT_DAILY_PRICE.close,
+                 ).filter(opt.OPT_DAILY_PRICE.code==data2.loc[t,'holding_contract'],
+                         opt.OPT_DAILY_PRICE.date==t)
+        price = opt.run_query(q_price)['close'][0]
+        data2.loc[t,'close'] = price
+        #收盘价,旧
+        q_price = query(opt.OPT_DAILY_PRICE.code, opt.OPT_DAILY_PRICE.date, opt.OPT_DAILY_PRICE.close,
+                 ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
+                         opt.OPT_DAILY_PRICE.date==data2.index[i-1])
+        price = opt.run_query(q_price)['close'][0]
+        data2.loc[t,'last_close'] = price
+        last_contract = data2.loc[t,'holding_contract']
+        
+print(data2)
+
+#计算卖出期权的收益
+
+opt_ret2 = pd.Series(0,index=data2.index)
+pre_close2 = data2['close'].iloc[0]
+
+for t in data2.index[1:]:
+    if data2.isna().loc[t,'last_close']:   #未换仓,last为空
+        opt_ret2[t] = -price_gap*(data2.loc[t,'close'] - pre_close2)
+    else:
+        opt_ret2[t] = -price_gap*(data2.loc[t,'last_close'] - pre_close2) - 5  #手续费5元
+    pre_close2 = data2.loc[t,'close']
+    
+opt_ret2
+
+#计算持仓收益
+commodity_price = [get_price(main_list[t],data2.index[t],data2.index[t],fields=['close'])['close'][0] for t in range(0,len(data2.index))]
+commodity_price = pd.Series(commodity_price,index=data2.index)
+commodity_ret = commodity_price.diff(1).fillna(0)
+commodity_ret
+
+#计算净值
+init_asset2 = commodity_price.iloc[0]*price_gap
+ass2 = init_asset2 + (commodity_ret + opt_ret2).cumsum()
+
+pfl_ret2 = (ass2/ass2.shift(1) - 1).fillna(0)
+pfl_nv2 = (1 + pfl_ret2).cumprod()
+pfl_nv2
+
+#绘制净值图
+plt.figure(figsize=(30,20))
+plt.plot(commodity_price/commodity_price.iloc[0], label='现货净值')
+plt.plot(pfl_nv2, label=symbol+'备兑看涨策略净值')
+plt.legend(loc='upper left', fontsize='large')
+plt.xlabel('时间',size=12)
+plt.ylabel('净值',size=12)
+plt.show()

+ 238 - 0
Lib/Options/Jiabailie/10_商品主力合约备兑认沽策略.py

@@ -0,0 +1,238 @@
+# 商品主力合约-备兑看跌策略
+# 参考资料:
+# - 原始策略来源: https://www.joinquant.com/view/community/detail/d4272cd0fac2981438b0f3f410bf6180
+# - 研究网址: https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E5%95%86%E5%93%81%E4%B8%BB%E5%8A%9B%E5%90%88%E7%BA%A6-%E5%A4%87%E5%85%91%E7%9C%8B%E8%B7%8C%E7%AD%96%E7%95%A5.ipynb
+
+# TODO: 添加商品主力合约备兑看跌策略相关代码
+
+import jqdata
+from   jqdata import *
+import pandas as pd
+import numpy as np
+import datetime
+import matplotlib.pyplot as plt
+from   datetime import datetime, timedelta
+
+plt.rcParams['font.sans-serif']=['SimHei']
+plt.rcParams['axes.unicode_minus'] = False
+
+def is_last_day_of_month(date_str):
+    
+    # 将字符串转换为日期对象
+    date_obj = datetime.strptime(date_str, '%Y-%m-%d')
+    
+    # 获取下一个日期对象
+    next_date_obj = date_obj + timedelta(days=1)
+    
+    # 判断是否为下个月的第一天,如果是,则当前日期为月末
+    return date_obj.month != next_date_obj.month
+
+
+def get_last_day_of_month(date_str):
+    
+    # 将字符串转换为日期对象
+    date_obj = datetime.strptime(date_str, '%Y-%m-%d')
+    
+    # 获取下个月的第一天日期对象
+    next_month_first_day = datetime(date_obj.year, date_obj.month + 1, 1)
+    
+    # 从下个月的第一天减去一天,得到当前月的月末日期对象
+    last_day_of_month = next_month_first_day - timedelta(days=1)
+    
+    # 返回月末日期的字符串形式
+    return last_day_of_month.strftime('%Y-%m-%d')
+
+
+#获取看涨期权
+def getContract(code,date):
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                            opt.OPT_CONTRACT_INFO.trading_code,
+                            opt.OPT_CONTRACT_INFO.name,
+                            opt.OPT_CONTRACT_INFO.exercise_price, 
+                            opt.OPT_CONTRACT_INFO.last_trade_date,
+                            # 行权价格,最后交易日
+                            opt.OPT_CONTRACT_INFO.list_date
+                            ).filter(opt.OPT_CONTRACT_INFO.underlying_symbol == code,
+                                     opt.OPT_CONTRACT_INFO.contract_type == 'PO',  # 认沽期权
+                                     )  
+    
+    contract_info = opt.run_query(q_contract_info)
+    commodity_cls = get_price(code, date, date, fields=['close']).values[0][0]
+    contract_info['price_spread'] = commodity_cls - contract_info['exercise_price']
+    
+    
+    if contract_info['price_spread'].max() > 0:
+        
+        contract_info = contract_info[contract_info['price_spread'] > 0]  # 选出虚值期权
+        contract_info = contract_info.sort_values('exercise_price')
+        
+    else:  # 全是实值期权
+        
+        contract_info = contract_info.sort_values('exercise_price', ascending=False)
+        
+    return(contract_info['code'].iloc[0])
+
+# 合约代码
+symbol = 'AL'
+
+#合约价差
+price_gap = 10
+
+#起始时间
+starttime =  '2023-01-01'
+endtime   =  '2024-01-01'
+
+SUBJECT_MATTER = get_dominant_future(symbol,date = starttime)
+
+#查询相关的合约,适用于商品
+qy = query(opt.OPT_CONTRACT_INFO).filter(
+           opt.OPT_CONTRACT_INFO.underlying_symbol == SUBJECT_MATTER, ##期权标的物
+           opt.OPT_CONTRACT_INFO.contract_type == 'PO'
+          ).order_by(opt.OPT_CONTRACT_INFO.exercise_price)
+
+optList = opt.run_query(qy)
+optList[:2]
+
+#获取交易时间和时间间隔(频率:月)
+#根据不同交易日分割月份
+#指定回测的起始时间
+
+trade_days = pd.Series(index=jqdata.get_trade_days(starttime,endtime))
+trade_days.index = pd.to_datetime(trade_days.index)
+
+##持仓情况
+main_list = pd.Series(index=trade_days.index)
+main_list[trade_days.index[0]] = SUBJECT_MATTER
+holding_contract2 = pd.Series(index=trade_days.index)
+
+#获取首个持仓合约
+contract = getContract(SUBJECT_MATTER,trade_days.index[0])
+holding_contract2[trade_days.index[0]] = contract
+holding_contract2[:3]
+
+#循环访问每一个交易日,判断交易情况
+#规则:判断当前主力合约对应的期权,持有略虚值看涨期权,待行权价低于现价的95%时,平仓原期权合约,重新开仓略虚值看涨期权
+error_date =[]
+pre_hold = holding_contract2[0]
+
+for i in range(1,len(trade_days)):
+    
+    pre_day = trade_days.index[i-1]
+    cur_day = trade_days.index[i]
+    cur_main = get_dominant_future(symbol,date = cur_day) #当前主力合约
+    pre_main = get_dominant_future(symbol,date = pre_day) #上一个交易日的主力合约
+    main_list[cur_day] = cur_main
+    
+    if cur_main != pre_main: #主力合约切换
+        
+        contract = getContract(cur_main,cur_day)
+        
+    else:
+        
+        pre_cls = get_price(cur_main, cur_day, cur_day, fields=['pre_close']).values[0][0]
+        
+        q_contract_info = query(opt.OPT_CONTRACT_INFO.code, opt.OPT_CONTRACT_INFO.trading_code,
+                                opt.OPT_CONTRACT_INFO.name,
+                                opt.OPT_CONTRACT_INFO.exercise_price, opt.OPT_CONTRACT_INFO.last_trade_date,
+                                # 行权价格,最后交易日
+                                opt.OPT_CONTRACT_INFO.list_date
+                                ).filter(opt.OPT_CONTRACT_INFO.code == pre_hold,
+                                         opt.OPT_CONTRACT_INFO.contract_type == 'PO',  # 认沽期权
+                                         opt.OPT_CONTRACT_INFO.last_trade_date >= cur_day
+                                         ) 
+        
+        contract_info = opt.run_query(q_contract_info)
+        pre_exercise_price = contract_info
+        
+        if pre_exercise_price.empty:
+            
+            error_date.append(cur_day)
+            continue
+            
+        else:
+            
+            pre_exercise_price = pre_exercise_price['exercise_price'][0]
+            if pre_cls * 0.95 <= pre_exercise_price:
+                contract = getContract(pre_main,cur_day)
+            else:
+                contract = pre_hold
+                
+    holding_contract2[cur_day] = contract
+    pre_hold = contract
+    
+holding_contract2 = holding_contract2.fillna(method='ffill')
+
+data2 = pd.DataFrame(holding_contract2)
+data2.columns = ['holding_contract']
+data2 = data2.reindex(columns=['holding_contract','close','last_close'])
+data2 = data2.drop(error_date)
+main_list = main_list.drop(error_date)
+
+
+last_contract = holding_contract2.iloc[0]   #记录上个持仓
+
+for i in range(0,len(data2.index)):
+    
+    t = data2.index[i]
+    
+    if last_contract == data2.loc[t,'holding_contract']:  #期权未换仓
+        q_price = query(opt.OPT_DAILY_PRICE.code, opt.OPT_DAILY_PRICE.date, opt.OPT_DAILY_PRICE.close,
+                 ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
+                         opt.OPT_DAILY_PRICE.date==t)
+        price = opt.run_query(q_price)['close'][0]
+        data2.loc[t,'close'] = price
+        
+    else: #合约换仓
+        
+        q_price = query(opt.OPT_DAILY_PRICE.code, opt.OPT_DAILY_PRICE.date, opt.OPT_DAILY_PRICE.close,
+                 ).filter(opt.OPT_DAILY_PRICE.code==data2.loc[t,'holding_contract'],
+                         opt.OPT_DAILY_PRICE.date==t)
+        price = opt.run_query(q_price)['close'][0]
+        data2.loc[t,'close'] = price
+        
+        #收盘价,旧
+        q_price = query(opt.OPT_DAILY_PRICE.code, opt.OPT_DAILY_PRICE.date, opt.OPT_DAILY_PRICE.close,
+                 ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
+                         opt.OPT_DAILY_PRICE.date==data2.index[i-1])
+        price = opt.run_query(q_price)['close'][0]
+        data2.loc[t,'last_close'] = price
+        last_contract = data2.loc[t,'holding_contract']
+        
+print(data2)
+
+#计算卖出期权的收益
+opt_ret2 = pd.Series(0,index=data2.index)
+pre_close2 = data2['close'].iloc[0]
+
+for t in data2.index[1:]:
+    
+    if data2.isna().loc[t,'last_close']:   #未换仓,last为空
+        opt_ret2[t] = price_gap*(data2.loc[t,'close'] - pre_close2)
+    else:
+        opt_ret2[t] = price_gap*(data2.loc[t,'last_close'] - pre_close2) - 5  #手续费5元
+        
+    pre_close2 = data2.loc[t,'close']
+    
+opt_ret2
+
+#计算持仓收益
+commodity_price = [get_price(main_list[t],data2.index[t],data2.index[t],fields=['close'])['close'][0] for t in range(0,len(data2.index))]
+commodity_price = pd.Series(commodity_price,index=data2.index)
+commodity_ret = commodity_price.diff(1).fillna(0)
+commodity_ret
+
+#计算净值
+init_asset2 = commodity_price.iloc[0]*price_gap
+ass2 = init_asset2 + (commodity_ret + opt_ret2).cumsum()
+pfl_ret2 = (ass2/ass2.shift(1) - 1).fillna(0)
+pfl_nv2 = (1 + pfl_ret2).cumprod()
+pfl_nv2
+
+#绘制净值图
+plt.figure(figsize=(30, 20))
+plt.plot(commodity_price/commodity_price.iloc[0], label='现货净值')
+plt.plot(pfl_nv2, label=symbol+'备兑看跌策略净值')
+plt.legend(loc='upper left', fontsize='large')
+plt.xlabel('时间',size=12)
+plt.ylabel('净值',size=12)
+plt.show()

+ 309 - 0
Lib/Options/Jiabailie/11_领口认购策略商品主力合约.py

@@ -0,0 +1,309 @@
+# 领口看涨策略-商品主力合约
+# 参考资料:
+# - 原始策略来源: https://www.joinquant.com/view/community/detail/bec4688fd7998652edfd929a4ef1f4df
+# - 研究网址: https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E9%A2%86%E5%8F%A3%E7%9C%8B%E6%B6%A8%E7%AD%96%E7%95%A5-%E5%95%86%E5%93%81%E4%B8%BB%E5%8A%9B%E5%90%88%E7%BA%A6.ipynb
+
+# TODO: 添加领口看涨策略商品主力合约相关代码
+
+import jqdata
+from   jqdata import *
+import pandas as pd
+import numpy as np
+import datetime
+import matplotlib.pyplot as plt
+from datetime import datetime, timedelta
+
+plt.rcParams['font.sans-serif']=['SimHei']
+plt.rcParams['axes.unicode_minus'] = False
+
+def is_last_day_of_month(date_str):
+    
+    # 将字符串转换为日期对象
+    date_obj = datetime.strptime(date_str, '%Y-%m-%d')
+    
+    # 获取下一个日期对象
+    next_date_obj = date_obj + timedelta(days=1)
+    
+    # 判断是否为下个月的第一天,如果是,则当前日期为月末
+    return date_obj.month != next_date_obj.month
+
+
+def get_last_day_of_month(date_str):
+    
+    # 将字符串转换为日期对象
+    date_obj = datetime.strptime(date_str, '%Y-%m-%d')
+    
+    # 获取下个月的第一天日期对象
+    next_month_first_day = datetime(date_obj.year, date_obj.month + 1, 1)
+    
+    # 从下个月的第一天减去一天,得到当前月的月末日期对象
+    last_day_of_month = next_month_first_day - timedelta(days=1)
+    
+    # 返回月末日期的字符串形式
+    return last_day_of_month.strftime('%Y-%m-%d')
+
+
+#获取期权
+def getContract(code,date,type="CO"): #CO为认购,PO为认沽
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                            opt.OPT_CONTRACT_INFO.trading_code,
+                            opt.OPT_CONTRACT_INFO.name,
+                            opt.OPT_CONTRACT_INFO.exercise_price, 
+                            opt.OPT_CONTRACT_INFO.last_trade_date,
+                            # 行权价格,最后交易日
+                            opt.OPT_CONTRACT_INFO.list_date
+                            ).filter(opt.OPT_CONTRACT_INFO.underlying_symbol == code,
+                                     opt.OPT_CONTRACT_INFO.contract_type == type,  # 看涨期权
+                                     )
+    
+    contract_info = opt.run_query(q_contract_info)
+    commodity_cls = get_price(code, date, date, fields=['close']).values[0][0]
+    
+    if type == 'CO':
+        
+        contract_info['price_spread'] = contract_info['exercise_price'] - commodity_cls
+        
+    else:
+        
+        contract_info['price_spread'] = commodity_cls - contract_info['exercise_price']
+        
+        
+    if contract_info['price_spread'].max() > 0:
+        
+        contract_info = contract_info[contract_info['price_spread'] > 0]  # 选出虚值期权
+        contract_info = contract_info.sort_values('exercise_price')
+        
+    else:  # 全是实值期权
+        
+        contract_info = contract_info.sort_values('exercise_price', ascending=False)
+        
+    return(contract_info['code'].iloc[0])
+
+
+#获取期权价格等信息
+def getContractPrice(code,last_trade_date,type='CO'):
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                            opt.OPT_CONTRACT_INFO.trading_code,
+                            opt.OPT_CONTRACT_INFO.name,
+                            opt.OPT_CONTRACT_INFO.exercise_price, 
+                            opt.OPT_CONTRACT_INFO.last_trade_date,
+                            # 行权价格,最后交易日
+                            opt.OPT_CONTRACT_INFO.list_date
+                            ).filter(opt.OPT_CONTRACT_INFO.code == code,
+                                     opt.OPT_CONTRACT_INFO.contract_type == type,  # 期权类型
+                                     opt.OPT_CONTRACT_INFO.last_trade_date >= last_trade_date
+                                    ) 
+    
+    contract_info = opt.run_query(q_contract_info)
+    
+    return(contract_info)
+
+
+#处理合约切换标记
+def contractChange(holding_contract,main_list,error_date):
+    
+    data2 = pd.DataFrame(holding_contract)
+    data2.columns = ['holding_contract']
+    data2 = data2.reindex(columns=['holding_contract','close','last_close'])
+    data2 = data2.drop(error_date)
+    main_list = main_list.drop(error_date)
+    last_contract = holding_contract.iloc[0]   #记录上个持仓
+    
+    for i in range(0,len(data2.index)):
+        
+        t = data2.index[i]
+        
+        if last_contract == data2.loc[t,'holding_contract']:  #期权未换仓
+            
+            q_price = query(opt.OPT_DAILY_PRICE.code, 
+                            opt.OPT_DAILY_PRICE.date, 
+                            opt.OPT_DAILY_PRICE.close,
+                     ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
+                              opt.OPT_DAILY_PRICE.date==t)
+            
+            price = opt.run_query(q_price)['close'][0]
+            data2.loc[t,'close'] = price
+            
+        else: #合约换仓
+            
+            q_price = query(opt.OPT_DAILY_PRICE.code, 
+                            opt.OPT_DAILY_PRICE.date, 
+                            opt.OPT_DAILY_PRICE.close,
+                     ).filter(opt.OPT_DAILY_PRICE.code==data2.loc[t,'holding_contract'],
+                             opt.OPT_DAILY_PRICE.date==t)
+            
+            price = opt.run_query(q_price)['close'][0]
+            
+            data2.loc[t,'close'] = price
+            
+            
+            #收盘价,旧
+            q_price = query(opt.OPT_DAILY_PRICE.code, 
+                            opt.OPT_DAILY_PRICE.date, 
+                            opt.OPT_DAILY_PRICE.close,
+                     ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
+                              opt.OPT_DAILY_PRICE.date==data2.index[i-1])
+            
+            price = opt.run_query(q_price)['close'][0]
+            
+            data2.loc[t,'last_close'] = price
+            
+            last_contract = data2.loc[t,'holding_contract']            
+            
+    return(data2)
+        
+    
+    
+def optionProfit(data2,price_gap,fee):
+    
+    #计算平仓期权的收益
+    opt_ret2 = pd.Series(0,index=data2.index)
+    pre_close2 = data2['close'].iloc[0]
+    
+    for t in data2.index[1:]:
+        
+        if data2.isna().loc[t,'last_close']:   #未换仓,last为空
+            
+            opt_ret2[t] = -price_gap*(data2.loc[t,'close'] - pre_close2)
+            
+        else:
+            
+            opt_ret2[t] = -price_gap*(data2.loc[t,'last_close'] - pre_close2) - fee  #手续费
+            
+        pre_close2 = data2.loc[t,'close']
+        
+    return(opt_ret2)
+
+# 设置各项参数
+# 合约代码
+symbol = 'AU'
+
+#手续费
+fee = 5
+
+#合约价差
+price_gap = 10
+
+#起始时间
+starttime = '2023-03-01'
+endtime   = '2024-01-01'
+
+SUBJECT_MATTER = get_dominant_future(symbol,date = starttime)
+
+#获取交易时间和时间间隔(频率:月)
+#根据不同交易日分割月份
+#指定回测的起始时间
+trade_days = pd.Series(index=jqdata.get_trade_days(starttime,endtime))
+trade_days.index = pd.to_datetime(trade_days.index)
+
+##持仓情况
+#主力合约列表
+main_list = pd.Series(index=trade_days.index)
+main_list[trade_days.index[0]] = SUBJECT_MATTER
+holding_contract_CO = pd.Series(index=trade_days.index)
+
+#获取首个认购持仓合约
+contract = getContract(SUBJECT_MATTER,trade_days.index[0],type='CO')
+holding_contract_CO[trade_days.index[0]] = contract
+print(holding_contract_CO[:3])
+
+#获取首个认沽持仓合约
+holding_contract_PO = pd.Series(index=trade_days.index)
+contract = getContract(SUBJECT_MATTER,trade_days.index[0],type='PO')
+holding_contract_PO[trade_days.index[0]] = contract
+print(holding_contract_PO[:3])
+
+#循环访问每一个交易日,判断交易情况
+#规则:判断当前主力合约对应的期权,持有略虚值看涨期权,待行权价低于现价的95%时,平仓原期权合约,重新开仓略虚值看涨期权
+error_date =[]
+pre_hold_CO = holding_contract_CO[0]
+pre_hold_PO = holding_contract_PO[0]
+
+for i in range(1,len(trade_days)):
+    
+    pre_day = trade_days.index[i-1]
+    cur_day = trade_days.index[i]
+    cur_main = get_dominant_future(symbol,date = cur_day) #当前主力合约
+    pre_main = get_dominant_future(symbol,date = pre_day) #上一个交易日的主力合约
+    main_list[cur_day] = cur_main
+    
+    
+    if cur_main != pre_main: #主力合约切换
+        
+        contract_CO = getContract(cur_main,cur_day,type='CO')
+        contract_PO = getContract(cur_main,cur_day,type='PO')
+        
+    else:
+        
+        pre_cls = get_price(cur_main, cur_day, cur_day, fields=['pre_close']).values[0][0]
+        contract_info_CO = getContractPrice(pre_hold_CO,cur_day,"CO")
+        pre_exercise_price_CO = contract_info_CO
+        contract_info_PO = getContractPrice(pre_hold_PO,cur_day,"PO")
+        pre_exercise_price_PO = contract_info_PO
+        
+        if contract_info_CO.empty | contract_info_PO.empty:
+            
+            error_date.append(cur_day)
+            continue
+            
+        else:
+            
+            pre_exercise_price_CO = pre_exercise_price_CO['exercise_price'][0]
+            pre_exercise_price_PO = pre_exercise_price_PO['exercise_price'][0]
+            
+            if pre_cls * 0.95 >= pre_exercise_price_CO:
+                
+                contract_CO = getContract(pre_main,cur_day)
+                
+            else:
+                
+                contract_CO = pre_hold_CO
+            
+            if pre_cls   <= 0.95 * pre_exercise_price_PO:
+                
+                contract_PO = getContract(pre_main,cur_day,type='PO')
+            else:
+                
+                contract_PO = pre_hold_PO
+    
+    
+    holding_contract_CO[cur_day] = contract_CO
+    pre_hold_CO = contract_CO
+    holding_contract_PO[cur_day] = contract_PO
+    pre_hold_PO = contract_PO
+    
+holding_contract_CO = holding_contract_CO.fillna(method='ffill')
+holding_contract_PO = holding_contract_PO.fillna(method='ffill')
+
+data_list_CO = contractChange(holding_contract_CO,main_list,error_date)
+data_list_PO = contractChange(holding_contract_PO,main_list,error_date)
+
+#计算平仓期权的收益
+opt_ret_CO = optionProfit(data_list_CO,price_gap,fee)
+opt_ret_PO = optionProfit(data_list_PO,price_gap,fee)
+opt_ret = opt_ret_CO + opt_ret_PO
+print(opt_ret)
+
+#计算持仓收益
+commodity_price = [get_price(main_list[t],data_list_CO.index[t],data_list_CO.index[t],fields=['close'])['close'][0] for t in range(0,len(data_list_CO.index))]
+commodity_price = pd.Series(commodity_price,index=data_list_CO.index)
+
+commodity_ret = commodity_price.diff(1).fillna(0)
+commodity_ret
+
+#计算净值
+init_asset2 = commodity_price.iloc[0]*price_gap
+ass2 = init_asset2 + (commodity_ret + opt_ret).cumsum()
+
+pfl_ret2 = (ass2/ass2.shift(1) - 1).fillna(0)
+pfl_nv2 = (1 + pfl_ret2).cumprod()
+pfl_nv2
+
+#绘制净值图
+plt.figure(figsize=(30,20))
+plt.plot(commodity_price/commodity_price.iloc[0], label='现货净值')
+plt.plot(pfl_nv2, label=symbol+'领式看涨策略净值')
+plt.legend(loc='upper left', fontsize='large')
+plt.xlabel('时间',size=12)
+plt.ylabel('净值',size=12)
+plt.show()

+ 273 - 0
Lib/Options/Jiabailie/12_领口认购策略50ETF.py

@@ -0,0 +1,273 @@
+# 领口看涨策略-50ETF
+# 参考资料:
+# - 原始策略来源: https://www.joinquant.com/view/community/detail/a99630091b24414da149e715ae6186f2
+# - 研究网址: https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E9%A2%86%E5%8F%A3%E7%9C%8B%E6%B6%A8%E7%AD%96%E7%95%A5-50ETF.ipynb
+
+# TODO: 添加领口看涨策略50ETF相关代码
+
+import jqdata
+from   jqdata import *
+import pandas as pd
+import numpy as np
+import datetime
+import matplotlib.pyplot as plt
+
+plt.rcParams['font.sans-serif']=['SimHei']
+plt.rcParams['axes.unicode_minus'] = False
+
+from datetime import datetime, timedelta
+
+def is_last_day_of_month(date_str):
+    
+    # 将字符串转换为日期对象
+    date_obj = datetime.strptime(date_str, '%Y-%m-%d')
+    
+    # 获取下一个日期对象
+    next_date_obj = date_obj + timedelta(days=1)
+    
+    # 判断是否为下个月的第一天,如果是,则当前日期为月末
+    return date_obj.month != next_date_obj.month
+
+
+def get_last_day_of_month(date_str):
+    
+    # 将字符串转换为日期对象
+    date_obj = datetime.strptime(date_str, '%Y-%m-%d')
+    
+    # 获取下个月的第一天日期对象
+    next_month_first_day = datetime(date_obj.year, date_obj.month + 1, 1)
+    
+    # 从下个月的第一天减去一天,得到当前月的月末日期对象
+    last_day_of_month = next_month_first_day - timedelta(days=1)
+    
+    # 返回月末日期的字符串形式
+    return last_day_of_month.strftime('%Y-%m-%d')
+
+
+#获得虚值合约
+def getContract(start,end,listdate,contratType='CO'):
+    
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                            opt.OPT_CONTRACT_INFO.trading_code, 
+                            opt.OPT_CONTRACT_INFO.name, #合约代码,合约交易代码,合约简称
+                            opt.OPT_CONTRACT_INFO.exercise_price, 
+                            opt.OPT_CONTRACT_INFO.last_trade_date, 
+                            opt.OPT_CONTRACT_INFO.list_date
+         ).filter(opt.OPT_CONTRACT_INFO.contract_type == contratType,  #期权类型
+                  opt.OPT_CONTRACT_INFO.exchange_code == 'XSHG',   #上交所
+                  opt.OPT_CONTRACT_INFO.last_trade_date > start,  #时间-到期月开始
+                  opt.OPT_CONTRACT_INFO.last_trade_date <= end,  #时间-到期月结束
+                  opt.OPT_CONTRACT_INFO.list_date < listdate)    #在交易前上市
+    
+    contract_info = opt.run_query(q_contract_info)
+    
+    etf_cls = get_price('510050.XSHG',listdate,listdate,fields=['close']).values[0][0]
+    
+    if contratType == 'CO':
+        
+        contract_info['price_spread'] = contract_info['exercise_price'] - etf_cls
+        
+    else:
+        
+        contract_info['price_spread'] = etf_cls - contract_info['exercise_price']
+        
+    if contract_info['price_spread'].max() > 0:
+        
+        contract_info = contract_info[contract_info['price_spread'] > 0]  #选出虚值期权
+        contract_info = contract_info.sort_values('exercise_price')
+        
+    else:  #全是实值期权
+        
+        contract_info = contract_info.sort_values('exercise_price',ascending=False)    
+        
+    return(contract_info)
+
+
+#处理合约切换标记
+def contractChange(holding_contract):
+    
+    data2 = pd.DataFrame(holding_contract)
+    data2.columns = ['holding_contract']
+    data2 = data2.reindex(columns=['holding_contract','close','last_close'])
+
+    last_contract = holding_contract.iloc[0]   #记录上个持仓
+    
+    for t in data2.index:
+        
+        if last_contract == data2.loc[t,'holding_contract']:  #期权未换仓
+            
+            #收盘价
+            q_price = query(opt.OPT_DAILY_PRICE.code, 
+                            opt.OPT_DAILY_PRICE.date, 
+                            opt.OPT_DAILY_PRICE.close,
+                     ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
+                              opt.OPT_DAILY_PRICE.date==t)
+            
+            price = opt.run_query(q_price)['close'][0]
+            data2.loc[t,'close'] = price
+
+        else:
+            
+            #收盘价,新
+            q_price = query(opt.OPT_DAILY_PRICE.code, 
+                            opt.OPT_DAILY_PRICE.date, 
+                            opt.OPT_DAILY_PRICE.close,
+                     ).filter(opt.OPT_DAILY_PRICE.code==data2.loc[t,'holding_contract'],
+                              opt.OPT_DAILY_PRICE.date==t)
+            
+            price = opt.run_query(q_price)['close'][0]
+            data2.loc[t,'close'] = price
+            
+            #收盘价,旧
+            q_price = query(opt.OPT_DAILY_PRICE.code, 
+                            opt.OPT_DAILY_PRICE.date, 
+                            opt.OPT_DAILY_PRICE.close,
+                     ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
+                              opt.OPT_DAILY_PRICE.date==t)
+            
+            price = opt.run_query(q_price)['close'][0]
+            data2.loc[t,'last_close'] = price
+            last_contract = data2.loc[t,'holding_contract']
+            
+    return(data2)
+
+
+#平仓期权收益
+def optionProfit(data2,fee=5):
+    
+    #计算平仓期权的收益
+    opt_ret2 = pd.Series(0,index=data2.index)
+    pre_close2 = data2['close'].iloc[0]
+    
+    for t in data2.index[1:]:
+        
+        if data2.isna().loc[t,'last_close']:   #未换仓,last为空
+            
+            opt_ret2[t] = -10000*(data2.loc[t,'close'] - pre_close2)
+            
+        else:
+            
+            opt_ret2[t] = -10000*(data2.loc[t,'last_close'] - pre_close2) - fee  #手续费
+            
+        pre_close2 = data2.loc[t,'close']
+        
+    return(opt_ret2)
+
+#获取交易时间和时间间隔(频率:月)
+#根据不同交易日分割月份
+#指定回测的起始时间
+starttime = '2020-01-01'
+endtime   = '2024-04-01'
+
+trade_days = pd.Series(index=jqdata.get_trade_days(starttime,endtime))
+trade_days.index = pd.to_datetime(trade_days.index)
+
+last_day = get_last_day_of_month(endtime)
+
+month_split = list(trade_days.resample('M',label='left').mean().index) + [pd.to_datetime(last_day)]
+month_split
+
+##持仓情况
+holding_contract_CO = pd.Series(index=trade_days.index)
+holding_contract_PO = pd.Series(index=trade_days.index)
+
+#获取首个持仓认购合约
+contract_info_CO = getContract(month_split[0],month_split[1],trade_days.index[0],'CO')
+holding_contract_CO[trade_days.index[0]] = contract_info_CO['code'].iloc[0]
+
+#获取首个持仓认沽合约
+contract_info_PO = getContract(month_split[0],month_split[1],trade_days.index[0],'PO')
+holding_contract_PO[trade_days.index[0]] = contract_info_PO['code'].iloc[0]
+
+newest_exercise_price = contract_info_CO['exercise_price'].iloc[0]
+newest_expire_date = contract_info_CO['last_trade_date'].iloc[0]
+
+#循环访问每一个交易日,判断交易情况
+#规则:持有略虚值看涨期权,待行权价低于现价的95%时,平仓原期权合约,重新开仓略虚值看涨期权;到期前1天移仓换月至次月合约
+erro_date = []
+
+for t in trade_days.index[1:]:
+    
+    #到期前一天
+    if t >= pd.to_datetime(get_trade_days(end_date=pd.to_datetime(newest_expire_date),count=2)[0]):
+        
+        #寻找month_idx
+        for month_idx in range(len(month_split)):
+            if month_split[month_idx] >= t:
+                break
+        if month_idx == len(month_split) -1:
+            lasttradeday = t
+            break
+        else:
+            lasttradeday = ''
+        
+        contract_info_CO = getContract(month_split[month_idx],month_split[month_idx+1],t,'CO')
+        contract_info_PO = getContract(month_split[month_idx],month_split[month_idx+1],t,'PO')
+
+        if contract_info_CO['last_trade_date'].iloc[0] >= newest_expire_date:
+            
+            holding_contract_CO[t] = contract_info_CO['code'].iloc[0]
+            holding_contract_PO[t] = contract_info_CO['code'].iloc[0]
+            
+            newest_exercise_price = contract_info_CO['exercise_price'].iloc[0]
+            newest_expire_date = contract_info_CO['last_trade_date'].iloc[0]
+    else:
+        
+        #获取昨日50etf收盘价
+        pre_cls = get_price('510050.XSHG',t,t,fields=['pre_close']).values[0][0]
+        
+        if pre_cls*0.95 >= newest_exercise_price:  #原虚值变为实值,重新开仓略虚值期权
+        
+            #寻找month_idx
+            for month_idx in range(len(month_split)):
+                if month_split[month_idx] >= t:
+                    break
+            contract_info_CO = getContract(month_split[month_idx-1],month_split[month_idx],t,'CO')
+            contract_info_PO = getContract(month_split[month_idx-1],month_split[month_idx],t,'PO')
+            
+            if contract_info_CO['last_trade_date'].iloc[0] >= newest_expire_date:
+                holding_contract_CO[t] = contract_info_CO['code'].iloc[0]
+                holding_contract_PO[t] = contract_info_CO['code'].iloc[0]
+                
+                newest_exercise_price = contract_info_CO['exercise_price'].iloc[0]  
+                newest_expire_date = contract_info_CO['last_trade_date'].iloc[0]
+
+holding_contract_CO = holding_contract_CO.fillna(method='ffill')
+holding_contract_PO = holding_contract_PO.fillna(method='ffill')
+
+if lasttradeday != '':
+    
+    holding_contract_CO = holding_contract_CO[holding_contract_CO.index < lasttradeday]
+    holding_contract_PO = holding_contract_PO[holding_contract_PO.index < lasttradeday]
+
+data_CO = contractChange(holding_contract_CO)
+data_PO = contractChange(holding_contract_PO)
+
+# 计算期权的收益
+opt_ret_CO = optionProfit(data_CO)
+opt_ret_PO = optionProfit(data_PO)
+
+opt_ret = opt_ret_CO + opt_ret_PO
+opt_ret
+
+# 计算持仓收益
+etf_price = get_price('510050.XSHG',trade_days.index[0],trade_days.index[-1],fields=['close'])['close']
+etf_ret = 10000*etf_price.diff(1).fillna(0)
+etf_ret
+
+# 计算净值
+init_asset2 = etf_price.iloc[0]*10000
+ass2 = init_asset2 + (etf_ret + opt_ret).cumsum()
+pfl_ret2 = (ass2/ass2.shift(1) - 1).fillna(0)
+
+pfl_nv2 = (1 + pfl_ret2).cumprod()
+pfl_nv2
+
+#绘制净值图
+plt.figure(figsize=(30, 20))
+plt.plot(etf_price/etf_price.iloc[0], label='50ETF现货净值')
+plt.plot(pfl_nv2, label='领口看涨策略净值')
+plt.legend(loc='upper left', fontsize='large')
+plt.xlabel('时间',size=12)
+plt.ylabel('净值',size=12)
+plt.show()

+ 296 - 0
Lib/Options/Jiabailie/13_卖出跨式策略50ETF.py

@@ -0,0 +1,296 @@
+# 卖出跨式策略-50ETF
+# 参考资料:
+# - 原始策略来源: https://www.joinquant.com/view/community/detail/73a7f044b73242b136c8c840ef7f1748
+# - 研究网址: https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E5%8D%96%E5%87%BA%E8%B7%A8%E5%BC%8F%E7%AD%96%E7%95%A5-50ETF.ipynb
+
+# TODO: 添加卖出跨式策略50ETF相关代码
+
+import jqdata
+from   jqdata import *
+import pandas as pd
+import numpy as np
+import datetime
+import matplotlib.pyplot as plt
+
+plt.rcParams['font.sans-serif']=['SimHei']
+plt.rcParams['axes.unicode_minus'] = False
+
+from datetime import datetime, timedelta
+
+def is_last_day_of_month(date_str):
+    
+    # 将字符串转换为日期对象
+    date_obj = datetime.strptime(date_str, '%Y-%m-%d')
+    
+    # 获取下一个日期对象
+    next_date_obj = date_obj + timedelta(days=1)
+    
+    # 判断是否为下个月的第一天,如果是,则当前日期为月末
+    return date_obj.month != next_date_obj.month
+
+
+
+def get_last_day_of_month(date_str):
+    
+    # 将字符串转换为日期对象
+    date_obj = datetime.strptime(date_str, '%Y-%m-%d')
+    
+    # 获取下个月的第一天日期对象
+    next_month_first_day = datetime(date_obj.year, date_obj.month + 1, 1)
+    
+    # 从下个月的第一天减去一天,得到当前月的月末日期对象
+    last_day_of_month = next_month_first_day - timedelta(days=1)
+    
+    # 返回月末日期的字符串形式
+    return last_day_of_month.strftime('%Y-%m-%d')
+
+
+
+#获得虚值合约
+def getContract(start,end,listdate,contratType='CO'):
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                            opt.OPT_CONTRACT_INFO.trading_code, 
+                            opt.OPT_CONTRACT_INFO.name, #合约代码,合约交易代码,合约简称
+                            opt.OPT_CONTRACT_INFO.exercise_price, 
+                            opt.OPT_CONTRACT_INFO.last_trade_date, 
+                            opt.OPT_CONTRACT_INFO.list_date
+         ).filter(opt.OPT_CONTRACT_INFO.contract_type == contratType,  #期权类型
+                  opt.OPT_CONTRACT_INFO.exchange_code == 'XSHG',   #上交所
+                  opt.OPT_CONTRACT_INFO.last_trade_date > start,  #时间-到期月开始
+                  opt.OPT_CONTRACT_INFO.last_trade_date <= end,  #时间-到期月结束
+                  opt.OPT_CONTRACT_INFO.list_date < listdate)    #在交易前上市
+    
+    contract_info = opt.run_query(q_contract_info)
+    
+    etf_cls = get_price('510050.XSHG',listdate,listdate,fields=['close']).values[0][0]
+    
+    
+    if contratType == 'CO':
+        
+        contract_info['price_spread'] = contract_info['exercise_price'] - etf_cls
+        
+    else:
+        
+        contract_info['price_spread'] = etf_cls - contract_info['exercise_price']
+        
+    if contract_info['price_spread'].max() > 0:
+        
+        contract_info = contract_info[contract_info['price_spread'] > 0]  #选出虚值期权
+        contract_info = contract_info.sort_values('exercise_price')
+        
+    else:  #全是实值期权
+        
+        contract_info = contract_info.sort_values('exercise_price',ascending=False)   
+        
+    return(contract_info)
+
+
+
+#处理合约切换标记
+def contractChange(holding_contract):
+    
+    data2 = pd.DataFrame(holding_contract)
+    data2.columns = ['holding_contract']
+    data2 = data2.reindex(columns=['holding_contract','close','last_close'])
+
+    last_contract = holding_contract.iloc[0]   #记录上个持仓
+    
+    for t in data2.index:
+        
+        if last_contract == data2.loc[t,'holding_contract']:  #期权未换仓
+            
+            #收盘价
+            q_price = query(opt.OPT_DAILY_PRICE.code, 
+                            opt.OPT_DAILY_PRICE.date, 
+                            opt.OPT_DAILY_PRICE.close,
+                     ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
+                              opt.OPT_DAILY_PRICE.date==t)
+            price = opt.run_query(q_price)['close'][0]
+            data2.loc[t,'close'] = price
+
+        else:
+            
+            #收盘价,新
+            q_price = query(opt.OPT_DAILY_PRICE.code, 
+                            opt.OPT_DAILY_PRICE.date, 
+                            opt.OPT_DAILY_PRICE.close,
+                     ).filter(opt.OPT_DAILY_PRICE.code==data2.loc[t,'holding_contract'],
+                              opt.OPT_DAILY_PRICE.date==t)
+            price = opt.run_query(q_price)['close'][0]
+            data2.loc[t,'close'] = price
+            #收盘价,旧
+            q_price = query(opt.OPT_DAILY_PRICE.code, 
+                            opt.OPT_DAILY_PRICE.date, 
+                            opt.OPT_DAILY_PRICE.close,
+                     ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
+                              opt.OPT_DAILY_PRICE.date==t)
+            price = opt.run_query(q_price)['close'][0]
+            data2.loc[t,'last_close'] = price
+            last_contract = data2.loc[t,'holding_contract']
+            
+    return(data2)
+
+
+#平仓期权收益
+def optionProfit(data2,fee=5):
+    
+    #计算平仓期权的收益
+    opt_ret2 = pd.Series(0,index=data2.index)
+    pre_close2 = data2['close'].iloc[0]
+    
+    for t in data2.index[1:]:
+        
+        if data2.isna().loc[t,'last_close']:   #未换仓,last为空
+            
+            opt_ret2[t] = -10000*(data2.loc[t,'close'] - pre_close2)
+            
+        else:
+            
+            opt_ret2[t] = -10000*(data2.loc[t,'last_close'] - pre_close2) - fee  #手续费
+            
+        pre_close2 = data2.loc[t,'close']
+        
+    return(opt_ret2)
+
+#获得指定行权价以及到期日的合约
+def getContractForPrice(exercise_price,enddate,contratType='PO'):
+    
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                            opt.OPT_CONTRACT_INFO.trading_code, 
+                            opt.OPT_CONTRACT_INFO.name, #合约代码,合约交易代码,合约简称
+                            opt.OPT_CONTRACT_INFO.exercise_price, 
+                            opt.OPT_CONTRACT_INFO.last_trade_date, 
+                            opt.OPT_CONTRACT_INFO.list_date
+                            
+         ).filter(opt.OPT_CONTRACT_INFO.contract_type == contratType,  #期权类型
+                  opt.OPT_CONTRACT_INFO.exchange_code == 'XSHG',   #上交所
+                  opt.OPT_CONTRACT_INFO.last_trade_date == enddate,  #到期月
+                  opt.OPT_CONTRACT_INFO.exercise_price == exercise_price)    #指定行权价
+    
+    contract_info = opt.run_query(q_contract_info)
+    return(contract_info)
+
+#获取交易时间和时间间隔(频率:月)
+#根据不同交易日分割月份
+#指定回测的起始时间
+
+starttime = '2020-01-01'
+endtime   = '2024-03-01'
+
+trade_days = pd.Series(index=jqdata.get_trade_days(starttime,endtime))
+trade_days.index = pd.to_datetime(trade_days.index)
+
+last_day = get_last_day_of_month(endtime)
+
+month_split = list(trade_days.resample('M',label='left').mean().index) + [pd.to_datetime(last_day)]
+
+month_split
+
+##持仓情况
+holding_contract_CO = pd.Series(index=trade_days.index)
+holding_contract_PO = pd.Series(index=trade_days.index)
+
+#获取首个持仓认购合约
+contract_info_CO = getContract(month_split[0],month_split[1],trade_days.index[0],'CO')
+holding_contract_CO[trade_days.index[0]] = contract_info_CO['code'].iloc[0]
+
+#获取首个持仓认沽合约
+contract_info_PO = getContractForPrice(contract_info_CO['exercise_price'].iloc[0],contract_info_CO['last_trade_date'].iloc[0],'PO')
+holding_contract_PO[trade_days.index[0]] = contract_info_PO['code'].iloc[0]
+
+newest_exercise_price = contract_info_CO['exercise_price'].iloc[0]
+newest_expire_date = contract_info_CO['last_trade_date'].iloc[0]
+
+# 循环访问每一个交易日,判断交易情况
+# 规则:同时卖出认购期权以及同一到期日的和同一行权价的认沽期权,待行权价低于现价的95%时
+# 平仓原期权合约,重新卖出认沽以及认购期权;到期前1天移仓换月至次月合约
+erro_date = []
+
+for t in trade_days.index[1:]:
+    
+    #到期前一天
+    if t >= pd.to_datetime(get_trade_days(end_date=pd.to_datetime(newest_expire_date),count=2)[0]):
+        
+        #寻找month_idx
+        for month_idx in range(len(month_split)):
+            if month_split[month_idx] >= t:
+                break
+        if month_idx == len(month_split) -1:
+            lasttradeday = t
+            break
+        else:
+            lasttradeday = ''
+        contract_info_CO = getContract(month_split[month_idx],month_split[month_idx+1],t,'CO')
+        contract_info_PO = contract_info_PO = getContractForPrice(contract_info_CO['exercise_price'].iloc[0],contract_info_CO['last_trade_date'].iloc[0],'PO')
+
+        if contract_info_CO['last_trade_date'].iloc[0] >= newest_expire_date:
+            holding_contract_CO[t] = contract_info_CO['code'].iloc[0]
+            holding_contract_PO[t] = contract_info_PO['code'].iloc[0]
+            
+            newest_exercise_price = contract_info_CO['exercise_price'].iloc[0]
+            newest_expire_date = contract_info_CO['last_trade_date'].iloc[0]
+    else:
+        
+        #获取昨日50etf收盘价
+        pre_cls = get_price('510050.XSHG',t,t,fields=['pre_close']).values[0][0]
+        
+        if pre_cls*0.95 >= newest_exercise_price:  #原虚值变为实值,重新开仓略虚值期权
+            
+            #寻找month_idx
+            for month_idx in range(len(month_split)):
+                if month_split[month_idx] >= t:
+                    break
+            contract_info_CO = getContract(month_split[month_idx-1],month_split[month_idx],t,'CO')
+            contract_info_PO = contract_info_PO = getContractForPrice(contract_info_CO['exercise_price'].iloc[0],contract_info_CO['last_trade_date'].iloc[0],'PO')
+            
+            if contract_info_CO['last_trade_date'].iloc[0] >= newest_expire_date:
+                holding_contract_CO[t] = contract_info_CO['code'].iloc[0]
+                holding_contract_PO[t] = contract_info_PO['code'].iloc[0]
+                
+                newest_exercise_price = contract_info_CO['exercise_price'].iloc[0]  
+                newest_expire_date = contract_info_CO['last_trade_date'].iloc[0]
+
+holding_contract_CO = holding_contract_CO.fillna(method='ffill')
+holding_contract_PO = holding_contract_PO.fillna(method='ffill') 
+
+if lasttradeday != '':
+    
+    holding_contract_CO = holding_contract_CO[holding_contract_CO.index < lasttradeday]
+    holding_contract_PO = holding_contract_PO[holding_contract_PO.index < lasttradeday]
+    
+data_CO = contractChange(holding_contract_CO)
+data_PO = contractChange(holding_contract_PO)
+
+#计算期权的收益
+opt_ret_CO = optionProfit(data_CO)
+opt_ret_PO = optionProfit(data_PO)
+
+opt_ret = opt_ret_CO + opt_ret_PO
+opt_ret
+
+#计算现货收益
+etf_price = get_price('510050.XSHG',trade_days.index[0],trade_days.index[-1],fields=['close'])['close']
+etf_ret = 10000*etf_price.diff(1).fillna(0)
+etf_ret
+
+#计算净值
+init_asset2 = etf_price.iloc[0]*10000
+ass2 = init_asset2 + opt_ret.cumsum()
+
+pfl_ret2 = (ass2/ass2.shift(1) - 1).fillna(0)
+pfl_nv2 = (1 + pfl_ret2).cumprod()
+
+pfl_nv2
+
+#绘制净值图
+plt.figure(figsize=(8, 5))
+
+plt.plot(etf_price/etf_price.iloc[0], label='50ETF现货净值')
+plt.plot(pfl_nv2, label='卖出跨式策略净值')
+
+plt.legend(loc='upper left', fontsize='large')
+plt.xlabel('时间',size=12)
+plt.ylabel('净值',size=12)
+
+plt.show()
+

+ 324 - 0
Lib/Options/Jiabailie/14_卖出跨式策略商品主力合约.py

@@ -0,0 +1,324 @@
+# 卖出跨式策略-商品主力合约
+# 参考资料:
+# - 原始策略来源: https://www.joinquant.com/view/community/detail/dc14876fee244d726f18c652eb44c7d7
+# - 研究网址: https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E5%8D%96%E5%87%BA%E8%B7%A8%E5%BC%8F%E7%AD%96%E7%95%A5-%E5%95%86%E5%93%81%E4%B8%BB%E5%8A%9B%E5%90%88%E7%BA%A6.ipynb
+
+# TODO: 添加卖出跨式策略商品主力合约相关代码
+
+import jqdata
+from   jqdata import *
+import pandas as pd
+import numpy as np
+import datetime
+import matplotlib.pyplot as plt
+from   datetime import datetime, timedelta
+
+plt.rcParams['font.sans-serif']=['SimHei']
+plt.rcParams['axes.unicode_minus'] = False
+
+#获取期权合约信息
+
+def getContractForCode(code): 
+    
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                            opt.OPT_CONTRACT_INFO.trading_code,                            
+                            opt.OPT_CONTRACT_INFO.name,
+                            opt.OPT_CONTRACT_INFO.exercise_price, 
+                            opt.OPT_CONTRACT_INFO.last_trade_date,
+                            
+                            # 行权价格,最后交易日
+                            opt.OPT_CONTRACT_INFO.list_date
+                            ).filter(opt.OPT_CONTRACT_INFO.code == code,
+                                         )  
+    contract_info = opt.run_query(q_contract_info)
+    return(contract_info)
+
+
+def getContract(symbol,date,type="CO"): #CO为认购,PO为认沽
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                            opt.OPT_CONTRACT_INFO.trading_code,
+                            opt.OPT_CONTRACT_INFO.name,
+                            opt.OPT_CONTRACT_INFO.exercise_price, 
+                            opt.OPT_CONTRACT_INFO.last_trade_date,
+                            # 行权价格,最后交易日
+                            opt.OPT_CONTRACT_INFO.list_date
+                            ).filter(opt.OPT_CONTRACT_INFO.underlying_symbol == symbol,
+                                     opt.OPT_CONTRACT_INFO.contract_type == type,  # 期权类型
+                                     opt.OPT_CONTRACT_INFO.last_trade_date >= date
+                                     )  
+    
+    contract_info = opt.run_query(q_contract_info)
+    commodity_cls = get_price(symbol, date, date, fields=['close']).values[0][0]
+    
+    if type == 'CO':
+        
+        contract_info['price_spread'] = contract_info['exercise_price'] - commodity_cls
+        
+    else:
+        
+        contract_info['price_spread'] = commodity_cls - contract_info['exercise_price']
+        
+    if contract_info['price_spread'].max() > 0:
+        
+        contract_info = contract_info[contract_info['price_spread'] > 0]  # 选出虚值期权
+        contract_info = contract_info.sort_values('exercise_price')
+        
+    else:  # 全是实值期权
+        
+        contract_info = contract_info.sort_values('exercise_price', ascending=False)
+    #return(contract_info['code'].iloc[0])    
+    return(contract_info)
+
+
+#获取期权价格等信息
+def getContractPrice(code,last_trade_date,type='CO'):
+    
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                            opt.OPT_CONTRACT_INFO.trading_code,
+                            opt.OPT_CONTRACT_INFO.name,
+                            opt.OPT_CONTRACT_INFO.exercise_price, 
+                            opt.OPT_CONTRACT_INFO.last_trade_date,
+                            # 行权价格,最后交易日
+                            opt.OPT_CONTRACT_INFO.list_date
+                            ).filter(opt.OPT_CONTRACT_INFO.code == code,
+                                     opt.OPT_CONTRACT_INFO.contract_type == type,  # 期权类型
+                                     opt.OPT_CONTRACT_INFO.last_trade_date >= last_trade_date
+                                    ) 
+    
+    contract_info = opt.run_query(q_contract_info)
+    
+    return(contract_info)
+
+
+
+#处理合约切换标记
+def contractChange(holding_contract,main_list,error_date):
+    
+    data2 = pd.DataFrame(holding_contract)
+    data2.columns = ['holding_contract']
+    data2 = data2.reindex(columns=['holding_contract','close','last_close'])
+    data2 = data2.drop(error_date)
+    main_list = main_list.drop(error_date)
+    last_contract = holding_contract.iloc[0]   #记录上个持仓
+    
+    for i in range(0,len(data2.index)):
+        
+        t = data2.index[i]
+        
+        if last_contract == data2.loc[t,'holding_contract']:  #期权未换仓
+            
+            q_price = query(opt.OPT_DAILY_PRICE.code, 
+                            opt.OPT_DAILY_PRICE.date, opt.OPT_DAILY_PRICE.close,
+                     ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
+                              opt.OPT_DAILY_PRICE.date==t)
+            price = opt.run_query(q_price)['close'][0]
+            data2.loc[t,'close'] = price
+            
+        else: #合约换仓
+            
+            q_price = query(opt.OPT_DAILY_PRICE.code, 
+                            opt.OPT_DAILY_PRICE.date, 
+                            opt.OPT_DAILY_PRICE.close,
+                     ).filter(opt.OPT_DAILY_PRICE.code==data2.loc[t,'holding_contract'],
+                              opt.OPT_DAILY_PRICE.date==t)
+            price = opt.run_query(q_price)['close'][0]
+            data2.loc[t,'close'] = price
+            
+            #收盘价,旧
+            q_price = query(opt.OPT_DAILY_PRICE.code, 
+                            opt.OPT_DAILY_PRICE.date, 
+                            opt.OPT_DAILY_PRICE.close,
+                     ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
+                              opt.OPT_DAILY_PRICE.date==data2.index[i-1])
+            
+            price = opt.run_query(q_price)['close'][0]
+            data2.loc[t,'last_close'] = price
+            last_contract = data2.loc[t,'holding_contract']
+            
+    return(data2)        
+
+
+
+def optionProfit(data2,price_gap,fee):
+    
+    #计算平仓期权的收益
+    opt_ret2 = pd.Series(0,index=data2.index)
+    pre_close2 = data2['close'].iloc[0]
+    
+    for t in data2.index[1:]:
+        
+        if data2.isna().loc[t,'last_close']:   #未换仓,last为空
+            
+            opt_ret2[t] = -price_gap*(data2.loc[t,'close'] - pre_close2)
+            
+        else:
+            
+            opt_ret2[t] = -price_gap*(data2.loc[t,'last_close'] - pre_close2) - fee  #手续费
+            
+        pre_close2 = data2.loc[t,'close']
+        
+    return(opt_ret2)
+
+
+
+#获得指定行权价以及到期日的合约
+def getContractForPrice(exercise_price,enddate,contratType='PO'):
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                            opt.OPT_CONTRACT_INFO.trading_code, 
+                            opt.OPT_CONTRACT_INFO.name, #合约代码,合约交易代码,合约简称
+                            opt.OPT_CONTRACT_INFO.exercise_price, 
+                            opt.OPT_CONTRACT_INFO.last_trade_date, 
+                            opt.OPT_CONTRACT_INFO.list_date
+         ).filter(opt.OPT_CONTRACT_INFO.contract_type == contratType,  #期权类型
+                  opt.OPT_CONTRACT_INFO.last_trade_date == enddate,    #到期月
+                  opt.OPT_CONTRACT_INFO.exercise_price == exercise_price)    #指定行权价
+    
+    contract_info = opt.run_query(q_contract_info)
+    return(contract_info)
+
+# 设置各项参数
+# 合约代码
+symbol = 'AL'
+
+#手续费
+fee = 5
+
+#合约价差
+price_gap = 10
+
+#起始时间
+starttime =  '2022-01-01'
+endtime   =  '2024-05-01'
+
+SUBJECT_MATTER = get_dominant_future(symbol,date = starttime)
+SUBJECT_MATTER
+
+#获取交易时间和时间间隔(频率:月)
+#根据不同交易日分割月份
+#指定回测的起始时间
+trade_days = pd.Series(index=jqdata.get_trade_days(starttime,endtime))
+trade_days.index = pd.to_datetime(trade_days.index)
+
+##持仓情况
+#主力合约列表
+main_list = pd.Series(index=trade_days.index)
+main_list[trade_days.index[0]] = SUBJECT_MATTER
+holding_contract_CO = pd.Series(index=trade_days.index)
+
+#获取首个认购持仓合约
+contract_info = getContract(SUBJECT_MATTER,trade_days.index[0],type='CO')
+contract = contract_info['code'].iloc[0]
+holding_contract_CO[trade_days.index[0]] = contract
+print(holding_contract_CO[:3])
+
+#获取首个认沽持仓合约
+holding_contract_PO = pd.Series(index=trade_days.index)
+contract_info = getContractForPrice(contract_info['exercise_price'].iloc[0],contract_info['last_trade_date'].iloc[0],'PO')
+contract = contract_info['code'].iloc[0]
+holding_contract_PO[trade_days.index[0]] = contract
+print(holding_contract_PO[:3])
+
+# 循环访问每一个交易日,判断交易情况
+# 规则:同时卖出认购期权以及同一到期日的和同一行权价的认沽期权
+# 待行权价低于现价的95%时,平仓原期权合约,重新卖出认沽以及认购期权;到期前1天移仓换月至次月合约
+
+error_date =[]
+pre_hold_CO = holding_contract_CO[0]
+pre_hold_PO = holding_contract_PO[0]
+
+for i in range(1,len(trade_days)):
+    
+    pre_day = trade_days.index[i-1]
+    cur_day = trade_days.index[i]
+    cur_main = get_dominant_future(symbol,date = cur_day) #当前主力合约
+    pre_main = get_dominant_future(symbol,date = pre_day) #上一个交易日的主力合约
+    main_list[cur_day] = cur_main
+    
+    if cur_main != pre_main: #主力合约切换
+        
+        contract_info_CO = getContract(cur_main,cur_day,type='CO')
+        contract_CO = contract_info_CO['code'].iloc[0]
+        
+        #获取相同行权价、到期日的认沽合约
+        contract_info_PO = getContractForPrice(contract_info_CO['exercise_price'].iloc[0],
+                                               contract_info_CO['last_trade_date'].iloc[0],'PO')
+        
+        contract_PO = contract_info_PO['code'].iloc[0]
+        
+    else:
+        
+        pre_cls = get_price(cur_main, cur_day, cur_day, fields=['pre_close']).values[0][0]
+        contract_info_CO = getContractPrice(pre_hold_CO,cur_day,"CO")
+        pre_exercise_price_CO = contract_info_CO
+        contract_info_PO = getContractPrice(pre_hold_PO,cur_day,"PO")
+        pre_exercise_price_PO = contract_info_PO
+        
+        if contract_info_CO.empty | contract_info_PO.empty:
+            
+            error_date.append(cur_day)
+            continue
+            
+        else:
+            
+            pre_exercise_price_CO = pre_exercise_price_CO['exercise_price'][0]
+            
+            if pre_cls * 0.95 >= pre_exercise_price_CO:
+                
+                contract_info_CO = getContract(pre_main,cur_day)
+                contract_CO = contract_info_CO['code'].iloc[0]
+                contract_info_PO = getContractForPrice(contract_info_CO['exercise_price'].iloc[0],
+                                                       contract_info_CO['last_trade_date'].iloc[0],'PO')
+                contract_PO = contract_info_PO['code'].iloc[0]
+                
+            else:
+                
+                contract_CO = pre_hold_CO
+                contract_PO = pre_hold_PO
+    
+    holding_contract_CO[cur_day] = contract_CO
+    pre_hold_CO = contract_CO
+    
+    holding_contract_PO[cur_day] = contract_PO
+    pre_hold_PO = contract_PO
+    
+holding_contract_CO = holding_contract_CO.fillna(method='ffill')
+holding_contract_PO = holding_contract_PO.fillna(method='ffill')
+
+data_list_CO = contractChange(holding_contract_CO,main_list,error_date)
+data_list_PO = contractChange(holding_contract_PO,main_list,error_date)
+
+#计算平仓期权的收益
+opt_ret_CO = optionProfit(data_list_CO,price_gap,fee)
+opt_ret_PO = optionProfit(data_list_PO,price_gap,fee)
+opt_ret = opt_ret_CO + opt_ret_PO
+print(opt_ret)
+
+#计算现货收益
+commodity_price = [get_price(main_list[t],
+                             data_list_CO.index[t],
+                             data_list_CO.index[t],
+                             fields=['close'])['close'][0] for t in range(0,len(data_list_CO.index))]
+
+commodity_price = pd.Series(commodity_price,index=data_list_CO.index)
+
+commodity_ret = commodity_price.diff(1).fillna(0)
+commodity_ret
+
+#计算净值
+init_asset2 = commodity_price.iloc[0]*price_gap
+ass2 = init_asset2 + opt_ret.cumsum()
+
+pfl_ret2 = (ass2/ass2.shift(1) - 1).fillna(0)
+pfl_nv2 = (1 + pfl_ret2).cumprod()
+pfl_nv2
+
+#绘制净值图
+plt.figure(figsize=(10,6))
+plt.plot(commodity_price/commodity_price.iloc[0], label='期货净值')
+plt.plot(pfl_nv2, label=symbol+'卖出跨式策略净值')
+
+plt.legend(loc='upper left', fontsize='large')
+
+plt.xlabel('时间',size=12)
+plt.ylabel('净值',size=12)
+plt.show()

+ 377 - 0
Lib/Options/Jiabailie/15_买入日历价差策略商品期货.py

@@ -0,0 +1,377 @@
+# 买入日历价差策略-商品期货
+# 参考资料:
+# - 原始策略来源: https://www.joinquant.com/view/community/detail/af07500292294804acd19f7f0f5b23e4
+# - 研究网址: https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E4%B9%B0%E5%85%A5%E6%97%A5%E5%8E%86%E4%BB%B7%E5%B7%AE%E7%AD%96%E7%95%A5-%E5%95%86%E5%93%81%E6%9C%9F%E8%B4%A7.ipynb
+
+import jqdata
+from   jqdata import *
+import pandas as pd
+import numpy as np
+import datetime
+import matplotlib.pyplot as plt
+from   datetime import datetime, timedelta
+
+plt.rcParams['font.sans-serif']=['SimHei']
+plt.rcParams['axes.unicode_minus'] = False
+
+#获取期权合约信息
+def getContractForCode(code): 
+    
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                            opt.OPT_CONTRACT_INFO.trading_code,
+                            opt.OPT_CONTRACT_INFO.name,
+                            opt.OPT_CONTRACT_INFO.exercise_price, opt.OPT_CONTRACT_INFO.last_trade_date,
+                            # 行权价格,最后交易日
+                            opt.OPT_CONTRACT_INFO.list_date
+                            ).filter(opt.OPT_CONTRACT_INFO.code == code,)
+    
+    contract_info = opt.run_query(q_contract_info)
+    
+    return(contract_info)
+
+def getContract(symbol,date,type="CO"):  # CO为认购,PO为认沽
+    
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                            opt.OPT_CONTRACT_INFO.trading_code,
+                            opt.OPT_CONTRACT_INFO.name,
+                            opt.OPT_CONTRACT_INFO.exercise_price, 
+                            opt.OPT_CONTRACT_INFO.last_trade_date,
+                            # 行权价格,最后交易日
+                            opt.OPT_CONTRACT_INFO.list_date
+                            ).filter(opt.OPT_CONTRACT_INFO.underlying_symbol == symbol,
+                                     opt.OPT_CONTRACT_INFO.contract_type == type,  # 期权类型
+                                     opt.OPT_CONTRACT_INFO.last_trade_date >= date,
+                                     opt.OPT_CONTRACT_INFO.list_date <= date
+                                     )  
+    
+    contract_info = opt.run_query(q_contract_info)
+    
+    commodity_cls = get_price(symbol, date, date, fields=['close']).values[0][0]
+    
+    
+    if type == 'CO':
+        
+        contract_info['price_spread'] = contract_info['exercise_price'] - commodity_cls
+        
+    else:
+        
+        contract_info['price_spread'] = commodity_cls - contract_info['exercise_price']
+        
+    if contract_info['price_spread'].max() > 0:
+        
+        contract_info = contract_info[contract_info['price_spread'] > 0]  # 选出虚值期权
+        contract_info = contract_info.sort_values('exercise_price')
+        
+    else:  # 全是实值期权
+        
+        contract_info = contract_info.sort_values('exercise_price', ascending=False)
+    #return(contract_info['code'].iloc[0])
+    return(contract_info)
+
+
+#获取期权价格等信息
+def getContractExercisePrice(code,last_trade_date,type='CO'):
+    
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.code,
+                            opt.OPT_CONTRACT_INFO.trading_code,                            
+                            opt.OPT_CONTRACT_INFO.name,
+                            opt.OPT_CONTRACT_INFO.exercise_price, 
+                            opt.OPT_CONTRACT_INFO.last_trade_date,
+                            # 行权价格,最后交易日
+                            opt.OPT_CONTRACT_INFO.list_date
+                            ).filter(opt.OPT_CONTRACT_INFO.code == code,
+                                     opt.OPT_CONTRACT_INFO.contract_type == type,  # 期权类型
+                                     opt.OPT_CONTRACT_INFO.last_trade_date >= last_trade_date,
+                                    ) 
+    
+    contract_info = opt.run_query(q_contract_info)
+    return(contract_info)
+
+#处理合约切换标记
+def contractChange(holding_contract,main_list,error_date):
+    
+    data2 = pd.DataFrame(holding_contract)
+    data2.columns = ['holding_contract']
+    data2 = data2.reindex(columns=['holding_contract','close','last_close'])
+    data2 = data2.drop(error_date)
+    main_list = main_list.drop(error_date)
+    last_contract = holding_contract.iloc[0]   #记录上个持仓
+    
+    for i in range(0,len(data2.index)):
+        
+        t = data2.index[i]
+        
+        if last_contract == data2.loc[t,'holding_contract']:  #期权未换仓
+            
+            q_price = query(opt.OPT_DAILY_PRICE.code, 
+                            opt.OPT_DAILY_PRICE.date, 
+                            opt.OPT_DAILY_PRICE.close,
+                     ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
+                              opt.OPT_DAILY_PRICE.date==t)
+            
+            price = opt.run_query(q_price)['close'][0]
+            data2.loc[t,'close'] = price
+            
+        else: #合约换仓
+            q_price = query(opt.OPT_DAILY_PRICE.code, 
+                            opt.OPT_DAILY_PRICE.date, 
+                            opt.OPT_DAILY_PRICE.close,
+                     ).filter(opt.OPT_DAILY_PRICE.code==data2.loc[t,'holding_contract'],
+                              opt.OPT_DAILY_PRICE.date==t)
+            
+            price = opt.run_query(q_price)['close'][0]
+            data2.loc[t,'close'] = price
+            
+            #收盘价,旧
+            q_price = query(opt.OPT_DAILY_PRICE.code, 
+                            opt.OPT_DAILY_PRICE.date, 
+                            opt.OPT_DAILY_PRICE.close,
+                     ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
+                              opt.OPT_DAILY_PRICE.date==data2.index[i-1])
+            
+            price = opt.run_query(q_price)['close'][0]
+            data2.loc[t,'last_close'] = price
+            last_contract = data2.loc[t,'holding_contract']
+            
+    return(data2)
+        
+def optionProfit(data2,price_gap,fee):
+    
+    #计算平仓期权的收益
+    opt_ret2 = pd.Series(0,index=data2.index)
+    pre_close2 = data2['close'].iloc[0]
+    
+    for t in data2.index[1:]:
+        
+        if data2.isna().loc[t,'last_close']:   #未换仓,last为空
+            
+            opt_ret2[t] = -price_gap*(data2.loc[t,'close'] - pre_close2)
+            
+        else:
+            
+            opt_ret2[t] = -price_gap*(data2.loc[t,'last_close'] - pre_close2) - fee  #手续费
+            
+        pre_close2 = data2.loc[t,'close']
+        
+    return(opt_ret2)
+
+#获得指定行权价以及到期日的合约
+def getContractForPrice(exercise_price,enddate,contratType='PO'):
+    
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                            opt.OPT_CONTRACT_INFO.trading_code, 
+                            opt.OPT_CONTRACT_INFO.name, #合约代码,合约交易代码,合约简称
+                            opt.OPT_CONTRACT_INFO.exercise_price, 
+                            opt.OPT_CONTRACT_INFO.last_trade_date, 
+                            opt.OPT_CONTRACT_INFO.list_date
+         ).filter(opt.OPT_CONTRACT_INFO.contract_type == contratType,  # 期权类型
+                  opt.OPT_CONTRACT_INFO.last_trade_date == enddate,    # 到期月
+                  opt.OPT_CONTRACT_INFO.exercise_price == exercise_price)    #指定行权价
+    
+    contract_info = opt.run_query(q_contract_info)
+    
+    return(contract_info)
+
+#获得指定行权价以及合约的期权
+def getContractForContractAndPrice(symbol,exercise_price,date,contratType='CO'):
+    
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                            opt.OPT_CONTRACT_INFO.trading_code, 
+                            opt.OPT_CONTRACT_INFO.name, #合约代码,合约交易代码,合约简称
+                            opt.OPT_CONTRACT_INFO.exercise_price, 
+                            opt.OPT_CONTRACT_INFO.last_trade_date, 
+                            opt.OPT_CONTRACT_INFO.list_date
+         ).filter(
+                  opt.OPT_CONTRACT_INFO.underlying_symbol == symbol,
+                  opt.OPT_CONTRACT_INFO.contract_type == contratType,  #期权类型
+                  opt.OPT_CONTRACT_INFO.exercise_price == exercise_price, #指定行权价
+                  opt.OPT_CONTRACT_INFO.list_date <= date)   
+    
+    contract_info = opt.run_query(q_contract_info)
+    
+    return(contract_info)
+
+
+# 返回相对于主力合约之前,或之后的合约,用来定位近月或远月合约,pos = 0 表示返回主力合约;
+# pos=-1,返回主力合约之前一个周期的可交易合约;pos=1,返回主力合约之后一个周期的可交易合约
+def findContractsForMonth(symbol,date,pos=0):
+    
+    dominant = get_dominant_future(symbol, date=date)
+    contracts_list = get_future_contracts(symbol, date=date)
+    pattern = re.compile(r'\d+')
+    
+    numbers_list = [pattern.findall(contract)[0] for contract in contracts_list]
+    
+    sorted_combined = sorted(zip(contracts_list, numbers_list), key=lambda x: x[1])
+    sorted_contracts, sorted_B = map(list, zip(*sorted_combined))
+    num = sorted_contracts.index(dominant)
+    
+    find_pos = num + pos
+    
+    if find_pos<0:
+        find_pos = 0
+    elif find_pos >= len(contracts_list):
+        find_pos = len(contracts_list)
+    return(sorted_contracts[find_pos])
+
+
+# 设置各项参数
+# 合约代码
+symbol = 'AL'
+
+#手续费
+fee = 5
+
+#合约价差
+price_gap = 10
+
+#起始时间
+starttime = '2022-01-01'
+endtime   = '2024-05-01'
+
+SUBJECT_MATTER = get_dominant_future(symbol,date = starttime)
+SUBJECT_MATTER
+
+#指定回测的起始时间
+trade_days = pd.Series(index=jqdata.get_trade_days(starttime,endtime))
+trade_days.index = pd.to_datetime(trade_days.index)
+
+##持仓情况
+#主力合约列表
+main_list = pd.Series(index=trade_days.index)
+main_list[trade_days.index[0]] = SUBJECT_MATTER
+
+close_main_list = pd.Series(index=trade_days.index)
+after_main_list = pd.Series(index=trade_days.index)
+
+#获取首个近月认购持仓合约
+holding_contract_close = pd.Series(index=trade_days.index)
+close_main = findContractsForMonth(symbol,trade_days.index[0],-1)
+close_main_list[trade_days.index[0]] = close_main
+contract_info = getContract(close_main,trade_days.index[0],type='CO')
+
+if contract_info.empty: #近月合约已经过期,则采用主力合约替代
+    contract_info = getContract(SUBJECT_MATTER,trade_days.index[0],type='CO')
+    close_main_list[trade_days.index[0]] = SUBJECT_MATTER
+contract = contract_info['code'].iloc[0]
+holding_contract_close[trade_days.index[0]] = contract
+print(holding_contract_close[:3])
+
+#获取首个远月认购持仓合约
+holding_contract_after = pd.Series(index=trade_days.index)
+after_main = findContractsForMonth(symbol,trade_days.index[0],1)
+contract_info = getContractForContractAndPrice(after_main,contract_info['exercise_price'].iloc[0],trade_days.index[0],'CO')
+after_main_list[trade_days.index[0]] = after_main
+contract = contract_info['code'].iloc[0]
+holding_contract_after[trade_days.index[0]] = contract
+print(holding_contract_after[:3])
+
+# 循环访问每一个交易日,判断交易情况
+# 规则: 同时卖出一份近月认购期权以及买入相同行权价的远月认购,待行权价低于现价的95%时
+# 平仓原期权合约,重新调整持仓;到期前1天移仓换月至次月合约
+error_date =[]
+pre_hold_close = holding_contract_close[0]
+pre_hold_after = holding_contract_after[0]
+
+for i in range(1,len(trade_days)):
+    
+    pre_day = trade_days.index[i-1]
+    cur_day = trade_days.index[i]
+    cur_main = get_dominant_future(symbol,date = cur_day) #当前主力合约
+    pre_main = get_dominant_future(symbol,date = pre_day) #上一个交易日的主力合约
+    main_list[cur_day] = cur_main
+    close_main_list[cur_day] = close_main_list[pre_day]
+    after_main_list[cur_day] = after_main_list[pre_day]
+    
+    expire_close = getContractForCode(pre_hold_close)
+    expire_close = expire_close['last_trade_date'].iloc[0]
+    expire_after = getContractForCode(pre_hold_after)
+    expire_after = expire_after['last_trade_date'].iloc[0]
+    
+    if cur_main != pre_main or expire_close < cur_day.date() or  expire_after < cur_day.date(): #主力合约切换 或者合约到期 
+        close_main = findContractsForMonth(symbol,cur_day,-1)
+        close_main_list[cur_day] = close_main
+        contract_info_close = getContract(close_main,cur_day,type='CO')
+        if contract_info_close.empty: #近月合约已经过期,则采用主力合约替代
+            contract_info_close = getContract(cur_main,cur_day,type='CO')
+            if contract_info_close.empty:
+                error_date.append(cur_day)
+                continue
+            close_main_list[cur_day] = cur_main
+        contract_close = contract_info_close['code'].iloc[0]
+        #获取相同行权价、到期日的认沽合约
+        after_main = findContractsForMonth(symbol,cur_day,1)
+        after_main_list[cur_day] = after_main
+        contract_info_after = getContractForContractAndPrice(after_main,contract_info_close['exercise_price'].iloc[0],cur_day,'CO')
+        if contract_info_after.empty:
+            error_date.append(cur_day)
+            continue
+        contract_after = contract_info_after['code'].iloc[0]
+    else:
+        pre_cls = get_price(close_main_list[cur_day], cur_day, cur_day, fields=['pre_close']).values[0][0]
+        contract_info_close = getContractExercisePrice(pre_hold_close,cur_day,"CO")
+        pre_exercise_price_close = contract_info_close
+        contract_info_after = getContractExercisePrice(pre_hold_after,cur_day,"CO")
+        pre_exercise_price_after = contract_info_after
+        if pre_exercise_price_close.empty | pre_exercise_price_after.empty:
+            error_date.append(cur_day)
+            continue
+        else:
+            pre_exercise_price_close = pre_exercise_price_close['exercise_price'][0]
+            if pre_cls * 0.95 >= pre_exercise_price_close:
+                close_main = findContractsForMonth(symbol,cur_day,-1)
+                close_main_list[cur_day] = close_main
+                contract_info_close = getContract(close_main,cur_day,type='CO')
+                if contract_info_close.empty: #近月合约已经过期,则采用主力合约替代
+                    contract_info_close = getContract(cur_main,cur_day)
+                    close_main_list[cur_day] = cur_main
+                contract_close = contract_info_close['code'].iloc[0]
+                after_main = findContractsForMonth(symbol,cur_day,1)
+                after_main_list[cur_day] = after_main
+                contract_info_after = getContractForContractAndPrice(after_main,contract_info_close['exercise_price'].iloc[0],cur_day,'CO')
+                contract_after = contract_info_after['code'].iloc[0]
+            else:
+                contract_close = pre_hold_close
+                contract_after = pre_hold_after
+    
+    holding_contract_close[cur_day] = contract_close
+    pre_hold_close = contract_close
+    holding_contract_after[cur_day] = contract_after
+    pre_hold_after = contract_after
+holding_contract_close = holding_contract_close.fillna(method='ffill')
+holding_contract_after = holding_contract_after.fillna(method='ffill')
+
+data_list_close = contractChange(holding_contract_close,close_main_list,error_date)
+data_list_after = contractChange(holding_contract_after,after_main_list,error_date)
+
+#计算平仓期权的收益
+opt_ret_close = optionProfit(data_list_close,price_gap,fee)
+opt_ret_after = optionProfit(data_list_after,price_gap,fee)
+opt_ret = opt_ret_close + opt_ret_after
+print(opt_ret)
+
+#计算现货收益
+commodity_price = [get_price(main_list[t],data_list_close.index[t],data_list_close.index[t],fields=['close'])['close'][0] for t in range(0,len(data_list_close.index))]
+commodity_price = pd.Series(commodity_price,index=data_list_close.index)
+
+commodity_ret = commodity_price.diff(1).fillna(0)
+commodity_ret
+
+#计算净值
+init_asset2 = commodity_price.iloc[0]*price_gap
+ass2 = init_asset2 + opt_ret.cumsum()
+
+pfl_ret2 = (ass2/ass2.shift(1) - 1).fillna(0)
+pfl_nv2 = (1 + pfl_ret2).cumprod()
+pfl_nv2
+
+#绘制净值图
+plt.figure(figsize=(30,20))
+plt.plot(commodity_price/commodity_price.iloc[0], label='现货净值')
+plt.plot(pfl_nv2, label=symbol+ '买入日历价差策略净值')
+
+plt.legend(loc='upper left', fontsize='large')
+plt.xlabel('时间',size=12)
+plt.ylabel('净值',size=12)
+
+plt.show()

+ 355 - 0
Lib/Options/Jiabailie/16_买入日历价差策略50ETF.py

@@ -0,0 +1,355 @@
+# 买入日历价差策略-50ETF
+# 参考资料:
+# - 原始策略来源: https://www.joinquant.com/view/community/detail/0d2479d4f374fce4a4b900f2c77d3ba3
+# - 研究网址: https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E4%B9%B0%E5%85%A5%E6%97%A5%E5%8E%86%E4%BB%B7%E5%B7%AE%E7%AD%96%E7%95%A5-50ETF.ipynb
+
+import jqdata
+from   jqdata import *
+import pandas as pd
+import numpy as np
+import datetime
+import matplotlib.pyplot as plt
+
+plt.rcParams['font.sans-serif']=['SimHei']
+plt.rcParams['axes.unicode_minus'] = False
+
+from datetime import datetime, timedelta
+#通过合约代码获得其他信息
+def getContractForCode(code): 
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                            opt.OPT_CONTRACT_INFO.trading_code,
+                            opt.OPT_CONTRACT_INFO.name,
+                            opt.OPT_CONTRACT_INFO.exercise_price, opt.OPT_CONTRACT_INFO.last_trade_date,
+                            # 行权价格,最后交易日
+                            opt.OPT_CONTRACT_INFO.list_date
+                            ).filter(opt.OPT_CONTRACT_INFO.code == code,
+                                         )  
+    contract_info = opt.run_query(q_contract_info)
+    
+    return(contract_info)
+
+#获得虚值合约
+def getContract(start,end,listdate,contratType='CO'):
+    
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                            opt.OPT_CONTRACT_INFO.trading_code, 
+                            opt.OPT_CONTRACT_INFO.name, #合约代码,合约交易代码,合约简称
+                            opt.OPT_CONTRACT_INFO.exercise_price, 
+                            opt.OPT_CONTRACT_INFO.last_trade_date, 
+                            opt.OPT_CONTRACT_INFO.list_date
+         ).filter(opt.OPT_CONTRACT_INFO.contract_type == contratType,  #期权类型
+                  opt.OPT_CONTRACT_INFO.underlying_type == 'ETF',#ETF类型
+                  opt.OPT_CONTRACT_INFO.exchange_code == 'XSHG',   #上交所
+                  opt.OPT_CONTRACT_INFO.last_trade_date > start,  #时间-到期月开始
+                  opt.OPT_CONTRACT_INFO.last_trade_date <= end,  #时间-到期月结束
+                  opt.OPT_CONTRACT_INFO.list_date < listdate)    #在交易前上市
+    
+    contract_info = opt.run_query(q_contract_info)
+    
+    etf_cls = get_price('510050.XSHG',listdate,listdate,fields=['close']).values[0][0]
+    
+    if contratType == 'CO':
+        
+        contract_info['price_spread'] = contract_info['exercise_price'] - etf_cls
+        
+    else:
+        
+        contract_info['price_spread'] = etf_cls - contract_info['exercise_price']
+        
+    if contract_info['price_spread'].max() > 0:
+        
+        contract_info = contract_info[contract_info['price_spread'] > 0]  #选出虚值期权
+        contract_info = contract_info.sort_values('exercise_price')
+        
+    else:  #全是实值期权
+        
+        contract_info = contract_info.sort_values('exercise_price',ascending=False)   
+        
+    return(contract_info)
+
+
+#获得指定行权日期的虚值合约
+def getContractForExerciseDate(exercisedate,date,contratType='CO'):
+    
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                            opt.OPT_CONTRACT_INFO.trading_code, 
+                            opt.OPT_CONTRACT_INFO.name, #合约代码,合约交易代码,合约简称
+                            opt.OPT_CONTRACT_INFO.exercise_price, 
+                            opt.OPT_CONTRACT_INFO.last_trade_date, 
+                            opt.OPT_CONTRACT_INFO.list_date
+         ).filter(opt.OPT_CONTRACT_INFO.contract_type == contratType,  #期权类型
+                  opt.OPT_CONTRACT_INFO.underlying_type == 'ETF',#ETF类型
+                  opt.OPT_CONTRACT_INFO.exchange_code == 'XSHG',   #上交所
+                  opt.OPT_CONTRACT_INFO.exercise_date == exercisedate, #指定行权日期
+                  opt.OPT_CONTRACT_INFO.last_trade_date >= date,
+                  opt.OPT_CONTRACT_INFO.list_date <= date
+                 )    #在交易前上市
+    
+    contract_info = opt.run_query(q_contract_info)
+    
+    etf_cls = get_price('510050.XSHG',date,date,fields=['close']).values[0][0]
+    
+    if contratType == 'CO':
+        
+        contract_info['price_spread'] = contract_info['exercise_price'] - etf_cls
+        
+    else:
+        
+        contract_info['price_spread'] = etf_cls - contract_info['exercise_price']
+        
+    if contract_info['price_spread'].max() > 0:
+        
+       contract_info = contract_info[contract_info['price_spread'] > 0]  #选出虚值期权
+       contract_info = contract_info.sort_values('exercise_price')
+        
+    else:  #全是实值期权
+        
+        contract_info = contract_info.sort_values('exercise_price',ascending=False)    
+        
+    return(contract_info)
+
+
+#获得指定行权日期以及价格的合约
+def getContractForExerciseDateAndPrice(exercisedate,exerciseprice,date,contratType='CO'):
+    
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                            opt.OPT_CONTRACT_INFO.trading_code,
+                            opt.OPT_CONTRACT_INFO.name, #合约代码,合约交易代码,合约简称
+                            opt.OPT_CONTRACT_INFO.exercise_price, 
+                            opt.OPT_CONTRACT_INFO.last_trade_date, 
+                            opt.OPT_CONTRACT_INFO.list_date
+         ).filter(opt.OPT_CONTRACT_INFO.contract_type == contratType,  #期权类型
+                  opt.OPT_CONTRACT_INFO.underlying_type == 'ETF',#ETF类型
+                  opt.OPT_CONTRACT_INFO.exchange_code == 'XSHG',   #上交所
+                  opt.OPT_CONTRACT_INFO.exercise_date == exercisedate, #指定行权日期
+                  opt.OPT_CONTRACT_INFO.exercise_price == exerciseprice, #指定行权价格
+                  opt.OPT_CONTRACT_INFO.last_trade_date >= date,
+                  opt.OPT_CONTRACT_INFO.list_date <= date
+                 )    #在交易前上市
+    
+    contract_info = opt.run_query(q_contract_info)
+    return(contract_info)
+
+#获得指定行权日期,以及最接近行权价格的合约
+def getContractForExerciseDateAndClosePrice(exercisedate,exerciseprice,date,contratType='CO'):
+    
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.code, 
+                            opt.OPT_CONTRACT_INFO.trading_code, 
+                            opt.OPT_CONTRACT_INFO.name, #合约代码,合约交易代码,合约简称
+                            opt.OPT_CONTRACT_INFO.exercise_price, 
+                            opt.OPT_CONTRACT_INFO.last_trade_date, 
+                            opt.OPT_CONTRACT_INFO.list_date
+                            
+         ).filter(opt.OPT_CONTRACT_INFO.contract_type == contratType,  #期权类型
+                  opt.OPT_CONTRACT_INFO.underlying_type == 'ETF',#ETF类型
+                  opt.OPT_CONTRACT_INFO.exchange_code == 'XSHG',   #上交所
+                  opt.OPT_CONTRACT_INFO.exercise_date == exercisedate, #指定行权日期
+                  #opt.OPT_CONTRACT_INFO.exercise_price == exerciseprice, #指定行权价格
+                  opt.OPT_CONTRACT_INFO.last_trade_date >= date,
+                  opt.OPT_CONTRACT_INFO.list_date <= date
+                 )    #在交易前上市
+    
+    contract_info = opt.run_query(q_contract_info)
+    
+    if not contract_info.empty:
+        
+        contract_info['difference'] = (contract_info['exercise_price'] - exerciseprice).abs()
+        closest_row = contract_info.loc[[contract_info['difference'].idxmin()]]
+        return(closest_row)
+    
+    else:
+        return(contract_info)
+
+
+#处理合约切换标记
+def contractChange(holding_contract):
+    
+    data2 = pd.DataFrame(holding_contract)
+    data2.columns = ['holding_contract']
+    data2 = data2.reindex(columns=['holding_contract','close','last_close'])
+
+    last_contract = holding_contract.iloc[0]   #记录上个持仓
+    
+    for i in range(0,len(data2.index)):
+        
+        t = data2.index[i]
+        if last_contract == data2.loc[t,'holding_contract']:  #期权未换仓
+            #收盘价
+            q_price = query(opt.OPT_DAILY_PRICE.code, 
+                            opt.OPT_DAILY_PRICE.date, opt.OPT_DAILY_PRICE.close,
+                     ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
+                              opt.OPT_DAILY_PRICE.date==t)
+            price = opt.run_query(q_price)['close'][0]
+            data2.loc[t,'close'] = price
+
+        else:
+            #收盘价,新
+            q_price = query(opt.OPT_DAILY_PRICE.code, opt.OPT_DAILY_PRICE.date, opt.OPT_DAILY_PRICE.close,
+                     ).filter(opt.OPT_DAILY_PRICE.code==data2.loc[t,'holding_contract'],
+                             opt.OPT_DAILY_PRICE.date==t)
+            price = opt.run_query(q_price)['close'][0]
+            data2.loc[t,'close'] = price
+            #收盘价,旧
+            q_price = query(opt.OPT_DAILY_PRICE.code, opt.OPT_DAILY_PRICE.date, opt.OPT_DAILY_PRICE.close,
+                     ).filter(opt.OPT_DAILY_PRICE.code==last_contract,
+                             opt.OPT_DAILY_PRICE.date==data2.index[i-1])
+            price = opt.run_query(q_price)['close'][0]
+            data2.loc[t,'last_close'] = price
+            last_contract = data2.loc[t,'holding_contract']
+            
+    return(data2)
+
+
+#平仓期权收益
+def optionProfit(data2,fee=5):
+    
+    #计算平仓期权的收益
+    opt_ret2 = pd.Series(0,index=data2.index)
+    pre_close2 = data2['close'].iloc[0]
+    for t in data2.index[1:]:
+        if data2.isna().loc[t,'last_close']:   #未换仓,last为空
+            opt_ret2[t] = -10000*(data2.loc[t,'close'] - pre_close2)
+        else:
+            opt_ret2[t] = -10000*(data2.loc[t,'last_close'] - pre_close2) - fee  #手续费
+        pre_close2 = data2.loc[t,'close']
+    return(opt_ret2)
+
+
+# 返回相对于当前月份,可以交易合约下,之前,或之后的合约,用来定位近月或远月合约,pos = 0 表示返回当月可以交易的合约;
+# pos=-1,返回当月之前一个周期的可交易合约;pos=1,返回当月之后一个周期的可交易合约
+def findContractsForMonth(symbol,date,pos=0):
+    
+    dominant = get_dominant_future(symbol, date=date)
+    contracts_list = get_future_contracts(symbol, date=date)
+    pattern = re.compile(r'\d+')
+    numbers_list = [pattern.findall(contract)[0] for contract in contracts_list]
+    sorted_combined = sorted(zip(contracts_list, numbers_list), key=lambda x: x[1])
+    sorted_contracts, sorted_B = map(list, zip(*sorted_combined))
+    num = sorted_contracts.index(dominant)
+    find_pos = num + pos
+    if find_pos<0:
+        find_pos = 0
+    elif find_pos >= len(contracts_list):
+        find_pos = len(contracts_list)
+    return(sorted_contracts[find_pos])
+
+#获取可用合约的行权日期,并按日期排序
+def getAvailableContractExerciseDate(date):
+    q_contract_info = query(opt.OPT_CONTRACT_INFO.underlying_type,
+                            opt.OPT_CONTRACT_INFO.code, 
+                            opt.OPT_CONTRACT_INFO.trading_code, 
+                            opt.OPT_CONTRACT_INFO.name, #合约代码,合约交易代码,合约简称
+                            opt.OPT_CONTRACT_INFO.exercise_price, 
+                            opt.OPT_CONTRACT_INFO.last_trade_date, 
+                            opt.OPT_CONTRACT_INFO.list_date,
+                            opt.OPT_CONTRACT_INFO.exercise_date                            
+         ).filter(opt.OPT_CONTRACT_INFO.contract_type == 'CO',  #期权类型
+                  opt.OPT_CONTRACT_INFO.underlying_type == 'ETF',
+                  opt.OPT_CONTRACT_INFO.exchange_code == 'XSHG',   #上交所
+                  opt.OPT_CONTRACT_INFO.last_trade_date >= date,  #时间-到期月结束
+                  opt.OPT_CONTRACT_INFO.list_date <= date)    #在交易前上市
+    
+    contract_info = opt.run_query(q_contract_info)
+    exercise_date = contract_info['exercise_date'].drop_duplicates()
+    exercise_date = exercise_date.sort_values(ascending=True)
+    
+    return(exercise_date)
+
+
+#获取交易时间和时间间隔(频率:月)
+#根据不同交易日分割月份
+#指定回测的起始时间
+starttime = '2021-01-01'
+endtime   = '2024-03-01'
+
+trade_days = pd.Series(index=jqdata.get_trade_days(starttime,endtime))
+trade_days.index = pd.to_datetime(trade_days.index)
+
+#获取可用合约
+avail_exercise_date = getAvailableContractExerciseDate(trade_days.index[0])
+close_date = avail_exercise_date.iloc[0]
+futher_date = avail_exercise_date.iloc[-1]
+
+##持仓情况
+holding_contract_close = pd.Series(index=trade_days.index)
+holding_contract_after = pd.Series(index=trade_days.index)
+
+#获取首个近月持仓认购合约
+contract_info_close = getContractForExerciseDate(close_date,trade_days.index[0],'CO')
+holding_contract_close[trade_days.index[0]] = contract_info_close['code'].iloc[0]
+
+#获取首个远月持仓认购合约
+contract_info_after = getContractForExerciseDateAndClosePrice(futher_date,contract_info_close['exercise_price'].iloc[0],trade_days.index[0],'CO')
+holding_contract_after[trade_days.index[0]] = contract_info_after['code'].iloc[0]
+
+#循环访问每一个交易日,判断交易情况
+#规则:同时卖出一份近月认购期权以及买入最接近行权价的远月认购,待行权价低于现价的95%时,平仓原期权合约,重新调整持仓;到期前1天移仓换月至次月合约
+pre_hold_close = holding_contract_close[0]
+pre_hold_after = holding_contract_after[0]
+error_date = []
+
+for i in range(1,len(trade_days)):
+    
+    #print(i)
+    pre_day = trade_days.index[i-1]
+    cur_day = trade_days.index[i]
+    expire_close = getContractForCode(pre_hold_close)
+    expire_close = expire_close['last_trade_date'].iloc[0]
+    expire_after = getContractForCode(pre_hold_after)
+    expire_after = expire_after['last_trade_date'].iloc[0]
+    pre_cls = get_price('510050.XSHG',cur_day,cur_day,fields=['pre_close']).values[0][0]
+    newest_exercise_price = getContractForCode(pre_hold_close)
+    newest_exercise_price = newest_exercise_price['exercise_price'].iloc[0]
+    
+    if expire_close < cur_day.date() or expire_after < cur_day.date() or pre_cls*0.95 >= newest_exercise_price: #主力合约切换或者合约到期或者原虚值变为实值,重新开仓略虚值期权
+        avail_exercise_date = getAvailableContractExerciseDate(cur_day)
+        close_date = avail_exercise_date.iloc[0]
+        futher_date = avail_exercise_date.iloc[-2]
+        contract_info_close = getContractForExerciseDate(close_date,cur_day,'CO')
+        holding_contract_close[cur_day] = contract_info_close['code'].iloc[0]
+        contract_info_after = getContractForExerciseDateAndClosePrice(futher_date,contract_info_close['exercise_price'].iloc[0],cur_day,'CO')
+        if contract_info_after.empty: #没有匹配的远期合约,则该日清仓
+            error_date.append(cur_day)
+            continue
+        holding_contract_after[cur_day] = contract_info_after['code'].iloc[0]
+    else:
+        holding_contract_close[cur_day] = pre_hold_close
+        holding_contract_after[cur_day] = pre_hold_after
+            
+
+holding_contract_close = holding_contract_close.fillna(method='ffill')
+holding_contract_after = holding_contract_after.fillna(method='ffill')
+
+holding_contract_close = holding_contract_close.drop(error_date)
+holding_contract_after = holding_contract_after.drop(error_date)
+
+data_close = contractChange(holding_contract_close)
+data_after = contractChange(holding_contract_after)
+
+#计算期权的收益
+opt_ret_close = optionProfit(data_close)
+opt_ret_after = optionProfit(data_after)
+
+opt_ret = opt_ret_close + opt_ret_after
+opt_ret
+
+#计算持仓收益
+etf_price = get_price('510050.XSHG',trade_days.index[0],trade_days.index[-1],fields=['close'])['close']
+etf_ret = 10000*etf_price.diff(1).fillna(0)
+etf_ret
+
+#计算净值
+init_asset2 = etf_price.iloc[0]*10000
+ass2 = init_asset2 + (etf_ret + opt_ret).cumsum()
+pfl_ret2 = (ass2/ass2.shift(1) - 1).fillna(0)
+pfl_nv2 = (1 + pfl_ret2).cumprod()
+pfl_nv2
+
+#绘制净值图
+plt.figure(figsize=(30,20))
+plt.plot(etf_price/etf_price.iloc[0], label='50ETF现货净值')
+plt.plot(pfl_nv2, label='买入日历价差策略净值')
+plt.legend(loc='upper left', fontsize='large')
+plt.xlabel('时间',size=12)
+plt.ylabel('净值',size=12)
+plt.show()

+ 354 - 0
Lib/Options/README.md

@@ -0,0 +1,354 @@
+# Options Trading Strategies
+
+## 0. BS_WhalleyWilmott.py - Whalley-Wilmott 期权对冲策略
+
+### 策略概述
+这是一个基于 Black-Scholes 模型的场外期权对冲策略,采用 Whalley-Wilmott 阈值方法进行动态对冲。该策略通过计算期权的 Delta 值并根据阈值条件进行股票仓位调整,以对冲期权风险。
+
+### 核心交易逻辑
+
+#### 1. 初始化设置
+- **标的证券**: 默认为 '002724.XSHE'
+- **执行价格倍数**: K=1 (平价期权)
+- **合约期限**: T=30天
+- **无风险利率**: rf=0.09 (9%)
+- **波动率**: 基于过去365天的历史数据计算
+- **名义本金**: 账户现金的95.24% (1/1.05)
+
+#### 2. 对冲触发机制
+策略采用 Whalley-Wilmott 阈值方法,只有当 Delta 变化超过特定阈值时才进行对冲交易:
+
+**阈值计算公式**:
+```
+wwt = (3/2 * a / risk_tolerance)^(1/3)
+其中: a = exp(-rf*τ/365) * trading_cost * S * gamma^2
+```
+
+**参数设置**:
+- 风险容忍度 (risk_tolerance): 5
+- 交易成本 (trading_cost): 0.00055 (0.055%)
+
+#### 3. 交易时间安排
+
+**第一天 (建仓日)**:
+- 计算初始 Delta 值
+- 建立对应的股票仓位: `仓位 = (名义本金/期初价格) * Delta`
+- 仓位调整为100股的整数倍
+
+**中间交易日**:
+- 跳过开盘第一分钟 (9:30) 避免价格异常
+- 实时监控当前 Delta 与上次 Delta 的差异
+- 当差异超过 Whalley-Wilmott 阈值时,调整股票仓位
+- 处理分红拆股事件,相应调整期初价格
+
+**到期日**:
+- 清空所有股票仓位
+
+#### 4. 关键计算函数
+
+**Delta 计算**:
+```python
+d1 = (ln(S/(S0*K)) + (rf + σ²/2)*(τ/365)) / (σ*√(τ/365))
+delta = N(d1)  # 标准正态分布累积函数
+```
+
+**Gamma 计算**:
+```python
+gamma = φ(d1) / (S * σ * √(τ/365))  # φ为标准正态分布密度函数
+```
+
+**波动率计算**:
+- 使用过去365天的日收盘价
+- 计算对数收益率的标准差
+- 年化处理 (乘以√250)
+
+#### 5. 风险管理特点
+
+1. **动态对冲**: 不是连续对冲,而是基于阈值的离散对冲,降低交易成本
+2. **成本优化**: Whalley-Wilmott 方法在对冲效果和交易成本之间找到最优平衡
+3. **事件处理**: 自动处理分红拆股等公司行为对期权参数的影响
+4. **仓位管理**: 仓位调整为100股整数倍,符合实际交易要求
+
+#### 6. 适用场景
+- 场外期权做市商的风险对冲
+- 结构化产品的 Delta 中性策略
+- 期权组合的动态风险管理
+
+#### 7. 注意事项
+- 策略假设期权为欧式期权,到期前不会被提前行权
+- 波动率使用历史波动率,可能与隐含波动率存在差异
+- 交易成本设置需要根据实际券商费率调整
+- 适合流动性较好的标的证券
+
+### 参考资料
+- 原始策略来源: 聚宽文章 "场外期权对冲策略回测框架-(以Whally-Wilmott为例)"
+- 作者: 颖硕
+- 链接: https://www.joinquant.com/post/14348
+
+# 加百利分享
+
+## 1. 聚宽平台期权数据获取与绘图
+
+### 核心功能:
+- **数据获取**: 从聚宽平台获取50ETF期权的历史交易数据
+- **数据分析**: 分别统计认购期权(Call)和认沽期权(Put)的成交量和成交金额
+- **可视化展示**: 绘制认购/认沽期权的成交量和成交金额对比图表
+- **应用场景**: 期权市场情绪分析、Put/Call比率研究、市场活跃度监控
+
+### 参考资料:
+- [原始策略来源](https://www.joinquant.com/view/community/detail/aa77127d7eccdaa699de7e87977f35dc)
+- [研究网址](https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E6%9C%9F%E6%9D%83%E6%95%B0%E6%8D%AE%E8%8E%B7%E5%8F%96%E4%B8%8E%E7%BB%98%E5%9B%BE.ipynb)
+
+## 2. 获取期权数据,列出符合要求的合约
+
+### 核心功能:
+- **合约筛选**: 根据到期日、行权价、期权类型等条件筛选期权合约
+- **数据查询**: 使用聚宽期权数据库查询符合条件的期权合约信息
+- **信息展示**: 列出合约代码、交易代码、行权价、到期日等关键信息
+- **应用场景**: 期权策略构建前的合约选择、期权链分析
+
+### 参考资料:
+- [原始策略来源](https://www.joinquant.com/view/community/detail/a8f4ad443448f4246260ea221c3d77ea)
+- [研究网址](https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E8%8E%B7%E5%8F%96%E6%9C%9F%E6%9D%83%E6%95%B0%E6%8D%AE%EF%BC%8C%E5%88%97%E5%87%BA%E7%AC%A6%E5%90%88%E8%A6%81%E6%B1%82%E7%9A%84%E5%90%88%E7%BA%A6.ipynb)
+
+## 3. 绘制期权损益分析图
+
+### 核心功能:
+- **损益计算**: 计算不同期权策略在不同标的价格下的损益情况
+- **图表绘制**: 绘制期权策略的损益曲线图(Payoff Diagram)
+- **盈亏分析**: 标识盈亏平衡点、最大盈利/亏损点
+- **应用场景**: 期权策略风险收益分析、策略比较、投资决策支持
+
+### 参考资料:
+- [原始策略来源](https://www.joinquant.com/view/community/detail/ed821dbf617e69a9e9568b4b34bae458)
+- [研究网址](https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E7%BB%98%E5%88%B6%E6%9C%9F%E6%9D%83%E6%8D%9F%E7%9B%8A%E5%88%86%E6%9E%90%E5%9B%BE.ipynb)
+
+## 4. 股指ETF期权T型报价
+
+### 核心功能:
+- **T型报价**: 以T型表格形式展示50ETF期权的买卖报价
+- **实时数据**: 获取期权的最新买一价、卖一价、成交价等信息
+- **分类展示**: 按行权价分类,同时显示认购和认沽期权报价
+- **应用场景**: 期权交易决策、价差分析、流动性评估
+
+### 参考资料:
+- [原始策略来源](https://www.joinquant.com/view/community/detail/a5a968ed72f2b827c051d337b0d74d04)
+- [研究网址](https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E8%82%A1%E6%8C%87ETF%E6%9C%9F%E6%9D%83T%E5%9E%8B%E6%8A%A5%E4%BB%B7.ipynb)
+
+## 5. 商品期权T型报价代码
+
+### 核心功能:
+- **商品期权报价**: 获取商品期权(如豆粕、白糖等)的T型报价表
+- **多品种支持**: 支持不同商品期权品种的报价查询
+- **价格展示**: 显示期权的理论价值、实际报价、隐含波动率等
+- **应用场景**: 商品期权交易、套利机会识别、波动率分析
+
+### 参考资料:
+- [原始策略来源](https://www.joinquant.com/view/community/detail/625edad0050315dcc2df540cd462df60)
+- [研究网址](https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E5%95%86%E5%93%81%E6%9C%9F%E6%9D%83T%E5%9E%8B%E6%8A%A5%E4%BB%B7.ipynb)
+
+## 6. 50ETF-期权-备兑认购策略
+
+### 核心交易逻辑:
+- **持仓构建**: 持有50ETF现货 + 卖出虚值认购期权
+- **合约选择**: 优先选择略虚值的认购期权(行权价>现价)
+- **调仓规则**: 当行权价低于现价95%时平仓原期权,重新开仓虚值期权
+- **到期处理**: 到期前1天移仓换月至次月合约
+- **收益来源**: 赚取期权权利金 + 标的资产增值(有上限)
+
+### 参考资料:
+- [原始策略来源](https://www.joinquant.com/view/community/detail/2b40f724dcea54aaa06419a46517f3db)
+- [研究网址](https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/50ETF-%E6%9C%9F%E6%9D%83-%E5%A4%87%E5%85%91%E7%9C%8B%E6%B6%A8%E7%AD%96%E7%95%A5.ipynb)
+
+## 7. 50ETF-备兑认购策略-改进版
+
+### 核心交易逻辑:
+- **动态调整**: 在传统备兑策略基础上增加动态调仓机制
+- **风险控制**: 设置止损条件,当亏损达到一定比例时主动平仓
+- **收益优化**: 根据市场波动率调整期权选择标准
+- **时间管理**: 优化到期日管理,避免临近到期的时间价值损失
+- **适应性强**: 能够适应不同市场环境下的波动特征
+
+### 参考资料:
+- [原始策略来源](https://www.joinquant.com/view/community/detail/22955d161ec0a36d836a0e4f13fe66d7)
+- [研究网址](https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/50ETF-%E5%A4%87%E5%85%91%E7%9C%8B%E6%B6%A8%E7%AD%96%E7%95%A5-%E6%94%B9%E8%BF%9B%E7%89%88.ipynb)
+
+## 8. 豆粕-备兑认购策略
+
+### 核心交易逻辑:
+- **商品应用**: 将备兑认购策略应用于豆粕期货市场
+- **持仓结构**: 持有豆粕期货多头 + 卖出豆粕认购期权
+- **合约管理**: 跟踪豆粕主力合约变化,及时调整持仓
+- **季节性考虑**: 结合豆粕的季节性供需特点调整策略参数
+- **风险特点**: 商品期货波动率通常高于股指,需要更严格的风险控制
+
+### 参考资料:
+- [原始策略来源](https://www.joinquant.com/view/community/detail/4bf820b677d7d774f54c122460533b2e)
+- [研究网址](https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E8%B1%86%E7%B2%95-%E5%A4%87%E5%85%91%E7%9C%8B%E6%B6%A8%E7%AD%96%E7%95%A5.ipynb)
+
+## 9. 商品主力合约-备兑认购策略
+
+### 核心交易逻辑:
+- **多品种适用**: 适用于各种商品期货的备兑认购策略框架
+- **主力合约跟踪**: 自动识别和切换到成交量最大的主力合约
+- **动态对冲**: 根据商品期货的高波动特性调整对冲频率
+- **保证金管理**: 考虑期货保证金制度,优化资金使用效率
+- **品种轮动**: 可在不同商品间轮动,寻找最佳投资机会
+
+### 参考资料:
+- [原始策略来源](https://www.joinquant.com/view/community/detail/e306e04ca7a0c557f759487e8d252c65)
+- [研究网址](https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E5%95%86%E5%93%81%E4%B8%BB%E5%8A%9B%E5%90%88%E7%BA%A6-%E5%A4%87%E5%85%91%E7%9C%8B%E6%B6%A8%E7%AD%96%E7%95%A5.ipynb)
+
+## 10. 商品主力合约-备兑认沽策略
+
+### 核心交易逻辑:
+- **反向策略**: 持有现金 + 卖出虚值认沽期权,等待标的下跌时以折价买入
+- **现金管理**: 预留足够现金以备行权时购买标的资产
+- **跌幅获利**: 在标的价格下跌过程中赚取期权权利金
+- **底部建仓**: 通过行权在相对低位建立多头仓位
+- **适用环境**: 适合在预期标的将在区间震荡或温和下跌时使用
+
+### 参考资料:
+- [原始策略来源](https://www.joinquant.com/view/community/detail/d4272cd0fac2981438b0f3f410bf6180)
+- [研究网址](https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E5%95%86%E5%93%81%E4%B8%BB%E5%8A%9B%E5%90%88%E7%BA%A6-%E5%A4%87%E5%85%91%E7%9C%8B%E8%B7%8C%E7%AD%96%E7%95%A5.ipynb)
+
+## 11. 领口认购策略-商品主力合约
+
+### 核心交易逻辑:
+- **三腿组合**: 持有标的多头 + 卖出虚值认购期权 + 买入虚值认沽期权
+- **风险限制**: 通过买入认沽期权为下跌风险设置保护下限
+- **收益优化**: 卖出认购期权降低保护成本,形成有限收益区间
+- **成本控制**: 认沽期权保护费用部分由认购期权权利金抵消
+- **适用场景**: 温和认购市场,既要保护下跌风险又要控制保护成本
+
+### 参考资料:
+- [原始策略来源](https://www.joinquant.com/view/community/detail/bec4688fd7998652edfd929a4ef1f4df)
+- [研究网址](https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E9%A2%86%E5%8F%A3%E7%9C%8B%E6%B6%A8%E7%AD%96%E7%95%A5-%E5%95%86%E5%93%81%E4%B8%BB%E5%8A%9B%E5%90%88%E7%BA%A6.ipynb)
+
+## 12. 领口认购策略-50ETF
+
+### 核心交易逻辑:
+- **ETF应用**: 将领口策略应用于50ETF,利用其高流动性优势
+- **精确定价**: 利用50ETF期权的活跃交易获得更精确的期权定价
+- **灵活调整**: 可根据市场情况灵活调整上下限保护区间
+- **成本效益**: 在50ETF相对稳定的波动环境下优化成本收益比
+- **风险管理**: 为50ETF投资组合提供有效的风险管理工具
+
+### 参考资料:
+- [原始策略来源](https://www.joinquant.com/view/community/detail/a99630091b24414da149e715ae6186f2)
+- [研究网址](https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E9%A2%86%E5%8F%A3%E7%9C%8B%E6%B6%A8%E7%AD%96%E7%95%A5-50ETF.ipynb)
+
+## 13. 卖出跨式策略-50ETF
+
+### 核心交易逻辑:
+- **双向卖出**: 同时卖出相同行权价的认购和认沽期权
+- **波动率交易**: 赚取时间价值衰减,适合低波动率环境
+- **区间获利**: 当标的价格在一定区间内震荡时获得最大收益
+- **风险特征**: 潜在亏损无限,需要严格的风险控制
+- **最佳环境**: 预期标的将在当前价格附近窄幅震荡
+
+### 参考资料:
+- [原始策略来源](https://www.joinquant.com/view/community/detail/73a7f044b73242b136c8c840ef7f1748)
+- [研究网址](https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E5%8D%96%E5%87%BA%E8%B7%A8%E5%BC%8F%E7%AD%96%E7%95%A5-50ETF.ipynb)
+
+## 14. 卖出跨式策略-商品主力合约
+
+### 核心交易逻辑:
+- **商品适配**: 将跨式策略应用于波动性更高的商品期货市场
+- **波动率管理**: 需要更谨慎地评估商品期货的波动率水平
+- **保证金考虑**: 商品期货的保证金制度影响策略的资金效率
+- **季节性因素**: 考虑商品的季节性供需变化对波动率的影响
+- **风险加大**: 商品期货的高波动性使得风险控制更加重要
+
+### 参考资料:
+- [原始策略来源](https://www.joinquant.com/view/community/detail/dc14876fee244d726f18c652eb44c7d7)
+- [研究网址](https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E5%8D%96%E5%87%BA%E8%B7%A8%E5%BC%8F%E7%AD%96%E7%95%A5-%E5%95%86%E5%93%81%E4%B8%BB%E5%8A%9B%E5%90%88%E7%BA%A6.ipynb)
+
+## 15. 买入日历价差策略-商品期货
+
+### 核心交易逻辑:
+- **时间价差**: 卖出近月期权 + 买入远月期权,利用时间价值衰减差异
+- **波动率套利**: 利用不同到期日期权的隐含波动率差异获利
+- **有限风险**: 最大亏损限于支付的净权利金
+- **最佳时机**: 适合在预期标的价格将在短期内保持相对稳定时使用
+- **到期管理**: 需要在近月期权到期前主动管理仓位
+
+### 参考资料:
+- [原始策略来源](https://www.joinquant.com/view/community/detail/af07500292294804acd19f7f0f5b23e4)
+- [研究网址](https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E4%B9%B0%E5%85%A5%E6%97%A5%E5%8E%86%E4%BB%B7%E5%B7%AE%E7%AD%96%E7%95%A5-%E5%95%86%E5%93%81%E6%9C%9F%E8%B4%A7.ipynb)
+
+## 16. 买入日历价差策略-50ETF
+
+### 核心交易逻辑:
+- **ETF优势**: 利用50ETF期权链的完整性和流动性优势
+- **精细操作**: 可以更精确地选择最优的时间价差组合
+- **成本控制**: 在相对稳定的ETF环境下控制策略成本
+- **收益稳定**: 适合追求稳定收益的投资者
+- **风险可控**: 最大风险明确,适合风险偏好较低的投资者
+
+### 参考资料:
+- [原始策略来源](https://www.joinquant.com/view/community/detail/0d2479d4f374fce4a4b900f2c77d3ba3)
+- [研究网址](https://www.joinquant.com/research?target=research&url=/user/75474983526/notebooks/Options/%E4%B9%B0%E5%85%A5%E6%97%A5%E5%8E%86%E4%BB%B7%E5%B7%AE%E7%AD%96%E7%95%A5-50ETF.ipynb)
+
+# 书生分享
+
+## 1. 卖沽ETF,卖购备兑策略
+
+## 1. 深度实值买购和卖购组合的牛差策略
+
+### 核心交易逻辑:
+
+1. 虽然是深度实值和卖购的组合,但是深度实值的本质是替代持仓ETF。所以在资金收益计算的时候要分成两个账户,一个是深度实值买购相关交易的收益情况,另一个则是卖购的收益情况。
+2. 开仓选择:
+    - 卖购标的选择:
+        - 做卖购的一般是平值,也就是如果标的ETF是2.828,那么2.85和2.8可以算是最接近的。
+        - 这时候则要看他们的权利金,2.85对应的是0.423,2。8对应的是0.179,我们选择权利金大的也就是2.85
+        - 一般选择都是优先考虑下个月的,其次就是考虑权利金,太低的不要。(???阈值)
+    - 买购标的的选择:
+        - 做买购的一般是深度实值,也就是时间价值非常小,也就是小于等于阈值(阈值暂定0.0001)
+        - 月份和卖购标的一致
+3. 开仓后记录:下面公式是帮助理解,实际记录只需要记录结果
+    - 标的名称 (标的编号):300ETF (510300)
+    - 开仓时价格:4.032
+    - 买购标的及价格:3.7, 0.3327
+    - 卖购标的及价格:4.0, 0.0614
+    - 张数:80
+    - 单张最大盈利:$(4.0-3.7-0.3327+0.0614)*10000=287$
+    - 最小盈利(这里重点记录卖购):$0.0614*10000=614$
+4. 平仓选择:
+    - ETF 大涨,牛差组合接近到期最大盈利:
+        - 全部平仓,向上进行移仓
+        - 平掉当前的卖购和买购组合(买入行权价K1+卖出行权价K2)
+        - 开仓一个更高行权价的卖购和买购牛差组合(买入行权价K3+卖出行权价 K4,其中 K3>K1;K4>K2)
+        - 月份选择:如果距离当前月合约结束还有n天及以上的时间(阈值暂定20)还可以选择当月,这里同时检查行权价是否满足条件;否则就选择下个月
+    - ETF下跌:
+        - 当ETF下跌时,且卖购权利金剩余低于50,则平仓当前组合
+        - 重新按照原始标准选择下个月的合约开仓
+    - 合约快到期了:
+        - 合约到期7个交易日内就开始考虑平仓,最晚提前2个交易日
+        - 准备开下个月的新仓
+5. 加仓选择:
+    - 假设当前开仓的是4.0左右的情况,做了一组牛差,等到跌到3.8 (这里窗口阈值设定为0.2)的时候会再开仓一组牛差。
+    - 这里的重点是记录开额外组牛差的ETF标的价格是多少,等到市场重新涨到这个价格之后,这组额外的牛差可以在牛差最大盈利或者卖购盈利基本达成的时候进行平仓,也就是这一组不再会进行开仓。
+6. 设定阈值:
+    - 一组张数:30
+    - 最小权利金:`{'沪深300ETF':0.03, '上证50ETF':0.05}`
+    - 最少开仓日期(距离到期日):20天(非交易日)
+    - 买购时间价值阈值:0.0001
+    - 卖购平仓权利金阈值:0.0005
+    - 合约到期移仓日期最大:7天(交易日)
+    - 合约到期移仓日期最小:2天(交易日)
+    - 加仓窗口阈值:`{'沪深300ETF':0.2, '上证50ETF':0.1}`
+    - 加仓次数上限:2
+
+
+
+
+
+
+
+
+
+
+
+

+ 195 - 0
Lib/Options/README_STRATEGY_TEST.md

@@ -0,0 +1,195 @@
+# 深度实值牛差策略测试工具使用说明
+
+## 概述
+
+基于您在 `Lib/Options/README.md` 中描述的深度实值买购和卖购组合的牛差策略,我已经创建了完整的整合测试工具。
+
+## 🎯 核心特性
+
+### ✅ 已完成的功能整合
+
+1. **策略核心逻辑** - 深度实值买购 + 平值卖购组合
+2. **数据导出功能** - 支持CSV格式导出,便于线下分析
+3. **分项收益分析** - 分别分析买购和卖购的收益情况
+4. **期权分析工具** - 内置期权损益分析和可视化
+5. **收益对比分析** - 与ETF持有收益的详细对比
+
+### 📁 文件说明
+
+- **`deep_itm_bull_spread_strategy.py`** - 🌟 **主文件**(所有功能已整合)
+- **`test_integrated_strategy.py`** - 本地功能验证测试
+- **`README_STRATEGY_TEST.md`** - 本使用说明文档
+
+## 策略核心逻辑
+
+根据您的策略描述,实现了以下核心交易逻辑:
+
+### 开仓条件
+1. **买购选择**:深度实值认购期权(时间价值 ≤ 0.0001)
+2. **卖购选择**:平值认购期权(权利金 ≥ 最小阈值)
+3. **张数配置**:每组30张(可配置)
+
+### 平仓条件
+1. **到期移仓**:距离到期7-2个交易日内
+2. **ETF大涨**:接近最大盈利时
+3. **ETF下跌**:卖购权利金 ≤ 0.0005时
+
+### 加仓逻辑
+- ETF下跌超过阈值时(沪深300ETF: 0.2,上证50ETF: 0.1)
+- 避免在相同价格水平重复加仓
+
+## 🚀 使用方法
+
+### 1. 线上环境(聚宽平台)- 推荐
+
+**步骤**:
+1. 将 `deep_itm_bull_spread_strategy.py` 完整复制到聚宽平台
+2. 在聚宽研究环境中运行以下代码:
+
+```python
+# 运行完整回测和分析
+strategy, results = test_strategy()
+
+# 导出数据到CSV(用于线下分析)
+positions_df, trades_df, daily_df = strategy.export_data_to_csv("bull_spread_300etf")
+
+# 分析期权组合
+option_analysis = analyze_bull_spread_example()
+
+# 收益对比分析
+comparison_results = compare_with_etf_holding()
+```
+
+**输出文件**:
+- `bull_spread_300etf_positions.csv` - 持仓数据
+- `bull_spread_300etf_trades.csv` - 交易记录
+- `bull_spread_300etf_daily.csv` - 每日损益(包含买购/卖购分项)
+
+### 2. 本地测试验证
+
+运行功能验证测试:
+
+```bash
+cd /path/to/jukuan
+python Lib/Options/test_integrated_strategy.py
+```
+
+**测试内容**:
+- ✅ 期权分析工具
+- ✅ 收益对比分析
+- ✅ 数据导出功能
+
+## 📊 分项收益分析特色
+
+### 🔍 买购和卖购分别分析
+
+本工具的核心特色是**分别分析买购和卖购的收益情况**,而不是仅看组合总收益:
+
+**测试结果示例**(以4.0价位为例):
+
+| 价格变化 | ETF收益 | 买购收益 | 卖购收益 | 牛差总收益 | 收益差异 |
+|---------|---------|----------|----------|------------|----------|
+| -20%    | -20,000元 | -180,400元 | +82,000元 | -98,400元 | -78,400元 |
+| -5%     | -5,000元  | -98,400元  | +82,000元 | -16,400元 | -11,400元 |
+| 0%      | 0元       | -16,400元  | +82,000元 | +65,600元 | +65,600元 |
+| +5%     | 5,000元   | +65,600元  | 0元       | +65,600元 | +60,600元 |
+| +20%    | 20,000元  | +311,600元 | -246,000元| +65,600元 | +45,600元 |
+
+### 📈 关键洞察
+
+1. **买购期权**:
+   - 标的下跌时损失较大(深度实值变为平值/虚值)
+   - 标的上涨时收益显著(杠杆效应)
+
+2. **卖购期权**:
+   - 标的下跌时提供稳定收益(权利金收入)
+   - 标的上涨时限制总收益(被行权)
+
+3. **组合效果**:
+   - 在标的横盘或温和上涨时表现最佳
+   - 通过卖购收入降低买购成本
+
+### 🎯 策略适用性
+
+**最佳表现区间**:标的价格 0% 到 +10% 变化
+**风险区间**:标的价格 -10% 以下变化
+**收益受限区间**:标的价格 +15% 以上变化
+
+## 参数配置
+
+### 策略参数(可在代码中调整)
+
+```python
+params = {
+    '一组张数': 30,
+    '最小权利金': {'沪深300ETF': 0.03, '上证50ETF': 0.05},
+    '最少开仓日期': 20,  # 距离到期日天数
+    '买购时间价值阈值': 0.0001,
+    '卖购平仓权利金阈值': 0.0005,
+    '合约到期移仓日期最大': 7,
+    '合约到期移仓日期最小': 2,
+    '加仓窗口阈值': {'沪深300ETF': 0.2, '上证50ETF': 0.1}
+}
+```
+
+### 测试参数
+
+- **标的**:300ETF (510300.XSHG)
+- **测试期间**:2024-01-01 到 2025-06-30
+- **初始资金**:100,000元
+
+## 风险提示
+
+1. **模拟环境限制**:简化版使用模拟期权定价,实际表现可能不同
+2. **交易成本**:未充分考虑手续费、滑点等交易成本
+3. **流动性风险**:深度实值期权可能存在流动性不足问题
+4. **模型风险**:期权定价模型的准确性影响策略表现
+
+## 🔧 主要功能类和方法
+
+### DeepITMBullSpreadStrategy 类
+
+**核心方法**:
+- `run_backtest()` - 运行完整回测
+- `generate_detailed_report()` - 生成分项收益分析报告
+- `export_data_to_csv(filename_prefix)` - 导出数据到CSV
+- `plot_detailed_performance()` - 绘制分项收益图表
+
+### OptionsAnalyzer 类
+
+**核心方法**:
+- `analyze_options(*options)` - 期权组合分析
+- 支持格式:`('buy', 'call', premium, strike, quantity)`
+
+### 便捷函数
+
+- `test_strategy()` - 一键运行完整测试
+- `analyze_bull_spread_example()` - 期权组合分析示例
+- `compare_with_etf_holding()` - 收益对比分析
+
+## 📋 使用流程建议
+
+### 线上环境工作流
+
+1. **上传文件**:将 `deep_itm_bull_spread_strategy.py` 上传到聚宽
+2. **运行回测**:执行 `test_strategy()` 获取策略表现
+3. **导出数据**:使用 `export_data_to_csv()` 导出详细数据
+4. **下载分析**:下载CSV文件到本地进行深度分析
+
+### 线下分析工作流
+
+1. **数据准备**:使用导出的CSV文件
+2. **分项分析**:重点关注买购和卖购的分别表现
+3. **参数优化**:基于分析结果调整策略参数
+4. **风险评估**:评估不同市场环境下的表现
+
+## ⚠️ 重要提示
+
+1. **数据完整性**:线上环境使用真实期权数据,线下测试使用模拟数据
+2. **交易成本**:实际交易需考虑手续费、滑点等成本
+3. **流动性风险**:深度实值期权可能存在流动性问题
+4. **风险管理**:建议添加止损和仓位管理机制
+
+---
+
+**🎯 核心优势**:本工具实现了您要求的所有功能整合,特别是买购和卖购的分项收益分析,为策略优化提供了详细的数据支持。

binární
Lib/Options/__pycache__/analysis_chart.cpython-310.pyc


+ 60 - 5
Lib/Options/analysis_chart.ipynb

@@ -58,7 +58,7 @@
     "            \n",
     "            v = [(price * count) for i in np.arange(start, X - gap * 0.1, gap)]\n",
     "            for i in np.arange(X, end - gap * 0.1, gap):\n",
-    "                v.append(-(i - X - price) * count)\n",
+    "                v.append((price - (i - X)) * count)\n",
     "                \n",
     "        elif opt_info == ['sell', 'put']:  # 卖出看跌期权\n",
     "            \n",
@@ -169,11 +169,66 @@
     "    print(f\"  opt1在Y轴的值: {opt1_y_value:.4f}\")\n",
     "    print(f\"  opt2在Y轴的值: {opt2_y_value:.4f}\")\n",
     "    print(f\"  sum在Y轴的值: {sum_y_value:.4f}\")\n",
-    "    print(f\"sum曲线最小值: {min_sum:.4f},一单最大亏损为: {min_sum*10000:.2f}\")\n",
+    "    \n",
+    "    # 计算最大收益和最大亏损\n",
+    "    max_sum = gainloss_df['sum'].max()\n",
+    "    max_index = gainloss_df['sum'].idxmax()\n",
+    "    \n",
+    "    print(f\"sum曲线最小值: {min_sum:.4f},一单最大亏损为: {abs(min_sum)*10000:.2f}\")\n",
     "    print(f\"最小值对应的标的价格: {min_index:.4f}\")\n",
-    "    print(f\"sum在大于{sum_crossings[0]:.4f}或者小于{sum_crossings[1]:.4f}才会盈利\")\n",
-    "    print(f\"当价格小于{sum_crossings[0]:.4f},收益为: {sum_y_value*10000:.2f}\")\n",
-    "    print(f\"当价格大于{sum_crossings[1]:.4f}收益没有上限\")"
+    "    print(f\"sum曲线最大值: {max_sum:.4f},一单最大收益为: {max_sum*10000:.2f}\")\n",
+    "    print(f\"最大值对应的标的价格: {max_index:.4f}\")\n",
+    "    \n",
+    "    # 盈亏平衡点分析\n",
+    "    if len(sum_crossings) == 0:\n",
+    "        print(\"该组合在当前价格区间内没有盈亏平衡点\")\n",
+    "        if sum_y_value > 0:\n",
+    "            print(\"该组合在整个价格区间内都盈利\")\n",
+    "        else:\n",
+    "            print(\"该组合在整个价格区间内都亏损\")\n",
+    "    elif len(sum_crossings) == 1:\n",
+    "        print(f\"盈亏平衡点: {sum_crossings[0]:.4f}\")\n",
+    "        if sum_y_value > 0:\n",
+    "            print(f\"当价格小于{sum_crossings[0]:.4f}时盈利,大于时亏损\")\n",
+    "        else:\n",
+    "            print(f\"当价格小于{sum_crossings[0]:.4f}时亏损,大于时盈利\")\n",
+    "    elif len(sum_crossings) >= 2:\n",
+    "        print(f\"盈亏平衡点: {sum_crossings[0]:.4f} 和 {sum_crossings[1]:.4f}\")\n",
+    "        print(f\"当价格小于{sum_crossings[0]:.4f}或大于{sum_crossings[1]:.4f}时才会盈利\")\n",
+    "        print(f\"当价格在{sum_crossings[0]:.4f}到{sum_crossings[1]:.4f}之间时亏损\")\n",
+    "    \n",
+    "    # 期权组合类型分析\n",
+    "    print(\"\\n=== 期权组合分析 ===\")\n",
+    "    opt1_info = opt_df.loc[['direction', 'catagory'], 'opt1'].tolist()\n",
+    "    opt2_info = opt_df.loc[['direction', 'catagory'], 'opt2'].tolist()\n",
+    "    \n",
+    "    if (opt1_info == ['sell', 'call'] and opt2_info == ['buy', 'call'] and \n",
+    "        opt_df.loc['X', 'opt1'] > opt_df.loc['X', 'opt2']):\n",
+    "        print(\"这是一个熊市看涨期权价差(Bear Call Spread)组合\")\n",
+    "        print(\"注意:这是一个看跌策略,当标的价格下跌时盈利\")\n",
+    "        net_premium = opt_df.loc['price', 'opt1'] - opt_df.loc['price', 'opt2']\n",
+    "        strike_diff = opt_df.loc['X', 'opt1'] - opt_df.loc['X', 'opt2']\n",
+    "        theoretical_max_profit = net_premium  # 最大收益就是净权利金收入\n",
+    "        theoretical_max_loss = strike_diff - net_premium  # 最大亏损\n",
+    "        print(f\"理论最大收益: {theoretical_max_profit:.4f} ({theoretical_max_profit*10000:.2f}元)\")\n",
+    "        print(f\"理论最大亏损: {theoretical_max_loss:.4f} ({theoretical_max_loss*10000:.2f}元)\")\n",
+    "        print(f\"净权利金收入: {net_premium:.4f}\")\n",
+    "        print(f\"执行价差: {strike_diff:.4f}\")\n",
+    "        print(f\"盈亏平衡点: {opt_df.loc['X', 'opt2'] + net_premium:.4f}\")\n",
+    "        print(\"当标的价格低于盈亏平衡点时盈利,高于时亏损\")\n",
+    "    elif (opt1_info == ['buy', 'call'] and opt2_info == ['sell', 'call'] and \n",
+    "          opt_df.loc['X', 'opt1'] < opt_df.loc['X', 'opt2']):\n",
+    "        print(\"这是一个牛市看涨期权价差(Bull Call Spread)组合\")\n",
+    "        net_premium = opt_df.loc['price', 'opt2'] - opt_df.loc['price', 'opt1']\n",
+    "        strike_diff = opt_df.loc['X', 'opt2'] - opt_df.loc['X', 'opt1']\n",
+    "        theoretical_max_profit = strike_diff - net_premium\n",
+    "        theoretical_max_loss = net_premium\n",
+    "        print(f\"理论最大收益: {theoretical_max_profit:.4f} ({theoretical_max_profit*10000:.2f}元)\")\n",
+    "        print(f\"理论最大亏损: {theoretical_max_loss:.4f} ({theoretical_max_loss*10000:.2f}元)\")\n",
+    "        print(f\"净权利金支出: {net_premium:.4f}\")\n",
+    "        print(f\"执行价差: {strike_diff:.4f}\")\n",
+    "    else:\n",
+    "        print(\"其他期权组合类型\")"
    ]
   },
   {

+ 305 - 0
Lib/Options/analysis_chart.py

@@ -0,0 +1,305 @@
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+from statistics import mean
+
+def analyze_options(*options):
+    """
+    统一的期权分析方法
+
+    参数:
+    *options: 一个或多个期权,每个期权格式为 (direction, option_type, premium, strike_price, quantity)
+             例如: ('buy', 'call', 0.0456, 2.75, 1)
+
+    示例:
+    # 单个期权
+    analyze_options(('buy', 'call', 0.05, 3.0, 1))
+
+    # 期权组合
+    analyze_options(('buy', 'call', 0.08, 2.9, 1), ('sell', 'call', 0.03, 3.1, 1))
+    """
+
+    if not options:
+        raise ValueError("请至少提供一个期权")
+
+    # 解析期权数据
+    option_list = []
+    all_strikes = []
+
+    for i, opt in enumerate(options):
+        if len(opt) != 5:
+            raise ValueError(f"期权{i+1}格式错误,应为(direction, option_type, premium, strike_price, quantity)")
+
+        direction, option_type, premium, strike_price, quantity = opt
+        option_list.append({
+            'direction': direction,
+            'option_type': option_type,
+            'premium': premium,
+            'strike_price': strike_price,
+            'quantity': quantity
+        })
+        all_strikes.append(strike_price)
+
+    # 确定价格分析区间
+    min_strike = min(all_strikes)
+    max_strike = max(all_strikes)
+    price_min = min_strike * 0.7
+    price_max = max_strike * 1.3
+
+    # 生成价格序列
+    gap = (price_max - price_min) / 1000
+    prices = np.arange(price_min, price_max + gap, gap)
+
+    # 计算每个期权的收益
+    results = {'price': prices}
+
+    for i, opt in enumerate(option_list):
+        profits = []
+        for price in prices:
+            profit = _calculate_profit(opt, price)
+            profits.append(profit)
+        results[f'opt{i+1}'] = profits
+
+    # 计算组合收益
+    if len(option_list) > 1:
+        combined_profits = []
+        for j in range(len(prices)):
+            total = sum(results[f'opt{i+1}'][j] for i in range(len(option_list)))
+            combined_profits.append(total)
+        results['combined'] = combined_profits
+
+    # 绘制图表
+    _plot_results(results, option_list, prices)
+
+    # 打印分析报告
+    _print_report(results, option_list, prices)
+
+    return pd.DataFrame(results)
+
+
+def _calculate_profit(option, price):
+    """计算单个期权在特定价格下的收益"""
+    direction = option['direction']
+    option_type = option['option_type']
+    premium = option['premium']
+    strike_price = option['strike_price']
+    quantity = option['quantity']
+
+    if direction == 'buy' and option_type == 'call':
+        # 买入认购
+        if price > strike_price:
+            return (price - strike_price - premium) * quantity
+        else:
+            return -premium * quantity
+
+    elif direction == 'sell' and option_type == 'call':
+        # 卖出认购
+        if price > strike_price:
+            return -(price - strike_price - premium) * quantity
+        else:
+            return premium * quantity
+
+    elif direction == 'buy' and option_type == 'put':
+        # 买入认沽
+        if price < strike_price:
+            return (strike_price - price - premium) * quantity
+        else:
+            return -premium * quantity
+
+    elif direction == 'sell' and option_type == 'put':
+        # 卖出认沽
+        if price < strike_price:
+            return -(strike_price - price - premium) * quantity
+        else:
+            return premium * quantity
+
+    return 0
+
+
+def _plot_results(results, option_list, prices):
+    """绘制分析图表"""
+    plt.figure(figsize=(14, 10))
+    plt.rcParams['axes.unicode_minus'] = False
+
+    colors = ['blue', 'green', 'orange', 'purple', 'brown']
+
+    # 绘制单个期权曲线
+    for i in range(len(option_list)):
+        opt = option_list[i]
+        opt_name = f'opt{i+1}'
+        strategy_name = f"{opt['direction'].upper()} {opt['option_type'].upper()}"
+        color = colors[i % len(colors)]
+
+        plt.plot(prices, results[opt_name], '--', color=color, linewidth=2, alpha=0.7,
+                label=f'{opt_name}: {strategy_name} (strike price:{opt["strike_price"]})')
+
+    # 绘制组合曲线
+    if 'combined' in results:
+        plt.plot(prices, results['combined'], 'r-', linewidth=3, label='Portfolio returns')
+
+    # 添加零线和行权价线
+    plt.axhline(0, color='gray', linestyle='-', alpha=0.5)
+    for opt in option_list:
+        plt.axvline(opt['strike_price'], color='gray', linestyle='--', alpha=0.3)
+
+    # 找到并标注关键点(盈亏平衡点、最大收益/损失边界点)
+    if 'combined' in results:
+        _mark_key_points(results['combined'], prices, 'Portfolio')
+    elif len(option_list) == 1:
+        _mark_key_points(results['opt1'], prices, 'Option')
+
+    plt.xlabel('Assest Price', fontsize=12)
+    plt.ylabel('Profit/Loss', fontsize=12)
+
+    if len(option_list) == 1:
+        opt = option_list[0]
+        title = f'{opt["direction"].upper()} {opt["option_type"].upper()} Option Analysis'
+    else:
+        title = f'Portfolio Analysis ({len(option_list)} options)'
+
+    plt.title(title, fontsize=14, weight='bold')
+    plt.grid(True, alpha=0.3)
+    plt.legend()
+    plt.tight_layout()
+    plt.show()
+
+
+def _mark_key_points(profits, prices, label_prefix):
+    """标注关键点:盈亏平衡点、最大收益/损失边界点"""
+
+    # 1. 标注盈亏平衡点
+    for i in range(len(profits) - 1):
+        if profits[i] * profits[i + 1] <= 0:  # 符号改变
+            # 线性插值找到精确平衡点
+            p1, profit1 = prices[i], profits[i]
+            p2, profit2 = prices[i + 1], profits[i + 1]
+            if profit2 != profit1:
+                breakeven_price = p1 - profit1 * (p2 - p1) / (profit2 - profit1)
+                plt.plot(breakeven_price, 0, 'ro', markersize=10)
+                plt.annotate(f'Equilibrium Point: {breakeven_price:.4f}',
+                            xy=(breakeven_price, 0),
+                            xytext=(breakeven_price + (prices.max() - prices.min()) * 0.05, max(profits) * 0.1),
+                            arrowprops=dict(arrowstyle='->', color='red'),
+                            fontsize=11, color='red', weight='bold')
+
+    # 2. 找到最大收益和最大损失点
+    max_profit = max(profits)
+    min_profit = min(profits)
+
+    # 3. 检查是否存在最大收益/损失的边界点
+    # 最大收益边界点:收益达到最大值后不再增长的点
+    max_boundary_points = _find_boundary_points(profits, prices, max_profit, 'max')
+
+    # 最大损失边界点:损失达到最大值后不再增长的点
+    min_boundary_points = _find_boundary_points(profits, prices, min_profit, 'min')
+
+    # 4. 标注最大收益边界点
+    for bp in max_boundary_points:
+        plt.plot(bp, max_profit, 'go', markersize=10)
+        plt.annotate(f'Max Returns: ({bp:.4f}, {max_profit:.4f})',
+                    xy=(bp, max_profit),
+                    xytext=(bp + (prices.max() - prices.min()) * 0.05, max_profit + (max_profit - min_profit) * 0.1),
+                    arrowprops=dict(arrowstyle='->', color='green'),
+                    fontsize=10, color='green', weight='bold')
+
+    # 5. 标注最大损失边界点
+    for bp in min_boundary_points:
+        plt.plot(bp, min_profit, 'mo', markersize=10)
+        plt.annotate(f'Max Loss: ({bp:.4f}, {min_profit:.4f})',
+                    xy=(bp, min_profit),
+                    xytext=(bp + (prices.max() - prices.min()) * 0.05, min_profit - (max_profit - min_profit) * 0.1),
+                    arrowprops=dict(arrowstyle='->', color='magenta'),
+                    fontsize=10, color='magenta', weight='bold')
+
+
+def _find_boundary_points(profits, prices, extreme_value, _extreme_type):
+    """找到最大收益或最大损失的边界点"""
+    boundary_points = []
+    tolerance = abs(extreme_value) * 0.001  # 允许的误差范围
+
+    # 找到所有接近极值的点
+    extreme_indices = []
+    for i, profit in enumerate(profits):
+        if abs(profit - extreme_value) <= tolerance:
+            extreme_indices.append(i)
+
+    if not extreme_indices:
+        return boundary_points
+
+    # 找到连续区间的边界点
+    if len(extreme_indices) > 1:
+        # 检查是否是连续的区间
+        continuous_regions = []
+        current_region = [extreme_indices[0]]
+
+        for i in range(1, len(extreme_indices)):
+            if extreme_indices[i] - extreme_indices[i-1] <= 2:  # 允许小的间隔
+                current_region.append(extreme_indices[i])
+            else:
+                continuous_regions.append(current_region)
+                current_region = [extreme_indices[i]]
+        continuous_regions.append(current_region)
+
+        # 对于每个连续区间,标注边界点
+        for region in continuous_regions:
+            if len(region) > 10:  # 只有当区间足够长时才标注边界点
+                # 左边界点
+                left_boundary = prices[region[0]]
+                boundary_points.append(left_boundary)
+
+                # 右边界点
+                right_boundary = prices[region[-1]]
+                boundary_points.append(right_boundary)
+
+    return boundary_points
+
+
+def _print_report(results, option_list, prices):
+    """打印分析报告"""
+    print("=" * 60)
+    print("期权分析报告")
+    print("=" * 60)
+
+    # 期权基本信息
+    for i, opt in enumerate(option_list):
+        print(f"期权{i+1}: {opt['direction'].upper()} {opt['option_type'].upper()}")
+        print(f"  行权价: {opt['strike_price']}")
+        print(f"  权利金: {opt['premium']}")
+        print(f"  数量: {opt['quantity']}手")
+
+    # 分析关键指标
+    if 'combined' in results:
+        profits = results['combined']
+        print(f"\n【组合分析】")
+    else:
+        profits = results['opt1']
+        print(f"\n【单期权分析】")
+
+    max_profit = max(profits)
+    min_profit = min(profits)
+    max_idx = profits.index(max_profit)
+    min_idx = profits.index(min_profit)
+
+    print(f"最大收益: {max_profit:.4f} (标的价格: {prices[max_idx]:.4f})")
+    print(f"最大损失: {min_profit:.4f} (标的价格: {prices[min_idx]:.4f})")
+    print(f"一单最大收益: {max_profit * 10000:.2f}元")
+    print(f"一单最大亏损: {abs(min_profit) * 10000:.2f}元")
+
+    # 找盈亏平衡点
+    breakeven_points = []
+    for i in range(len(profits) - 1):
+        if profits[i] * profits[i + 1] <= 0:
+            p1, profit1 = prices[i], profits[i]
+            p2, profit2 = prices[i + 1], profits[i + 1]
+            if profit2 != profit1:
+                bp = p1 - profit1 * (p2 - p1) / (profit2 - profit1)
+                breakeven_points.append(bp)
+
+    if breakeven_points:
+        print(f"盈亏平衡点: {[f'{bp:.4f}' for bp in breakeven_points]}")
+    else:
+        print("无盈亏平衡点")
+
+    print("=" * 60)
+
+analyze_options(('sell', 'call', 0.0199, 1.0, 1), ('buy', 'call', 0.0482, 1.05, 1))

+ 1248 - 0
Lib/Options/deep_itm_bull_spread_strategy.py

@@ -0,0 +1,1248 @@
+# 深度实值买购和卖购组合的牛差策略
+# 参考文档: Lib/Options/README.md - 策略1
+
+from jqdata import *
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+import tqdm
+from datetime import datetime, timedelta
+import warnings
+warnings.filterwarnings('ignore')
+
+class DeepITMBullSpreadStrategy:
+    """深度实值买购和卖购组合的牛差策略"""
+    
+    def __init__(self, underlying='510300.XSHG', start_date='2024-01-01', end_date='2025-06-30'):
+        self.underlying = underlying  # 标的ETF
+        self.start_date = start_date
+        self.end_date = end_date
+        
+        # 策略参数设置
+        self.params = {
+            '一组张数': 30,
+            '最小权利金': {'沪深300ETF': 0.03, '上证50ETF': 0.05},
+            '最少开仓日期': 20,  # 距离到期日天数
+            '买购时间价值阈值': 0.0001,
+            '卖购平仓权利金阈值': 0.0005,
+            '合约到期移仓日期最大': 7,  # 交易日
+            '合约到期移仓日期最小': 2,  # 交易日
+            '加仓窗口阈值': {'沪深300ETF': 0.2, '上证50ETF': 0.1}
+        }
+        
+        # 交易记录
+        self.positions = []  # 持仓记录
+        self.trades = []     # 交易记录
+        self.daily_pnl = []  # 每日损益
+        
+        # 获取交易日历
+        self.trade_days = get_trade_days(start_date, end_date)
+        
+    def get_etf_name(self):
+        """根据标的代码获取ETF名称"""
+        if self.underlying == '510300.XSHG':
+            return '沪深300ETF'
+        elif self.underlying == '510050.XSHG':
+            return '上证50ETF'
+        else:
+            return '未知ETF'
+    
+    def get_option_contracts(self, date, contract_type='CO', min_days_to_expire=20):
+        """获取期权合约信息"""
+        # 查询期权合约
+        q = query(opt.OPT_CONTRACT_INFO.code,
+                  opt.OPT_CONTRACT_INFO.trading_code,
+                  opt.OPT_CONTRACT_INFO.name,
+                  opt.OPT_CONTRACT_INFO.exercise_price,
+                  opt.OPT_CONTRACT_INFO.last_trade_date,
+                  opt.OPT_CONTRACT_INFO.list_date
+                 ).filter(
+                     opt.OPT_CONTRACT_INFO.underlying_symbol == self.underlying,
+                     opt.OPT_CONTRACT_INFO.contract_type == contract_type,
+                     opt.OPT_CONTRACT_INFO.list_date <= date,
+                     opt.OPT_CONTRACT_INFO.last_trade_date > date
+                 )
+
+        contracts = opt.run_query(q)
+        print(f"  查询到{len(contracts)}个{contract_type}类型的期权合约")
+
+        # 过滤距离到期日至少min_days_to_expire天的合约
+        valid_contracts = []
+        for _, contract in contracts.iterrows():
+            days_to_expire = (pd.to_datetime(contract['last_trade_date']) - pd.to_datetime(date)).days
+            if days_to_expire >= min_days_to_expire:
+                valid_contracts.append(contract)
+
+        print(f"  过滤后剩余{len(valid_contracts)}个距离到期日至少{min_days_to_expire}天的合约")
+        return pd.DataFrame(valid_contracts) if valid_contracts else pd.DataFrame()
+    
+    def get_option_price(self, option_code, date):
+        """获取期权价格"""
+        try:
+            q = query(opt.OPT_DAILY_PRICE.close).filter(
+                opt.OPT_DAILY_PRICE.code == option_code,
+                opt.OPT_DAILY_PRICE.date == date
+            )
+            result = opt.run_query(q)
+            return result['close'].iloc[0] if not result.empty else None
+        except:
+            return None
+    
+    def calculate_time_value(self, option_price, intrinsic_value):
+        """计算时间价值"""
+        return max(0, option_price - max(0, intrinsic_value))
+    
+    def select_call_option_to_buy(self, date, etf_price):
+        """选择深度实值买购期权"""
+        contracts = self.get_option_contracts(date, 'CO')
+        if contracts.empty:
+            print(f"    买购选择失败: 未找到任何认购期权合约")
+            return None
+
+        print(f"    找到{len(contracts)}个认购期权合约")
+
+        # 筛选深度实值期权(时间价值 <= 阈值)
+        suitable_contracts = []
+        checked_count = 0
+        for _, contract in contracts.iterrows():
+            option_price = self.get_option_price(contract['code'], date)
+            checked_count += 1
+            if option_price is None:
+                continue
+
+            intrinsic_value = max(0, etf_price - contract['exercise_price'])
+            time_value = self.calculate_time_value(option_price, intrinsic_value)
+
+            if time_value <= self.params['买购时间价值阈值']:
+                suitable_contracts.append({
+                    'code': contract['code'],
+                    'exercise_price': contract['exercise_price'],
+                    'option_price': option_price,
+                    'time_value': time_value,
+                    'last_trade_date': contract['last_trade_date']
+                })
+
+        print(f"    检查了{checked_count}个合约,找到{len(suitable_contracts)}个符合时间价值条件的买购期权")
+
+        # 选择时间价值最小的
+        if suitable_contracts:
+            return min(suitable_contracts, key=lambda x: x['time_value'])
+        return None
+    
+    def select_call_option_to_sell(self, date, etf_price):
+        """选择平值卖购期权"""
+        contracts = self.get_option_contracts(date, 'CO')
+        if contracts.empty:
+            print(f"    卖购选择失败: 未找到任何认购期权合约")
+            return None
+
+        # 找到最接近平值的期权
+        etf_name = self.get_etf_name()
+        min_premium = self.params['最小权利金'].get(etf_name, 0.03)
+        print(f"    最小权利金要求: {min_premium}")
+
+        suitable_contracts = []
+        checked_count = 0
+        for _, contract in contracts.iterrows():
+            option_price = self.get_option_price(contract['code'], date)
+            checked_count += 1
+            if option_price is None or option_price < min_premium:
+                continue
+
+            price_diff = abs(contract['exercise_price'] - etf_price)
+            suitable_contracts.append({
+                'code': contract['code'],
+                'exercise_price': contract['exercise_price'],
+                'option_price': option_price,
+                'price_diff': price_diff,
+                'last_trade_date': contract['last_trade_date']
+            })
+
+        print(f"    检查了{checked_count}个合约,找到{len(suitable_contracts)}个符合权利金条件的卖购期权")
+
+        # 选择最接近平值且权利金较高的
+        if suitable_contracts:
+            # 先按价格差排序,再按权利金排序
+            suitable_contracts.sort(key=lambda x: (x['price_diff'], -x['option_price']))
+            return suitable_contracts[0]
+        return None
+    
+    def open_bull_spread_position(self, date, etf_price, quantity=None):
+        """开仓牛差组合"""
+        if quantity is None:
+            quantity = self.params['一组张数']
+
+        print(f"尝试开仓: 日期={date}, ETF价格={etf_price:.4f}")
+
+        # 选择买购期权(深度实值)
+        buy_call = self.select_call_option_to_buy(date, etf_price)
+        if buy_call is None:
+            print(f"  失败: 未找到合适的买购期权")
+            return None
+
+        print(f"  找到买购期权: 代码={buy_call['code']}, 行权价={buy_call['exercise_price']:.4f}, 权利金={buy_call['option_price']:.4f}")
+
+        # 选择卖购期权(平值)
+        sell_call = self.select_call_option_to_sell(date, etf_price)
+        if sell_call is None:
+            print(f"  失败: 未找到合适的卖购期权")
+            return None
+
+        print(f"  找到卖购期权: 代码={sell_call['code']}, 行权价={sell_call['exercise_price']:.4f}, 权利金={sell_call['option_price']:.4f}")
+
+        # 确保买购行权价 < 卖购行权价
+        if buy_call['exercise_price'] >= sell_call['exercise_price']:
+            print(f"  失败: 买购行权价({buy_call['exercise_price']:.4f}) >= 卖购行权价({sell_call['exercise_price']:.4f})")
+            return None
+        
+        # 计算单张最大盈利和最小盈利
+        max_profit_per_contract = (sell_call['exercise_price'] - buy_call['exercise_price']
+                                 - buy_call['option_price'] + sell_call['option_price'])
+        min_profit_per_contract = sell_call['option_price']
+
+        position = {
+            'open_date': date,
+            'etf_price': etf_price,
+            'buy_call_code': buy_call['code'],
+            'buy_call_strike': buy_call['exercise_price'],
+            'buy_call_price': buy_call['option_price'],
+            'sell_call_code': sell_call['code'],
+            'sell_call_strike': sell_call['exercise_price'],
+            'sell_call_price': sell_call['option_price'],
+            'quantity': quantity,
+            'max_profit_per_contract': max_profit_per_contract,
+            'min_profit_per_contract': min_profit_per_contract,
+            'max_profit_total': max_profit_per_contract * quantity * 10000,
+            'min_profit_total': min_profit_per_contract * quantity * 10000,
+            'expire_date': sell_call['last_trade_date'],
+            'status': 'open',
+            'is_additional': False  # 是否为加仓
+        }
+
+        self.positions.append(position)
+
+        # 记录交易
+        self.trades.append({
+            'date': date,
+            'action': 'open_bull_spread',
+            'details': position
+        })
+
+        print(f"  成功开仓: 数量={quantity}张, 最大盈利={max_profit_per_contract:.4f}元/张")
+
+        return position
+
+    def should_close_position(self, position, date, etf_price):
+        """判断是否应该平仓"""
+        # 1. 检查是否接近到期
+        days_to_expire = (pd.to_datetime(position['expire_date']) - pd.to_datetime(date)).days
+        if days_to_expire <= self.params['合约到期移仓日期最大']:
+            return True, "approaching_expiry"
+
+        # 2. 检查ETF大涨情况(接近最大盈利)
+        current_max_profit = max(0, min(etf_price - position['buy_call_strike'],
+                                      position['sell_call_strike'] - position['buy_call_strike']))
+        if current_max_profit >= position['max_profit_per_contract'] * 0.9:
+            return True, "max_profit_reached"
+
+        # 3. 检查ETF下跌情况(卖购权利金剩余低于阈值)
+        sell_call_price = self.get_option_price(position['sell_call_code'], date)
+        if sell_call_price is not None and sell_call_price <= self.params['卖购平仓权利金阈值']:
+            return True, "etf_declined"
+
+        return False, None
+
+    def close_position(self, position, date, reason):
+        """平仓操作"""
+        buy_call_price = self.get_option_price(position['buy_call_code'], date)
+        sell_call_price = self.get_option_price(position['sell_call_code'], date)
+
+        if buy_call_price is None or sell_call_price is None:
+            return None
+
+        # 计算平仓收益
+        buy_call_pnl = (buy_call_price - position['buy_call_price']) * position['quantity'] * 10000
+        sell_call_pnl = (position['sell_call_price'] - sell_call_price) * position['quantity'] * 10000
+        total_pnl = buy_call_pnl + sell_call_pnl
+
+        # 更新持仓状态
+        position['status'] = 'closed'
+        position['close_date'] = date
+        position['close_reason'] = reason
+        position['close_buy_call_price'] = buy_call_price
+        position['close_sell_call_price'] = sell_call_price
+        position['realized_pnl'] = total_pnl
+
+        # 记录交易
+        self.trades.append({
+            'date': date,
+            'action': 'close_bull_spread',
+            'reason': reason,
+            'pnl': total_pnl,
+            'details': position
+        })
+
+        return total_pnl
+
+    def should_add_position(self, etf_price):
+        """判断是否应该加仓"""
+        # 检查是否有开仓的主仓位
+        main_positions = [p for p in self.positions if p['status'] == 'open' and not p['is_additional']]
+        if not main_positions:
+            return False
+
+        # 获取最近的主仓位
+        latest_main = max(main_positions, key=lambda x: x['open_date'])
+
+        # 检查价格下跌幅度
+        etf_name = self.get_etf_name()
+        threshold = self.params['加仓窗口阈值'].get(etf_name, 0.2)
+
+        if latest_main['etf_price'] - etf_price >= threshold:
+            # 检查是否已经在这个价格水平加过仓
+            existing_additional = [p for p in self.positions
+                                 if p['status'] == 'open' and p['is_additional']
+                                 and abs(p['etf_price'] - etf_price) < threshold * 0.5]
+            if not existing_additional:
+                return True
+
+        return False
+
+    def run_backtest(self):
+        """运行回测"""
+        print(f"开始回测: {self.start_date} 到 {self.end_date}")
+        print(f"标的: {self.underlying} ({self.get_etf_name()})")
+
+        # 获取ETF价格数据
+        etf_prices = get_price(self.underlying, self.start_date, self.end_date, fields=['close'])['close']
+
+        # 初始化
+        portfolio_value = []
+        etf_benchmark = []
+
+        with tqdm.tqdm(self.trade_days, desc="回测进度") as pbar:
+            for date in pbar:
+                if date not in etf_prices.index:
+                    continue
+
+                etf_price = etf_prices[date]
+
+                # 检查现有持仓是否需要平仓
+                open_positions = [p for p in self.positions if p['status'] == 'open']
+                for position in open_positions:
+                    should_close, reason = self.should_close_position(position, date, etf_price)
+                    if should_close:
+                        self.close_position(position, date, reason)
+
+                # 检查是否需要开新仓
+                open_positions = [p for p in self.positions if p['status'] == 'open']
+                if not open_positions:  # 没有持仓时开仓
+                    self.open_bull_spread_position(date, etf_price)
+                elif self.should_add_position(etf_price):  # 加仓
+                    additional_pos = self.open_bull_spread_position(date, etf_price)
+                    if additional_pos:
+                        additional_pos['is_additional'] = True
+
+                # 计算当日组合价值
+                daily_pnl = self.calculate_daily_pnl(date)
+                portfolio_value.append(daily_pnl)
+
+                # ETF基准收益
+                if len(etf_benchmark) == 0:
+                    etf_benchmark.append(0)
+                else:
+                    etf_return = (etf_price / etf_prices[self.trade_days[0]] - 1) * 100000  # 假设初始投资10万
+                    etf_benchmark.append(etf_return)
+
+        # 生成回测报告
+        self.generate_detailed_report()
+
+        return portfolio_value, etf_benchmark
+
+    def calculate_daily_pnl(self, date):
+        """计算每日损益"""
+        total_pnl = 0
+
+        for position in self.positions:
+            if position['status'] == 'closed':
+                if 'realized_pnl' in position:
+                    total_pnl += position['realized_pnl']
+            elif position['status'] == 'open':
+                # 计算未实现损益
+                buy_call_price = self.get_option_price(position['buy_call_code'], date)
+                sell_call_price = self.get_option_price(position['sell_call_code'], date)
+
+                if buy_call_price is not None and sell_call_price is not None:
+                    buy_call_pnl = (buy_call_price - position['buy_call_price']) * position['quantity'] * 10000
+                    sell_call_pnl = (position['sell_call_price'] - sell_call_price) * position['quantity'] * 10000
+                    total_pnl += buy_call_pnl + sell_call_pnl
+
+        return total_pnl
+
+    def export_data_to_csv(self, filename_prefix="bull_spread_data"):
+        """导出数据到CSV文件,用于线下分析"""
+
+        # 1. 导出持仓数据
+        positions_data = []
+        for pos in self.positions:
+            positions_data.append({
+                'open_date': pos['open_date'],
+                'etf_price': pos['etf_price'],
+                'buy_call_code': pos['buy_call_code'],
+                'buy_call_strike': pos['buy_call_strike'],
+                'buy_call_price': pos['buy_call_price'],
+                'sell_call_code': pos['sell_call_code'],
+                'sell_call_strike': pos['sell_call_strike'],
+                'sell_call_price': pos['sell_call_price'],
+                'quantity': pos['quantity'],
+                'max_profit_total': pos['max_profit_total'],
+                'expire_date': pos['expire_date'],
+                'status': pos['status'],
+                'is_additional': pos['is_additional'],
+                'close_date': pos.get('close_date', ''),
+                'close_reason': pos.get('close_reason', ''),
+                'realized_pnl': pos.get('realized_pnl', 0)
+            })
+
+        positions_df = pd.DataFrame(positions_data)
+        positions_df.to_csv(f"{filename_prefix}_positions.csv", index=False, encoding='utf-8-sig')
+
+        # 2. 导出交易记录
+        trades_data = []
+        for trade in self.trades:
+            trades_data.append({
+                'date': trade['date'],
+                'action': trade['action'],
+                'pnl': trade.get('pnl', 0),
+                'reason': trade.get('reason', ''),
+                'details': str(trade.get('details', ''))
+            })
+
+        trades_df = pd.DataFrame(trades_data)
+        trades_df.to_csv(f"{filename_prefix}_trades.csv", index=False, encoding='utf-8-sig')
+
+        # 3. 导出每日损益数据
+        daily_data = []
+        etf_prices = get_price(self.underlying, self.start_date, self.end_date, fields=['close'])['close']
+
+        for date in self.trade_days:
+            if date not in etf_prices.index:
+                continue
+
+            etf_price = etf_prices[date]
+
+            # 计算买购和卖购分别的损益
+            buy_call_pnl = 0
+            sell_call_pnl = 0
+            total_positions = 0
+
+            for position in self.positions:
+                if position['open_date'] <= date and (position['status'] == 'open' or position.get('close_date', date) >= date):
+                    total_positions += 1
+
+                    if position['status'] == 'closed' and position.get('close_date') == date:
+                        # 已平仓的实现损益
+                        buy_call_pnl += position.get('realized_pnl', 0) / 2  # 简化分配
+                        sell_call_pnl += position.get('realized_pnl', 0) / 2
+                    elif position['status'] == 'open':
+                        # 未实现损益
+                        buy_call_price = self.get_option_price(position['buy_call_code'], date)
+                        sell_call_price = self.get_option_price(position['sell_call_code'], date)
+
+                        if buy_call_price is not None:
+                            buy_call_pnl += (buy_call_price - position['buy_call_price']) * position['quantity'] * 10000
+
+                        if sell_call_price is not None:
+                            sell_call_pnl += (position['sell_call_price'] - sell_call_price) * position['quantity'] * 10000
+
+            # ETF基准收益
+            etf_return = (etf_price / etf_prices.iloc[0] - 1) * 100000 if len(etf_prices) > 0 else 0
+
+            daily_data.append({
+                'date': date,
+                'etf_price': etf_price,
+                'etf_return': etf_return,
+                'buy_call_pnl': buy_call_pnl,
+                'sell_call_pnl': sell_call_pnl,
+                'total_pnl': buy_call_pnl + sell_call_pnl,
+                'total_positions': total_positions
+            })
+
+        daily_df = pd.DataFrame(daily_data)
+        daily_df.to_csv(f"{filename_prefix}_daily.csv", index=False, encoding='utf-8-sig')
+
+        print(f"数据已导出到以下文件:")
+        print(f"- {filename_prefix}_positions.csv (持仓数据)")
+        print(f"- {filename_prefix}_trades.csv (交易记录)")
+        print(f"- {filename_prefix}_daily.csv (每日损益)")
+
+        return positions_df, trades_df, daily_df
+
+    def generate_detailed_report(self):
+        """生成详细的分析报告,分别分析买购和卖购收益"""
+        print("\n" + "="*80)
+        print("深度实值牛差策略详细回测报告")
+        print("="*80)
+
+        # 获取ETF价格数据
+        etf_prices = get_price(self.underlying, self.start_date, self.end_date, fields=['close'])['close']
+
+        # 计算分项收益
+        buy_call_returns = []
+        sell_call_returns = []
+        combined_returns = []
+        etf_returns = []
+        dates = []
+
+        initial_etf_price = etf_prices.iloc[0] if len(etf_prices) > 0 else 1
+
+        for date in self.trade_days:
+            if date not in etf_prices.index:
+                continue
+
+            etf_price = etf_prices[date]
+            dates.append(date)
+
+            # 计算买购和卖购分别的损益
+            buy_call_pnl = 0
+            sell_call_pnl = 0
+
+            for position in self.positions:
+                if position['open_date'] <= date:
+                    if position['status'] == 'closed':
+                        if 'realized_pnl' in position:
+                            # 简化处理:假设买购和卖购各承担一半损益
+                            buy_call_pnl += position['realized_pnl'] / 2
+                            sell_call_pnl += position['realized_pnl'] / 2
+                    elif position['status'] == 'open':
+                        # 计算未实现损益
+                        buy_call_price = self.get_option_price(position['buy_call_code'], date)
+                        sell_call_price = self.get_option_price(position['sell_call_code'], date)
+
+                        if buy_call_price is not None:
+                            buy_call_pnl += (buy_call_price - position['buy_call_price']) * position['quantity'] * 10000
+
+                        if sell_call_price is not None:
+                            sell_call_pnl += (position['sell_call_price'] - sell_call_price) * position['quantity'] * 10000
+
+            buy_call_returns.append(buy_call_pnl)
+            sell_call_returns.append(sell_call_pnl)
+            combined_returns.append(buy_call_pnl + sell_call_pnl)
+
+            # ETF基准收益
+            etf_return = (etf_price / initial_etf_price - 1) * 100000  # 假设10万本金
+            etf_returns.append(etf_return)
+
+        # 基本统计
+        total_trades = len([t for t in self.trades if t['action'] == 'close_bull_spread'])
+        winning_trades = len([t for t in self.trades if t['action'] == 'close_bull_spread' and t['pnl'] > 0])
+
+        if total_trades > 0:
+            win_rate = winning_trades / total_trades * 100
+            total_pnl = sum([t['pnl'] for t in self.trades if t['action'] == 'close_bull_spread'])
+            avg_pnl = total_pnl / total_trades
+        else:
+            win_rate = 0
+            total_pnl = 0
+            avg_pnl = 0
+
+        print(f"交易次数: {total_trades}")
+        print(f"胜率: {win_rate:.2f}%")
+        print(f"总收益: {total_pnl:.2f}元")
+        print(f"平均每笔收益: {avg_pnl:.2f}元")
+
+        # 分项收益统计和对比分析
+        if len(buy_call_returns) > 0:
+            final_buy_call = buy_call_returns[-1]
+            final_sell_call = sell_call_returns[-1]
+            final_combined = combined_returns[-1]
+            final_etf = etf_returns[-1]
+
+            print(f"\n=== 分项收益分析 ===")
+            print(f"买购期权收益: {final_buy_call:.2f}元")
+            print(f"卖购期权收益: {final_sell_call:.2f}元")
+            print(f"组合总收益: {final_combined:.2f}元")
+            print(f"ETF基准收益: {final_etf:.2f}元")
+
+            print(f"\n=== 与ETF基准对比分析 ===")
+            print(f"1. 牛差策略 vs ETF: {final_combined:.2f} vs {final_etf:.2f} = {final_combined - final_etf:+.2f}元")
+            print(f"2. 买购策略 vs ETF: {final_buy_call:.2f} vs {final_etf:.2f} = {final_buy_call - final_etf:+.2f}元")
+            print(f"3. 卖购策略 vs ETF: {final_sell_call:.2f} vs {final_etf:.2f} = {final_sell_call - final_etf:+.2f}元")
+
+        # 持仓统计
+        open_positions = len([p for p in self.positions if p['status'] == 'open'])
+        closed_positions = len([p for p in self.positions if p['status'] == 'closed'])
+        print(f"\n=== 持仓统计 ===")
+        print(f"当前持仓: {open_positions}个")
+        print(f"已平仓位: {closed_positions}个")
+
+        # 绘制分项收益曲线
+        if len(dates) > 0:
+            self.plot_detailed_performance(dates, buy_call_returns, sell_call_returns, combined_returns, etf_returns)
+            self.plot_strategy_vs_etf_comparison(dates, buy_call_returns, sell_call_returns, combined_returns, etf_returns)
+
+        return {
+            'dates': dates,
+            'buy_call_returns': buy_call_returns,
+            'sell_call_returns': sell_call_returns,
+            'combined_returns': combined_returns,
+            'etf_returns': etf_returns
+        }
+
+    def plot_detailed_performance(self, dates, buy_call_returns, sell_call_returns, combined_returns, etf_returns):
+        """绘制详细的策略表现图,分别显示买购和卖购收益"""
+        # 设置中文字体
+        plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
+        plt.rcParams['axes.unicode_minus'] = False
+
+        plt.figure(figsize=(15, 12))
+
+        # 第一个子图:分项收益对比
+        plt.subplot(3, 1, 1)
+        plt.plot(dates, buy_call_returns, label='买购期权收益', linewidth=2, color='blue')
+        plt.plot(dates, sell_call_returns, label='卖购期权收益', linewidth=2, color='red')
+        plt.plot(dates, combined_returns, label='组合总收益', linewidth=2, color='green')
+        plt.plot(dates, etf_returns, label=f'{self.get_etf_name()}基准', linewidth=2, color='orange')
+        plt.axhline(0, color='gray', linestyle='--', alpha=0.5)
+        plt.title('策略分项收益对比')
+        plt.ylabel('收益(元)')
+        plt.legend()
+        plt.grid(True, alpha=0.3)
+
+        # 第二个子图:累计收益率
+        plt.subplot(3, 1, 2)
+        if len(combined_returns) > 0 and len(etf_returns) > 0:
+            combined_cumret = [(x / 100000) * 100 for x in combined_returns]  # 转换为百分比
+            etf_cumret = [(x / 100000) * 100 for x in etf_returns]
+
+            plt.plot(dates, combined_cumret, label='策略累计收益率', linewidth=2, color='green')
+            plt.plot(dates, etf_cumret, label='ETF累计收益率', linewidth=2, color='orange')
+            plt.axhline(0, color='gray', linestyle='--', alpha=0.5)
+            plt.title('累计收益率对比')
+            plt.ylabel('收益率(%)')
+            plt.legend()
+            plt.grid(True, alpha=0.3)
+
+        # 第三个子图:持仓数量变化
+        plt.subplot(3, 1, 3)
+        position_counts = []
+        for date in dates:
+            count = len([p for p in self.positions
+                        if p['open_date'] <= date and
+                        (p['status'] == 'open' or p.get('close_date', date) >= date)])
+            position_counts.append(count)
+
+        plt.plot(dates, position_counts, label='持仓数量', linewidth=2, color='purple')
+        plt.title('持仓数量变化')
+        plt.ylabel('持仓数量')
+        plt.xlabel('日期')
+        plt.legend()
+        plt.grid(True, alpha=0.3)
+
+        plt.tight_layout()
+        plt.show()
+
+    def plot_strategy_vs_etf_comparison(self, dates, buy_call_returns, sell_call_returns, combined_returns, etf_returns):
+        """绘制三个策略分别与ETF的对比图"""
+        # 设置中文字体
+        plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
+        plt.rcParams['axes.unicode_minus'] = False
+
+        plt.figure(figsize=(15, 12))
+
+        # 第一个子图:牛差策略 vs ETF
+        plt.subplot(3, 1, 1)
+        plt.plot(dates, combined_returns, label='牛差策略收益', linewidth=2, color='green')
+        plt.plot(dates, etf_returns, label='ETF基准收益', linewidth=2, color='orange')
+        plt.axhline(0, color='gray', linestyle='--', alpha=0.5)
+        plt.title('牛差策略 vs ETF基准收益对比')
+        plt.ylabel('收益(元)')
+        plt.legend()
+        plt.grid(True, alpha=0.3)
+
+        # 第二个子图:买购策略 vs ETF
+        plt.subplot(3, 1, 2)
+        plt.plot(dates, buy_call_returns, label='买购期权收益', linewidth=2, color='blue')
+        plt.plot(dates, etf_returns, label='ETF基准收益', linewidth=2, color='orange')
+        plt.axhline(0, color='gray', linestyle='--', alpha=0.5)
+        plt.title('买购期权 vs ETF基准收益对比')
+        plt.ylabel('收益(元)')
+        plt.legend()
+        plt.grid(True, alpha=0.3)
+
+        # 第三个子图:卖购策略 vs ETF
+        plt.subplot(3, 1, 3)
+        plt.plot(dates, sell_call_returns, label='卖购期权收益', linewidth=2, color='red')
+        plt.plot(dates, etf_returns, label='ETF基准收益', linewidth=2, color='orange')
+        plt.axhline(0, color='gray', linestyle='--', alpha=0.5)
+        plt.title('卖购期权 vs ETF基准收益对比')
+        plt.ylabel('收益(元)')
+        plt.xlabel('日期')
+        plt.legend()
+        plt.grid(True, alpha=0.3)
+
+        plt.tight_layout()
+        plt.show()
+
+    def plot_performance(self, portfolio_value, etf_benchmark):
+        """绘制策略表现图"""
+        plt.figure(figsize=(12, 8))
+
+        dates = self.trade_days[:len(portfolio_value)]
+
+        plt.subplot(2, 1, 1)
+        plt.plot(dates, portfolio_value, label='牛差策略', linewidth=2)
+        plt.plot(dates, etf_benchmark, label=f'{self.get_etf_name()}基准', linewidth=2)
+        plt.title('策略收益对比')
+        plt.ylabel('收益(元)')
+        plt.legend()
+        plt.grid(True, alpha=0.3)
+
+        plt.subplot(2, 1, 2)
+        # 绘制持仓数量变化
+        position_counts = []
+        for date in dates:
+            count = len([p for p in self.positions
+                        if p['open_date'] <= date and
+                        (p['status'] == 'open' or p.get('close_date', date) >= date)])
+            position_counts.append(count)
+
+        plt.plot(dates, position_counts, label='持仓数量', linewidth=2, color='orange')
+        plt.title('持仓数量变化')
+        plt.ylabel('持仓数量')
+        plt.xlabel('日期')
+        plt.legend()
+        plt.grid(True, alpha=0.3)
+
+        plt.tight_layout()
+        plt.show()
+
+    def print_position_details(self):
+        """打印持仓详情"""
+        print("\n" + "="*80)
+        print("持仓详情")
+        print("="*80)
+
+        for i, pos in enumerate(self.positions):
+            print(f"\n持仓 {i+1}:")
+            print(f"  开仓日期: {pos['open_date']}")
+            print(f"  ETF价格: {pos['etf_price']:.4f}")
+            print(f"  买购: {pos['buy_call_strike']:.2f} @ {pos['buy_call_price']:.4f}")
+            print(f"  卖购: {pos['sell_call_strike']:.2f} @ {pos['sell_call_price']:.4f}")
+            print(f"  数量: {pos['quantity']}张")
+            print(f"  单张最大盈利: {pos['max_profit_per_contract']:.4f}")
+            print(f"  总最大盈利: {pos['max_profit_total']:.2f}元")
+            print(f"  状态: {pos['status']}")
+            print(f"  是否加仓: {'是' if pos['is_additional'] else '否'}")
+
+            if pos['status'] == 'closed':
+                print(f"  平仓日期: {pos['close_date']}")
+                print(f"  平仓原因: {pos['close_reason']}")
+                print(f"  实现损益: {pos['realized_pnl']:.2f}元")
+
+
+class OptionsAnalyzer:
+    """期权分析工具类"""
+
+    @staticmethod
+    def analyze_options(*options):
+        """
+        统一的期权分析方法
+        参数: *options: 一个或多个期权,每个期权格式为 (direction, option_type, premium, strike_price, quantity)
+        """
+        if not options:
+            raise ValueError("请至少提供一个期权")
+
+        # 解析期权数据
+        option_list = []
+        all_strikes = []
+
+        for i, opt in enumerate(options):
+            if len(opt) != 5:
+                raise ValueError(f"期权{i+1}格式错误,应为(direction, option_type, premium, strike_price, quantity)")
+
+            direction, option_type, premium, strike_price, quantity = opt
+            option_list.append({
+                'direction': direction,
+                'option_type': option_type,
+                'premium': premium,
+                'strike_price': strike_price,
+                'quantity': quantity
+            })
+            all_strikes.append(strike_price)
+
+        # 确定价格分析区间
+        min_strike = min(all_strikes)
+        max_strike = max(all_strikes)
+        price_min = min_strike * 0.7
+        price_max = max_strike * 1.3
+
+        # 生成价格序列
+        gap = (price_max - price_min) / 1000
+        prices = np.arange(price_min, price_max + gap, gap)
+
+        # 计算每个期权的收益
+        results = {'price': prices}
+
+        for i, opt in enumerate(option_list):
+            profits = []
+            for price in prices:
+                profit = OptionsAnalyzer._calculate_profit(opt, price)
+                profits.append(profit)
+            results[f'opt{i+1}'] = profits
+
+        # 计算组合收益
+        if len(option_list) > 1:
+            combined_profits = []
+            for j in range(len(prices)):
+                total = sum(results[f'opt{i+1}'][j] for i in range(len(option_list)))
+                combined_profits.append(total)
+            results['combined'] = combined_profits
+
+        # 绘制图表
+        OptionsAnalyzer._plot_results(results, option_list, prices)
+
+        # 打印分析报告
+        OptionsAnalyzer._print_report(results, option_list, prices)
+
+        return pd.DataFrame(results)
+
+    @staticmethod
+    def _calculate_profit(option, price):
+        """计算单个期权在特定价格下的收益"""
+        direction = option['direction']
+        option_type = option['option_type']
+        premium = option['premium']
+        strike_price = option['strike_price']
+        quantity = option['quantity']
+
+        if direction == 'buy' and option_type == 'call':
+            # 买入认购
+            if price > strike_price:
+                return (price - strike_price - premium) * quantity
+            else:
+                return -premium * quantity
+
+        elif direction == 'sell' and option_type == 'call':
+            # 卖出认购
+            if price > strike_price:
+                return -(price - strike_price - premium) * quantity
+            else:
+                return premium * quantity
+
+        elif direction == 'buy' and option_type == 'put':
+            # 买入认沽
+            if price < strike_price:
+                return (strike_price - price - premium) * quantity
+            else:
+                return -premium * quantity
+
+        elif direction == 'sell' and option_type == 'put':
+            # 卖出认沽
+            if price < strike_price:
+                return -(strike_price - price - premium) * quantity
+            else:
+                return premium * quantity
+
+        return 0
+
+    @staticmethod
+    def _plot_results(results, option_list, prices):
+        """绘制分析图表"""
+        # 设置中文字体
+        plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
+        plt.rcParams['axes.unicode_minus'] = False
+
+        plt.figure(figsize=(14, 10))
+
+        colors = ['blue', 'green', 'orange', 'purple', 'brown']
+
+        # 绘制单个期权曲线
+        for i in range(len(option_list)):
+            opt = option_list[i]
+            opt_name = f'opt{i+1}'
+            strategy_name = f"{opt['direction'].upper()} {opt['option_type'].upper()}"
+            color = colors[i % len(colors)]
+
+            plt.plot(prices, results[opt_name], '--', color=color, linewidth=2, alpha=0.7,
+                    label=f'{opt_name}: {strategy_name} (行权价:{opt["strike_price"]})')
+
+        # 绘制组合曲线
+        if 'combined' in results:
+            plt.plot(prices, results['combined'], 'r-', linewidth=3, label='组合收益')
+
+        # 添加零线和行权价线
+        plt.axhline(0, color='gray', linestyle='-', alpha=0.5)
+        for opt in option_list:
+            plt.axvline(opt['strike_price'], color='gray', linestyle='--', alpha=0.3)
+
+        # 找到并标注关键点
+        if 'combined' in results:
+            OptionsAnalyzer._mark_key_points(results['combined'], prices, '组合')
+        elif len(option_list) == 1:
+            OptionsAnalyzer._mark_key_points(results['opt1'], prices, '期权')
+
+        plt.xlabel('标的资产价格', fontsize=12)
+        plt.ylabel('收益/损失', fontsize=12)
+
+        if len(option_list) == 1:
+            opt = option_list[0]
+            title = f'{opt["direction"].upper()} {opt["option_type"].upper()} 期权分析'
+        else:
+            title = f'期权组合分析 ({len(option_list)}个期权)'
+
+        plt.title(title, fontsize=14, weight='bold')
+        plt.grid(True, alpha=0.3)
+        plt.legend()
+        plt.tight_layout()
+        plt.show()
+
+    @staticmethod
+    def _mark_key_points(profits, prices, label_prefix):
+        """标注关键点:盈亏平衡点、最大收益/损失边界点"""
+        # 标注盈亏平衡点
+        for i in range(len(profits) - 1):
+            if profits[i] * profits[i + 1] <= 0:  # 符号改变
+                # 线性插值找到精确平衡点
+                p1, profit1 = prices[i], profits[i]
+                p2, profit2 = prices[i + 1], profits[i + 1]
+                if profit2 != profit1:
+                    breakeven_price = p1 - profit1 * (p2 - p1) / (profit2 - profit1)
+                    plt.plot(breakeven_price, 0, 'ro', markersize=10)
+                    plt.annotate(f'平衡点: {breakeven_price:.3f}',
+                                xy=(breakeven_price, 0),
+                                xytext=(breakeven_price + (prices.max() - prices.min()) * 0.05, max(profits) * 0.1),
+                                arrowprops=dict(arrowstyle='->', color='red'),
+                                fontsize=11, color='red', weight='bold')
+
+    @staticmethod
+    def _print_report(results, option_list, prices):
+        """打印分析报告"""
+        print("=" * 60)
+        print("期权分析报告")
+        print("=" * 60)
+
+        # 期权基本信息
+        for i, opt in enumerate(option_list):
+            print(f"期权{i+1}: {opt['direction'].upper()} {opt['option_type'].upper()}")
+            print(f"  行权价: {opt['strike_price']}")
+            print(f"  权利金: {opt['premium']}")
+            print(f"  数量: {opt['quantity']}手")
+
+        # 分析关键指标
+        if 'combined' in results:
+            profits = results['combined']
+            print(f"\n【组合分析】")
+        else:
+            profits = results['opt1']
+            print(f"\n【单期权分析】")
+
+        max_profit = max(profits)
+        min_profit = min(profits)
+        max_idx = profits.tolist().index(max_profit) if hasattr(profits, 'tolist') else profits.index(max_profit)
+        min_idx = profits.tolist().index(min_profit) if hasattr(profits, 'tolist') else profits.index(min_profit)
+
+        print(f"最大收益: {max_profit:.4f} (标的价格: {prices[max_idx]:.4f})")
+        print(f"最大损失: {min_profit:.4f} (标的价格: {prices[min_idx]:.4f})")
+        print(f"一单最大收益: {max_profit * 10000:.2f}元")
+        print(f"一单最大亏损: {abs(min_profit) * 10000:.2f}元")
+
+        # 找盈亏平衡点
+        breakeven_points = []
+        for i in range(len(profits) - 1):
+            if profits[i] * profits[i + 1] <= 0:
+                p1, profit1 = prices[i], profits[i]
+                p2, profit2 = prices[i + 1], profits[i + 1]
+                if profit2 != profit1:
+                    bp = p1 - profit1 * (p2 - p1) / (profit2 - profit1)
+                    breakeven_points.append(bp)
+
+        if breakeven_points:
+            print(f"盈亏平衡点: {[f'{bp:.4f}' for bp in breakeven_points]}")
+        else:
+            print("无盈亏平衡点")
+
+        print("=" * 60)
+
+
+def test_opening_logic():
+    """专门测试开仓逻辑"""
+    print("="*60)
+    print("测试开仓逻辑")
+    print("="*60)
+
+    # 创建策略实例
+    strategy = DeepITMBullSpreadStrategy(
+        underlying='510300.XSHG',  # 300ETF
+        start_date='2024-01-01',
+        end_date='2025-06-30'
+    )
+
+    # 获取ETF价格数据
+    try:
+        etf_prices = get_price(strategy.underlying, strategy.start_date, strategy.end_date, fields=['close'])['close']
+        print(f"成功获取ETF价格数据,共{len(etf_prices)}个交易日")
+
+        # 测试前几个交易日的开仓逻辑
+        test_dates = strategy.trade_days[:10]  # 只测试前10个交易日
+
+        for i, date in enumerate(test_dates):
+            if date not in etf_prices.index:
+                continue
+
+            etf_price = etf_prices[date]
+            print(f"\n第{i+1}个交易日测试: {date}, ETF价格: {etf_price:.4f}")
+
+            # 尝试开仓
+            position = strategy.open_bull_spread_position(date, etf_price)
+
+            if position:
+                print(f"✓ 成功开仓!")
+                print(f"  持仓总数: {len(strategy.positions)}")
+                break
+            else:
+                print(f"✗ 开仓失败")
+
+        # 打印最终结果
+        print(f"\n测试结果:")
+        print(f"总持仓数: {len(strategy.positions)}")
+        print(f"总交易数: {len(strategy.trades)}")
+
+        if strategy.positions:
+            print(f"第一个持仓详情:")
+            pos = strategy.positions[0]
+            for key, value in pos.items():
+                print(f"  {key}: {value}")
+
+        return strategy
+
+    except Exception as e:
+        print(f"测试过程中出现错误: {e}")
+        import traceback
+        traceback.print_exc()
+        return None
+
+
+def test_strategy():
+    """测试策略"""
+    print("开始测试深度实值牛差策略...")
+
+    # 创建策略实例
+    strategy = DeepITMBullSpreadStrategy(
+        underlying='510300.XSHG',  # 300ETF
+        start_date='2024-01-01',
+        end_date='2025-06-30'
+    )
+
+    # 运行回测
+    try:
+        print("正在运行回测...")
+        strategy.run_backtest()
+
+        print("正在生成详细报告...")
+        # 生成详细报告(包含买购和卖购分项分析)
+        detailed_results = strategy.generate_detailed_report()
+
+        print("正在导出数据...")
+        # 导出数据到CSV
+        positions_df, trades_df, daily_df = strategy.export_data_to_csv("bull_spread_300etf")
+
+        print("正在打印持仓详情...")
+        # 打印详细持仓信息
+        strategy.print_position_details()
+
+        return strategy, detailed_results
+
+    except Exception as e:
+        print(f"回测过程中出现错误: {e}")
+        import traceback
+        traceback.print_exc()
+        return None, None
+
+
+def analyze_bull_spread_example():
+    """分析牛差策略示例"""
+    print("\n" + "="*60)
+    print("牛差策略期权组合分析示例")
+    print("="*60)
+
+    # 假设当前300ETF价格为4.0
+    underlying_price = 4.0
+
+    # 根据策略逻辑选择期权
+    buy_strike = underlying_price * 0.90  # 深度实值买购
+    sell_strike = underlying_price * 1.00  # 平值卖购
+
+    # 估算权利金(简化)
+    buy_premium = 0.44  # 深度实值期权,主要是内在价值
+    sell_premium = 0.20  # 平值期权,主要是时间价值
+
+    print(f"当前标的价格: {underlying_price}")
+    print(f"买购期权: 行权价 {buy_strike:.2f}, 权利金 {buy_premium:.4f}")
+    print(f"卖购期权: 行权价 {sell_strike:.2f}, 权利金 {sell_premium:.4f}")
+
+    # 使用期权分析工具
+    analyzer = OptionsAnalyzer()
+    result = analyzer.analyze_options(
+        ('buy', 'call', buy_premium, buy_strike, 1),
+        ('sell', 'call', sell_premium, sell_strike, 1)
+    )
+
+    return result
+
+
+def compare_with_etf_holding():
+    """三个策略分别与ETF持有收益对比"""
+    print("\n" + "="*80)
+    print("三个策略分别与ETF持有收益对比")
+    print("="*80)
+
+    initial_price = 4.0
+    initial_investment = 100000
+
+    # 牛差策略参数
+    buy_strike = initial_price * 0.90
+    sell_strike = initial_price * 1.00
+    buy_premium = 0.44
+    sell_premium = 0.20
+
+    # 计算可开仓张数
+    net_premium = buy_premium - sell_premium
+    contracts = int(initial_investment / (net_premium * 10000))
+
+    print(f"初始投资: {initial_investment:,.0f}元")
+    print(f"初始ETF价格: {initial_price:.2f}")
+    print(f"牛差组合: 买购{buy_strike:.2f}@{buy_premium:.4f}, 卖购{sell_strike:.2f}@{sell_premium:.4f}")
+    print(f"可开仓张数: {contracts}张")
+
+    # 模拟不同价格变化下的收益
+    price_changes = np.arange(-20, 21, 5)
+    results = []
+
+    for change in price_changes:
+        new_price = initial_price * (1 + change / 100)
+
+        # ETF持有收益
+        etf_return = (new_price - initial_price) / initial_price * initial_investment
+
+        # 分别计算三个策略的收益
+        buy_call_value = max(0, new_price - buy_strike)
+        sell_call_value = max(0, new_price - sell_strike)
+
+        # 1. 买购期权收益
+        buy_call_pnl = (buy_call_value - buy_premium) * contracts * 10000
+
+        # 2. 卖购期权收益
+        sell_call_pnl = (sell_premium - sell_call_value) * contracts * 10000
+
+        # 3. 牛差组合收益
+        bull_spread_return = buy_call_pnl + sell_call_pnl
+
+        results.append({
+            'price_change': change,
+            'new_price': new_price,
+            'etf_return': etf_return,
+            'buy_call_return': buy_call_pnl,
+            'sell_call_return': sell_call_pnl,
+            'bull_spread_return': bull_spread_return,
+            'bull_vs_etf': bull_spread_return - etf_return,
+            'buy_vs_etf': buy_call_pnl - etf_return,
+            'sell_vs_etf': sell_call_pnl - etf_return
+        })
+
+    # 创建对比表格
+    df = pd.DataFrame(results)
+
+    print(f"\n=== 详细收益对比表 ===")
+    print("价格变化(%) | 新价格 | ETF收益 | 买购收益 | 卖购收益 | 牛差收益")
+    print("-" * 75)
+
+    for _, row in df.iterrows():
+        print(f"{row['price_change']:>8.0f}% | {row['new_price']:>6.2f} | "
+              f"{row['etf_return']:>7.0f} | {row['buy_call_return']:>8.0f} | "
+              f"{row['sell_call_return']:>8.0f} | {row['bull_spread_return']:>8.0f}")
+
+    print(f"\n=== 与ETF基准的差异对比 ===")
+    print("价格变化(%) | 牛差-ETF | 买购-ETF | 卖购-ETF")
+    print("-" * 50)
+
+    for _, row in df.iterrows():
+        print(f"{row['price_change']:>8.0f}% | {row['bull_vs_etf']:>8.0f} | "
+              f"{row['buy_vs_etf']:>8.0f} | {row['sell_vs_etf']:>8.0f}")
+
+    # 绘制三个对比图
+    # 设置中文字体
+    plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
+    plt.rcParams['axes.unicode_minus'] = False
+
+    plt.figure(figsize=(15, 12))
+
+    # 第一个子图:牛差策略 vs ETF
+    plt.subplot(3, 1, 1)
+    plt.plot(df['price_change'], df['etf_return'], 'o-', linewidth=2, label='ETF持有', color='orange')
+    plt.plot(df['price_change'], df['bull_spread_return'], 's-', linewidth=2, label='牛差策略', color='green')
+    plt.axhline(0, color='gray', linestyle='--', alpha=0.5)
+    plt.xlabel('价格变化 (%)')
+    plt.ylabel('收益 (元)')
+    plt.title('牛差策略 vs ETF持有收益对比')
+    plt.legend()
+    plt.grid(True, alpha=0.3)
+
+    # 第二个子图:买购策略 vs ETF
+    plt.subplot(3, 1, 2)
+    plt.plot(df['price_change'], df['etf_return'], 'o-', linewidth=2, label='ETF持有', color='orange')
+    plt.plot(df['price_change'], df['buy_call_return'], '^-', linewidth=2, label='买购期权', color='blue')
+    plt.axhline(0, color='gray', linestyle='--', alpha=0.5)
+    plt.xlabel('价格变化 (%)')
+    plt.ylabel('收益 (元)')
+    plt.title('买购期权 vs ETF持有收益对比')
+    plt.legend()
+    plt.grid(True, alpha=0.3)
+
+    # 第三个子图:卖购策略 vs ETF
+    plt.subplot(3, 1, 3)
+    plt.plot(df['price_change'], df['etf_return'], 'o-', linewidth=2, label='ETF持有', color='orange')
+    plt.plot(df['price_change'], df['sell_call_return'], 'v-', linewidth=2, label='卖购期权', color='red')
+    plt.axhline(0, color='gray', linestyle='--', alpha=0.5)
+    plt.xlabel('价格变化 (%)')
+    plt.ylabel('收益 (元)')
+    plt.title('卖购期权 vs ETF持有收益对比')
+    plt.legend()
+    plt.grid(True, alpha=0.3)
+
+    plt.tight_layout()
+    plt.show()
+
+    return df
+
+
+if __name__ == "__main__":
+    print("深度实值牛差策略综合测试工具")
+    print("="*50)
+
+    # 0. 首先测试开仓逻辑
+    print("\n0. 测试开仓逻辑:")
+    test_strategy_instance = test_opening_logic()
+
+    if test_strategy_instance and len(test_strategy_instance.positions) > 0:
+        print("✓ 开仓逻辑测试通过,继续完整回测")
+
+        # 1. 运行策略回测
+        print("\n1. 运行策略回测:")
+        strategy, detailed_results = test_strategy()
+
+        # 2. 分析期权组合
+        print("\n2. 期权组合分析:")
+        option_analysis = analyze_bull_spread_example()
+
+        # 3. 与ETF持有对比
+        print("\n3. 与ETF持有收益对比:")
+        comparison_results = compare_with_etf_holding()
+
+        print(f"\n测试完成!数据已导出到CSV文件,可用于进一步分析。")
+    else:
+        print("✗ 开仓逻辑测试失败,请检查期权数据和策略参数")
+        print("建议检查以下几点:")
+        print("1. 期权合约数据是否可用")
+        print("2. 策略参数设置是否合理")
+        print("3. 时间价值阈值是否过于严格")

+ 219 - 0
Lib/Options/product_T_price.py

@@ -0,0 +1,219 @@
+import pandas as pd
+import numpy as np
+import talib as tb
+from pandas.plotting import  table
+from jqdata import *
+
+# 期权合约信息
+class OptionContract:
+    
+    close: 0 #现价
+    volume: 0 #成交量
+    position: 0 #当前持仓量
+    priceChangePct: 0 #收盘价涨跌幅
+    intrinsic_value: 0 #内在价值
+    time_value: 0 #时间价值
+        
+        
+    def __init__(self,close, volume, position, priceChangePct, intrinsic_value=0, time_value=0):
+        self.close = close
+        self.volume = volume
+        self.position = position
+        self.priceChangePct = priceChangePct
+        self.intrinsic_value = intrinsic_value
+        self.time_value = time_value
+
+
+        
+# 同一行权价的一组期权
+class OptionGroup:
+    
+    exercisePrice: 0 ##行权价
+    call: None ##提醒的品种
+    put: None  ##提醒的类型
+        
+    def __init__(self, exercisePrice, call, put):
+        self.exercisePrice = exercisePrice
+        self.call = call
+        self.put = put
+        
+    def __init__(self, exercisePrice):
+        self.exercisePrice = exercisePrice
+
+#数据内容
+
+CONTENT = [
+    'close', 
+    'volume', 
+    'position', 
+    'priceChangePct', 
+]
+
+
+##数据域
+CONTENT_HEADER_MAP = {
+    'prePosition': '昨日持仓',
+    'close': '现价', 
+    'volume': '成交量', 
+    'position': '持仓量', 
+    'priceChangePct': '涨跌幅'
+}
+
+
+# 豆粕期权
+symbol = 'AU'
+
+starttime =  '2025-06-30'
+endtime = '2025-07-14'
+
+SUBJECT_MATTER = get_dominant_future(symbol,date = starttime)
+print(f"SUBJECT_MATTER: {SUBJECT_MATTER}")
+
+#m2407 匹配 2024-04-19
+EXPIRE_MONTH = '2025-09-24'
+
+## 行权价间隔 比如3800 3750 每50元一档
+gap = 8
+
+# 获取期货价格
+try:
+    # 获取期货价格数据
+    future_price_data = get_price(SUBJECT_MATTER, end_date=endtime, count=1, fields=['close'])
+    current_future_price = float(future_price_data['close'].iloc[-1])
+except Exception as e:
+    print(f"获取期货价格失败: {e}")
+    current_future_price = 781.4  # 使用模拟价格
+
+print(f"标的期货价格: {current_future_price}")
+
+#查询相关的合约,适用于商品
+
+qy = query(opt.OPT_CONTRACT_INFO).filter(
+    opt.OPT_CONTRACT_INFO.underlying_symbol == SUBJECT_MATTER, ##期权标的物
+    opt.OPT_CONTRACT_INFO.expire_date == EXPIRE_MONTH,##期权到期日
+).order_by(opt.OPT_CONTRACT_INFO.exercise_price, opt.OPT_CONTRACT_INFO.contract_type)
+
+optList = opt.run_query(qy)
+
+optList
+
+code = SUBJECT_MATTER.split('.')[0]
+print(f"code: {code}")
+
+# optList.to_csv(f'price_list_{code}.csv')
+
+# 获取实时数据
+optionGroups = {}
+
+for index, row in optList.iterrows():
+    
+    code = row['code'] #key - 期权代码
+#     print(f"检查{code}的数据")
+    #查询具体合约的信息,商品需要去掉开盘前的静态信息
+    realTimeQuery = query(opt.OPT_DAILY_PRICE).filter(
+        opt.OPT_DAILY_PRICE.code==code, 
+        opt.OPT_DAILY_PRICE.date==endtime
+    ).order_by(opt.OPT_DAILY_PRICE.date.desc()).limit(1)
+    realTimeData = opt.run_query(realTimeQuery)
+#     print(f"realTimeQuery: {realTimeQuery}")
+#     print(f"realTimeData: {realTimeData}")
+    
+    #期权基本信息
+    exercisePrice = row['exercise_price'] #行权价
+    contractType = row['contract_type'] #合约类型。CO-认购期权,PO-认沽期权
+    
+    #实时表查询
+    close = realTimeData.loc[0].at['close'] #现价
+    volume = int(realTimeData.loc[0].at['volume']) #成交量
+    position = realTimeData.loc[0].at['position'] #当前持仓量
+    priceChangePct = str(round(realTimeData.loc[0].at['change_pct_close'], 2)) + '%' #收盘价涨跌幅
+        
+    #去除非标准的带A合约
+    if(exercisePrice  % gap != 0):
+        continue
+        
+    # 计算内在价值和时间价值
+    if contractType == 'CO':  # 认购期权
+        intrinsic_value = max(current_future_price - exercisePrice, 0)
+        time_value = close - intrinsic_value
+        print(f"处理认购期权, 期货价格: {current_future_price}, 行权价:{exercisePrice}")
+        print(f"获得内在价值: {intrinsic_value}, 时间价值: {time_value}")
+    else:  # 认沽期权
+        intrinsic_value = max(exercisePrice - current_future_price, 0)
+        time_value = close - intrinsic_value
+        print(f"处理认沽期权, 期货价格: {current_future_price}, 行权价:{exercisePrice}")
+        print(f"获得内在价值: {intrinsic_value}, 时间价值: {time_value}")
+        
+    optionContract = OptionContract(close, volume, position, priceChangePct, intrinsic_value, time_value)
+    
+    if(exercisePrice in optionGroups):
+        
+        if(contractType == 'CO'):
+            optionGroups[exercisePrice].call = optionContract
+        else:
+            optionGroups[exercisePrice].put = optionContract
+            
+    else:
+        
+        optionGroup = OptionGroup(exercisePrice)
+        
+        if(contractType == 'CO'):
+            optionGroup.call = optionContract
+        else:
+            optionGroup.put = optionContract
+            
+        optionGroups[exercisePrice] = optionGroup
+
+# 使用optionGroups构造T型报价表
+data = []
+
+for exercisePrice in sorted(optionGroups.keys()):
+    group = optionGroups[exercisePrice]
+#     print(f"exercisePrice: {exercisePrice}, group: {group}")
+    
+    # 认购期权数据
+    call_price = group.call.close if group.call else None
+    call_volume = group.call.volume if group.call else None
+    call_position = group.call.position if group.call else None
+    call_change = group.call.priceChangePct if group.call else None
+    call_intrinsic = group.call.intrinsic_value if group.call else None
+#     print(f"exercisePrice: {exercisePrice}, call_intrinsic: {call_intrinsic}")
+    call_time = group.call.time_value if group.call else None
+    
+    # 认沽期权数据
+    put_price = group.put.close if group.put else None
+    put_volume = group.put.volume if group.put else None
+    put_position = group.put.position if group.put else None
+    put_change = group.put.priceChangePct if group.put else None
+    put_intrinsic = group.put.intrinsic_value if group.put else None
+    put_time = group.put.time_value if group.put else None
+    
+    data.append({
+        '认购涨跌幅': call_change,
+#         '认购持仓量': call_position,
+#         '认购成交量': call_volume,
+        '认购时间价值': call_time,
+        '认购内在价值': call_intrinsic,
+        '认购价格': call_price,
+        '行权价': exercisePrice,
+        '认沽价格': put_price,
+        '认沽内在价值': put_intrinsic,
+        '认沽时间价值': put_time,
+#         '认沽成交量': put_volume,
+#         '认沽持仓量': put_position,
+        '认沽涨跌幅': put_change
+    })
+
+# 创建T型报价DataFrame
+df = pd.DataFrame(data)
+# 确保df的列顺序为:'认购涨跌幅','认购持仓量','认购成交量','认购时间价值','认购内在价值','认购价格','行权价','认沽价格','认沽内在价值','认沽时间价值','认沽成交量','认沽持仓量','认沽涨跌幅'
+df = df[['认购涨跌幅', '认购时间价值', '认购内在价值', '认购价格', '行权价', '认沽价格', '认沽内在价值', '认沽时间价值', '认沽涨跌幅']]
+
+print("\n" + "="*100)
+print("期权T型报价表(含内在价值和时间价值)")
+print("="*100)
+print(f"标的期货价格: {current_future_price}")
+print("="*100)
+
+# 显示T型报价
+print(df.to_string(index=False))

+ 1 - 1
Lib/fund/FundPremium_DynamicPosition.py

@@ -49,7 +49,7 @@ def initialize(context):
     run_daily(before_market_open, '09:20', reference_security='000300.XSHG')
     run_daily(market_open, '09:30', reference_security='000300.XSHG')
     run_daily(check_loss_up, time='14:10', reference_security='000300.XSHG')
-    # run_daily(print_position_info, time='15:10', reference_security='000300.XSHG')
+    run_daily(print_position_info, time='15:10', reference_security='000300.XSHG')
     # end_check_time = time.time()
     # elapsed_time = end_check_time - start_check_time
     # print(f"initialize time: {elapsed_time}")

+ 530 - 147
Lib/future/MultiMABreakoutStrategy_v001.py

@@ -44,42 +44,107 @@ def initialize(context):
     g.max_ma_crosses = 4  # 最大允许的均线交叉数量
     g.ma_cross_check_days = 10  # 检查均线交叉的天数
     
-    # 定义默认的保证金比例
-    g.margin_rates = {
-        'long': {'A': 0.07, 'AG': 0.04, 'AL': 0.05, 'AO': 0.05, 'AP': 0.08, 'AU': 0.04, 'B': 0.05,
-        'BC': 0.13, 'BR': 0.07, 'BU': 0.04, 'C': 0.07, 'CF': 0.05, 'CJ': 0.07, 'CS': 0.07,
-        'CU': 0.05, 'CY': 0.05, 'EB': 0.12, 'EC': 0.12, 'EG': 0.05, 'FG': 0.05, 'FU': 0.08,
-        'HC': 0.04, 'I': 0.1, 'J': 0.22, 'JD': 0.08, 'JM': 0.22, 
-        'L': 0.07, 'LC': 0.05, 'LH': 0.1, 'LR': 0.05, 'LU': 0.15, 'M': 0.07, 'MA': 0.05, 'NI': 0.05, 'NR': 0.13, 'OI': 0.05,
-        'P': 0.05, 'PB': 0.05, 'PF': 0.1, 'PG': 0.05, 'PK': 0.05,
-        'PP': 0.07, 'RB': 0.05, 'RI': 0.05, 'RM': 0.05, 'RU': 0.05,
-        'SA': 0.05, 'SC': 0.12, 'SF': 0.05, 'SH': 0.05, 'SI': 0.13, 'SM': 0.05, 'SN': 0.05, 'SP': 0.1, 'SR': 0.05,
-        'SS': 0.05, 'TA': 0.05, 'UR': 0.09, 'V': 0.07,
-        'Y': 0.05, 'ZC': 0.05, 'ZN': 0.05}, 
-        'short': {'A': 0.07, 'AG': 0.04, 'AL': 0.05, 'AO': 0.05, 'AP': 0.08, 'AU': 0.04, 'B': 0.05,
-        'BC': 0.13, 'BR': 0.07, 'BU': 0.04, 'C': 0.07, 'CF': 0.05, 'CJ': 0.07, 'CS': 0.07,
-        'CU': 0.05, 'CY': 0.05, 'EB': 0.12, 'EC': 0.12, 'EG': 0.05, 'FG': 0.05, 'FU': 0.08,
-        'HC': 0.04, 'I': 0.1, 'J': 0.22, 'JD': 0.08, 'JM': 0.22, 
-        'L': 0.07, 'LC': 0.05, 'LH': 0.1, 'LR': 0.05, 'LU': 0.15, 'M': 0.07, 'MA': 0.05, 'NI': 0.05, 'NR': 0.13, 'OI': 0.05,
-        'P': 0.05, 'PB': 0.05, 'PF': 0.1, 'PG': 0.05, 'PK': 0.05,
-        'PP': 0.07, 'RB': 0.05, 'RI': 0.05, 'RM': 0.05, 'RU': 0.05,
-        'SA': 0.05, 'SC': 0.12, 'SF': 0.05, 'SH': 0.05, 'SI': 0.13, 'SM': 0.05, 'SN': 0.05, 'SP': 0.1, 'SR': 0.05,
-        'SS': 0.05, 'TA': 0.05, 'UR': 0.09, 'V': 0.07,
-        'Y': 0.05, 'ZC': 0.05, 'ZN': 0.05}
+    # 止损止盈策略参数
+    g.gap_ratio_threshold = 0.002  # 跳空比例:0.2%
+    g.market_close_times = ["14:55:00"]  # 盘尾时间
+    g.profit_thresholds = [5000, 15000]  # 价格分区
+    g.stop_ratios = [0.0025, 0.005, 0.01, 0.02]  # 止盈止损比例:[0.25%, 0.5%, 1%, 2%]
+    
+    # 期货品种完整配置字典
+    g.futures_config = {
+        # 贵金属
+        'AU': {'has_night_session': True, 'margin_rate': {'long': 0.04, 'short': 0.04}, 'multiplier': 1000},
+        'AG': {'has_night_session': True, 'margin_rate': {'long': 0.04, 'short': 0.04}, 'multiplier': 15},
+        
+        # 有色金属
+        'CU': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 5},
+        'AL': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 5},
+        'ZN': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 5},
+        'PB': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 5},
+        'NI': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 1},
+        'SN': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 1},
+        'SS': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 5},
+        
+        # 黑色系
+        'RB': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 10},
+        'HC': {'has_night_session': True, 'margin_rate': {'long': 0.04, 'short': 0.04}, 'multiplier': 10},
+        'I': {'has_night_session': True, 'margin_rate': {'long': 0.1, 'short': 0.1}, 'multiplier': 100},
+        'JM': {'has_night_session': True, 'margin_rate': {'long': 0.22, 'short': 0.22}, 'multiplier': 100},
+        'J': {'has_night_session': True, 'margin_rate': {'long': 0.22, 'short': 0.22}, 'multiplier': 60},
+        
+        # 能源化工
+        'SP': {'has_night_session': True, 'margin_rate': {'long': 0.1, 'short': 0.1}, 'multiplier': 10},
+        'FU': {'has_night_session': True, 'margin_rate': {'long': 0.08, 'short': 0.08}, 'multiplier': 10},
+        'BU': {'has_night_session': True, 'margin_rate': {'long': 0.04, 'short': 0.04}, 'multiplier': 10},
+        'RU': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 10},
+        'BR': {'has_night_session': True, 'margin_rate': {'long': 0.07, 'short': 0.07}, 'multiplier': 5},
+        'AO': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 20},
+        'SC': {'has_night_session': True, 'margin_rate': {'long': 0.12, 'short': 0.12}, 'multiplier': 1000},
+        'NR': {'has_night_session': True, 'margin_rate': {'long': 0.13, 'short': 0.13}, 'multiplier': 10},
+        'LU': {'has_night_session': True, 'margin_rate': {'long': 0.15, 'short': 0.15}, 'multiplier': 10},
+        'BC': {'has_night_session': True, 'margin_rate': {'long': 0.13, 'short': 0.13}, 'multiplier': 5},
+        
+        # 化工
+        'FG': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 20},
+        'TA': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 5},
+        'MA': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 10},
+        'SA': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 20},
+        'L': {'has_night_session': True, 'margin_rate': {'long': 0.07, 'short': 0.07}, 'multiplier': 5},
+        'V': {'has_night_session': True, 'margin_rate': {'long': 0.07, 'short': 0.07}, 'multiplier': 5},
+        'EG': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 10},
+        'PP': {'has_night_session': True, 'margin_rate': {'long': 0.07, 'short': 0.07}, 'multiplier': 5},
+        'EB': {'has_night_session': True, 'margin_rate': {'long': 0.12, 'short': 0.12}, 'multiplier': 5},
+        'PG': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 20},
+        'CY': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 5},
+        'SH': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 30},
+        'PX': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 5},
+        
+        # 农产品
+        'RM': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 10},
+        'OI': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 10},
+        'CF': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 5},
+        'SR': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 10},
+        'PF': {'has_night_session': True, 'margin_rate': {'long': 0.1, 'short': 0.1}, 'multiplier': 5},
+        'C': {'has_night_session': True, 'margin_rate': {'long': 0.07, 'short': 0.07}, 'multiplier': 10},
+        'CS': {'has_night_session': True, 'margin_rate': {'long': 0.07, 'short': 0.07}, 'multiplier': 10},
+        'A': {'has_night_session': True, 'margin_rate': {'long': 0.07, 'short': 0.07}, 'multiplier': 10},
+        'B': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 10},
+        'M': {'has_night_session': True, 'margin_rate': {'long': 0.07, 'short': 0.07}, 'multiplier': 10},
+        'Y': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 10},
+        'P': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 10},
+        'PR': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 10},
+        'AD': {'has_night_session': True, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 10},
+        
+        # 无夜盘品种
+        'IF': {'has_night_session': False, 'margin_rate': {'long': 0.08, 'short': 0.08}, 'multiplier': 300},
+        'IH': {'has_night_session': False, 'margin_rate': {'long': 0.08, 'short': 0.08}, 'multiplier': 300},
+        'IC': {'has_night_session': False, 'margin_rate': {'long': 0.08, 'short': 0.08}, 'multiplier': 200},
+        'IM': {'has_night_session': False, 'margin_rate': {'long': 0.08, 'short': 0.08}, 'multiplier': 200},
+        'EC': {'has_night_session': False, 'margin_rate': {'long': 0.12, 'short': 0.12}, 'multiplier': 50},
+        'SF': {'has_night_session': False, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 5},
+        'SM': {'has_night_session': False, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 5},
+        'UR': {'has_night_session': False, 'margin_rate': {'long': 0.09, 'short': 0.09}, 'multiplier': 20},
+        'AP': {'has_night_session': False, 'margin_rate': {'long': 0.08, 'short': 0.08}, 'multiplier': 10},
+        'CJ': {'has_night_session': False, 'margin_rate': {'long': 0.07, 'short': 0.07}, 'multiplier': 5},
+        'PK': {'has_night_session': False, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 5},
+        'JD': {'has_night_session': False, 'margin_rate': {'long': 0.08, 'short': 0.08}, 'multiplier': 5},
+        'LH': {'has_night_session': False, 'margin_rate': {'long': 0.1, 'short': 0.1}, 'multiplier': 16},
+        'SI': {'has_night_session': False, 'margin_rate': {'long': 0.13, 'short': 0.13}, 'multiplier': 5},
+        'LC': {'has_night_session': False, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 1},
+        'PS': {'has_night_session': False, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 5},
+        'LG': {'has_night_session': False, 'margin_rate': {'long': 0.05, 'short': 0.05}, 'multiplier': 5}
     }
     
-    g.multiplier = {
-        'A': 10, 'AG': 15, 'AL': 5, 'AO': 20, 'AP': 10, 'AU': 1000, 'B': 10,
-        'BC': 5, 'BR': 5, 'BU': 10, 'C': 10, 'CF': 5, 'CJ': 5, 'CS': 10,
-        'CU': 5, 'CY': 5, 'EB': 5, 'EC': 50, 'EG': 10, 'FG': 20, 'FU': 10,
-        'HC': 10, 'I': 100, 'J': 60, 'JD': 5, 'JM': 100, 
-        'L': 5, 'LC': 1, 'LH': 16, 'LR': 0.05, 'LU': 10, 'M': 10, 'MA': 10, 'NI': 1, 'NR': 10, 'OI': 10,
-        'P': 10, 'PB': 5, 'PF': 5, 'PG': 20, 'PK': 5,
-        'PP': 5, 'RB': 10, 'RI': 0.05, 'RM': 10, 'RU': 10,
-        'SA': 20, 'SC': 1000, 'SF': 5, 'SH': 30, 'SI': 5, 'SM': 5, 'SN': 1, 'SP': 10, 'SR': 10,
-        'SS': 5, 'TA': 5, 'UR': 20, 'V': 5,
-        'Y': 10, 'ZC': 0.05, 'ZN': 5
-    }
+    # 当前策略关注的标的列表(可以根据需要调整,为空则考虑所有品种)
+    g.strategy_focus_symbols = ['RM']
+    
+    # 如果关注列表为空,则使用所有配置的品种
+    if not g.strategy_focus_symbols:
+        g.strategy_focus_symbols = list(g.futures_config.keys())
+        log.info("策略关注品种列表为空,将考虑所有配置的品种")
+    
+    # 打印配置摘要
+    log.info(f"策略关注品种总数: {len(g.strategy_focus_symbols)}")
     
     # 交易记录和数据存储
     g.trade_history = {}
@@ -88,8 +153,9 @@ def initialize(context):
     g.minute_data_cache = {}  # 存储今日分钟数据缓存
     g.ma_data_cache = {}  # 存储均线数据缓存
     g.ma_cross_filtered_futures = {}  # 存储通过均线交叉检查的品种(每日缓存)
+    g.gap_check_results = {}  # 存储跳空检查结果(每日缓存)
     
-    # 保证金比例管理(g.margin_rates会根据实际交易校准)
+    # 保证金比例管理(g.futures_config中的保证金比例会根据实际交易校准)
     g.margin_rate_history = {}  # 保证金比例变化历史记录
     g.today_trades = []  # 当日交易记录
     
@@ -236,19 +302,23 @@ def task_1_get_tradable_futures(context):
     """任务1: 获取所有可交易品种(分白天和晚上)"""
     # log.info("执行任务1: 获取可交易品种")
     
-    # 夜盘品种
-    # potential_night_list = ['NI', 'CF', 'PF', 'Y', 'M', 'B', 'SN', 'RM', 'RB', 'HC', 'I', 'J', 'JM']
-    potential_night_list = ['PF', 'Y', 'SN']
-    # 日盘品种  
-    potential_day_list = ['PK']
-    # potential_day_list = ['JD', 'UR', 'AP', 'CJ', 'PK']
-    
     current_time = str(context.current_dt.time())[:2]
+    
+    # 从策略关注列表中筛选可交易品种
+    # 如果关注列表为空,则使用所有配置的品种
+    focus_symbols = g.strategy_focus_symbols if g.strategy_focus_symbols else list(g.futures_config.keys())
+    
+    potential_icon_list = []
+    
     if current_time in ('21', '22'):
-        potential_icon_list = potential_night_list
-        log.info(f"夜盘时间,可交易品种: {potential_night_list}")
+        # 夜盘时间:只考虑有夜盘的品种
+        for symbol in focus_symbols:
+            if get_futures_config(symbol, 'has_night_session', False):
+                potential_icon_list.append(symbol)
+        log.info(f"夜盘时间,可交易品种: {potential_icon_list}")
     else:
-        potential_icon_list = potential_day_list + potential_night_list
+        # 日盘时间:所有关注的品种都可以交易
+        potential_icon_list = focus_symbols[:]
         log.info(f"日盘时间,可交易品种: {potential_icon_list}")
     
     potential_future_list = []
@@ -349,14 +419,28 @@ def task_3_check_ma_crosses(context):
     return filtered_futures
 
 def task_4_update_realtime_data(context):
-    """任务4: 获取所有可交易品种今天所需的分钟数据作为今天数据,和2中的数据合并出最新的均线数据,并保存到内存中"""
-    # log.info("执行任务4: 更新实时数据和均线")
+    """任务4: 获取所有可交易品种和持仓品种今天所需的分钟数据作为今天数据,和2中的数据合并出最新的均线数据,并保存到内存中"""
+    log.info("执行任务4: 更新实时数据和均线")
     
-    if not hasattr(g, 'tradable_futures'):
+    # 收集需要更新数据的品种
+    update_symbols = set()
+    
+    # 添加可交易品种
+    if hasattr(g, 'tradable_futures') and g.tradable_futures:
+        update_symbols.update(g.tradable_futures)
+    
+    # 添加持仓品种(用于止损止盈)
+    if hasattr(g, 'trade_history') and g.trade_history:
+        update_symbols.update(g.trade_history.keys())
+    
+    if not update_symbols:
+        log.info("没有需要更新的品种")
         return
     
-    for future_code in g.tradable_futures:
-        log.info(f"任务4 future_code: {future_code}")
+    today_date = context.current_dt.date()
+    
+    for future_code in update_symbols:
+        # log.info(f"任务4 future_code: {future_code}")
         try:
             # 获取今日分钟数据
             minute_data = get_today_minute_data(context, future_code)
@@ -364,11 +448,35 @@ def task_4_update_realtime_data(context):
             if minute_data is None:
                 continue
             
-            # 获取历史数据
+            # 获取历史数据,如果缓存中没有则现场获取
             historical_data = g.daily_data_cache.get(future_code)
+            if historical_data is None:
+                # 为持仓品种临时获取历史数据
+                try:
+                    data = attribute_history(future_code, 50, '1d', 
+                                           ['open', 'close', 'high', 'low', 'volume'], 
+                                           df=True)
+                    if data is not None and len(data) > 0:
+                        # 排除今天的数据
+                        today = context.current_dt.date()
+                        data = data[data.index.date < today]
+                        g.daily_data_cache[future_code] = data
+                        historical_data = data
+                        log.info(f"为持仓品种 {future_code} 临时获取历史数据 {len(data)} 条")
+                except Exception as e:
+                    log.warning(f"获取{future_code}历史数据失败: {str(e)}")
+                    continue
+            
             if historical_data is None:
                 continue
             
+            # 检查跳空(只在第一次获取今日数据时检查)
+            if future_code not in g.gap_check_results:
+                gap_result = check_gap_opening(historical_data, minute_data)
+                g.gap_check_results[future_code] = gap_result
+                if gap_result['has_gap']:
+                    log.info(f"{future_code} 检测到跳空开盘,跳空比例: {gap_result['gap_ratio']:.3%}")
+            
             # 合并数据并计算均线
             combined_data = combine_and_calculate_ma(historical_data, minute_data)
             if combined_data is not None:
@@ -379,7 +487,7 @@ def task_4_update_realtime_data(context):
             log.warning(f"更新{future_code}实时数据时出错: {str(e)}")
             continue
     
-    # log.info(f"实时数据更新完成,共更新 {len(g.ma_data_cache)} 个品种")
+    log.info(f"实时数据更新完成,共更新 {len(g.ma_data_cache)} 个品种(可交易: {len(g.tradable_futures) if hasattr(g, 'tradable_futures') else 0},持仓: {len(g.trade_history) if hasattr(g, 'trade_history') else 0})")
 
 def task_5_analyze_ma_crosses(context):
     """任务5: 根据交易品种的今天数据和均线数据,判断是否出现了上穿或者下穿的情况"""
@@ -473,7 +581,9 @@ def task_7_execute_trades(context, filtered_signals):
         if success:
             # 获取实际保证金(只有在开仓成功时才能获取)
             actual_margin = g.trade_history[symbol]['actual_margin']
-            log.info(f"成功开仓 {symbol} {direction} 金额: {order_value}, 实际保证金: {actual_margin:.0f}")
+            # 获取成交价格
+            actual_price = g.trade_history[symbol]['entry_price']
+            log.info(f"成功开仓 {symbol} {direction}, 成交价格: {actual_price:.2f}, 金额: {order_value}, 实际保证金: {actual_margin:.0f}")
         else:
             log.warning(f"开仓失败 {symbol} {direction}")
 
@@ -512,14 +622,7 @@ def task_9_check_stop_loss_profit(context):
 
 def check_has_night_session(underlying_symbol):
     """检查品种是否有夜盘"""
-    # 有夜盘的品种列表
-    night_session_symbols = {
-        'NI', 'CF', 'PF', 'Y', 'M', 'B', 'SN', 'RM', 'RB', 'HC', 'I', 'J', 'JM',
-        'A', 'AG', 'AL', 'AU', 'BU', 'C', 'CU', 'FU', 'L', 'P', 'PB', 'RU', 
-        'SC', 'SP', 'SS', 'TA', 'V', 'ZC', 'ZN', 'MA', 'SR', 'OI', 'CF', 'RM',
-        'FG', 'SA', 'UR', 'NR', 'LU', 'BC', 'EC', 'PP', 'EB', 'EG', 'PG'
-    }
-    return underlying_symbol in night_session_symbols
+    return get_futures_config(underlying_symbol, 'has_night_session', False)
 
 def get_today_minute_data(context, future_code):
     """获取今日分钟数据"""
@@ -540,12 +643,12 @@ def get_today_minute_data(context, future_code):
         if minute_data is None or len(minute_data) == 0:
             return None
         
-        # log.debug(f"原始分钟数据范围: {minute_data.index[0]} 到 {minute_data.index[-1]}")
+        log.debug(f"原始分钟数据范围: {minute_data.index[0]} 到 {minute_data.index[-1]}")
         
         # 提取所有日期(年月日维度)
         minute_data['date'] = minute_data.index.date
         unique_dates = sorted(minute_data['date'].unique())
-        # log.debug(f"数据包含的日期: {unique_dates}")
+        log.debug(f"数据包含的日期: {unique_dates}")
         
         if has_night_session:
             # 有夜盘的品种:需要找到前一交易日的21:00作为今日开盘起点
@@ -553,13 +656,13 @@ def get_today_minute_data(context, future_code):
             
             # 找到今天之前的最后一个交易日
             previous_trading_dates = [d for d in unique_dates if d < today_date]
-            # log.debug(f"夜盘标的 today_date: {today_date}, previous_trading_dates: {previous_trading_dates}")
+            log.debug(f"夜盘标的 today_date: {today_date}, previous_trading_dates: {previous_trading_dates}")
             if not previous_trading_dates:
-                # log.warning(f"找不到{future_code}的前一交易日数据")
+                log.warning(f"找不到{future_code}的前一交易日数据")
                 return minute_data
             
             previous_trading_date = max(previous_trading_dates)
-            # log.debug(f"前一交易日: {previous_trading_date}")
+            log.debug(f"前一交易日: {previous_trading_date}")
             
             # 找到前一交易日21:00:00的数据作为开盘起点
             previous_day_data = minute_data[minute_data['date'] == previous_trading_date]
@@ -569,10 +672,10 @@ def get_today_minute_data(context, future_code):
                 # 从前一交易日21:00开始的所有数据
                 start_time = night_21_data.index[0]  # 21:00:00的时间点
                 filtered_data = minute_data[minute_data.index >= start_time]
-                # log.debug(f"夜盘品种,从{start_time}开始,数据量: {len(filtered_data)}")
+                log.debug(f"夜盘品种,从{start_time}开始,数据量: {len(filtered_data)}")
                 return filtered_data.drop(columns=['date'])
             else:
-                # log.warning(f"找不到{future_code}前一交易日21:00的数据")
+                log.warning(f"找不到{future_code}前一交易日21:00的数据")
                 # 备选方案:使用今天9:00开始的数据
                 today_data = minute_data[minute_data['date'] == today_date]
                 day_9_data = today_data[today_data.index.hour >= 9]
@@ -589,10 +692,10 @@ def get_today_minute_data(context, future_code):
             day_9_data = today_data[today_data.index.hour >= 9]
             
             if len(day_9_data) > 0:
-                # log.debug(f"日盘品种,从今天9:00开始,数据量: {len(day_9_data)}")
+                log.debug(f"日盘品种,从今天9:00开始,数据量: {len(day_9_data)}")
                 return day_9_data.drop(columns=['date'])
             else:
-                # log.warning(f"找不到{future_code}今天9:00的数据")
+                log.warning(f"找不到{future_code}今天9:00的数据")
                 return today_data.drop(columns=['date']) if len(today_data) > 0 else minute_data.drop(columns=['date'])
         
     except Exception as e:
@@ -662,6 +765,40 @@ def aggregate_minute_to_daily(minute_data):
         log.warning(f"聚合分钟数据时出错: {str(e)}")
         return None
 
+def check_gap_opening(historical_data, minute_data):
+    """
+    检查开盘是否跳空
+    :param historical_data: 历史日线数据
+    :param minute_data: 今日分钟数据
+    :return: 跳空检查结果字典
+    """
+    try:
+        if historical_data is None or len(historical_data) == 0 or minute_data is None or len(minute_data) == 0:
+            return {'has_gap': False, 'gap_ratio': 0.0}
+        
+        # 获取前一交易日收盘价
+        previous_close = historical_data['close'].iloc[-1]
+        
+        # 获取今日开盘价
+        today_open = minute_data['open'].iloc[0]
+        
+        # 计算跳空比例
+        gap_ratio = abs(today_open - previous_close) / previous_close
+        
+        # 判断是否跳空
+        has_gap = gap_ratio >= g.gap_ratio_threshold
+        
+        return {
+            'has_gap': has_gap,
+            'gap_ratio': gap_ratio,
+            'previous_close': previous_close,
+            'today_open': today_open
+        }
+        
+    except Exception as e:
+        log.warning(f"检查跳空开盘时出错: {str(e)}")
+        return {'has_gap': False, 'gap_ratio': 0.0}
+
 ############################ 原有函数保持不变 ###################################
 
 def check_latest_multi_ma_cross(data, future_code):
@@ -672,8 +809,8 @@ def check_latest_multi_ma_cross(data, future_code):
     # 获取最新两天的数据
     today = data.iloc[-1]
     yesterday = data.iloc[-2]
-    # log.info(f"today: {today}")
-    # log.info(f"yesterday: {yesterday}")
+    log.debug(f"today: {today}")
+    log.debug(f"yesterday: {yesterday}")
     
     # 检查多均线穿越
     cross_result = check_multi_ma_cross_single_day(today)
@@ -717,38 +854,85 @@ def check_multi_ma_cross_single_day(row):
     if open_price == close_price:
         return None
     
-    crossed_mas = []
-    
-    # 上涨(阳线),检查上穿
-    if close_price > open_price:
+    # 1. 统计开盘价和均线的高低关系
+    open_above_count = 0  # 开盘价高于均线的数量
+    open_below_count = 0  # 开盘价低于均线的数量
+    open_above_mas = []   # 开盘价高于的均线列表
+    open_below_mas = []   # 开盘价低于的均线列表
+    
+    for ma_name, ma_value in ma_values:
+        if open_price > ma_value:
+            open_above_count += 1
+            open_above_mas.append((ma_name, ma_value))
+        elif open_price < ma_value:
+            open_below_count += 1
+            open_below_mas.append((ma_name, ma_value))
+    
+    # 2. 统计收盘价和均线的高低关系
+    close_above_count = 0  # 收盘价高于均线的数量
+    close_below_count = 0  # 收盘价低于均线的数量
+    close_above_mas = []   # 收盘价高于的均线列表
+    close_below_mas = []   # 收盘价低于的均线列表
+    
+    for ma_name, ma_value in ma_values:
+        if close_price > ma_value:
+            close_above_count += 1
+            close_above_mas.append((ma_name, ma_value))
+        elif close_price < ma_value:
+            close_below_count += 1
+            close_below_mas.append((ma_name, ma_value))
+    
+    # 3. 计算穿越情况
+    # 上穿:收盘价高于的数量比开盘价高于的数量增加了
+    upward_cross_count = close_above_count - open_above_count
+    # 下穿:收盘价低于的数量比开盘价低于的数量增加了
+    downward_cross_count = close_below_count - open_below_count
+    
+    log.debug(f"开盘价高于均线数量: {open_above_count}, 收盘价高于均线数量: {close_above_count}")
+    log.debug(f"开盘价低于均线数量: {open_below_count}, 收盘价低于均线数量: {close_below_count}")
+    log.debug(f"上穿数量: {upward_cross_count}, 下穿数量: {downward_cross_count}")
+    
+    # 4. 判断是否满足最少穿越条件并找到临界线
+    if upward_cross_count >= g.min_cross_mas:
+        # 上穿:找到被穿越的均线中值最大的一条作为临界线
+        # 被穿越的均线是那些开盘价低于但收盘价高于的均线
+        crossed_mas = []
+        crossed_ma_names = []
         for ma_name, ma_value in ma_values:
-            if open_price < ma_value and close_price > ma_value:
+            if open_price <= ma_value and close_price > ma_value:
                 crossed_mas.append((ma_name, ma_value))
+                crossed_ma_names.append(ma_name)
         
-        if len(crossed_mas) >= g.min_cross_mas:
-            # 找到最大的穿越线作为临界线
+        if len(crossed_mas) > 0:
+            # 找到值最大的被穿越均线
             critical_ma = max(crossed_mas, key=lambda x: x[1])
             return {
                 'direction': 'up',
                 'critical_ma_name': critical_ma[0],
                 'critical_ma_value': critical_ma[1],
-                'crossed_count': len(crossed_mas)
+                'crossed_count': upward_cross_count,
+                'crossed_ma_names': crossed_ma_names  # 添加被穿越的均线名称列表
             }
     
-    # 下跌(阴线),检查下穿        
-    elif open_price > close_price:
+    elif downward_cross_count >= g.min_cross_mas:
+        # 下穿:找到被穿越的均线中值最小的一条作为临界线
+        # 被穿越的均线是那些开盘价高于但收盘价低于的均线
+        crossed_mas = []
+        crossed_ma_names = []
         for ma_name, ma_value in ma_values:
-            if open_price > ma_value and close_price < ma_value:
+            if open_price >= ma_value and close_price < ma_value:
                 crossed_mas.append((ma_name, ma_value))
+                crossed_ma_names.append(ma_name)
         
-        if len(crossed_mas) >= g.min_cross_mas:
-            # 找到最小的穿越线作为临界线
+        if len(crossed_mas) > 0:
+            # 找到值最小的被穿越均线
             critical_ma = min(crossed_mas, key=lambda x: x[1])
             return {
                 'direction': 'down',
                 'critical_ma_name': critical_ma[0],
                 'critical_ma_value': critical_ma[1],
-                'crossed_count': len(crossed_mas)
+                'crossed_count': downward_cross_count,
+                'crossed_ma_names': crossed_ma_names  # 添加被穿越的均线名称列表
             }
     
     return None
@@ -804,7 +988,7 @@ def open_position(context, security, value, direction, signal):
         cash_before = context.portfolio.available_cash
         
         order = order_target_value(security, value, side=direction)
-        log.info(f"order: {order}")
+        log.debug(f"order: {order}")
         
         if order is not None and order.filled > 0:
             # 记录交易后的可用资金
@@ -819,7 +1003,7 @@ def open_position(context, security, value, direction, signal):
             
             # 计算实际保证金比例
             underlying_symbol = security.split('.')[0][:-4]
-            multiplier = g.multiplier.get(underlying_symbol, 10)
+            multiplier = get_multiplier(underlying_symbol)
             
             # 单笔保证金 = 资金变化 / 数量
             single_margin = cash_change / order_amount if order_amount > 0 else 0
@@ -829,7 +1013,7 @@ def open_position(context, security, value, direction, signal):
             actual_margin_rate = single_margin / contract_value if contract_value > 0 else 0
             
             # 校准保证金比例(只有变化大于1%时才更新)
-            current_rate = g.margin_rates.get(direction, {}).get(underlying_symbol, 0.10)
+            current_rate = get_margin_rate(underlying_symbol, direction)
             rate_change = abs(actual_margin_rate - current_rate) / current_rate if current_rate > 0 else 0
             
             if rate_change > 0.01:  # 变化大于1%
@@ -847,35 +1031,36 @@ def open_position(context, security, value, direction, signal):
                     'security': security
                 })
                 
-                # 直接更新默认保证金比例
-                g.margin_rates[direction][underlying_symbol] = actual_margin_rate
+                # 直接更新配置字典中的保证金比例
+                if underlying_symbol in g.futures_config:
+                    g.futures_config[underlying_symbol]['margin_rate'][direction] = actual_margin_rate
                 
-                log.info(f"保证金比例校准: {underlying_symbol}_{direction} {current_rate:.4f} -> {actual_margin_rate:.4f} (变化{rate_change*100:.1f}%)")
+                log.debug(f"保证金比例校准: {underlying_symbol}_{direction} {current_rate:.4f} -> {actual_margin_rate:.4f} (变化{rate_change*100:.1f}%)")
             
             # 记录当日交易
             g.today_trades.append({
-                'security': security,
-                'underlying_symbol': underlying_symbol,
-                'direction': direction,
-                'order_amount': order_amount,
-                'order_price': order_price,
-                'cash_change': cash_change,
-                'actual_margin_rate': actual_margin_rate,
-                'time': context.current_dt
+                'security': security, # 交易标的
+                'underlying_symbol': underlying_symbol, # 标的字母
+                'direction': direction, # 方向
+                'order_amount': order_amount, # 开仓数量
+                'order_price': order_price, # 开仓金额
+                'cash_change': cash_change, # 现金变化
+                'actual_margin_rate': actual_margin_rate, # 实际保证金率
+                'time': context.current_dt # 成交日期
             })
             
             # 记录交易信息
             g.trade_history[security] = {
-                'entry_price': order_price,
-                'position_value': value,
-                'actual_margin': cash_change,
-                'direction': direction, 
-                'entry_time': context.current_dt,
-                'signal_info': signal
+                'entry_price': order_price, # 成交价格
+                'position_value': value, # 开仓金额
+                'actual_margin': cash_change, # 实际保证金
+                'direction': direction, # 方向
+                'entry_time': context.current_dt, # 开仓时间
+                'signal_info': signal # 信号信息
             }
             
-            log.info(f"开仓成功 - 品种: {underlying_symbol}, 手数: {order_amount}, 订单价格: {order_price:.2f}")
-            log.info(f"资金变化: {cash_change:.0f}, 实际保证金比例: {actual_margin_rate:.4f}")
+            log.debug(f"开仓成功 - 品种: {underlying_symbol}, 手数: {order_amount}, 订单价格: {order_price:.2f}")
+            log.debug(f"资金变化: {cash_change:.0f}, 实际保证金比例: {actual_margin_rate:.4f}")
             return True
             
     except Exception as e:
@@ -923,50 +1108,249 @@ def check_stop_loss_profit(context, position):
         return False
     
     trade_info = g.trade_history[security]
-    entry_price = trade_info['entry_price']
-    current_price = position.price
-    direction = trade_info['direction']
-    
-    # 计算盈亏
-    if direction == 'long':
-        pnl_ratio = (current_price - entry_price) / entry_price
-    else:
-        pnl_ratio = (entry_price - current_price) / entry_price
     
-    # 止损条件
-    stop_loss_ratio = -0.03  # 3%止损
-    # 止盈条件
-    take_profit_ratio = 0.05  # 5%止盈
-    
-    if pnl_ratio <= stop_loss_ratio:
-        log.info(f"触发止损 {security} 盈亏比例: {pnl_ratio:.2%}")
-        close_position(context, security, direction)
-        return True
-    elif pnl_ratio >= take_profit_ratio:
-        log.info(f"触发止盈 {security} 盈亏比例: {pnl_ratio:.2%}")
-        close_position(context, security, direction) 
+    # 跟踪均线止损止盈
+    tracking_stop_result = check_tracking_ma_stop(context, security, position, trade_info)
+    if tracking_stop_result:
         return True
     
     return False
 
+def check_tracking_ma_stop(context, security, position, trade_info):
+    """
+    检查跟踪均线止损止盈
+    :param context: 上下文对象
+    :param security: 标的代码
+    :param position: 持仓对象
+    :param trade_info: 交易信息
+    :return: 是否触发止损止盈
+    """
+    try:
+        direction = trade_info['direction']
+        entry_price = trade_info['entry_price']
+        current_price = position.price
+        
+        # 获取最新的均线数据
+        if security not in g.ma_data_cache:
+            return False
+        
+        ma_data = g.ma_data_cache[security]
+        if len(ma_data) == 0:
+            return False
+        
+        latest_data = ma_data.iloc[-1]
+        
+        # 获取四条均线价格和今日最高最低价
+        ma5 = latest_data['MA5']
+        ma10 = latest_data['MA10']
+        ma20 = latest_data['MA20']
+        ma30 = latest_data['MA30']
+        today_high = latest_data['high']
+        today_low = latest_data['low']
+        
+        # 检查是否有NaN值
+        if pd.isna(ma5) or pd.isna(ma10) or pd.isna(ma20) or pd.isna(ma30):
+            return False
+        
+        # 获取开仓时被穿越的均线信息
+        signal_info = trade_info.get('signal_info', {})
+        crossed_ma_names = signal_info.get('crossed_ma_names', ['MA5', 'MA10', 'MA20', 'MA30'])
+        
+        # 根据方向确定止损均线,需要过滤掉不符合条件的均线
+        mas = [ma5, ma10, ma20, ma30]
+        ma_names = ['MA5', 'MA10', 'MA20', 'MA30']
+        
+        # 第一步:只考虑被穿越的均线
+        crossed_mas = []
+        crossed_ma_prices = []
+        for i, ma_name in enumerate(ma_names):
+            if ma_name in crossed_ma_names:
+                crossed_mas.append(ma_name)
+                crossed_ma_prices.append(mas[i])
+        
+        log.debug(f"开仓时被穿越的均线: {crossed_ma_names}")
+        
+        if direction == 'long':
+            # 多仓:在被穿越的均线中,选择价格低于今日最高价的均线
+            valid_mas = []
+            valid_ma_names = []
+            for i, ma_name in enumerate(crossed_mas):
+                ma_price = crossed_ma_prices[i]
+                if ma_price <= today_high:
+                    valid_mas.append(ma_price)
+                    valid_ma_names.append(ma_name)
+            
+            if not valid_mas:
+                # 如果被穿越的均线都高于今日最高价,使用被穿越均线中的最低价
+                if crossed_ma_prices:
+                    stop_ma_price = min(crossed_ma_prices)
+                    stop_ma_name = crossed_mas[crossed_ma_prices.index(stop_ma_price)]
+                    log.warning(f"多仓被穿越均线都高于今日最高价{today_high:.2f},使用被穿越均线中最低的")
+                else:
+                    # 如果没有被穿越均线信息,使用所有均线中的最低价
+                    stop_ma_price = min(mas)
+                    stop_ma_name = ma_names[mas.index(stop_ma_price)]
+                    log.warning(f"多仓无被穿越均线信息,使用所有均线中最低的")
+            else:
+                # 多仓跟踪符合条件的被穿越均线中价格最高的
+                stop_ma_price = max(valid_mas)
+                stop_ma_name = valid_ma_names[valid_mas.index(stop_ma_price)]
+        else:
+            # 空仓:在被穿越的均线中,选择价格高于今日最低价的均线
+            valid_mas = []
+            valid_ma_names = []
+            for i, ma_name in enumerate(crossed_mas):
+                ma_price = crossed_ma_prices[i]
+                if ma_price >= today_low:
+                    valid_mas.append(ma_price)
+                    valid_ma_names.append(ma_name)
+            
+            if not valid_mas:
+                # 如果被穿越的均线都低于今日最低价,使用被穿越均线中的最高价
+                if crossed_ma_prices:
+                    stop_ma_price = max(crossed_ma_prices)
+                    stop_ma_name = crossed_mas[crossed_ma_prices.index(stop_ma_price)]
+                    log.warning(f"空仓被穿越均线都低于今日最低价{today_low:.2f},使用被穿越均线中最高的")
+                else:
+                    # 如果没有被穿越均线信息,使用所有均线中的最高价
+                    stop_ma_price = max(mas)
+                    stop_ma_name = ma_names[mas.index(stop_ma_price)]
+                    log.warning(f"空仓无被穿越均线信息,使用所有均线中最高的")
+            else:
+                # 空仓跟踪符合条件的被穿越均线中价格最低的
+                stop_ma_price = min(valid_mas)
+                stop_ma_name = valid_ma_names[valid_mas.index(stop_ma_price)]
+        
+        log.debug(f"今日高低价: {today_high:.2f}/{today_low:.2f}, 被穿越均线: {crossed_ma_names}, 选择止损均线: {stop_ma_name}({stop_ma_price:.2f})")
+        log.debug(f"所有均线: MA5={ma5:.2f}, MA10={ma10:.2f}, MA20={ma20:.2f}, MA30={ma30:.2f}")
+        # 计算止损均线的收益水平
+        underlying_symbol = security.split('.')[0][:-4]
+        multiplier = get_multiplier(underlying_symbol)
+        position_value = abs(position.total_amount) * stop_ma_price * multiplier
+        
+        if direction == 'long':
+            ma_profit = (stop_ma_price - entry_price) * multiplier * abs(position.total_amount)
+        else:
+            ma_profit = (entry_price - stop_ma_price) * multiplier * abs(position.total_amount)
+        
+        # 获取跳空信息
+        gap_info = g.gap_check_results.get(security, {'has_gap': False})
+        has_gap = gap_info['has_gap']
+        
+        # 判断是否盘尾
+        current_time = context.current_dt.strftime('%H:%M:%S')
+        is_market_close = current_time in g.market_close_times
+        
+        # 确定止损比例
+        stop_ratio = get_tracking_stop_ratio(ma_profit, has_gap, is_market_close)
+        
+        # 计算止损价格
+        if direction == 'long':
+            stop_price = stop_ma_price * (1 - stop_ratio)
+            should_stop = current_price <= stop_price
+        else:
+            stop_price = stop_ma_price * (1 + stop_ratio)
+            should_stop = current_price >= stop_price
+        
+        if should_stop:
+            log.info(f"触发跟踪均线止损 {security} {direction}")
+            log.info(f"止损均线价格: {stop_ma_price:.2f}, 方向: {direction}, 收益: {ma_profit:.0f}, 跳空: {has_gap}, 盘尾: {is_market_close}")
+            log.info(f"止损比例: {stop_ratio:.4f}, 止损价格: {stop_price:.2f}, 当前价格: {current_price:.2f}")
+            close_position(context, security, direction)
+            return True
+        
+        return False
+        
+    except Exception as e:
+        log.warning(f"检查跟踪均线止损时出错 {security}: {str(e)}")
+        return False
+
+def get_tracking_stop_ratio(ma_profit, has_gap, is_market_close):
+    """
+    根据均线收益、跳空情况、盘中盘尾确定止损比例
+    :param ma_profit: 止损均线的收益水平
+    :param has_gap: 是否跳空
+    :param is_market_close: 是否盘尾
+    :return: 止损比例
+    """
+    # 根据收益水平分区
+    if ma_profit < g.profit_thresholds[0]:  # 收益 < 5000
+        if is_market_close:  # 盘尾
+            if has_gap:
+                return g.stop_ratios[0]  # 0.25%
+            else:
+                return g.stop_ratios[1]  # 0.5%
+        else:  # 盘中
+            if has_gap:
+                return g.stop_ratios[1]  # 0.5%
+            else:
+                return g.stop_ratios[2]  # 1%
+    elif ma_profit < g.profit_thresholds[1]:  # 5000 <= 收益 < 15000
+        if is_market_close:  # 盘尾
+            return g.stop_ratios[1]  # 0.5%
+        else:  # 盘中
+            return g.stop_ratios[2]  # 1%
+    else:  # 收益 >= 15000
+        if is_market_close:  # 盘尾
+            return g.stop_ratios[2]  # 1%
+        else:  # 盘中
+            return g.stop_ratios[3]  # 2%
+
 ############################ 辅助函数 ###################################
 
+def get_futures_config(underlying_symbol, config_key=None, default_value=None):
+    """
+    获取期货品种配置信息的辅助函数
+    :param underlying_symbol: 品种符号,如 'AU', 'PF' 等
+    :param config_key: 配置键,如 'multiplier', 'has_night_session' 等
+    :param default_value: 默认值
+    :return: 配置值或整个配置字典
+    """
+    if underlying_symbol not in g.futures_config:
+        if config_key and default_value is not None:
+            return default_value
+        return {}
+    
+    if config_key is None:
+        return g.futures_config[underlying_symbol]
+    
+    return g.futures_config[underlying_symbol].get(config_key, default_value)
+
+def get_margin_rate(underlying_symbol, direction, default_rate=0.10):
+    """
+    获取保证金比例的辅助函数
+    :param underlying_symbol: 品种符号
+    :param direction: 方向 'long' 或 'short'
+    :param default_rate: 默认保证金比例
+    :return: 保证金比例
+    """
+    return g.futures_config.get(underlying_symbol, {}).get('margin_rate', {}).get(direction, default_rate)
+
+def get_multiplier(underlying_symbol, default_multiplier=10):
+    """
+    获取合约乘数的辅助函数
+    :param underlying_symbol: 品种符号
+    :param default_multiplier: 默认合约乘数
+    :return: 合约乘数
+    """
+    return g.futures_config.get(underlying_symbol, {}).get('multiplier', default_multiplier)
+
 def calculate_order_value(context, security, direction):
     """计算开仓金额"""
     current_price = get_current_data()[security].last_price
     underlying_symbol = security.split('.')[0][:-4]
     
     # 使用保证金比例(已经过校准)
-    margin_rate = g.margin_rates.get(direction, {}).get(underlying_symbol, 0.10)
+    margin_rate = get_margin_rate(underlying_symbol, direction)
     
-    multiplier = g.multiplier.get(underlying_symbol, 10)
+    multiplier = get_multiplier(underlying_symbol)
     
     # 计算单手保证金
     single_hand_margin = current_price * multiplier * margin_rate
     
     # 还要考虑可用资金限制
     available_cash = context.portfolio.available_cash * g.usage_percentage
-    log.info(f"可用资金: {available_cash:.0f}")
+    log.debug(f"可用资金: {available_cash:.0f}")
     
     # 根据单个标的最大持仓保证金限制计算开仓数量
     max_margin = g.max_margin_per_position
@@ -986,7 +1370,7 @@ def calculate_order_value(context, security, direction):
         # 计算订单金额
         order_value = actual_margin
         
-        log.info(f"单手保证金: {single_hand_margin:.0f}, 最大手数(保证金限制): {max_hands}, 最大手数(资金限制): {max_hands_by_cash}, 实际开仓手数: {actual_hands}, 实际保证金: {actual_margin:.0f}")
+        log.debug(f"单手保证金: {single_hand_margin:.0f}, 最大手数(保证金限制): {max_hands}, 最大手数(资金限制): {max_hands_by_cash}, 实际开仓手数: {actual_hands}, 实际保证金: {actual_margin:.0f}")
     else:
         # 如果单手保证金超过最大限制,默认开仓1手
         actual_hands = 1
@@ -995,9 +1379,9 @@ def calculate_order_value(context, security, direction):
         # 计算订单金额
         order_value = actual_margin
         
-        log.info(f"单手保证金: {single_hand_margin:.0f} 超过最大限制: {max_margin}, 默认开仓1手, 实际保证金: {actual_margin:.0f}")
+        log.debug(f"单手保证金: {single_hand_margin:.0f} 超过最大限制: {max_margin}, 默认开仓1手, 实际保证金: {actual_margin:.0f}")
     
-    log.info(f"计算结果 - 品种: {underlying_symbol}, 开仓手数: {actual_hands}, 订单金额: {order_value:.0f}, 实际保证金: {actual_margin:.0f}")
+    log.debug(f"计算结果 - 品种: {underlying_symbol}, 开仓手数: {actual_hands}, 订单金额: {order_value:.0f}, 实际保证金: {actual_margin:.0f}")
     
     return order_value
 
@@ -1006,8 +1390,8 @@ def calculate_required_margin(context, symbol):
     current_price = get_current_data()[symbol].last_price
     underlying_symbol = symbol.split('.')[0][:-4]
     
-    margin_rate = g.margin_rates.get('long', {}).get(underlying_symbol, 0.10)
-    multiplier = g.multiplier.get(underlying_symbol, 10)
+    margin_rate = get_margin_rate(underlying_symbol, 'long')
+    multiplier = get_multiplier(underlying_symbol)
     
     return current_price * multiplier * margin_rate
 
@@ -1033,7 +1417,6 @@ def after_market_close(context):
         # 清空当日交易记录
         g.today_trades = []
     
-    log.info('A day ends')
     log.info('##############################################################')
 
 def print_daily_trading_summary(context):
@@ -1041,34 +1424,34 @@ def print_daily_trading_summary(context):
     if not g.today_trades:
         return
     
-    log.info("\n=== 当日交易汇总 ===")
+    log.debug("\n=== 当日交易汇总 ===")
     total_margin = 0
     
     for trade in g.today_trades:
         if trade['order_amount'] > 0:  # 开仓
-            log.info(f"开仓 {trade['underlying_symbol']} {trade['direction']} {trade['order_amount']}手 "
+            log.debug(f"开仓 {trade['underlying_symbol']} {trade['direction']} {trade['order_amount']}手 "
                   f"价格:{trade['order_price']:.2f} 保证金:{trade['cash_change']:.0f} "
                   f"比例:{trade['actual_margin_rate']:.4f}")
             total_margin += trade['cash_change']
         else:  # 平仓
-            log.info(f"平仓 {trade['underlying_symbol']} {trade['direction']} {abs(trade['order_amount'])}手 "
+            log.debug(f"平仓 {trade['underlying_symbol']} {trade['direction']} {abs(trade['order_amount'])}手 "
                   f"价格:{trade['order_price']:.2f}")
     
-    log.info(f"当日保证金占用: {total_margin:.0f}")
-    log.info("==================\n")
+    log.debug(f"当日保证金占用: {total_margin:.0f}")
+    log.debug("==================\n")
 
 def print_margin_rate_changes(context):
     """打印保证金比例变化记录"""
     if not g.margin_rate_history:
         return
     
-    log.info("\n=== 保证金比例变化记录 ===")
+    log.debug("\n=== 保证金比例变化记录 ===")
     for key, history in g.margin_rate_history.items():
-        log.info(f"{key}:")
+        log.debug(f"{key}:")
         for record in history:
-            log.info(f"  {record['date']} {record['time']}: {record['old_rate']:.4f} -> {record['new_rate']:.4f} "
+            log.debug(f"  {record['date']} {record['time']}: {record['old_rate']:.4f} -> {record['new_rate']:.4f} "
                   f"(变化{record['change_pct']:.1f}%)")
-    log.info("========================\n")
+    log.debug("========================\n")
 
 ########################## 自动移仓换月函数 #################################
 def position_auto_switch(context,pindex=0,switch_func=None, callback=None):

+ 27 - 7
Lib/future/README.md

@@ -194,7 +194,7 @@
   - 滑点:按照品种特性设置
   - 保证金:按照交易所要求设置
 
-## 穿越均线趋势交易策略
+## 多均线穿越突破策略 v001
 
 ### 核心思路
 该策略基于K线实体在同一天内穿越多条均线来识别建仓时机。策略同时结合止损和移仓换月机制来控制风险。
@@ -273,12 +273,32 @@
 #### 4. 风险控制
 
 ##### 4.1 止损机制
-- 固定止损:
-  - 初始止损额度:**initial_loss_limit**(默认-4000)
-  - 每日调整:**loss_increment_per_day**(默认200)
-- 均线止损:
-  - 监控均线偏离度
-  - 突破重要均线立即止损
+- **跟踪均线止损**:
+  - **多仓**:跟踪价格最高的均线(MA5、MA10、MA20、MA30中的最高值)
+  - **空仓**:跟踪价格最低的均线(MA5、MA10、MA20、MA30中的最低值)
+  - **跳空检查**:当天开盘价与前一交易日收盘价差异超过0.2%即为跳空
+  - **时间分类**:14:55:00为盘尾,其他时间为盘中
+  - **动态止损比例**:根据收益水平、跳空情况、交易时段确定0.25%-2%的止损比例
+
+##### 4.1.1 详细止损规则
+| 收益水平 | 跳空情况 | 时段 | 止损比例 | 说明 |
+|---------|---------|------|----------|------|
+| < 5000 | 无跳空 | 盘中 | 1.0% | 基础止损 |
+| < 5000 | 有跳空 | 盘中 | 0.5% | 跳空风险较大,收紧止损 |
+| 5000-15000 | 不限 | 盘中 | 1.0% | 中等收益,标准止损 |
+| ≥ 15000 | 不限 | 盘中 | 2.0% | 高收益,放宽止损 |
+| < 5000 | 有跳空 | 盘尾 | 0.25% | 最严格止损 |
+| < 5000 | 无跳空 | 盘尾 | 0.5% | 盘尾收紧止损 |
+| 5000-15000 | 不限 | 盘尾 | 0.5% | 中等收益盘尾止损 |
+| ≥ 15000 | 不限 | 盘尾 | 1.0% | 高收益盘尾止损 |
+
+##### 4.1.2 配置参数
+```python
+g.gap_ratio_threshold = 0.002      # 跳空比例:0.2%
+g.market_close_times = ["14:55:00"] # 盘尾时间
+g.profit_thresholds = [5000, 15000] # 价格分区
+g.stop_ratios = [0.0025, 0.005, 0.01, 0.02] # 止损比例
+```
 
 ##### 4.2 特殊情况处理
 - 涨跌停板:

binární
Lib/future/__pycache__/MultiMABreakoutStrategy_v001.cpython-38.pyc


+ 252 - 0
Lib/future/with_model_anaylsis.ipynb

@@ -0,0 +1,252 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# LSTM"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numpy as np\n",
+    "import pandas as pd\n",
+    "from sklearn.preprocessing import MinMaxScaler \n",
+    "from keras.models import Sequential,load_model#线性神经网络\n",
+    "from keras.layers.core import Dense,Activation,Dropout#神经网络的激活函数\n",
+    "from keras.optimizers import SGD\n",
+    "import numpy as numpy\n",
+    "import matplotlib.pyplot as plt\n",
+    "from keras.layers.recurrent import LSTM"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "security='000001.XSHG'\n",
+    "df=get_price(security, start_date=None, end_date='2020-12-10', frequency='daily', fields=['open', 'close', 'low', 'high', 'volume', 'money',  'pre_close', ], \n",
+    "          skip_paused=False, fq='pre', count=1500, panel=True)\n",
+    "df['rate']=(df['close']/df['pre_close']-1)*100\n",
+    "df.head()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# 数据处理\n",
+    "face_back=10\n",
+    "def Processing_data(array,face_back=5):\n",
+    "    data=list()\n",
+    "    for i in range(len(array)-face_back):\n",
+    "        a=list(array[i:i+face_back].values)\n",
+    "        data.append(a)\n",
+    "    return np.array(data)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "array=df['rate']\n",
+    "x=Processing_data(array,face_back)\n",
+    "y=array.values[face_back:]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "X=np.expand_dims(x, axis=1)#增加数据维度,LSTM神经网络维度至少为3维\n",
+    "X.shape"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# 分割数据为训练集和测试集\n",
+    "train_X,test_X=X[:1000,:,:],X[1000:,:,:]\n",
+    "train_y,test_y=y[:1000,],y[1000:,]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# 搭建LSTM模型\n",
+    "def build_STLM():\n",
+    "    model = Sequential()\n",
+    "    model.add(LSTM(25, input_shape=(train_X.shape[1], train_X.shape[2]),return_sequences=True))\n",
+    "    model.add(LSTM(48))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# fit network\n",
+    "model=build_STLM()\n",
+    "history = model.fit(X, y, epochs=50, batch_size=300, validation_split=0.25, verbose=1,shuffle=True)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# 绘制损失图\n",
+    "plt.plot(history.history['loss'], label='train')\n",
+    "plt.plot(history.history['val_loss'], label='test')\n",
+    "plt.title('LSTM_600000.SH', fontsize='12')\n",
+    "plt.ylabel('loss', fontsize='10')"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 数据标准化后的模型"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "pre_data=pd.DataFrame()\n",
+    "pre_data['y']=y\n",
+    "prediction5=model.predict(X)\n",
+    "pre_data['prediction5']=prediction5"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# 进行归一化处理\n",
+    "from sklearn.preprocessing import StandardScaler\n",
+    "minmax=StandardScaler()\n",
+    "minmax.fit(np.array(df['rate']).reshape(1500,1))\n",
+    "df['ration']=minmax.transform(np.array(df['rate']).reshape(len(df),1))\n",
+    "x_scaler=Processing_data(df['ration'],face_back)\n",
+    "y_scaler=df['ration'].values[face_back:]\n",
+    "X_scaler=np.expand_dims(x_scaler, axis=1)#增加维度"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# fit network\n",
+    "model2=build_STLM()\n",
+    "history = model2.fit(X, y, epochs=50, batch_size=300, validation_split=0.25, verbose=1,shuffle=True)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "predict6=model2.predict(X_scaler)\n",
+    "prediction6=minmax.inverse_transform(predict6)\n",
+    "pre_data['prediction6']=prediction6"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def draw_Distribution_map(data=pre_data,col='y'):\n",
+    "    cats=pd.cut(data[col],bins=100).value_counts(sort=False)\n",
+    "    cats.plot(kind='bar',title='%s的区间频数统计'%(col),figsize=(8,5))\n",
+    "    new_xticks=np.linspace(0,99,10)\n",
+    "    atick=[cats.index[int(x)] for x in new_xticks]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# 查看预测值分布\n",
+    "draw_Distribution_map(data=pre_data,col='prediction5')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "#查看预测值分布图\n",
+    "draw_Distribution_map(data=pre_data,col='prediction6')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "#MSE均方误差\n",
+    "from sklearn.metrics import mean_squared_error\n",
+    "#MAEX\n",
+    "from sklearn.metrics import mean_absolute_error\n",
+    "#R^2决定系数"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "valuetion(model='model5',col='prediction5')\n",
+    "valuetion(model='model6',col='prediction6')"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "common_3.8",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "name": "python",
+   "version": "3.8.17"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}

+ 476 - 0
Lib/stock/machine_learning.py

@@ -0,0 +1,476 @@
+# 机器学习框架
+# https://www.joinquant.com/view/community/detail/7253d06b938dc84af3e0c3c996d7d5bd?type=1
+
+# 1. 数据集制作
+from jqdata import *
+from jqlib.technical_analysis import *
+from jqfactor import get_factor_values, winsorize_med, standardlize, neutralize
+import datetime
+import pandas as pd
+import numpy as np
+from scipy import stats
+import statsmodels.api as sm
+from statsmodels import regression
+from six import StringIO
+from sklearn.decomposition import PCA
+from sklearn import svm
+from sklearn.model_selection import train_test_split
+from sklearn.grid_search import GridSearchCV
+from sklearn import metrics
+from tqdm import tqdm
+import matplotlib.dates as mdates
+import matplotlib.pyplot as plt
+import warnings
+import seaborn as sns
+import pickle
+warnings.filterwarnings("ignore")
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+import seaborn as sns
+from sklearn.metrics import (accuracy_score, precision_score, recall_score, 
+                            f1_score, roc_auc_score, confusion_matrix, 
+                            roc_curve, precision_recall_curve, auc, classification_report)
+import lightgbm as lgb
+
+jqfactors_list=['asset_impairment_loss_ttm', 'cash_flow_to_price_ratio', 'market_cap', 'interest_free_current_liability', 'EBITDA', 'financial_assets', 'gross_profit_ttm', 'net_working_capital', 'non_recurring_gain_loss', 'EBIT', 'sales_to_price_ratio', 'AR', 'ARBR', 'ATR6', 'DAVOL10', 'MAWVAD', 'TVMA6', 'PSY', 'VOL10', 'VDIFF', 'VEMA26', 'VMACD', 'VOL120', 'VOSC', 'VR', 'WVAD', 'arron_down_25', 'arron_up_25', 'BBIC', 'MASS', 'Rank1M', 'single_day_VPT', 'single_day_VPT_12', 'single_day_VPT_6', 'Volume1M', 'capital_reserve_fund_per_share', 'net_asset_per_share', 'net_operate_cash_flow_per_share', 'operating_profit_per_share', 'total_operating_revenue_per_share', 'surplus_reserve_fund_per_share', 'ACCA', 'account_receivable_turnover_days', 'account_receivable_turnover_rate', 'adjusted_profit_to_total_profit', 'super_quick_ratio', 'MLEV', 'debt_to_equity_ratio', 'debt_to_tangible_equity_ratio', 'equity_to_fixed_asset_ratio', 'fixed_asset_ratio', 'intangible_asset_ratio', 'invest_income_associates_to_total_profit', 'long_debt_to_asset_ratio', 'long_debt_to_working_capital_ratio', 'net_operate_cash_flow_to_total_liability', 'net_operating_cash_flow_coverage', 'non_current_asset_ratio', 'operating_profit_to_total_profit', 'roa_ttm', 'roe_ttm', 'Kurtosis120', 'Kurtosis20', 'Kurtosis60', 'sharpe_ratio_20', 'sharpe_ratio_60', 'Skewness120', 'Skewness20', 'Skewness60', 'Variance120', 'Variance20', 'liquidity', 'beta', 'book_to_price_ratio', 'cash_earnings_to_price_ratio', 'cube_of_size', 'earnings_to_price_ratio', 'earnings_yield', 'growth', 'momentum', 'natural_log_of_market_cap', 'boll_down', 'MFI14', 'price_no_fq']
+print(len(jqfactors_list))
+
+def get_period_date(peroid, start_date, end_date):
+    stock_data = get_price('000001.XSHE', start_date, end_date, 'daily', fields=['close'])
+    stock_data['date'] = stock_data.index
+    period_stock_data = stock_data.resample(peroid, how='last')
+    period_stock_data = period_stock_data.set_index('date').dropna()
+    date = period_stock_data.index
+    pydate_array = date.to_pydatetime()
+    date_only_array = np.vectorize(lambda s: s.strftime('%Y-%m-%d'))(pydate_array)
+    date_only_series = pd.Series(date_only_array)
+    start_date = datetime.datetime.strptime(start_date, "%Y-%m-%d")
+    start_date = start_date - datetime.timedelta(days=1)
+    start_date = start_date.strftime("%Y-%m-%d")
+    date_list = date_only_series.values.tolist()
+    date_list.insert(0, start_date)
+    return date_list
+
+peroid = 'M'
+start_date = '2019-01-01'
+end_date = '2024-01-01'
+DAY = get_period_date(peroid, start_date, end_date)
+print(len(DAY))
+
+def delect_stop(stocks, beginDate, n=30 * 3):
+    stockList = []
+    beginDate = datetime.datetime.strptime(beginDate, "%Y-%m-%d")
+    for stock in stocks:
+        start_date = get_security_info(stock).start_date
+        if start_date < (beginDate - datetime.timedelta(days=n)).date():
+            stockList.append(stock)
+    return stockList
+
+def get_stock(stockPool, begin_date):
+    if stockPool == 'HS300':
+        stockList = get_index_stocks('000300.XSHG', begin_date)
+    elif stockPool == 'ZZ500':
+        stockList = get_index_stocks('399905.XSHE', begin_date)
+    elif stockPool == 'ZZ800':
+        stockList = get_index_stocks('399906.XSHE', begin_date)
+    elif stockPool == 'CYBZ':
+        stockList = get_index_stocks('399006.XSHE', begin_date)
+    elif stockPool == 'ZXBZ':
+        stockList = get_index_stocks('399101.XSHE', begin_date)
+    elif stockPool == 'A':
+        stockList = get_index_stocks('000002.XSHG', begin_date) + get_index_stocks('399107.XSHE', begin_date)
+        stockList = [stock for stock in stockList if not stock.startswith(('68', '4', '8'))]
+    elif stockPool == 'AA':
+        stockList = get_index_stocks('000985.XSHG', begin_date)
+        stockList = [stock for stock in stockList if not stock.startswith(('3', '68', '4', '8'))]
+    st_data = get_extras('is_st', stockList, count=1, end_date=begin_date)
+    stockList = [stock for stock in stockList if not st_data[stock][0]]
+    stockList = delect_stop(stockList, begin_date)
+    return stockList
+
+def get_factor_data(securities_list, date):
+    factor_data = get_factor_values(securities=securities_list, factors=jqfactors_list, count=1, end_date=date)
+    df_jq_factor = pd.DataFrame(index=securities_list)
+    for i in factor_data.keys():
+        df_jq_factor[i] = factor_data[i].iloc[0, :]
+    return df_jq_factor
+
+dateList = get_period_date(peroid, start_date, end_date)
+DF = pd.DataFrame()
+
+for date in tqdm(dateList[:-1]):
+    stockList = get_stock('ZXBZ', date)
+    factor_origl_data = get_factor_data(stockList, date)
+    data_close = get_price(stockList, date, dateList[dateList.index(date) + 1], '1d', 'close')['close']
+    factor_origl_data['pchg'] = data_close.iloc[-1] / data_close.iloc[1] - 1
+    factor_origl_data = factor_origl_data.dropna()
+    median_pchg = factor_origl_data['pchg'].median()
+    factor_origl_data['label'] = np.where(factor_origl_data['pchg'] >= median_pchg, 1, 0)
+    factor_origl_data = factor_origl_data.drop(columns=['pchg'])
+    DF = pd.concat([DF, factor_origl_data], ignore_index=True)
+
+DF.to_csv(r'train_small.csv', index=False)
+
+# 2. 数据分析
+df = pd.read_csv(r'train_small.csv')
+plot_cols = jqfactors_list
+print(len(plot_cols))
+corr_matrix = df.corr()
+plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题
+label_corr = corr_matrix['label'].sort_values(ascending=False)
+plt.figure(figsize=(12, 10))
+corr = df[plot_cols].corr()
+mask = np.triu(np.ones_like(corr, dtype=bool))
+sns.heatmap(corr, mask=mask, annot=False, fmt=".2f", cmap='RdBu_r', vmin=-1, vmax=1)
+plt.title('因子间相关性矩阵')
+plt.show()
+
+plt.figure(figsize=(14, 100))
+for i, col in enumerate(plot_cols, 1):
+    plt.subplot(42, 2, i)
+    sns.distplot(df[col].dropna(), bins=30, color='skyblue', kde=True)
+    stats_text = f"均值: {df[col].mean():.2f}\n中位数: {df[col].median():.2f}\n标准差: {df[col].std():.2f}"
+    plt.gca().text(0.95, 0.95, stats_text, transform=plt.gca().transAxes, 
+                  verticalalignment='top', horizontalalignment='right',
+                  bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
+    plt.title(f'{col} 分布')
+    plt.xlabel('')
+plt.tight_layout()
+plt.show()
+
+# 3. 数据预处理
+from collections import defaultdict
+# 计算每个特征的缺失值数量
+missing_counts = df[plot_cols].isnull().sum().to_dict()
+
+# 计算特征间的相关系数矩阵
+corr_matrix = df[plot_cols].corr()
+
+# 创建图结构存储高度相关的特征对
+graph = defaultdict(list)
+threshold = 0.6  # 相关性阈值
+
+# 遍历上三角矩阵找到高度相关的特征对
+n = len(plot_cols)
+for i in range(n):
+    for j in range(i + 1, n):
+        col1, col2 = plot_cols[i], plot_cols[j]
+        corr_value = corr_matrix.iloc[i, j]
+
+        if not pd.isna(corr_value) and abs(corr_value) > threshold:
+            graph[col1].append(col2)
+            graph[col2].append(col1)
+
+# 使用DFS找到连通分量(高度相关的特征组)
+visited = set()
+components = []
+
+def dfs(node, comp):
+    visited.add(node)
+    comp.append(node)
+    for neighbor in graph[node]:
+        if neighbor not in visited:
+            dfs(neighbor, comp)
+
+for col in plot_cols:
+    if col not in visited:
+        comp = []
+        dfs(col, comp)
+        components.append(comp)
+
+# 处理每个连通分量:保留缺失值最少的特征
+to_keep = []
+to_remove = []
+
+for comp in components:
+    if len(comp) == 1:  # 独立特征直接保留
+        to_keep.append(comp[0])
+    else:
+        # 按缺失值数量排序(升序),相同缺失值时按特征名字母顺序排序
+        comp_sorted = sorted(comp, key=lambda x: (missing_counts[x], x))
+        keep_feature = comp_sorted[0]  # 缺失值最少的特征
+        to_keep.append(keep_feature)
+        to_remove.extend(comp_sorted[1:])  # 组内其他特征移除
+
+
+
+print(f"\n最终保留特征数量: {len(to_keep)}")
+print(f"移除特征数量: {len(to_remove)}")
+print("\n移除的特征列表:", to_remove)
+print("\n保留的特征列表:", to_keep)
+# 可视化保留特征的相关矩阵(可选)
+plt.figure(figsize=(12, 10))
+corr_kept = df[to_keep].corr()
+mask = np.triu(np.ones_like(corr_kept, dtype=bool))
+sns.heatmap(corr_kept, mask=mask, annot=True, fmt=".2f", cmap='RdBu_r', vmin=-1, vmax=1)
+plt.title('保留特征间相关性矩阵')
+plt.show()
+
+# 4. 训练模型
+X = df[to_keep]
+y = df['label']
+lgb_train = lgb.Dataset(X, label=y)
+params = {
+    'objective': 'binary',
+    'metric': 'binary_logloss',
+    'boosting_type': 'gbdt',
+    'verbose': -1
+}
+model = lgb.train(params, lgb_train, num_boost_round=200)
+y_pred_proba = model.predict(X)
+y_pred = (y_pred_proba > 0.5).astype(int)
+precision, recall, _ = precision_recall_curve(y, y_pred_proba)
+prauc = auc(recall, precision)
+print("\n模型性能评估:")
+print("准确率 (Accuracy):", accuracy_score(y, y_pred))
+print("精确率 (Precision):", precision_score(y, y_pred))
+print("召回率 (Recall):", recall_score(y, y_pred))
+print("F1分数 (F1-score):", f1_score(y, y_pred))
+print("AUC分数:", roc_auc_score(y, y_pred_proba))
+print("PRAUC分数:", prauc)  # 新增PRAUC输出
+plt.figure(figsize=(15, 12))
+plt.subplot(2, 2, 1)
+cm = confusion_matrix(y, y_pred)
+sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', 
+            xticklabels=['预测0', '预测1'], 
+            yticklabels=['实际0', '实际1'])
+plt.title('混淆矩阵')
+plt.ylabel('实际标签')
+plt.xlabel('预测标签')
+plt.subplot(2, 2, 2)
+fpr, tpr, _ = roc_curve(y, y_pred_proba)
+roc_auc = roc_auc_score(y, y_pred_proba)
+plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'AUC = {roc_auc:.3f}')
+plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
+plt.xlim([0.0, 1.0])
+plt.ylim([0.0, 1.05])
+plt.xlabel('假正率 (FPR)')
+plt.ylabel('真正率 (TPR)')
+plt.title('ROC曲线')
+plt.legend(loc="lower right")
+plt.subplot(2, 2, 3)
+importance = pd.Series(model.feature_importance(), index=to_keep)
+importance.sort_values().plot(kind='barh')
+plt.title('特征重要性')
+plt.xlabel('重要性分数')
+plt.ylabel('特征')
+plt.subplot(2, 2, 4)
+for label in [0, 1]:
+    sns.kdeplot(y_pred_proba[y == label], label=f'真实标签={label}', shade=True)
+plt.title('预测概率分布')
+plt.xlabel('预测为正类的概率')
+plt.ylabel('密度')
+plt.legend()
+plt.axvline(0.5, color='red', linestyle='--')
+plt.tight_layout()
+plt.show()
+plt.figure(figsize=(12, 5))
+plt.subplot(1, 2, 1)
+plt.plot(recall, precision, color='darkblue', lw=2, label=f'PRAUC = {prauc:.3f}')
+plt.fill_between(recall, precision, alpha=0.2, color='darkblue')
+plt.xlabel('召回率 (Recall)')
+plt.ylabel('精确率 (Precision)')
+plt.title('PRAUC曲线')
+plt.legend(loc='upper right')
+plt.grid(True)
+plt.tight_layout()
+plt.show()
+with open('model_small.pkl', 'wb') as model_file:
+    pickle.dump(model, model_file)
+
+# 5. 回测代码
+from jqdata import *
+from jqfactor import *
+import numpy as np
+import pandas as pd
+import pickle
+
+
+
+# 初始化函数
+def initialize(context):
+    # 设定基准
+    set_benchmark('399101.XSHE')
+    # 用真实价格交易
+    set_option('use_real_price', True)
+    # 打开防未来函数
+    set_option("avoid_future_data", True)
+    # 将滑点设置为0
+    set_slippage(FixedSlippage(0))
+    # 设置交易成本万分之三,不同滑点影响可在归因分析中查看
+    set_order_cost(OrderCost(open_tax=0, close_tax=0.001, open_commission=0.0003, close_commission=0.0003,
+                             close_today_commission=0, min_commission=5), type='stock')
+    # 过滤order中低于error级别的日志
+    log.set_level('order', 'error')
+    # 初始化全局变量
+
+    g.stock_num = 10
+    g.hold_list = []  # 当前持仓的全部股票
+    g.yesterday_HL_list = []  # 记录持仓中昨日涨停的股票
+
+
+    g.model_small = pickle.loads(read_file('model_small.pkl'))
+
+    # 因子列表
+    g.factor_list =  ['asset_impairment_loss_ttm', 'cash_flow_to_price_ratio', 'EBIT', 'net_working_capital', 'non_recurring_gain_loss', 'sales_to_price_ratio', 'AR', 'ARBR', 'ATR6', 'DAVOL10', 'MAWVAD', 'TVMA6', 'PSY', 'VOL10', 'VDIFF', 'VEMA26', 'VMACD', 'VOL120', 'VOSC', 'VR', 'arron_down_25', 'arron_up_25', 'BBIC', 'MASS', 'Rank1M', 'single_day_VPT', 'single_day_VPT_12', 'Volume1M', 'capital_reserve_fund_per_share', 'net_operate_cash_flow_per_share', 'operating_profit_per_share', 'total_operating_revenue_per_share', 'surplus_reserve_fund_per_share', 'ACCA', 'account_receivable_turnover_days', 'account_receivable_turnover_rate', 'adjusted_profit_to_total_profit', 'super_quick_ratio', 'MLEV', 'debt_to_equity_ratio', 'debt_to_tangible_equity_ratio', 'equity_to_fixed_asset_ratio', 'fixed_asset_ratio', 'intangible_asset_ratio', 'invest_income_associates_to_total_profit', 'long_debt_to_asset_ratio', 'long_debt_to_working_capital_ratio', 'net_operate_cash_flow_to_total_liability', 'net_operating_cash_flow_coverage', 'non_current_asset_ratio', 'operating_profit_to_total_profit', 'roa_ttm', 'Kurtosis120', 'Kurtosis20', 'Kurtosis60', 'sharpe_ratio_20', 'sharpe_ratio_60', 'Skewness120', 'Skewness20', 'Skewness60', 'Variance120', 'Variance20', 'beta', 'book_to_price_ratio', 'cash_earnings_to_price_ratio', 'cube_of_size', 'earnings_to_price_ratio', 'earnings_yield', 'growth', 'momentum', 'natural_log_of_market_cap', 'boll_down', 'MFI14', 'price_no_fq']
+    run_daily(prepare_stock_list, '9:05')
+    run_monthly(weekly_adjustment, 1, '9:30')
+    run_daily(check_limit_up, '14:00') 
+
+
+
+
+# 1-1 准备股票池
+def prepare_stock_list(context):
+    # 获取已持有列表
+    g.hold_list = []
+    for position in list(context.portfolio.positions.values()):
+        stock = position.security
+        g.hold_list.append(stock)
+    # 获取昨日涨停列表
+    if g.hold_list != []:
+        df = get_price(g.hold_list, end_date=context.previous_date, frequency='daily', fields=['close', 'high_limit'],
+                       count=1, panel=False, fill_paused=False)
+        df = df[df['close'] == df['high_limit']]
+        g.yesterday_HL_list = list(df.code)
+    else:
+        g.yesterday_HL_list = []
+
+# 1-2 选股模块
+def get_stock_list(context):
+    yesterday = context.previous_date
+    today = context.current_dt
+    stocks = get_index_stocks('399101.XSHE', yesterday)
+    initial_list = filter_kcbj_stock(stocks)
+    initial_list = filter_st_stock(initial_list)
+    initial_list = filter_paused_stock(initial_list)
+    initial_list = filter_new_stock(context, initial_list)
+    initial_list = filter_limitup_stock(context,initial_list)
+    initial_list = filter_limitdown_stock(context,initial_list)
+    factor_data = get_factor_values(initial_list, g.factor_list, end_date=yesterday, count=1)
+    df_jq_factor_value = pd.DataFrame(index=initial_list, columns=g.factor_list)
+    for factor in g.factor_list:
+        df_jq_factor_value[factor] = list(factor_data[factor].T.iloc[:, 0])
+    tar = g.model_small.predict(df_jq_factor_value)
+    df = df_jq_factor_value
+    df['total_score'] = list(tar)
+    df = df.sort_values(by=['total_score'], ascending=False)  
+    lst = df.index.tolist()
+    lst = lst[:min(g.stock_num, len(lst))]
+    return lst
+
+
+# 1-3 整体调整持仓
+def weekly_adjustment(context):
+
+        # 获取应买入列表
+        target_list = get_stock_list(context)
+        # 调仓卖出
+        for stock in g.hold_list:
+            if (stock not in target_list) and (stock not in g.yesterday_HL_list):
+                log.info("卖出[%s]" % (stock))
+                position = context.portfolio.positions[stock]
+                close_position(position)
+            else:
+                log.info("已持有[%s]" % (stock))
+        # 调仓买入
+        position_count = len(context.portfolio.positions)
+        target_num = len(target_list)
+        if target_num > position_count:
+            value = context.portfolio.cash / (target_num - position_count)
+            for stock in target_list:
+                if context.portfolio.positions[stock].total_amount == 0:
+                    if open_position(stock, value):
+                        if len(context.portfolio.positions) == target_num:
+                            break
+
+
+
+# 1-4 调整昨日涨停股票
+def check_limit_up(context):
+    now_time = context.current_dt
+    if g.yesterday_HL_list != []:
+        # 对昨日涨停股票观察到尾盘如不涨停则提前卖出,如果涨停即使不在应买入列表仍暂时持有
+        for stock in g.yesterday_HL_list:
+            current_data = get_price(stock, end_date=now_time, frequency='1m', fields=['close', 'high_limit'],
+                                     skip_paused=False, fq='pre', count=1, panel=False, fill_paused=True)
+            if current_data.iloc[0, 0] < current_data.iloc[0, 1]:
+                log.info("[%s]涨停打开,卖出" % (stock))
+                position = context.portfolio.positions[stock]
+                close_position(position)
+            else:
+                log.info("[%s]涨停,继续持有" % (stock))
+
+# 3-1 交易模块-自定义下单
+def order_target_value_(security, value):
+    if value == 0:
+        log.debug("Selling out %s" % (security))
+    else:
+        log.debug("Order %s to value %f" % (security, value))
+    return order_target_value(security, value)
+
+
+# 3-2 交易模块-开仓
+def open_position(security, value):
+    order = order_target_value_(security, value)
+    if order != None and order.filled > 0:
+        return True
+    return False
+
+
+# 3-3 交易模块-平仓
+def close_position(position):
+    security = position.security
+    order = order_target_value_(security, 0)  # 可能会因停牌失败
+    if order != None:
+        if order.status == OrderStatus.held and order.filled == order.amount:
+            return True
+    return False
+
+
+# 2-1 过滤停牌股票
+def filter_paused_stock(stock_list):
+    current_data = get_current_data()
+    return [stock for stock in stock_list if not current_data[stock].paused]
+
+
+# 2-2 过滤ST及其他具有退市标签的股票
+def filter_st_stock(stock_list):
+    current_data = get_current_data()
+    return [stock for stock in stock_list
+            if not current_data[stock].is_st
+            and 'ST' not in current_data[stock].name
+            and '*' not in current_data[stock].name
+            and '退' not in current_data[stock].name]
+
+
+# 2-3 过滤科创北交股票
+def filter_kcbj_stock(stock_list):
+    for stock in stock_list[:]:
+        if stock[0] == '4' or stock[0] == '8' or stock[:2] == '68' or stock[0] == '3':
+            stock_list.remove(stock)
+    return stock_list
+
+
+# 2-4 过滤涨停的股票
+def filter_limitup_stock(context, stock_list):
+    last_prices = history(1, unit='1m', field='close', security_list=stock_list)
+    current_data = get_current_data()
+    return [stock for stock in stock_list if stock in context.portfolio.positions.keys()
+            or last_prices[stock][-1] < current_data[stock].high_limit]
+
+
+# 2-5 过滤跌停的股票
+def filter_limitdown_stock(context, stock_list):
+    last_prices = history(1, unit='1m', field='close', security_list=stock_list)
+    current_data = get_current_data()
+    return [stock for stock in stock_list if stock in context.portfolio.positions.keys()
+            or last_prices[stock][-1] > current_data[stock].low_limit]
+
+
+# 2-6 过滤次新股
+def filter_new_stock(context, stock_list):
+    yesterday = context.previous_date
+    return [stock for stock in stock_list if
+            not yesterday - get_security_info(stock).start_date < datetime.timedelta(days=375)]

+ 204 - 0
Lib/stock/separate_warehouse.py

@@ -0,0 +1,204 @@
+# https://www.joinquant.com/algorithm/index/edit?algorithmId=4519676386ce59c6457da930b52d0e7c&startTime=2015-07-01&endTime=2016-04-06&baseCapital=100000&frequency=day&pyVersion=2
+
+# 克隆自聚宽文章:https://www.joinquant.com/post/1079
+# 标题:多策略组合利器——分仓管控技术【非交易策略】
+# 作者:莫邪的救赎
+
+def initialize(context):
+    # 获取股票
+    df = get_fundamentals(query(
+            valuation.code,valuation.market_cap
+        ).order_by(
+            valuation.market_cap.asc()
+        ).limit(
+            200
+        )).dropna()
+    g.security = list(df['code'])
+    g.buylist = g.security[-100:]
+    set_universe(g.security)
+    
+    log.set_level('order', 'error')#屏蔽order warning提示
+    g.stocknum = 5 #仓位数
+    g.per_sell_stock = [] #欲卖出股票列表
+    g.proportion_initial_buy_cash = 0.6 #建仓资金占总资金的比例
+    g.proportion_cash = 0.4 #预留补仓现金比例占总资金的比例
+    #(0.6代表用仓位总资金的六成用于建仓,其余四成用于加仓)
+    initialize_position_cash(context) #初始化仓位个数及资金
+    # 执行函数
+    run_daily(buy_stocks, time='open')
+    run_daily(overweight_and_stop_profit, time='open')
+    run_daily(stop_loss, time='open')
+    run_daily(change_position_cash, time='after_close')
+
+def before_trading_start(context):
+    #获取前日持仓股票列表
+    g.already_hold_stock = context.portfolio.positions.keys()
+    log.info('buylist长度:%s',len(g.buylist))
+
+def initialize_position_cash(context):
+    '''
+    初始化每个仓位的信息
+    '''
+    start_cash = context.portfolio.starting_cash
+    every_position = start_cash/g.stocknum
+    g.hold_temp = {'total':every_position, 'initial_buy_cash':every_position*g.proportion_initial_buy_cash, 'cash':every_position*g.proportion_cash}
+    g.hold = {} # 仓位信息
+    g.hold_stock_name = {} # 仓位对于的股票名称
+    for i in range(g.stocknum):
+        g.hold[i] = g.hold_temp.copy() 
+        g.hold_stock_name[i] = None #仓位为空,则对应的value值为None
+    # log.info('init hold:',g.hold)
+    # log.info('hold_stock_name:',g.hold_stock_name)
+
+def buy_stocks(context):
+    current_data = get_current_data(g.buylist)
+    for stock in g.buylist:
+        if current_data[stock].paused == 0:#跳过停牌
+            for n in g.hold_stock_name.items():
+                if n[1] == None:
+                    g.hold_stock_name[n[0]] = stock #标记仓位对应的股票
+                    Cash = g.hold[n[0]]['initial_buy_cash'] #获取建仓资金
+                    order_value(stock, Cash) #建仓
+                    # log.info('buy: %s',stock)
+                    g.buylist.remove(stock)
+                    break
+                else:
+                    pass
+        else:
+            g.buylist.remove(stock)
+    # log.info('hold_stock_name:',g.hold_stock_name)
+def overweight_and_stop_profit(context):
+    '''
+    加仓以及止盈
+    '''
+    hold_stock = context.portfolio.positions.keys()
+    if len(hold_stock)>0:
+        current_data = get_current_data(hold_stock)
+        for stock in hold_stock:
+            #跳过停牌,因为T+1。所以sellable_amount>0即跳过当日建仓的股票
+            if current_data[stock].paused == 0 and context.portfolio.positions[stock].sellable_amount>0:
+                avg_cost = context.portfolio.positions[stock].avg_cost #持仓成本
+                price = context.portfolio.positions[stock].price #持仓股票当前价
+                # 根据触发条件,对不在“欲卖出”列表的股票进行加仓,降低持仓成本
+                if ((price/avg_cost) <= 0.9) and (stock not in g.per_sell_stock):
+                    log.info("overweight: %s", stock) #打印加仓股票代码
+                    i = get_key(stock) #获取加仓股票对应的key值,get_key函数见下
+                    Cash = get_buy_cash(i) #获取用于加仓资金,get_buy_cash函数见下
+                    order_value(stock, Cash) #买入
+                    # log.info('buy: %s',stock)
+                # 止盈
+                elif (price/avg_cost) >= 1.2:
+                    order_target(stock, 0)
+                    log.info('sell: %s', stock)
+                    # 如果股票在“欲卖出”列表中,则将其删除
+                    if stock in g.per_sell_stock:
+                        g.per_sell_stock.remove(stock)
+            else:
+                pass
+
+def get_key(stock):
+    '''
+    获取stock在g.hold_stock_name对应的key值
+    '''
+    for n in g.hold_stock_name.items():
+        if n[1] == stock:
+            return n[0]
+
+def get_buy_cash(i):
+    '''
+    获取g.hold中i键对应仓位的可用购买现金,
+    这里设定:将剩余四成仓位分两次购买
+    (如需更改购买现金占比,请自行修改)
+    并在现金用尽之后,将股票加入“欲卖出”股票列表,如下跌找过一定比例,则进行止损
+    '''
+    all_cash = g.hold[i]['cash'] #仓位中可用现金
+    total = g.hold[i]['total'] #仓位初始总资金
+    if all_cash/total > 0.3:
+        cash = all_cash*0.5
+        return cash
+    elif all_cash/total < 0.3:
+        will_sell_stock = g.hold_stock_name[i]
+        if will_sell_stock not in g.per_sell_stock:
+            g.per_sell_stock.append(will_sell_stock)
+        cash = all_cash
+        return cash
+
+def stop_loss(context):
+    '''
+    “欲卖出”股票列表,如下跌找过一定比例,则进行止损
+    '''
+    if len(g.per_sell_stock)>0:
+        current_data = get_current_data(g.per_sell_stock)
+        for stock in g.per_sell_stock:
+            if current_data[stock].paused == 0: #跳过停牌
+                avg_cost = context.portfolio.positions[stock].avg_cost
+                price = context.portfolio.positions[stock].price
+                if (price/avg_cost) <= 0.9:
+                    order_target(stock, 0)
+                    g.per_sell_stock.remove(stock)
+                    log.info('sell: %s', stock)
+            else:
+                pass
+    else:
+        pass
+
+
+def change_position_cash(context):
+    '''
+    I.  更新每个仓位的可用现金
+
+    II. 将已卖掉的股票现金仓位进行资金再平衡。
+    (这里只是将当前空余的仓位进行了资金再平衡
+    如,有N支仓位空余,及重新平均分配N支仓位的资金)
+    '''
+    g.selled_list_keys = [] #已卖掉的股票keys
+    g.rebalance_total_money = 0 #再分仓的总金额
+
+    trades=get_orders() #过去当天订单
+    hold_stock = context.portfolio.positions
+    for t in trades.values(): 
+        # 更新每个仓位的可用现金
+        if t.is_buy and t.filled > 0: #买入有效订单
+            if (t.security not in g.already_hold_stock) and (hold_stock[t.security].sellable_amount  > 0):
+                i = get_key(t.security)
+                # g.hold[i]['initial_buy_cash'] = t.cash
+                g.hold[i]['cash'] = g.hold[i]['total'] - t.cash #更新可用现金
+            elif (t.security in g.already_hold_stock) and (hold_stock[t.security].sellable_amount  > 0):
+                i = get_key(t.security)
+                # g.hold[i]['initial_buy_cash'] = g.hold[i]['cash'] + t.cash
+                g.hold[i]['cash'] = g.hold[i]['cash'] - t.cash #更新可用现金
+
+        # 将已卖掉的股票现金仓位进行资金再平衡。
+        elif not t.is_buy and t.filled > 0:#卖出有效订单
+            if t.security not in context.portfolio.positions.keys():
+                i = get_key(t.security)
+                g.selled_list_keys.append(i)
+                g.rebalance_total_money += t.cash
+
+    # 资金重分配函数,如需隔离仓位,则不用执行该函数
+    if len(g.selled_list_keys) > 0:
+        rebalance_money(g.selled_list_keys, g.rebalance_total_money)
+    
+    # 打印结果(用于调试)
+    # log.info('hold:',g.hold)
+    log.info('hold_stock_name:',g.hold_stock_name)
+    log.info('per_sell_stock:',g.per_sell_stock)
+
+def rebalance_money(selled_list_keys, rebalance_total_money):
+    '''
+    资金重分配函数
+    如需隔离仓位,则不用执行该函数
+    '''
+    # 获取再分仓的总金额
+    for i in selled_list_keys:
+        rebalance_total_money += g.hold[i]['cash']
+    
+    # 确定每个仓位的总金额
+    every_position = rebalance_total_money/len(selled_list_keys)
+    
+    # 资金重分配
+    for i in selled_list_keys:
+        g.hold_stock_name[i] = None
+        g.hold[i]['total'] = every_position
+        g.hold[i]['initial_buy_cash'] = every_position*g.proportion_initial_buy_cash
+        g.hold[i]['cash'] = every_position*g.proportion_cash

+ 6 - 0
main.py

@@ -0,0 +1,6 @@
+def main():
+    print("Hello from jukuan!")
+
+
+if __name__ == "__main__":
+    main()

+ 10 - 0
pyproject.toml

@@ -0,0 +1,10 @@
+[project]
+name = "jukuan"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.11"
+dependencies = [
+    "matplotlib>=3.10.3",
+    "pandas>=2.3.1",
+]

+ 39 - 0
quick_test.py

@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+"""
+快速测试新的边界点标注功能
+"""
+
+import sys
+sys.path.append('.')
+
+from Lib.Options.analysis_chart import analyze_options
+
+def quick_test():
+    """快速测试边界点标注功能"""
+    print("=== 快速测试边界点标注功能 ===")
+    
+    try:
+        # 测试熊市价差 - 应该有最大收益的边界点
+        print("\n测试熊市价差(应该有最大收益边界点):")
+        print("卖出低行权价认购 + 买入高行权价认购")
+        
+        result = analyze_options(
+            ('sell', 'call', 0.08, 2.9, 1),  # 卖出低行权价认购
+            ('buy', 'call', 0.03, 3.1, 1)    # 买入高行权价认购
+        )
+        
+        print("✅ 测试完成!请查看图表中是否正确标注了:")
+        print("  - 盈亏平衡点(红色圆点)")
+        print("  - 最大收益边界点(绿色圆点)- 当价格低于2.9时达到最大收益")
+        print("  - 最大损失边界点(紫色圆点)- 当价格高于3.1时达到最大损失")
+        
+        return True
+        
+    except Exception as e:
+        print(f"❌ 测试失败: {e}")
+        import traceback
+        traceback.print_exc()
+        return False
+
+if __name__ == "__main__":
+    quick_test()

+ 467 - 0
uv.lock

@@ -0,0 +1,467 @@
+version = 1
+revision = 2
+requires-python = ">=3.11"
+resolution-markers = [
+    "python_full_version >= '3.12'",
+    "python_full_version < '3.12'",
+]
+
+[[package]]
+name = "contourpy"
+version = "1.3.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" },
+    { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" },
+    { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" },
+    { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" },
+    { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" },
+    { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" },
+    { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" },
+    { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" },
+    { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" },
+    { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" },
+    { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" },
+    { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" },
+    { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" },
+    { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" },
+    { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" },
+    { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" },
+    { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" },
+    { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" },
+    { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" },
+    { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" },
+    { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" },
+    { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" },
+    { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" },
+    { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" },
+    { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" },
+    { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" },
+    { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" },
+    { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" },
+    { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" },
+    { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" },
+    { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" },
+    { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" },
+    { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" },
+    { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" },
+    { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" },
+    { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" },
+    { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" },
+    { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" },
+    { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" },
+]
+
+[[package]]
+name = "cycler"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
+]
+
+[[package]]
+name = "fonttools"
+version = "4.58.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/52/97/5735503e58d3816b0989955ef9b2df07e4c99b246469bd8b3823a14095da/fonttools-4.58.5.tar.gz", hash = "sha256:b2a35b0a19f1837284b3a23dd64fd7761b8911d50911ecd2bdbaf5b2d1b5df9c", size = 3526243, upload-time = "2025-07-03T14:04:47.736Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/14/50/26c683bf6f30dcbde6955c8e07ec6af23764aab86ff06b36383654ab6739/fonttools-4.58.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cda226253bf14c559bc5a17c570d46abd70315c9a687d91c0e01147f87736182", size = 2769557, upload-time = "2025-07-03T14:03:35.383Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/00/c3c75fb6196b9ff9988e6a82319ae23f4ae7098e1c01e2408e58d2e7d9c7/fonttools-4.58.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:83a96e4a4e65efd6c098da549ec34f328f08963acd2d7bc910ceba01d2dc73e6", size = 2329367, upload-time = "2025-07-03T14:03:37.322Z" },
+    { url = "https://files.pythonhosted.org/packages/59/e9/6946366c8e88650c199da9b284559de5d47a6e66ed6d175a166953347959/fonttools-4.58.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d172b92dff59ef8929b4452d5a7b19b8e92081aa87bfb2d82b03b1ff14fc667", size = 5019491, upload-time = "2025-07-03T14:03:39.759Z" },
+    { url = "https://files.pythonhosted.org/packages/76/12/2f3f7d09bba7a93bd48dcb54b170fba665f0b7e80e959ac831b907d40785/fonttools-4.58.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0bfddfd09aafbbfb3bd98ae67415fbe51eccd614c17db0c8844fe724fbc5d43d", size = 4961579, upload-time = "2025-07-03T14:03:41.611Z" },
+    { url = "https://files.pythonhosted.org/packages/2c/95/87e84071189e51c714074646dfac8275b2e9c6b2b118600529cc74f7451e/fonttools-4.58.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cfde5045f1bc92ad11b4b7551807564045a1b38cb037eb3c2bc4e737cd3a8d0f", size = 4997792, upload-time = "2025-07-03T14:03:44.529Z" },
+    { url = "https://files.pythonhosted.org/packages/73/47/5c4df7473ecbeb8aa4e01373e4f614ca33f53227fe13ae673c6d5ca99be7/fonttools-4.58.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3515ac47a9a5ac025d2899d195198314023d89492340ba86e4ba79451f7518a8", size = 5109361, upload-time = "2025-07-03T14:03:46.693Z" },
+    { url = "https://files.pythonhosted.org/packages/06/00/31406853c570210232b845e08e5a566e15495910790381566ffdbdc7f9a2/fonttools-4.58.5-cp311-cp311-win32.whl", hash = "sha256:9f7e2ab9c10b6811b4f12a0768661325a48e664ec0a0530232c1605896a598db", size = 2201369, upload-time = "2025-07-03T14:03:48.885Z" },
+    { url = "https://files.pythonhosted.org/packages/c5/90/ac0facb57962cef53a5734d0be5d2f2936e55aa5c62647c38ca3497263d8/fonttools-4.58.5-cp311-cp311-win_amd64.whl", hash = "sha256:126c16ec4a672c9cb5c1c255dc438d15436b470afc8e9cac25a2d39dd2dc26eb", size = 2249021, upload-time = "2025-07-03T14:03:51.232Z" },
+    { url = "https://files.pythonhosted.org/packages/d6/68/66b498ee66f3e7e92fd68476c2509508082b7f57d68c0cdb4b8573f44331/fonttools-4.58.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c3af3fefaafb570a03051a0d6899b8374dcf8e6a4560e42575843aef33bdbad6", size = 2754751, upload-time = "2025-07-03T14:03:52.976Z" },
+    { url = "https://files.pythonhosted.org/packages/f1/1e/edbc14b79290980c3944a1f43098624bc8965f534964aa03d52041f24cb4/fonttools-4.58.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:688137789dbd44e8757ad77b49a771539d8069195ffa9a8bcf18176e90bbd86d", size = 2322342, upload-time = "2025-07-03T14:03:54.957Z" },
+    { url = "https://files.pythonhosted.org/packages/c1/d7/3c87cf147185d91c2e946460a5cf68c236427b4a23ab96793ccb7d8017c9/fonttools-4.58.5-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af65836cf84cd7cb882d0b353bdc73643a497ce23b7414c26499bb8128ca1af", size = 4897011, upload-time = "2025-07-03T14:03:56.829Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/d6/fbb44cc85d4195fe54356658bd9f934328b4f74ae14addd90b4b5558b5c9/fonttools-4.58.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2d79cfeb456bf438cb9fb87437634d4d6f228f27572ca5c5355e58472d5519d", size = 4942291, upload-time = "2025-07-03T14:03:59.204Z" },
+    { url = "https://files.pythonhosted.org/packages/4d/c8/453f82e21aedf25cdc2ae619c03a73512398cec9bd8b6c3b1c571e0b6632/fonttools-4.58.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0feac9dda9a48a7a342a593f35d50a5cee2dbd27a03a4c4a5192834a4853b204", size = 4886824, upload-time = "2025-07-03T14:04:01.517Z" },
+    { url = "https://files.pythonhosted.org/packages/40/54/e9190001b8e22d123f78925b2f508c866d9d18531694b979277ad45d59b0/fonttools-4.58.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36555230e168511e83ad8637232268649634b8dfff6ef58f46e1ebc057a041ad", size = 5038510, upload-time = "2025-07-03T14:04:03.917Z" },
+    { url = "https://files.pythonhosted.org/packages/cf/9c/07cdad4774841a6304aabae939f8cbb9538cb1d8e97f5016b334da98e73a/fonttools-4.58.5-cp312-cp312-win32.whl", hash = "sha256:26ec05319353842d127bd02516eacb25b97ca83966e40e9ad6fab85cab0576f4", size = 2188459, upload-time = "2025-07-03T14:04:06.103Z" },
+    { url = "https://files.pythonhosted.org/packages/0e/4d/1eaaad22781d55f49d1b184563842172aeb6a4fe53c029e503be81114314/fonttools-4.58.5-cp312-cp312-win_amd64.whl", hash = "sha256:778a632e538f82c1920579c0c01566a8f83dc24470c96efbf2fbac698907f569", size = 2236565, upload-time = "2025-07-03T14:04:08.27Z" },
+    { url = "https://files.pythonhosted.org/packages/3a/ee/764dd8b99891f815241f449345863cfed9e546923d9cef463f37fd1d7168/fonttools-4.58.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f4b6f1360da13cecc88c0d60716145b31e1015fbe6a59e32f73a4404e2ea92cf", size = 2745867, upload-time = "2025-07-03T14:04:10.586Z" },
+    { url = "https://files.pythonhosted.org/packages/e2/23/8fef484c02fef55e226dfeac4339a015c5480b6a496064058491759ac71e/fonttools-4.58.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a036822e915692aa2c03e2decc60f49a8190f8111b639c947a4f4e5774d0d7a", size = 2317933, upload-time = "2025-07-03T14:04:12.335Z" },
+    { url = "https://files.pythonhosted.org/packages/ab/47/f92b135864fa777e11ad68420bf89446c91a572fe2782745586f8e6aac0c/fonttools-4.58.5-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d7709fcf4577b0f294ee6327088884ca95046e1eccde87c53bbba4d5008541", size = 4877844, upload-time = "2025-07-03T14:04:14.58Z" },
+    { url = "https://files.pythonhosted.org/packages/3e/65/6c1a83511d8ac32411930495645edb3f8dfabebcb78f08cf6009ba2585ec/fonttools-4.58.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9b5099ca99b79d6d67162778b1b1616fc0e1de02c1a178248a0da8d78a33852", size = 4940106, upload-time = "2025-07-03T14:04:16.563Z" },
+    { url = "https://files.pythonhosted.org/packages/fa/90/df8eb77d6cf266cbbba01866a1349a3e9121e0a63002cf8d6754e994f755/fonttools-4.58.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f2c05a8d82a4d15aebfdb3506e90793aea16e0302cec385134dd960647a36c0", size = 4879458, upload-time = "2025-07-03T14:04:19.584Z" },
+    { url = "https://files.pythonhosted.org/packages/26/b1/e32f8de51b7afcfea6ad62780da2fa73212c43a32cd8cafcc852189d7949/fonttools-4.58.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79f0c4b1cc63839b61deeac646d8dba46f8ed40332c2ac1b9997281462c2e4ba", size = 5021917, upload-time = "2025-07-03T14:04:21.736Z" },
+    { url = "https://files.pythonhosted.org/packages/89/72/578aa7fe32918dd763c62f447aaed672d665ee10e3eeb1725f4d6493fe96/fonttools-4.58.5-cp313-cp313-win32.whl", hash = "sha256:a1a9a2c462760976882131cbab7d63407813413a2d32cd699e86a1ff22bf7aa5", size = 2186827, upload-time = "2025-07-03T14:04:24.237Z" },
+    { url = "https://files.pythonhosted.org/packages/71/a3/21e921b16cb9c029d3308e0cb79c9a937e9ff1fc1ee28c2419f0957b9e7c/fonttools-4.58.5-cp313-cp313-win_amd64.whl", hash = "sha256:bca61b14031a4b7dc87e14bf6ca34c275f8e4b9f7a37bc2fe746b532a924cf30", size = 2235706, upload-time = "2025-07-03T14:04:26.082Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/d4/1d85a1996b6188cd2713230e002d79a6f3a289bb17cef600cba385848b72/fonttools-4.58.5-py3-none-any.whl", hash = "sha256:e48a487ed24d9b611c5c4b25db1e50e69e9854ca2670e39a3486ffcd98863ec4", size = 1115318, upload-time = "2025-07-03T14:04:45.378Z" },
+]
+
+[[package]]
+name = "jukuan"
+version = "0.1.0"
+source = { virtual = "." }
+dependencies = [
+    { name = "matplotlib" },
+    { name = "pandas" },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "matplotlib", specifier = ">=3.10.3" },
+    { name = "pandas", specifier = ">=2.3.1" },
+]
+
+[[package]]
+name = "kiwisolver"
+version = "1.4.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538, upload-time = "2024-12-24T18:30:51.519Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635, upload-time = "2024-12-24T18:28:51.826Z" },
+    { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717, upload-time = "2024-12-24T18:28:54.256Z" },
+    { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413, upload-time = "2024-12-24T18:28:55.184Z" },
+    { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994, upload-time = "2024-12-24T18:28:57.493Z" },
+    { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804, upload-time = "2024-12-24T18:29:00.077Z" },
+    { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690, upload-time = "2024-12-24T18:29:01.401Z" },
+    { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839, upload-time = "2024-12-24T18:29:02.685Z" },
+    { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109, upload-time = "2024-12-24T18:29:04.113Z" },
+    { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269, upload-time = "2024-12-24T18:29:05.488Z" },
+    { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468, upload-time = "2024-12-24T18:29:06.79Z" },
+    { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394, upload-time = "2024-12-24T18:29:08.24Z" },
+    { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901, upload-time = "2024-12-24T18:29:09.653Z" },
+    { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306, upload-time = "2024-12-24T18:29:12.644Z" },
+    { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966, upload-time = "2024-12-24T18:29:14.089Z" },
+    { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311, upload-time = "2024-12-24T18:29:15.892Z" },
+    { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152, upload-time = "2024-12-24T18:29:16.85Z" },
+    { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555, upload-time = "2024-12-24T18:29:19.146Z" },
+    { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067, upload-time = "2024-12-24T18:29:20.096Z" },
+    { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443, upload-time = "2024-12-24T18:29:22.843Z" },
+    { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728, upload-time = "2024-12-24T18:29:24.463Z" },
+    { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388, upload-time = "2024-12-24T18:29:25.776Z" },
+    { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849, upload-time = "2024-12-24T18:29:27.202Z" },
+    { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533, upload-time = "2024-12-24T18:29:28.638Z" },
+    { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898, upload-time = "2024-12-24T18:29:30.368Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605, upload-time = "2024-12-24T18:29:33.151Z" },
+    { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801, upload-time = "2024-12-24T18:29:34.584Z" },
+    { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077, upload-time = "2024-12-24T18:29:36.138Z" },
+    { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410, upload-time = "2024-12-24T18:29:39.991Z" },
+    { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853, upload-time = "2024-12-24T18:29:42.006Z" },
+    { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424, upload-time = "2024-12-24T18:29:44.38Z" },
+    { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156, upload-time = "2024-12-24T18:29:45.368Z" },
+    { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555, upload-time = "2024-12-24T18:29:46.37Z" },
+    { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071, upload-time = "2024-12-24T18:29:47.333Z" },
+    { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053, upload-time = "2024-12-24T18:29:49.636Z" },
+    { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278, upload-time = "2024-12-24T18:29:51.164Z" },
+    { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139, upload-time = "2024-12-24T18:29:52.594Z" },
+    { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517, upload-time = "2024-12-24T18:29:53.941Z" },
+    { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952, upload-time = "2024-12-24T18:29:56.523Z" },
+    { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132, upload-time = "2024-12-24T18:29:57.989Z" },
+    { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997, upload-time = "2024-12-24T18:29:59.393Z" },
+    { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060, upload-time = "2024-12-24T18:30:01.338Z" },
+    { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471, upload-time = "2024-12-24T18:30:04.574Z" },
+    { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793, upload-time = "2024-12-24T18:30:06.25Z" },
+    { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855, upload-time = "2024-12-24T18:30:07.535Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430, upload-time = "2024-12-24T18:30:08.504Z" },
+    { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294, upload-time = "2024-12-24T18:30:09.508Z" },
+    { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736, upload-time = "2024-12-24T18:30:11.039Z" },
+    { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194, upload-time = "2024-12-24T18:30:14.886Z" },
+    { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942, upload-time = "2024-12-24T18:30:18.927Z" },
+    { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341, upload-time = "2024-12-24T18:30:22.102Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455, upload-time = "2024-12-24T18:30:24.947Z" },
+    { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138, upload-time = "2024-12-24T18:30:26.286Z" },
+    { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857, upload-time = "2024-12-24T18:30:28.86Z" },
+    { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129, upload-time = "2024-12-24T18:30:30.34Z" },
+    { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538, upload-time = "2024-12-24T18:30:33.334Z" },
+    { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661, upload-time = "2024-12-24T18:30:34.939Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710, upload-time = "2024-12-24T18:30:37.281Z" },
+    { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213, upload-time = "2024-12-24T18:30:40.019Z" },
+]
+
+[[package]]
+name = "matplotlib"
+version = "3.10.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "contourpy" },
+    { name = "cycler" },
+    { name = "fonttools" },
+    { name = "kiwisolver" },
+    { name = "numpy" },
+    { name = "packaging" },
+    { name = "pillow" },
+    { name = "pyparsing" },
+    { name = "python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811, upload-time = "2025-05-08T19:10:54.39Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f5/bd/af9f655456f60fe1d575f54fb14704ee299b16e999704817a7645dfce6b0/matplotlib-3.10.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0ef061f74cd488586f552d0c336b2f078d43bc00dc473d2c3e7bfee2272f3fa8", size = 8178873, upload-time = "2025-05-08T19:09:53.857Z" },
+    { url = "https://files.pythonhosted.org/packages/c2/86/e1c86690610661cd716eda5f9d0b35eaf606ae6c9b6736687cfc8f2d0cd8/matplotlib-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96985d14dc5f4a736bbea4b9de9afaa735f8a0fc2ca75be2fa9e96b2097369d", size = 8052205, upload-time = "2025-05-08T19:09:55.684Z" },
+    { url = "https://files.pythonhosted.org/packages/54/51/a9f8e49af3883dacddb2da1af5fca1f7468677f1188936452dd9aaaeb9ed/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5f0283da91e9522bdba4d6583ed9d5521566f63729ffb68334f86d0bb98049", size = 8465823, upload-time = "2025-05-08T19:09:57.442Z" },
+    { url = "https://files.pythonhosted.org/packages/e7/e3/c82963a3b86d6e6d5874cbeaa390166458a7f1961bab9feb14d3d1a10f02/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdfa07c0ec58035242bc8b2c8aae37037c9a886370eef6850703d7583e19964b", size = 8606464, upload-time = "2025-05-08T19:09:59.471Z" },
+    { url = "https://files.pythonhosted.org/packages/0e/34/24da1027e7fcdd9e82da3194c470143c551852757a4b473a09a012f5b945/matplotlib-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c0b9849a17bce080a16ebcb80a7b714b5677d0ec32161a2cc0a8e5a6030ae220", size = 9413103, upload-time = "2025-05-08T19:10:03.208Z" },
+    { url = "https://files.pythonhosted.org/packages/a6/da/948a017c3ea13fd4a97afad5fdebe2f5bbc4d28c0654510ce6fd6b06b7bd/matplotlib-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:eef6ed6c03717083bc6d69c2d7ee8624205c29a8e6ea5a31cd3492ecdbaee1e1", size = 8065492, upload-time = "2025-05-08T19:10:05.271Z" },
+    { url = "https://files.pythonhosted.org/packages/eb/43/6b80eb47d1071f234ef0c96ca370c2ca621f91c12045f1401b5c9b28a639/matplotlib-3.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ab1affc11d1f495ab9e6362b8174a25afc19c081ba5b0775ef00533a4236eea", size = 8179689, upload-time = "2025-05-08T19:10:07.602Z" },
+    { url = "https://files.pythonhosted.org/packages/0f/70/d61a591958325c357204870b5e7b164f93f2a8cca1dc6ce940f563909a13/matplotlib-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2a818d8bdcafa7ed2eed74487fdb071c09c1ae24152d403952adad11fa3c65b4", size = 8050466, upload-time = "2025-05-08T19:10:09.383Z" },
+    { url = "https://files.pythonhosted.org/packages/e7/75/70c9d2306203148cc7902a961240c5927dd8728afedf35e6a77e105a2985/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748ebc3470c253e770b17d8b0557f0aa85cf8c63fd52f1a61af5b27ec0b7ffee", size = 8456252, upload-time = "2025-05-08T19:10:11.958Z" },
+    { url = "https://files.pythonhosted.org/packages/c4/91/ba0ae1ff4b3f30972ad01cd4a8029e70a0ec3b8ea5be04764b128b66f763/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed70453fd99733293ace1aec568255bc51c6361cb0da94fa5ebf0649fdb2150a", size = 8601321, upload-time = "2025-05-08T19:10:14.47Z" },
+    { url = "https://files.pythonhosted.org/packages/d2/88/d636041eb54a84b889e11872d91f7cbf036b3b0e194a70fa064eb8b04f7a/matplotlib-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbed9917b44070e55640bd13419de83b4c918e52d97561544814ba463811cbc7", size = 9406972, upload-time = "2025-05-08T19:10:16.569Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/79/0d1c165eac44405a86478082e225fce87874f7198300bbebc55faaf6d28d/matplotlib-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:cf37d8c6ef1a48829443e8ba5227b44236d7fcaf7647caa3178a4ff9f7a5be05", size = 8067954, upload-time = "2025-05-08T19:10:18.663Z" },
+    { url = "https://files.pythonhosted.org/packages/3b/c1/23cfb566a74c696a3b338d8955c549900d18fe2b898b6e94d682ca21e7c2/matplotlib-3.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9f2efccc8dcf2b86fc4ee849eea5dcaecedd0773b30f47980dc0cbeabf26ec84", size = 8180318, upload-time = "2025-05-08T19:10:20.426Z" },
+    { url = "https://files.pythonhosted.org/packages/6c/0c/02f1c3b66b30da9ee343c343acbb6251bef5b01d34fad732446eaadcd108/matplotlib-3.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ddbba06a6c126e3301c3d272a99dcbe7f6c24c14024e80307ff03791a5f294e", size = 8051132, upload-time = "2025-05-08T19:10:22.569Z" },
+    { url = "https://files.pythonhosted.org/packages/b4/ab/8db1a5ac9b3a7352fb914133001dae889f9fcecb3146541be46bed41339c/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748302b33ae9326995b238f606e9ed840bf5886ebafcb233775d946aa8107a15", size = 8457633, upload-time = "2025-05-08T19:10:24.749Z" },
+    { url = "https://files.pythonhosted.org/packages/f5/64/41c4367bcaecbc03ef0d2a3ecee58a7065d0a36ae1aa817fe573a2da66d4/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a80fcccbef63302c0efd78042ea3c2436104c5b1a4d3ae20f864593696364ac7", size = 8601031, upload-time = "2025-05-08T19:10:27.03Z" },
+    { url = "https://files.pythonhosted.org/packages/12/6f/6cc79e9e5ab89d13ed64da28898e40fe5b105a9ab9c98f83abd24e46d7d7/matplotlib-3.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55e46cbfe1f8586adb34f7587c3e4f7dedc59d5226719faf6cb54fc24f2fd52d", size = 9406988, upload-time = "2025-05-08T19:10:29.056Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/0f/eed564407bd4d935ffabf561ed31099ed609e19287409a27b6d336848653/matplotlib-3.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:151d89cb8d33cb23345cd12490c76fd5d18a56581a16d950b48c6ff19bb2ab93", size = 8068034, upload-time = "2025-05-08T19:10:31.221Z" },
+    { url = "https://files.pythonhosted.org/packages/3e/e5/2f14791ff69b12b09e9975e1d116d9578ac684460860ce542c2588cb7a1c/matplotlib-3.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c26dd9834e74d164d06433dc7be5d75a1e9890b926b3e57e74fa446e1a62c3e2", size = 8218223, upload-time = "2025-05-08T19:10:33.114Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/08/30a94afd828b6e02d0a52cae4a29d6e9ccfcf4c8b56cc28b021d3588873e/matplotlib-3.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:24853dad5b8c84c8c2390fc31ce4858b6df504156893292ce8092d190ef8151d", size = 8094985, upload-time = "2025-05-08T19:10:35.337Z" },
+    { url = "https://files.pythonhosted.org/packages/89/44/f3bc6b53066c889d7a1a3ea8094c13af6a667c5ca6220ec60ecceec2dabe/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68f7878214d369d7d4215e2a9075fef743be38fa401d32e6020bab2dfabaa566", size = 8483109, upload-time = "2025-05-08T19:10:37.611Z" },
+    { url = "https://files.pythonhosted.org/packages/ba/c7/473bc559beec08ebee9f86ca77a844b65747e1a6c2691e8c92e40b9f42a8/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6929fc618cb6db9cb75086f73b3219bbb25920cb24cee2ea7a12b04971a4158", size = 8618082, upload-time = "2025-05-08T19:10:39.892Z" },
+    { url = "https://files.pythonhosted.org/packages/d8/e9/6ce8edd264c8819e37bbed8172e0ccdc7107fe86999b76ab5752276357a4/matplotlib-3.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7818292a5cc372a2dc4c795e5c356942eb8350b98ef913f7fda51fe175ac5d", size = 9413699, upload-time = "2025-05-08T19:10:42.376Z" },
+    { url = "https://files.pythonhosted.org/packages/1b/92/9a45c91089c3cf690b5badd4be81e392ff086ccca8a1d4e3a08463d8a966/matplotlib-3.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4f23ffe95c5667ef8a2b56eea9b53db7f43910fa4a2d5472ae0f72b64deab4d5", size = 8139044, upload-time = "2025-05-08T19:10:44.551Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "2.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2e/19/d7c972dfe90a353dbd3efbbe1d14a5951de80c99c9dc1b93cd998d51dc0f/numpy-2.3.1.tar.gz", hash = "sha256:1ec9ae20a4226da374362cca3c62cd753faf2f951440b0e3b98e93c235441d2b", size = 20390372, upload-time = "2025-06-21T12:28:33.469Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b0/c7/87c64d7ab426156530676000c94784ef55676df2f13b2796f97722464124/numpy-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ea9e48336a402551f52cd8f593343699003d2353daa4b72ce8d34f66b722070", size = 21199346, upload-time = "2025-06-21T11:47:47.57Z" },
+    { url = "https://files.pythonhosted.org/packages/58/0e/0966c2f44beeac12af8d836e5b5f826a407cf34c45cb73ddcdfce9f5960b/numpy-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ccb7336eaf0e77c1635b232c141846493a588ec9ea777a7c24d7166bb8533ae", size = 14361143, upload-time = "2025-06-21T11:48:10.766Z" },
+    { url = "https://files.pythonhosted.org/packages/7d/31/6e35a247acb1bfc19226791dfc7d4c30002cd4e620e11e58b0ddf836fe52/numpy-2.3.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0bb3a4a61e1d327e035275d2a993c96fa786e4913aa089843e6a2d9dd205c66a", size = 5378989, upload-time = "2025-06-21T11:48:19.998Z" },
+    { url = "https://files.pythonhosted.org/packages/b0/25/93b621219bb6f5a2d4e713a824522c69ab1f06a57cd571cda70e2e31af44/numpy-2.3.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:e344eb79dab01f1e838ebb67aab09965fb271d6da6b00adda26328ac27d4a66e", size = 6912890, upload-time = "2025-06-21T11:48:31.376Z" },
+    { url = "https://files.pythonhosted.org/packages/ef/60/6b06ed98d11fb32e27fb59468b42383f3877146d3ee639f733776b6ac596/numpy-2.3.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:467db865b392168ceb1ef1ffa6f5a86e62468c43e0cfb4ab6da667ede10e58db", size = 14569032, upload-time = "2025-06-21T11:48:52.563Z" },
+    { url = "https://files.pythonhosted.org/packages/75/c9/9bec03675192077467a9c7c2bdd1f2e922bd01d3a69b15c3a0fdcd8548f6/numpy-2.3.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:afed2ce4a84f6b0fc6c1ce734ff368cbf5a5e24e8954a338f3bdffa0718adffb", size = 16930354, upload-time = "2025-06-21T11:49:17.473Z" },
+    { url = "https://files.pythonhosted.org/packages/6a/e2/5756a00cabcf50a3f527a0c968b2b4881c62b1379223931853114fa04cda/numpy-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0025048b3c1557a20bc80d06fdeb8cc7fc193721484cca82b2cfa072fec71a93", size = 15879605, upload-time = "2025-06-21T11:49:41.161Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/86/a471f65f0a86f1ca62dcc90b9fa46174dd48f50214e5446bc16a775646c5/numpy-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5ee121b60aa509679b682819c602579e1df14a5b07fe95671c8849aad8f2115", size = 18666994, upload-time = "2025-06-21T11:50:08.516Z" },
+    { url = "https://files.pythonhosted.org/packages/43/a6/482a53e469b32be6500aaf61cfafd1de7a0b0d484babf679209c3298852e/numpy-2.3.1-cp311-cp311-win32.whl", hash = "sha256:a8b740f5579ae4585831b3cf0e3b0425c667274f82a484866d2adf9570539369", size = 6603672, upload-time = "2025-06-21T11:50:19.584Z" },
+    { url = "https://files.pythonhosted.org/packages/6b/fb/bb613f4122c310a13ec67585c70e14b03bfc7ebabd24f4d5138b97371d7c/numpy-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:d4580adadc53311b163444f877e0789f1c8861e2698f6b2a4ca852fda154f3ff", size = 13024015, upload-time = "2025-06-21T11:50:39.139Z" },
+    { url = "https://files.pythonhosted.org/packages/51/58/2d842825af9a0c041aca246dc92eb725e1bc5e1c9ac89712625db0c4e11c/numpy-2.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:ec0bdafa906f95adc9a0c6f26a4871fa753f25caaa0e032578a30457bff0af6a", size = 10456989, upload-time = "2025-06-21T11:50:55.616Z" },
+    { url = "https://files.pythonhosted.org/packages/c6/56/71ad5022e2f63cfe0ca93559403d0edef14aea70a841d640bd13cdba578e/numpy-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2959d8f268f3d8ee402b04a9ec4bb7604555aeacf78b360dc4ec27f1d508177d", size = 20896664, upload-time = "2025-06-21T12:15:30.845Z" },
+    { url = "https://files.pythonhosted.org/packages/25/65/2db52ba049813670f7f987cc5db6dac9be7cd95e923cc6832b3d32d87cef/numpy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:762e0c0c6b56bdedfef9a8e1d4538556438288c4276901ea008ae44091954e29", size = 14131078, upload-time = "2025-06-21T12:15:52.23Z" },
+    { url = "https://files.pythonhosted.org/packages/57/dd/28fa3c17b0e751047ac928c1e1b6990238faad76e9b147e585b573d9d1bd/numpy-2.3.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:867ef172a0976aaa1f1d1b63cf2090de8b636a7674607d514505fb7276ab08fc", size = 5112554, upload-time = "2025-06-21T12:16:01.434Z" },
+    { url = "https://files.pythonhosted.org/packages/c9/fc/84ea0cba8e760c4644b708b6819d91784c290288c27aca916115e3311d17/numpy-2.3.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:4e602e1b8682c2b833af89ba641ad4176053aaa50f5cacda1a27004352dde943", size = 6646560, upload-time = "2025-06-21T12:16:11.895Z" },
+    { url = "https://files.pythonhosted.org/packages/61/b2/512b0c2ddec985ad1e496b0bd853eeb572315c0f07cd6997473ced8f15e2/numpy-2.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8e333040d069eba1652fb08962ec5b76af7f2c7bce1df7e1418c8055cf776f25", size = 14260638, upload-time = "2025-06-21T12:16:32.611Z" },
+    { url = "https://files.pythonhosted.org/packages/6e/45/c51cb248e679a6c6ab14b7a8e3ead3f4a3fe7425fc7a6f98b3f147bec532/numpy-2.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e7cbf5a5eafd8d230a3ce356d892512185230e4781a361229bd902ff403bc660", size = 16632729, upload-time = "2025-06-21T12:16:57.439Z" },
+    { url = "https://files.pythonhosted.org/packages/e4/ff/feb4be2e5c09a3da161b412019caf47183099cbea1132fd98061808c2df2/numpy-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1b8f26d1086835f442286c1d9b64bb3974b0b1e41bb105358fd07d20872952", size = 15565330, upload-time = "2025-06-21T12:17:20.638Z" },
+    { url = "https://files.pythonhosted.org/packages/bc/6d/ceafe87587101e9ab0d370e4f6e5f3f3a85b9a697f2318738e5e7e176ce3/numpy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ee8340cb48c9b7a5899d1149eece41ca535513a9698098edbade2a8e7a84da77", size = 18361734, upload-time = "2025-06-21T12:17:47.938Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/19/0fb49a3ea088be691f040c9bf1817e4669a339d6e98579f91859b902c636/numpy-2.3.1-cp312-cp312-win32.whl", hash = "sha256:e772dda20a6002ef7061713dc1e2585bc1b534e7909b2030b5a46dae8ff077ab", size = 6320411, upload-time = "2025-06-21T12:17:58.475Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/3e/e28f4c1dd9e042eb57a3eb652f200225e311b608632bc727ae378623d4f8/numpy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cfecc7822543abdea6de08758091da655ea2210b8ffa1faf116b940693d3df76", size = 12734973, upload-time = "2025-06-21T12:18:17.601Z" },
+    { url = "https://files.pythonhosted.org/packages/04/a8/8a5e9079dc722acf53522b8f8842e79541ea81835e9b5483388701421073/numpy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:7be91b2239af2658653c5bb6f1b8bccafaf08226a258caf78ce44710a0160d30", size = 10191491, upload-time = "2025-06-21T12:18:33.585Z" },
+    { url = "https://files.pythonhosted.org/packages/d4/bd/35ad97006d8abff8631293f8ea6adf07b0108ce6fec68da3c3fcca1197f2/numpy-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25a1992b0a3fdcdaec9f552ef10d8103186f5397ab45e2d25f8ac51b1a6b97e8", size = 20889381, upload-time = "2025-06-21T12:19:04.103Z" },
+    { url = "https://files.pythonhosted.org/packages/f1/4f/df5923874d8095b6062495b39729178eef4a922119cee32a12ee1bd4664c/numpy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7dea630156d39b02a63c18f508f85010230409db5b2927ba59c8ba4ab3e8272e", size = 14152726, upload-time = "2025-06-21T12:19:25.599Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/0f/a1f269b125806212a876f7efb049b06c6f8772cf0121139f97774cd95626/numpy-2.3.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bada6058dd886061f10ea15f230ccf7dfff40572e99fef440a4a857c8728c9c0", size = 5105145, upload-time = "2025-06-21T12:19:34.782Z" },
+    { url = "https://files.pythonhosted.org/packages/6d/63/a7f7fd5f375b0361682f6ffbf686787e82b7bbd561268e4f30afad2bb3c0/numpy-2.3.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:a894f3816eb17b29e4783e5873f92faf55b710c2519e5c351767c51f79d8526d", size = 6639409, upload-time = "2025-06-21T12:19:45.228Z" },
+    { url = "https://files.pythonhosted.org/packages/bf/0d/1854a4121af895aab383f4aa233748f1df4671ef331d898e32426756a8a6/numpy-2.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:18703df6c4a4fee55fd3d6e5a253d01c5d33a295409b03fda0c86b3ca2ff41a1", size = 14257630, upload-time = "2025-06-21T12:20:06.544Z" },
+    { url = "https://files.pythonhosted.org/packages/50/30/af1b277b443f2fb08acf1c55ce9d68ee540043f158630d62cef012750f9f/numpy-2.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5902660491bd7a48b2ec16c23ccb9124b8abfd9583c5fdfa123fe6b421e03de1", size = 16627546, upload-time = "2025-06-21T12:20:31.002Z" },
+    { url = "https://files.pythonhosted.org/packages/6e/ec/3b68220c277e463095342d254c61be8144c31208db18d3fd8ef02712bcd6/numpy-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:36890eb9e9d2081137bd78d29050ba63b8dab95dff7912eadf1185e80074b2a0", size = 15562538, upload-time = "2025-06-21T12:20:54.322Z" },
+    { url = "https://files.pythonhosted.org/packages/77/2b/4014f2bcc4404484021c74d4c5ee8eb3de7e3f7ac75f06672f8dcf85140a/numpy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a780033466159c2270531e2b8ac063704592a0bc62ec4a1b991c7c40705eb0e8", size = 18360327, upload-time = "2025-06-21T12:21:21.053Z" },
+    { url = "https://files.pythonhosted.org/packages/40/8d/2ddd6c9b30fcf920837b8672f6c65590c7d92e43084c25fc65edc22e93ca/numpy-2.3.1-cp313-cp313-win32.whl", hash = "sha256:39bff12c076812595c3a306f22bfe49919c5513aa1e0e70fac756a0be7c2a2b8", size = 6312330, upload-time = "2025-06-21T12:25:07.447Z" },
+    { url = "https://files.pythonhosted.org/packages/dd/c8/beaba449925988d415efccb45bf977ff8327a02f655090627318f6398c7b/numpy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d5ee6eec45f08ce507a6570e06f2f879b374a552087a4179ea7838edbcbfa42", size = 12731565, upload-time = "2025-06-21T12:25:26.444Z" },
+    { url = "https://files.pythonhosted.org/packages/0b/c3/5c0c575d7ec78c1126998071f58facfc124006635da75b090805e642c62e/numpy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c4d9e0a8368db90f93bd192bfa771ace63137c3488d198ee21dfb8e7771916e", size = 10190262, upload-time = "2025-06-21T12:25:42.196Z" },
+    { url = "https://files.pythonhosted.org/packages/ea/19/a029cd335cf72f79d2644dcfc22d90f09caa86265cbbde3b5702ccef6890/numpy-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b0b5397374f32ec0649dd98c652a1798192042e715df918c20672c62fb52d4b8", size = 20987593, upload-time = "2025-06-21T12:21:51.664Z" },
+    { url = "https://files.pythonhosted.org/packages/25/91/8ea8894406209107d9ce19b66314194675d31761fe2cb3c84fe2eeae2f37/numpy-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c5bdf2015ccfcee8253fb8be695516ac4457c743473a43290fd36eba6a1777eb", size = 14300523, upload-time = "2025-06-21T12:22:13.583Z" },
+    { url = "https://files.pythonhosted.org/packages/a6/7f/06187b0066eefc9e7ce77d5f2ddb4e314a55220ad62dd0bfc9f2c44bac14/numpy-2.3.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d70f20df7f08b90a2062c1f07737dd340adccf2068d0f1b9b3d56e2038979fee", size = 5227993, upload-time = "2025-06-21T12:22:22.53Z" },
+    { url = "https://files.pythonhosted.org/packages/e8/ec/a926c293c605fa75e9cfb09f1e4840098ed46d2edaa6e2152ee35dc01ed3/numpy-2.3.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:2fb86b7e58f9ac50e1e9dd1290154107e47d1eef23a0ae9145ded06ea606f992", size = 6736652, upload-time = "2025-06-21T12:22:33.629Z" },
+    { url = "https://files.pythonhosted.org/packages/e3/62/d68e52fb6fde5586650d4c0ce0b05ff3a48ad4df4ffd1b8866479d1d671d/numpy-2.3.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:23ab05b2d241f76cb883ce8b9a93a680752fbfcbd51c50eff0b88b979e471d8c", size = 14331561, upload-time = "2025-06-21T12:22:55.056Z" },
+    { url = "https://files.pythonhosted.org/packages/fc/ec/b74d3f2430960044bdad6900d9f5edc2dc0fb8bf5a0be0f65287bf2cbe27/numpy-2.3.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ce2ce9e5de4703a673e705183f64fd5da5bf36e7beddcb63a25ee2286e71ca48", size = 16693349, upload-time = "2025-06-21T12:23:20.53Z" },
+    { url = "https://files.pythonhosted.org/packages/0d/15/def96774b9d7eb198ddadfcbd20281b20ebb510580419197e225f5c55c3e/numpy-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c4913079974eeb5c16ccfd2b1f09354b8fed7e0d6f2cab933104a09a6419b1ee", size = 15642053, upload-time = "2025-06-21T12:23:43.697Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/57/c3203974762a759540c6ae71d0ea2341c1fa41d84e4971a8e76d7141678a/numpy-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:010ce9b4f00d5c036053ca684c77441f2f2c934fd23bee058b4d6f196efd8280", size = 18434184, upload-time = "2025-06-21T12:24:10.708Z" },
+    { url = "https://files.pythonhosted.org/packages/22/8a/ccdf201457ed8ac6245187850aff4ca56a79edbea4829f4e9f14d46fa9a5/numpy-2.3.1-cp313-cp313t-win32.whl", hash = "sha256:6269b9edfe32912584ec496d91b00b6d34282ca1d07eb10e82dfc780907d6c2e", size = 6440678, upload-time = "2025-06-21T12:24:21.596Z" },
+    { url = "https://files.pythonhosted.org/packages/f1/7e/7f431d8bd8eb7e03d79294aed238b1b0b174b3148570d03a8a8a8f6a0da9/numpy-2.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:2a809637460e88a113e186e87f228d74ae2852a2e0c44de275263376f17b5bdc", size = 12870697, upload-time = "2025-06-21T12:24:40.644Z" },
+    { url = "https://files.pythonhosted.org/packages/d4/ca/af82bf0fad4c3e573c6930ed743b5308492ff19917c7caaf2f9b6f9e2e98/numpy-2.3.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eccb9a159db9aed60800187bc47a6d3451553f0e1b08b068d8b277ddfbb9b244", size = 10260376, upload-time = "2025-06-21T12:24:56.884Z" },
+    { url = "https://files.pythonhosted.org/packages/e8/34/facc13b9b42ddca30498fc51f7f73c3d0f2be179943a4b4da8686e259740/numpy-2.3.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ad506d4b09e684394c42c966ec1527f6ebc25da7f4da4b1b056606ffe446b8a3", size = 21070637, upload-time = "2025-06-21T12:26:12.518Z" },
+    { url = "https://files.pythonhosted.org/packages/65/b6/41b705d9dbae04649b529fc9bd3387664c3281c7cd78b404a4efe73dcc45/numpy-2.3.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ebb8603d45bc86bbd5edb0d63e52c5fd9e7945d3a503b77e486bd88dde67a19b", size = 5304087, upload-time = "2025-06-21T12:26:22.294Z" },
+    { url = "https://files.pythonhosted.org/packages/7a/b4/fe3ac1902bff7a4934a22d49e1c9d71a623204d654d4cc43c6e8fe337fcb/numpy-2.3.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:15aa4c392ac396e2ad3d0a2680c0f0dee420f9fed14eef09bdb9450ee6dcb7b7", size = 6817588, upload-time = "2025-06-21T12:26:32.939Z" },
+    { url = "https://files.pythonhosted.org/packages/ae/ee/89bedf69c36ace1ac8f59e97811c1f5031e179a37e4821c3a230bf750142/numpy-2.3.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c6e0bf9d1a2f50d2b65a7cf56db37c095af17b59f6c132396f7c6d5dd76484df", size = 14399010, upload-time = "2025-06-21T12:26:54.086Z" },
+    { url = "https://files.pythonhosted.org/packages/15/08/e00e7070ede29b2b176165eba18d6f9784d5349be3c0c1218338e79c27fd/numpy-2.3.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:eabd7e8740d494ce2b4ea0ff05afa1b7b291e978c0ae075487c51e8bd93c0c68", size = 16752042, upload-time = "2025-06-21T12:27:19.018Z" },
+    { url = "https://files.pythonhosted.org/packages/48/6b/1c6b515a83d5564b1698a61efa245727c8feecf308f4091f565988519d20/numpy-2.3.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e610832418a2bc09d974cc9fecebfa51e9532d6190223bc5ef6a7402ebf3b5cb", size = 12927246, upload-time = "2025-06-21T12:27:38.618Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
+]
+
+[[package]]
+name = "pandas"
+version = "2.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "numpy" },
+    { name = "python-dateutil" },
+    { name = "pytz" },
+    { name = "tzdata" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d1/6f/75aa71f8a14267117adeeed5d21b204770189c0a0025acbdc03c337b28fc/pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2", size = 4487493, upload-time = "2025-07-07T19:20:04.079Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/76/1c/ccf70029e927e473a4476c00e0d5b32e623bff27f0402d0a92b7fc29bb9f/pandas-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2b0540963d83431f5ce8870ea02a7430adca100cec8a050f0811f8e31035541b", size = 11566608, upload-time = "2025-07-07T19:18:33.86Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/d3/3c37cb724d76a841f14b8f5fe57e5e3645207cc67370e4f84717e8bb7657/pandas-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fe7317f578c6a153912bd2292f02e40c1d8f253e93c599e82620c7f69755c74f", size = 10823181, upload-time = "2025-07-07T19:18:36.151Z" },
+    { url = "https://files.pythonhosted.org/packages/8a/4c/367c98854a1251940edf54a4df0826dcacfb987f9068abf3e3064081a382/pandas-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6723a27ad7b244c0c79d8e7007092d7c8f0f11305770e2f4cd778b3ad5f9f85", size = 11793570, upload-time = "2025-07-07T19:18:38.385Z" },
+    { url = "https://files.pythonhosted.org/packages/07/5f/63760ff107bcf5146eee41b38b3985f9055e710a72fdd637b791dea3495c/pandas-2.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3462c3735fe19f2638f2c3a40bd94ec2dc5ba13abbb032dd2fa1f540a075509d", size = 12378887, upload-time = "2025-07-07T19:18:41.284Z" },
+    { url = "https://files.pythonhosted.org/packages/15/53/f31a9b4dfe73fe4711c3a609bd8e60238022f48eacedc257cd13ae9327a7/pandas-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:98bcc8b5bf7afed22cc753a28bc4d9e26e078e777066bc53fac7904ddef9a678", size = 13230957, upload-time = "2025-07-07T19:18:44.187Z" },
+    { url = "https://files.pythonhosted.org/packages/e0/94/6fce6bf85b5056d065e0a7933cba2616dcb48596f7ba3c6341ec4bcc529d/pandas-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d544806b485ddf29e52d75b1f559142514e60ef58a832f74fb38e48d757b299", size = 13883883, upload-time = "2025-07-07T19:18:46.498Z" },
+    { url = "https://files.pythonhosted.org/packages/c8/7b/bdcb1ed8fccb63d04bdb7635161d0ec26596d92c9d7a6cce964e7876b6c1/pandas-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b3cd4273d3cb3707b6fffd217204c52ed92859533e31dc03b7c5008aa933aaab", size = 11340212, upload-time = "2025-07-07T19:18:49.293Z" },
+    { url = "https://files.pythonhosted.org/packages/46/de/b8445e0f5d217a99fe0eeb2f4988070908979bec3587c0633e5428ab596c/pandas-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:689968e841136f9e542020698ee1c4fbe9caa2ed2213ae2388dc7b81721510d3", size = 11588172, upload-time = "2025-07-07T19:18:52.054Z" },
+    { url = "https://files.pythonhosted.org/packages/1e/e0/801cdb3564e65a5ac041ab99ea6f1d802a6c325bb6e58c79c06a3f1cd010/pandas-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:025e92411c16cbe5bb2a4abc99732a6b132f439b8aab23a59fa593eb00704232", size = 10717365, upload-time = "2025-07-07T19:18:54.785Z" },
+    { url = "https://files.pythonhosted.org/packages/51/a5/c76a8311833c24ae61a376dbf360eb1b1c9247a5d9c1e8b356563b31b80c/pandas-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b7ff55f31c4fcb3e316e8f7fa194566b286d6ac430afec0d461163312c5841e", size = 11280411, upload-time = "2025-07-07T19:18:57.045Z" },
+    { url = "https://files.pythonhosted.org/packages/da/01/e383018feba0a1ead6cf5fe8728e5d767fee02f06a3d800e82c489e5daaf/pandas-2.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dcb79bf373a47d2a40cf7232928eb7540155abbc460925c2c96d2d30b006eb4", size = 11988013, upload-time = "2025-07-07T19:18:59.771Z" },
+    { url = "https://files.pythonhosted.org/packages/5b/14/cec7760d7c9507f11c97d64f29022e12a6cc4fc03ac694535e89f88ad2ec/pandas-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56a342b231e8862c96bdb6ab97170e203ce511f4d0429589c8ede1ee8ece48b8", size = 12767210, upload-time = "2025-07-07T19:19:02.944Z" },
+    { url = "https://files.pythonhosted.org/packages/50/b9/6e2d2c6728ed29fb3d4d4d302504fb66f1a543e37eb2e43f352a86365cdf/pandas-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ca7ed14832bce68baef331f4d7f294411bed8efd032f8109d690df45e00c4679", size = 13440571, upload-time = "2025-07-07T19:19:06.82Z" },
+    { url = "https://files.pythonhosted.org/packages/80/a5/3a92893e7399a691bad7664d977cb5e7c81cf666c81f89ea76ba2bff483d/pandas-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ac942bfd0aca577bef61f2bc8da8147c4ef6879965ef883d8e8d5d2dc3e744b8", size = 10987601, upload-time = "2025-07-07T19:19:09.589Z" },
+    { url = "https://files.pythonhosted.org/packages/32/ed/ff0a67a2c5505e1854e6715586ac6693dd860fbf52ef9f81edee200266e7/pandas-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9026bd4a80108fac2239294a15ef9003c4ee191a0f64b90f170b40cfb7cf2d22", size = 11531393, upload-time = "2025-07-07T19:19:12.245Z" },
+    { url = "https://files.pythonhosted.org/packages/c7/db/d8f24a7cc9fb0972adab0cc80b6817e8bef888cfd0024eeb5a21c0bb5c4a/pandas-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6de8547d4fdb12421e2d047a2c446c623ff4c11f47fddb6b9169eb98ffba485a", size = 10668750, upload-time = "2025-07-07T19:19:14.612Z" },
+    { url = "https://files.pythonhosted.org/packages/0f/b0/80f6ec783313f1e2356b28b4fd8d2148c378370045da918c73145e6aab50/pandas-2.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782647ddc63c83133b2506912cc6b108140a38a37292102aaa19c81c83db2928", size = 11342004, upload-time = "2025-07-07T19:19:16.857Z" },
+    { url = "https://files.pythonhosted.org/packages/e9/e2/20a317688435470872885e7fc8f95109ae9683dec7c50be29b56911515a5/pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba6aff74075311fc88504b1db890187a3cd0f887a5b10f5525f8e2ef55bfdb9", size = 12050869, upload-time = "2025-07-07T19:19:19.265Z" },
+    { url = "https://files.pythonhosted.org/packages/55/79/20d746b0a96c67203a5bee5fb4e00ac49c3e8009a39e1f78de264ecc5729/pandas-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5635178b387bd2ba4ac040f82bc2ef6e6b500483975c4ebacd34bec945fda12", size = 12750218, upload-time = "2025-07-07T19:19:21.547Z" },
+    { url = "https://files.pythonhosted.org/packages/7c/0f/145c8b41e48dbf03dd18fdd7f24f8ba95b8254a97a3379048378f33e7838/pandas-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f3bf5ec947526106399a9e1d26d40ee2b259c66422efdf4de63c848492d91bb", size = 13416763, upload-time = "2025-07-07T19:19:23.939Z" },
+    { url = "https://files.pythonhosted.org/packages/b2/c0/54415af59db5cdd86a3d3bf79863e8cc3fa9ed265f0745254061ac09d5f2/pandas-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c78cf43c8fde236342a1cb2c34bcff89564a7bfed7e474ed2fffa6aed03a956", size = 10987482, upload-time = "2025-07-07T19:19:42.699Z" },
+    { url = "https://files.pythonhosted.org/packages/48/64/2fd2e400073a1230e13b8cd604c9bc95d9e3b962e5d44088ead2e8f0cfec/pandas-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8dfc17328e8da77be3cf9f47509e5637ba8f137148ed0e9b5241e1baf526e20a", size = 12029159, upload-time = "2025-07-07T19:19:26.362Z" },
+    { url = "https://files.pythonhosted.org/packages/d8/0a/d84fd79b0293b7ef88c760d7dca69828d867c89b6d9bc52d6a27e4d87316/pandas-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ec6c851509364c59a5344458ab935e6451b31b818be467eb24b0fe89bd05b6b9", size = 11393287, upload-time = "2025-07-07T19:19:29.157Z" },
+    { url = "https://files.pythonhosted.org/packages/50/ae/ff885d2b6e88f3c7520bb74ba319268b42f05d7e583b5dded9837da2723f/pandas-2.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911580460fc4884d9b05254b38a6bfadddfcc6aaef856fb5859e7ca202e45275", size = 11309381, upload-time = "2025-07-07T19:19:31.436Z" },
+    { url = "https://files.pythonhosted.org/packages/85/86/1fa345fc17caf5d7780d2699985c03dbe186c68fee00b526813939062bb0/pandas-2.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f4d6feeba91744872a600e6edbbd5b033005b431d5ae8379abee5bcfa479fab", size = 11883998, upload-time = "2025-07-07T19:19:34.267Z" },
+    { url = "https://files.pythonhosted.org/packages/81/aa/e58541a49b5e6310d89474333e994ee57fea97c8aaa8fc7f00b873059bbf/pandas-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fe37e757f462d31a9cd7580236a82f353f5713a80e059a29753cf938c6775d96", size = 12704705, upload-time = "2025-07-07T19:19:36.856Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/f9/07086f5b0f2a19872554abeea7658200824f5835c58a106fa8f2ae96a46c/pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444", size = 13189044, upload-time = "2025-07-07T19:19:39.999Z" },
+]
+
+[[package]]
+name = "pillow"
+version = "11.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" },
+    { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" },
+    { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" },
+    { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" },
+    { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" },
+    { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" },
+    { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" },
+    { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" },
+    { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" },
+    { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" },
+    { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" },
+    { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" },
+    { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" },
+    { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" },
+    { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" },
+    { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" },
+    { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" },
+    { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" },
+    { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" },
+    { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" },
+    { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" },
+    { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" },
+    { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" },
+    { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" },
+    { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" },
+    { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" },
+    { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" },
+    { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" },
+    { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" },
+    { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" },
+    { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" },
+    { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" },
+    { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" },
+    { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" },
+    { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" },
+    { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" },
+    { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" },
+    { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" },
+    { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" },
+    { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" },
+    { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" },
+    { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" },
+    { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" },
+    { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" },
+    { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" },
+    { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" },
+    { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" },
+    { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" },
+    { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" },
+    { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" },
+    { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" },
+    { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" },
+    { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" },
+    { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" },
+    { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" },
+    { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" },
+    { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" },
+    { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" },
+    { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" },
+    { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" },
+    { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" },
+    { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" },
+    { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" },
+    { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" },
+    { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" },
+    { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" },
+    { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" },
+    { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" },
+    { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" },
+]
+
+[[package]]
+name = "pyparsing"
+version = "3.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
+[[package]]
+name = "pytz"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[package]]
+name = "tzdata"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
+]