trading_training_tool.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937
  1. # 交易训练工具
  2. # 用于从交易记录CSV文件中随机选择交易,显示部分K线图让用户判断是否开仓,然后显示完整结果并记录
  3. from jqdata import *
  4. import pandas as pd
  5. import numpy as np
  6. import matplotlib.pyplot as plt
  7. import matplotlib.patches as patches
  8. from datetime import datetime, timedelta, date
  9. import re
  10. import os
  11. import random
  12. import warnings
  13. warnings.filterwarnings('ignore')
  14. # 中文字体设置
  15. plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'DejaVu Sans']
  16. plt.rcParams['axes.unicode_minus'] = False
  17. # ========== 参数配置区域(硬编码参数都在这里) ==========
  18. CONFIG = {
  19. # CSV文件名
  20. 'csv_filename': 'transaction1.csv',
  21. # 结果记录文件名
  22. 'result_filename': 'training_results.csv',
  23. # 历史数据天数
  24. 'history_days': 100,
  25. # 未来数据天数
  26. 'future_days': 20,
  27. # 输出目录
  28. 'output_dir': 'training_images',
  29. # 是否显示图片(在某些环境下可能需要设置为False)
  30. 'show_plots': True,
  31. # 图片DPI
  32. 'plot_dpi': 150,
  33. # 随机种子(设置为None表示不固定种子)
  34. 'random_seed': 42
  35. }
  36. # =====================================================
  37. def _get_current_directory():
  38. """
  39. 获取当前文件所在目录
  40. """
  41. try:
  42. current_dir = os.path.dirname(os.path.abspath(__file__))
  43. except NameError:
  44. current_dir = os.getcwd()
  45. if not os.path.exists(os.path.join(current_dir, CONFIG['csv_filename'])):
  46. parent_dir = os.path.dirname(current_dir)
  47. future_dir = os.path.join(parent_dir, 'future')
  48. if os.path.exists(os.path.join(future_dir, CONFIG['csv_filename'])):
  49. current_dir = future_dir
  50. return current_dir
  51. def read_transaction_data(csv_path):
  52. """
  53. 读取交易记录CSV文件
  54. """
  55. encodings = ['utf-8-sig', 'utf-8', 'gbk', 'gb2312', 'gb18030', 'latin1']
  56. for encoding in encodings:
  57. try:
  58. df = pd.read_csv(csv_path, encoding=encoding)
  59. print(f"成功使用 {encoding} 编码读取CSV文件,共 {len(df)} 条记录")
  60. return df
  61. except UnicodeDecodeError:
  62. continue
  63. except Exception as e:
  64. if encoding == encodings[-1]:
  65. print(f"读取CSV文件时出错: {str(e)}")
  66. raise
  67. return pd.DataFrame()
  68. def extract_contract_info(row):
  69. """
  70. 从交易记录中提取合约信息和交易信息
  71. """
  72. try:
  73. # 提取合约编号
  74. target_str = str(row['标的'])
  75. match = re.search(r'\(([^)]+)\)', target_str)
  76. if match:
  77. contract_code = match.group(1)
  78. else:
  79. return None, None, None, None, None, None, None, None, None
  80. # 提取日期
  81. date_str = str(row['日期']).strip()
  82. date_formats = ['%Y-%m-%d', '%d/%m/%Y', '%Y/%m/%d', '%d-%m-%Y', '%Y%m%d']
  83. trade_date = None
  84. for date_format in date_formats:
  85. try:
  86. trade_date = datetime.strptime(date_str, date_format).date()
  87. break
  88. except ValueError:
  89. continue
  90. if trade_date is None:
  91. return None, None, None, None, None, None, None, None, None
  92. # 提取委托时间
  93. order_time_str = str(row['委托时间']).strip()
  94. try:
  95. time_parts = order_time_str.split(':')
  96. hour = int(time_parts[0])
  97. # 如果委托时间 >= 21:00,需要找到下一个交易日
  98. if hour >= 21:
  99. try:
  100. trade_days = get_trade_days(start_date=trade_date, count=2)
  101. if len(trade_days) >= 2:
  102. next_trade_day = trade_days[1]
  103. if isinstance(next_trade_day, datetime):
  104. actual_trade_date = next_trade_day.date()
  105. elif isinstance(next_trade_day, date):
  106. actual_trade_date = next_trade_day
  107. else:
  108. actual_trade_date = trade_date
  109. else:
  110. actual_trade_date = trade_date
  111. except:
  112. actual_trade_date = trade_date
  113. else:
  114. actual_trade_date = trade_date
  115. except:
  116. actual_trade_date = trade_date
  117. # 提取成交价
  118. try:
  119. trade_price = float(row['成交价'])
  120. except:
  121. return None, None, None, None, None, None, None, None, None
  122. # 提取交易方向和类型
  123. trade_type = str(row['交易类型']).strip()
  124. if '开多' in trade_type:
  125. direction = 'long'
  126. action = 'open'
  127. elif '开空' in trade_type:
  128. direction = 'short'
  129. action = 'open'
  130. elif '平多' in trade_type or '平空' in trade_type:
  131. action = 'close'
  132. direction = 'long' if '平多' in trade_type else 'short'
  133. else:
  134. return None, None, None, None, None, None, None, None, None
  135. # 提取交易对ID和连续交易对ID
  136. trade_pair_id = row.get('交易对ID', 'N/A')
  137. continuous_pair_id = row.get('连续交易对ID', 'N/A')
  138. return contract_code, actual_trade_date, trade_price, direction, action, order_time_str, trade_type, trade_pair_id, continuous_pair_id
  139. except Exception as e:
  140. print(f"提取合约信息时出错: {str(e)}")
  141. return None, None, None, None, None, None, None, None, None
  142. def get_trade_day_range(trade_date, days_before, days_after):
  143. """
  144. 获取交易日范围
  145. """
  146. try:
  147. # 获取历史交易日
  148. trade_days_before = get_trade_days(end_date=trade_date, count=days_before + 1)
  149. if len(trade_days_before) < days_before + 1:
  150. return None, None
  151. first_day = trade_days_before[0]
  152. if isinstance(first_day, datetime):
  153. start_date = first_day.date()
  154. elif isinstance(first_day, date):
  155. start_date = first_day
  156. else:
  157. start_date = first_day
  158. # 获取未来交易日
  159. trade_days_after = get_trade_days(start_date=trade_date, count=days_after + 1)
  160. if len(trade_days_after) < days_after + 1:
  161. return None, None
  162. last_day = trade_days_after[-1]
  163. if isinstance(last_day, datetime):
  164. end_date = last_day.date()
  165. elif isinstance(last_day, date):
  166. end_date = last_day
  167. else:
  168. end_date = last_day
  169. return start_date, end_date
  170. except Exception as e:
  171. print(f"计算交易日范围时出错: {str(e)}")
  172. return None, None
  173. def get_kline_data_with_future(contract_code, trade_date, days_before=100, days_after=20):
  174. """
  175. 获取包含历史和未来数据的K线数据
  176. """
  177. try:
  178. # 获取完整的数据范围
  179. start_date, end_date = get_trade_day_range(trade_date, days_before, days_after)
  180. if start_date is None or end_date is None:
  181. return None, None
  182. # 获取K线数据
  183. price_data = get_price(
  184. contract_code,
  185. start_date=start_date,
  186. end_date=end_date,
  187. frequency='1d',
  188. fields=['open', 'close', 'high', 'low']
  189. )
  190. if price_data is None or len(price_data) == 0:
  191. return None, None
  192. # 若该标的存在开收高低完全一致的日期,则视为异常并直接过滤
  193. same_all_mask = (
  194. (price_data['close'] == price_data['open']) &
  195. (price_data['close'] == price_data['high']) &
  196. (price_data['close'] == price_data['low'])
  197. )
  198. if same_all_mask.any():
  199. print(f"{contract_code} 数据异常:存在开盘、收盘、最高、最低完全一致的交易日,跳过该标的。")
  200. return None, None
  201. # 计算均线
  202. price_data['ma5'] = price_data['close'].rolling(window=5).mean()
  203. price_data['ma10'] = price_data['close'].rolling(window=10).mean()
  204. price_data['ma20'] = price_data['close'].rolling(window=20).mean()
  205. price_data['ma30'] = price_data['close'].rolling(window=30).mean()
  206. # 找到交易日在数据中的位置
  207. trade_date_normalized = pd.Timestamp(trade_date)
  208. trade_idx = None
  209. for i, idx in enumerate(price_data.index):
  210. if isinstance(idx, pd.Timestamp):
  211. if idx.date() == trade_date:
  212. trade_idx = i
  213. break
  214. return price_data, trade_idx
  215. except Exception as e:
  216. print(f"获取K线数据时出错: {str(e)}")
  217. return None, None
  218. def plot_partial_kline(data, trade_idx, trade_price, direction, contract_code, trade_date, order_time, save_path=None):
  219. # contract_code 参数保留用于可能的扩展功能
  220. """
  221. 绘制部分K线图(仅显示历史数据和当天)
  222. """
  223. try:
  224. # 截取历史数据和当天数据
  225. partial_data = data.iloc[:trade_idx + 1].copy()
  226. # 根据交易方向修改当天的价格数据
  227. if direction == 'long':
  228. # 做多时,用成交价替代最高价(表示买入点)
  229. partial_data.iloc[-1, partial_data.columns.get_loc('close')] = trade_price
  230. partial_data.iloc[-1, partial_data.columns.get_loc('high')] = trade_price
  231. else:
  232. # 做空时,用成交价替代最低价(表示卖出点)
  233. partial_data.iloc[-1, partial_data.columns.get_loc('close')] = trade_price
  234. partial_data.iloc[-1, partial_data.columns.get_loc('low')] = trade_price
  235. fig, ax = plt.subplots(figsize=(16, 10))
  236. # 准备数据
  237. dates = partial_data.index
  238. opens = partial_data['open']
  239. highs = partial_data['high']
  240. lows = partial_data['low']
  241. closes = partial_data['close']
  242. # 绘制K线
  243. for i in range(len(partial_data)):
  244. # 检查是否是交易日
  245. is_trade_day = (i == trade_idx)
  246. if is_trade_day:
  247. # 成交日根据涨跌用不同颜色
  248. if closes.iloc[i] > opens.iloc[i]: # 上涨
  249. color = '#FFD700' # 金黄色(黄红色混合)
  250. edge_color = '#FF8C00' # 深橙色
  251. else: # 下跌
  252. color = '#ADFF2F' # 黄绿色
  253. edge_color = '#9ACD32' # 黄绿色深版
  254. else:
  255. # 正常K线颜色
  256. color = 'red' if closes.iloc[i] > opens.iloc[i] else 'green'
  257. edge_color = 'darkred' if closes.iloc[i] > opens.iloc[i] else 'darkgreen'
  258. # 影线
  259. ax.plot([i, i], [lows.iloc[i], highs.iloc[i]], color='black', linewidth=1)
  260. # 实体
  261. body_height = abs(closes.iloc[i] - opens.iloc[i])
  262. if body_height == 0:
  263. body_height = 0.01
  264. bottom = min(opens.iloc[i], closes.iloc[i])
  265. rect = patches.Rectangle((i-0.4, bottom), 0.8, body_height,
  266. linewidth=1, edgecolor=edge_color,
  267. facecolor=color, alpha=0.8)
  268. ax.add_patch(rect)
  269. # 绘制均线
  270. ax.plot(range(len(partial_data)), partial_data['ma5'], label='MA5', color='blue', linewidth=1.5, alpha=0.8)
  271. ax.plot(range(len(partial_data)), partial_data['ma10'], label='MA10', color='orange', linewidth=1.5, alpha=0.8)
  272. ax.plot(range(len(partial_data)), partial_data['ma20'], label='MA20', color='purple', linewidth=1.5, alpha=0.8)
  273. ax.plot(range(len(partial_data)), partial_data['ma30'], label='MA30', color='brown', linewidth=1.5, alpha=0.8)
  274. # 获取当天的最高价(用于画连接线)
  275. day_high = highs.iloc[trade_idx]
  276. # 标注信息
  277. date_label = trade_date.strftime('%Y-%m-%d')
  278. price_label = f'Price: {trade_price:.2f}'
  279. direction_label = f'Direction: {"Long" if direction == "long" else "Short"}'
  280. time_label = f'Time: {order_time}'
  281. # 将文本框移到左上角
  282. annotation_text = f'{date_label}\n{price_label}\n{direction_label}\n{time_label}'
  283. text_box = ax.text(0.02, 0.98, annotation_text,
  284. fontsize=10, ha='left', va='top', transform=ax.transAxes,
  285. bbox=dict(boxstyle='round,pad=0.6', facecolor='yellow', alpha=0.9, edgecolor='black', linewidth=1.5),
  286. zorder=11, weight='bold')
  287. # 画黄色虚线连接文本框底部和交易日最高价
  288. # 获取文本框在数据坐标系中的位置
  289. fig.canvas.draw() # 需要先绘制一次才能获取准确位置
  290. bbox = text_box.get_window_extent().transformed(ax.transData.inverted())
  291. text_bottom_y = bbox.ymin
  292. # 从文本框底部到交易日最高价画虚线
  293. ax.plot([trade_idx, trade_idx], [day_high, text_bottom_y],
  294. color='yellow', linestyle='--', linewidth=1.5, alpha=0.7, zorder=5)
  295. # 设置标题和标签
  296. direction_text = "Long" if direction == "long" else "Short"
  297. ax.set_title(f'{direction_text} Position Decision\n'
  298. f'Historical Data + Trade Day Only',
  299. fontsize=14, fontweight='bold', pad=20)
  300. ax.set_xlabel('Time', fontsize=12)
  301. ax.set_ylabel('Price', fontsize=12)
  302. ax.grid(True, alpha=0.3)
  303. ax.legend(loc='lower left', fontsize=10)
  304. # 设置x轴标签
  305. step = max(1, len(partial_data) // 10)
  306. tick_positions = range(0, len(partial_data), step)
  307. tick_labels = []
  308. for pos in tick_positions:
  309. date_val = dates[pos]
  310. if isinstance(date_val, (date, datetime)):
  311. tick_labels.append(date_val.strftime('%Y-%m-%d'))
  312. else:
  313. tick_labels.append(str(date_val))
  314. ax.set_xticks(tick_positions)
  315. ax.set_xticklabels(tick_labels, rotation=45, ha='right')
  316. plt.tight_layout()
  317. if save_path:
  318. plt.savefig(save_path, dpi=CONFIG['plot_dpi'], bbox_inches='tight')
  319. if CONFIG['show_plots']:
  320. plt.show()
  321. plt.close(fig)
  322. except Exception as e:
  323. print(f"绘制部分K线图时出错: {str(e)}")
  324. plt.close('all')
  325. raise
  326. def plot_full_kline(data, trade_idx, trade_price, direction, contract_code, trade_date, order_time, profit_loss, save_path=None):
  327. """
  328. 绘制完整K线图(包含未来数据)
  329. """
  330. try:
  331. fig, ax = plt.subplots(figsize=(16, 10))
  332. # 准备数据
  333. dates = data.index
  334. opens = data['open']
  335. highs = data['high']
  336. lows = data['low']
  337. closes = data['close']
  338. # 绘制K线
  339. for i in range(len(data)):
  340. # 检查是否是交易日
  341. is_trade_day = (i == trade_idx)
  342. if is_trade_day:
  343. # 成交日根据涨跌用不同颜色
  344. if closes.iloc[i] > opens.iloc[i]: # 上涨
  345. color = '#FFD700' # 金黄色(黄红色混合)
  346. edge_color = '#FF8C00' # 深橙色
  347. else: # 下跌
  348. color = '#ADFF2F' # 黄绿色
  349. edge_color = '#9ACD32' # 黄绿色深版
  350. else:
  351. # 正常K线颜色
  352. color = 'red' if closes.iloc[i] > opens.iloc[i] else 'green'
  353. edge_color = 'darkred' if closes.iloc[i] > opens.iloc[i] else 'darkgreen'
  354. # 影线
  355. ax.plot([i, i], [lows.iloc[i], highs.iloc[i]], color='black', linewidth=1)
  356. # 实体
  357. body_height = abs(closes.iloc[i] - opens.iloc[i])
  358. if body_height == 0:
  359. body_height = 0.01
  360. bottom = min(opens.iloc[i], closes.iloc[i])
  361. rect = patches.Rectangle((i-0.4, bottom), 0.8, body_height,
  362. linewidth=1, edgecolor=edge_color,
  363. facecolor=color, alpha=0.8)
  364. ax.add_patch(rect)
  365. # 绘制均线
  366. ax.plot(range(len(data)), data['ma5'], label='MA5', color='blue', linewidth=1.5, alpha=0.8)
  367. ax.plot(range(len(data)), data['ma10'], label='MA10', color='orange', linewidth=1.5, alpha=0.8)
  368. ax.plot(range(len(data)), data['ma20'], label='MA20', color='purple', linewidth=1.5, alpha=0.8)
  369. ax.plot(range(len(data)), data['ma30'], label='MA30', color='brown', linewidth=1.5, alpha=0.8)
  370. # 获取当天的最高价(用于画连接线)
  371. day_high = highs.iloc[trade_idx]
  372. # 添加未来区域背景
  373. ax.axvspan(trade_idx + 0.5, len(data) - 0.5, alpha=0.1, color='gray', label='Future Data')
  374. # 标注信息
  375. date_label = trade_date.strftime('%Y-%m-%d')
  376. price_label = f'Price: {trade_price:.2f}'
  377. direction_label = f'Direction: {"Long" if direction == "long" else "Short"}'
  378. time_label = f'Time: {order_time}'
  379. profit_label = f'P&L: {profit_loss:+.2f}'
  380. # 将文本框移到左上角
  381. annotation_text = f'{date_label}\n{price_label}\n{direction_label}\n{time_label}\n{profit_label}'
  382. text_box = ax.text(0.02, 0.98, annotation_text,
  383. fontsize=10, ha='left', va='top', transform=ax.transAxes,
  384. bbox=dict(boxstyle='round,pad=0.6', facecolor='yellow', alpha=0.9, edgecolor='black', linewidth=1.5),
  385. zorder=11, weight='bold')
  386. # 画黄色虚线连接文本框底部和交易日最高价
  387. # 获取文本框在数据坐标系中的位置
  388. fig.canvas.draw() # 需要先绘制一次才能获取准确位置
  389. bbox = text_box.get_window_extent().transformed(ax.transData.inverted())
  390. text_bottom_y = bbox.ymin
  391. # 从文本框底部到交易日最高价画虚线
  392. ax.plot([trade_idx, trade_idx], [day_high, text_bottom_y],
  393. color='yellow', linestyle='--', linewidth=1.5, alpha=0.7, zorder=5)
  394. # 设置标题和标签
  395. contract_simple = contract_code.split('.')[0]
  396. direction_text = "Long" if direction == "long" else "Short"
  397. ax.set_title(f'{contract_simple} - {direction_text} Position Result\n'
  398. f'Complete Data with Future {CONFIG["future_days"]} Days',
  399. fontsize=14, fontweight='bold', pad=20)
  400. ax.set_xlabel('Time', fontsize=12)
  401. ax.set_ylabel('Price', fontsize=12)
  402. ax.grid(True, alpha=0.3)
  403. ax.legend(loc='lower left', fontsize=10)
  404. # 设置x轴标签
  405. step = max(1, len(data) // 15)
  406. tick_positions = range(0, len(data), step)
  407. tick_labels = []
  408. for pos in tick_positions:
  409. date_val = dates[pos]
  410. if isinstance(date_val, (date, datetime)):
  411. tick_labels.append(date_val.strftime('%Y-%m-%d'))
  412. else:
  413. tick_labels.append(str(date_val))
  414. ax.set_xticks(tick_positions)
  415. ax.set_xticklabels(tick_labels, rotation=45, ha='right')
  416. plt.tight_layout()
  417. if save_path:
  418. plt.savefig(save_path, dpi=CONFIG['plot_dpi'], bbox_inches='tight')
  419. if CONFIG['show_plots']:
  420. plt.show()
  421. plt.close(fig)
  422. except Exception as e:
  423. print(f"绘制完整K线图时出错: {str(e)}")
  424. plt.close('all')
  425. raise
  426. def load_processed_results(result_path):
  427. """
  428. 加载已处理的结果文件
  429. """
  430. if not os.path.exists(result_path):
  431. return pd.DataFrame(), set()
  432. try:
  433. # 简单读取CSV文件
  434. df = pd.read_csv(result_path, header=0)
  435. # 确保必要的列存在
  436. required_columns = ['交易对ID']
  437. for col in required_columns:
  438. if col not in df.columns:
  439. print(f"警告:结果文件缺少必要列 '{col}'")
  440. return pd.DataFrame(), set()
  441. # 获取已处理的交易对ID
  442. processed_pairs = set(df['交易对ID'].dropna().unique())
  443. return df, processed_pairs
  444. except Exception as e:
  445. # 详细打印错误信息
  446. print(f"加载结果文件时出错: {str(e)}")
  447. print(f"错误类型: {type(e)}")
  448. return pd.DataFrame(), set()
  449. def calculate_profit_loss(df, trade_pair_id, continuous_pair_id):
  450. """
  451. 计算平仓盈亏
  452. """
  453. try:
  454. if continuous_pair_id != 'N/A' and pd.notna(continuous_pair_id):
  455. # 合并所有同一连续交易对ID的平仓盈亏
  456. close_trades = df[
  457. (df['连续交易对ID'] == continuous_pair_id) &
  458. (df['交易类型'].str[0] == '平')
  459. ]
  460. total_profit = close_trades['平仓盈亏'].sum()
  461. else:
  462. # 只查找当前交易对ID的平仓交易
  463. close_trades = df[
  464. (df['交易对ID'] == trade_pair_id) &
  465. (df['交易类型'].str[0] == '平')
  466. ]
  467. if len(close_trades) > 0:
  468. total_profit = close_trades['平仓盈亏'].iloc[0]
  469. else:
  470. total_profit = 0
  471. return total_profit
  472. except Exception as e:
  473. print(f"计算盈亏时出错: {str(e)}")
  474. return 0
  475. def record_result(result_data, result_path):
  476. """
  477. 记录训练结果
  478. """
  479. try:
  480. # 创建结果DataFrame
  481. result_df = pd.DataFrame([result_data])
  482. # 如果文件已存在,读取现有格式并确保新数据格式一致
  483. if os.path.exists(result_path):
  484. try:
  485. # 读取现有文件的列名
  486. existing_df = pd.read_csv(result_path, nrows=0) # 只读取列名
  487. existing_columns = existing_df.columns.tolist()
  488. # 如果新数据列与现有文件不一致,调整格式
  489. if list(result_df.columns) != existing_columns:
  490. # 重新创建DataFrame,确保列顺序一致
  491. aligned_data = {}
  492. for col in existing_columns:
  493. aligned_data[col] = result_data.get(col, 'N/A' if col == '连续交易总盈亏' else '')
  494. result_df = pd.DataFrame([aligned_data])
  495. # 追加写入
  496. result_df.to_csv(result_path, mode='a', header=False, index=False, encoding='utf-8-sig')
  497. except Exception:
  498. # 如果无法读取现有格式,直接覆盖
  499. result_df.to_csv(result_path, mode='w', header=True, index=False, encoding='utf-8-sig')
  500. else:
  501. # 文件不存在,创建新文件
  502. result_df.to_csv(result_path, mode='w', header=True, index=False, encoding='utf-8-sig')
  503. print(f"结果已记录到: {result_path}")
  504. except Exception as e:
  505. print(f"记录结果时出错: {str(e)}")
  506. def is_first_continuous_trade(transaction_df, trade_pair_id, continuous_pair_id):
  507. """
  508. 判断是否为连续交易的第一笔交易
  509. 参数:
  510. transaction_df: 交易数据DataFrame
  511. trade_pair_id: 当前交易对ID
  512. continuous_pair_id: 连续交易对ID
  513. 返回:
  514. bool: 是否为连续交易的第一笔交易(或不是连续交易)
  515. """
  516. # 如果不是连续交易,返回True
  517. if continuous_pair_id == 'N/A' or pd.isna(continuous_pair_id):
  518. return True
  519. # 获取同一连续交易组的所有交易
  520. continuous_trades = transaction_df[transaction_df['连续交易对ID'] == continuous_pair_id]
  521. # 获取所有交易对ID并按时间排序
  522. pair_ids = continuous_trades['交易对ID'].unique()
  523. # 获取每个交易对的开仓时间
  524. pair_times = []
  525. for pid in pair_ids:
  526. pair_records = continuous_trades[continuous_trades['交易对ID'] == pid]
  527. open_records = pair_records[pair_records['交易类型'].str.contains('开', na=False)]
  528. if len(open_records) > 0:
  529. # 获取第一个开仓记录的日期和时间
  530. first_open = open_records.iloc[0]
  531. date_str = str(first_open['日期']).strip()
  532. time_str = str(first_open['委托时间']).strip()
  533. try:
  534. dt = pd.to_datetime(f"{date_str} {time_str}")
  535. pair_times.append((pid, dt))
  536. except:
  537. pass
  538. # 按时间排序
  539. pair_times.sort(key=lambda x: x[1])
  540. # 检查当前交易对是否为第一个
  541. if pair_times and pair_times[0][0] == trade_pair_id:
  542. return True
  543. return False
  544. def get_user_decision():
  545. """
  546. 获取用户的开仓决策和信心指数
  547. 返回:
  548. tuple: (是否开仓, 信心指数)
  549. - 是否开仓: bool
  550. - 信心指数: int (1-3)
  551. """
  552. while True:
  553. decision = input("\n是否开仓?请输入 'y,信心指数' (开仓) 或 'n,信心指数' (不开仓)\n" +
  554. "例如: 'y,3' (开仓,高信心) 或 'n,1' (不开仓,低信心)\n" +
  555. "信心指数: 1=低, 2=中, 3=高 (默认为2): ").strip().lower()
  556. # 解析输入
  557. parts = decision.split(',')
  558. decision_part = parts[0].strip()
  559. confidence = 2 # 默认信心指数
  560. # 检查是否提供了信心指数
  561. if len(parts) >= 2:
  562. try:
  563. confidence = int(parts[1].strip())
  564. if confidence not in [1, 2, 3]:
  565. print("信心指数必须是 1、2 或 3,请重新输入")
  566. continue
  567. except ValueError:
  568. print("信心指数必须是数字 1、2 或 3,请重新输入")
  569. continue
  570. # 检查开仓决策
  571. if decision_part in ['y', 'yes', '是', '开仓']:
  572. return True, confidence
  573. elif decision_part in ['n', 'no', '否', '不开仓']:
  574. return False, confidence
  575. else:
  576. print("请输入有效的选项: 'y' 或 'n' (可选择性添加信心指数,如 'y,3')")
  577. def main():
  578. """
  579. 主函数
  580. """
  581. print("=" * 60)
  582. print("交易训练工具")
  583. print("=" * 60)
  584. # 设置随机种子
  585. if CONFIG['random_seed'] is not None:
  586. random.seed(CONFIG['random_seed'])
  587. np.random.seed(CONFIG['random_seed'])
  588. # 获取当前目录
  589. current_dir = _get_current_directory()
  590. csv_path = os.path.join(current_dir, CONFIG['csv_filename'])
  591. result_path = os.path.join(current_dir, CONFIG['result_filename'])
  592. output_dir = os.path.join(current_dir, CONFIG['output_dir'])
  593. # 创建输出目录
  594. os.makedirs(output_dir, exist_ok=True)
  595. # 1. 读取交易数据
  596. print("\n=== 步骤1: 读取交易数据 ===")
  597. transaction_df = read_transaction_data(csv_path)
  598. if len(transaction_df) == 0:
  599. print("未能读取交易数据,退出")
  600. return
  601. # 2. 加载已处理的结果
  602. print("\n=== 步骤2: 加载已处理记录 ===")
  603. _, processed_pairs = load_processed_results(result_path)
  604. print(f"已处理 {len(processed_pairs)} 个交易对")
  605. # 3. 提取所有开仓交易
  606. print("\n=== 步骤3: 提取开仓交易 ===")
  607. open_trades = []
  608. for idx, row in transaction_df.iterrows():
  609. contract_code, trade_date, trade_price, direction, action, order_time, trade_type, trade_pair_id, continuous_pair_id = extract_contract_info(row)
  610. if contract_code is None or action != 'open':
  611. continue
  612. # 跳过已处理的交易对
  613. if trade_pair_id in processed_pairs:
  614. continue
  615. # 检查是否为连续交易的第一笔交易(如果不是第一笔,跳过)
  616. if not is_first_continuous_trade(transaction_df, trade_pair_id, continuous_pair_id):
  617. continue
  618. # 查找对应的平仓交易
  619. profit_loss = calculate_profit_loss(transaction_df, trade_pair_id, continuous_pair_id)
  620. # 如果是连续交易,获取连续交易总盈亏
  621. continuous_total_profit = 'N/A'
  622. if continuous_pair_id != 'N/A' and pd.notna(continuous_pair_id):
  623. continuous_trades = transaction_df[transaction_df['连续交易对ID'] == continuous_pair_id]
  624. try:
  625. close_profit_loss_str = continuous_trades['平仓盈亏'].astype(str).str.replace(',', '')
  626. close_profit_loss_numeric = pd.to_numeric(close_profit_loss_str, errors='coerce').fillna(0)
  627. continuous_total_profit = close_profit_loss_numeric.sum()
  628. except:
  629. continuous_total_profit = 0
  630. open_trades.append({
  631. 'index': idx,
  632. 'contract_code': contract_code,
  633. 'trade_date': trade_date,
  634. 'trade_price': trade_price,
  635. 'direction': direction,
  636. 'order_time': order_time,
  637. 'trade_type': trade_type,
  638. 'trade_pair_id': trade_pair_id,
  639. 'continuous_pair_id': continuous_pair_id,
  640. 'profit_loss': profit_loss,
  641. 'continuous_total_profit': continuous_total_profit,
  642. 'original_row': row
  643. })
  644. print(f"找到 {len(open_trades)} 个未处理的开仓交易(已过滤非首笔连续交易)")
  645. if len(open_trades) == 0:
  646. print("没有未处理的开仓交易,退出")
  647. return
  648. # 4. 构建候选交易列表(按标的类型分组轮询,避免同类集中)
  649. print("\n=== 步骤4: 构建候选交易列表 ===")
  650. # 按标的类型分组(提取合约代码的核心字母部分)
  651. def get_contract_type(contract_code):
  652. """提取合约类型,如'M2405'提取为'M','AG2406'提取为'AG'"""
  653. import re
  654. match = re.match(r'^([A-Za-z]+)', contract_code.split('.')[0])
  655. return match.group(1) if match else 'UNKNOWN'
  656. # 按合约类型分组
  657. trades_by_type = {}
  658. for trade in open_trades:
  659. contract_type = get_contract_type(trade['contract_code'])
  660. if contract_type not in trades_by_type:
  661. trades_by_type[contract_type] = []
  662. trades_by_type[contract_type].append(trade)
  663. # 打乱每个组内的顺序
  664. for contract_type in trades_by_type:
  665. random.shuffle(trades_by_type[contract_type])
  666. def build_trade_queue(trade_groups):
  667. type_order = list(trade_groups.keys())
  668. random.shuffle(type_order)
  669. queue = []
  670. while True:
  671. added = False
  672. for contract_type in type_order:
  673. if trade_groups[contract_type]:
  674. queue.append(trade_groups[contract_type].pop(0))
  675. added = True
  676. if not added:
  677. break
  678. return queue
  679. trade_queue = build_trade_queue(trades_by_type)
  680. print(f"候选交易数量: {len(trade_queue)}")
  681. # 5. 依次尝试获取K线数据,若失败则自动尝试下一候选
  682. print("\n=== 步骤5: 获取K线数据 ===")
  683. selected_trade = None
  684. kline_data = None
  685. trade_idx = None
  686. for i, candidate_trade in enumerate(trade_queue):
  687. print(f"尝试候选 {i + 1}/{len(trade_queue)}: {candidate_trade['contract_code']} {candidate_trade['trade_date']}")
  688. kline_data, trade_idx = get_kline_data_with_future(
  689. candidate_trade['contract_code'],
  690. candidate_trade['trade_date'],
  691. CONFIG['history_days'],
  692. CONFIG['future_days']
  693. )
  694. if kline_data is None or trade_idx is None:
  695. print("获取K线数据失败,尝试下一个候选。")
  696. continue
  697. selected_trade = candidate_trade
  698. remaining = len(trade_queue) - (i + 1)
  699. print(f"成功获取K线数据,剩余候选 {remaining} 个")
  700. break
  701. if selected_trade is None:
  702. print("所有候选交易均无法获取有效K线数据,退出")
  703. return
  704. # 6. 显示部分K线图
  705. print("\n=== 步骤6: 显示部分K线图 ===")
  706. partial_image_name = f"partial_{selected_trade['contract_code']}_{selected_trade['trade_date']}_{selected_trade['direction']}.png"
  707. partial_image_path = os.path.join(output_dir, partial_image_name)
  708. plot_partial_kline(
  709. kline_data, trade_idx, selected_trade['trade_price'],
  710. selected_trade['direction'], selected_trade['contract_code'],
  711. selected_trade['trade_date'], selected_trade['order_time'],
  712. partial_image_path
  713. )
  714. # 7. 获取用户决策和信心指数
  715. user_decision, confidence_level = get_user_decision()
  716. # 8. 显示完整K线图
  717. print("\n=== 步骤7: 显示完整K线图 ===")
  718. full_image_name = f"full_{selected_trade['contract_code']}_{selected_trade['trade_date']}_{selected_trade['direction']}.png"
  719. full_image_path = os.path.join(output_dir, full_image_name)
  720. plot_full_kline(
  721. kline_data, trade_idx, selected_trade['trade_price'],
  722. selected_trade['direction'], selected_trade['contract_code'],
  723. selected_trade['trade_date'], selected_trade['order_time'],
  724. selected_trade['profit_loss'],
  725. full_image_path
  726. )
  727. # 在完整K线图之后显示交易信息
  728. print(f"\n交易信息:")
  729. print(f"合约: {selected_trade['contract_code']}")
  730. print(f"日期: {selected_trade['trade_date']}")
  731. print(f"方向: {'多头' if selected_trade['direction'] == 'long' else '空头'}")
  732. print(f"成交价: {selected_trade['trade_price']}")
  733. # 9. 记录结果
  734. print("\n=== 步骤8: 记录结果 ===")
  735. # 计算判定收益(使用连续交易总盈亏或普通盈亏)
  736. if selected_trade['continuous_total_profit'] != 'N/A':
  737. # 连续交易使用连续交易总盈亏
  738. decision_profit = selected_trade['continuous_total_profit'] if user_decision else -selected_trade['continuous_total_profit']
  739. profit_to_show = selected_trade['continuous_total_profit']
  740. else:
  741. # 普通交易使用单笔盈亏
  742. decision_profit = selected_trade['profit_loss'] if user_decision else -selected_trade['profit_loss']
  743. profit_to_show = selected_trade['profit_loss']
  744. result_data = {
  745. '日期': selected_trade['original_row']['日期'],
  746. '委托时间': selected_trade['original_row']['委托时间'],
  747. '标的': selected_trade['original_row']['标的'],
  748. '交易类型': selected_trade['original_row']['交易类型'],
  749. '成交数量': selected_trade['original_row']['成交数量'],
  750. '成交价': selected_trade['original_row']['成交价'],
  751. '平仓盈亏': selected_trade['profit_loss'],
  752. '用户判定': '开仓' if user_decision else '不开仓',
  753. '信心指数': confidence_level,
  754. '判定收益': decision_profit,
  755. '交易对ID': selected_trade['trade_pair_id'],
  756. '连续交易对ID': selected_trade['continuous_pair_id'],
  757. '连续交易总盈亏': selected_trade['continuous_total_profit']
  758. }
  759. record_result(result_data, result_path)
  760. print(f"\n=== 训练完成 ===")
  761. print(f"用户判定: {'开仓' if user_decision else '不开仓'}")
  762. print(f"信心指数: {confidence_level} ({'低' if confidence_level == 1 else '中' if confidence_level == 2 else '高'})")
  763. if selected_trade['continuous_total_profit'] != 'N/A':
  764. print(f"连续交易总盈亏: {profit_to_show:+.2f}")
  765. else:
  766. print(f"实际盈亏: {profit_to_show:+.2f}")
  767. print(f"判定收益: {decision_profit:+.2f}")
  768. print(f"结果已保存到: {result_path}")
  769. if __name__ == "__main__":
  770. main()