future_info.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  1. """
  2. 期货基础信息相关路由
  3. 包括期货品种信息的查询、创建、更新等
  4. """
  5. from flask import Blueprint, jsonify, request, render_template, send_file, make_response, current_app
  6. from app.database.db_manager import db
  7. from app.models.future_info import FutureInfo, FutureDaily
  8. from app.models.dimension import TrendInfo
  9. from app.services.data_scraper import FutureDataScraper
  10. from app.services.data_update import data_update_service
  11. import pandas as pd
  12. import io
  13. import os
  14. from datetime import datetime
  15. from werkzeug.utils import secure_filename
  16. import threading
  17. import logging
  18. logger = logging.getLogger(__name__)
  19. # 创建蓝图
  20. bp = Blueprint('future_info', __name__, url_prefix='/api/future_info')
  21. @bp.route('/', methods=['GET'])
  22. def index():
  23. """期货基础信息列表页面 (仅渲染骨架)"""
  24. # # 获取查询参数 (不再需要从后端传递数据)
  25. # search = request.args.get('search', '')
  26. # page = request.args.get('page', 1, type=int)
  27. # limit = request.args.get('limit', 10, type=int)
  28. # # 构建查询 (不再需要从后端传递数据)
  29. # query = FutureInfo.query
  30. # # 应用查询条件 (不再需要从后端传递数据)
  31. # if search:
  32. # query = query.filter(
  33. # db.or_(
  34. # FutureInfo.contract_letter.like(f'%{search}%'),
  35. # FutureInfo.name.like(f'%{search}%')
  36. # )
  37. # )
  38. # # 执行分页查询 (不再需要从后端传递数据)
  39. # pagination = query.order_by(FutureInfo.id.asc()).paginate(page=page, per_page=limit, error_out=False)
  40. # futures = pagination.items
  41. # total = pagination.total
  42. # 只渲染模板,数据由前端AJAX获取
  43. return render_template('future_info/index.html')
  44. # futures=futures,
  45. # pagination=pagination,
  46. # total=total,
  47. # search=search,
  48. # limit=limit # 传递limit到模板,以便选择器知道当前值
  49. @bp.route('/add', methods=['GET'])
  50. def add():
  51. """添加期货基础信息页面"""
  52. return render_template('future_info/add.html')
  53. @bp.route('/detail/<int:id>', methods=['GET'])
  54. def detail(id):
  55. """期货基础信息详情页面"""
  56. future = FutureInfo.query.get_or_404(id)
  57. # 获取相关的每日数据(可选)
  58. daily_data = FutureDaily.query.filter_by(product_code=future.contract_letter).all()
  59. return render_template('future_info/detail.html', future=future, daily_data=daily_data)
  60. @bp.route('/edit/<int:id>', methods=['GET'])
  61. def edit(id):
  62. """编辑期货基础信息页面"""
  63. return render_template('future_info/edit.html', future_id=id)
  64. @bp.route('/get/<int:future_id>', methods=['GET'])
  65. def get_future_info_detail(future_id):
  66. """获取期货基础信息详情"""
  67. future = FutureInfo.query.get_or_404(future_id)
  68. return jsonify({
  69. 'code': 0,
  70. 'msg': '获取成功',
  71. 'data': future.to_dict()
  72. })
  73. @bp.route('/list', methods=['GET'])
  74. def get_future_info_list():
  75. """获取期货基础信息列表 (支持分页和搜索)"""
  76. # 获取查询参数
  77. page = request.args.get('page', 1, type=int)
  78. limit = request.args.get('limit', 10, type=int)
  79. search = request.args.get('search', '')
  80. # 保留旧的过滤参数,如果需要的话
  81. market = request.args.get('market', type=int)
  82. contract_letter = request.args.get('contract_letter')
  83. name = request.args.get('name')
  84. long_term_trend = request.args.get('long_term_trend')
  85. future_id = request.args.get('id', type=int)
  86. # 构建查询
  87. query = FutureInfo.query
  88. # 应用过滤条件
  89. if market is not None:
  90. query = query.filter(FutureInfo.market == market)
  91. if contract_letter:
  92. query = query.filter(FutureInfo.contract_letter.like(f'%{contract_letter}%'))
  93. if name:
  94. query = query.filter(FutureInfo.name.like(f'%{name}%'))
  95. if long_term_trend:
  96. query = query.filter(FutureInfo.long_term_trend.like(f'%{long_term_trend}%'))
  97. if future_id is not None:
  98. query = query.filter(FutureInfo.id == future_id)
  99. # 添加搜索逻辑 (合并contract_letter和name搜索)
  100. if search:
  101. query = query.filter(
  102. db.or_(
  103. FutureInfo.contract_letter.like(f'%{search}%'),
  104. FutureInfo.name.like(f'%{search}%')
  105. )
  106. )
  107. # 执行分页查询并获取结果
  108. pagination = query.order_by(FutureInfo.id.asc()).paginate(page=page, per_page=limit, error_out=False)
  109. futures = pagination.items
  110. total = pagination.total
  111. # 将查询结果转换为JSON格式,列表API中排除core_ratio字段
  112. future_data = []
  113. for future in futures:
  114. data = future.to_dict()
  115. # 列表API中不包含核心比率字段
  116. data.pop('core_ratio', None)
  117. future_data.append(data)
  118. result = {
  119. 'code': 0,
  120. 'msg': '获取成功',
  121. 'count': total, # 返回总数以供分页
  122. 'data': future_data
  123. }
  124. return jsonify(result)
  125. @bp.route('/update/<int:future_id>', methods=['PUT'])
  126. def update_future_info(future_id):
  127. """更新期货基础信息"""
  128. future = FutureInfo.query.get_or_404(future_id)
  129. data = request.json
  130. # 更新字段
  131. if 'contract_letter' in data:
  132. future.contract_letter = data['contract_letter']
  133. if 'name' in data:
  134. future.name = data['name']
  135. if 'market' in data:
  136. future.market = data['market']
  137. if 'exchange' in data:
  138. future.exchange = data['exchange']
  139. if 'contract_multiplier' in data:
  140. future.contract_multiplier = data['contract_multiplier']
  141. if 'long_margin_rate' in data:
  142. future.long_margin_rate = data['long_margin_rate']
  143. if 'short_margin_rate' in data:
  144. future.short_margin_rate = data['short_margin_rate']
  145. if 'open_fee' in data:
  146. future.open_fee = data['open_fee']
  147. if 'close_fee' in data:
  148. future.close_fee = data['close_fee']
  149. if 'close_today_rate' in data:
  150. future.close_today_rate = data['close_today_rate']
  151. if 'close_today_fee' in data:
  152. future.close_today_fee = data['close_today_fee']
  153. if 'long_margin_amount' in data:
  154. future.long_margin_amount = data['long_margin_amount']
  155. if 'short_margin_amount' in data:
  156. future.short_margin_amount = data['short_margin_amount']
  157. if 'th_main_contract' in data:
  158. future.th_main_contract = data['th_main_contract']
  159. if 'current_main_contract' in data:
  160. future.current_main_contract = data['current_main_contract']
  161. if 'th_order' in data:
  162. future.th_order = data['th_order']
  163. if 'long_term_trend' in data:
  164. future.long_term_trend = data['long_term_trend']
  165. if 'core_ratio' in data:
  166. future.core_ratio = data['core_ratio']
  167. # 保存到数据库
  168. db.session.commit()
  169. return jsonify({
  170. 'code': 0,
  171. 'msg': '更新成功',
  172. 'data': future.to_dict()
  173. })
  174. @bp.route('/add', methods=['POST'])
  175. def create_future_info():
  176. """创建期货基础信息"""
  177. data = request.json
  178. # 创建新记录
  179. future_info = FutureInfo(
  180. contract_letter=data.get('contract_letter'),
  181. name=data.get('name'),
  182. market=data.get('market'),
  183. exchange=data.get('exchange'),
  184. contract_multiplier=data.get('contract_multiplier'),
  185. long_margin_rate=data.get('long_margin_rate'),
  186. short_margin_rate=data.get('short_margin_rate'),
  187. open_fee=data.get('open_fee'),
  188. close_fee=data.get('close_fee'),
  189. close_today_rate=data.get('close_today_rate'),
  190. close_today_fee=data.get('close_today_fee'),
  191. long_margin_amount=data.get('long_margin_amount'),
  192. short_margin_amount=data.get('short_margin_amount'),
  193. th_main_contract=data.get('th_main_contract'),
  194. current_main_contract=data.get('current_main_contract'),
  195. th_order=data.get('th_order'),
  196. long_term_trend=data.get('long_term_trend'),
  197. core_ratio=data.get('core_ratio')
  198. )
  199. # 保存到数据库
  200. db.session.add(future_info)
  201. db.session.commit()
  202. return jsonify({
  203. 'code': 0,
  204. 'msg': '创建成功',
  205. 'data': future_info.to_dict()
  206. })
  207. @bp.route('/delete/<int:future_id>', methods=['DELETE'])
  208. def delete_future_info(future_id):
  209. """删除期货基础信息"""
  210. future = FutureInfo.query.get_or_404(future_id)
  211. # 从数据库中删除
  212. db.session.delete(future)
  213. db.session.commit()
  214. return jsonify({
  215. 'code': 0,
  216. 'msg': '删除成功'
  217. })
  218. @bp.route('/template', methods=['GET'])
  219. def get_template():
  220. """获取期货基础信息的Excel导入模板"""
  221. # 创建DataFrame
  222. columns = [
  223. '合约字母', '名称', '市场(0-国内,1-国外)', '交易所', '合约乘数',
  224. '做多保证金率', '做空保证金率', '开仓费用', '平仓费用',
  225. '平今费率', '平今费用', '同花主力合约', '当前主力合约',
  226. '同花顺顺序', '长期趋势', '核心比率'
  227. ]
  228. # 创建示例数据
  229. data = [
  230. ['CU', '沪铜', 0, 'SHFE', 5,
  231. 0.1, 0.1, 3, 3,
  232. 0, 0, 'CU2305', 'CU2305',
  233. 1, '长期上涨', 0.85]
  234. ]
  235. df = pd.DataFrame(data, columns=columns)
  236. # 创建Excel文件
  237. output = io.BytesIO()
  238. with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
  239. df.to_excel(writer, sheet_name='期货基础信息导入模板', index=False)
  240. # 自动调整列宽
  241. worksheet = writer.sheets['期货基础信息导入模板']
  242. for i, col in enumerate(df.columns):
  243. column_width = max(df[col].astype(str).map(len).max(), len(col) + 2)
  244. worksheet.set_column(i, i, column_width)
  245. output.seek(0)
  246. # 设置下载文件名
  247. filename = f'期货基础信息导入模板_{datetime.now().strftime("%Y%m%d%H%M%S")}.xlsx'
  248. return send_file(
  249. output,
  250. mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  251. as_attachment=True,
  252. download_name=filename
  253. )
  254. @bp.route('/import', methods=['POST'])
  255. def import_excel():
  256. """从Excel导入期货基础信息"""
  257. if 'file' not in request.files:
  258. return jsonify({
  259. 'code': 1,
  260. 'msg': '没有上传文件'
  261. })
  262. file = request.files['file']
  263. if file.filename == '':
  264. return jsonify({
  265. 'code': 1,
  266. 'msg': '没有选择文件'
  267. })
  268. if not file.filename.endswith('.xlsx'):
  269. return jsonify({
  270. 'code': 1,
  271. 'msg': '请上传Excel文件(.xlsx)'
  272. })
  273. try:
  274. # 读取Excel文件
  275. df = pd.read_excel(file)
  276. # 验证必填列
  277. required_columns = ['合约字母', '名称', '市场(0-国内,1-国外)']
  278. for col in required_columns:
  279. if col not in df.columns:
  280. return jsonify({
  281. 'code': 1,
  282. 'msg': f'Excel文件缺少必填列: {col}'
  283. })
  284. # 导入数据
  285. success_count = 0
  286. error_count = 0
  287. error_messages = []
  288. for i, row in df.iterrows():
  289. try:
  290. # 检查是否已存在相同合约字母
  291. existing = FutureInfo.query.filter_by(contract_letter=row['合约字母']).first()
  292. if existing:
  293. # 更新现有记录
  294. existing.name = row['名称']
  295. existing.market = int(row.get('市场(0-国内,1-国外)', 0))
  296. existing.exchange = row.get('交易所')
  297. existing.contract_multiplier = float(row.get('合约乘数', 0)) if not pd.isna(row.get('合约乘数')) else None
  298. existing.long_margin_rate = float(row.get('做多保证金率', 0)) if not pd.isna(row.get('做多保证金率')) else None
  299. existing.short_margin_rate = float(row.get('做空保证金率', 0)) if not pd.isna(row.get('做空保证金率')) else None
  300. existing.open_fee = float(row.get('开仓费用', 0)) if not pd.isna(row.get('开仓费用')) else None
  301. existing.close_fee = float(row.get('平仓费用', 0)) if not pd.isna(row.get('平仓费用')) else None
  302. existing.close_today_rate = float(row.get('平今费率', 0)) if not pd.isna(row.get('平今费率')) else None
  303. existing.close_today_fee = float(row.get('平今费用', 0)) if not pd.isna(row.get('平今费用')) else None
  304. existing.th_main_contract = row.get('同花主力合约')
  305. existing.current_main_contract = row.get('当前主力合约')
  306. existing.th_order = int(row.get('同花顺顺序', 0)) if not pd.isna(row.get('同花顺顺序')) else None
  307. existing.long_term_trend = row.get('长期趋势')
  308. existing.core_ratio = float(row.get('核心比率', 0)) if not pd.isna(row.get('核心比率')) else None
  309. else:
  310. # 创建新记录
  311. future_info = FutureInfo(
  312. contract_letter=row['合约字母'],
  313. name=row['名称'],
  314. market=int(row.get('市场(0-国内,1-国外)', 0)),
  315. exchange=row.get('交易所'),
  316. contract_multiplier=float(row.get('合约乘数', 0)) if not pd.isna(row.get('合约乘数')) else None,
  317. long_margin_rate=float(row.get('做多保证金率', 0)) if not pd.isna(row.get('做多保证金率')) else None,
  318. short_margin_rate=float(row.get('做空保证金率', 0)) if not pd.isna(row.get('做空保证金率')) else None,
  319. open_fee=float(row.get('开仓费用', 0)) if not pd.isna(row.get('开仓费用')) else None,
  320. close_fee=float(row.get('平仓费用', 0)) if not pd.isna(row.get('平仓费用')) else None,
  321. close_today_rate=float(row.get('平今费率', 0)) if not pd.isna(row.get('平今费率')) else None,
  322. close_today_fee=float(row.get('平今费用', 0)) if not pd.isna(row.get('平今费用')) else None,
  323. th_main_contract=row.get('同花主力合约'),
  324. current_main_contract=row.get('当前主力合约'),
  325. th_order=int(row.get('同花顺顺序', 0)) if not pd.isna(row.get('同花顺顺序')) else None,
  326. long_term_trend=row.get('长期趋势'),
  327. core_ratio=float(row.get('核心比率', 0)) if not pd.isna(row.get('核心比率')) else None
  328. )
  329. db.session.add(future_info)
  330. success_count += 1
  331. except Exception as e:
  332. error_count += 1
  333. error_messages.append(f'第{i+2}行出错: {str(e)}')
  334. # 提交所有更改
  335. db.session.commit()
  336. return jsonify({
  337. 'code': 0,
  338. 'msg': f'成功导入{success_count}条记录,失败{error_count}条',
  339. 'data': {
  340. 'success_count': success_count,
  341. 'error_count': error_count,
  342. 'error_messages': error_messages
  343. }
  344. })
  345. except Exception as e:
  346. return jsonify({
  347. 'code': 1,
  348. 'msg': f'导入失败: {str(e)}'
  349. })
  350. @bp.route('/import', methods=['GET'])
  351. def import_view():
  352. """导入期货基础信息页面"""
  353. return render_template('future_info/import.html')
  354. @bp.route('/update-data', methods=['POST'])
  355. def update_future_data():
  356. """手动触发期货数据更新"""
  357. # 获取更新模式
  358. data = request.json
  359. update_mode = data.get('update_mode', 'both')
  360. if update_mode not in ['daily', 'info', 'both']:
  361. return jsonify({
  362. 'code': 1,
  363. 'msg': '无效的更新模式,有效的选项为: daily, info, both'
  364. })
  365. # 获取当前应用实例,在线程外部获取避免上下文问题
  366. app = current_app._get_current_object()
  367. # 在后台线程中执行更新,避免阻塞请求
  368. def update_data_thread():
  369. try:
  370. from app.services.data_update import data_update_service
  371. logger.info(f"开始后台更新期货数据,模式: {update_mode}")
  372. with app.app_context():
  373. if update_mode in ['daily', 'both']:
  374. # 使用统一的数据更新服务
  375. result = data_update_service.manual_update()
  376. if result['code'] == 0:
  377. logger.info(f"数据更新成功: {result['msg']}")
  378. app.config['DATA_UPDATE_COMPLETE'] = True
  379. else:
  380. logger.error(f"数据更新失败: {result['msg']}")
  381. app.config['DATA_UPDATE_COMPLETE'] = False
  382. app.config['DATA_UPDATE_ERROR'] = result['msg']
  383. elif update_mode == 'info':
  384. # 仅更新期货信息表
  385. scraper = FutureDataScraper()
  386. updated_count = scraper.update_future_info(db.session, FutureInfo)
  387. logger.info(f"future_info表更新完成,共更新{updated_count}条记录")
  388. app.config['DATA_UPDATE_COMPLETE'] = True
  389. except Exception as e:
  390. logger.error(f"更新期货数据时出错: {str(e)}")
  391. # 更新失败时设置错误标记
  392. try:
  393. with app.app_context():
  394. app.config['DATA_UPDATE_COMPLETE'] = False
  395. app.config['DATA_UPDATE_ERROR'] = str(e)
  396. except Exception as context_error:
  397. logger.error(f"设置更新状态标记失败: {str(context_error)}")
  398. # 启动后台线程执行更新
  399. thread = threading.Thread(target=update_data_thread)
  400. thread.daemon = True
  401. thread.start()
  402. # 重置完成标记和错误信息 (启动时重置)
  403. current_app.config['DATA_UPDATE_COMPLETE'] = None
  404. current_app.config['DATA_UPDATE_ERROR'] = None
  405. return jsonify({
  406. 'code': 0,
  407. 'msg': '期货数据更新已在后台启动,请稍后查看结果或等待页面自动刷新'
  408. })
  409. @bp.route('/update-status', methods=['GET'])
  410. def get_update_status():
  411. """检查后台数据更新的状态"""
  412. try:
  413. complete = current_app.config.get('DATA_UPDATE_COMPLETE')
  414. error = current_app.config.get('DATA_UPDATE_ERROR')
  415. status = {
  416. 'code': 0,
  417. 'data': {
  418. 'complete': complete,
  419. 'error': error
  420. }
  421. }
  422. # 如果已完成或出错,清除标记,避免重复通知
  423. if complete is not None:
  424. # 清除标记的操作移到实际获取状态之后,确保前端能至少获取一次结果
  425. # current_app.config['DATA_UPDATE_COMPLETE'] = None
  426. # current_app.config['DATA_UPDATE_ERROR'] = None
  427. pass
  428. return jsonify(status)
  429. except Exception as e:
  430. logger.error(f"获取更新状态时出错: {str(e)}")
  431. return jsonify({
  432. 'code': 1,
  433. 'data': {
  434. 'complete': None,
  435. 'error': f"获取状态失败: {str(e)}"
  436. }
  437. })
  438. @bp.route('/daily-list', methods=['GET'])
  439. def get_future_daily_list():
  440. """获取期货每日数据列表"""
  441. # 获取查询参数
  442. exchange = request.args.get('exchange')
  443. product_code = request.args.get('product_code')
  444. contract_code = request.args.get('contract_code')
  445. is_main_contract = request.args.get('is_main_contract', type=int)
  446. # 构建查询
  447. query = FutureDaily.query
  448. # 应用过滤条件
  449. if exchange:
  450. query = query.filter(FutureDaily.exchange.like(f'%{exchange}%'))
  451. if product_code:
  452. query = query.filter(FutureDaily.product_code.like(f'%{product_code}%'))
  453. if contract_code:
  454. query = query.filter(FutureDaily.contract_code.like(f'%{contract_code}%'))
  455. if is_main_contract is not None:
  456. query = query.filter(FutureDaily.is_main_contract == bool(is_main_contract))
  457. # 执行查询并获取结果
  458. daily_data = query.all()
  459. # 将查询结果转换为JSON格式
  460. result = {
  461. 'code': 0,
  462. 'msg': '获取成功',
  463. 'count': len(daily_data),
  464. 'data': [daily.to_dict() for daily in daily_data]
  465. }
  466. return jsonify(result)
  467. @bp.route('/manual_update', methods=['POST'])
  468. def manual_update():
  469. """手动触发数据更新"""
  470. return jsonify(data_update_service.manual_update())
  471. @bp.route('/trends', methods=['GET'])
  472. def get_trend_info_list():
  473. """获取趋势信息列表,用于在编辑期货信息时选择长期趋势特征"""
  474. # 获取查询参数
  475. category = request.args.get('category', type=int)
  476. # 构建查询
  477. query = TrendInfo.query
  478. # 应用过滤条件
  479. if category is not None:
  480. query = query.filter(TrendInfo.category == category)
  481. # 执行查询并获取结果
  482. trends = query.all()
  483. # 将查询结果转换为JSON格式
  484. trend_list = []
  485. for trend in trends:
  486. trend_data = {
  487. 'id': trend.id,
  488. 'category': trend.category,
  489. 'name': trend.name,
  490. 'time_range_id': trend.time_range_id,
  491. 'amplitude_id': trend.amplitude_id,
  492. 'position_id': trend.position_id,
  493. 'speed_type_id': trend.speed_type_id,
  494. 'trend_type_id': trend.trend_type_id,
  495. 'extra_info': trend.extra_info
  496. }
  497. trend_list.append(trend_data)
  498. result = {
  499. 'code': 0,
  500. 'msg': '获取成功',
  501. 'count': len(trends),
  502. 'data': trend_list
  503. }
  504. return jsonify(result)
  505. @bp.route('/validate-trends', methods=['POST'])
  506. def validate_trend_names():
  507. """验证趋势特征名称是否有效"""
  508. data = request.json
  509. if not data or 'trend_names' not in data:
  510. return jsonify({
  511. 'code': 1,
  512. 'msg': '缺少趋势特征名称',
  513. 'data': {'invalid_trends': []}
  514. })
  515. trend_names = data['trend_names']
  516. # 如果为空字符串,视为有效
  517. if not trend_names.strip():
  518. return jsonify({
  519. 'code': 0,
  520. 'msg': '验证成功',
  521. 'data': {'invalid_trends': []}
  522. })
  523. # 分割趋势特征名称
  524. trend_names_list = [name.strip() for name in trend_names.split('+') if name.strip()]
  525. # 查询所有有效的趋势特征名称
  526. valid_trends = {trend.name for trend in TrendInfo.query.all()}
  527. # 找出无效的趋势特征名称
  528. invalid_trends = [name for name in trend_names_list if name not in valid_trends]
  529. if invalid_trends:
  530. return jsonify({
  531. 'code': 1,
  532. 'msg': '存在无效的趋势特征名称',
  533. 'data': {'invalid_trends': invalid_trends}
  534. })
  535. return jsonify({
  536. 'code': 0,
  537. 'msg': '所有趋势特征名称均有效',
  538. 'data': {'invalid_trends': []}
  539. })
  540. @bp.route('/search', methods=['GET'])
  541. def search_future_info():
  542. """搜索期货品种,支持模糊匹配"""
  543. query_text = request.args.get('q', '').strip()
  544. limit = request.args.get('limit', 10, type=int)
  545. if not query_text:
  546. return jsonify({
  547. 'code': 0,
  548. 'msg': '成功',
  549. 'data': []
  550. })
  551. # 优先精确匹配合约字母,再进行模糊匹配
  552. # 首先尝试精确匹配合约字母
  553. exact_match = FutureInfo.query.filter(
  554. FutureInfo.contract_letter == query_text
  555. ).all()
  556. # 如果精确匹配有结果,优先返回精确匹配的结果
  557. if exact_match:
  558. results = exact_match[:limit]
  559. else:
  560. # 精确匹配无结果时,再进行模糊匹配
  561. search_query = FutureInfo.query.filter(
  562. db.or_(
  563. FutureInfo.name.like(f'%{query_text}%'),
  564. FutureInfo.contract_letter.like(f'%{query_text}%')
  565. )
  566. ).order_by(db.func.length(FutureInfo.contract_letter)).limit(limit)
  567. results = search_query.all()
  568. return jsonify({
  569. 'code': 0,
  570. 'msg': '成功',
  571. 'data': [
  572. {
  573. 'id': future.id,
  574. 'name': future.name,
  575. 'contract_letter': future.contract_letter,
  576. 'market': future.market,
  577. 'current_main_contract': future.current_main_contract,
  578. 'display_text': f"{future.name} ({future.contract_letter})"
  579. }
  580. for future in results
  581. ]
  582. })
  583. @bp.route('/sync-main-contracts', methods=['POST'])
  584. def sync_main_contracts():
  585. """同步主力合约:将同花主力合约更新为当前主力合约"""
  586. try:
  587. # 获取所有期货基础信息
  588. futures = FutureInfo.query.all()
  589. updated_count = 0
  590. for future in futures:
  591. # 如果当前主力合约不为空,且与同花主力合约不同,则更新
  592. if future.current_main_contract and future.current_main_contract != future.th_main_contract:
  593. future.th_main_contract = future.current_main_contract
  594. updated_count += 1
  595. logger.debug(f"同步主力合约: {future.contract_letter} {future.th_main_contract} -> {future.current_main_contract}")
  596. # 提交更改
  597. db.session.commit()
  598. return jsonify({
  599. 'code': 0,
  600. 'msg': f'同步成功,共更新{updated_count}个期货品种的主力合约',
  601. 'data': {
  602. 'updated_count': updated_count
  603. }
  604. })
  605. except Exception as e:
  606. logger.error(f"同步主力合约失败: {str(e)}")
  607. db.session.rollback()
  608. return jsonify({
  609. 'code': 1,
  610. 'msg': f'同步失败: {str(e)}'
  611. })