cyb50_30min_intraday_reversal.py 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992
  1. import pandas as pd
  2. import numpy as np
  3. import akshare as ak
  4. import warnings
  5. import json
  6. import os
  7. from datetime import datetime, timedelta
  8. warnings.filterwarnings('ignore')
  9. # ==================== 配置管理模块 ====================
  10. class ConfigManager:
  11. """配置文件管理类"""
  12. def __init__(self, config_file='config.json'):
  13. self.config_file = config_file
  14. self.config = self.load_config()
  15. def load_config(self):
  16. """加载配置文件"""
  17. try:
  18. if os.path.exists(self.config_file):
  19. with open(self.config_file, 'r', encoding='utf-8') as f:
  20. config = json.load(f)
  21. print(f"配置文件加载成功: {self.config_file}")
  22. return config
  23. else:
  24. print(f"配置文件不存在,使用默认配置: {self.config_file}")
  25. return self.get_default_config()
  26. except Exception as e:
  27. print(f"配置文件加载失败: {e},使用默认配置")
  28. return self.get_default_config()
  29. def get_default_config(self):
  30. """获取默认配置"""
  31. return {
  32. "data_source": {
  33. "use_local_file": False,
  34. "local_file_path": "D:\\work\\project\\catfly\\data\\SZ#399673_30min.csv"
  35. },
  36. "strategy": {
  37. "initial_capital": 1000000,
  38. "backtest_start_date": "2025-10-01",
  39. "prewamp_days": 30,
  40. "position_size_pct": 1.0,
  41. "stop_loss_pct": 0.008,
  42. "take_profit_pct": 0.015,
  43. "max_hold_bars": 16
  44. }
  45. }
  46. def get(self, section, key, default=None):
  47. """获取配置项"""
  48. try:
  49. return self.config.get(section, {}).get(key, default)
  50. except:
  51. return default
  52. def save_config(self):
  53. """保存配置到文件"""
  54. try:
  55. with open(self.config_file, 'w', encoding='utf-8') as f:
  56. json.dump(self.config, f, indent=2, ensure_ascii=False)
  57. print(f"配置文件保存成功: {self.config_file}")
  58. except Exception as e:
  59. print(f"配置文件保存失败: {e}")
  60. # ==================== 数据获取模块 ====================
  61. class IntradayDataFetcher:
  62. """30分钟K线数据获取类"""
  63. def __init__(self, config_manager=None):
  64. self.symbol = "399673" # 创业板50指数
  65. self.config_manager = config_manager
  66. def fetch_30min_data(self, start_date=None, end_date=None) -> pd.DataFrame:
  67. """获取指定时间范围的30分钟K线数据"""
  68. try:
  69. if start_date is None:
  70. start_date = datetime.now() - timedelta(days=60)
  71. if end_date is None:
  72. end_date = datetime.now()
  73. print(f"正在获取创业板50指数的30分钟K线数据...")
  74. print(f"时间范围: {start_date.strftime('%Y-%m-%d')} 至 {end_date.strftime('%Y-%m-%d')}")
  75. # 检查数据源开关
  76. use_local_file = False
  77. local_file_path = ""
  78. if self.config_manager:
  79. use_local_file = self.config_manager.get('data_source', 'use_local_file', False)
  80. local_file_path = self.config_manager.get('data_source', 'local_file_path', '')
  81. # 如果开关打开,从本地文件读取
  82. if use_local_file:
  83. print(f"数据源开关: 本地文件模式")
  84. print(f"本地文件路径: {local_file_path}")
  85. return self._load_local_file(local_file_path, start_date, end_date)
  86. else:
  87. print(f"数据源开关: 在线获取模式")
  88. return self._fetch_online_data(start_date, end_date)
  89. except Exception as e:
  90. print(f"获取数据时出错: {str(e)}")
  91. raise
  92. def _load_local_file(self, file_path, start_date, end_date) -> pd.DataFrame:
  93. """从本地文件加载数据"""
  94. try:
  95. if not os.path.exists(file_path):
  96. raise FileNotFoundError(f"本地文件不存在: {file_path}")
  97. print(f"正在从本地文件读取数据...")
  98. print(f"文件路径: {file_path}")
  99. # 检查文件扩展名,选择不同的读取方式
  100. if file_path.endswith('.txt'):
  101. # 处理文本格式文件
  102. print("检测到文本格式文件,使用文本解析模式...")
  103. return self._parse_text_file(file_path, start_date, end_date)
  104. else:
  105. # 处理CSV格式文件
  106. print("检测到CSV格式文件,使用CSV解析模式...")
  107. data = pd.read_csv(file_path)
  108. return self._process_dataframe(data, start_date, end_date)
  109. except Exception as e:
  110. print(f"从本地文件加载数据失败: {e}")
  111. raise
  112. def _parse_text_file(self, file_path, start_date, end_date) -> pd.DataFrame:
  113. """解析文本格式的数据文件"""
  114. try:
  115. data_list = []
  116. # 尝试多种编码格式
  117. encodings = ['gbk', 'gb2312', 'utf-8', 'latin-1']
  118. lines = None
  119. for encoding in encodings:
  120. try:
  121. with open(file_path, 'r', encoding=encoding) as f:
  122. lines = f.readlines()
  123. print(f"成功使用编码: {encoding}")
  124. break
  125. except UnicodeDecodeError:
  126. continue
  127. if lines is None:
  128. raise ValueError("无法读取文件,尝试了多种编码格式都失败")
  129. # 跳过前两行(标题行)
  130. for line in lines[2:]:
  131. line = line.strip()
  132. if not line:
  133. continue
  134. parts = line.split()
  135. if len(parts) >= 7:
  136. try:
  137. # 格式: 日期 时间 开盘 最高 最低 收盘 成交量 成交额
  138. date_time_str = f"{parts[0]} {parts[1]}"
  139. datetime_obj = pd.to_datetime(date_time_str, format='%Y/%m/%d %H%M')
  140. data_list.append({
  141. 'DateTime': datetime_obj,
  142. 'Open': float(parts[2]),
  143. 'High': float(parts[3]),
  144. 'Low': float(parts[4]),
  145. 'Close': float(parts[5]),
  146. 'Volume': float(parts[6]),
  147. 'Amount': float(parts[7]) if len(parts) > 7 else 0
  148. })
  149. except (ValueError, IndexError) as e:
  150. print(f"跳过异常行: {line[:50]}... 错误: {e}")
  151. continue
  152. if not data_list:
  153. raise ValueError("文本文件中没有解析到有效数据")
  154. print(f"成功解析 {len(data_list)} 条数据")
  155. data = pd.DataFrame(data_list)
  156. return self._process_dataframe(data, start_date, end_date)
  157. except Exception as e:
  158. print(f"解析文本文件失败: {e}")
  159. raise
  160. def _process_dataframe(self, data, start_date, end_date) -> pd.DataFrame:
  161. """处理和标准化数据框"""
  162. try:
  163. # 检查并转换列名
  164. print(f"原始数据列名: {data.columns.tolist()}")
  165. print(f"原始数据行数: {len(data)}")
  166. # 标准化列名
  167. column_mapping = {
  168. '时间': 'DateTime', '日期': 'DateTime', 'datetime': 'DateTime', 'time': 'DateTime',
  169. '开盘': 'Open', 'open': 'Open', 'Open': 'Open', '开盘价': 'Open',
  170. '收盘': 'Close', 'close': 'Close', 'Close': 'Close', '收盘价': 'Close',
  171. '最高': 'High', 'high': 'High', 'High': 'High', '最高价': 'High',
  172. '最低': 'Low', 'low': 'Low', 'Low': 'Low', '最低价': 'Low',
  173. '成交量': 'Volume', 'volume': 'Volume', 'Volume': 'Volume', 'vol': 'Volume'
  174. }
  175. # 重命名列
  176. data.rename(columns=column_mapping, inplace=True)
  177. # 设置时间索引
  178. if 'DateTime' in data.columns:
  179. data['DateTime'] = pd.to_datetime(data['DateTime'])
  180. data.set_index('DateTime', inplace=True)
  181. else:
  182. raise ValueError("数据中找不到时间列(DateTime/时间/日期)")
  183. data.sort_index(inplace=True)
  184. # 筛选时间范围
  185. filtered_data = data[(data.index >= start_date) & (data.index <= end_date)].copy()
  186. if filtered_data.empty:
  187. print(f"警告:指定时间范围没有数据")
  188. print(f"可用数据范围: {data.index[0]} 到 {data.index[-1]}")
  189. print(f"请求的时间范围: {start_date} 到 {end_date}")
  190. # 返回空数据框而不是抛出异常
  191. return filtered_data
  192. # 确保必需的列存在
  193. required_columns = ['Open', 'High', 'Low', 'Close', 'Volume']
  194. missing_columns = [col for col in required_columns if col not in filtered_data.columns]
  195. if missing_columns:
  196. raise ValueError(f"数据缺少必需的列: {missing_columns}")
  197. # 添加缺失的列
  198. if 'Amount' not in filtered_data.columns:
  199. filtered_data['Amount'] = 0
  200. # 计算基础指标(本地文件可能缺少这些)
  201. if 'Returns' not in filtered_data.columns:
  202. filtered_data['Returns'] = filtered_data['Close'].pct_change()
  203. if 'High_Low_Pct' not in filtered_data.columns:
  204. filtered_data['High_Low_Pct'] = (filtered_data['High'] - filtered_data['Low']) / filtered_data['Close'].shift(1)
  205. if 'Close_Open_Pct' not in filtered_data.columns:
  206. filtered_data['Close_Open_Pct'] = (filtered_data['Close'] - filtered_data['Open']) / filtered_data['Open']
  207. # 处理缺失值
  208. filtered_data.fillna(method='ffill', inplace=True)
  209. filtered_data.dropna(inplace=True)
  210. print(f"本地文件数据处理成功: {len(filtered_data)}条")
  211. print(f"数据范围: {filtered_data.index[0]} 到 {filtered_data.index[-1]}")
  212. return filtered_data
  213. except Exception as e:
  214. print(f"处理数据框失败: {e}")
  215. raise
  216. def _fetch_online_data(self, start_date, end_date) -> pd.DataFrame:
  217. """在线获取30分钟K线数据 - 东方财富优先,新浪财经备用"""
  218. data = None
  219. # ===== 方法1: 东方财富数据源(主要数据源) =====
  220. try:
  221. print("[DATA_SOURCE_1] 正在使用东方财富30分钟K线接口...")
  222. data = ak.index_zh_a_hist_min_em(symbol=self.symbol, period="30")
  223. if not data.empty and len(data) >= 50:
  224. print(f"[SUCCESS] 东方财富获取到{len(data)}条30分钟数据")
  225. print(f"[TIME_RANGE] 数据范围: {data.index[0]} 到 {data.index[-1]}")
  226. # 检查数据时效性
  227. latest_time = data.index[-1]
  228. current_time = datetime.now()
  229. if hasattr(latest_time, 'hour') and hasattr(latest_time, 'minute'):
  230. time_delay = current_time - latest_time
  231. print(f"[DATA_DELAY] 数据延迟: {time_delay}")
  232. else:
  233. print(f"[INFO] 数据索引类型: {type(latest_time)}")
  234. else:
  235. print(f"[FAIL] 东方财富数据不足或为空")
  236. except Exception as e:
  237. print(f"[ERROR] 东方财富数据源失败: {e}")
  238. # ===== 方法2: 新浪财经数据源(备用数据源) =====
  239. if data is None or data.empty or len(data) < 50:
  240. try:
  241. print("[DATA_SOURCE_2] 正在使用新浪财经30分钟K线接口...")
  242. import requests
  243. import json
  244. import re
  245. symbol = "sz399673"
  246. url = f"https://quotes.sina.cn/cn/api/jsonp_v2.php/var_{symbol}_30_1768824839904=/CN_MarketDataService.getKLineData?symbol={symbol}&scale=30&ma=no&datalen=1023"
  247. headers = {
  248. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
  249. 'Referer': 'https://finance.sina.com.cn/'
  250. }
  251. response = requests.get(url, headers=headers, timeout=15)
  252. response_text = response.text
  253. # 解析JSONP响应
  254. array_pattern = r'=([\[ ].+?\])'
  255. match = re.search(array_pattern, response_text)
  256. if match:
  257. json_str = match.group(1)
  258. else:
  259. json_start = response_text.find('[')
  260. json_end = response_text.rfind(']') + 1
  261. if json_start >= 0 and json_end > json_start:
  262. json_str = response_text[json_start:json_end]
  263. else:
  264. raise Exception("无法解析JSONP响应")
  265. data_dict = json.loads(json_str)
  266. if data_dict and isinstance(data_dict, list):
  267. data_list = []
  268. for item in data_dict:
  269. try:
  270. data_list.append({
  271. 'DateTime': item.get('day'),
  272. 'Open': float(item.get('open', 0)),
  273. 'High': float(item.get('high', 0)),
  274. 'Low': float(item.get('low', 0)),
  275. 'Close': float(item.get('close', 0)),
  276. 'Volume': float(item.get('volume', 0)),
  277. 'Amount': 0
  278. })
  279. except Exception:
  280. continue
  281. if data_list:
  282. data = pd.DataFrame(data_list)
  283. print(f"[SUCCESS] 新浪财经获取到{len(data)}条30分钟数据")
  284. print(f"[TIME_RANGE] 数据范围: {data.index[0]} 到 {data.index[-1]}")
  285. else:
  286. print("[FAIL] 新浪财经数据解析失败")
  287. else:
  288. print("[FAIL] 新浪财经返回数据格式错误")
  289. except Exception as e:
  290. print(f"[ERROR] 新浪财经数据源失败: {e}")
  291. # ===== 数据验证和格式化 =====
  292. if data is None or data.empty:
  293. raise ValueError("[FATAL_ERROR] 所有数据源均无法获取数据")
  294. # 重命名列(针对东方财富数据格式)
  295. data.rename(columns={
  296. '时间': 'DateTime', '开盘': 'Open', '收盘': 'Close',
  297. '最高': 'High', '最低': 'Low', '成交量': 'Volume',
  298. '成交额': 'Amount', '振幅': 'Amplitude', '涨跌幅': 'Change_Pct',
  299. '涨跌额': 'Change_Amount', '换手率': 'Turnover'
  300. }, inplace=True)
  301. # 设置时间索引
  302. if 'DateTime' in data.columns:
  303. data['DateTime'] = pd.to_datetime(data['DateTime'])
  304. data.set_index('DateTime', inplace=True)
  305. else:
  306. raise ValueError("[ERROR] 数据中找不到时间列(DateTime)")
  307. data.sort_index(inplace=True)
  308. # 筛选时间范围
  309. filtered_data = data[(data.index >= start_date) & (data.index <= end_date)].copy()
  310. if filtered_data.empty:
  311. print(f"[WARNING] 筛选后数据为空,可用范围: {data.index[0]} 到 {data.index[-1]}")
  312. raise ValueError(f"[ERROR] 指定时间范围没有数据")
  313. # 计算基础指标
  314. filtered_data['Returns'] = filtered_data['Close'].pct_change()
  315. filtered_data['High_Low_Pct'] = (filtered_data['High'] - filtered_data['Low']) / filtered_data['Close'].shift(1)
  316. filtered_data['Close_Open_Pct'] = (filtered_data['Close'] - filtered_data['Open']) / filtered_data['Open']
  317. # 处理缺失值
  318. filtered_data.fillna(method='ffill', inplace=True)
  319. filtered_data.dropna(inplace=True)
  320. print(f"[FINAL_DATA] 成功处理{len(filtered_data)}条数据")
  321. print(f"[FINAL_RANGE] 最终数据范围: {filtered_data.index[0]} 到 {filtered_data.index[-1]}")
  322. return filtered_data
  323. def calculate_intraday_indicators(self, data: pd.DataFrame) -> pd.DataFrame:
  324. """计算30分钟技术指标"""
  325. print("正在计算30分钟技术指标...")
  326. df = data.copy()
  327. # 短期移动平均线
  328. df['MA6'] = df['Close'].rolling(window=6).mean() # 3小时
  329. df['MA12'] = df['Close'].rolling(window=12).mean() # 6小时
  330. df['MA24'] = df['Close'].rolling(window=24).mean() # 12小时(一天)
  331. # RSI
  332. delta = df['Close'].diff()
  333. gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
  334. loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
  335. rs = gain / loss
  336. df['RSI'] = 100 - (100 / (1 + rs))
  337. # 布林带
  338. df['BB_middle'] = df['Close'].rolling(window=20).mean()
  339. bb_std = df['Close'].rolling(window=20).std()
  340. df['BB_upper'] = df['BB_middle'] + (bb_std * 2)
  341. df['BB_lower'] = df['BB_middle'] - (bb_std * 2)
  342. df['BB_width'] = (df['BB_upper'] - df['BB_lower']) / df['BB_middle']
  343. # MACD
  344. exp1 = df['Close'].ewm(span=12, adjust=False).mean()
  345. exp2 = df['Close'].ewm(span=26, adjust=False).mean()
  346. df['MACD'] = exp1 - exp2
  347. df['MACD_signal'] = df['MACD'].ewm(span=9, adjust=False).mean()
  348. df['MACD_hist'] = df['MACD'] - df['MACD_signal']
  349. # KDJ
  350. low_9 = df['Low'].rolling(window=9).min()
  351. high_9 = df['High'].rolling(window=9).max()
  352. rsv = (df['Close'] - low_9) / (high_9 - low_9) * 100
  353. df['K'] = rsv.ewm(com=2, adjust=False).mean()
  354. df['D'] = df['K'].ewm(com=2, adjust=False).mean()
  355. df['J'] = 3 * df['K'] - 2 * df['D']
  356. # ATR
  357. high_low = df['High'] - df['Low']
  358. high_close = abs(df['High'] - df['Close'].shift())
  359. low_close = abs(df['Low'] - df['Close'].shift())
  360. true_range = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
  361. df['ATR'] = true_range.rolling(window=14).mean()
  362. df['ATR_Pct'] = df['ATR'] / df['Close']
  363. # 动量指标
  364. df['Momentum'] = df['Close'] / df['Close'].shift(4) - 1 # 2小时动量
  365. # 成交量变化
  366. df['Volume_MA'] = df['Volume'].rolling(window=12).mean()
  367. df['Volume_Ratio'] = df['Volume'] / df['Volume_MA']
  368. # 价格动量
  369. df['Price_Momentum'] = (df['Close'] - df['Close'].shift(6)) / df['Close'].shift(6)
  370. print("技术指标计算完成")
  371. return df
  372. # ==================== 翻转信号生成器 ====================
  373. class ReversalSignalGenerator:
  374. """日内翻转信号生成器"""
  375. def __init__(self):
  376. self.signal_count = 0
  377. def generate_reversal_signals(self, data: pd.DataFrame) -> pd.DataFrame:
  378. """生成日内翻转信号"""
  379. print("正在生成日内翻转信号...")
  380. signals = []
  381. df = data.copy()
  382. for i in range(24, len(df)): # 至少需要12小时(24个30分钟)的历史数据
  383. current_bar = df.iloc[i]
  384. current_time = df.index[i]
  385. # 跳过不适合交易的时间段(如午休时间等)
  386. # 如果是日线数据,不进行时间过滤
  387. if hasattr(current_time, 'hour'): # 有小时信息的30分钟数据
  388. hour = current_time.hour
  389. if hour < 9 or hour > 15: # 只在交易时间内
  390. continue
  391. # 生成信号
  392. signal = {
  393. 'DateTime': str(current_time), # 转换为字符串确保兼容性
  394. 'Open': current_bar['Open'],
  395. 'High': current_bar['High'],
  396. 'Low': current_bar['Low'],
  397. 'Close': current_bar['Close'],
  398. 'Volume': current_bar['Volume'],
  399. 'RSI': current_bar['RSI'],
  400. 'MACD': current_bar['MACD'],
  401. 'MACD_hist': current_bar['MACD_hist'],
  402. 'K': current_bar['K'],
  403. 'D': current_bar['D'],
  404. 'J': current_bar['J'],
  405. 'ATR_Pct': current_bar['ATR_Pct'],
  406. 'Volume_Ratio': current_bar['Volume_Ratio'],
  407. 'Price_Momentum': current_bar['Price_Momentum'],
  408. 'Close_Open_Pct': current_bar['Close_Open_Pct']
  409. }
  410. # 计算各种翻转信号
  411. reversal_score = 0
  412. reversal_signals = []
  413. # 1. RSI超卖翻转
  414. if current_bar['RSI'] < 30:
  415. reversal_score += 2
  416. reversal_signals.append("RSI超卖")
  417. elif current_bar['RSI'] < 35:
  418. reversal_score += 1
  419. reversal_signals.append("RSI偏弱")
  420. # 2. KDJ超卖翻转
  421. if current_bar['K'] < 20 and current_bar['D'] < 20:
  422. reversal_score += 2
  423. reversal_signals.append("KDJ超卖")
  424. elif current_bar['J'] < 0:
  425. reversal_score += 2
  426. reversal_signals.append("KDJ极端超卖")
  427. # 3. MACD金叉
  428. if current_bar['MACD_hist'] > 0 and df.iloc[i-1]['MACD_hist'] <= 0:
  429. reversal_score += 2
  430. reversal_signals.append("MACD金叉")
  431. elif current_bar['MACD_hist'] > df.iloc[i-1]['MACD_hist']:
  432. reversal_score += 1
  433. reversal_signals.append("MACD改善")
  434. # 4. 价格触及布林带下轨
  435. bb_width = current_bar['BB_width']
  436. if current_bar['Close'] <= current_bar['BB_lower'] * 1.005:
  437. reversal_score += 2
  438. reversal_signals.append("触及下轨")
  439. elif current_bar['Close'] <= current_bar['BB_lower'] * 1.01:
  440. reversal_score += 1
  441. reversal_signals.append("接近下轨")
  442. # 5. 连续下跌后的反转
  443. recent_returns = df.iloc[i-6:i]['Returns']
  444. if recent_returns.min() < -0.015: # 最近2小时内有超过1.5%的下跌
  445. consecutive_decline = sum(recent_returns < 0)
  446. if consecutive_decline >= 4: # 连续4个周期下跌
  447. reversal_score += 2
  448. reversal_signals.append("连续下跌反转")
  449. # 6. 价格动量反转
  450. if current_bar['Price_Momentum'] < -0.02: # 3小时下跌超过2%
  451. reversal_score += 1
  452. reversal_signals.append("动量超卖")
  453. # 7. 成交量配合
  454. if current_bar['Volume_Ratio'] > 1.2: # 放量
  455. reversal_score += 1
  456. reversal_signals.append("放量配合")
  457. # 8. 当日开盘价格关系
  458. daily_high = df[df.index.date == current_time.date()]['High'].max()
  459. daily_low = df[df.index.date == current_time.date()]['Low'].min()
  460. daily_range = daily_high - daily_low
  461. if daily_range > 0:
  462. position_in_day = (current_bar['Close'] - daily_low) / daily_range
  463. if position_in_day < 0.3: # 在当日低位区域
  464. reversal_score += 1
  465. reversal_signals.append("日内低位")
  466. # 设置信号
  467. signal['Reversal_Score'] = reversal_score
  468. signal['Reversal_Signals'] = ', '.join(reversal_signals) if reversal_signals else ''
  469. # 生成买入信号(阈值降低以增加交易频率)
  470. if reversal_score >= 4:
  471. signal['Signal'] = 1
  472. signal['Signal_Type'] = '做多翻转'
  473. self.signal_count += 1
  474. else:
  475. signal['Signal'] = 0
  476. signal['Signal_Type'] = ''
  477. signals.append(signal)
  478. signals_df = pd.DataFrame(signals)
  479. # 调试信息
  480. print(f"生成的信号数量: {len(signals_df)}")
  481. if len(signals_df) > 0:
  482. print(f"信号DataFrame的列: {signals_df.columns.tolist()}")
  483. signals_df.set_index('DateTime', inplace=True)
  484. else:
  485. print("警告:没有生成任何信号")
  486. print(f"信号生成完成,共产生{self.signal_count}个翻转信号")
  487. if len(signals_df) > 0:
  488. print(f"信号密度: {self.signal_count/len(signals_df)*100:.2f}%")
  489. return signals_df
  490. # ==================== 日内交易执行器 ====================
  491. class IntradayReversalExecutor:
  492. """日内翻转交易执行器"""
  493. def __init__(self, initial_capital=1000000):
  494. self.initial_capital = initial_capital
  495. self.params = {
  496. 'commission_rate': 0.0001, # 万分之一
  497. 'slippage_rate': 0.0, # 无滑点
  498. 'position_size_pct': 1.0, # 每次开仓100%仓位(满仓)
  499. 'stop_loss_pct': 0.008, # 0.8%止损
  500. 'take_profit_pct': 0.015, # 1.5%止盈
  501. 'max_hold_bars': 16, # 最多持有8小时(16个30分钟)
  502. 'min_signal_strength': 4 # 最小信号强度
  503. }
  504. def execute_intraday_trades(self, signals_df: pd.DataFrame) -> tuple:
  505. """执行日内翻转交易"""
  506. print("正在执行日内翻转交易...")
  507. df = signals_df.copy()
  508. # 初始化
  509. trades = []
  510. capital = self.initial_capital
  511. position = 0
  512. entry_price = 0
  513. entry_time = None
  514. holding_bars = 0
  515. entry_signals = ''
  516. # 添加资金列
  517. df = df.copy()
  518. df['capital'] = capital
  519. df['position'] = 0
  520. df['net_value'] = capital
  521. for i in range(len(df)):
  522. current_time = df.index[i]
  523. current_bar = df.iloc[i]
  524. price = current_bar['Close']
  525. # 更新当前净值
  526. if position > 0:
  527. current_value = capital + position * price
  528. df.iloc[i, df.columns.get_loc('net_value')] = current_value
  529. else:
  530. df.iloc[i, df.columns.get_loc('net_value')] = capital
  531. # 开仓逻辑
  532. if position == 0 and current_bar['Signal'] == 1:
  533. # 开仓
  534. position_size = int((capital * self.params['position_size_pct']) / price)
  535. if position_size > 0:
  536. cost = position_size * price * (1 + self.params['commission_rate'] + self.params['slippage_rate'])
  537. if cost <= capital:
  538. position = position_size
  539. entry_price = price
  540. entry_time = current_time
  541. entry_signals = current_bar.get('Reversal_Signals', '')
  542. holding_bars = 0
  543. capital -= cost
  544. df.iloc[i, df.columns.get_loc('position')] = position
  545. # 打印开仓详情
  546. print(f"\n{'='*60}")
  547. print(f"[OPEN] 开仓信号 #{len(trades) + 1}")
  548. print(f"{'='*60}")
  549. print(f"开仓时间: {entry_time}")
  550. print(f"开仓价格: {entry_price:.2f} 元")
  551. print(f"持仓数量: {position_size} 股")
  552. print(f"开仓市值: {position_size * entry_price:,.2f} 元")
  553. print(f"交易成本: {cost:,.2f} 元")
  554. print(f"剩余资金: {capital:,.2f} 元")
  555. print(f"入场信号: {entry_signals}")
  556. print(f"总资产: {capital + position_size * entry_price:,.2f} 元")
  557. # 平仓逻辑
  558. elif position > 0:
  559. holding_bars += 1
  560. # 计算止损止盈价格
  561. stop_loss = entry_price * (1 - self.params['stop_loss_pct'])
  562. take_profit = entry_price * (1 + self.params['take_profit_pct'])
  563. exit_signal = False
  564. exit_reason = ''
  565. exit_price = price
  566. # 止损
  567. if price <= stop_loss:
  568. exit_signal = True
  569. loss_pct = (entry_price - price) / entry_price * 100
  570. exit_reason = f"止损触发(价格{price:.2f}跌破止损线{stop_loss:.2f},亏损{loss_pct:.2f}%)"
  571. exit_price = stop_loss
  572. # 止盈
  573. elif price >= take_profit:
  574. exit_signal = True
  575. profit_pct = (price - entry_price) / entry_price * 100
  576. exit_reason = f"止盈触发(价格{price:.2f}突破止盈线{take_profit:.2f},盈利{profit_pct:.2f}%)"
  577. exit_price = take_profit
  578. # 最大持仓时间
  579. elif holding_bars >= self.params['max_hold_bars']:
  580. exit_signal = True
  581. current_pnl_pct = (price - entry_price) / entry_price * 100
  582. exit_reason = f"时间止损(持仓{holding_bars}周期达上限{self.params['max_hold_bars']}周期,当前盈亏{current_pnl_pct:+.2f}%)"
  583. # 翻转信号消失
  584. elif current_bar['RSI'] > 70: # RSI超买
  585. exit_signal = True
  586. current_pnl_pct = (price - entry_price) / entry_price * 100
  587. exit_reason = f"RSI超买平仓(RSI={current_bar['RSI']:.1f}超买,信号消失,当前盈亏{current_pnl_pct:+.2f}%)"
  588. # 执行平仓
  589. if exit_signal:
  590. # 计算盈亏 - 修复:包含开仓和平仓的总成本
  591. gross_pnl = (exit_price - entry_price) * position
  592. # 开仓成本(已经在开仓时扣除)
  593. open_cost = position * entry_price * (self.params['commission_rate'] + self.params['slippage_rate'])
  594. # 平仓成本
  595. close_revenue = position * exit_price
  596. close_cost = close_revenue * (self.params['commission_rate'] + self.params['slippage_rate'])
  597. # 净盈亏 = 价差收益 - 开仓成本 - 平仓成本
  598. pnl = gross_pnl - open_cost - close_cost
  599. # 更新资金
  600. capital += close_revenue - close_cost
  601. # 记录交易
  602. trade = {
  603. '买入时间': entry_time,
  604. '卖出时间': current_time,
  605. '买入价格': entry_price,
  606. '卖出价格': exit_price,
  607. '仓位': position,
  608. '盈亏金额': pnl,
  609. '盈亏百分比': (exit_price - entry_price) / entry_price * 100,
  610. '退出原因': exit_reason,
  611. '持仓周期数': holding_bars,
  612. '持仓小时数': holding_bars * 0.5,
  613. '入场信号': entry_signals,
  614. '卖出时资金': capital,
  615. '开仓市值': position * entry_price
  616. }
  617. trades.append(trade)
  618. # 打印平仓详情
  619. profit_ratio = (exit_price - entry_price) / entry_price * 100
  620. status = "[PROFIT]" if pnl > 0 else "[LOSS]"
  621. print(f"\n{'='*60}")
  622. print(f"{status} 平仓信号 #{len(trades)}")
  623. print(f"{'='*60}")
  624. print(f"平仓时间: {current_time}")
  625. print(f"平仓价格: {exit_price:.2f} 元")
  626. print(f"持仓时长: {holding_bars * 0.5:.1f} 小时 ({holding_bars} 个30分钟周期)")
  627. print(f"退出原因: {exit_reason}")
  628. print(f"{'-'*60}")
  629. print(f"盈亏金额: {pnl:+,.2f} 元")
  630. print(f"盈亏比例: {profit_ratio:+.2f}%")
  631. print(f"盈亏比: {abs(pnl):.2f}")
  632. print(f"{'-'*60}")
  633. print(f"当前资金: {capital:,.2f} 元")
  634. print(f"累计收益率: {(capital / self.initial_capital - 1) * 100:+.2f}%")
  635. print(f"胜率统计: {sum(1 for t in trades if t['盈亏金额'] > 0)}/{len(trades)} ({sum(1 for t in trades if t['盈亏金额'] > 0)/len(trades)*100:.1f}%)")
  636. print(f"{'='*60}")
  637. # 重置
  638. position = 0
  639. entry_price = 0
  640. entry_time = None
  641. holding_bars = 0
  642. # 更新资金
  643. df.iloc[i, df.columns.get_loc('capital')] = capital
  644. df.iloc[i, df.columns.get_loc('position')] = position
  645. # 强制平仓剩余持仓 - 修复:包含开仓和平仓的总成本
  646. if position > 0:
  647. final_price = df.iloc[-1]['Close']
  648. # 计算总盈亏
  649. gross_pnl = (final_price - entry_price) * position
  650. open_cost = position * entry_price * (self.params['commission_rate'] + self.params['slippage_rate'])
  651. close_revenue = position * final_price
  652. close_cost = close_revenue * (self.params['commission_rate'] + self.params['slippage_rate'])
  653. pnl = gross_pnl - open_cost - close_cost
  654. capital += close_revenue - close_cost
  655. trade = {
  656. '买入时间': entry_time,
  657. '卖出时间': df.index[-1],
  658. '买入价格': entry_price,
  659. '卖出价格': final_price,
  660. '仓位': position,
  661. '盈亏金额': pnl,
  662. '盈亏百分比': (final_price - entry_price) / entry_price * 100,
  663. '退出原因': f'强制平仓(回测结束,持仓{holding_bars}周期,最终价格{final_price:.2f},盈亏{(final_price - entry_price) / entry_price * 100:+.2f}%)',
  664. '持仓周期数': holding_bars,
  665. '持仓小时数': holding_bars * 0.5,
  666. '入场信号': entry_signals,
  667. '卖出时资金': capital,
  668. '开仓市值': position * entry_price
  669. }
  670. trades.append(trade)
  671. # 打印强制平仓详情
  672. profit_ratio = (final_price - entry_price) / entry_price * 100
  673. status = "[FORCE]" # 强制平仓
  674. print(f"\n{'='*60}")
  675. print(f"{status} 强制平仓信号 #{len(trades)}")
  676. print(f"{'='*60}")
  677. print(f"平仓时间: {df.index[-1]}")
  678. print(f"平仓价格: {final_price:.2f} 元")
  679. print(f"持仓时长: {holding_bars * 0.5:.1f} 小时 ({holding_bars} 个30分钟周期)")
  680. print(f"退出原因: 强制平仓(回测结束,持仓{holding_bars}周期,最终价格{final_price:.2f},盈亏{profit_ratio:+.2f}%)")
  681. print(f"{'-'*60}")
  682. print(f"盈亏金额: {pnl:+,.2f} 元")
  683. print(f"盈亏比例: {profit_ratio:+.2f}%")
  684. print(f"{'-'*60}")
  685. print(f"最终资金: {capital:,.2f} 元")
  686. print(f"累计收益率: {(capital / self.initial_capital - 1) * 100:+.2f}%")
  687. print(f"{'='*60}")
  688. trades_df = pd.DataFrame(trades)
  689. if len(trades_df) > 0:
  690. trades_df['买入时间'] = pd.to_datetime(trades_df['买入时间'])
  691. trades_df['卖出时间'] = pd.to_datetime(trades_df['卖出时间'])
  692. trades_df = trades_df.sort_values('买入时间')
  693. print(f"交易执行完成,共{len(trades_df)}笔交易")
  694. return df, trades_df
  695. # ==================== 验证分析模块 ====================
  696. def validate_intraday_results(results_df, trades_df, initial_capital):
  697. """验证日内交易结果"""
  698. print("\n" + "=" * 80)
  699. print("日内翻转交易结果验证")
  700. print("=" * 80)
  701. print(f"\n【基础数据验证】")
  702. final_capital = results_df['net_value'].iloc[-1]
  703. total_return = (final_capital - initial_capital) / initial_capital * 100
  704. print(f"初始资金: {initial_capital:,.2f}元")
  705. print(f"最终资金: {final_capital:,.2f}元")
  706. print(f"总收益率: {total_return:.2f}%")
  707. print(f"交易次数: {len(trades_df)}笔")
  708. if len(trades_df) > 0:
  709. print(f"\n【交易统计】")
  710. win_trades = trades_df[trades_df['盈亏金额'] > 0]
  711. lose_trades = trades_df[trades_df['盈亏金额'] < 0]
  712. print(f"盈利交易: {len(win_trades)}笔 ({len(win_trades)/len(trades_df)*100:.1f}%)")
  713. print(f"亏损交易: {len(lose_trades)}笔 ({len(lose_trades)/len(trades_df)*100:.1f}%)")
  714. print(f"平均持仓时间: {trades_df['持仓小时数'].mean():.1f}小时")
  715. print(f"平均收益率: {trades_df['盈亏百分比'].mean():.2f}%")
  716. # 按退出原因统计
  717. print(f"\n【退出原因统计】")
  718. for reason, count in trades_df['退出原因'].value_counts().items():
  719. percentage = count / len(trades_df) * 100
  720. reason_pnl = trades_df[trades_df['退出原因'] == reason]['盈亏金额'].sum()
  721. print(f" {reason}: {count}次 ({percentage:.1f}%) - 总盈亏: {reason_pnl:+,.2f}元")
  722. # ==================== 主程序 ====================
  723. def main():
  724. """主程序 - 运行30分钟日内翻转策略"""
  725. print("=" * 80)
  726. print("创业板50 30分钟日内翻转策略")
  727. print("=" * 80)
  728. # 加载配置文件
  729. config_manager = ConfigManager('config.json')
  730. # 从配置文件读取参数
  731. BACKTEST_START_DATE = config_manager.get('strategy', 'backtest_start_date', "2025-10-01")
  732. PREWARMP_DAYS = config_manager.get('strategy', 'prewamp_days', 30)
  733. INITIAL_CAPITAL = config_manager.get('strategy', 'initial_capital', 1000000)
  734. # 读取截止时间配置,支持"now"或具体日期
  735. backtest_end_config = config_manager.get('strategy', 'backtest_end_date', "now")
  736. if backtest_end_config.lower() == "now":
  737. BACKTEST_END_DATE = datetime.now().strftime('%Y-%m-%d')
  738. else:
  739. BACKTEST_END_DATE = backtest_end_config
  740. # 转换日期格式
  741. start_date = datetime.strptime(BACKTEST_START_DATE, "%Y-%m-%d")
  742. end_date = datetime.strptime(BACKTEST_END_DATE, "%Y-%m-%d").replace(hour=23, minute=59, second=59) # 包含指定日期全天数据
  743. # 计算数据获取开始时间(回测开始时间 - 预热期)
  744. data_start_date = start_date - timedelta(days=PREWARMP_DAYS)
  745. # 显示数据源配置
  746. use_local_file = config_manager.get('data_source', 'use_local_file', False)
  747. data_source_mode = "本地文件模式" if use_local_file else "在线获取模式"
  748. local_file_path = config_manager.get('data_source', 'local_file_path', '')
  749. print(f"\n策略参数:")
  750. print(f" 回测期间: {BACKTEST_START_DATE} 至 {BACKTEST_END_DATE}")
  751. print(f" 数据获取期间: {data_start_date.strftime('%Y-%m-%d')} 至 {BACKTEST_END_DATE}")
  752. print(f" 指标预热期: {PREWARMP_DAYS}天")
  753. print(f" K线周期: 30分钟")
  754. print(f" 初始资金: {INITIAL_CAPITAL:,}元")
  755. print(f" 标的指数: 创业板50 (399673)")
  756. print(f" 数据源: {data_source_mode}")
  757. if use_local_file:
  758. print(f" 本地文件路径: {local_file_path}")
  759. try:
  760. # Phase 1: 数据获取
  761. print(f"\n【Phase 1: 30分钟数据获取】")
  762. fetcher = IntradayDataFetcher(config_manager)
  763. # 获取包含预热期的完整数据
  764. full_data = fetcher.fetch_30min_data(start_date=data_start_date, end_date=end_date)
  765. full_data = fetcher.calculate_intraday_indicators(full_data)
  766. # 筛选回测期间的数据
  767. original_len = len(full_data)
  768. backtest_data = full_data[(full_data.index >= start_date) & (full_data.index <= end_date)].copy()
  769. print(f"筛选回测数据: {original_len} -> {len(backtest_data)} 条")
  770. print(f"回测数据范围: {backtest_data.index[0]} 到 {backtest_data.index[-1]}")
  771. # Phase 2: 信号生成
  772. print(f"\n【Phase 2: 翻转信号生成】")
  773. signal_gen = ReversalSignalGenerator()
  774. signals_df = signal_gen.generate_reversal_signals(backtest_data)
  775. # Phase 3: 交易执行
  776. print(f"\n【Phase 3: 日内交易执行】")
  777. executor = IntradayReversalExecutor(initial_capital=INITIAL_CAPITAL)
  778. results_df, trades_df = executor.execute_intraday_trades(signals_df)
  779. # Phase 4: 验证分析
  780. print(f"\n【Phase 4: 结果验证与分析】")
  781. validate_intraday_results(results_df, trades_df, INITIAL_CAPITAL)
  782. # Phase 5: 导出数据
  783. if len(trades_df) > 0:
  784. print(f"\n【Phase 5: 导出交易数据】")
  785. # 确保时间戳格式精确到分钟
  786. trades_df['买入时间'] = pd.to_datetime(trades_df['买入时间']).dt.strftime('%Y-%m-%d %H:%M:%S')
  787. trades_df['卖出时间'] = pd.to_datetime(trades_df['卖出时间']).dt.strftime('%Y-%m-%d %H:%M:%S')
  788. # 生成带时间戳的文件名
  789. timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
  790. output_file = f'cyb50_30min_intraday_reversal_trades_{timestamp}.csv'
  791. trades_df.to_csv(output_file, index=False, encoding='utf-8-sig')
  792. print(f"交易记录已保存到: {output_file}")
  793. print(f"时间戳格式: YYYY-MM-DD HH:MM:SS")
  794. # 策略总结
  795. print(f"\n" + "=" * 80)
  796. print("策略运行总结")
  797. print("=" * 80)
  798. if len(trades_df) > 0:
  799. final_capital = results_df['net_value'].iloc[-1]
  800. total_return = (final_capital - INITIAL_CAPITAL) / INITIAL_CAPITAL * 100
  801. print(f"初始资金: {INITIAL_CAPITAL:,.2f}元")
  802. print(f"最终资金: {final_capital:,.2f}元")
  803. print(f"总收益率: {total_return:.2f}%")
  804. print(f"交易次数: {len(trades_df)}笔")
  805. print(f"胜率: {(trades_df['盈亏金额'] > 0).sum() / len(trades_df) * 100:.1f}%")
  806. print(f"平均收益率: {trades_df['盈亏百分比'].mean():.2f}%")
  807. print(f"最大单笔盈利: {trades_df['盈亏金额'].max():+,.2f}元")
  808. print(f"最大单笔亏损: {trades_df['盈亏金额'].min():+,.2f}元")
  809. print(f"\n[SUCCESS] 策略运行成功!")
  810. else:
  811. print("未产生任何交易信号")
  812. except Exception as e:
  813. print(f"\n[ERROR] 策略运行出错: {str(e)}")
  814. import traceback
  815. traceback.print_exc()
  816. finally:
  817. print(f"\n" + "=" * 80)
  818. if __name__ == "__main__":
  819. main()