trade.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. """
  2. 交易汇总记录相关路由
  3. """
  4. from flask import Blueprint, jsonify, request, render_template, send_file, make_response
  5. from app import db
  6. from app.models.trade import TradeRecord, RollTradeRecord
  7. from app.models.transaction import TransactionRecord
  8. from datetime import datetime
  9. import pandas as pd
  10. import io
  11. import os
  12. from openpyxl.utils import get_column_letter
  13. from io import BytesIO
  14. from sqlalchemy import func
  15. from app.services.trade_logic import sync_all_trades_from_transactions
  16. bp = Blueprint('trade', __name__, url_prefix='/trade')
  17. @bp.route('/', methods=['GET'])
  18. def index():
  19. """交易汇总记录列表页面"""
  20. return render_template('trade/index.html')
  21. @bp.route('/list', methods=['GET'])
  22. def get_list():
  23. """获取交易汇总记录列表"""
  24. try:
  25. print("获取交易汇总记录列表...")
  26. # 获取分页参数
  27. page = request.args.get('page', 1, type=int)
  28. limit = request.args.get('limit', 10, type=int)
  29. # 获取筛选参数
  30. start_time = request.args.get('start_time')
  31. end_time = request.args.get('end_time')
  32. names = request.args.getlist('name')
  33. contract_letters = request.args.getlist('contract_letter')
  34. contract_code = request.args.get('contract_code')
  35. strategy_ids = request.args.getlist('strategy_id')
  36. trade_type = request.args.get('trade_type')
  37. position_type = request.args.get('position_type')
  38. # 构建查询
  39. query = TradeRecord.query
  40. if start_time:
  41. query = query.filter(TradeRecord.open_time >= datetime.strptime(start_time, '%Y-%m-%d'))
  42. if end_time:
  43. query = query.filter(TradeRecord.open_time <= datetime.strptime(end_time, '%Y-%m-%d'))
  44. if names:
  45. query = query.filter(TradeRecord.name.in_(names))
  46. if contract_letters:
  47. # 假设合约代码的前1-2位是合约字母
  48. query = query.filter(db.or_(*[TradeRecord.contract_code.startswith(letter) for letter in contract_letters]))
  49. if contract_code:
  50. query = query.filter(TradeRecord.contract_code.like(f'%{contract_code}%'))
  51. if strategy_ids:
  52. try:
  53. strategy_ids = [int(i) for i in strategy_ids if i.strip()]
  54. if strategy_ids:
  55. query = query.filter(TradeRecord.strategy_id.in_(strategy_ids))
  56. except ValueError:
  57. pass
  58. if trade_type is not None and trade_type.strip():
  59. try:
  60. query = query.filter(TradeRecord.trade_type == int(trade_type))
  61. except ValueError:
  62. pass
  63. if position_type is not None and position_type.strip():
  64. try:
  65. query = query.filter(TradeRecord.position_type == int(position_type))
  66. except ValueError:
  67. pass
  68. # 执行分页查询
  69. pagination = query.order_by(TradeRecord.open_time.desc()).paginate(page=page, per_page=limit, error_out=False)
  70. trades = pagination.items
  71. total = pagination.total
  72. print(f"找到{len(trades)}条交易汇总记录,总共{total}条")
  73. # 返回结果
  74. return jsonify({
  75. 'code': 0,
  76. 'msg': '成功',
  77. 'count': total,
  78. 'data': [trade.to_dict() for trade in trades]
  79. })
  80. except Exception as e:
  81. print(f"获取交易汇总记录列表失败: {str(e)}")
  82. import traceback
  83. print(traceback.format_exc())
  84. return jsonify({
  85. 'code': 1,
  86. 'msg': f'获取列表失败: {str(e)}',
  87. 'count': 0,
  88. 'data': []
  89. })
  90. @bp.route('/detail/<int:id>', methods=['GET'])
  91. def get_detail(id):
  92. """获取交易汇总记录详情,改为渲染模板"""
  93. trade = TradeRecord.query.get_or_404(id)
  94. # 如果需要关联查询其他信息(如策略、K线形态、趋势名称),可以在这里进行
  95. # 例如,查询策略名称
  96. # strategy = StrategyInfo.query.get(trade.strategy_id) if trade.strategy_id else None
  97. # trade_data = trade.to_dict()
  98. # trade_data['strategy_name'] = strategy.name if strategy else trade.strategy_name # 优先使用查询到的名称
  99. return render_template('trade/detail.html', trade=trade)
  100. @bp.route('/delete/<int:id>', methods=['DELETE'])
  101. def delete(id):
  102. """删除交易汇总记录"""
  103. trade = TradeRecord.query.get_or_404(id)
  104. # 从数据库删除
  105. db.session.delete(trade)
  106. db.session.commit()
  107. return jsonify({
  108. 'code': 0,
  109. 'msg': '删除成功'
  110. })
  111. # 换月交易记录相关路由
  112. @bp.route('/roll/list', methods=['GET'])
  113. def get_roll_list():
  114. """获取换月交易记录列表"""
  115. roll_trades = RollTradeRecord.query.all()
  116. return jsonify({
  117. 'code': 0,
  118. 'msg': '成功',
  119. 'data': [roll_trade.to_dict() for roll_trade in roll_trades]
  120. })
  121. @bp.route('/roll/detail/<int:id>', methods=['GET'])
  122. def get_roll_detail(id):
  123. """获取换月交易记录详情"""
  124. roll_trade = RollTradeRecord.query.get_or_404(id)
  125. return jsonify({
  126. 'code': 0,
  127. 'msg': '成功',
  128. 'data': roll_trade.to_dict()
  129. })
  130. @bp.route('/roll/create', methods=['POST'])
  131. def create_roll():
  132. """创建换月交易记录"""
  133. data = request.json
  134. roll_trade = RollTradeRecord(
  135. roll_trade_main_id=data.get('roll_trade_main_id'),
  136. related_trade_ids=data.get('related_trade_ids'),
  137. contract_letter=data.get('contract_letter'),
  138. related_contracts=data.get('related_contracts')
  139. )
  140. db.session.add(roll_trade)
  141. db.session.commit()
  142. return jsonify({
  143. 'code': 0,
  144. 'msg': '创建成功',
  145. 'data': roll_trade.to_dict()
  146. })
  147. @bp.route('/export', methods=['GET'])
  148. def export():
  149. """导出交易汇总记录为Excel文件"""
  150. # 获取所有交易记录
  151. trades = TradeRecord.query.all()
  152. # 转换为DataFrame
  153. data = []
  154. for trade in trades:
  155. data.append({
  156. '合约代码': trade.contract_code,
  157. '名称': trade.name,
  158. '账户': trade.account,
  159. '策略': trade.strategy_name,
  160. '持仓类型': trade.position_type,
  161. 'K线形态': trade.candle_pattern,
  162. '开仓时间': trade.open_time.strftime('%Y-%m-%d %H:%M') if trade.open_time else '',
  163. '平仓时间': trade.close_time.strftime('%Y-%m-%d %H:%M') if trade.close_time else '',
  164. '持仓量': trade.position_volume,
  165. '合约乘数': trade.contract_multiplier,
  166. '持仓成本': trade.past_position_cost,
  167. '平均售价': trade.average_sale_price,
  168. '单笔利润': trade.single_profit,
  169. '投资利润': trade.investment_profit,
  170. '投资收益率': trade.investment_profit_rate,
  171. '持仓天数': trade.holding_days,
  172. '年化收益率': trade.annual_profit_rate,
  173. '交易类型': trade.trade_type,
  174. '置信指数': trade.confidence_index,
  175. '相似度评价': trade.similarity_evaluation,
  176. '长期趋势': trade.long_trend_name,
  177. '中期趋势': trade.mid_trend_name,
  178. })
  179. df = pd.DataFrame(data)
  180. # 创建Excel文件
  181. output = io.BytesIO()
  182. with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
  183. df.to_excel(writer, sheet_name='交易汇总', index=False)
  184. # 自动调整列宽
  185. worksheet = writer.sheets['交易汇总']
  186. for i, col in enumerate(df.columns):
  187. column_width = max(df[col].astype(str).map(len).max(), len(col) + 2)
  188. worksheet.set_column(i, i, column_width)
  189. output.seek(0)
  190. # 设置下载文件名
  191. filename = f'交易汇总_{datetime.now().strftime("%Y%m%d%H%M%S")}.xlsx'
  192. return send_file(
  193. output,
  194. mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  195. as_attachment=True,
  196. download_name=filename
  197. )
  198. @bp.route('/template', methods=['GET'])
  199. def get_template():
  200. """获取交易汇总记录的Excel导入模板"""
  201. # 创建DataFrame
  202. columns = [
  203. '换月交易主ID', '合约代码', '名称', '账户',
  204. '操作策略ID', '操作策略', '多空仓位(0-多头,1-空头)',
  205. 'K线形态ID', 'K线形态', '开仓时间', '平仓时间',
  206. '持仓手数', '合约乘数', '过往持仓成本', '平均售价',
  207. '单笔收益', '投资收益', '投资收益率', '持仓天数', '年化收益率',
  208. '交易类别(0-模拟,1-真实)', '信心指数', '相似度评估',
  209. '长期趋势IDs', '长期趋势名称', '中期趋势IDs', '中期趋势名称'
  210. ]
  211. # 创建示例数据
  212. data = [
  213. [None, 'CU2305', '沪铜', '华安期货',
  214. 1, '趋势突破', 0,
  215. 1, '突破回踩', '2023-03-29 14:30', '2023-03-30 14:30',
  216. 1, 5, 68000, 68500,
  217. 2500, 2500, 0.0735, 1, 26.86,
  218. 0, 1.5, '80%相似',
  219. '1,2', '长期上涨+短期震荡', '3,4', '中期下跌+短期震荡']
  220. ]
  221. df = pd.DataFrame(data, columns=columns)
  222. # 创建Excel文件
  223. output = io.BytesIO()
  224. with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
  225. df.to_excel(writer, sheet_name='交易汇总导入模板', index=False)
  226. # 自动调整列宽
  227. worksheet = writer.sheets['交易汇总导入模板']
  228. for i, col in enumerate(df.columns):
  229. column_width = max(df[col].astype(str).map(len).max(), len(col) + 2)
  230. worksheet.set_column(i, i, column_width)
  231. output.seek(0)
  232. # 设置下载文件名
  233. filename = f'交易汇总导入模板_{datetime.now().strftime("%Y%m%d%H%M%S")}.xlsx'
  234. return send_file(
  235. output,
  236. mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  237. as_attachment=True,
  238. download_name=filename
  239. )
  240. @bp.route('/import_page', methods=['GET'])
  241. def import_page():
  242. """Display the import page."""
  243. return render_template('trade/import.html')
  244. @bp.route('/import', methods=['POST'])
  245. def import_excel():
  246. """从Excel导入交易汇总记录"""
  247. if 'file' not in request.files:
  248. return jsonify({
  249. 'code': 1,
  250. 'msg': '没有上传文件'
  251. })
  252. file = request.files['file']
  253. if file.filename == '':
  254. return jsonify({
  255. 'code': 1,
  256. 'msg': '没有选择文件'
  257. })
  258. if not file.filename.endswith('.xlsx'):
  259. return jsonify({
  260. 'code': 1,
  261. 'msg': '请上传Excel文件(.xlsx)'
  262. })
  263. try:
  264. # 读取Excel文件
  265. df = pd.read_excel(file)
  266. # 验证必填列
  267. required_columns = ['合约代码', '名称', '多空仓位(0-多头,1-空头)', '开仓时间', '持仓手数', '合约乘数']
  268. for col in required_columns:
  269. if col not in df.columns:
  270. return jsonify({
  271. 'code': 1,
  272. 'msg': f'Excel文件缺少必填列: {col}'
  273. })
  274. # 导入数据
  275. success_count = 0
  276. error_count = 0
  277. error_messages = []
  278. for i, row in df.iterrows():
  279. try:
  280. # 处理日期时间
  281. open_time = None
  282. if '开仓时间' in row and not pd.isna(row['开仓时间']):
  283. if isinstance(row['开仓时间'], str):
  284. open_time = datetime.strptime(row['开仓时间'], '%Y-%m-%d %H:%M')
  285. else:
  286. open_time = row['开仓时间']
  287. else:
  288. open_time = datetime.now()
  289. close_time = None
  290. if '平仓时间' in row and not pd.isna(row['平仓时间']):
  291. if isinstance(row['平仓时间'], str):
  292. close_time = datetime.strptime(row['平仓时间'], '%Y-%m-%d %H:%M')
  293. else:
  294. close_time = row['平仓时间']
  295. # 计算持仓天数
  296. holding_days = None
  297. if close_time and open_time:
  298. holding_days = (close_time - open_time).days
  299. # 创建新记录
  300. trade = TradeRecord(
  301. roll_trade_main_id=int(row['换月交易主ID']) if not pd.isna(row.get('换月交易主ID')) else None,
  302. contract_code=row['合约代码'],
  303. name=row['名称'],
  304. account=row.get('账户', '华安期货'),
  305. strategy_id=row.get('操作策略ID'),
  306. strategy_name=row.get('操作策略'),
  307. position_type=int(row['多空仓位(0-多头,1-空头)']),
  308. candle_pattern_id=row.get('K线形态ID'),
  309. candle_pattern=row.get('K线形态'),
  310. open_time=open_time,
  311. close_time=close_time,
  312. position_volume=float(row['持仓手数']),
  313. contract_multiplier=float(row['合约乘数']),
  314. past_position_cost=float(row.get('过往持仓成本', 0)) if not pd.isna(row.get('过往持仓成本')) else None,
  315. average_sale_price=float(row.get('平均售价', 0)) if not pd.isna(row.get('平均售价')) else None,
  316. single_profit=float(row.get('单笔收益', 0)) if not pd.isna(row.get('单笔收益')) else None,
  317. investment_profit=float(row.get('投资收益', 0)) if not pd.isna(row.get('投资收益')) else None,
  318. investment_profit_rate=float(row.get('投资收益率', 0)) if not pd.isna(row.get('投资收益率')) else None,
  319. holding_days=holding_days,
  320. annual_profit_rate=float(row.get('年化收益率', 0)) if not pd.isna(row.get('年化收益率')) else None,
  321. trade_type=int(row.get('交易类别(0-模拟,1-真实)', 0)) if not pd.isna(row.get('交易类别(0-模拟,1-真实)')) else 0,
  322. confidence_index=float(row.get('信心指数', 0)) if not pd.isna(row.get('信心指数')) else None,
  323. similarity_evaluation=row.get('相似度评估'),
  324. long_trend_ids=row.get('长期趋势IDs'),
  325. long_trend_name=row.get('长期趋势名称'),
  326. mid_trend_ids=row.get('中期趋势IDs'),
  327. mid_trend_name=row.get('中期趋势名称')
  328. )
  329. # 保存到数据库
  330. db.session.add(trade)
  331. success_count += 1
  332. except Exception as e:
  333. error_count += 1
  334. error_messages.append(f'第{i+2}行出错: {str(e)}')
  335. # 提交所有更改
  336. db.session.commit()
  337. return jsonify({
  338. 'code': 0,
  339. 'msg': f'成功导入{success_count}条记录,失败{error_count}条',
  340. 'data': {
  341. 'success_count': success_count,
  342. 'error_count': error_count,
  343. 'error_messages': error_messages
  344. }
  345. })
  346. except Exception as e:
  347. return jsonify({
  348. 'code': 1,
  349. 'msg': f'导入失败: {str(e)}'
  350. })
  351. @bp.route('/sync_all', methods=['POST'])
  352. def sync_all():
  353. """全面同步交易汇总记录"""
  354. print("接收到全面同步请求...")
  355. result = sync_all_trades_from_transactions()
  356. print(f"全面同步完成,结果: {result}")
  357. return jsonify(result)