trading_calculator.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. from datetime import timedelta, date
  2. from collections import defaultdict
  3. import logging
  4. # 配置日志记录
  5. logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(filename)s - %(message)s')
  6. class TradingCalculator:
  7. """
  8. 根据每日持仓信息计算交易日期和买卖点。
  9. """
  10. def __init__(self, pdf_files_info):
  11. """
  12. 初始化 TradingCalculator。
  13. Args:
  14. pdf_files_info (list): 包含字典的列表,每个字典代表一个 PDF 文件信息。
  15. 格式: [{'record_date': date, 'codes': [code1, ...], 'path': str}, ...]
  16. 需要确保列表按 record_date 升序排列。
  17. """
  18. self.pdf_files_info = pdf_files_info
  19. # 记录日期到持仓代码集合的映射
  20. self.holdings_by_record_date = {info['record_date']: set(info['codes']) for info in pdf_files_info}
  21. # 原始记录日期列表,已排序
  22. self.record_dates = sorted(self.holdings_by_record_date.keys())
  23. print(f"初始化 TradingCalculator,共有 {len(self.record_dates)} 个记录日期。")
  24. def determine_buy_sell_dates(self):
  25. """
  26. 根据记录日期和每日持仓,计算每个股票的买入卖出日期。
  27. Returns:
  28. list: 包含交易记录的列表。
  29. 格式: [{'code': str, 'buy_date': date or str, 'sell_date': date or str}, ...]
  30. 买入或卖出日期可能为字符串 "未知 (范围外)"。
  31. """
  32. if not self.record_dates:
  33. logging.warning("没有记录日期,无法计算买卖点。")
  34. return []
  35. # 按股票代码收集其出现的所有记录日期
  36. stock_record_dates = defaultdict(list)
  37. for record_date in self.record_dates:
  38. holdings = self.holdings_by_record_date.get(record_date, set())
  39. for code in holdings:
  40. stock_record_dates[code].append(record_date)
  41. transactions = []
  42. # record_dates 已经是排序好的完整日期列表
  43. all_available_dates = self.record_dates
  44. first_overall_record_date = all_available_dates[0]
  45. last_overall_record_date = all_available_dates[-1]
  46. for code, dates in stock_record_dates.items():
  47. if not dates:
  48. continue
  49. # dates 已经是 stock_record_dates 中按 key (record_date) 排序后append的,所以是排序好的
  50. # dates.sort() # 确保日期有序,但这里应该已经是排序好的
  51. # 识别连续的持有日期段(基于记录日期)
  52. segments = []
  53. current_segment = [dates[0]]
  54. for i in range(1, len(dates)):
  55. current_date = dates[i]
  56. last_date_in_segment = current_segment[-1]
  57. # 检查当前记录日期是否是上一记录日期的下一个日期(在 all_available_dates 列表中)
  58. try:
  59. last_date_index = all_available_dates.index(last_date_in_segment)
  60. # 确保索引+1在列表范围内,并且下一个日期就是当前日期
  61. if last_date_index + 1 < len(all_available_dates) and all_available_dates[last_date_index + 1] == current_date:
  62. # 日期连续
  63. current_segment.append(current_date)
  64. else:
  65. # 日期不连续,当前段结束,开始新段
  66. segments.append(current_segment)
  67. current_segment = [current_date]
  68. except ValueError:
  69. # This should not happen if dates come from self.record_dates
  70. logging.error(f"日期 {last_date_in_segment} 未在 record_dates 中找到,代码 {code}")
  71. # Treat as interruption if date is unexpected
  72. segments.append(current_segment)
  73. current_segment = [current_date]
  74. # 添加最后一个段
  75. if current_segment:
  76. segments.append(current_segment)
  77. # 根据连续段确定买卖日期
  78. # 买入日期是持有段开始记录日期的前一个记录日期(作为交易日)
  79. # 卖出日期是持有段结束记录日期的前一个记录日期(作为交易日),除非是最后一天,则卖出日期未知
  80. # PRD 提到交易日是上一个记录日,持仓是基于当天记录日。
  81. # 记录日 i 的持仓对应交易日 i-1
  82. # 持仓段 [记录日 i, 记录日 j] 对应的交易日段为 [记录日 i-1, 记录日 j-1]
  83. # 买入发生在 记录日 i-1
  84. # 卖出发生在 记录日 j
  85. for segment in segments:
  86. segment_start_record_date = segment[0] # 持有段的第一个记录日期
  87. segment_end_record_date = segment[-1] # 持有段的最后一个记录日期
  88. buy_date = None
  89. sell_date = None
  90. # 确定买入日期 (上一个记录日对应的交易日)
  91. if segment_start_record_date == first_overall_record_date:
  92. # 如果持有从第一个记录日开始,买入日期未知
  93. logging.info(f"{code} 在第一个记录日 {first_overall_record_date} 持有,买入日期未知 (范围外)。")
  94. buy_date = None
  95. else:
  96. # 找到 segment_start_record_date 在 all_available_dates 中的前一个记录日期
  97. try:
  98. start_date_index = all_available_dates.index(segment_start_record_date)
  99. if start_date_index > 0:
  100. # 买入日期是持有段开始记录日期的前一个记录日期
  101. buy_date = all_available_dates[start_date_index - 1]
  102. else:
  103. # 这应该被上面的 if 覆盖,作为备用
  104. logging.warning(f"{code} 的开始记录日期 {segment_start_record_date} 是记录日列表的第一个日期,买入日期未知。")
  105. buy_date = None
  106. except ValueError:
  107. logging.error(f"开始记录日期 {segment_start_record_date} 未在 record_dates 中找到,代码 {code}")
  108. buy_date = "错误" # Should not happen
  109. # 确定卖出日期
  110. if segment_end_record_date == last_overall_record_date:
  111. # 如果持有到最后一个记录日结束,卖出日期未知
  112. logging.info(f"{code} 在最后一个记录日 {last_overall_record_date} 仍持有,卖出日期未知 (范围外)。")
  113. sell_date = None
  114. else:
  115. # 卖出日期是持有段的最后一个记录日期本身 (因为PRD说卖出是当天)
  116. sell_date = segment_end_record_date
  117. logging.info(f"{code} 在 {sell_date} 卖出。")
  118. # 如果买入日期是日期对象,卖出日期也是日期对象,才添加交易
  119. # 如果任何一个未知,则不添加具体的交易记录,只记录日志
  120. if isinstance(buy_date, date) or isinstance(sell_date, date):
  121. transactions.append({'code': code, 'buy_date': buy_date, 'sell_date': sell_date})
  122. logging.info(f"{code} 的交易 ({buy_date} -> {sell_date}) 已添加。")
  123. else:
  124. logging.info(f"{code} 的交易 ({buy_date} -> {sell_date}) 因日期范围外而忽略。")
  125. print(f"获得了 {len(transactions)} 笔买卖交易 (含范围外未确定日期的)。")
  126. # 过滤掉未知日期的交易,只返回有确定买卖日期的交易
  127. final_transactions = [t for t in transactions if isinstance(t['buy_date'], date) and isinstance(t['sell_date'], date)]
  128. partial_transactions = [t for t in transactions if not isinstance(t['buy_date'], date) or not isinstance(t['sell_date'], date)]
  129. print(f"确定了 {len(final_transactions)} 笔有明确买卖日期的交易。")
  130. print(f"忽略了 {len(partial_transactions)} 笔有范围外未确定日期的交易。")
  131. return final_transactions