cyb50_30min_long_only_t1.py 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002
  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. return filtered_data
  191. # 确保必需的列存在
  192. required_columns = ['Open', 'High', 'Low', 'Close', 'Volume']
  193. missing_columns = [col for col in required_columns if col not in filtered_data.columns]
  194. if missing_columns:
  195. raise ValueError(f"数据缺少必需的列: {missing_columns}")
  196. # 添加缺失的列
  197. if 'Amount' not in filtered_data.columns:
  198. filtered_data['Amount'] = 0
  199. # 计算基础指标
  200. if 'Returns' not in filtered_data.columns:
  201. filtered_data['Returns'] = filtered_data['Close'].pct_change()
  202. if 'High_Low_Pct' not in filtered_data.columns:
  203. filtered_data['High_Low_Pct'] = (filtered_data['High'] - filtered_data['Low']) / filtered_data['Close'].shift(1)
  204. if 'Close_Open_Pct' not in filtered_data.columns:
  205. filtered_data['Close_Open_Pct'] = (filtered_data['Close'] - filtered_data['Open']) / filtered_data['Open']
  206. # 处理缺失值
  207. filtered_data.ffill(inplace=True)
  208. filtered_data.dropna(inplace=True)
  209. print(f"本地文件数据处理成功: {len(filtered_data)}条")
  210. print(f"数据范围: {filtered_data.index[0]} 到 {filtered_data.index[-1]}")
  211. return filtered_data
  212. except Exception as e:
  213. print(f"处理数据框失败: {e}")
  214. raise
  215. def _fetch_online_data(self, start_date, end_date) -> pd.DataFrame:
  216. """在线获取30分钟K线数据 - 东方财富优先,新浪财经备用"""
  217. data = None
  218. # ===== 方法1: 东方财富数据源(主要数据源) =====
  219. try:
  220. print("[DATA_SOURCE_1] 正在使用东方财富30分钟K线接口...")
  221. data = ak.index_zh_a_hist_min_em(symbol=self.symbol, period="30")
  222. if not data.empty and len(data) >= 50:
  223. print(f"[SUCCESS] 东方财富获取到{len(data)}条30分钟数据")
  224. print(f"[TIME_RANGE] 数据范围: {data.index[0]} 到 {data.index[-1]}")
  225. else:
  226. print(f"[FAIL] 东方财富数据不足或为空")
  227. except Exception as e:
  228. print(f"[ERROR] 东方财富数据源失败: {e}")
  229. # ===== 方法2: 新浪财经数据源(备用数据源) =====
  230. if data is None or data.empty or len(data) < 50:
  231. try:
  232. print("[DATA_SOURCE_2] 正在使用新浪财经30分钟K线接口...")
  233. import requests
  234. import json
  235. import re
  236. symbol = "sz399673"
  237. 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"
  238. headers = {
  239. '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',
  240. 'Referer': 'https://finance.sina.com.cn/'
  241. }
  242. response = requests.get(url, headers=headers, timeout=15)
  243. response_text = response.text
  244. # 解析JSONP响应
  245. array_pattern = r'=([\[ ].+?\])'
  246. match = re.search(array_pattern, response_text)
  247. if match:
  248. json_str = match.group(1)
  249. else:
  250. json_start = response_text.find('[')
  251. json_end = response_text.rfind(']') + 1
  252. if json_start >= 0 and json_end > json_start:
  253. json_str = response_text[json_start:json_end]
  254. else:
  255. raise Exception("无法解析JSONP响应")
  256. data_dict = json.loads(json_str)
  257. if data_dict and isinstance(data_dict, list):
  258. data_list = []
  259. for item in data_dict:
  260. try:
  261. data_list.append({
  262. 'DateTime': item.get('day'),
  263. 'Open': float(item.get('open', 0)),
  264. 'High': float(item.get('high', 0)),
  265. 'Low': float(item.get('low', 0)),
  266. 'Close': float(item.get('close', 0)),
  267. 'Volume': float(item.get('volume', 0)),
  268. 'Amount': 0
  269. })
  270. except Exception:
  271. continue
  272. if data_list:
  273. data = pd.DataFrame(data_list)
  274. print(f"[SUCCESS] 新浪财经获取到{len(data)}条30分钟数据")
  275. else:
  276. print("[FAIL] 新浪财经数据解析失败")
  277. else:
  278. print("[FAIL] 新浪财经返回数据格式错误")
  279. except Exception as e:
  280. print(f"[ERROR] 新浪财经数据源失败: {e}")
  281. # ===== 数据验证和格式化 =====
  282. if data is None or data.empty:
  283. raise ValueError("[FATAL_ERROR] 所有数据源均无法获取数据")
  284. # 重命名列
  285. data.rename(columns={
  286. '时间': 'DateTime', '开盘': 'Open', '收盘': 'Close',
  287. '最高': 'High', '最低': 'Low', '成交量': 'Volume',
  288. '成交额': 'Amount', '振幅': 'Amplitude', '涨跌幅': 'Change_Pct',
  289. '涨跌额': 'Change_Amount', '换手率': 'Turnover'
  290. }, inplace=True)
  291. # 设置时间索引
  292. if 'DateTime' in data.columns:
  293. data['DateTime'] = pd.to_datetime(data['DateTime'])
  294. data.set_index('DateTime', inplace=True)
  295. else:
  296. raise ValueError("[ERROR] 数据中找不到时间列(DateTime)")
  297. data.sort_index(inplace=True)
  298. # 筛选时间范围
  299. filtered_data = data[(data.index >= start_date) & (data.index <= end_date)].copy()
  300. if filtered_data.empty:
  301. print(f"[WARNING] 筛选后数据为空,可用范围: {data.index[0]} 到 {data.index[-1]}")
  302. raise ValueError(f"[ERROR] 指定时间范围没有数据")
  303. # 计算基础指标
  304. filtered_data['Returns'] = filtered_data['Close'].pct_change()
  305. filtered_data['High_Low_Pct'] = (filtered_data['High'] - filtered_data['Low']) / filtered_data['Close'].shift(1)
  306. filtered_data['Close_Open_Pct'] = (filtered_data['Close'] - filtered_data['Open']) / filtered_data['Open']
  307. # 处理缺失值
  308. filtered_data.ffill(inplace=True)
  309. filtered_data.dropna(inplace=True)
  310. print(f"[FINAL_DATA] 成功处理{len(filtered_data)}条数据")
  311. print(f"[FINAL_RANGE] 最终数据范围: {filtered_data.index[0]} 到 {filtered_data.index[-1]}")
  312. return filtered_data
  313. def calculate_intraday_indicators(self, data: pd.DataFrame) -> pd.DataFrame:
  314. """计算30分钟技术指标"""
  315. print("正在计算30分钟技术指标...")
  316. df = data.copy()
  317. # 短期移动平均线
  318. df['MA6'] = df['Close'].rolling(window=6).mean()
  319. df['MA12'] = df['Close'].rolling(window=12).mean()
  320. df['MA24'] = df['Close'].rolling(window=24).mean()
  321. # RSI
  322. delta = df['Close'].diff()
  323. gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
  324. loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
  325. rs = gain / loss
  326. df['RSI'] = 100 - (100 / (1 + rs))
  327. # 布林带
  328. df['BB_middle'] = df['Close'].rolling(window=20).mean()
  329. bb_std = df['Close'].rolling(window=20).std()
  330. df['BB_upper'] = df['BB_middle'] + (bb_std * 2)
  331. df['BB_lower'] = df['BB_middle'] - (bb_std * 2)
  332. df['BB_width'] = (df['BB_upper'] - df['BB_lower']) / df['BB_middle']
  333. # MACD
  334. exp1 = df['Close'].ewm(span=12, adjust=False).mean()
  335. exp2 = df['Close'].ewm(span=26, adjust=False).mean()
  336. df['MACD'] = exp1 - exp2
  337. df['MACD_signal'] = df['MACD'].ewm(span=9, adjust=False).mean()
  338. df['MACD_hist'] = df['MACD'] - df['MACD_signal']
  339. # KDJ
  340. low_9 = df['Low'].rolling(window=9).min()
  341. high_9 = df['High'].rolling(window=9).max()
  342. rsv = (df['Close'] - low_9) / (high_9 - low_9) * 100
  343. df['K'] = rsv.ewm(com=2, adjust=False).mean()
  344. df['D'] = df['K'].ewm(com=2, adjust=False).mean()
  345. df['J'] = 3 * df['K'] - 2 * df['D']
  346. # ATR
  347. high_low = df['High'] - df['Low']
  348. high_close = abs(df['High'] - df['Close'].shift())
  349. low_close = abs(df['Low'] - df['Close'].shift())
  350. true_range = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
  351. df['ATR'] = true_range.rolling(window=14).mean()
  352. df['ATR_Pct'] = df['ATR'] / df['Close']
  353. # 动量指标
  354. df['Momentum'] = df['Close'] / df['Close'].shift(4) - 1
  355. # 成交量变化
  356. df['Volume_MA'] = df['Volume'].rolling(window=12).mean()
  357. df['Volume_Ratio'] = df['Volume'] / df['Volume_MA']
  358. # 价格动量
  359. df['Price_Momentum'] = (df['Close'] - df['Close'].shift(6)) / df['Close'].shift(6)
  360. print("技术指标计算完成")
  361. return df
  362. # ==================== 只做多信号生成器 ====================
  363. class LongOnlySignalGenerator:
  364. """只做多信号生成器(移除做空信号)"""
  365. def __init__(self):
  366. self.long_signal_count = 0
  367. def generate_long_only_signals(self, data: pd.DataFrame) -> pd.DataFrame:
  368. """生成只做多信号"""
  369. print("正在生成只做多信号...")
  370. signals = []
  371. df = data.copy()
  372. for i in range(24, len(df)):
  373. current_bar = df.iloc[i]
  374. current_time = df.index[i]
  375. # 跳过不适合交易的时间段
  376. if hasattr(current_time, 'hour'):
  377. hour = current_time.hour
  378. if hour < 9 or hour > 15:
  379. continue
  380. # 生成基础信号数据
  381. signal = {
  382. 'DateTime': str(current_time),
  383. 'Open': current_bar['Open'],
  384. 'High': current_bar['High'],
  385. 'Low': current_bar['Low'],
  386. 'Close': current_bar['Close'],
  387. 'Volume': current_bar['Volume'],
  388. 'RSI': current_bar['RSI'],
  389. 'MACD': current_bar['MACD'],
  390. 'MACD_hist': current_bar['MACD_hist'],
  391. 'K': current_bar['K'],
  392. 'D': current_bar['D'],
  393. 'J': current_bar['J'],
  394. 'ATR_Pct': current_bar['ATR_Pct'],
  395. 'Volume_Ratio': current_bar['Volume_Ratio'],
  396. 'Price_Momentum': current_bar['Price_Momentum'],
  397. 'Close_Open_Pct': current_bar['Close_Open_Pct']
  398. }
  399. # 计算做多信号强度
  400. long_score, long_signals = self._calculate_long_signals(current_bar, df, i)
  401. # 设置信号分数和描述
  402. signal['Long_Score'] = long_score
  403. signal['Long_Signals'] = ', '.join(long_signals) if long_signals else ''
  404. # 只做多信号判断
  405. final_signal = 0
  406. signal_type = ''
  407. if long_score >= 4:
  408. final_signal = 1
  409. signal_type = '做多翻转'
  410. self.long_signal_count += 1
  411. signal['Signal'] = final_signal
  412. signal['Signal_Type'] = signal_type
  413. signals.append(signal)
  414. signals_df = pd.DataFrame(signals)
  415. if len(signals_df) > 0:
  416. signals_df.set_index('DateTime', inplace=True)
  417. else:
  418. print("警告:没有生成任何信号")
  419. print(f"只做多信号生成完成: {self.long_signal_count}个")
  420. if len(signals_df) > 0:
  421. print(f"信号密度: {self.long_signal_count/len(signals_df)*100:.2f}%")
  422. return signals_df
  423. def _calculate_long_signals(self, current_bar, df, i):
  424. """计算做多信号强度"""
  425. long_score = 0
  426. long_signals = []
  427. # 1. RSI超卖做多
  428. if current_bar['RSI'] < 30:
  429. long_score += 2
  430. long_signals.append("RSI超卖")
  431. elif current_bar['RSI'] < 35:
  432. long_score += 1
  433. long_signals.append("RSI偏弱")
  434. # 2. KDJ超卖做多
  435. if current_bar['K'] < 20 and current_bar['D'] < 20:
  436. long_score += 2
  437. long_signals.append("KDJ超卖")
  438. elif current_bar['J'] < 0:
  439. long_score += 1
  440. long_signals.append("KDJ极端超卖")
  441. # 3. MACD金叉
  442. if current_bar['MACD_hist'] > 0 and df.iloc[i-1]['MACD_hist'] <= 0:
  443. long_score += 2
  444. long_signals.append("MACD金叉")
  445. elif current_bar['MACD_hist'] > df.iloc[i-1]['MACD_hist']:
  446. long_score += 1
  447. long_signals.append("MACD改善")
  448. # 4. 价格触及布林带下轨
  449. if current_bar['Close'] <= current_bar['BB_lower'] * 1.005:
  450. long_score += 2
  451. long_signals.append("触及下轨")
  452. elif current_bar['Close'] <= current_bar['BB_lower'] * 1.01:
  453. long_score += 1
  454. long_signals.append("接近下轨")
  455. # 5. 连续下跌后的反转
  456. recent_returns = df.iloc[i-6:i]['Returns']
  457. if recent_returns.min() < -0.015:
  458. consecutive_decline = sum(recent_returns < 0)
  459. if consecutive_decline >= 4:
  460. long_score += 2
  461. long_signals.append("连续下跌反转")
  462. # 6. 价格动量反转
  463. if current_bar['Price_Momentum'] < -0.02:
  464. long_score += 1
  465. long_signals.append("动量超卖")
  466. # 7. 成交量配合
  467. if current_bar['Volume_Ratio'] > 1.2:
  468. long_score += 1
  469. long_signals.append("放量配合")
  470. # 8. 当日开盘价格关系
  471. try:
  472. daily_high = df[df.index.date == df.index[i].date()]['High'].max()
  473. daily_low = df[df.index.date == df.index[i].date()]['Low'].min()
  474. daily_range = daily_high - daily_low
  475. if daily_range > 0:
  476. position_in_day = (current_bar['Close'] - daily_low) / daily_range
  477. if position_in_day < 0.3:
  478. long_score += 1
  479. long_signals.append("日内低位")
  480. except:
  481. pass
  482. # MA趋势过滤
  483. if current_bar['MA6'] < current_bar['MA12'] < current_bar['MA24']:
  484. long_score -= 1
  485. long_signals.append("MA下降趋势惩罚")
  486. elif current_bar['MA6'] > current_bar['MA12']:
  487. long_score += 1
  488. long_signals.append("MA短期上行")
  489. return long_score, long_signals
  490. # ==================== T+1只做多交易执行器 ====================
  491. class LongOnlyT1Executor:
  492. """只做多T+1交易执行器(当天买入,第二天才能卖出;卖出后当天可再买入)"""
  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,
  499. 'stop_loss_pct': 0.008,
  500. 'take_profit_pct': 0.02,
  501. 'max_hold_bars': 16,
  502. 'min_signal_strength': 4
  503. }
  504. def execute_long_only_t1_trades(self, signals_df: pd.DataFrame) -> tuple:
  505. """执行只做多T+1交易"""
  506. print("正在执行只做多T+1交易...")
  507. df = signals_df.copy()
  508. # 初始化
  509. trades = []
  510. capital = self.initial_capital
  511. # 持仓状态
  512. long_position = 0
  513. long_entry_price = 0
  514. long_entry_time = None
  515. long_entry_date = None # T+1限制:记录买入日期
  516. long_holding_bars = 0
  517. long_entry_signals = ''
  518. # 添加资金列
  519. df = df.copy()
  520. df['capital'] = float(capital)
  521. df['long_position'] = 0
  522. df['net_value'] = float(capital)
  523. for i in range(len(df)):
  524. current_time = pd.to_datetime(df.index[i])
  525. current_bar = df.iloc[i]
  526. price = current_bar['Close']
  527. current_date = current_time.date()
  528. # 更新当前净值
  529. current_value = capital
  530. if long_position > 0:
  531. current_value += long_position * price
  532. df.iloc[i, df.columns.get_loc('net_value')] = current_value
  533. # 开仓逻辑 - 只在无持仓时开仓(卖出后当天可再买入)
  534. if long_position == 0:
  535. # 做多开仓
  536. if current_bar['Signal'] == 1:
  537. position_size = int((capital * self.params['position_size_pct']) / price)
  538. if position_size > 0:
  539. cost = position_size * price * (1 + self.params['commission_rate'] + self.params['slippage_rate'])
  540. if cost <= capital:
  541. long_position = position_size
  542. long_entry_price = price
  543. long_entry_time = current_time
  544. long_entry_date = current_date # 记录买入日期用于T+1判断
  545. long_entry_signals = current_bar.get('Long_Signals', '')
  546. long_holding_bars = 0
  547. capital -= cost
  548. long_stop_loss_price = long_entry_price * (1 - self.params['stop_loss_pct'])
  549. long_take_profit_price = long_entry_price * (1 + self.params['take_profit_pct'])
  550. df.iloc[i, df.columns.get_loc('long_position')] = long_position
  551. print(f"\n{'='*60}")
  552. print(f"[LONG_OPEN] 做多开仓信号 #{len(trades) + 1}")
  553. print(f"{'='*60}")
  554. print(f"开仓时间: {long_entry_time}")
  555. print(f"开仓日期: {long_entry_date} (T+1: 最早{long_entry_date + timedelta(days=1)}后可卖)")
  556. print(f"开仓价格: {long_entry_price:.2f} 元")
  557. print(f"预计止损: {long_stop_loss_price:.2f} 元 (-{self.params['stop_loss_pct']*100:.1f}%)")
  558. print(f"预计止盈: {long_take_profit_price:.2f} 元 (+{self.params['take_profit_pct']*100:.1f}%)")
  559. print(f"持仓数量: {position_size} 股")
  560. print(f"开仓市值: {position_size * long_entry_price:,.2f} 元")
  561. print(f"剩余资金: {capital:,.2f} 元")
  562. print(f"入场信号: {long_entry_signals}")
  563. # 平仓逻辑 - 做多平仓(T+1限制:买入当天不能卖出)
  564. elif long_position > 0:
  565. long_holding_bars += 1
  566. # T+1检查:买入当天不能卖出
  567. is_t1_restricted = (current_date == long_entry_date)
  568. stop_loss = long_entry_price * (1 - self.params['stop_loss_pct'])
  569. take_profit = long_entry_price * (1 + self.params['take_profit_pct'])
  570. exit_signal = False
  571. exit_reason = ''
  572. exit_price = price
  573. if is_t1_restricted:
  574. # T+1限制日:记录但不能卖出
  575. if price <= stop_loss:
  576. print(f"[T+1限制] {current_time} 价格{price:.2f}触及止损线{stop_loss:.2f},当天买入不能卖出,继续持有")
  577. elif price >= take_profit:
  578. print(f"[T+1限制] {current_time} 价格{price:.2f}触及止盈线{take_profit:.2f},当天买入不能卖出,继续持有")
  579. else:
  580. # T+1已过,可以正常平仓
  581. if price <= stop_loss:
  582. exit_signal = True
  583. loss_pct = (long_entry_price - stop_loss) / long_entry_price * 100
  584. exit_reason = f"做多止损触发(价格{price:.2f}跌破止损线{stop_loss:.2f},亏损{loss_pct:.2f}%)"
  585. exit_price = price
  586. elif price >= take_profit:
  587. exit_signal = True
  588. profit_pct = (price - long_entry_price) / long_entry_price * 100
  589. exit_reason = f"做多止盈触发(价格{price:.2f}突破止盈线{take_profit:.2f},盈利{profit_pct:.2f}%)"
  590. exit_price = price
  591. elif long_holding_bars >= self.params['max_hold_bars']:
  592. exit_signal = True
  593. current_pnl_pct = (price - long_entry_price) / long_entry_price * 100
  594. exit_reason = f"做多时间止损(持仓{long_holding_bars}周期达上限,当前盈亏{current_pnl_pct:+.2f}%)"
  595. elif current_bar['RSI'] > 70:
  596. exit_signal = True
  597. current_pnl_pct = (price - long_entry_price) / long_entry_price * 100
  598. exit_reason = f"做多RSI超买平仓(RSI={current_bar['RSI']:.1f}超买,信号消失,当前盈亏{current_pnl_pct:+.2f}%)"
  599. # 执行平仓
  600. if exit_signal:
  601. gross_pnl = (exit_price - long_entry_price) * long_position
  602. open_cost = long_position * long_entry_price * (self.params['commission_rate'] + self.params['slippage_rate'])
  603. close_revenue = long_position * exit_price
  604. close_cost = close_revenue * (self.params['commission_rate'] + self.params['slippage_rate'])
  605. pnl = gross_pnl - open_cost - close_cost
  606. capital += close_revenue - close_cost
  607. trade = {
  608. '交易方向': '做多',
  609. '开仓时间': long_entry_time,
  610. '平仓时间': current_time,
  611. '开仓价格': long_entry_price,
  612. '平仓价格': exit_price,
  613. '仓位': long_position,
  614. '盈亏金额': pnl,
  615. '盈亏百分比': (exit_price - long_entry_price) / long_entry_price * 100,
  616. '退出原因': exit_reason,
  617. '持仓周期数': long_holding_bars,
  618. '持仓小时数': long_holding_bars * 0.5,
  619. 'T+1限制': '是' if is_t1_restricted else '否',
  620. '入场信号': long_entry_signals,
  621. '平仓时资金': capital,
  622. '开仓市值': long_position * long_entry_price,
  623. '预计止损价格': long_stop_loss_price,
  624. '预计止盈价格': long_take_profit_price
  625. }
  626. trades.append(trade)
  627. profit_ratio = (exit_price - long_entry_price) / long_entry_price * 100
  628. status = "[PROFIT]" if pnl > 0 else "[LOSS]"
  629. print(f"\n{'='*60}")
  630. print(f"{status} [LONG_CLOSE] 做多平仓信号 #{len(trades)}")
  631. print(f"{'='*60}")
  632. print(f"平仓时间: {current_time}")
  633. print(f"持仓时长: {long_holding_bars * 0.5:.1f}小时")
  634. print(f"退出原因: {exit_reason}")
  635. print(f"盈亏金额: {pnl:+,.2f}元 ({profit_ratio:+.2f}%)")
  636. print(f"当前资金: {capital:,.2f}元")
  637. print(f"累计收益: {(capital/self.initial_capital-1)*100:+.2f}%")
  638. # 关键:平仓后重置所有持仓状态,允许当天再买入
  639. long_position = 0
  640. long_entry_price = 0
  641. long_entry_time = None
  642. long_entry_date = None # 重置T+1日期限制
  643. long_holding_bars = 0
  644. # 更新资金和持仓状态
  645. df.iloc[i, df.columns.get_loc('capital')] = capital
  646. df.iloc[i, df.columns.get_loc('long_position')] = long_position
  647. # 强制平仓剩余持仓
  648. if long_position > 0:
  649. final_price = df.iloc[-1]['Close']
  650. final_time = pd.to_datetime(df.index[-1])
  651. gross_pnl = (final_price - long_entry_price) * long_position
  652. open_cost = long_position * long_entry_price * (self.params['commission_rate'] + self.params['slippage_rate'])
  653. close_revenue = long_position * final_price
  654. close_cost = close_revenue * (self.params['commission_rate'] + self.params['slippage_rate'])
  655. pnl = gross_pnl - open_cost - close_cost
  656. capital += close_revenue - close_cost
  657. trade = {
  658. '交易方向': '做多',
  659. '开仓时间': long_entry_time,
  660. '平仓时间': df.index[-1],
  661. '开仓价格': long_entry_price,
  662. '平仓价格': final_price,
  663. '仓位': long_position,
  664. '盈亏金额': pnl,
  665. '盈亏百分比': (final_price - long_entry_price) / long_entry_price * 100,
  666. '退出原因': f'做多强制平仓(回测结束,持仓{long_holding_bars}周期,盈亏{(final_price-long_entry_price)/long_entry_price*100:+.2f}%)',
  667. '持仓周期数': long_holding_bars,
  668. '持仓小时数': long_holding_bars * 0.5,
  669. 'T+1限制': '否',
  670. '入场信号': long_entry_signals,
  671. '平仓时资金': capital,
  672. '开仓市值': long_position * long_entry_price,
  673. '预计止损价格': long_stop_loss_price,
  674. '预计止盈价格': long_take_profit_price
  675. }
  676. trades.append(trade)
  677. print(f"\n{'='*60}")
  678. print(f"[FORCE] 做多强制平仓 #{len(trades)}")
  679. print(f"盈亏金额: {pnl:+,.2f}元")
  680. trades_df = pd.DataFrame(trades)
  681. if len(trades_df) > 0:
  682. for col in trades_df.columns:
  683. if '时间' in col:
  684. trades_df[col] = pd.to_datetime(trades_df[col])
  685. trades_df = trades_df.sort_values('开仓时间')
  686. print(f"\n只做多T+1交易执行完成,共{len(trades_df)}笔交易")
  687. return df, trades_df
  688. # ==================== 验证分析模块 ====================
  689. def validate_long_only_t1_results(results_df, trades_df, initial_capital):
  690. """验证只做多T+1交易结果"""
  691. print("\n" + "=" * 80)
  692. print("创业板50 30分钟只做多T+1交易结果验证")
  693. print("=" * 80)
  694. print(f"\n【基础数据验证】")
  695. final_capital = results_df['net_value'].iloc[-1]
  696. total_return = (final_capital - initial_capital) / initial_capital * 100
  697. print(f"初始资金: {initial_capital:,.2f}元")
  698. print(f"最终资金: {final_capital:,.2f}元")
  699. print(f"总收益率: {total_return:.2f}%")
  700. print(f"交易次数: {len(trades_df)}笔")
  701. if len(trades_df) > 0:
  702. long_trades = trades_df
  703. long_win_trades = long_trades[long_trades['盈亏金额'] > 0]
  704. long_lose_trades = long_trades[long_trades['盈亏金额'] < 0]
  705. long_total_pnl = long_trades['盈亏金额'].sum()
  706. print(f"\n【只做多交易统计】")
  707. print(f"盈利交易: {len(long_win_trades)}笔 ({len(long_win_trades)/len(long_trades)*100:.1f}%)")
  708. print(f"亏损交易: {len(long_lose_trades)}笔 ({len(long_lose_trades)/len(long_trades)*100:.1f}%)")
  709. print(f"平均持仓时间: {long_trades['持仓小时数'].mean():.1f}小时")
  710. print(f"平均收益率: {long_trades['盈亏百分比'].mean():.2f}%")
  711. print(f"总盈亏: {long_total_pnl:+,.2f}元")
  712. # T+1影响统计
  713. if 'T+1限制' in trades_df.columns:
  714. t1_affected = trades_df[trades_df['T+1限制'] == '是']
  715. print(f"\n【T+1限制影响】")
  716. print(f"受T+1限制的交易: {len(t1_affected)}笔 ({len(t1_affected)/len(trades_df)*100:.1f}%)")
  717. if len(t1_affected) > 0:
  718. print(f"T+1限制交易盈亏: {t1_affected['盈亏金额'].sum():+,.2f}元")
  719. # 按退出原因统计
  720. print(f"\n【退出原因统计】")
  721. for reason, count in trades_df['退出原因'].value_counts().items():
  722. percentage = count / len(trades_df) * 100
  723. reason_pnl = trades_df[trades_df['退出原因'] == reason]['盈亏金额'].sum()
  724. print(f" {reason}: {count}次 ({percentage:.1f}%) - 总盈亏: {reason_pnl:+,.2f}元")
  725. # ==================== 主程序 ====================
  726. def main():
  727. """主程序 - 运行30分钟只做多T+1策略"""
  728. print("=" * 80)
  729. print("创业板50 30分钟只做多T+1策略")
  730. print("=" * 80)
  731. # 加载配置文件
  732. config_manager = ConfigManager('config.json')
  733. # 自动加载best_parameters.json覆盖默认参数
  734. best_params_file = 'best_parameters.json'
  735. if os.path.exists(best_params_file):
  736. try:
  737. with open(best_params_file, 'r', encoding='utf-8') as f:
  738. best_data = json.load(f)
  739. best_params = best_data.get('best_params', {})
  740. strategy_keys = ['position_size_pct', 'stop_loss_pct', 'take_profit_pct', 'max_hold_bars']
  741. for key in strategy_keys:
  742. if key in best_params:
  743. config_manager.config.setdefault('strategy', {})[key] = best_params[key]
  744. print(f"已加载优化参数: {best_params_file}")
  745. except Exception as e:
  746. print(f"加载优化参数失败: {e}")
  747. BACKTEST_START_DATE = config_manager.get('strategy', 'backtest_start_date', "2025-10-01")
  748. PREWARMP_DAYS = config_manager.get('strategy', 'prewamp_days', 30)
  749. INITIAL_CAPITAL = config_manager.get('strategy', 'initial_capital', 1000000)
  750. backtest_end_config = config_manager.get('strategy', 'backtest_end_date', "now")
  751. if backtest_end_config.lower() == "now":
  752. BACKTEST_END_DATE = datetime.now().strftime('%Y-%m-%d')
  753. else:
  754. BACKTEST_END_DATE = backtest_end_config
  755. start_date = datetime.strptime(BACKTEST_START_DATE, "%Y-%m-%d")
  756. end_date = datetime.strptime(BACKTEST_END_DATE, "%Y-%m-%d").replace(hour=23, minute=59, second=59)
  757. data_start_date = start_date - timedelta(days=PREWARMP_DAYS)
  758. use_local_file = config_manager.get('data_source', 'use_local_file', False)
  759. data_source_mode = "本地文件模式" if use_local_file else "在线获取模式"
  760. local_file_path = config_manager.get('data_source', 'local_file_path', '')
  761. print(f"\n策略参数:")
  762. print(f" 回测期间: {BACKTEST_START_DATE} 至 {BACKTEST_END_DATE}")
  763. print(f" 指标预热期: {PREWARMP_DAYS}天")
  764. print(f" 初始资金: {INITIAL_CAPITAL:,}元")
  765. print(f" 交易方向: 只做多(T+1: 买入当天不能卖出,卖出后当天可再买入)")
  766. print(f" 数据源: {data_source_mode}")
  767. if use_local_file:
  768. print(f" 本地文件路径: {local_file_path}")
  769. try:
  770. # Phase 1: 数据获取
  771. print(f"\n【Phase 1: 30分钟数据获取】")
  772. fetcher = IntradayDataFetcher(config_manager)
  773. full_data = fetcher.fetch_30min_data(start_date=data_start_date, end_date=end_date)
  774. full_data = fetcher.calculate_intraday_indicators(full_data)
  775. original_len = len(full_data)
  776. backtest_data = full_data[(full_data.index >= start_date) & (full_data.index <= end_date)].copy()
  777. print(f"筛选回测数据: {original_len} -> {len(backtest_data)} 条")
  778. print(f"回测数据范围: {backtest_data.index[0]} 到 {backtest_data.index[-1]}")
  779. # Phase 2: 信号生成
  780. print(f"\n【Phase 2: 只做多信号生成】")
  781. signal_gen = LongOnlySignalGenerator()
  782. signals_df = signal_gen.generate_long_only_signals(backtest_data)
  783. # Phase 3: 交易执行
  784. print(f"\n【Phase 3: 只做多T+1交易执行】")
  785. executor = LongOnlyT1Executor(initial_capital=INITIAL_CAPITAL)
  786. executor.params['stop_loss_pct'] = config_manager.get('strategy', 'stop_loss_pct', 0.008)
  787. executor.params['take_profit_pct'] = config_manager.get('strategy', 'take_profit_pct', 0.02)
  788. executor.params['max_hold_bars'] = config_manager.get('strategy', 'max_hold_bars', 16)
  789. executor.params['position_size_pct'] = config_manager.get('strategy', 'position_size_pct', 1.0)
  790. results_df, trades_df = executor.execute_long_only_t1_trades(signals_df)
  791. # Phase 4: 验证分析
  792. print(f"\n【Phase 4: 结果验证与分析】")
  793. validate_long_only_t1_results(results_df, trades_df, INITIAL_CAPITAL)
  794. # Phase 5: 导出数据
  795. if len(trades_df) > 0:
  796. print(f"\n【Phase 5: 导出交易数据】")
  797. for col in trades_df.columns:
  798. if '时间' in col:
  799. trades_df[col] = pd.to_datetime(trades_df[col]).dt.strftime('%Y-%m-%d %H:%M:%S')
  800. timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
  801. output_file = f'cyb50_30min_long_only_t1_trades_{timestamp}.csv'
  802. trades_df.to_csv(output_file, index=False, encoding='utf-8-sig')
  803. print(f"只做多T+1交易记录已保存到: {output_file}")
  804. # 策略总结
  805. print(f"\n" + "=" * 80)
  806. print("只做多T+1策略运行总结")
  807. print("=" * 80)
  808. if len(trades_df) > 0:
  809. final_capital = results_df['net_value'].iloc[-1]
  810. total_return = (final_capital - INITIAL_CAPITAL) / INITIAL_CAPITAL * 100
  811. print(f"初始资金: {INITIAL_CAPITAL:,.2f}元")
  812. print(f"最终资金: {final_capital:,.2f}元")
  813. print(f"总收益率: {total_return:.2f}%")
  814. print(f"总交易次数: {len(trades_df)}笔")
  815. print(f"整体胜率: {(trades_df['盈亏金额'] > 0).sum() / len(trades_df) * 100:.1f}%")
  816. print(f"\n[SUCCESS] 只做多T+1策略运行成功!")
  817. else:
  818. print("未产生任何交易信号")
  819. except Exception as e:
  820. print(f"\n[ERROR] 只做多T+1策略运行出错: {str(e)}")
  821. import traceback
  822. traceback.print_exc()
  823. finally:
  824. print(f"\n" + "=" * 80)
  825. if __name__ == "__main__":
  826. main()