cyb50_30min_long_only_t1.py 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074
  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 = filtered_data.ffill()
  209. filtered_data = filtered_data.dropna()
  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 = filtered_data.ffill()
  319. filtered_data = filtered_data.dropna()
  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 LongOnlySignalGenerator:
  374. """只做多信号生成器(移除做空信号)"""
  375. def __init__(self):
  376. self.long_signal_count = 0
  377. self.total_signal_count = 0
  378. def generate_long_only_signals(self, data: pd.DataFrame) -> pd.DataFrame:
  379. """生成只做多信号"""
  380. print("正在生成只做多信号...")
  381. signals = []
  382. df = data.copy()
  383. for i in range(24, len(df)): # 至少需要12小时(24个30分钟)的历史数据
  384. current_bar = df.iloc[i]
  385. current_time = df.index[i]
  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. long_score, long_signals = self._calculate_long_signals(current_bar, df, i)
  412. # 设置信号分数和描述
  413. signal['Long_Score'] = long_score
  414. signal['Long_Signals'] = ', '.join(long_signals) if long_signals else ''
  415. # 决定最终信号方向和强度
  416. final_signal = 0
  417. signal_type = ''
  418. # 只做多信号判断
  419. if long_score >= 4:
  420. final_signal = 1
  421. signal_type = '做多翻转'
  422. self.long_signal_count += 1
  423. self.total_signal_count = self.long_signal_count
  424. signal['Signal'] = final_signal
  425. signal['Signal_Type'] = signal_type
  426. signals.append(signal)
  427. signals_df = pd.DataFrame(signals)
  428. if len(signals_df) > 0:
  429. signals_df.set_index('DateTime', inplace=True)
  430. else:
  431. print("警告:没有生成任何信号")
  432. print(f"只做多信号生成完成")
  433. print(f"做多信号: {self.long_signal_count}个")
  434. if len(signals_df) > 0:
  435. print(f"信号密度: {self.total_signal_count/len(signals_df)*100:.2f}%")
  436. return signals_df
  437. def _calculate_long_signals(self, current_bar, df, i):
  438. """计算做多信号强度"""
  439. long_score = 0
  440. long_signals = []
  441. # 1. RSI超卖做多
  442. if current_bar['RSI'] < 30:
  443. long_score += 2
  444. long_signals.append("RSI超卖")
  445. elif current_bar['RSI'] < 35:
  446. long_score += 1
  447. long_signals.append("RSI偏弱")
  448. # 2. KDJ超卖做多
  449. if current_bar['K'] < 20 and current_bar['D'] < 20:
  450. long_score += 2
  451. long_signals.append("KDJ超卖")
  452. elif current_bar['J'] < 0:
  453. long_score += 1
  454. long_signals.append("KDJ极端超卖")
  455. # 3. MACD金叉
  456. if current_bar['MACD_hist'] > 0 and df.iloc[i-1]['MACD_hist'] <= 0:
  457. long_score += 2
  458. long_signals.append("MACD金叉")
  459. elif current_bar['MACD_hist'] > df.iloc[i-1]['MACD_hist']:
  460. long_score += 1
  461. long_signals.append("MACD改善")
  462. # 4. 价格触及布林带下轨
  463. if current_bar['Close'] <= current_bar['BB_lower'] * 1.005:
  464. long_score += 2
  465. long_signals.append("触及下轨")
  466. elif current_bar['Close'] <= current_bar['BB_lower'] * 1.01:
  467. long_score += 1
  468. long_signals.append("接近下轨")
  469. # 5. 连续下跌后的反转
  470. recent_returns = df.iloc[i-6:i]['Returns']
  471. if recent_returns.min() < -0.015:
  472. consecutive_decline = sum(recent_returns < 0)
  473. if consecutive_decline >= 4:
  474. long_score += 2
  475. long_signals.append("连续下跌反转")
  476. # 6. 价格动量反转
  477. if current_bar['Price_Momentum'] < -0.02:
  478. long_score += 1
  479. long_signals.append("动量超卖")
  480. # 7. 成交量配合
  481. if current_bar['Volume_Ratio'] > 1.2:
  482. long_score += 1
  483. long_signals.append("放量配合")
  484. # 8. 当日开盘价格关系
  485. try:
  486. daily_high = df[df.index.date == df.index[i].date()]['High'].max()
  487. daily_low = df[df.index.date == df.index[i].date()]['Low'].min()
  488. daily_range = daily_high - daily_low
  489. if daily_range > 0:
  490. position_in_day = (current_bar['Close'] - daily_low) / daily_range
  491. if position_in_day < 0.3:
  492. long_score += 1
  493. long_signals.append("日内低位")
  494. except:
  495. pass
  496. # MA趋势过滤
  497. if current_bar['MA6'] < current_bar['MA12'] < current_bar['MA24']:
  498. long_score -= 1
  499. long_signals.append("MA下降趋势惩罚")
  500. elif current_bar['MA6'] > current_bar['MA12']:
  501. long_score += 1
  502. long_signals.append("MA短期上行")
  503. return long_score, long_signals
  504. # ==================== T+1只做多交易执行器 ====================
  505. class LongOnlyT1Executor:
  506. """只做多T+1交易执行器(当天买入,第二天才能卖出)"""
  507. def __init__(self, initial_capital=1000000):
  508. self.initial_capital = initial_capital
  509. self.params = {
  510. 'commission_rate': 0.0001, # 万分之一
  511. 'slippage_rate': 0.0, # 无滑点
  512. 'position_size_pct': 1.0, # 每次开仓100%仓位
  513. 'stop_loss_pct': 0.008, # 0.8%止损
  514. 'take_profit_pct': 0.02, # 2.0%止盈
  515. 'max_hold_bars': 16, # 最多持有8小时(16个30分钟)
  516. 'min_signal_strength': 4 # 最小信号强度
  517. }
  518. def execute_long_only_t1_trades(self, signals_df: pd.DataFrame) -> tuple:
  519. """执行只做多T+1交易"""
  520. print("正在执行只做多T+1交易...")
  521. df = signals_df.copy()
  522. # 初始化
  523. trades = []
  524. capital = self.initial_capital
  525. # 持仓状态
  526. long_position = 0 # 做多持仓数量
  527. long_entry_price = 0 # 做多开仓价
  528. long_entry_time = None # 做多开仓时间
  529. long_entry_date = None # 做多开仓日期(用于T+1判断)
  530. long_holding_bars = 0 # 做多持仓周期
  531. long_entry_signals = '' # 做多入场信号
  532. long_t1_affected = False # 是否受到T+1限制影响
  533. # 添加资金列
  534. df = df.copy()
  535. df['capital'] = float(capital)
  536. df['long_position'] = 0
  537. df['net_value'] = float(capital)
  538. for i in range(len(df)):
  539. current_time = pd.to_datetime(df.index[i])
  540. current_bar = df.iloc[i]
  541. price = current_bar['Close']
  542. current_date = current_time.date()
  543. # 更新当前净值
  544. current_value = capital
  545. if long_position > 0:
  546. current_value += long_position * price
  547. df.iloc[i, df.columns.get_loc('net_value')] = current_value
  548. # 开仓逻辑 - 只在无持仓时开仓
  549. if long_position == 0:
  550. # 做多开仓
  551. if current_bar['Signal'] == 1:
  552. position_size = int((capital * self.params['position_size_pct']) / price)
  553. if position_size > 0:
  554. cost = position_size * price * (1 + self.params['commission_rate'] + self.params['slippage_rate'])
  555. if cost <= capital:
  556. long_position = position_size
  557. long_entry_price = price
  558. long_entry_time = current_time
  559. long_entry_date = current_date # 记录买入日期
  560. long_entry_signals = current_bar.get('Long_Signals', '')
  561. long_holding_bars = 0
  562. long_t1_affected = False # 重置T+1限制标记
  563. capital -= cost
  564. # 计算预计止损止盈价格
  565. long_stop_loss_price = long_entry_price * (1 - self.params['stop_loss_pct'])
  566. long_take_profit_price = long_entry_price * (1 + self.params['take_profit_pct'])
  567. df.iloc[i, df.columns.get_loc('long_position')] = long_position
  568. print(f"\n{'='*60}")
  569. print(f"[LONG_OPEN] 做多开仓信号 #{len(trades) + 1}")
  570. print(f"{'='*60}")
  571. print(f"开仓时间: {long_entry_time}")
  572. print(f"开仓日期: {long_entry_date} (T+1限制: 最早{long_entry_date + timedelta(days=1)}后可卖出)")
  573. print(f"开仓价格: {long_entry_price:.2f} 元")
  574. print(f"预计止损: {long_stop_loss_price:.2f} 元 (-{self.params['stop_loss_pct']*100:.1f}%)")
  575. print(f"预计止盈: {long_take_profit_price:.2f} 元 (+{self.params['take_profit_pct']*100:.1f}%)")
  576. print(f"持仓数量: {position_size} 股")
  577. print(f"开仓市值: {position_size * long_entry_price:,.2f} 元")
  578. print(f"交易成本: {cost:,.2f} 元")
  579. print(f"剩余资金: {capital:,.2f} 元")
  580. print(f"入场信号: {long_entry_signals}")
  581. print(f"总资产: {capital + position_size * long_entry_price:,.2f} 元")
  582. # 平仓逻辑 - 做多平仓(T+1限制:买入当天不能卖出)
  583. elif long_position > 0:
  584. long_holding_bars += 1
  585. # T+1检查:买入当天不能卖出
  586. is_t1_restricted = (current_date == long_entry_date)
  587. # 计算止损止盈价格
  588. stop_loss = long_entry_price * (1 - self.params['stop_loss_pct'])
  589. take_profit = long_entry_price * (1 + self.params['take_profit_pct'])
  590. exit_signal = False
  591. exit_reason = ''
  592. exit_price = price
  593. # T+1限制:即使触发止损/止盈,当天也不能卖出
  594. if is_t1_restricted:
  595. # 检查是否触发止损/止盈,但只记录不执行
  596. if price <= stop_loss:
  597. print(f"[T+1限制] {current_time} 价格{price:.2f}触及止损线{stop_loss:.2f},但当天买入不能卖出,继续持有")
  598. long_t1_affected = True # 标记受到了T+1限制影响
  599. elif price >= take_profit:
  600. print(f"[T+1限制] {current_time} 价格{price:.2f}触及止盈线{take_profit:.2f},但当天买入不能卖出,继续持有")
  601. long_t1_affected = True # 标记受到了T+1限制影响
  602. else:
  603. # T+1已过,可以正常平仓
  604. # 止损
  605. if price <= stop_loss:
  606. exit_signal = True
  607. loss_pct = (long_entry_price - stop_loss) / long_entry_price * 100
  608. exit_reason = f"做多止损触发(价格{price:.2f}跌破止损线{stop_loss:.2f},亏损{loss_pct:.2f}%)"
  609. exit_price = price
  610. # 止盈
  611. elif price >= take_profit:
  612. exit_signal = True
  613. profit_pct = (price - long_entry_price) / long_entry_price * 100
  614. exit_reason = f"做多止盈触发(价格{price:.2f}突破止盈线{take_profit:.2f},盈利{profit_pct:.2f}%)"
  615. exit_price = price
  616. # 最大持仓时间
  617. elif long_holding_bars >= self.params['max_hold_bars']:
  618. exit_signal = True
  619. current_pnl_pct = (price - long_entry_price) / long_entry_price * 100
  620. exit_reason = f"做多时间止损(持仓{long_holding_bars}周期达上限{self.params['max_hold_bars']}周期,当前盈亏{current_pnl_pct:+.2f}%)"
  621. # 做多信号消失
  622. elif current_bar['RSI'] > 70:
  623. exit_signal = True
  624. current_pnl_pct = (price - long_entry_price) / long_entry_price * 100
  625. exit_reason = f"做多RSI超买平仓(RSI={current_bar['RSI']:.1f}超买,信号消失,当前盈亏{current_pnl_pct:+.2f}%)"
  626. # 执行平仓
  627. if exit_signal:
  628. # 计算盈亏
  629. gross_pnl = (exit_price - long_entry_price) * long_position
  630. open_cost = long_position * long_entry_price * (self.params['commission_rate'] + self.params['slippage_rate'])
  631. close_revenue = long_position * exit_price
  632. close_cost = close_revenue * (self.params['commission_rate'] + self.params['slippage_rate'])
  633. pnl = gross_pnl - open_cost - close_cost
  634. # 更新资金
  635. capital += close_revenue - close_cost
  636. # 记录交易
  637. trade = {
  638. '交易方向': '做多',
  639. '开仓时间': long_entry_time,
  640. '平仓时间': current_time,
  641. '开仓价格': long_entry_price,
  642. '平仓价格': exit_price,
  643. '仓位': long_position,
  644. '盈亏金额': pnl,
  645. '盈亏百分比': (exit_price - long_entry_price) / long_entry_price * 100,
  646. '退出原因': exit_reason,
  647. '持仓周期数': long_holding_bars,
  648. '持仓小时数': long_holding_bars * 0.5,
  649. 'T+1限制': '是' if long_t1_affected else '否',
  650. '入场信号': long_entry_signals,
  651. '平仓时资金': capital,
  652. '开仓市值': long_position * long_entry_price,
  653. '预计止损价格': long_stop_loss_price,
  654. '预计止盈价格': long_take_profit_price
  655. }
  656. trades.append(trade)
  657. # 打印平仓详情
  658. profit_ratio = (exit_price - long_entry_price) / long_entry_price * 100
  659. status = "[PROFIT]" if pnl > 0 else "[LOSS]"
  660. print(f"\n{'='*60}")
  661. print(f"{status} [LONG_CLOSE] 做多平仓信号 #{len(trades)}")
  662. print(f"{'='*60}")
  663. print(f"平仓时间: {current_time}")
  664. print(f"平仓价格: {exit_price:.2f} 元")
  665. print(f"持仓时长: {long_holding_bars * 0.5:.1f} 小时 ({long_holding_bars} 个30分钟周期)")
  666. print(f"退出原因: {exit_reason}")
  667. print(f"{'-'*60}")
  668. print(f"盈亏金额: {pnl:+,.2f} 元")
  669. print(f"盈亏比例: {profit_ratio:+.2f}%")
  670. print(f"{'-'*60}")
  671. print(f"当前资金: {capital:,.2f} 元")
  672. print(f"累计收益率: {(capital / self.initial_capital - 1) * 100:+.2f}%")
  673. print(f"{'='*60}")
  674. # 重置做多持仓
  675. long_position = 0
  676. long_entry_price = 0
  677. long_entry_time = None
  678. long_entry_date = None
  679. long_holding_bars = 0
  680. # 更新资金和持仓状态
  681. df.iloc[i, df.columns.get_loc('capital')] = capital
  682. df.iloc[i, df.columns.get_loc('long_position')] = long_position
  683. # 强制平仓剩余持仓
  684. if long_position > 0:
  685. final_price = df.iloc[-1]['Close']
  686. final_time = pd.to_datetime(df.index[-1])
  687. gross_pnl = (final_price - long_entry_price) * long_position
  688. open_cost = long_position * long_entry_price * (self.params['commission_rate'] + self.params['slippage_rate'])
  689. close_revenue = long_position * final_price
  690. close_cost = close_revenue * (self.params['commission_rate'] + self.params['slippage_rate'])
  691. pnl = gross_pnl - open_cost - close_cost
  692. capital += close_revenue - close_cost
  693. trade = {
  694. '交易方向': '做多',
  695. '开仓时间': long_entry_time,
  696. '平仓时间': df.index[-1],
  697. '开仓价格': long_entry_price,
  698. '平仓价格': final_price,
  699. '仓位': long_position,
  700. '盈亏金额': pnl,
  701. '盈亏百分比': (final_price - long_entry_price) / long_entry_price * 100,
  702. '退出原因': f'做多强制平仓(回测结束,持仓{long_holding_bars}周期,最终价格{final_price:.2f},盈亏{(final_price - long_entry_price) / long_entry_price * 100:+.2f}%)',
  703. '持仓周期数': long_holding_bars,
  704. '持仓小时数': long_holding_bars * 0.5,
  705. 'T+1限制': '是' if long_t1_affected else '否',
  706. '入场信号': long_entry_signals,
  707. '平仓时资金': capital,
  708. '开仓市值': long_position * long_entry_price,
  709. '预计止损价格': long_stop_loss_price,
  710. '预计止盈价格': long_take_profit_price
  711. }
  712. trades.append(trade)
  713. print(f"\n{'='*60}")
  714. print(f"[FORCE] [LONG_CLOSE] 做多强制平仓信号 #{len(trades)}")
  715. print(f"{'='*60}")
  716. print(f"平仓时间: {df.index[-1]}")
  717. print(f"平仓价格: {final_price:.2f} 元")
  718. print(f"退出原因: 做多强制平仓(回测结束)")
  719. print(f"T+1限制: {'是' if long_t1_affected else '否'}")
  720. print(f"盈亏金额: {pnl:+,.2f} 元")
  721. print(f"{'='*60}")
  722. trades_df = pd.DataFrame(trades)
  723. if len(trades_df) > 0:
  724. # 统一时间格式
  725. for col in trades_df.columns:
  726. if '时间' in col:
  727. trades_df[col] = pd.to_datetime(trades_df[col])
  728. trades_df = trades_df.sort_values('开仓时间')
  729. print(f"只做多T+1交易执行完成,共{len(trades_df)}笔交易")
  730. return df, trades_df
  731. # ==================== 验证分析模块 ====================
  732. def validate_long_only_t1_results(results_df, trades_df, initial_capital):
  733. """验证只做多T+1交易结果"""
  734. print("\n" + "=" * 80)
  735. print("创业板50 30分钟只做多T+1交易结果验证")
  736. print("=" * 80)
  737. print(f"\n【基础数据验证】")
  738. final_capital = results_df['net_value'].iloc[-1]
  739. total_return = (final_capital - initial_capital) / initial_capital * 100
  740. print(f"初始资金: {initial_capital:,.2f}元")
  741. print(f"最终资金: {final_capital:,.2f}元")
  742. print(f"总收益率: {total_return:.2f}%")
  743. print(f"交易次数: {len(trades_df)}笔")
  744. if len(trades_df) > 0:
  745. # 只做多统计
  746. long_trades = trades_df
  747. print(f"\n【只做多交易统计】")
  748. long_win_trades = long_trades[long_trades['盈亏金额'] > 0]
  749. long_lose_trades = long_trades[long_trades['盈亏金额'] < 0]
  750. long_total_pnl = long_trades['盈亏金额'].sum()
  751. print(f"总交易数: {len(long_trades)}笔")
  752. print(f"盈利交易: {len(long_win_trades)}笔 ({len(long_win_trades)/len(long_trades)*100:.1f}%)")
  753. print(f"亏损交易: {len(long_lose_trades)}笔 ({len(long_lose_trades)/len(long_trades)*100:.1f}%)")
  754. print(f"平均持仓时间: {long_trades['持仓小时数'].mean():.1f}小时")
  755. print(f"平均收益率: {long_trades['盈亏百分比'].mean():.2f}%")
  756. print(f"总盈亏: {long_total_pnl:+,.2f}元")
  757. # 按退出原因统计
  758. print(f"\n【退出原因统计】")
  759. for reason, count in trades_df['退出原因'].value_counts().items():
  760. percentage = count / len(trades_df) * 100
  761. reason_pnl = trades_df[trades_df['退出原因'] == reason]['盈亏金额'].sum()
  762. print(f" {reason}: {count}次 ({percentage:.1f}%) - 总盈亏: {reason_pnl:+,.2f}元")
  763. # T+1影响统计
  764. if 'T+1限制' in trades_df.columns:
  765. t1_affected = trades_df[trades_df['T+1限制'] == '是']
  766. print(f"\n【T+1限制影响】")
  767. print(f"受T+1限制的交易: {len(t1_affected)}笔 ({len(t1_affected)/len(trades_df)*100:.1f}%)")
  768. if len(t1_affected) > 0:
  769. print(f"T+1限制交易盈亏: {t1_affected['盈亏金额'].sum():+,.2f}元")
  770. # ==================== 主程序 ====================
  771. def main():
  772. """主程序 - 运行30分钟只做多T+1策略"""
  773. print("=" * 80)
  774. print("创业板50 30分钟只做多T+1策略")
  775. print("=" * 80)
  776. # 加载配置文件
  777. config_manager = ConfigManager('config.json')
  778. # 自动加载best_parameters.json覆盖默认参数
  779. best_params_file = 'best_parameters.json'
  780. if os.path.exists(best_params_file):
  781. try:
  782. with open(best_params_file, 'r', encoding='utf-8') as f:
  783. best_data = json.load(f)
  784. best_params = best_data.get('best_params', {})
  785. strategy_keys = ['position_size_pct', 'stop_loss_pct', 'take_profit_pct', 'max_hold_bars']
  786. for key in strategy_keys:
  787. if key in best_params:
  788. config_manager.config.setdefault('strategy', {})[key] = best_params[key]
  789. print(f"已加载优化参数: {best_params_file}")
  790. except Exception as e:
  791. print(f"加载优化参数失败: {e},使用config.json默认参数")
  792. # 从配置文件读取参数
  793. BACKTEST_START_DATE = config_manager.get('strategy', 'backtest_start_date', "2025-10-01")
  794. PREWARMP_DAYS = config_manager.get('strategy', 'prewamp_days', 30)
  795. INITIAL_CAPITAL = config_manager.get('strategy', 'initial_capital', 1000000)
  796. # 读取截止时间配置,支持"now"或具体日期
  797. backtest_end_config = config_manager.get('strategy', 'backtest_end_date', "now")
  798. if backtest_end_config.lower() == "now":
  799. BACKTEST_END_DATE = datetime.now().strftime('%Y-%m-%d')
  800. else:
  801. BACKTEST_END_DATE = backtest_end_config
  802. # 转换日期格式
  803. start_date = datetime.strptime(BACKTEST_START_DATE, "%Y-%m-%d")
  804. end_date = datetime.strptime(BACKTEST_END_DATE, "%Y-%m-%d").replace(hour=23, minute=59, second=59)
  805. # 计算数据获取开始时间(回测开始时间 - 预热期)
  806. data_start_date = start_date - timedelta(days=PREWARMP_DAYS)
  807. # 显示数据源配置
  808. use_local_file = config_manager.get('data_source', 'use_local_file', False)
  809. data_source_mode = "本地文件模式" if use_local_file else "在线获取模式"
  810. local_file_path = config_manager.get('data_source', 'local_file_path', '')
  811. print(f"\n策略参数:")
  812. print(f" 回测期间: {BACKTEST_START_DATE} 至 {BACKTEST_END_DATE}")
  813. print(f" 数据获取期间: {data_start_date.strftime('%Y-%m-%d')} 至 {BACKTEST_END_DATE}")
  814. print(f" 指标预热期: {PREWARMP_DAYS}天")
  815. print(f" K线周期: 30分钟")
  816. print(f" 初始资金: {INITIAL_CAPITAL:,}元")
  817. print(f" 标的指数: 创业板50 (399673)")
  818. print(f" 交易方向: 只做多(T+1限制)")
  819. print(f" 数据源: {data_source_mode}")
  820. if use_local_file:
  821. print(f" 本地文件路径: {local_file_path}")
  822. try:
  823. # Phase 1: 数据获取
  824. print(f"\n【Phase 1: 30分钟数据获取】")
  825. fetcher = IntradayDataFetcher(config_manager)
  826. # 获取包含预热期的完整数据
  827. full_data = fetcher.fetch_30min_data(start_date=data_start_date, end_date=end_date)
  828. full_data = fetcher.calculate_intraday_indicators(full_data)
  829. # 筛选回测期间的数据
  830. original_len = len(full_data)
  831. backtest_data = full_data[(full_data.index >= start_date) & (full_data.index <= end_date)].copy()
  832. print(f"筛选回测数据: {original_len} -> {len(backtest_data)} 条")
  833. print(f"回测数据范围: {backtest_data.index[0]} 到 {backtest_data.index[-1]}")
  834. # Phase 2: 信号生成
  835. print(f"\n【Phase 2: 只做多信号生成】")
  836. signal_gen = LongOnlySignalGenerator()
  837. signals_df = signal_gen.generate_long_only_signals(backtest_data)
  838. # Phase 3: 交易执行
  839. print(f"\n【Phase 3: 只做多T+1交易执行】")
  840. executor = LongOnlyT1Executor(initial_capital=INITIAL_CAPITAL)
  841. # 用配置参数覆盖默认值
  842. executor.params['stop_loss_pct'] = config_manager.get('strategy', 'stop_loss_pct', 0.008)
  843. executor.params['take_profit_pct'] = config_manager.get('strategy', 'take_profit_pct', 0.02)
  844. executor.params['max_hold_bars'] = config_manager.get('strategy', 'max_hold_bars', 16)
  845. executor.params['position_size_pct'] = config_manager.get('strategy', 'position_size_pct', 1.0)
  846. results_df, trades_df = executor.execute_long_only_t1_trades(signals_df)
  847. # Phase 4: 验证分析
  848. print(f"\n【Phase 4: 结果验证与分析】")
  849. validate_long_only_t1_results(results_df, trades_df, INITIAL_CAPITAL)
  850. # Phase 5: 导出数据
  851. if len(trades_df) > 0:
  852. print(f"\n【Phase 5: 导出交易数据】")
  853. # 统一时间格式
  854. for col in trades_df.columns:
  855. if '时间' in col:
  856. trades_df[col] = pd.to_datetime(trades_df[col]).dt.strftime('%Y-%m-%d %H:%M:%S')
  857. # 生成带时间戳的文件名
  858. timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
  859. output_file = f'cyb50_30min_long_only_t1_trades_{timestamp}.csv'
  860. trades_df.to_csv(output_file, index=False, encoding='utf-8-sig')
  861. print(f"只做多T+1交易记录已保存到: {output_file}")
  862. print(f"时间戳格式: YYYY-MM-DD HH:MM:SS")
  863. # 策略总结
  864. print(f"\n" + "=" * 80)
  865. print("只做多T+1策略运行总结")
  866. print("=" * 80)
  867. if len(trades_df) > 0:
  868. final_capital = results_df['net_value'].iloc[-1]
  869. total_return = (final_capital - INITIAL_CAPITAL) / INITIAL_CAPITAL * 100
  870. print(f"初始资金: {INITIAL_CAPITAL:,.2f}元")
  871. print(f"最终资金: {final_capital:,.2f}元")
  872. print(f"总收益率: {total_return:.2f}%")
  873. print(f"总交易次数: {len(trades_df)}笔")
  874. print(f"整体胜率: {(trades_df['盈亏金额'] > 0).sum() / len(trades_df) * 100:.1f}%")
  875. print(f"平均收益率: {trades_df['盈亏百分比'].mean():.2f}%")
  876. print(f"最大单笔盈利: {trades_df['盈亏金额'].max():+,.2f}元")
  877. print(f"最大单笔亏损: {trades_df['盈亏金额'].min():+,.2f}元")
  878. print(f"\n[SUCCESS] 只做多T+1策略运行成功!")
  879. else:
  880. print("未产生任何交易信号")
  881. except Exception as e:
  882. print(f"\n[ERROR] 只做多T+1策略运行出错: {str(e)}")
  883. import traceback
  884. traceback.print_exc()
  885. finally:
  886. print(f"\n" + "=" * 80)
  887. if __name__ == "__main__":
  888. main()