cyb50_30min_dual_direction.py 64 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393
  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.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.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 DualDirectionSignalGenerator:
  374. """多空双向信号生成器"""
  375. def __init__(self):
  376. self.long_signal_count = 0
  377. self.short_signal_count = 0
  378. self.total_signal_count = 0
  379. def generate_dual_direction_signals(self, data: pd.DataFrame) -> pd.DataFrame:
  380. """生成多空双向信号"""
  381. print("正在生成多空双向信号...")
  382. signals = []
  383. df = data.copy()
  384. for i in range(24, len(df)): # 至少需要12小时(24个30分钟)的历史数据
  385. current_bar = df.iloc[i]
  386. current_time = df.index[i]
  387. # 跳过不适合交易的时间段
  388. if hasattr(current_time, 'hour'): # 有小时信息的30分钟数据
  389. hour = current_time.hour
  390. if hour < 9 or hour > 15: # 只在交易时间内
  391. continue
  392. # 生成基础信号数据
  393. signal = {
  394. 'DateTime': str(current_time),
  395. 'Open': current_bar['Open'],
  396. 'High': current_bar['High'],
  397. 'Low': current_bar['Low'],
  398. 'Close': current_bar['Close'],
  399. 'Volume': current_bar['Volume'],
  400. 'RSI': current_bar['RSI'],
  401. 'MACD': current_bar['MACD'],
  402. 'MACD_hist': current_bar['MACD_hist'],
  403. 'K': current_bar['K'],
  404. 'D': current_bar['D'],
  405. 'J': current_bar['J'],
  406. 'ATR_Pct': current_bar['ATR_Pct'],
  407. 'Volume_Ratio': current_bar['Volume_Ratio'],
  408. 'Price_Momentum': current_bar['Price_Momentum'],
  409. 'Close_Open_Pct': current_bar['Close_Open_Pct']
  410. }
  411. # 计算做多信号强度
  412. long_score, long_signals = self._calculate_long_signals(current_bar, df, i)
  413. # 计算做空信号强度
  414. short_score, short_signals = self._calculate_short_signals(current_bar, df, i)
  415. # 设置信号分数和描述
  416. signal['Long_Score'] = long_score
  417. signal['Long_Signals'] = ', '.join(long_signals) if long_signals else ''
  418. signal['Short_Score'] = short_score
  419. signal['Short_Signals'] = ', '.join(short_signals) if short_signals else ''
  420. # 决定最终信号方向和强度
  421. final_signal = 0
  422. signal_type = ''
  423. # 信号优先级和冲突处理
  424. if long_score >= 4 and short_score >= 4:
  425. # 两个方向都达到阈值,选择信号强度更高的
  426. if long_score > short_score:
  427. final_signal = 1
  428. signal_type = f'做多翻转(强度{long_score} vs {short_score})'
  429. self.long_signal_count += 1
  430. elif short_score > long_score:
  431. final_signal = -1
  432. signal_type = f'做空反转(强度{short_score} vs {long_score})'
  433. self.short_signal_count += 1
  434. else:
  435. # 强度相等时,根据当前价格位置决定
  436. bb_position = (current_bar['Close'] - current_bar['BB_lower']) / (current_bar['BB_upper'] - current_bar['BB_lower'])
  437. if bb_position < 0.3: # 偏向下轨,优先做多
  438. final_signal = 1
  439. signal_type = f'做多翻转(位置优先)'
  440. self.long_signal_count += 1
  441. elif bb_position > 0.7: # 偏向上轨,优先做空
  442. final_signal = -1
  443. signal_type = f'做空反转(位置优先)'
  444. self.short_signal_count += 1
  445. else:
  446. # 中间位置,暂不开仓
  447. final_signal = 0
  448. signal_type = '信号冲突(强度相等)'
  449. elif long_score >= 4:
  450. final_signal = 1
  451. signal_type = '做多翻转'
  452. self.long_signal_count += 1
  453. elif short_score >= 4:
  454. final_signal = -1
  455. signal_type = '做空反转'
  456. self.short_signal_count += 1
  457. self.total_signal_count = self.long_signal_count + self.short_signal_count
  458. signal['Signal'] = final_signal
  459. signal['Signal_Type'] = signal_type
  460. signals.append(signal)
  461. signals_df = pd.DataFrame(signals)
  462. if len(signals_df) > 0:
  463. signals_df.set_index('DateTime', inplace=True)
  464. else:
  465. print("警告:没有生成任何信号")
  466. print(f"多空双向信号生成完成")
  467. print(f"做多信号: {self.long_signal_count}个")
  468. print(f"做空信号: {self.short_signal_count}个")
  469. print(f"总信号: {self.total_signal_count}个")
  470. if len(signals_df) > 0:
  471. print(f"信号密度: {self.total_signal_count/len(signals_df)*100:.2f}%")
  472. return signals_df
  473. def _calculate_long_signals(self, current_bar, df, i):
  474. """计算做多信号强度"""
  475. long_score = 0
  476. long_signals = []
  477. # 1. RSI超卖做多
  478. if current_bar['RSI'] < 30:
  479. long_score += 2
  480. long_signals.append("RSI超卖")
  481. elif current_bar['RSI'] < 35:
  482. long_score += 1
  483. long_signals.append("RSI偏弱")
  484. # 2. KDJ超卖做多
  485. if current_bar['K'] < 20 and current_bar['D'] < 20:
  486. long_score += 2
  487. long_signals.append("KDJ超卖")
  488. elif current_bar['J'] < 0:
  489. long_score += 1
  490. long_signals.append("KDJ极端超卖")
  491. # 3. MACD金叉
  492. if current_bar['MACD_hist'] > 0 and df.iloc[i-1]['MACD_hist'] <= 0:
  493. long_score += 2
  494. long_signals.append("MACD金叉")
  495. elif current_bar['MACD_hist'] > df.iloc[i-1]['MACD_hist']:
  496. long_score += 1
  497. long_signals.append("MACD改善")
  498. # 4. 价格触及布林带下轨
  499. if current_bar['Close'] <= current_bar['BB_lower'] * 1.005:
  500. long_score += 2
  501. long_signals.append("触及下轨")
  502. elif current_bar['Close'] <= current_bar['BB_lower'] * 1.01:
  503. long_score += 1
  504. long_signals.append("接近下轨")
  505. # 5. 连续下跌后的反转
  506. recent_returns = df.iloc[i-6:i]['Returns']
  507. if recent_returns.min() < -0.015:
  508. consecutive_decline = sum(recent_returns < 0)
  509. if consecutive_decline >= 4:
  510. long_score += 2
  511. long_signals.append("连续下跌反转")
  512. # 6. 价格动量反转
  513. if current_bar['Price_Momentum'] < -0.02:
  514. long_score += 1
  515. long_signals.append("动量超卖")
  516. # 7. 成交量配合
  517. if current_bar['Volume_Ratio'] > 1.2:
  518. long_score += 1
  519. long_signals.append("放量配合")
  520. # 8. 当日开盘价格关系
  521. try:
  522. daily_high = df[df.index.date == df.index[i].date()]['High'].max()
  523. daily_low = df[df.index.date == df.index[i].date()]['Low'].min()
  524. daily_range = daily_high - daily_low
  525. if daily_range > 0:
  526. position_in_day = (current_bar['Close'] - daily_low) / daily_range
  527. if position_in_day < 0.3:
  528. long_score += 1
  529. long_signals.append("日内低位")
  530. except:
  531. pass
  532. # MA趋势过滤
  533. if current_bar['MA6'] < current_bar['MA12'] < current_bar['MA24']:
  534. long_score -= 1
  535. long_signals.append("MA下降趋势惩罚")
  536. elif current_bar['MA6'] > current_bar['MA12']:
  537. long_score += 1
  538. long_signals.append("MA短期上行")
  539. return long_score, long_signals
  540. def _calculate_short_signals(self, current_bar, df, i):
  541. """计算做空信号强度"""
  542. short_score = 0
  543. short_signals = []
  544. # 1. RSI超买做空
  545. if current_bar['RSI'] > 70:
  546. short_score += 2
  547. short_signals.append("RSI超买")
  548. elif current_bar['RSI'] > 65:
  549. short_score += 1
  550. short_signals.append("RSI偏强")
  551. # 2. KDJ超买做空
  552. if current_bar['K'] > 80 and current_bar['D'] > 80:
  553. short_score += 2
  554. short_signals.append("KDJ超买")
  555. elif current_bar['J'] > 100:
  556. short_score += 1
  557. short_signals.append("KDJ极端超买")
  558. # 3. MACD死叉
  559. if current_bar['MACD_hist'] < 0 and df.iloc[i-1]['MACD_hist'] >= 0:
  560. short_score += 2
  561. short_signals.append("MACD死叉")
  562. elif current_bar['MACD_hist'] < df.iloc[i-1]['MACD_hist']:
  563. short_score += 1
  564. short_signals.append("MACD恶化")
  565. # 4. 价格触及布林带上轨
  566. if current_bar['Close'] >= current_bar['BB_upper'] * 0.995:
  567. short_score += 2
  568. short_signals.append("触及上轨")
  569. elif current_bar['Close'] >= current_bar['BB_upper'] * 0.99:
  570. short_score += 1
  571. short_signals.append("接近上轨")
  572. # 5. 连续上涨后的反转
  573. recent_returns = df.iloc[i-6:i]['Returns']
  574. if recent_returns.max() > 0.015:
  575. consecutive_rise = sum(recent_returns > 0)
  576. if consecutive_rise >= 4:
  577. short_score += 2
  578. short_signals.append("连续上涨反转")
  579. # 6. 价格动量反转
  580. if current_bar['Price_Momentum'] > 0.02:
  581. short_score += 1
  582. short_signals.append("动量超买")
  583. # 7. 成交量配合
  584. if current_bar['Volume_Ratio'] > 1.2:
  585. short_score += 1
  586. short_signals.append("放量配合")
  587. # 8. 当日开盘价格关系
  588. try:
  589. daily_high = df[df.index.date == df.index[i].date()]['High'].max()
  590. daily_low = df[df.index.date == df.index[i].date()]['Low'].min()
  591. daily_range = daily_high - daily_low
  592. if daily_range > 0:
  593. position_in_day = (current_bar['Close'] - daily_low) / daily_range
  594. if position_in_day > 0.7:
  595. short_score += 1
  596. short_signals.append("日内高位")
  597. except:
  598. pass
  599. # MA趋势过滤
  600. if current_bar['MA6'] > current_bar['MA12'] > current_bar['MA24']:
  601. short_score -= 1
  602. short_signals.append("MA上升趋势惩罚")
  603. elif current_bar['MA6'] < current_bar['MA12']:
  604. short_score += 1
  605. short_signals.append("MA短期下行")
  606. return short_score, short_signals
  607. # ==================== 多空双向交易执行器 ====================
  608. class DualDirectionExecutor:
  609. """多空双向交易执行器"""
  610. def __init__(self, initial_capital=1000000):
  611. self.initial_capital = initial_capital
  612. self.params = {
  613. 'commission_rate': 0.0, # 无手续费
  614. 'slippage_rate': 0.0, # 无滑点
  615. 'position_size_pct': 1.0, # 每次开仓100%仓位
  616. 'stop_loss_pct': 0.008, # 0.8%止损
  617. 'take_profit_pct': 0.02, # 2.0%止盈
  618. 'max_hold_bars': 16, # 最多持有8小时(16个30分钟)
  619. 'min_signal_strength': 4 # 最小信号强度
  620. }
  621. def execute_dual_direction_trades(self, signals_df: pd.DataFrame) -> tuple:
  622. """执行多空双向交易"""
  623. print("正在执行多空双向交易...")
  624. df = signals_df.copy()
  625. # 初始化
  626. trades = []
  627. capital = self.initial_capital
  628. # 持仓状态
  629. long_position = 0 # 做多持仓数量
  630. short_position = 0 # 做空持仓数量
  631. long_entry_price = 0 # 做多开仓价
  632. short_entry_price = 0 # 做空开仓价
  633. long_entry_time = None # 做多开仓时间
  634. short_entry_time = None # 做空开仓时间
  635. long_holding_bars = 0 # 做多持仓周期
  636. short_holding_bars = 0 # 做空持仓周期
  637. long_entry_signals = '' # 做多入场信号
  638. short_entry_signals = '' # 做空入场信号
  639. # 添加资金列
  640. df = df.copy()
  641. df['capital'] = float(capital) # 确保是浮点数
  642. df['long_position'] = 0
  643. df['short_position'] = 0
  644. df['net_value'] = float(capital) # 确保是浮点数
  645. for i in range(len(df)):
  646. current_time = df.index[i]
  647. current_bar = df.iloc[i]
  648. price = current_bar['Close']
  649. # 更新当前净值
  650. current_value = capital
  651. if long_position > 0:
  652. current_value += long_position * price
  653. if short_position < 0:
  654. # 做空盈亏
  655. short_pnl = (short_entry_price - price) * abs(short_position)
  656. margin_held = abs(short_position) * short_entry_price
  657. current_value += margin_held + short_pnl
  658. df.iloc[i, df.columns.get_loc('net_value')] = current_value
  659. # 开仓逻辑 - 只在无持仓时开仓
  660. if long_position == 0 and short_position == 0:
  661. # 做多开仓
  662. if current_bar['Signal'] == 1:
  663. position_size = int((capital * self.params['position_size_pct']) / price)
  664. if position_size > 0:
  665. cost = position_size * price * (1 + self.params['commission_rate'] + self.params['slippage_rate'])
  666. if cost <= capital:
  667. long_position = position_size
  668. long_entry_price = price
  669. long_entry_time = current_time
  670. long_entry_signals = current_bar.get('Long_Signals', '')
  671. long_holding_bars = 0
  672. capital -= cost
  673. # 计算预计止损止盈价格
  674. long_stop_loss_price = long_entry_price * (1 - self.params['stop_loss_pct'])
  675. long_take_profit_price = long_entry_price * (1 + self.params['take_profit_pct'])
  676. df.iloc[i, df.columns.get_loc('long_position')] = long_position
  677. print(f"\n{'='*60}")
  678. print(f"[LONG_OPEN] 做多开仓信号 #{len(trades) + 1}")
  679. print(f"{'='*60}")
  680. print(f"开仓时间: {long_entry_time}")
  681. print(f"开仓价格: {long_entry_price:.2f} 元")
  682. print(f"预计止损: {long_stop_loss_price:.2f} 元 (-{self.params['stop_loss_pct']*100:.1f}%)")
  683. print(f"预计止盈: {long_take_profit_price:.2f} 元 (+{self.params['take_profit_pct']*100:.1f}%)")
  684. print(f"持仓数量: {position_size} 股")
  685. print(f"开仓市值: {position_size * long_entry_price:,.2f} 元")
  686. print(f"交易成本: {cost:,.2f} 元")
  687. print(f"剩余资金: {capital:,.2f} 元")
  688. print(f"入场信号: {long_entry_signals}")
  689. print(f"总资产: {capital + position_size * long_entry_price:,.2f} 元")
  690. # 做空开仓
  691. elif current_bar['Signal'] == -1:
  692. position_value = capital * self.params['position_size_pct']
  693. position_size = int(position_value / price)
  694. if position_size > 0:
  695. margin_required = position_size * price
  696. commission = position_size * price * (self.params['commission_rate'] + self.params['slippage_rate'])
  697. total_cost = margin_required + commission
  698. if total_cost <= capital:
  699. short_position = -position_size
  700. short_entry_price = price
  701. short_entry_time = current_time
  702. short_entry_signals = current_bar.get('Short_Signals', '')
  703. short_holding_bars = 0
  704. capital -= total_cost
  705. # 计算预计止损止盈价格
  706. short_stop_loss_price = short_entry_price * (1 + self.params['stop_loss_pct'])
  707. short_take_profit_price = short_entry_price * (1 - self.params['take_profit_pct'])
  708. df.iloc[i, df.columns.get_loc('short_position')] = short_position
  709. print(f"\n{'='*60}")
  710. print(f"[SHORT_OPEN] 做空开仓信号 #{len(trades) + 1}")
  711. print(f"{'='*60}")
  712. print(f"开仓时间: {short_entry_time}")
  713. print(f"开仓价格: {short_entry_price:.2f} 元")
  714. print(f"预计止损: {short_stop_loss_price:.2f} 元 (+{self.params['stop_loss_pct']*100:.1f}%)")
  715. print(f"预计止盈: {short_take_profit_price:.2f} 元 (-{self.params['take_profit_pct']*100:.1f}%)")
  716. print(f"做空数量: {position_size} 股")
  717. print(f"开仓市值: {position_size * short_entry_price:,.2f} 元")
  718. print(f"保证金占用: {margin_required:,.2f} 元")
  719. print(f"手续费: {commission:,.2f} 元")
  720. print(f"总扣除: {total_cost:,.2f} 元")
  721. print(f"剩余资金: {capital:,.2f} 元")
  722. print(f"入场信号: {short_entry_signals}")
  723. print(f"总资产: {capital + margin_required:,.2f} 元")
  724. # 平仓逻辑 - 做多平仓
  725. elif long_position > 0:
  726. long_holding_bars += 1
  727. # 计算止损止盈价格
  728. stop_loss = long_entry_price * (1 - self.params['stop_loss_pct'])
  729. take_profit = long_entry_price * (1 + self.params['take_profit_pct'])
  730. exit_signal = False
  731. exit_reason = ''
  732. exit_price = price
  733. # 止损
  734. if price <= stop_loss:
  735. exit_signal = True
  736. actual_loss_pct = (long_entry_price - price) / long_entry_price * 100
  737. exit_reason = f"做多止损触发(价格{price:.2f}跌破止损线{stop_loss:.2f},实际亏损{actual_loss_pct:.2f}%)"
  738. exit_price = price # 使用实际市场价格,而不是止损线价格
  739. # 止盈
  740. elif price >= take_profit:
  741. exit_signal = True
  742. actual_profit_pct = (price - long_entry_price) / long_entry_price * 100
  743. exit_reason = f"做多止盈触发(价格{price:.2f}突破止盈线{take_profit:.2f},实际盈利{actual_profit_pct:.2f}%)"
  744. exit_price = price # 使用实际市场价格,而不是止盈线价格
  745. # 最大持仓时间
  746. elif long_holding_bars >= self.params['max_hold_bars']:
  747. exit_signal = True
  748. current_pnl_pct = (price - long_entry_price) / long_entry_price * 100
  749. exit_reason = f"做多时间止损(持仓{long_holding_bars}周期达上限{self.params['max_hold_bars']}周期,当前盈亏{current_pnl_pct:+.2f}%)"
  750. # 做多信号消失
  751. elif current_bar['RSI'] > 70:
  752. exit_signal = True
  753. current_pnl_pct = (price - long_entry_price) / long_entry_price * 100
  754. exit_reason = f"做多RSI超买平仓(RSI={current_bar['RSI']:.1f}超买,信号消失,当前盈亏{current_pnl_pct:+.2f}%)"
  755. # 执行平仓
  756. if exit_signal:
  757. # 计算盈亏
  758. gross_pnl = (exit_price - long_entry_price) * long_position
  759. open_cost = long_position * long_entry_price * (self.params['commission_rate'] + self.params['slippage_rate'])
  760. close_revenue = long_position * exit_price
  761. close_cost = close_revenue * (self.params['commission_rate'] + self.params['slippage_rate'])
  762. pnl = gross_pnl - open_cost - close_cost
  763. # 更新资金
  764. capital += close_revenue - close_cost
  765. # 记录交易
  766. trade = {
  767. '交易方向': '做多',
  768. '开仓时间': long_entry_time,
  769. '平仓时间': current_time,
  770. '开仓价格': long_entry_price,
  771. '平仓价格': exit_price,
  772. '仓位': long_position,
  773. '盈亏金额': pnl,
  774. '盈亏百分比': (exit_price - long_entry_price) / long_entry_price * 100,
  775. '退出原因': exit_reason,
  776. '持仓周期数': long_holding_bars,
  777. '持仓小时数': long_holding_bars * 0.5,
  778. '入场信号': long_entry_signals,
  779. '平仓时资金': capital,
  780. '开仓市值': long_position * long_entry_price,
  781. '预计止损价格': long_stop_loss_price,
  782. '预计止盈价格': long_take_profit_price
  783. }
  784. trades.append(trade)
  785. # 打印平仓详情
  786. profit_ratio = (exit_price - long_entry_price) / long_entry_price * 100
  787. status = "[PROFIT]" if pnl > 0 else "[LOSS]"
  788. print(f"\n{'='*60}")
  789. print(f"{status} [LONG_CLOSE] 做多平仓信号 #{len(trades)}")
  790. print(f"{'='*60}")
  791. print(f"平仓时间: {current_time}")
  792. print(f"平仓价格: {exit_price:.2f} 元")
  793. print(f"持仓时长: {long_holding_bars * 0.5:.1f} 小时 ({long_holding_bars} 个30分钟周期)")
  794. print(f"退出原因: {exit_reason}")
  795. print(f"{'-'*60}")
  796. print(f"盈亏金额: {pnl:+,.2f} 元")
  797. print(f"盈亏比例: {profit_ratio:+.2f}%")
  798. print(f"{'-'*60}")
  799. print(f"当前资金: {capital:,.2f} 元")
  800. print(f"累计收益率: {(capital / self.initial_capital - 1) * 100:+.2f}%")
  801. print(f"{'='*60}")
  802. # 重置做多持仓
  803. long_position = 0
  804. long_entry_price = 0
  805. long_entry_time = None
  806. long_holding_bars = 0
  807. # 平仓逻辑 - 做空平仓
  808. elif short_position < 0:
  809. short_holding_bars += 1
  810. # 计算止损止盈价格(做空逻辑相反)
  811. stop_loss_price = short_entry_price * (1 + self.params['stop_loss_pct']) # 价格上涨止损
  812. take_profit_price = short_entry_price * (1 - self.params['take_profit_pct']) # 价格下跌止盈
  813. exit_signal = False
  814. exit_reason = ''
  815. exit_price = price
  816. # 止损(价格上涨)
  817. if price >= stop_loss_price:
  818. exit_signal = True
  819. actual_loss_pct = (price - short_entry_price) / short_entry_price * 100
  820. exit_reason = f"做空止损触发(价格{price:.2f}突破止损线{stop_loss_price:.2f},实际亏损{actual_loss_pct:.2f}%)"
  821. exit_price = price # 使用实际市场价格,而不是止损线价格
  822. # 止盈(价格下跌)
  823. elif price <= take_profit_price:
  824. exit_signal = True
  825. actual_profit_pct = (short_entry_price - price) / short_entry_price * 100
  826. exit_reason = f"做空止盈触发(价格{price:.2f}跌破止盈线{take_profit_price:.2f},实际盈利{actual_profit_pct:.2f}%)"
  827. exit_price = price # 使用实际市场价格,而不是止盈线价格
  828. # 最大持仓时间
  829. elif short_holding_bars >= self.params['max_hold_bars']:
  830. exit_signal = True
  831. current_pnl_pct = (short_entry_price - price) / short_entry_price * 100
  832. exit_reason = f"做空时间止损(持仓{short_holding_bars}周期达上限{self.params['max_hold_bars']}周期,当前盈亏{current_pnl_pct:+.2f}%)"
  833. # 做空信号消失
  834. elif current_bar['RSI'] < 30:
  835. exit_signal = True
  836. current_pnl_pct = (short_entry_price - price) / short_entry_price * 100
  837. exit_reason = f"做空RSI超卖平仓(RSI={current_bar['RSI']:.1f}超卖,信号消失,当前盈亏{current_pnl_pct:+.2f}%)"
  838. # 执行平仓
  839. if exit_signal:
  840. # 计算盈亏
  841. gross_pnl = (short_entry_price - exit_price) * abs(short_position)
  842. open_commission = abs(short_position) * short_entry_price * (self.params['commission_rate'] + self.params['slippage_rate'])
  843. close_commission = abs(short_position) * exit_price * (self.params['commission_rate'] + self.params['slippage_rate'])
  844. net_pnl = gross_pnl - close_commission
  845. # 更新资金(返还保证金 + 净盈亏)
  846. margin_returned = abs(short_position) * short_entry_price
  847. capital += margin_returned + net_pnl
  848. # 记录交易
  849. trade = {
  850. '交易方向': '做空',
  851. '开仓时间': short_entry_time,
  852. '平仓时间': current_time,
  853. '开仓价格': short_entry_price,
  854. '平仓价格': exit_price,
  855. '仓位': abs(short_position),
  856. '盈亏金额': net_pnl,
  857. '盈亏百分比': (short_entry_price - exit_price) / short_entry_price * 100,
  858. '退出原因': exit_reason,
  859. '持仓周期数': short_holding_bars,
  860. '持仓小时数': short_holding_bars * 0.5,
  861. '入场信号': short_entry_signals,
  862. '平仓时资金': capital,
  863. '开仓市值': abs(short_position) * short_entry_price,
  864. '保证金返还': margin_returned,
  865. '预计止损价格': short_stop_loss_price,
  866. '预计止盈价格': short_take_profit_price
  867. }
  868. trades.append(trade)
  869. # 打印平仓详情
  870. profit_ratio = (short_entry_price - exit_price) / short_entry_price * 100
  871. status = "[PROFIT]" if net_pnl > 0 else "[LOSS]"
  872. print(f"\n{'='*60}")
  873. print(f"{status} [SHORT_CLOSE] 做空平仓信号 #{len(trades)}")
  874. print(f"{'='*60}")
  875. print(f"平仓时间: {current_time}")
  876. print(f"平仓价格: {exit_price:.2f} 元")
  877. print(f"持仓时长: {short_holding_bars * 0.5:.1f} 小时 ({short_holding_bars} 个30分钟周期)")
  878. print(f"退出原因: {exit_reason}")
  879. print(f"{'-'*60}")
  880. print(f"价差盈亏: {gross_pnl:+,.2f} 元")
  881. print(f"平仓手续费: {close_commission:,.2f} 元")
  882. print(f"净盈亏: {net_pnl:+,.2f} 元")
  883. print(f"盈亏比例: {profit_ratio:+.2f}%")
  884. print(f"保证金返还: {margin_returned:,.2f} 元")
  885. print(f"{'-'*60}")
  886. print(f"当前资金: {capital:,.2f} 元")
  887. print(f"累计收益率: {(capital / self.initial_capital - 1) * 100:+.2f}%")
  888. print(f"{'='*60}")
  889. # 重置做空持仓
  890. short_position = 0
  891. short_entry_price = 0
  892. short_entry_time = None
  893. short_holding_bars = 0
  894. # 更新资金和持仓状态
  895. df.iloc[i, df.columns.get_loc('capital')] = capital
  896. df.iloc[i, df.columns.get_loc('long_position')] = long_position
  897. df.iloc[i, df.columns.get_loc('short_position')] = short_position
  898. # 强制平仓剩余持仓
  899. # 做多持仓强制平仓
  900. if long_position > 0:
  901. final_price = df.iloc[-1]['Close']
  902. gross_pnl = (final_price - long_entry_price) * long_position
  903. open_cost = long_position * long_entry_price * (self.params['commission_rate'] + self.params['slippage_rate'])
  904. close_revenue = long_position * final_price
  905. close_cost = close_revenue * (self.params['commission_rate'] + self.params['slippage_rate'])
  906. pnl = gross_pnl - open_cost - close_cost
  907. capital += close_revenue - close_cost
  908. trade = {
  909. '交易方向': '做多',
  910. '开仓时间': long_entry_time,
  911. '平仓时间': df.index[-1],
  912. '开仓价格': long_entry_price,
  913. '平仓价格': final_price,
  914. '仓位': long_position,
  915. '盈亏金额': pnl,
  916. '盈亏百分比': (final_price - long_entry_price) / long_entry_price * 100,
  917. '退出原因': f'做多强制平仓(回测结束,持仓{long_holding_bars}周期,最终价格{final_price:.2f},盈亏{(final_price - long_entry_price) / long_entry_price * 100:+.2f}%)',
  918. '持仓周期数': long_holding_bars,
  919. '持仓小时数': long_holding_bars * 0.5,
  920. '入场信号': long_entry_signals,
  921. '平仓时资金': capital,
  922. '开仓市值': long_position * long_entry_price,
  923. '预计止损价格': long_stop_loss_price,
  924. '预计止盈价格': long_take_profit_price
  925. }
  926. trades.append(trade)
  927. print(f"\n{'='*60}")
  928. print(f"[FORCE] [LONG_CLOSE] 做多强制平仓信号 #{len(trades)}")
  929. print(f"{'='*60}")
  930. print(f"平仓时间: {df.index[-1]}")
  931. print(f"平仓价格: {final_price:.2f} 元")
  932. print(f"退出原因: 做多强制平仓(回测结束)")
  933. print(f"盈亏金额: {pnl:+,.2f} 元")
  934. print(f"{'='*60}")
  935. # 更新最终资金到results_df
  936. df.iloc[-1, df.columns.get_loc('capital')] = capital
  937. df.iloc[-1, df.columns.get_loc('net_value')] = capital
  938. # 做空持仓强制平仓
  939. if short_position < 0:
  940. final_price = df.iloc[-1]['Close']
  941. gross_pnl = (short_entry_price - final_price) * abs(short_position)
  942. close_commission = abs(short_position) * final_price * (self.params['commission_rate'] + self.params['slippage_rate'])
  943. net_pnl = gross_pnl - close_commission
  944. margin_returned = abs(short_position) * short_entry_price
  945. capital += margin_returned + net_pnl
  946. trade = {
  947. '交易方向': '做空',
  948. '开仓时间': short_entry_time,
  949. '平仓时间': df.index[-1],
  950. '开仓价格': short_entry_price,
  951. '平仓价格': final_price,
  952. '仓位': abs(short_position),
  953. '盈亏金额': net_pnl,
  954. '盈亏百分比': (short_entry_price - final_price) / short_entry_price * 100,
  955. '退出原因': f'做空强制平仓(回测结束,持仓{short_holding_bars}周期,最终价格{final_price:.2f},盈亏{(short_entry_price - final_price) / short_entry_price * 100:+.2f}%)',
  956. '持仓周期数': short_holding_bars,
  957. '持仓小时数': short_holding_bars * 0.5,
  958. '入场信号': short_entry_signals,
  959. '平仓时资金': capital,
  960. '开仓市值': abs(short_position) * short_entry_price,
  961. '保证金返还': margin_returned,
  962. '预计止损价格': short_stop_loss_price,
  963. '预计止盈价格': short_take_profit_price
  964. }
  965. trades.append(trade)
  966. print(f"\n{'='*60}")
  967. print(f"[FORCE] [SHORT_CLOSE] 做空强制平仓信号 #{len(trades)}")
  968. print(f"{'='*60}")
  969. print(f"平仓时间: {df.index[-1]}")
  970. print(f"平仓价格: {final_price:.2f} 元")
  971. print(f"退出原因: 做空强制平仓(回测结束)")
  972. print(f"盈亏金额: {net_pnl:+,.2f} 元")
  973. print(f"{'='*60}")
  974. # 更新最终资金到results_df
  975. df.iloc[-1, df.columns.get_loc('capital')] = capital
  976. df.iloc[-1, df.columns.get_loc('net_value')] = capital
  977. trades_df = pd.DataFrame(trades)
  978. if len(trades_df) > 0:
  979. # 统一时间格式
  980. for col in trades_df.columns:
  981. if '时间' in col:
  982. trades_df[col] = pd.to_datetime(trades_df[col])
  983. trades_df = trades_df.sort_values('开仓时间')
  984. print(f"多空双向交易执行完成,共{len(trades_df)}笔交易")
  985. return df, trades_df
  986. # ==================== 验证分析模块 ====================
  987. def validate_dual_direction_results(results_df, trades_df, initial_capital):
  988. """验证多空双向交易结果"""
  989. print("\n" + "=" * 80)
  990. print("创业板50 30分钟多空双向交易结果验证")
  991. print("=" * 80)
  992. print(f"\n【基础数据验证】")
  993. final_capital = results_df['net_value'].iloc[-1]
  994. total_return = (final_capital - initial_capital) / initial_capital * 100
  995. print(f"初始资金: {initial_capital:,.2f}元")
  996. print(f"最终资金: {final_capital:,.2f}元")
  997. print(f"总收益率: {total_return:.2f}%")
  998. print(f"交易次数: {len(trades_df)}笔")
  999. if len(trades_df) > 0:
  1000. # 按交易方向分类统计
  1001. long_trades = trades_df[trades_df['交易方向'] == '做多']
  1002. short_trades = trades_df[trades_df['交易方向'] == '做空']
  1003. print(f"\n【交易方向统计】")
  1004. print(f"做多交易: {len(long_trades)}笔 ({len(long_trades)/len(trades_df)*100:.1f}%)")
  1005. print(f"做空交易: {len(short_trades)}笔 ({len(short_trades)/len(trades_df)*100:.1f}%)")
  1006. # 做多统计
  1007. if len(long_trades) > 0:
  1008. long_win_trades = long_trades[long_trades['盈亏金额'] > 0]
  1009. long_lose_trades = long_trades[long_trades['盈亏金额'] < 0]
  1010. long_total_pnl = long_trades['盈亏金额'].sum()
  1011. print(f"\n【做多交易统计】")
  1012. print(f"盈利交易: {len(long_win_trades)}笔 ({len(long_win_trades)/len(long_trades)*100:.1f}%)")
  1013. print(f"亏损交易: {len(long_lose_trades)}笔 ({len(long_lose_trades)/len(long_trades)*100:.1f}%)")
  1014. print(f"平均持仓时间: {long_trades['持仓小时数'].mean():.1f}小时")
  1015. print(f"平均收益率: {long_trades['盈亏百分比'].mean():.2f}%")
  1016. print(f"总盈亏: {long_total_pnl:+,.2f}元")
  1017. # 做空统计
  1018. if len(short_trades) > 0:
  1019. short_win_trades = short_trades[short_trades['盈亏金额'] > 0]
  1020. short_lose_trades = short_trades[short_trades['盈亏金额'] < 0]
  1021. short_total_pnl = short_trades['盈亏金额'].sum()
  1022. print(f"\n【做空交易统计】")
  1023. print(f"盈利交易: {len(short_win_trades)}笔 ({len(short_win_trades)/len(short_trades)*100:.1f}%)")
  1024. print(f"亏损交易: {len(short_lose_trades)}笔 ({len(short_lose_trades)/len(short_trades)*100:.1f}%)")
  1025. print(f"平均持仓时间: {short_trades['持仓小时数'].mean():.1f}小时")
  1026. print(f"平均收益率: {short_trades['盈亏百分比'].mean():.2f}%")
  1027. print(f"总盈亏: {short_total_pnl:+,.2f}元")
  1028. # 整体统计
  1029. win_trades = trades_df[trades_df['盈亏金额'] > 0]
  1030. lose_trades = trades_df[trades_df['盈亏金额'] < 0]
  1031. print(f"\n【整体交易统计】")
  1032. print(f"盈利交易: {len(win_trades)}笔 ({len(win_trades)/len(trades_df)*100:.1f}%)")
  1033. print(f"亏损交易: {len(lose_trades)}笔 ({len(lose_trades)/len(trades_df)*100:.1f}%)")
  1034. print(f"平均持仓时间: {trades_df['持仓小时数'].mean():.1f}小时")
  1035. print(f"平均收益率: {trades_df['盈亏百分比'].mean():.2f}%")
  1036. # 按退出原因统计
  1037. print(f"\n【退出原因统计】")
  1038. for reason, count in trades_df['退出原因'].value_counts().items():
  1039. percentage = count / len(trades_df) * 100
  1040. reason_pnl = trades_df[trades_df['退出原因'] == reason]['盈亏金额'].sum()
  1041. print(f" {reason}: {count}次 ({percentage:.1f}%) - 总盈亏: {reason_pnl:+,.2f}元")
  1042. # ==================== 主程序 ====================
  1043. def main():
  1044. """主程序 - 运行30分钟多空双向策略"""
  1045. print("=" * 80)
  1046. print("创业板50 30分钟多空双向策略")
  1047. print("=" * 80)
  1048. # 加载配置文件
  1049. config_manager = ConfigManager('config.json')
  1050. # 自动加载best_parameters.json覆盖默认参数
  1051. best_params_file = 'best_parameters.json'
  1052. if os.path.exists(best_params_file):
  1053. try:
  1054. with open(best_params_file, 'r', encoding='utf-8') as f:
  1055. best_data = json.load(f)
  1056. best_params = best_data.get('best_params', {})
  1057. strategy_keys = ['position_size_pct', 'stop_loss_pct', 'take_profit_pct', 'max_hold_bars']
  1058. for key in strategy_keys:
  1059. if key in best_params:
  1060. config_manager.config.setdefault('strategy', {})[key] = best_params[key]
  1061. print(f"已加载优化参数: {best_params_file}")
  1062. except Exception as e:
  1063. print(f"加载优化参数失败: {e},使用config.json默认参数")
  1064. # 从配置文件读取参数
  1065. BACKTEST_START_DATE = config_manager.get('strategy', 'backtest_start_date', "2025-10-01")
  1066. PREWARMP_DAYS = config_manager.get('strategy', 'prewamp_days', 30)
  1067. INITIAL_CAPITAL = config_manager.get('strategy', 'initial_capital', 1000000)
  1068. # 读取截止时间配置,支持"now"或具体日期
  1069. backtest_end_config = config_manager.get('strategy', 'backtest_end_date', "now")
  1070. if backtest_end_config.lower() == "now":
  1071. BACKTEST_END_DATE = datetime.now().strftime('%Y-%m-%d')
  1072. else:
  1073. BACKTEST_END_DATE = backtest_end_config
  1074. # 转换日期格式
  1075. start_date = datetime.strptime(BACKTEST_START_DATE, "%Y-%m-%d")
  1076. end_date = datetime.strptime(BACKTEST_END_DATE, "%Y-%m-%d").replace(hour=23, minute=59, second=59)
  1077. # 计算数据获取开始时间(回测开始时间 - 预热期)
  1078. data_start_date = start_date - timedelta(days=PREWARMP_DAYS)
  1079. # 显示数据源配置
  1080. use_local_file = config_manager.get('data_source', 'use_local_file', False)
  1081. data_source_mode = "本地文件模式" if use_local_file else "在线获取模式"
  1082. local_file_path = config_manager.get('data_source', 'local_file_path', '')
  1083. print(f"\n策略参数:")
  1084. print(f" 回测期间: {BACKTEST_START_DATE} 至 {BACKTEST_END_DATE}")
  1085. print(f" 数据获取期间: {data_start_date.strftime('%Y-%m-%d')} 至 {BACKTEST_END_DATE}")
  1086. print(f" 指标预热期: {PREWARMP_DAYS}天")
  1087. print(f" K线周期: 30分钟")
  1088. print(f" 初始资金: {INITIAL_CAPITAL:,}元")
  1089. print(f" 标的指数: 创业板50 (399673)")
  1090. print(f" 交易方向: 多空双向交易")
  1091. print(f" 数据源: {data_source_mode}")
  1092. if use_local_file:
  1093. print(f" 本地文件路径: {local_file_path}")
  1094. try:
  1095. # Phase 1: 数据获取
  1096. print(f"\n【Phase 1: 30分钟数据获取】")
  1097. fetcher = IntradayDataFetcher(config_manager)
  1098. # 获取包含预热期的完整数据
  1099. full_data = fetcher.fetch_30min_data(start_date=data_start_date, end_date=end_date)
  1100. full_data = fetcher.calculate_intraday_indicators(full_data)
  1101. # 筛选回测期间的数据
  1102. original_len = len(full_data)
  1103. backtest_data = full_data[(full_data.index >= start_date) & (full_data.index <= end_date)].copy()
  1104. print(f"筛选回测数据: {original_len} -> {len(backtest_data)} 条")
  1105. print(f"回测数据范围: {backtest_data.index[0]} 到 {backtest_data.index[-1]}")
  1106. # Phase 2: 信号生成
  1107. print(f"\n【Phase 2: 多空双向信号生成】")
  1108. signal_gen = DualDirectionSignalGenerator()
  1109. signals_df = signal_gen.generate_dual_direction_signals(backtest_data)
  1110. # Phase 3: 交易执行
  1111. print(f"\n【Phase 3: 多空双向交易执行】")
  1112. executor = DualDirectionExecutor(initial_capital=INITIAL_CAPITAL)
  1113. # 用配置参数覆盖默认值
  1114. executor.params['stop_loss_pct'] = config_manager.get('strategy', 'stop_loss_pct', 0.008)
  1115. executor.params['take_profit_pct'] = config_manager.get('strategy', 'take_profit_pct', 0.02)
  1116. executor.params['max_hold_bars'] = config_manager.get('strategy', 'max_hold_bars', 16)
  1117. executor.params['position_size_pct'] = config_manager.get('strategy', 'position_size_pct', 1.0)
  1118. results_df, trades_df = executor.execute_dual_direction_trades(signals_df)
  1119. # Phase 4: 验证分析
  1120. print(f"\n【Phase 4: 结果验证与分析】")
  1121. validate_dual_direction_results(results_df, trades_df, INITIAL_CAPITAL)
  1122. # Phase 5: 导出数据
  1123. if len(trades_df) > 0:
  1124. print(f"\n【Phase 5: 导出交易数据】")
  1125. # 统一时间格式
  1126. for col in trades_df.columns:
  1127. if '时间' in col:
  1128. trades_df[col] = pd.to_datetime(trades_df[col]).dt.strftime('%Y-%m-%d %H:%M:%S')
  1129. # 生成带时间戳的文件名
  1130. timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
  1131. output_file = f'cyb50_30min_dual_direction_trades_{timestamp}.csv'
  1132. trades_df.to_csv(output_file, index=False, encoding='utf-8-sig')
  1133. print(f"多空双向交易记录已保存到: {output_file}")
  1134. print(f"时间戳格式: YYYY-MM-DD HH:MM:SS")
  1135. # 策略总结
  1136. print(f"\n" + "=" * 80)
  1137. print("多空双向策略运行总结")
  1138. print("=" * 80)
  1139. if len(trades_df) > 0:
  1140. final_capital = results_df['net_value'].iloc[-1]
  1141. total_return = (final_capital - INITIAL_CAPITAL) / INITIAL_CAPITAL * 100
  1142. # 按方向统计
  1143. long_trades = trades_df[trades_df['交易方向'] == '做多']
  1144. short_trades = trades_df[trades_df['交易方向'] == '做空']
  1145. print(f"初始资金: {INITIAL_CAPITAL:,.2f}元")
  1146. print(f"最终资金: {final_capital:,.2f}元")
  1147. print(f"总收益率: {total_return:.2f}%")
  1148. print(f"总交易次数: {len(trades_df)}笔")
  1149. print(f"做多交易: {len(long_trades)}笔")
  1150. print(f"做空交易: {len(short_trades)}笔")
  1151. print(f"整体胜率: {(trades_df['盈亏金额'] > 0).sum() / len(trades_df) * 100:.1f}%")
  1152. print(f"平均收益率: {trades_df['盈亏百分比'].mean():.2f}%")
  1153. print(f"最大单笔盈利: {trades_df['盈亏金额'].max():+,.2f}元")
  1154. print(f"最大单笔亏损: {trades_df['盈亏金额'].min():+,.2f}元")
  1155. print(f"\n[SUCCESS] 多空双向策略运行成功!")
  1156. else:
  1157. print("未产生任何交易信号")
  1158. except Exception as e:
  1159. print(f"\n[ERROR] 多空双向策略运行出错: {str(e)}")
  1160. import traceback
  1161. traceback.print_exc()
  1162. finally:
  1163. print(f"\n" + "=" * 80)
  1164. if __name__ == "__main__":
  1165. main()