cyb50_30min_intraday_short.py 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014
  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 ShortSignalGenerator:
  374. """日内做空信号生成器"""
  375. def __init__(self):
  376. self.signal_count = 0
  377. def generate_short_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. short_score = 0
  412. short_signals = []
  413. # 1. RSI超买做空
  414. if current_bar['RSI'] > 70:
  415. short_score += 2
  416. short_signals.append("RSI超买")
  417. elif current_bar['RSI'] > 65:
  418. short_score += 1
  419. short_signals.append("RSI偏强")
  420. # 2. KDJ超买做空
  421. if current_bar['K'] > 80 and current_bar['D'] > 80:
  422. short_score += 2
  423. short_signals.append("KDJ超买")
  424. elif current_bar['J'] > 100:
  425. short_score += 2
  426. short_signals.append("KDJ极端超买")
  427. # 3. MACD死叉
  428. if current_bar['MACD_hist'] < 0 and df.iloc[i-1]['MACD_hist'] >= 0:
  429. short_score += 2
  430. short_signals.append("MACD死叉")
  431. elif current_bar['MACD_hist'] < df.iloc[i-1]['MACD_hist']:
  432. short_score += 1
  433. short_signals.append("MACD恶化")
  434. # 4. 价格触及布林带上轨
  435. bb_width = current_bar['BB_width']
  436. if current_bar['Close'] >= current_bar['BB_upper'] * 0.995:
  437. short_score += 2
  438. short_signals.append("触及上轨")
  439. elif current_bar['Close'] >= current_bar['BB_upper'] * 0.99:
  440. short_score += 1
  441. short_signals.append("接近上轨")
  442. # 5. 连续上涨后的反转
  443. recent_returns = df.iloc[i-6:i]['Returns']
  444. if recent_returns.max() > 0.015: # 最近2小时内有超过1.5%的上涨
  445. consecutive_rise = sum(recent_returns > 0)
  446. if consecutive_rise >= 4: # 连续4个周期上涨
  447. short_score += 2
  448. short_signals.append("连续上涨反转")
  449. # 6. 价格动量反转
  450. if current_bar['Price_Momentum'] > 0.02: # 3小时上涨超过2%
  451. short_score += 1
  452. short_signals.append("动量超买")
  453. # 7. 成交量配合
  454. if current_bar['Volume_Ratio'] > 1.2: # 放量
  455. short_score += 1
  456. short_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.7: # 在当日高位区域
  464. short_score += 1
  465. short_signals.append("日内高位")
  466. # 设置信号
  467. signal['Short_Score'] = short_score
  468. signal['Short_Signals'] = ', '.join(short_signals) if short_signals else ''
  469. # 生成做空信号(阈值降低以增加交易频率)
  470. if short_score >= 4:
  471. signal['Signal'] = -1 # -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 IntradayShortExecutor:
  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_short_trades(self, signals_df: pd.DataFrame) -> tuple:
  505. """执行日内做空交易"""
  506. print("正在执行日内做空交易...")
  507. df = signals_df.copy()
  508. # 初始化
  509. trades = []
  510. capital = self.initial_capital
  511. short_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['short_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 short_position < 0:
  527. # 做空盈亏 = (开仓价 - 当前价) * 持仓数量
  528. current_pnl = (entry_price - price) * abs(short_position)
  529. # 净值 = 剩余资金 + 保证金 + 浮动盈亏
  530. margin_held = abs(short_position) * entry_price
  531. current_value = capital + margin_held + current_pnl
  532. df.iloc[i, df.columns.get_loc('net_value')] = current_value
  533. else:
  534. df.iloc[i, df.columns.get_loc('net_value')] = capital
  535. # 开仓逻辑 - 做空开仓(卖出开仓)
  536. if short_position == 0 and current_bar['Signal'] == -1:
  537. # 做空开仓:卖出开仓
  538. position_value = capital * self.params['position_size_pct']
  539. position_size = int(position_value / price)
  540. if position_size > 0:
  541. # 做空开仓成本(保证金)
  542. margin_required = position_size * price # 保证金 = 开仓市值
  543. commission = position_size * price * (self.params['commission_rate'] + self.params['slippage_rate'])
  544. total_cost = margin_required + commission
  545. if total_cost <= capital:
  546. short_position = -position_size # 负数表示做空
  547. entry_price = price
  548. entry_time = current_time
  549. entry_signals = current_bar.get('Short_Signals', '')
  550. holding_bars = 0
  551. capital -= total_cost # 扣除保证金+手续费
  552. df.iloc[i, df.columns.get_loc('short_position')] = short_position
  553. # 打印开仓详情
  554. print(f"\n{'='*60}")
  555. print(f"[SHORT_OPEN] 做空开仓信号 #{len(trades) + 1}")
  556. print(f"{'='*60}")
  557. print(f"开仓时间: {entry_time}")
  558. print(f"开仓价格: {entry_price:.2f} 元")
  559. print(f"做空数量: {position_size} 股")
  560. print(f"开仓市值: {position_size * entry_price:,.2f} 元")
  561. print(f"保证金占用: {margin_required:,.2f} 元")
  562. print(f"手续费: {commission:,.2f} 元")
  563. print(f"总扣除: {total_cost:,.2f} 元")
  564. print(f"剩余资金: {capital:,.2f} 元")
  565. print(f"入场信号: {entry_signals}")
  566. print(f"总资产: {capital + margin_required:,.2f} 元")
  567. # 平仓逻辑 - 做空平仓(买入平仓)
  568. elif short_position < 0:
  569. holding_bars += 1
  570. # 计算止损止盈价格(做空逻辑相反)
  571. # 做空止损:价格上涨超过entry_price * (1 + stop_loss_pct)
  572. # 做空止盈:价格下跌超过entry_price * (1 - take_profit_pct)
  573. stop_loss_price = entry_price * (1 + self.params['stop_loss_pct']) # 价格上涨止损
  574. take_profit_price = entry_price * (1 - self.params['take_profit_pct']) # 价格下跌止盈
  575. exit_signal = False
  576. exit_reason = ''
  577. exit_price = price
  578. # 止损(价格上涨)
  579. if price >= stop_loss_price:
  580. exit_signal = True
  581. loss_pct = (price - entry_price) / entry_price * 100
  582. exit_reason = f"止损触发(价格{price:.2f}突破止损线{stop_loss_price:.2f},亏损{loss_pct:.2f}%)"
  583. exit_price = stop_loss_price
  584. # 止盈(价格下跌)
  585. elif price <= take_profit_price:
  586. exit_signal = True
  587. profit_pct = (entry_price - price) / entry_price * 100
  588. exit_reason = f"止盈触发(价格{price:.2f}跌破止盈线{take_profit_price:.2f},盈利{profit_pct:.2f}%)"
  589. exit_price = take_profit_price
  590. # 最大持仓时间
  591. elif holding_bars >= self.params['max_hold_bars']:
  592. exit_signal = True
  593. current_pnl_pct = (entry_price - price) / entry_price * 100
  594. exit_reason = f"时间止损(持仓{holding_bars}周期达上限{self.params['max_hold_bars']}周期,当前盈亏{current_pnl_pct:+.2f}%)"
  595. # 做空信号消失
  596. elif current_bar['RSI'] < 30: # RSI超卖(做空信号消失)
  597. exit_signal = True
  598. current_pnl_pct = (entry_price - price) / entry_price * 100
  599. exit_reason = f"RSI超卖平仓(RSI={current_bar['RSI']:.1f}超卖,信号消失,当前盈亏{current_pnl_pct:+.2f}%)"
  600. # 执行平仓
  601. if exit_signal:
  602. # 做空平仓:买入平仓
  603. # 计算盈亏 - 做空盈亏 = (开仓价 - 平仓价) * 持仓数量
  604. gross_pnl = (entry_price - exit_price) * abs(short_position)
  605. # 开仓手续费(已经在开仓时扣除)
  606. open_commission = abs(short_position) * entry_price * (self.params['commission_rate'] + self.params['slippage_rate'])
  607. # 平仓手续费
  608. close_commission = abs(short_position) * exit_price * (self.params['commission_rate'] + self.params['slippage_rate'])
  609. # 净盈亏 = 价差收益 - 平仓手续费(开仓手续费已扣除)
  610. net_pnl = gross_pnl - close_commission
  611. # 更新资金(返还保证金 + 净盈亏)
  612. margin_returned = abs(short_position) * entry_price # 返还开仓时的保证金
  613. capital += margin_returned + net_pnl
  614. # 记录交易
  615. trade = {
  616. '卖出开仓时间': entry_time,
  617. '买入平仓时间': current_time,
  618. '开仓价格': entry_price,
  619. '平仓价格': exit_price,
  620. '做空仓位': abs(short_position),
  621. '盈亏金额': net_pnl,
  622. '盈亏百分比': (entry_price - exit_price) / entry_price * 100, # 做空盈亏比例
  623. '退出原因': exit_reason,
  624. '持仓周期数': holding_bars,
  625. '持仓小时数': holding_bars * 0.5,
  626. '入场信号': entry_signals,
  627. '平仓时资金': capital,
  628. '开仓市值': abs(short_position) * entry_price,
  629. '保证金返还': margin_returned
  630. }
  631. trades.append(trade)
  632. # 打印平仓详情
  633. profit_ratio = (entry_price - exit_price) / entry_price * 100 # 做空盈亏比例
  634. status = "[PROFIT]" if net_pnl > 0 else "[LOSS]"
  635. print(f"\n{'='*60}")
  636. print(f"{status} 做空平仓信号 #{len(trades)}")
  637. print(f"{'='*60}")
  638. print(f"平仓时间: {current_time}")
  639. print(f"平仓价格: {exit_price:.2f} 元")
  640. print(f"持仓时长: {holding_bars * 0.5:.1f} 小时 ({holding_bars} 个30分钟周期)")
  641. print(f"退出原因: {exit_reason}")
  642. print(f"{'-'*60}")
  643. print(f"价差盈亏: {gross_pnl:+,.2f} 元")
  644. print(f"平仓手续费: {close_commission:,.2f} 元")
  645. print(f"净盈亏: {net_pnl:+,.2f} 元")
  646. print(f"盈亏比例: {profit_ratio:+.2f}%")
  647. print(f"保证金返还: {margin_returned:,.2f} 元")
  648. print(f"{'-'*60}")
  649. print(f"当前资金: {capital:,.2f} 元")
  650. print(f"累计收益率: {(capital / self.initial_capital - 1) * 100:+.2f}%")
  651. 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}%)")
  652. print(f"{'='*60}")
  653. # 重置
  654. short_position = 0
  655. entry_price = 0
  656. entry_time = None
  657. holding_bars = 0
  658. # 更新资金
  659. df.iloc[i, df.columns.get_loc('capital')] = capital
  660. df.iloc[i, df.columns.get_loc('short_position')] = short_position
  661. # 强制平仓剩余做空持仓
  662. if short_position < 0:
  663. final_price = df.iloc[-1]['Close']
  664. # 计算总盈亏
  665. gross_pnl = (entry_price - final_price) * abs(short_position)
  666. close_commission = abs(short_position) * final_price * (self.params['commission_rate'] + self.params['slippage_rate'])
  667. net_pnl = gross_pnl - close_commission
  668. # 更新资金(返还保证金 + 净盈亏)
  669. margin_returned = abs(short_position) * entry_price
  670. capital += margin_returned + net_pnl
  671. trade = {
  672. '卖出开仓时间': entry_time,
  673. '买入平仓时间': df.index[-1],
  674. '开仓价格': entry_price,
  675. '平仓价格': final_price,
  676. '做空仓位': abs(short_position),
  677. '盈亏金额': net_pnl,
  678. '盈亏百分比': (entry_price - final_price) / entry_price * 100,
  679. '退出原因': f'强制平仓(回测结束,持仓{holding_bars}周期,最终价格{final_price:.2f},盈亏{(entry_price - final_price) / entry_price * 100:+.2f}%)',
  680. '持仓周期数': holding_bars,
  681. '持仓小时数': holding_bars * 0.5,
  682. '入场信号': entry_signals,
  683. '平仓时资金': capital,
  684. '开仓市值': abs(short_position) * entry_price,
  685. '保证金返还': margin_returned
  686. }
  687. trades.append(trade)
  688. # 打印强制平仓详情
  689. profit_ratio = (entry_price - final_price) / entry_price * 100
  690. status = "[FORCE]" # 强制平仓
  691. print(f"\n{'='*60}")
  692. print(f"{status} 强制平仓信号 #{len(trades)}")
  693. print(f"{'='*60}")
  694. print(f"平仓时间: {df.index[-1]}")
  695. print(f"平仓价格: {final_price:.2f} 元")
  696. print(f"持仓时长: {holding_bars * 0.5:.1f} 小时 ({holding_bars} 个30分钟周期)")
  697. print(f"退出原因: 强制平仓(回测结束,持仓{holding_bars}周期,最终价格{final_price:.2f},盈亏{profit_ratio:+.2f}%)")
  698. print(f"{'-'*60}")
  699. print(f"价差盈亏: {gross_pnl:+,.2f} 元")
  700. print(f"平仓手续费: {close_commission:,.2f} 元")
  701. print(f"净盈亏: {net_pnl:+,.2f} 元")
  702. print(f"盈亏比例: {profit_ratio:+.2f}%")
  703. print(f"保证金返还: {margin_returned:,.2f} 元")
  704. print(f"{'-'*60}")
  705. print(f"最终资金: {capital:,.2f} 元")
  706. print(f"累计收益率: {(capital / self.initial_capital - 1) * 100:+.2f}%")
  707. print(f"{'='*60}")
  708. trades_df = pd.DataFrame(trades)
  709. if len(trades_df) > 0:
  710. trades_df['卖出开仓时间'] = pd.to_datetime(trades_df['卖出开仓时间'])
  711. trades_df['买入平仓时间'] = pd.to_datetime(trades_df['买入平仓时间'])
  712. trades_df = trades_df.sort_values('卖出开仓时间')
  713. print(f"做空交易执行完成,共{len(trades_df)}笔交易")
  714. return df, trades_df
  715. # ==================== 验证分析模块 ====================
  716. def validate_short_results(results_df, trades_df, initial_capital):
  717. """验证日内做空交易结果"""
  718. print("\n" + "=" * 80)
  719. print("日内做空交易结果验证")
  720. print("=" * 80)
  721. print(f"\n【基础数据验证】")
  722. final_capital = results_df['net_value'].iloc[-1]
  723. total_return = (final_capital - initial_capital) / initial_capital * 100
  724. print(f"初始资金: {initial_capital:,.2f}元")
  725. print(f"最终资金: {final_capital:,.2f}元")
  726. print(f"总收益率: {total_return:.2f}%")
  727. print(f"交易次数: {len(trades_df)}笔")
  728. if len(trades_df) > 0:
  729. print(f"\n【交易统计】")
  730. win_trades = trades_df[trades_df['盈亏金额'] > 0]
  731. lose_trades = trades_df[trades_df['盈亏金额'] < 0]
  732. print(f"盈利交易: {len(win_trades)}笔 ({len(win_trades)/len(trades_df)*100:.1f}%)")
  733. print(f"亏损交易: {len(lose_trades)}笔 ({len(lose_trades)/len(trades_df)*100:.1f}%)")
  734. print(f"平均持仓时间: {trades_df['持仓小时数'].mean():.1f}小时")
  735. print(f"平均收益率: {trades_df['盈亏百分比'].mean():.2f}%")
  736. # 按退出原因统计
  737. print(f"\n【退出原因统计】")
  738. for reason, count in trades_df['退出原因'].value_counts().items():
  739. percentage = count / len(trades_df) * 100
  740. reason_pnl = trades_df[trades_df['退出原因'] == reason]['盈亏金额'].sum()
  741. print(f" {reason}: {count}次 ({percentage:.1f}%) - 总盈亏: {reason_pnl:+,.2f}元")
  742. # ==================== 主程序 ====================
  743. def main():
  744. """主程序 - 运行30分钟日内做空策略"""
  745. print("=" * 80)
  746. print("创业板50 30分钟日内做空策略")
  747. print("=" * 80)
  748. # 加载配置文件
  749. config_manager = ConfigManager('config.json')
  750. # 从配置文件读取参数
  751. BACKTEST_START_DATE = config_manager.get('strategy', 'backtest_start_date', "2025-10-01")
  752. PREWARMP_DAYS = config_manager.get('strategy', 'prewamp_days', 30)
  753. INITIAL_CAPITAL = config_manager.get('strategy', 'initial_capital', 1000000)
  754. # 读取截止时间配置,支持"now"或具体日期
  755. backtest_end_config = config_manager.get('strategy', 'backtest_end_date', "now")
  756. if backtest_end_config.lower() == "now":
  757. BACKTEST_END_DATE = datetime.now().strftime('%Y-%m-%d')
  758. else:
  759. BACKTEST_END_DATE = backtest_end_config
  760. # 转换日期格式
  761. start_date = datetime.strptime(BACKTEST_START_DATE, "%Y-%m-%d")
  762. end_date = datetime.strptime(BACKTEST_END_DATE, "%Y-%m-%d").replace(hour=23, minute=59, second=59) # 包含指定日期全天数据
  763. # 计算数据获取开始时间(回测开始时间 - 预热期)
  764. data_start_date = start_date - timedelta(days=PREWARMP_DAYS)
  765. # 显示数据源配置
  766. use_local_file = config_manager.get('data_source', 'use_local_file', False)
  767. data_source_mode = "本地文件模式" if use_local_file else "在线获取模式"
  768. local_file_path = config_manager.get('data_source', 'local_file_path', '')
  769. print(f"\n策略参数:")
  770. print(f" 回测期间: {BACKTEST_START_DATE} 至 {BACKTEST_END_DATE}")
  771. print(f" 数据获取期间: {data_start_date.strftime('%Y-%m-%d')} 至 {BACKTEST_END_DATE}")
  772. print(f" 指标预热期: {PREWARMP_DAYS}天")
  773. print(f" K线周期: 30分钟")
  774. print(f" 初始资金: {INITIAL_CAPITAL:,}元")
  775. print(f" 标的指数: 创业板50 (399673)")
  776. print(f" 交易方向: 做空交易")
  777. print(f" 数据源: {data_source_mode}")
  778. if use_local_file:
  779. print(f" 本地文件路径: {local_file_path}")
  780. try:
  781. # Phase 1: 数据获取
  782. print(f"\n【Phase 1: 30分钟数据获取】")
  783. fetcher = IntradayDataFetcher(config_manager)
  784. # 获取包含预热期的完整数据
  785. full_data = fetcher.fetch_30min_data(start_date=data_start_date, end_date=end_date)
  786. full_data = fetcher.calculate_intraday_indicators(full_data)
  787. # 筛选回测期间的数据
  788. original_len = len(full_data)
  789. backtest_data = full_data[(full_data.index >= start_date) & (full_data.index <= end_date)].copy()
  790. print(f"筛选回测数据: {original_len} -> {len(backtest_data)} 条")
  791. print(f"回测数据范围: {backtest_data.index[0]} 到 {backtest_data.index[-1]}")
  792. # Phase 2: 信号生成
  793. print(f"\n【Phase 2: 做空信号生成】")
  794. signal_gen = ShortSignalGenerator()
  795. signals_df = signal_gen.generate_short_signals(backtest_data)
  796. # Phase 3: 交易执行
  797. print(f"\n【Phase 3: 日内做空交易执行】")
  798. executor = IntradayShortExecutor(initial_capital=INITIAL_CAPITAL)
  799. results_df, trades_df = executor.execute_short_trades(signals_df)
  800. # Phase 4: 验证分析
  801. print(f"\n【Phase 4: 结果验证与分析】")
  802. validate_short_results(results_df, trades_df, INITIAL_CAPITAL)
  803. # Phase 5: 导出数据
  804. if len(trades_df) > 0:
  805. print(f"\n【Phase 5: 导出交易数据】")
  806. # 确保时间戳格式精确到分钟
  807. trades_df['卖出开仓时间'] = pd.to_datetime(trades_df['卖出开仓时间']).dt.strftime('%Y-%m-%d %H:%M:%S')
  808. trades_df['买入平仓时间'] = pd.to_datetime(trades_df['买入平仓时间']).dt.strftime('%Y-%m-%d %H:%M:%S')
  809. # 生成带时间戳的文件名
  810. timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
  811. output_file = f'cyb50_30min_intraday_short_trades_{timestamp}.csv'
  812. trades_df.to_csv(output_file, index=False, encoding='utf-8-sig')
  813. print(f"做空交易记录已保存到: {output_file}")
  814. print(f"时间戳格式: YYYY-MM-DD HH:MM:SS")
  815. # 策略总结
  816. print(f"\n" + "=" * 80)
  817. print("做空策略运行总结")
  818. print("=" * 80)
  819. if len(trades_df) > 0:
  820. final_capital = results_df['net_value'].iloc[-1]
  821. total_return = (final_capital - INITIAL_CAPITAL) / INITIAL_CAPITAL * 100
  822. print(f"初始资金: {INITIAL_CAPITAL:,.2f}元")
  823. print(f"最终资金: {final_capital:,.2f}元")
  824. print(f"总收益率: {total_return:.2f}%")
  825. print(f"交易次数: {len(trades_df)}笔")
  826. print(f"胜率: {(trades_df['盈亏金额'] > 0).sum() / len(trades_df) * 100:.1f}%")
  827. print(f"平均收益率: {trades_df['盈亏百分比'].mean():.2f}%")
  828. print(f"最大单笔盈利: {trades_df['盈亏金额'].max():+,.2f}元")
  829. print(f"最大单笔亏损: {trades_df['盈亏金额'].min():+,.2f}元")
  830. print(f"\n[SUCCESS] 做空策略运行成功!")
  831. else:
  832. print("未产生任何做空信号")
  833. except Exception as e:
  834. print(f"\n[ERROR] 做空策略运行出错: {str(e)}")
  835. import traceback
  836. traceback.print_exc()
  837. finally:
  838. print(f"\n" + "=" * 80)
  839. if __name__ == "__main__":
  840. main()