Эх сурвалжийг харах

添加核心应用代码和配置文件

maxfeng 1 сар өмнө
parent
commit
b045e24d24
58 өөрчлөгдсөн 14069 нэмэгдсэн , 0 устгасан
  1. 52 0
      app.py
  2. 200 0
      app/__init__.py
  3. 80 0
      app/config/candle_info.csv
  4. BIN
      app/config/transaction.xlsx
  5. 194 0
      app/config/trend_info.csv
  6. BIN
      app/config/交易记录_20250409001.xlsx
  7. BIN
      app/config/期货基础信息导入模板_20250330190551.xlsx
  8. 8 0
      app/database/__init__.py
  9. 59 0
      app/database/db_manager.py
  10. 27 0
      app/database/init_db.py
  11. 227 0
      app/database/init_trend_info.py
  12. 4 0
      app/database/migrations/__init__.py
  13. 399 0
      app/database/schema.py
  14. 33 0
      app/init_data.py
  15. 14 0
      app/models/__init__.py
  16. 157 0
      app/models/dimension.py
  17. 128 0
      app/models/future_info.py
  18. 80 0
      app/models/monitor.py
  19. 176 0
      app/models/system.py
  20. 108 0
      app/models/trade.py
  21. 154 0
      app/models/transaction.py
  22. 5 0
      app/routes/__init__.py
  23. 35 0
      app/routes/dimension.py
  24. 713 0
      app/routes/future_info.py
  25. 649 0
      app/routes/monitor.py
  26. 418 0
      app/routes/trade.py
  27. 1048 0
      app/routes/transaction.py
  28. 5 0
      app/services/__init__.py
  29. 290 0
      app/services/config_service.py
  30. 597 0
      app/services/data_scraper.py
  31. 418 0
      app/services/data_update.py
  32. 302 0
      app/services/trade_logic.py
  33. 79 0
      app/templates/base.html
  34. 395 0
      app/templates/future_info/add.html
  35. 181 0
      app/templates/future_info/detail.html
  36. 473 0
      app/templates/future_info/edit.html
  37. 121 0
      app/templates/future_info/import.html
  38. 455 0
      app/templates/future_info/index.html
  39. 51 0
      app/templates/index.html
  40. 639 0
      app/templates/monitor/add.html
  41. 191 0
      app/templates/monitor/detail.html
  42. 498 0
      app/templates/monitor/edit.html
  43. 121 0
      app/templates/monitor/import.html
  44. 571 0
      app/templates/monitor/index.html
  45. 96 0
      app/templates/trade/detail.html
  46. 121 0
      app/templates/trade/import.html
  47. 393 0
      app/templates/trade/index.html
  48. 669 0
      app/templates/transaction/add.html
  49. 267 0
      app/templates/transaction/detail.html
  50. 563 0
      app/templates/transaction/edit.html
  51. 211 0
      app/templates/transaction/import.html
  52. 198 0
      app/templates/transaction/import.html.bak
  53. 505 0
      app/templates/transaction/index.html
  54. 6 0
      config.yaml
  55. 2 0
      data/README.md
  56. 6 0
      main.py
  57. 22 0
      pyproject.toml
  58. 655 0
      uv.lock

+ 52 - 0
app.py

@@ -0,0 +1,52 @@
+"""
+期货数据管理系统入口文件
+"""
+
+from app import create_app
+import os
+from app.database.schema import create_schemas
+import logging
+
+app = create_app()
+
+if __name__ == '__main__':
+    # 设置数据库文件路径
+    data_dir = os.path.join(os.getcwd(), "data")
+    if not os.path.exists(data_dir):
+        os.makedirs(data_dir, exist_ok=True)
+    app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{os.path.join(data_dir, "financial_tools.db")}'
+    
+    # 确保表结构存在
+    create_schemas(app)
+    print("数据库表结构已验证!")
+    
+    # 从配置服务读取服务器配置参数
+    try:
+        from app.services.config_service import get_str_config, get_int_config, get_bool_config
+        
+        # 配置日志级别(支持热更新)
+        log_level_str = get_str_config('log_level', 'DEBUG')
+        log_level = getattr(logging, log_level_str.upper(), logging.DEBUG)
+        logging.basicConfig(
+            level=log_level,
+            format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+            force=True  # 强制重新配置
+        )
+        print(f"日志级别已设置为: {log_level_str}")
+        
+        # 服务器配置(需重启生效)
+        app_host = get_str_config('app_host', '0.0.0.0')
+        app_port = get_int_config('app_port', 4950)
+        debug_mode = get_bool_config('debug_mode', True)
+        
+        print(f"服务器配置: host={app_host}, port={app_port}, debug={debug_mode}")
+        app.run(debug=debug_mode, host=app_host, port=app_port)
+        
+    except Exception as e:
+        # 如果配置服务出错,使用默认配置
+        print(f"配置服务获取失败,使用默认配置: {e}")
+        logging.basicConfig(
+            level=logging.DEBUG,
+            format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+        )
+        app.run(debug=True, host='0.0.0.0', port=4950) 

+ 200 - 0
app/__init__.py

@@ -0,0 +1,200 @@
+"""
+期货数据管理系统的Flask应用包
+"""
+
+import os
+import yaml
+import click
+from flask import Flask
+from app.database.db_manager import db, migrate, init_db
+from app.routes.future_info import bp as future_info_bp
+from app.routes.transaction import bp as transaction_bp
+from app.routes.trade import bp as trade_bp
+from app.routes.monitor import bp as monitor_bp
+from app.routes.dimension import bp as dimension_bp
+
+def _update_flask_config_from_service(app):
+    """
+    从配置服务更新Flask应用配置
+    在数据库初始化完成后调用,尝试从配置数据库更新Flask配置
+    """
+    try:
+        with app.app_context():
+            from app.services.config_service import get_str_config, get_bool_config
+            
+            # 更新支持热更新的配置
+            secret_key = get_str_config('flask_secret_key', app.config.get('SECRET_KEY', 'dev'))
+            if secret_key and secret_key != 'dev':  # 只有在非默认值时才更新
+                app.config['SECRET_KEY'] = secret_key
+                print(f"Flask SECRET_KEY 已从配置服务更新")
+            
+            # 更新SQLAlchemy配置
+            track_modifications = get_bool_config('sqlalchemy_track_modifications', False)
+            app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = track_modifications
+            
+            # 数据库URI通常不支持热更新,但我们可以记录当前配置
+            db_prefix = get_str_config('database_uri_prefix', 'sqlite:///')
+            print(f"数据库配置已确认: 前缀={db_prefix}, 追踪修改={track_modifications}")
+            
+    except Exception as e:
+        print(f"从配置服务更新Flask配置失败,继续使用默认配置: {e}")
+
+def _initialize_config_service(app):
+    """
+    初始化配置服务缓存
+    在数据库完全准备好后调用,预热配置缓存
+    """
+    try:
+        with app.app_context():
+            from app.services.config_service import config_service
+            
+            # 强制刷新配置缓存,预热缓存
+            config_service._refresh_cache()
+            print("配置服务缓存初始化完成")
+            
+    except Exception as e:
+        print(f"配置服务缓存初始化失败: {e}")
+
+def create_app(test_config=None):
+    """创建并配置Flask应用"""
+    app = Flask(__name__, instance_relative_config=True)
+    
+    # 设置默认配置(先使用硬编码默认值)
+    app.config.from_mapping(
+        SECRET_KEY='dev',
+        SQLALCHEMY_DATABASE_URI=f'sqlite:///{os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "financial_tools.db")}',
+        SQLALCHEMY_TRACK_MODIFICATIONS=False,
+    )
+
+    # 加载配置文件
+    if test_config is None:
+        # 尝试从config.yaml加载配置
+        config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.yaml')
+        if os.path.exists(config_path):
+            with open(config_path, 'r', encoding='utf-8') as f:
+                config = yaml.safe_load(f)
+                if config and 'flask' in config:
+                    app.config.update(config['flask'])
+    else:
+        # 使用测试配置
+        app.config.from_mapping(test_config)
+
+    # 确保data文件夹存在
+    data_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data')
+    try:
+        os.makedirs(data_path, exist_ok=True)
+    except OSError:
+        pass
+        
+    # 确保实例文件夹存在
+    try:
+        os.makedirs(app.instance_path)
+    except OSError:
+        pass
+
+    # 初始化数据库
+    init_db(app)
+    
+    # 数据库初始化完成后,尝试从配置服务更新Flask配置
+    _update_flask_config_from_service(app)
+    
+    # 初始化配置服务缓存
+    _initialize_config_service(app)
+
+    # 注册蓝图
+    register_blueprints(app)
+
+    # 初始化数据更新服务
+    from app.services.data_update import init_data_update_service
+    init_data_update_service(app)
+    
+    # 注册命令行命令
+    register_commands(app)
+
+    # 注册首页路由
+    @app.route('/')
+    def index():
+        from flask import render_template
+        return render_template('index.html')
+
+    return app
+
+def register_blueprints(app):
+    app.register_blueprint(future_info_bp)
+    app.register_blueprint(transaction_bp)
+    app.register_blueprint(trade_bp)
+    app.register_blueprint(monitor_bp)
+    app.register_blueprint(dimension_bp)
+
+def register_commands(app):
+    """注册Flask命令行命令"""
+    @app.cli.command('update-future-data')
+    @click.option('--mode', '-m', type=click.Choice(['daily', 'info', 'both']), default='both',
+                  help='更新模式: daily=只更新future_daily表, info=只更新future_info表, both=两者都更新')
+    def update_future_data(mode):
+        """更新期货数据,包括future_daily和future_info表"""
+        from app.models.future_info import FutureInfo, FutureDaily
+        from app.services.data_scraper import FutureDataScraper
+        
+        click.echo(f"开始更新期货数据,模式: {mode}")
+        
+        scraper = FutureDataScraper()
+        
+        if mode in ['daily', 'both']:
+            # 更新future_daily表
+            click.echo("正在更新future_daily表...")
+            records_count = scraper.update_future_daily(db.session, FutureDaily)
+            click.echo(f"future_daily表更新完成,共{records_count}条记录")
+        
+        if mode in ['info', 'both']:
+            if mode == 'both':
+                # 根据future_daily表更新future_info表
+                click.echo("正在根据future_daily表更新future_info表...")
+                updated_count = scraper.update_future_info_from_daily(db.session, FutureInfo, FutureDaily)
+            else:
+                # 直接从网站更新future_info表
+                click.echo("正在直接从网站更新future_info表...")
+                updated_count = scraper.update_future_info(db.session, FutureInfo)
+            click.echo(f"future_info表更新完成,共更新{updated_count}条记录")
+            
+        click.echo("期货数据更新任务完成")
+    
+    @app.cli.command('init-trend-info')
+    @click.option('--force', '-f', is_flag=True, help='强制重新初始化,清空现有数据')
+    def init_trend_info_cmd(force):
+        """初始化trend_info表数据,从config/trend_list.csv导入"""
+        from app.models.dimension import TrendInfo
+        from app.database.init_trend_info import init_trend_info
+        
+        if force:
+            # 如果强制初始化,先清空现有数据
+            click.echo("正在清空trend_info表...")
+            db.session.query(TrendInfo).delete()
+            db.session.commit()
+            click.echo("trend_info表已清空")
+        
+        # 初始化trend_info表
+        click.echo("正在初始化trend_info表...")
+        init_trend_info()
+        count = db.session.query(TrendInfo).count()
+        click.echo(f"trend_info表初始化完成,共{count}条记录")
+
+    @app.cli.command('init-candle-info')
+    @click.option('--force', '-f', is_flag=True, help='强制重新初始化,清空现有数据')
+    def init_candle_info_cmd(force):
+        """初始化candle_info表数据,从config/candle_info.csv导入"""
+        from app.models.dimension import CandleInfo
+        from app.database.init_trend_info import init_candle_info
+        
+        if force:
+            # 如果强制初始化,先清空现有数据
+            click.echo("正在清空candle_info表...")
+            db.session.query(CandleInfo).delete()
+            db.session.commit()
+            click.echo("candle_info表已清空")
+        
+        # 初始化candle_info表
+        click.echo("正在初始化candle_info表...")
+        init_candle_info()
+        count = db.session.query(CandleInfo).count()
+        click.echo(f"candle_info表初始化完成,共{count}条记录") 

+ 80 - 0
app/config/candle_info.csv

@@ -0,0 +1,80 @@
+id,name,,,
+1,上跳,,,
+2,上跳破,,,
+3,上跳跌回,,,
+4,下跳,,,
+5,下跳十字星,,,
+6,下跳涨回,,,
+7,下跳破,,,
+8,十字星,,,
+9,双底,,,
+10,无,,,
+11,涨不上去,,,
+12,破趋势,,,
+13,跌不下来,,,
+14,连续上跳,,,
+15,连续下跌,,,
+16,连续下跳,,,
+17,连续刺不破,,,
+18,连续抬升,,,
+19,连续长上影,,,
+20,长上影,,,
+21,长下影,,,
+22,长阳包前,,,
+23,长阳突破,,,
+24,长阴包前,,,
+25,长阴跌破,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,
+,,,,长上影

BIN
app/config/transaction.xlsx


+ 194 - 0
app/config/trend_info.csv

@@ -0,0 +1,194 @@
+id,category,name,time_range_id,amplitude_id,position_id,speed_type_id,trend_type_id,extra_info
+1,,短期小幅连续上涨,,,,,,
+2,,短期小幅连续下跌,,,,,,
+3,,短期小幅急速上涨,,,,,,
+4,,短期小幅急速下跌,,,,,,
+5,,短期小幅震荡上涨,,,,,,
+6,,短期小幅震荡下跌,,,,,,
+7,,短期中幅连续上涨,,,,,,
+8,,短期中幅连续下跌,,,,,,
+9,,短期中幅急速上涨,,,,,,
+10,,短期中幅急速下跌,,,,,,
+11,,短期中幅震荡上涨,,,,,,
+12,,短期中幅震荡下跌,,,,,,
+13,,短期大幅连续上涨,,,,,,
+14,,短期大幅连续下跌,,,,,,
+15,,短期大幅急速上涨,,,,,,
+16,,短期大幅急速下跌,,,,,,
+17,,短期大幅震荡上涨,,,,,,
+18,,短期大幅震荡下跌,,,,,,
+19,,中期小幅连续上涨,,,,,,
+20,,中期小幅连续下跌,,,,,,
+21,,中期小幅急速上涨,,,,,,
+22,,中期小幅急速下跌,,,,,,
+23,,中期小幅震荡上涨,,,,,,
+24,,中期小幅震荡下跌,,,,,,
+25,,中期中幅连续上涨,,,,,,
+26,,中期中幅连续下跌,,,,,,
+27,,中期中幅急速上涨,,,,,,
+28,,中期中幅急速下跌,,,,,,
+29,,中期中幅震荡上涨,,,,,,
+30,,中期中幅震荡下跌,,,,,,
+31,,中期大幅连续上涨,,,,,,
+32,,中期大幅连续下跌,,,,,,
+33,,中期大幅急速上涨,,,,,,
+34,,中期大幅急速下跌,,,,,,
+35,,中期大幅震荡上涨,,,,,,
+36,,中期大幅震荡下跌,,,,,,
+37,,长期小幅连续上涨,,,,,,
+38,,长期小幅连续下跌,,,,,,
+39,,长期小幅急速上涨,,,,,,
+40,,长期小幅急速下跌,,,,,,
+41,,长期小幅震荡上涨,,,,,,
+42,,长期小幅震荡下跌,,,,,,
+43,,长期中幅连续上涨,,,,,,
+44,,长期中幅连续下跌,,,,,,
+45,,长期中幅急速上涨,,,,,,
+46,,长期中幅急速下跌,,,,,,
+47,,长期中幅震荡上涨,,,,,,
+48,,长期中幅震荡下跌,,,,,,
+49,,长期大幅连续上涨,,,,,,
+50,,长期大幅连续下跌,,,,,,
+51,,长期大幅急速上涨,,,,,,
+52,,长期大幅急速下跌,,,,,,
+53,,长期大幅震荡上涨,,,,,,
+54,,长期大幅震荡下跌,,,,,,
+55,,短期低位小幅震荡,,,,,,
+56,,短期低位小幅震荡且顶部下移,,,,,,
+57,,短期低位小幅震荡且顶部上移,,,,,,
+58,,短期低位小幅震荡且底部下移,,,,,,
+59,,短期低位小幅震荡且底部上移,,,,,,
+60,,短期低位中幅震荡,,,,,,
+61,,短期低位中幅震荡且顶部下移,,,,,,
+62,,短期低位中幅震荡且顶部上移,,,,,,
+63,,短期低位中幅震荡且底部下移,,,,,,
+64,,短期低位中幅震荡且底部上移,,,,,,
+65,,短期低位大幅震荡,,,,,,
+66,,短期低位大幅震荡且顶部下移,,,,,,
+67,,短期低位大幅震荡且顶部上移,,,,,,
+68,,短期低位大幅震荡且底部下移,,,,,,
+69,,短期低位大幅震荡且底部上移,,,,,,
+70,,短期中位小幅震荡,,,,,,
+71,,短期中位小幅震荡且顶部下移,,,,,,
+72,,短期中位小幅震荡且顶部上移,,,,,,
+73,,短期中位小幅震荡且底部下移,,,,,,
+74,,短期中位小幅震荡且底部上移,,,,,,
+75,,短期中位中幅震荡,,,,,,
+76,,短期中位中幅震荡且顶部下移,,,,,,
+77,,短期中位中幅震荡且顶部上移,,,,,,
+78,,短期中位中幅震荡且底部下移,,,,,,
+79,,短期中位中幅震荡且底部上移,,,,,,
+80,,短期中位大幅震荡,,,,,,
+81,,短期中位大幅震荡且顶部下移,,,,,,
+82,,短期中位大幅震荡且顶部上移,,,,,,
+83,,短期中位大幅震荡且底部下移,,,,,,
+84,,短期中位大幅震荡且底部上移,,,,,,
+85,,短期高位小幅震荡,,,,,,
+86,,短期高位小幅震荡且顶部下移,,,,,,
+87,,短期高位小幅震荡且顶部上移,,,,,,
+88,,短期高位小幅震荡且底部下移,,,,,,
+89,,短期高位小幅震荡且底部上移,,,,,,
+90,,短期高位中幅震荡,,,,,,
+91,,短期高位中幅震荡且顶部下移,,,,,,
+92,,短期高位中幅震荡且顶部上移,,,,,,
+93,,短期高位中幅震荡且底部下移,,,,,,
+94,,短期高位中幅震荡且底部上移,,,,,,
+95,,短期高位大幅震荡,,,,,,
+96,,短期高位大幅震荡且顶部下移,,,,,,
+97,,短期高位大幅震荡且顶部上移,,,,,,
+98,,短期高位大幅震荡且底部下移,,,,,,
+99,,短期高位大幅震荡且底部上移,,,,,,
+100,,中期低位小幅震荡,,,,,,
+101,,中期低位小幅震荡且顶部下移,,,,,,
+102,,中期低位小幅震荡且顶部上移,,,,,,
+103,,中期低位小幅震荡且底部下移,,,,,,
+104,,中期低位小幅震荡且底部上移,,,,,,
+105,,中期低位中幅震荡,,,,,,
+106,,中期低位中幅震荡且顶部下移,,,,,,
+107,,中期低位中幅震荡且顶部上移,,,,,,
+108,,中期低位中幅震荡且底部下移,,,,,,
+109,,中期低位中幅震荡且底部上移,,,,,,
+110,,中期低位大幅震荡,,,,,,
+111,,中期低位大幅震荡且顶部下移,,,,,,
+112,,中期低位大幅震荡且顶部上移,,,,,,
+113,,中期低位大幅震荡且底部下移,,,,,,
+114,,中期低位大幅震荡且底部上移,,,,,,
+115,,中期中位小幅震荡,,,,,,
+116,,中期中位小幅震荡且顶部下移,,,,,,
+117,,中期中位小幅震荡且顶部上移,,,,,,
+118,,中期中位小幅震荡且底部下移,,,,,,
+119,,中期中位小幅震荡且底部上移,,,,,,
+120,,中期中位中幅震荡,,,,,,
+121,,中期中位中幅震荡且顶部下移,,,,,,
+122,,中期中位中幅震荡且顶部上移,,,,,,
+123,,中期中位中幅震荡且底部下移,,,,,,
+124,,中期中位中幅震荡且底部上移,,,,,,
+125,,中期中位大幅震荡,,,,,,
+126,,中期中位大幅震荡且顶部下移,,,,,,
+127,,中期中位大幅震荡且顶部上移,,,,,,
+128,,中期中位大幅震荡且底部下移,,,,,,
+129,,中期中位大幅震荡且底部上移,,,,,,
+130,,中期高位小幅震荡,,,,,,
+131,,中期高位小幅震荡且顶部下移,,,,,,
+132,,中期高位小幅震荡且顶部上移,,,,,,
+133,,中期高位小幅震荡且底部下移,,,,,,
+134,,中期高位小幅震荡且底部上移,,,,,,
+135,,中期高位中幅震荡,,,,,,
+136,,中期高位中幅震荡且顶部下移,,,,,,
+137,,中期高位中幅震荡且顶部上移,,,,,,
+138,,中期高位中幅震荡且底部下移,,,,,,
+139,,中期高位中幅震荡且底部上移,,,,,,
+140,,中期高位大幅震荡,,,,,,
+141,,中期高位大幅震荡且顶部下移,,,,,,
+142,,中期高位大幅震荡且顶部上移,,,,,,
+143,,中期高位大幅震荡且底部下移,,,,,,
+144,,中期高位大幅震荡且底部上移,,,,,,
+145,,长期低位小幅震荡,,,,,,
+146,,长期低位小幅震荡且顶部下移,,,,,,
+147,,长期低位小幅震荡且顶部上移,,,,,,
+148,,长期低位小幅震荡且底部下移,,,,,,
+149,,长期低位小幅震荡且底部上移,,,,,,
+150,,长期低位中幅震荡,,,,,,
+151,,长期低位中幅震荡且顶部下移,,,,,,
+152,,长期低位中幅震荡且顶部上移,,,,,,
+153,,长期低位中幅震荡且底部下移,,,,,,
+154,,长期低位中幅震荡且底部上移,,,,,,
+155,,长期低位大幅震荡,,,,,,
+156,,长期低位大幅震荡且顶部下移,,,,,,
+157,,长期低位大幅震荡且顶部上移,,,,,,
+158,,长期低位大幅震荡且底部下移,,,,,,
+159,,长期低位大幅震荡且底部上移,,,,,,
+160,,长期中位小幅震荡,,,,,,
+161,,长期中位小幅震荡且顶部下移,,,,,,
+162,,长期中位小幅震荡且顶部上移,,,,,,
+163,,长期中位小幅震荡且底部下移,,,,,,
+164,,长期中位小幅震荡且底部上移,,,,,,
+165,,长期中位中幅震荡,,,,,,
+166,,长期中位中幅震荡且顶部下移,,,,,,
+167,,长期中位中幅震荡且顶部上移,,,,,,
+168,,长期中位中幅震荡且底部下移,,,,,,
+169,,长期中位中幅震荡且底部上移,,,,,,
+170,,长期中位大幅震荡,,,,,,
+171,,长期中位大幅震荡且顶部下移,,,,,,
+172,,长期中位大幅震荡且顶部上移,,,,,,
+173,,长期中位大幅震荡且底部下移,,,,,,
+174,,长期中位大幅震荡且底部上移,,,,,,
+175,,长期高位小幅震荡,,,,,,
+176,,长期高位小幅震荡且顶部下移,,,,,,
+177,,长期高位小幅震荡且顶部上移,,,,,,
+178,,长期高位小幅震荡且底部下移,,,,,,
+179,,长期高位小幅震荡且底部上移,,,,,,
+180,,长期高位中幅震荡,,,,,,
+181,,长期高位中幅震荡且顶部下移,,,,,,
+182,,长期高位中幅震荡且顶部上移,,,,,,
+183,,长期高位中幅震荡且底部下移,,,,,,
+184,,长期高位中幅震荡且底部上移,,,,,,
+185,,长期高位大幅震荡,,,,,,
+186,,长期高位大幅震荡且顶部下移,,,,,,
+187,,长期高位大幅震荡且顶部上移,,,,,,
+188,,长期高位大幅震荡且底部下移,,,,,,
+189,,长期高位大幅震荡且底部上移,,,,,,
+190,,突破回踩,,,,,,
+191,,突破压力位,,,,,,
+192,,跌破支撑位,,,,,,
+193,,跌破反弹,,,,,,

BIN
app/config/交易记录_20250409001.xlsx


BIN
app/config/期货基础信息导入模板_20250330190551.xlsx


+ 8 - 0
app/database/__init__.py

@@ -0,0 +1,8 @@
+"""
+数据库模块初始化文件
+"""
+
+from app.database.db_manager import db, migrate
+from app.database.schema import create_schemas
+
+__all__ = ['db', 'migrate', 'create_schemas'] 

+ 59 - 0
app/database/db_manager.py

@@ -0,0 +1,59 @@
+"""
+数据库管理器
+负责数据库连接及会话管理
+"""
+
+from flask_sqlalchemy import SQLAlchemy
+from flask_migrate import Migrate
+import os
+
+# 初始化数据库对象
+db = SQLAlchemy()
+migrate = Migrate()
+
+def init_db(app):
+    """
+    初始化数据库连接
+    
+    Args:
+        app: Flask应用实例
+    """
+    # 确保数据目录存在
+    data_dir = os.path.join(os.getcwd(), "data")
+    if not os.path.exists(data_dir):
+        os.makedirs(data_dir, exist_ok=True)
+    
+    # 设置数据库URI
+    sqlite_path = os.path.join(data_dir, "financial_tools.db")
+    app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{sqlite_path}'
+    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
+    
+    # 初始化SQLAlchemy和Migrate
+    db.init_app(app)
+    migrate.init_app(app, db)
+    
+    # 初始化数据库表结构和基础数据
+    with app.app_context():
+        from app.database.schema import create_schemas
+        create_schemas(app)
+        
+        # 初始化trend_info表数据
+        from app.database.init_trend_info import init_trend_info
+        init_trend_info()
+    
+    return db
+
+def get_db():
+    """
+    获取数据库实例
+    
+    Returns:
+        SQLAlchemy实例
+    """
+    return db
+
+def close_db():
+    """
+    关闭数据库连接
+    """
+    db.session.close() 

+ 27 - 0
app/database/init_db.py

@@ -0,0 +1,27 @@
+"""
+数据库初始化脚本
+用于创建数据库和初始化表结构
+"""
+
+from app import create_app
+from app.database.db_manager import db
+from app.database.schema import create_schemas, register_models
+
+def init_db():
+    """
+    初始化数据库
+    创建所有定义的表结构
+    """
+    # 创建Flask应用实例
+    flask_app = create_app()
+    
+    # 确保所有模型都已注册
+    register_models()
+    
+    # 创建表结构并初始化基础数据
+    create_schemas(flask_app)
+    
+    print("数据库表结构已成功创建!")
+
+if __name__ == "__main__":
+    init_db() 

+ 227 - 0
app/database/init_trend_info.py

@@ -0,0 +1,227 @@
+"""
+初始化trend_info表的脚本
+从CSV文件中读取数据,按照规则生成存储趋势信息
+
+根据规则:
+1. 从"name"解析出对应的字段:
+   1.1 首先获得"name"的长度,如果是13,"category"一定是1;如果长度是4或5,"category"一定是2;如果长度是8"category"可能是0或1
+   1.2 如果是13,那么最后5个字符就是"extra_info",剩下的就是8个字符
+   1.3 如果长度是8,分析最后两个字符,如果是"上涨"或"下跌","category"是0,否则是1
+   1.4 针对8个字符的部分,每两个字符为一组,如果"category"是0,对应的分别是"time_range", "amplitude", "speed_type", "trend_type",然后根据内容找到id
+   1.5 针对8个字符的部分,每两个字符为一组,如果"category"是1,对应的分别是"time_range", "position", "amplitude", "trend_type",然后根据内容找到id
+"""
+
+import os
+import csv
+import logging
+from pathlib import Path
+from app import db, create_app
+from app.models.dimension import (
+    TrendInfo, DimTimeRange, DimAmplitude, 
+    DimPosition, DimSpeedType, DimTrendType, CandleInfo
+)
+import pandas as pd
+
+# 配置日志
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+logger = logging.getLogger(__name__)
+
+def load_csv_data():
+    """加载CSV文件数据"""
+    csv_path = Path(__file__).parent.parent / 'config' / 'trend_info.csv'
+    data = []
+    
+    if not csv_path.exists():
+        logger.error(f"CSV文件不存在: {csv_path}")
+        raise FileNotFoundError(f"CSV文件不存在: {csv_path}")
+    
+    try:
+        with open(csv_path, 'r', encoding='utf-8') as file:
+            reader = csv.DictReader(file)
+            for row in reader:
+                data.append(row)
+        
+        logger.info(f"从CSV文件成功加载了{len(data)}条记录")
+        return data
+    except Exception as e:
+        logger.error(f"加载CSV文件时出错: {e}")
+        raise
+
+def init_trend_info():
+    """初始化trend_info表"""
+    # 检查表中是否已有数据
+    if db.session.query(TrendInfo).count() > 0:
+        logger.info("trend_info表已有数据,跳过初始化")
+        return
+    
+    # 加载CSV数据
+    try:
+        data = load_csv_data()
+    except Exception as e:
+        logger.error(f"加载CSV数据失败: {e}")
+        return
+    
+    # 提前查询所有需要的维度数据
+    time_ranges = {tr.name: tr for tr in db.session.query(DimTimeRange).all()}
+    amplitudes = {a.name: a for a in db.session.query(DimAmplitude).all()}
+    positions = {p.name: p for p in db.session.query(DimPosition).all()}
+    speed_types = {s.name: s for s in db.session.query(DimSpeedType).all()}
+    trend_types = {t.name: t for t in db.session.query(DimTrendType).all()}
+    
+    # 创建所有trend_info记录
+    trend_records = []
+    errors = []
+    
+    for i, row in enumerate(data, 1):
+        try:
+            name = row['name'].strip()
+            if not name:
+                logger.warning(f"第{i}行缺少name值,跳过")
+                continue
+                
+            # 根据name的长度和特征确定category及其他字段
+            category, time_range_id, amplitude_id, position_id, speed_type_id, trend_type_id, extra_info = parse_name(
+                name, time_ranges, amplitudes, positions, speed_types, trend_types)
+            
+            # 创建TrendInfo对象
+            trend_info = TrendInfo(
+                category=category,
+                name=name,
+                time_range_id=time_range_id,
+                position_id=position_id,
+                amplitude_id=amplitude_id,
+                speed_type_id=speed_type_id,
+                trend_type_id=trend_type_id,
+                extra_info=extra_info
+            )
+            
+            trend_records.append(trend_info)
+        except Exception as e:
+            error_msg = f"处理第{i}行时出错: {e}, 行数据: {row}"
+            logger.error(error_msg)
+            errors.append(error_msg)
+    
+    if errors:
+        logger.warning(f"初始化过程中有{len(errors)}个错误,请检查日志获取详情")
+    
+    try:
+        # 批量添加记录并提交
+        db.session.add_all(trend_records)
+        db.session.commit()
+        logger.info(f"成功初始化 {len(trend_records)} 条trend_info记录")
+    except Exception as e:
+        db.session.rollback()
+        logger.error(f"提交数据到数据库时出错: {e}")
+        raise
+
+def parse_name(name, time_ranges, amplitudes, positions, speed_types, trend_types):
+    """
+    从name解析出category和其他字段的ID
+    
+    Args:
+        name: 趋势名称
+        time_ranges: 时间范围映射表 {name: object}
+        amplitudes: 幅度范围映射表 {name: object}
+        positions: 位置范围映射表 {name: object}
+        speed_types: 速度类型映射表 {name: object}
+        trend_types: 趋势类型映射表 {name: object}
+        
+    Returns:
+        tuple: (category, time_range_id, amplitude_id, position_id, speed_type_id, trend_type_id, extra_info)
+    """
+    name_length = len(name)
+    
+    # 初始化所有字段为None
+    category = None
+    time_range_id = None
+    amplitude_id = None
+    position_id = None
+    speed_type_id = None
+    trend_type_id = None
+    extra_info = None
+    
+    # 根据长度判断category
+    if name_length == 13:
+        # 长度为13,一定是category=1,带extra_info
+        category = 1
+        extra_info = name[-5:]
+        main_part = name[:8]
+    elif name_length in [4, 5]:
+        # 长度为4或5,一定是category=2,其他字段为空
+        category = 2
+        return category, None, None, None, None, None, None
+    elif name_length == 8:
+        # 长度为8,需要进一步分析
+        main_part = name
+        if name.endswith('上涨') or name.endswith('下跌'):
+            category = 0
+        else:
+            category = 1
+    else:
+        # 其他长度,出错
+        raise ValueError(f"无法处理的名称长度: {name_length}, 名称: {name}")
+    
+    # 拆分main_part为4个两字符的部分
+    parts = [main_part[i:i+2] for i in range(0, 8, 2)]
+    
+    if category == 0:
+        # 对于category=0,顺序是:时间范围、幅度范围、速度类型、趋势类型
+        time_range_name, amplitude_name, speed_type_name, trend_type_name = parts
+        
+        if time_range_name in time_ranges:
+            time_range_id = time_ranges[time_range_name].id
+        else:
+            raise ValueError(f"找不到时间范围: {time_range_name}")
+            
+        if amplitude_name in amplitudes:
+            amplitude_id = amplitudes[amplitude_name].id
+        else:
+            raise ValueError(f"找不到幅度范围: {amplitude_name}")
+            
+        if speed_type_name in speed_types:
+            speed_type_id = speed_types[speed_type_name].id
+        else:
+            raise ValueError(f"找不到速度类型: {speed_type_name}")
+            
+        if trend_type_name in trend_types:
+            trend_type_id = trend_types[trend_type_name].id
+        else:
+            raise ValueError(f"找不到趋势类型: {trend_type_name}")
+    
+    elif category == 1:
+        # 对于category=1,顺序是:时间范围、位置范围、幅度范围、趋势类型(震荡)
+        time_range_name, position_name, amplitude_name, trend_type_name = parts
+        
+        if time_range_name in time_ranges:
+            time_range_id = time_ranges[time_range_name].id
+        else:
+            raise ValueError(f"找不到时间范围: {time_range_name}")
+            
+        if position_name in positions:
+            position_id = positions[position_name].id
+        else:
+            raise ValueError(f"找不到位置范围: {position_name}")
+            
+        if amplitude_name in amplitudes:
+            amplitude_id = amplitudes[amplitude_name].id
+        else:
+            raise ValueError(f"找不到幅度范围: {amplitude_name}")
+            
+        # 对于category=1,趋势类型固定为"震荡"
+        if '震荡' in trend_types:
+            trend_type_id = trend_types['震荡'].id
+        else:
+            raise ValueError("找不到趋势类型: 震荡")
+    
+    return category, time_range_id, amplitude_id, position_id, speed_type_id, trend_type_id, extra_info
+
+if __name__ == "__main__":
+    logger.info("开始执行trend_info表初始化")
+    app = create_app()
+    with app.app_context():
+        try:
+            init_trend_info()
+            logger.info("trend_info表初始化完成")
+        except Exception as e:
+            logger.error(f"初始化trend_info表时发生错误: {e}")
+            raise 

+ 4 - 0
app/database/migrations/__init__.py

@@ -0,0 +1,4 @@
+"""
+数据库迁移脚本目录
+使用Flask-Migrate进行数据库迁移
+""" 

+ 399 - 0
app/database/schema.py

@@ -0,0 +1,399 @@
+"""
+数据库表结构定义
+定义所有表的基本结构和关系
+
+根据BRD文档定义的表结构:
+1. future_info - 期货标的基础信息表
+2. transaction_records - 交易记录表,记录每一笔开仓平仓的具体数据
+3. trade_records - 交易汇总表,对一组开仓平仓交易的汇总记录
+4. monitor_records - 监控标的信息表
+5. 维度相关表:
+   - strategy_info - 交易策略表
+   - candle_info - K线形态表
+   - trend_info - 走势类型基本信息表
+   - dim_time_range - 走势类型的时间范围
+   - dim_amplitude - 走势类型的幅度范围
+   - dim_position - 走势类型的位置范围
+   - dim_speed_type - 走势类型的速度范围
+   - dim_trend_type - 走势类型的趋势范围
+6. future_daily - 每日期货数据更新表
+7. roll_trade_records - 期货换月交易记录表
+"""
+
+from app.database.db_manager import db
+from sqlalchemy import MetaData, text
+import os
+import pandas as pd
+
+# 定义元数据,用于创建表
+metadata = MetaData()
+
+def create_schemas(app):
+    """
+    创建数据库表结构
+    
+    Args:
+        app: Flask应用实例
+    """
+    with app.app_context():
+        # 导入所有模型以确保它们被注册到元数据中
+        import app.models.future_info
+        import app.models.transaction
+        import app.models.trade
+        import app.models.monitor
+        import app.models.dimension
+        import app.models.system
+        
+        # 创建所有表
+        db.create_all()
+        
+        # 初始化维度数据(如果需要)
+        _initialize_dimension_data()
+        
+        # 初始化系统配置数据(如果需要)
+        _initialize_system_config()
+        
+        # 添加表和列的注释
+        _add_comments()
+
+def _add_comments():
+    """
+    添加表和列的详细注释
+    基于BRD文档中的描述
+    """
+    # 这里添加注释逻辑,SQLite不支持注释,所以这里只是作为文档记录
+    # 如果之后切换到支持注释的数据库(如MySQL或PostgreSQL),可以实现这部分逻辑
+    
+    # 例如在PostgreSQL中:
+    # db.session.execute(text("COMMENT ON TABLE future_info IS '期货标的基础信息表';"))
+    # db.session.execute(text("COMMENT ON COLUMN future_info.contract_letter IS '合约字母:1位或者2位的英文字母,这是唯一的';"))
+    pass
+        
+def _initialize_dimension_data():
+    """
+    初始化维度数据
+    创建基本的维度数据,如时间范围、幅度范围、位置范围等
+    
+    BRD文档中描述的维度数据:
+    1. dim_time_range - 短期、中期、长期
+    2. dim_amplitude - 小幅、中幅、大幅
+    3. dim_position - 低位、中位、高位
+    4. dim_speed_type - 急速、连续、震荡
+    5. dim_trend_type - 上涨、下跌、震荡
+    """
+    from app.models.dimension import (
+        DimTimeRange, DimAmplitude, DimPosition, 
+        DimSpeedType, DimTrendType, StrategyInfo, CandleInfo
+    )
+    
+    # 如果表中没有数据,添加初始数据
+    if db.session.query(DimTimeRange).count() == 0:
+        time_ranges = [
+            DimTimeRange(name="短期"),
+            DimTimeRange(name="中期"),
+            DimTimeRange(name="长期")
+        ]
+        db.session.add_all(time_ranges)
+    
+    if db.session.query(DimAmplitude).count() == 0:
+        amplitudes = [
+            DimAmplitude(name="小幅"),
+            DimAmplitude(name="中幅"),
+            DimAmplitude(name="大幅")
+        ]
+        db.session.add_all(amplitudes)
+    
+    if db.session.query(DimPosition).count() == 0:
+        positions = [
+            DimPosition(name="低位"),
+            DimPosition(name="中位"),
+            DimPosition(name="高位")
+        ]
+        db.session.add_all(positions)
+    
+    if db.session.query(DimSpeedType).count() == 0:
+        speed_types = [
+            DimSpeedType(name="急速"),
+            DimSpeedType(name="连续"),
+            DimSpeedType(name="震荡")
+        ]
+        db.session.add_all(speed_types)
+    
+    if db.session.query(DimTrendType).count() == 0:
+        trend_types = [
+            DimTrendType(name="上涨"),
+            DimTrendType(name="下跌"),
+            DimTrendType(name="震荡")
+        ]
+        db.session.add_all(trend_types)
+    
+    # 添加基础策略数据
+    if db.session.query(StrategyInfo).count() == 0:
+        strategies = [
+            StrategyInfo(name="趋势假突破", open_close_type=0, strategy_type=2),
+            StrategyInfo(name="趋势真突破", open_close_type=0, strategy_type=2),
+            StrategyInfo(name="趋势真跌破", open_close_type=0, strategy_type=2),
+            StrategyInfo(name="趋势假跌破", open_close_type=0, strategy_type=2),
+            StrategyInfo(name="压力位真突破", open_close_type=0, strategy_type=0),
+            StrategyInfo(name="压力位假突破", open_close_type=0, strategy_type=0),
+            StrategyInfo(name="支撑位真跌破", open_close_type=0, strategy_type=1),
+            StrategyInfo(name="支撑位假跌破", open_close_type=0, strategy_type=1),
+            StrategyInfo(name="换月", open_close_type=1, strategy_type=3),
+            StrategyInfo(name="涨破5K", open_close_type=1, strategy_type=3),
+            StrategyInfo(name="涨破10K", open_close_type=1, strategy_type=3),
+            StrategyInfo(name="涨破20K", open_close_type=1, strategy_type=3),
+            StrategyInfo(name="涨破30K", open_close_type=1, strategy_type=3),
+            StrategyInfo(name="跌破5K", open_close_type=1, strategy_type=3),
+            StrategyInfo(name="跌破10K", open_close_type=1, strategy_type=3),
+            StrategyInfo(name="跌破20K", open_close_type=1, strategy_type=3),
+            StrategyInfo(name="跌破30K", open_close_type=1, strategy_type=3),
+            StrategyInfo(name="盘中比例止损", open_close_type=1, strategy_type=3),
+            StrategyInfo(name="涨破支撑位", open_close_type=1, strategy_type=3),
+            StrategyInfo(name="跌破支撑位", open_close_type=1, strategy_type=3),
+            StrategyInfo(name="涨破压力位", open_close_type=1, strategy_type=3),
+            StrategyInfo(name="跌破压力位", open_close_type=1, strategy_type=3),
+            StrategyInfo(name="跌破手画压力位", open_close_type=1, strategy_type=3),
+            StrategyInfo(name="换月不继续", open_close_type=1, strategy_type=3),
+            StrategyInfo(name="比例止损", open_close_type=1, strategy_type=3),
+        ]
+        db.session.add_all(strategies)
+    
+    # 添加基础K线形态数据
+    if db.session.query(CandleInfo).count() == 0:
+        # 从CSV文件加载K线形态数据
+        csv_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config', 'candle_info.csv')
+        if os.path.exists(csv_path):
+            df = pd.read_csv(csv_path)
+            candle_patterns = []
+            for _, row in df.iterrows():
+                if pd.notna(row['name']):  # 只添加name不为空的记录
+                    candle_patterns.append(CandleInfo(
+                        id=row['id'],
+                        name=row['name']
+                    ))
+            if candle_patterns:
+                db.session.add_all(candle_patterns)
+        else:
+            # 如果CSV文件不存在,使用默认数据
+            candle_patterns = [
+                CandleInfo(name="连续阳线"),
+                CandleInfo(name="连续阴线"),
+                CandleInfo(name="长阳破位"),
+                CandleInfo(name="长阴破位"),
+                CandleInfo(name="上下影线"),
+                CandleInfo(name="十字星")
+            ]
+            db.session.add_all(candle_patterns)
+    
+    # 提交事务
+    db.session.commit()
+
+def register_models():
+    """
+    注册所有模型
+    确保所有模型都已导入并注册到SQLAlchemy
+    """
+    import app.models.future_info
+    import app.models.transaction
+    import app.models.trade
+    import app.models.monitor
+    import app.models.dimension
+
+def _initialize_system_config():
+    """
+    初始化系统配置数据
+    从CSV文件中读取配置参数并插入到数据库中
+    
+    如果system_config表中已有数据,则跳过初始化
+    """
+    from app.models.system import SystemConfig
+    import csv
+    import os
+    
+    # 如果表中已有数据,跳过初始化
+    if db.session.query(SystemConfig).count() > 0:
+        print("system_config表已有数据,跳过初始化")
+        return
+    
+    # 配置CSV文件路径
+    csv_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'data', 'config_parameters.csv')
+    
+    if not os.path.exists(csv_path):
+        print(f"配置文件不存在: {csv_path}")
+        return
+    
+    try:
+        print("开始初始化系统配置数据...")
+        configs = []
+        
+        with open(csv_path, 'r', encoding='utf-8') as f:
+            reader = csv.DictReader(f)
+            for row_num, row in enumerate(reader, start=2):  # 从第2行开始(第1行是表头)
+                try:
+                    # 验证必要字段
+                    parameter_name = row.get('参数名称', '').strip()
+                    if not parameter_name:
+                        print(f"第{row_num}行:参数名称为空,跳过")
+                        continue
+                    
+                    current_value = row.get('当前值', '').strip()
+                    parameter_type = row.get('参数类型', 'string').strip()
+                    category = row.get('所属分类', '').strip()
+                    
+                    # 创建配置对象
+                    config = SystemConfig(
+                        parameter_name=parameter_name,
+                        current_value=current_value,
+                        parameter_type=parameter_type,
+                        category=category,
+                        code_location=row.get('代码位置', '').strip(),
+                        description=row.get('参数说明', '').strip(),
+                        hot_update_support=row.get('热更新支持', '热更新').strip(),
+                        edit_permission=row.get('修改权限', '管理员').strip(),
+                        importance_level=row.get('重要程度', '中').strip(),
+                        notes=row.get('备注', '').strip(),
+                        is_active=True
+                    )
+                    
+                    configs.append(config)
+                    
+                except Exception as e:
+                    print(f"解析第{row_num}行配置数据时出错: {e}")
+                    continue
+        
+        # 批量插入数据库
+        if configs:
+            db.session.add_all(configs)
+            db.session.commit()
+            print(f"成功初始化 {len(configs)} 条系统配置数据")
+        else:
+            print("没有有效的配置数据可插入")
+            
+    except Exception as e:
+        print(f"初始化系统配置数据时出错: {e}")
+        db.session.rollback()
+
+"""
+BRD文档中表结构详细描述:
+
+future_info表:
+- id: 序号,主键
+- contract_letter: 合约字母,1位或者2位的英文字母,唯一标识
+- name: 名称,可能是中文(还可能包含数字),也可能是英文
+- market: 市场,分为国内(0)和国外(1)
+- exchange: 交易所,3-5位的英文字母
+- contract_multiplier: 合约乘数,数字
+- long_margin_rate: 做多保证金率(按金额),数字
+- short_margin_rate: 做空保证金率(按金额),数字
+- open_fee: 开仓费用(按手),数字
+- close_fee: 平仓费用(按手),数字
+- close_today_rate: 平今费率(按金额),数字
+- close_today_fee: 平今费用(按手),数字
+- th_main_contract: 同花主力合约,字母加上4位数字如PG2503
+- current_main_contract: 当前主力合约,同"同花主力合约"
+- th_order: 同花顺顺序,数字
+- long_term_trend: 长期趋势
+
+transaction_records表:
+- id: 自动生成,主键
+- trade_id: 记录从属于哪个交易,对应"trade_records"里的id
+- transaction_time: 成交时间,年月日小时分
+- contract_code: 合约代码,格式和"同花主力合约"一致
+- name: 名称,和"名称"一致
+- account: 账户,中文,记录期货账户,默认为"华安期货"
+- strategy_ids: 操作策略ID,对应"strategy_info"里的序号
+- strategy_name: 操作策略,对应"strategy_info"里的名称
+- position_type: 多空仓位,0代表开多,1代表平多,2代表开空,3代表平空
+- candle_pattern_id: K线形态ID,对应"candle_info"的id
+- candle_pattern: K线形态,类似"连续上跳+长阳突破"这样的数据
+- price: 成交价格,1位小数
+- volume: 成交手数,实数
+- contract_multiplier: 合约乘数,对应"future_info"的合约乘数
+- transaction_amount: 成交金额,等于成交价格*成交手数*合约乘数
+- fee: 手续费,根据开平仓类型计算
+- volume_change: 手数变化,开仓为正,平仓为负
+- cash_flow: 现金流,根据开平仓类型计算
+- margin: 保证金,根据开平仓类型和合约规则计算
+- trade_type: 交易类别,0代表模拟交易,1代表真实交易
+- status: 交易状态,0代表进行,1代表暂停,2代表暂停进行,3代表结束
+- latest_price: 最新价格,1位小数
+- actual_yield_rate: 实际收益率,百分比2位小数
+- actual_yield: 实际收益,1位小数
+- stop_loss_price: 止损价格,1位小数
+- stop_loss_ratio: 止损比例,1位小数
+- stop_loss_yield: 止损收益,1位小数
+- operation_time: 操作时间,默认为成交时间
+- confidence: 信心指数,0-2
+- similarity: 相似度评估
+- long_term_trend_ids: 长期趋势id,"trend_info"id的list
+- long_term_trend_name: 长期趋势name,多个name合并
+- mid_term_trend_ids: 中期趋势id,"trend_info"id的list
+- mid_term_trend_name: 中期趋势name,多个name合并
+
+trade_records表:
+- id: 自动生成
+- roll_trade_main_id: 换月交易主id,可选
+- contract_code: 合约代码,格式和"同花主力合约"一致
+- name: 名称,格式和"transaction_records"的"名称"一致
+- account: 账户,格式和"transaction_records"的"账户"一致
+- strategy_ids: 操作策略ID,对应"strategy_info"里的序号
+- strategy_name: 操作策略,对应"strategy_info"里的名称
+- position_type: 多空仓位,0代表多头仓位,1代表空头仓位
+- candle_pattern_id: K线形态ID
+- candle_pattern: K线形态
+- open_time: 开仓时间
+- close_time: 平仓时间
+- volume: 持仓手数
+- contract_multiplier: 合约乘数
+- cost_price: 过往持仓成本
+- avg_sell_price: 平均售价
+- single_profit: 单笔收益
+- investment_profit: 投资收益
+- investment_yield: 投资收益率
+- hold_days: 持仓天数
+- annual_yield: 投资年化收益率
+- trade_type: 交易类别,0代表模拟交易,1代表真实交易
+- confidence: 信心指数,0-2
+- similarity: 相似度评估
+- long_term_trend_ids: 长期趋势ids
+- long_term_trend_name: 长期趋势name
+- mid_term_trend_ids: 中期趋势ids
+- mid_term_trend_name: 中期趋势name
+
+monitor_records表:
+- id: 序号,相当于ID
+- contract: 合约,类似"future_info"的"同花主力合约"
+- name: 名称
+- market: 市场,分为国内和国外
+- opportunity: 机会
+- key_price: 关键价格,1位小数
+- long_price: 开多价格,1位小数
+- short_price: 开空价格,1位小数
+- status: 状态,0代表有效,1代表失效,等
+- latest_price: 最新价格
+- long_trigger_price: 开多触发价格,1位小数
+- short_trigger_price: 开空触发价格,1位小数
+- long_margin: 开多一手保证金,1位小数
+- short_margin: 开空一手保证金,1位小数
+- candle_pattern_id: K线形态ID
+- candle_pattern: K线形态
+- long_term_trend_ids: 长期趋势id
+- long_term_trend_name: 长期趋势name
+- mid_term_trend_ids: 中期趋势id
+- mid_term_trend_name: 中期趋势name
+- similarity: 相似度评估
+- possible_trigger_price: 可能触发价格
+- ratio_ref_price: 比例对照价格,0代表最新价格,1代表关键价格
+- ratio: 相应比例
+
+工具类表格:
+1. roll_trade_records - 专门记录期货的换月交易记录
+2. strategy_info - 记录交易策略
+3. candle_info - 记录K线形态
+4. trend_info - 走势类型的基本信息
+5. dim_time_range - 走势类型的时间范围
+6. dim_amplitude - 走势类型的幅度范围
+7. dim_position - 走势类型的位置范围
+8. dim_speed_type - 走势类型的速度范围
+9. dim_trend_type - 走势类型的趋势类型
+""" 

+ 33 - 0
app/init_data.py

@@ -0,0 +1,33 @@
+import os
+import pandas as pd
+from app import db
+from app.models.dimension import CandleInfo
+
+def init_candle_info():
+    """
+    初始化K线形态信息表
+    如果表为空,则从CSV文件导入数据
+    """
+    # 检查表是否为空
+    if CandleInfo.query.first() is None:
+        # 获取CSV文件的绝对路径
+        csv_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'app', 'config', 'candle_info.csv')
+        
+        # 读取CSV文件
+        df = pd.read_csv(csv_path)
+        
+        # 遍历数据并插入到数据库
+        for _, row in df.iterrows():
+            if pd.notna(row['name']):  # 只插入name不为空的记录
+                candle = CandleInfo(
+                    id=row['id'],
+                    name=row['name']
+                )
+                db.session.add(candle)
+        
+        try:
+            db.session.commit()
+            print("K线形态数据初始化成功")
+        except Exception as e:
+            db.session.rollback()
+            print(f"K线形态数据初始化失败: {str(e)}") 

+ 14 - 0
app/models/__init__.py

@@ -0,0 +1,14 @@
+"""
+数据模型初始化文件
+"""
+
+from app.models.future_info import FutureInfo, FutureDaily
+from app.models.transaction import TransactionRecord
+from app.models.trade import TradeRecord, RollTradeRecord
+from app.models.monitor import MonitorRecord
+from app.models.dimension import (
+    StrategyInfo, CandleInfo, TrendInfo,
+    DimTimeRange, DimAmplitude, DimPosition,
+    DimSpeedType, DimTrendType
+)
+from app.models.system import SystemConfig 

+ 157 - 0
app/models/dimension.py

@@ -0,0 +1,157 @@
+"""
+维度数据模型文件
+包含策略信息、K线形态、趋势类型等维度表
+"""
+
+from app import db
+
+class StrategyInfo(db.Model):
+    """
+    交易策略信息表
+    对应BRD文档中的"strategy_info"表
+    """
+    __tablename__ = 'strategy_info'
+
+    id = db.Column(db.Integer, primary_key=True, comment='序号')
+    name = db.Column(db.String(50), nullable=False, comment='名称')
+    open_close_type = db.Column(db.Integer, nullable=False, comment='开平仓类型,0-开仓,1-平仓')
+    strategy_type = db.Column(db.Integer, nullable=False, comment='策略类型,0-压力位,1-支撑位,2-趋势,3-换月')
+    
+    def __repr__(self):
+        return f'<StrategyInfo {self.id} - {self.name}>'
+
+class CandleInfo(db.Model):
+    """
+    K线形态信息表
+    对应BRD文档中的"candle_info"表
+    """
+    __tablename__ = 'candle_info'
+
+    id = db.Column(db.Integer, primary_key=True, comment='序号')
+    name = db.Column(db.String(100), nullable=False, comment='名称')
+    
+    def __repr__(self):
+        return f'<CandleInfo {self.id} - {self.name}>'
+    
+    def to_dict(self):
+        """转换为字典"""
+        return {
+            'id': self.id,
+            'name': self.name
+        }
+
+class TrendInfo(db.Model):
+    """
+    走势类型基本信息表
+    对应BRD文档中的"trend_info"表
+    """
+    __tablename__ = 'trend_info'
+
+    id = db.Column(db.Integer, primary_key=True, comment='ID')
+    category = db.Column(db.Integer, nullable=False, comment='类别,0-上涨下跌类,1-震荡类,2-和压力位支撑位相关类')
+    name = db.Column(db.String(100), comment='名称')
+    time_range_id = db.Column(db.Integer, db.ForeignKey('dim_time_range.id'), comment='时间范围ID')
+    amplitude_id = db.Column(db.Integer, db.ForeignKey('dim_amplitude.id'), comment='幅度范围ID')
+    position_id = db.Column(db.Integer, db.ForeignKey('dim_position.id'), comment='位置范围ID')
+    speed_type_id = db.Column(db.Integer, db.ForeignKey('dim_speed_type.id'), comment='速度类型ID')
+    trend_type_id = db.Column(db.Integer, db.ForeignKey('dim_trend_type.id'), comment='趋势类型ID')
+    extra_info = db.Column(db.String(100), comment='额外信息')
+    
+    # 关联关系
+    time_range = db.relationship('DimTimeRange', backref='trends')
+    amplitude = db.relationship('DimAmplitude', backref='trends')
+    position = db.relationship('DimPosition', backref='trends')
+    speed_type = db.relationship('DimSpeedType', backref='trends')
+    trend_type = db.relationship('DimTrendType', backref='trends')
+    
+    def __repr__(self):
+        return f'<TrendInfo {self.id} - {self.name}>'
+
+class DimTimeRange(db.Model):
+    """
+    走势类型的时间范围维度表
+    对应BRD文档中的"dim_time_range"表
+    """
+    __tablename__ = 'dim_time_range'
+
+    id = db.Column(db.Integer, primary_key=True, comment='ID')
+    name = db.Column(db.String(20), nullable=False, comment='名称')
+    
+    def __repr__(self):
+        return f'<DimTimeRange {self.id} - {self.name}>'
+
+class DimAmplitude(db.Model):
+    """
+    走势类型的幅度范围维度表
+    对应BRD文档中的"dim_amplitude"表
+    """
+    __tablename__ = 'dim_amplitude'
+
+    id = db.Column(db.Integer, primary_key=True, comment='ID')
+    name = db.Column(db.String(20), nullable=False, comment='名称')
+    
+    def __repr__(self):
+        return f'<DimAmplitude {self.id} - {self.name}>'
+
+class DimPosition(db.Model):
+    """
+    走势类型的位置范围维度表
+    对应BRD文档中的"dim_position"表
+    """
+    __tablename__ = 'dim_position'
+
+    id = db.Column(db.Integer, primary_key=True, comment='ID')
+    name = db.Column(db.String(20), nullable=False, comment='名称')
+    
+    def __repr__(self):
+        return f'<DimPosition {self.id} - {self.name}>'
+
+class DimSpeedType(db.Model):
+    """
+    走势类型的速度类型维度表
+    对应BRD文档中的"dim_speed_type"表
+    """
+    __tablename__ = 'dim_speed_type'
+
+    id = db.Column(db.Integer, primary_key=True, comment='ID')
+    name = db.Column(db.String(20), nullable=False, comment='名称')
+    
+    def __repr__(self):
+        return f'<DimSpeedType {self.id} - {self.name}>'
+
+class DimTrendType(db.Model):
+    """
+    走势类型的趋势类型维度表
+    对应BRD文档中的"dim_trend_type"表
+    """
+    __tablename__ = 'dim_trend_type'
+
+    id = db.Column(db.Integer, primary_key=True, comment='ID')
+    name = db.Column(db.String(20), nullable=False, comment='名称')
+    
+    def __repr__(self):
+        return f'<DimTrendType {self.id} - {self.name}>'
+
+class PositionMode(db.Model):
+    """
+    开仓模式表
+    管理不同的开仓模式类型
+    """
+    __tablename__ = 'position_mode'
+
+    id = db.Column(db.Integer, primary_key=True, comment='ID')
+    name = db.Column(db.String(20), nullable=False, comment='名称')
+    type = db.Column(db.Integer, nullable=False, default=0, comment='类型,0-虚拟,1-执行')
+    direction = db.Column(db.Integer, comment='方向,0-多,1-空,NULL-不限')
+    
+    def __repr__(self):
+        return f'<PositionMode {self.id} - {self.name}>'
+    
+    def to_dict(self):
+        """转换为字典"""
+        return {
+            'id': self.id,
+            'name': self.name,
+            'type': self.type,
+            'direction': self.direction
+        } 

+ 128 - 0
app/models/future_info.py

@@ -0,0 +1,128 @@
+"""
+期货基础信息模型文件
+
+定义期货标的的基础信息表结构,包括合约字母、名称、交易所、保证金率等
+"""
+
+from app.database.db_manager import db
+from datetime import datetime
+
+class FutureInfo(db.Model):
+    """
+    期货标的基础信息表
+    对应BRD文档中的"future_info"表
+    
+    维护期货标的及主连的基础信息,记录合约的保证金、手续费等交易参数
+    """
+    __tablename__ = 'future_info'
+
+    id = db.Column(db.Integer, primary_key=True, comment='序号:相当于是ID')
+    contract_letter = db.Column(db.String(2), unique=True, nullable=False, comment='合约字母:1位或者2位的英文字母,这是唯一的')
+    name = db.Column(db.String(50), nullable=False, comment='名称:可能是中文(还可能包含数字),也可能是英文')
+    market = db.Column(db.Integer, nullable=False, comment='市场:分为国内(0)和国外(1)')
+    exchange = db.Column(db.String(5), comment='交易所:3-5位的英文字母,从"future_daily"用合约字母匹配')
+    contract_multiplier = db.Column(db.Float, comment='合约乘数:数字,从"future_daily"用合约字母匹配')
+    long_margin_rate = db.Column(db.Float, comment='做多保证金率(按金额):数字,从"future_daily"用合约字母匹配')
+    short_margin_rate = db.Column(db.Float, comment='做空保证金率(按金额):数字,从"future_daily"用合约字母匹配')
+    open_fee = db.Column(db.Float, comment='开仓费用(按手):数字,从"future_daily"用合约字母匹配')
+    close_fee = db.Column(db.Float, comment='平仓费用(按手):数字,从"future_daily"用合约字母匹配')
+    close_today_rate = db.Column(db.Float, comment='平今费率(按金额):数字,从"future_daily"用合约字母匹配')
+    close_today_fee = db.Column(db.Float, comment='平今费用(按手):数字,从"future_daily"用合约字母匹配')
+    long_margin_amount = db.Column(db.Float, comment='做多1手保证金(金额):数字,根据最新价格计算得出')
+    short_margin_amount = db.Column(db.Float, comment='做空1手保证金(金额):数字,根据最新价格计算得出')
+    th_main_contract = db.Column(db.String(6), comment='同花主力合约:字母加上4位数字,如PG2503')
+    current_main_contract = db.Column(db.String(6), comment='当前主力合约:同"同花主力合约"')
+    th_order = db.Column(db.Integer, comment='同花顺顺序:数字')
+    long_term_trend = db.Column(db.String(100), comment='长期趋势:记录期货品种的长期趋势特征')
+    core_ratio = db.Column(db.Float, comment='核心比率:用于记录期货品种的核心比率信息')
+    
+    def __repr__(self):
+        return f'<FutureInfo {self.contract_letter} - {self.name}>'
+    
+    def to_dict(self):
+        """转换为字典,用于API返回"""
+        return {
+            'id': self.id,
+            'contract_letter': self.contract_letter,
+            'name': self.name,
+            'market': self.market,
+            'exchange': self.exchange,
+            'contract_multiplier': self.contract_multiplier,
+            'long_margin_rate': self.long_margin_rate,
+            'short_margin_rate': self.short_margin_rate,
+            'open_fee': self.open_fee,
+            'close_fee': self.close_fee,
+            'close_today_rate': self.close_today_rate,
+            'close_today_fee': self.close_today_fee,
+            'long_margin_amount': self.long_margin_amount,
+            'short_margin_amount': self.short_margin_amount,
+            'th_main_contract': self.th_main_contract,
+            'current_main_contract': self.current_main_contract,
+            'th_order': self.th_order,
+            'long_term_trend': self.long_term_trend,
+            'core_ratio': self.core_ratio
+        }
+
+class FutureDaily(db.Model):
+    """
+    期货每日数据表
+    存储从网页爬取的期货每日数据
+    
+    该表每日更新,覆盖之前的数据
+    """
+    __tablename__ = 'future_daily'
+    
+    id = db.Column(db.Integer, primary_key=True, comment='自增ID')
+    exchange = db.Column(db.String(10), nullable=False, comment='交易所')
+    contract_code = db.Column(db.String(20), index=True, nullable=False, comment='合约代码')
+    contract_name = db.Column(db.String(50), nullable=False, comment='合约名称')
+    product_code = db.Column(db.String(10), index=True, nullable=False, comment='品种代码')
+    product_name = db.Column(db.String(50), nullable=False, comment='品种名称')
+    contract_multiplier = db.Column(db.Float, nullable=False, comment='合约乘数')
+    price_tick = db.Column(db.Float, nullable=False, comment='跳动变动')
+    open_fee_rate = db.Column(db.Float, comment='开仓费率(按金额)')
+    open_fee = db.Column(db.Float, comment='开仓费用(按手)')
+    close_fee_rate = db.Column(db.Float, comment='平仓费率(按金额)')
+    close_fee = db.Column(db.Float, comment='平仓费用(按手)')
+    close_today_fee_rate = db.Column(db.Float, comment='平今费率(按金额)')
+    close_today_fee = db.Column(db.Float, comment='平今费用(按手)')
+    long_margin_rate = db.Column(db.Float, comment='做多保证金率(按金额)')
+    long_margin_fee = db.Column(db.Float, comment='做多保证金(按手)')
+    short_margin_rate = db.Column(db.Float, comment='做空保证金率(按金额)')
+    short_margin_fee = db.Column(db.Float, comment='做空保证金(按手)')
+    latest_price = db.Column(db.Float, comment='最新价')
+    open_interest = db.Column(db.Integer, comment='持仓量')
+    volume = db.Column(db.Integer, comment='成交量')
+    is_main_contract = db.Column(db.Boolean, default=False, comment='是否是主力合约')
+    update_time = db.Column(db.DateTime, default=datetime.now, comment='更新时间')
+    
+    def __repr__(self):
+        return f'<FutureDaily {self.contract_code} - {self.product_name}>'
+    
+    def to_dict(self):
+        """转换为字典,用于API返回"""
+        return {
+            'id': self.id,
+            'exchange': self.exchange,
+            'contract_code': self.contract_code,
+            'contract_name': self.contract_name,
+            'product_code': self.product_code,
+            'product_name': self.product_name,
+            'contract_multiplier': self.contract_multiplier,
+            'price_tick': self.price_tick,
+            'open_fee_rate': self.open_fee_rate,
+            'open_fee': self.open_fee,
+            'close_fee_rate': self.close_fee_rate,
+            'close_fee': self.close_fee,
+            'close_today_fee_rate': self.close_today_fee_rate,
+            'close_today_fee': self.close_today_fee,
+            'long_margin_rate': self.long_margin_rate,
+            'long_margin_fee': self.long_margin_fee,
+            'short_margin_rate': self.short_margin_rate,
+            'short_margin_fee': self.short_margin_fee,
+            'latest_price': self.latest_price,
+            'open_interest': self.open_interest,
+            'volume': self.volume,
+            'is_main_contract': self.is_main_contract,
+            'update_time': self.update_time.strftime('%Y-%m-%d %H:%M:%S') if self.update_time else None
+        } 

+ 80 - 0
app/models/monitor.py

@@ -0,0 +1,80 @@
+"""
+监控记录模型文件
+"""
+
+from app import db
+
+class MonitorRecord(db.Model):
+    """
+    监控标的信息表
+    对应BRD文档中的"monitor_records"表
+    """
+    __tablename__ = 'monitor_records'
+
+    id = db.Column(db.Integer, primary_key=True, comment='序号')
+    contract = db.Column(db.String(6), nullable=False, comment='合约')
+    name = db.Column(db.String(50), nullable=False, comment='名称')
+    market = db.Column(db.Integer, nullable=False, comment='市场,0-国内,1-国外')
+    opportunity = db.Column(db.String(200), comment='机会')
+    key_price = db.Column(db.Float, comment='关键价格')
+    open_long_price = db.Column(db.Float, comment='开多价格')
+    open_short_price = db.Column(db.Float, comment='开空价格')
+    status = db.Column(db.Integer, nullable=False, default=0, comment='状态,0-有效,1-失效,2-虚拟多,3-虚拟空,4-真实多,5-真实空')
+    latest_price = db.Column(db.Float, comment='最新价格')
+    open_long_trigger_price = db.Column(db.Float, comment='开多触发价格')
+    open_short_trigger_price = db.Column(db.Float, comment='开空触发价格')
+    open_long_margin_per_unit = db.Column(db.Float, comment='开多一手保证金')
+    open_short_margin_per_unit = db.Column(db.Float, comment='开空一手保证金')
+    candle_pattern_id = db.Column(db.Integer, db.ForeignKey('candle_info.id'), comment='K线形态ID')
+    candle_pattern = db.Column(db.String(100), comment='K线形态')
+    candle_pattern_ids = db.Column(db.String(200), comment='K线形态IDs')
+    long_trend_ids = db.Column(db.String(200), comment='长期趋势IDs')
+    long_trend_name = db.Column(db.String(200), comment='长期趋势名称')
+    mid_trend_ids = db.Column(db.String(200), comment='中期趋势IDs')
+    mid_trend_name = db.Column(db.String(200), comment='中期趋势名称')
+    similarity_evaluation = db.Column(db.String(200), comment='相似度评估')
+    possible_trigger_price = db.Column(db.Float, comment='可能触发价格')
+    reference_price_type = db.Column(db.Integer, comment='比例对照价格类型,0-最新价格,1-关键价格')
+    relative_ratio = db.Column(db.Float, comment='相应比例')
+    contract_letter = db.Column(db.String(2), comment='合约字母')
+    open_price = db.Column(db.Float, comment='开仓价格')
+    position_mode_id = db.Column(db.Integer, comment='开仓模式ID')
+    
+    # 关联关系
+    candle = db.relationship('CandleInfo', backref='monitors')
+    
+    def __repr__(self):
+        return f'<MonitorRecord {self.id} - {self.contract}>'
+    
+    def to_dict(self):
+        """转换为字典"""
+        return {
+            'id': self.id,
+            'contract': self.contract,
+            'name': self.name,
+            'market': self.market,
+            'opportunity': self.opportunity,
+            'key_price': self.key_price,
+            'open_long_price': self.open_long_price,
+            'open_short_price': self.open_short_price,
+            'status': self.status,
+            'latest_price': self.latest_price,
+            'open_long_trigger_price': self.open_long_trigger_price,
+            'open_short_trigger_price': self.open_short_trigger_price,
+            'open_long_margin_per_unit': self.open_long_margin_per_unit,
+            'open_short_margin_per_unit': self.open_short_margin_per_unit,
+            'candle_pattern_id': self.candle_pattern_id,
+            'candle_pattern': self.candle_pattern,
+            'candle_pattern_ids': self.candle_pattern_ids,
+            'long_trend_ids': self.long_trend_ids,
+            'long_trend_name': self.long_trend_name,
+            'mid_trend_ids': self.mid_trend_ids,
+            'mid_trend_name': self.mid_trend_name,
+            'similarity_evaluation': self.similarity_evaluation,
+            'possible_trigger_price': self.possible_trigger_price,
+            'reference_price_type': self.reference_price_type,
+            'relative_ratio': self.relative_ratio,
+            'contract_letter': self.contract_letter,
+            'open_price': self.open_price,
+            'position_mode_id': self.position_mode_id
+        } 

+ 176 - 0
app/models/system.py

@@ -0,0 +1,176 @@
+"""
+系统配置模型文件
+
+定义系统配置表结构,用于存储和管理系统的各种配置参数,
+包括服务器配置、业务参数、数据源配置等,支持热更新和权限控制
+"""
+
+from app.database.db_manager import db
+from datetime import datetime
+import json
+
+class SystemConfig(db.Model):
+    """
+    系统配置表
+    用于统一管理系统中的各种配置参数,替代硬编码的配置值
+    
+    支持不同类型的配置值(字符串、数字、布尔、列表等)
+    支持热更新标识和权限控制
+    """
+    __tablename__ = 'system_config'
+
+    id = db.Column(db.Integer, primary_key=True, comment='ID')
+    parameter_name = db.Column(db.String(100), unique=True, nullable=False, comment='参数名称,唯一标识符')
+    current_value = db.Column(db.Text, comment='当前值,以字符串形式存储,根据参数类型解析')
+    parameter_type = db.Column(db.String(20), nullable=False, comment='参数类型:int, float, string, bool, list, json')
+    category = db.Column(db.String(50), nullable=False, comment='所属分类:服务器配置、业务配置等')
+    code_location = db.Column(db.String(200), comment='代码位置:便于定位参数使用位置')
+    description = db.Column(db.String(500), comment='参数说明:详细说明参数的作用')
+    hot_update_support = db.Column(db.String(20), default='热更新', comment='热更新支持:热更新、需重启、不可更新')
+    edit_permission = db.Column(db.String(20), default='管理员', comment='修改权限:系统、运维、管理员')
+    importance_level = db.Column(db.String(10), default='中', comment='重要程度:高、中、低')
+    notes = db.Column(db.String(500), comment='备注:额外说明信息')
+    created_time = db.Column(db.DateTime, default=datetime.now, comment='创建时间')
+    updated_time = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment='最后更新时间')
+    is_active = db.Column(db.Boolean, default=True, comment='是否启用')
+
+    def __repr__(self):
+        return f'<SystemConfig {self.parameter_name}: {self.current_value}>'
+
+    def to_dict(self):
+        """转换为字典格式"""
+        return {
+            'id': self.id,
+            'parameter_name': self.parameter_name,
+            'current_value': self.current_value,
+            'parameter_type': self.parameter_type,
+            'category': self.category,
+            'code_location': self.code_location,
+            'description': self.description,
+            'hot_update_support': self.hot_update_support,
+            'edit_permission': self.edit_permission,
+            'importance_level': self.importance_level,
+            'notes': self.notes,
+            'created_time': self.created_time.isoformat() if self.created_time else None,
+            'updated_time': self.updated_time.isoformat() if self.updated_time else None,
+            'is_active': self.is_active
+        }
+
+    def get_typed_value(self):
+        """
+        根据参数类型返回正确类型的值
+        
+        Returns:
+            根据parameter_type返回对应类型的值
+        """
+        if self.current_value is None:
+            return None
+        
+        # 对于字符串类型,允许空字符串
+        if self.parameter_type == 'string':
+            return str(self.current_value)
+            
+        if not self.current_value:
+            return None
+            
+        try:
+            if self.parameter_type == 'int':
+                return int(self.current_value)
+            elif self.parameter_type == 'float':
+                return float(self.current_value)
+            elif self.parameter_type == 'bool':
+                return self.current_value.upper() in ['TRUE', 'YES', '1', 'ON']
+            elif self.parameter_type == 'list':
+                # 处理简单列表格式,如 [item1,item2,item3]
+                if self.current_value.startswith('[') and self.current_value.endswith(']'):
+                    content = self.current_value[1:-1]  # 去掉方括号
+                    if content.strip():
+                        return [item.strip() for item in content.split(',')]
+                    else:
+                        return []
+                else:
+                    # 如果不是方括号格式,尝试用逗号分割
+                    return [item.strip() for item in self.current_value.split(',')]
+            elif self.parameter_type == 'json':
+                return json.loads(self.current_value)
+            else:  # string 或其他类型
+                return str(self.current_value)
+        except (ValueError, TypeError, json.JSONDecodeError) as e:
+            # 如果转换失败,记录错误并返回原始字符串
+            print(f"配置参数 {self.parameter_name} 类型转换失败: {e}")
+            return str(self.current_value)
+
+    def set_typed_value(self, value):
+        """
+        设置值,自动转换为字符串存储
+        
+        Args:
+            value: 要设置的值,任意类型
+        """
+        if value is None:
+            self.current_value = None
+            return
+            
+        if self.parameter_type == 'list':
+            # 列表格式化为 [item1,item2,item3]
+            if isinstance(value, (list, tuple)):
+                self.current_value = '[' + ','.join(str(item) for item in value) + ']'
+            else:
+                self.current_value = str(value)
+        elif self.parameter_type == 'json':
+            self.current_value = json.dumps(value, ensure_ascii=False)
+        elif self.parameter_type == 'bool':
+            self.current_value = 'TRUE' if value else 'FALSE'
+        else:
+            self.current_value = str(value)
+        
+        self.updated_time = datetime.now()
+
+    @staticmethod
+    def get_config_value(parameter_name, default_value=None):
+        """
+        静态方法:根据参数名获取配置值
+        
+        Args:
+            parameter_name: 参数名称
+            default_value: 默认值
+            
+        Returns:
+            配置的实际类型值,如果不存在则返回默认值
+        """
+        config = SystemConfig.query.filter_by(
+            parameter_name=parameter_name, 
+            is_active=True
+        ).first()
+        
+        if config:
+            return config.get_typed_value()
+        else:
+            return default_value
+
+    @staticmethod 
+    def set_config_value(parameter_name, value):
+        """
+        静态方法:设置配置值
+        
+        Args:
+            parameter_name: 参数名称  
+            value: 要设置的值
+            
+        Returns:
+            bool: 是否设置成功
+        """
+        config = SystemConfig.query.filter_by(parameter_name=parameter_name).first()
+        
+        if config:
+            config.set_typed_value(value)
+            try:
+                db.session.commit()
+                return True
+            except Exception as e:
+                db.session.rollback()
+                print(f"设置配置参数 {parameter_name} 失败: {e}")
+                return False
+        else:
+            print(f"配置参数 {parameter_name} 不存在")
+            return False

+ 108 - 0
app/models/trade.py

@@ -0,0 +1,108 @@
+"""
+交易汇总记录和换月交易记录模型文件
+"""
+
+from app import db
+import datetime
+
+class TradeRecord(db.Model):
+    """
+    交易汇总记录表
+    对应BRD文档中的"trade_records"表
+    """
+    __tablename__ = 'trade_records'
+
+    id = db.Column(db.Integer, primary_key=True, comment='ID')
+    roll_trade_main_id = db.Column(db.Integer, comment='换月交易主ID')
+    contract_code = db.Column(db.String(6), nullable=False, comment='合约代码')
+    name = db.Column(db.String(50), nullable=False, comment='名称')
+    account = db.Column(db.String(20), nullable=False, default='华安期货', comment='账户')
+    strategy_id = db.Column(db.Integer, db.ForeignKey('strategy_info.id'), comment='操作策略ID')
+    strategy_name = db.Column(db.String(50), comment='操作策略')
+    position_type = db.Column(db.Integer, nullable=False, comment='多空仓位,0-多头,1-空头')
+    candle_pattern_id = db.Column(db.Integer, db.ForeignKey('candle_info.id'), comment='K线形态ID')
+    candle_pattern = db.Column(db.String(100), comment='K线形态')
+    open_time = db.Column(db.DateTime, nullable=False, comment='开仓时间')
+    close_time = db.Column(db.DateTime, comment='平仓时间')
+    position_volume = db.Column(db.Float, nullable=False, comment='持仓手数')
+    contract_multiplier = db.Column(db.Float, nullable=False, comment='合约乘数')
+    past_position_cost = db.Column(db.Float, comment='过往持仓成本')
+    average_sale_price = db.Column(db.Float, comment='平均售价')
+    single_profit = db.Column(db.Float, comment='单笔收益')
+    investment_profit = db.Column(db.Float, comment='投资收益')
+    investment_profit_rate = db.Column(db.Float, comment='投资收益率')
+    holding_days = db.Column(db.Integer, comment='持仓天数')
+    annual_profit_rate = db.Column(db.Float, comment='投资年化收益率')
+    trade_type = db.Column(db.Integer, nullable=False, default=0, comment='交易类别,0-模拟交易,1-真实交易')
+    confidence_index = db.Column(db.Float, comment='信心指数,0-2')
+    similarity_evaluation = db.Column(db.String(200), comment='相似度评估')
+    long_trend_ids = db.Column(db.String(200), comment='长期趋势IDs')
+    long_trend_name = db.Column(db.String(200), comment='长期趋势名称')
+    mid_trend_ids = db.Column(db.String(200), comment='中期趋势IDs')
+    mid_trend_name = db.Column(db.String(200), comment='中期趋势名称')
+    
+    # 关联关系
+    strategy = db.relationship('StrategyInfo', backref='trades')
+    candle = db.relationship('CandleInfo', backref='trades')
+    
+    def __repr__(self):
+        return f'<TradeRecord {self.id} - {self.contract_code}>'
+    
+    def to_dict(self):
+        """转换为字典"""
+        return {
+            'id': self.id,
+            'roll_trade_main_id': self.roll_trade_main_id,
+            'contract_code': self.contract_code,
+            'name': self.name,
+            'account': self.account,
+            'strategy_id': self.strategy_id,
+            'strategy_name': self.strategy_name,
+            'position_type': self.position_type,
+            'candle_pattern_ids': self.candle_pattern_id,
+            'candle_pattern': self.candle_pattern,
+            'open_time': self.open_time.strftime('%Y-%m-%d %H:%M') if self.open_time else None,
+            'close_time': self.close_time.strftime('%Y-%m-%d %H:%M') if self.close_time else None,
+            'position_volume': self.position_volume,
+            'contract_multiplier': self.contract_multiplier,
+            'past_position_cost': self.past_position_cost,
+            'average_sale_price': self.average_sale_price,
+            'single_profit': self.single_profit,
+            'investment_profit': self.investment_profit,
+            'investment_profit_rate': self.investment_profit_rate,
+            'holding_days': self.holding_days,
+            'annual_profit_rate': self.annual_profit_rate,
+            'trade_type': self.trade_type,
+            'confidence_index': self.confidence_index,
+            'similarity_evaluation': self.similarity_evaluation,
+            'long_trend_ids': self.long_trend_ids,
+            'long_trend_name': self.long_trend_name,
+            'mid_trend_ids': self.mid_trend_ids,
+            'mid_trend_name': self.mid_trend_name
+        }
+
+class RollTradeRecord(db.Model):
+    """
+    换月交易记录表
+    对应BRD文档中的"roll_trade_records"表
+    """
+    __tablename__ = 'roll_trade_records'
+
+    id = db.Column(db.Integer, primary_key=True, comment='ID')
+    roll_trade_main_id = db.Column(db.Integer, nullable=False, comment='换月交易主ID')
+    related_trade_ids = db.Column(db.String(200), nullable=False, comment='关联交易IDs')
+    contract_letter = db.Column(db.String(2), nullable=False, comment='合约字母')
+    related_contracts = db.Column(db.String(200), nullable=False, comment='关联合约')
+    
+    def __repr__(self):
+        return f'<RollTradeRecord {self.id} - {self.contract_letter}>'
+    
+    def to_dict(self):
+        """转换为字典"""
+        return {
+            'id': self.id,
+            'roll_trade_main_id': self.roll_trade_main_id,
+            'related_trade_ids': self.related_trade_ids,
+            'contract_letter': self.contract_letter,
+            'related_contracts': self.related_contracts
+        } 

+ 154 - 0
app/models/transaction.py

@@ -0,0 +1,154 @@
+"""
+交易记录模型文件
+"""
+
+from app import db
+import datetime
+
+class TransactionRecord(db.Model):
+    """
+    开仓平仓交易记录表
+    对应BRD文档中的"transaction_records"表
+    """
+    __tablename__ = 'transaction_records'
+
+    id = db.Column(db.Integer, primary_key=True, comment='ID')
+    trade_id = db.Column(db.Integer, db.ForeignKey('trade_records.id'), comment='交易ID')
+    roll_id = db.Column(db.Integer, comment='换月ID')
+    transaction_time = db.Column(db.DateTime, nullable=False, default=datetime.datetime.now, comment='成交时间')
+    contract_code = db.Column(db.String(6), nullable=False, comment='合约代码')
+    name = db.Column(db.String(50), nullable=False, comment='名称')
+    account = db.Column(db.String(20), nullable=False, default='华安期货', comment='账户')
+    strategy_ids = db.Column(db.String(200), comment='操作策略IDs')
+    strategy_name = db.Column(db.String(200), comment='操作策略,多个策略用+号连接')
+    position_type = db.Column(db.Integer, nullable=False, comment='多空仓位,0-开多,1-平多,2-开空,3-平空')
+    candle_pattern_ids = db.Column(db.String(200), comment='K线形态IDs')
+    candle_pattern = db.Column(db.String(200), comment='K线形态,多个形态用+号连接')
+    price = db.Column(db.Float, nullable=False, comment='成交价格')
+    volume = db.Column(db.Float, nullable=False, comment='成交手数')
+    contract_multiplier = db.Column(db.Float, nullable=False, comment='合约乘数')
+    amount = db.Column(db.Float, comment='成交金额')
+    fee = db.Column(db.Float, comment='手续费')
+    volume_change = db.Column(db.Float, comment='手数变化')
+    cash_flow = db.Column(db.Float, comment='现金流')
+    margin = db.Column(db.Float, comment='保证金')
+    fund_threshold = db.Column(db.Integer, comment='资金阈值判定,0-可以,1-不可以')
+    trade_type = db.Column(db.Integer, nullable=False, default=0, comment='交易类别,0-模拟交易,1-真实交易')
+    trade_status = db.Column(db.Integer, nullable=False, default=0, comment='交易状态,0-进行,1-暂停,2-暂停进行,3-结束')
+    latest_price = db.Column(db.Float, comment='最新价格')
+    actual_profit_rate = db.Column(db.Float, comment='实际收益率')
+    actual_profit = db.Column(db.Float, comment='实际收益')
+    stop_loss_price = db.Column(db.Float, comment='止损价格')
+    stop_loss_rate = db.Column(db.Float, comment='止损比例')
+    stop_loss_profit = db.Column(db.Float, comment='止损收益')
+    operation_time = db.Column(db.DateTime, default=datetime.datetime.now, comment='操作时间')
+    confidence_index = db.Column(db.Float, comment='信心指数,0-2')
+    similarity_evaluation = db.Column(db.Float, comment='相似度评估,百分比数值')
+    long_trend_ids = db.Column(db.String(200), comment='长期趋势IDs')
+    long_trend_name = db.Column(db.String(200), comment='长期趋势名称')
+    mid_trend_ids = db.Column(db.String(200), comment='中期趋势IDs')
+    mid_trend_name = db.Column(db.String(200), comment='中期趋势名称')
+    
+    # 关联关系
+    trade = db.relationship('TradeRecord', backref=db.backref('transactions', lazy='dynamic'))
+    
+    def __repr__(self):
+        return f'<TransactionRecord {self.id} - {self.contract_code}>'
+    
+    def to_dict(self):
+        """转换为字典"""
+        
+        # Helper function for safe division
+        def safe_division(numerator, denominator):
+            if denominator is None or denominator == 0 or numerator is None:
+                return None
+            try:
+                return numerator / denominator
+            except ZeroDivisionError:
+                return None
+
+        # Helper function for profit/rate calculation direction multiplier
+        def get_direction_multiplier(position_type):
+            # 0=开多, 1=平多 -> Long (+)
+            # 2=开空, 3=平空 -> Short (-)
+            if position_type in [2, 3]:
+                return -1.0
+            return 1.0
+
+        direction_multiplier = get_direction_multiplier(self.position_type)
+
+        # Calculate actual profit rate
+        actual_profit_rate = None
+        if self.latest_price is not None:
+             rate = safe_division(self.latest_price - self.price, self.price)
+             if rate is not None:
+                 actual_profit_rate = direction_multiplier * rate
+
+        # Calculate actual profit
+        actual_profit = None
+        if self.latest_price is not None and self.volume_change is not None and self.contract_multiplier is not None:
+            actual_profit = (self.latest_price - self.price) * self.volume_change * self.contract_multiplier
+            # 根据BRD,实际收益还需要减去手续费,假设开平仓手续费相同
+            if self.fee is not None:
+                 # 乘以2代表开仓和平仓的总手续费,但列表显示的是单条记录,此处逻辑可能需调整
+                 # 暂时按BRD公式 q 计算(假设这是平仓记录且包含了开仓手续费信息或fee字段代表总手续费)
+                 # 或者更合理的做法是仅在汇总记录(TradeRecord)中计算包含手续费的净收益
+                 # 这里暂时不减去fee,保持公式一致性: (最新价格 - 成交价格) * 手数变化 * 合约乘数
+                 # actual_profit = actual_profit - (2 * self.fee) # 暂时注释掉
+                 pass
+
+
+        # Calculate stop loss rate
+        stop_loss_rate = None
+        if self.stop_loss_price is not None:
+            rate = safe_division(self.stop_loss_price - self.price, self.price)
+            if rate is not None:
+                stop_loss_rate = direction_multiplier * rate
+
+        # Calculate stop loss profit
+        stop_loss_profit = None
+        if self.stop_loss_price is not None and self.volume_change is not None and self.contract_multiplier is not None:
+            stop_loss_profit = (self.stop_loss_price - self.price) * self.volume_change * self.contract_multiplier
+            # 同样,根据BRD公式 t,止损收益也应考虑手续费
+            # if self.fee is not None:
+            #    stop_loss_profit = stop_loss_profit - (2 * self.fee) # 暂时注释掉
+            #    pass
+        
+        return {
+            'id': self.id,
+            'trade_id': self.trade_id,
+            'roll_id': self.roll_id,
+            'transaction_time': self.transaction_time.strftime('%Y-%m-%d %H:%M') if self.transaction_time else None,
+            'contract_code': self.contract_code,
+            'name': self.name,
+            'account': self.account,
+            'strategy_ids': self.strategy_ids,
+            'strategy_name': self.strategy_name,
+            'position_type': self.position_type,
+            'candle_pattern_ids': self.candle_pattern_ids,
+            'candle_pattern': self.candle_pattern,
+            'price': self.price,
+            'volume': self.volume,
+            'contract_multiplier': self.contract_multiplier,
+            'amount': self.amount,
+            'fee': self.fee,
+            'volume_change': self.volume_change,
+            'cash_flow': self.cash_flow,
+            'margin': self.margin, # margin 在创建/更新时计算并存储
+            'fund_threshold': self.fund_threshold,
+            'trade_type': self.trade_type,
+            'trade_status': self.trade_status,
+            'latest_price': self.latest_price,
+            'actual_profit_rate': actual_profit_rate, # Calculated
+            'actual_profit': actual_profit,           # Calculated
+            'stop_loss_price': self.stop_loss_price,
+            'stop_loss_rate': stop_loss_rate,         # Calculated
+            'stop_loss_profit': stop_loss_profit,       # Calculated
+            'operation_time': self.operation_time.strftime('%Y-%m-%d %H:%M') if self.operation_time else None,
+            'confidence_index': self.confidence_index,
+            'similarity_evaluation': self.similarity_evaluation,
+            'long_trend_ids': self.long_trend_ids,
+            'long_trend_name': self.long_trend_name,
+            'mid_trend_ids': self.mid_trend_ids,
+            'mid_trend_name': self.mid_trend_name
+        } 

+ 5 - 0
app/routes/__init__.py

@@ -0,0 +1,5 @@
+"""
+路由模块初始化文件
+"""
+
+# 路由模块将在此处被导入 

+ 35 - 0
app/routes/dimension.py

@@ -0,0 +1,35 @@
+from flask import Blueprint, jsonify
+from app.models.dimension import StrategyInfo, CandleInfo, TrendInfo
+from app import db
+
+bp = Blueprint('dimension', __name__, url_prefix='/api/dimension')
+
+@bp.route('/strategy/list_all', methods=['GET'])
+def list_all_strategies():
+    """获取所有策略信息"""
+    try:
+        strategies = StrategyInfo.query.all()
+        strategy_list = [{'id': s.id, 'name': s.name} for s in strategies]
+        return jsonify({'code': 0, 'msg': 'Success', 'data': strategy_list})
+    except Exception as e:
+        return jsonify({'code': 500, 'msg': f'Error fetching strategies: {str(e)}', 'data': []}), 500
+
+@bp.route('/candle/list_all', methods=['GET'])
+def list_all_candles():
+    """获取所有K线形态信息"""
+    try:
+        candles = CandleInfo.query.all()
+        candle_list = [{'id': c.id, 'name': c.name} for c in candles]
+        return jsonify({'code': 0, 'msg': 'Success', 'data': candle_list})
+    except Exception as e:
+        return jsonify({'code': 500, 'msg': f'Error fetching candles: {str(e)}', 'data': []}), 500
+
+@bp.route('/trend/list_all', methods=['GET'])
+def list_all_trends():
+    """获取所有趋势类型信息"""
+    try:
+        trends = TrendInfo.query.all()
+        trend_list = [{'id': t.id, 'name': t.name} for t in trends]
+        return jsonify({'code': 0, 'msg': 'Success', 'data': trend_list})
+    except Exception as e:
+        return jsonify({'code': 500, 'msg': f'Error fetching trends: {str(e)}', 'data': []}), 500 

+ 713 - 0
app/routes/future_info.py

@@ -0,0 +1,713 @@
+"""
+期货基础信息相关路由
+包括期货品种信息的查询、创建、更新等
+"""
+
+from flask import Blueprint, jsonify, request, render_template, send_file, make_response, current_app
+from app.database.db_manager import db
+from app.models.future_info import FutureInfo, FutureDaily
+from app.models.dimension import TrendInfo
+from app.services.data_scraper import FutureDataScraper
+from app.services.data_update import data_update_service
+import pandas as pd
+import io
+import os
+from datetime import datetime
+from werkzeug.utils import secure_filename
+import threading
+import logging
+
+logger = logging.getLogger(__name__)
+
+# 创建蓝图
+bp = Blueprint('future_info', __name__, url_prefix='/api/future_info')
+
+@bp.route('/', methods=['GET'])
+def index():
+    """期货基础信息列表页面 (仅渲染骨架)"""
+    # # 获取查询参数 (不再需要从后端传递数据)
+    # search = request.args.get('search', '')
+    # page = request.args.get('page', 1, type=int)
+    # limit = request.args.get('limit', 10, type=int)
+    
+    # # 构建查询 (不再需要从后端传递数据)
+    # query = FutureInfo.query
+    
+    # # 应用查询条件 (不再需要从后端传递数据)
+    # if search:
+    #     query = query.filter(
+    #         db.or_(
+    #             FutureInfo.contract_letter.like(f'%{search}%'),
+    #             FutureInfo.name.like(f'%{search}%')
+    #         )
+    #     )
+    
+    # # 执行分页查询 (不再需要从后端传递数据)
+    # pagination = query.order_by(FutureInfo.id.asc()).paginate(page=page, per_page=limit, error_out=False)
+    # futures = pagination.items
+    # total = pagination.total
+    
+    # 只渲染模板,数据由前端AJAX获取
+    return render_template('future_info/index.html')
+                           # futures=futures, 
+                           # pagination=pagination, 
+                           # total=total, 
+                           # search=search,
+                           # limit=limit # 传递limit到模板,以便选择器知道当前值
+                           
+
+@bp.route('/add', methods=['GET'])
+def add():
+    """添加期货基础信息页面"""
+    return render_template('future_info/add.html')
+
+@bp.route('/detail/<int:id>', methods=['GET'])
+def detail(id):
+    """期货基础信息详情页面"""
+    future = FutureInfo.query.get_or_404(id)
+    
+    # 获取相关的每日数据(可选)
+    daily_data = FutureDaily.query.filter_by(product_code=future.contract_letter).all()
+    
+    return render_template('future_info/detail.html', future=future, daily_data=daily_data)
+
+@bp.route('/edit/<int:id>', methods=['GET'])
+def edit(id):
+    """编辑期货基础信息页面"""
+    return render_template('future_info/edit.html', future_id=id)
+
+@bp.route('/get/<int:future_id>', methods=['GET'])
+def get_future_info_detail(future_id):
+    """获取期货基础信息详情"""
+    future = FutureInfo.query.get_or_404(future_id)
+    
+    return jsonify({
+        'code': 0, 
+        'msg': '获取成功',
+        'data': future.to_dict()
+    })
+
+@bp.route('/list', methods=['GET'])
+def get_future_info_list():
+    """获取期货基础信息列表 (支持分页和搜索)"""
+    # 获取查询参数
+    page = request.args.get('page', 1, type=int)
+    limit = request.args.get('limit', 10, type=int)
+    search = request.args.get('search', '')
+    # 保留旧的过滤参数,如果需要的话
+    market = request.args.get('market', type=int)
+    contract_letter = request.args.get('contract_letter')
+    name = request.args.get('name')
+    long_term_trend = request.args.get('long_term_trend')
+    future_id = request.args.get('id', type=int)
+    
+    # 构建查询
+    query = FutureInfo.query
+    
+    # 应用过滤条件
+    if market is not None:
+        query = query.filter(FutureInfo.market == market)
+    if contract_letter:
+        query = query.filter(FutureInfo.contract_letter.like(f'%{contract_letter}%'))
+    if name:
+        query = query.filter(FutureInfo.name.like(f'%{name}%'))
+    if long_term_trend:
+        query = query.filter(FutureInfo.long_term_trend.like(f'%{long_term_trend}%'))
+    if future_id is not None:
+        query = query.filter(FutureInfo.id == future_id)
+    
+    # 添加搜索逻辑 (合并contract_letter和name搜索)
+    if search:
+        query = query.filter(
+            db.or_(
+                FutureInfo.contract_letter.like(f'%{search}%'),
+                FutureInfo.name.like(f'%{search}%')
+            )
+        )
+    
+    # 执行分页查询并获取结果
+    pagination = query.order_by(FutureInfo.id.asc()).paginate(page=page, per_page=limit, error_out=False)
+    futures = pagination.items
+    total = pagination.total
+    
+    # 将查询结果转换为JSON格式,列表API中排除core_ratio字段
+    future_data = []
+    for future in futures:
+        data = future.to_dict()
+        # 列表API中不包含核心比率字段
+        data.pop('core_ratio', None)
+        future_data.append(data)
+    
+    result = {
+        'code': 0,
+        'msg': '获取成功',
+        'count': total, # 返回总数以供分页
+        'data': future_data
+    }
+    
+    return jsonify(result)
+
+@bp.route('/update/<int:future_id>', methods=['PUT'])
+def update_future_info(future_id):
+    """更新期货基础信息"""
+    future = FutureInfo.query.get_or_404(future_id)
+    data = request.json
+    
+    # 更新字段
+    if 'contract_letter' in data:
+        future.contract_letter = data['contract_letter']
+    if 'name' in data:
+        future.name = data['name']
+    if 'market' in data:
+        future.market = data['market']
+    if 'exchange' in data:
+        future.exchange = data['exchange']
+    if 'contract_multiplier' in data:
+        future.contract_multiplier = data['contract_multiplier']
+    if 'long_margin_rate' in data:
+        future.long_margin_rate = data['long_margin_rate']
+    if 'short_margin_rate' in data:
+        future.short_margin_rate = data['short_margin_rate']
+    if 'open_fee' in data:
+        future.open_fee = data['open_fee']
+    if 'close_fee' in data:
+        future.close_fee = data['close_fee']
+    if 'close_today_rate' in data:
+        future.close_today_rate = data['close_today_rate']
+    if 'close_today_fee' in data:
+        future.close_today_fee = data['close_today_fee']
+    if 'long_margin_amount' in data:
+        future.long_margin_amount = data['long_margin_amount']
+    if 'short_margin_amount' in data:
+        future.short_margin_amount = data['short_margin_amount']
+    if 'th_main_contract' in data:
+        future.th_main_contract = data['th_main_contract']
+    if 'current_main_contract' in data:
+        future.current_main_contract = data['current_main_contract']
+    if 'th_order' in data:
+        future.th_order = data['th_order']
+    if 'long_term_trend' in data:
+        future.long_term_trend = data['long_term_trend']
+    if 'core_ratio' in data:
+        future.core_ratio = data['core_ratio']
+    
+    # 保存到数据库
+    db.session.commit()
+    
+    return jsonify({
+        'code': 0,
+        'msg': '更新成功',
+        'data': future.to_dict()
+    })
+
+@bp.route('/add', methods=['POST'])
+def create_future_info():
+    """创建期货基础信息"""
+    data = request.json
+    
+    # 创建新记录
+    future_info = FutureInfo(
+        contract_letter=data.get('contract_letter'),
+        name=data.get('name'),
+        market=data.get('market'),
+        exchange=data.get('exchange'),
+        contract_multiplier=data.get('contract_multiplier'),
+        long_margin_rate=data.get('long_margin_rate'),
+        short_margin_rate=data.get('short_margin_rate'),
+        open_fee=data.get('open_fee'),
+        close_fee=data.get('close_fee'),
+        close_today_rate=data.get('close_today_rate'),
+        close_today_fee=data.get('close_today_fee'),
+        long_margin_amount=data.get('long_margin_amount'),
+        short_margin_amount=data.get('short_margin_amount'),
+        th_main_contract=data.get('th_main_contract'),
+        current_main_contract=data.get('current_main_contract'),
+        th_order=data.get('th_order'),
+        long_term_trend=data.get('long_term_trend'),
+        core_ratio=data.get('core_ratio')
+    )
+    
+    # 保存到数据库
+    db.session.add(future_info)
+    db.session.commit()
+    
+    return jsonify({
+        'code': 0,
+        'msg': '创建成功',
+        'data': future_info.to_dict()
+    })
+
+@bp.route('/delete/<int:future_id>', methods=['DELETE'])
+def delete_future_info(future_id):
+    """删除期货基础信息"""
+    future = FutureInfo.query.get_or_404(future_id)
+    
+    # 从数据库中删除
+    db.session.delete(future)
+    db.session.commit()
+    
+    return jsonify({
+        'code': 0,
+        'msg': '删除成功'
+    })
+
+@bp.route('/template', methods=['GET'])
+def get_template():
+    """获取期货基础信息的Excel导入模板"""
+    # 创建DataFrame
+    columns = [
+        '合约字母', '名称', '市场(0-国内,1-国外)', '交易所', '合约乘数', 
+        '做多保证金率', '做空保证金率', '开仓费用', '平仓费用', 
+        '平今费率', '平今费用', '同花主力合约', '当前主力合约', 
+        '同花顺顺序', '长期趋势', '核心比率'
+    ]
+    
+    # 创建示例数据
+    data = [
+        ['CU', '沪铜', 0, 'SHFE', 5, 
+         0.1, 0.1, 3, 3, 
+         0, 0, 'CU2305', 'CU2305', 
+         1, '长期上涨', 0.85]
+    ]
+    
+    df = pd.DataFrame(data, columns=columns)
+    
+    # 创建Excel文件
+    output = io.BytesIO()
+    with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
+        df.to_excel(writer, sheet_name='期货基础信息导入模板', index=False)
+        
+        # 自动调整列宽
+        worksheet = writer.sheets['期货基础信息导入模板']
+        for i, col in enumerate(df.columns):
+            column_width = max(df[col].astype(str).map(len).max(), len(col) + 2)
+            worksheet.set_column(i, i, column_width)
+    
+    output.seek(0)
+    
+    # 设置下载文件名
+    filename = f'期货基础信息导入模板_{datetime.now().strftime("%Y%m%d%H%M%S")}.xlsx'
+    
+    return send_file(
+        output,
+        mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+        as_attachment=True,
+        download_name=filename
+    )
+
+@bp.route('/import', methods=['POST'])
+def import_excel():
+    """从Excel导入期货基础信息"""
+    if 'file' not in request.files:
+        return jsonify({
+            'code': 1,
+            'msg': '没有上传文件'
+        })
+    
+    file = request.files['file']
+    if file.filename == '':
+        return jsonify({
+            'code': 1,
+            'msg': '没有选择文件'
+        })
+    
+    if not file.filename.endswith('.xlsx'):
+        return jsonify({
+            'code': 1,
+            'msg': '请上传Excel文件(.xlsx)'
+        })
+    
+    try:
+        # 读取Excel文件
+        df = pd.read_excel(file)
+        
+        # 验证必填列
+        required_columns = ['合约字母', '名称', '市场(0-国内,1-国外)']
+        for col in required_columns:
+            if col not in df.columns:
+                return jsonify({
+                    'code': 1,
+                    'msg': f'Excel文件缺少必填列: {col}'
+                })
+        
+        # 导入数据
+        success_count = 0
+        error_count = 0
+        error_messages = []
+        
+        for i, row in df.iterrows():
+            try:
+                # 检查是否已存在相同合约字母
+                existing = FutureInfo.query.filter_by(contract_letter=row['合约字母']).first()
+                if existing:
+                    # 更新现有记录
+                    existing.name = row['名称']
+                    existing.market = int(row.get('市场(0-国内,1-国外)', 0))
+                    existing.exchange = row.get('交易所')
+                    existing.contract_multiplier = float(row.get('合约乘数', 0)) if not pd.isna(row.get('合约乘数')) else None
+                    existing.long_margin_rate = float(row.get('做多保证金率', 0)) if not pd.isna(row.get('做多保证金率')) else None
+                    existing.short_margin_rate = float(row.get('做空保证金率', 0)) if not pd.isna(row.get('做空保证金率')) else None
+                    existing.open_fee = float(row.get('开仓费用', 0)) if not pd.isna(row.get('开仓费用')) else None
+                    existing.close_fee = float(row.get('平仓费用', 0)) if not pd.isna(row.get('平仓费用')) else None
+                    existing.close_today_rate = float(row.get('平今费率', 0)) if not pd.isna(row.get('平今费率')) else None
+                    existing.close_today_fee = float(row.get('平今费用', 0)) if not pd.isna(row.get('平今费用')) else None
+                    existing.th_main_contract = row.get('同花主力合约')
+                    existing.current_main_contract = row.get('当前主力合约')
+                    existing.th_order = int(row.get('同花顺顺序', 0)) if not pd.isna(row.get('同花顺顺序')) else None
+                    existing.long_term_trend = row.get('长期趋势')
+                    existing.core_ratio = float(row.get('核心比率', 0)) if not pd.isna(row.get('核心比率')) else None
+                else:
+                    # 创建新记录
+                    future_info = FutureInfo(
+                        contract_letter=row['合约字母'],
+                        name=row['名称'],
+                        market=int(row.get('市场(0-国内,1-国外)', 0)),
+                        exchange=row.get('交易所'),
+                        contract_multiplier=float(row.get('合约乘数', 0)) if not pd.isna(row.get('合约乘数')) else None,
+                        long_margin_rate=float(row.get('做多保证金率', 0)) if not pd.isna(row.get('做多保证金率')) else None,
+                        short_margin_rate=float(row.get('做空保证金率', 0)) if not pd.isna(row.get('做空保证金率')) else None,
+                        open_fee=float(row.get('开仓费用', 0)) if not pd.isna(row.get('开仓费用')) else None,
+                        close_fee=float(row.get('平仓费用', 0)) if not pd.isna(row.get('平仓费用')) else None,
+                        close_today_rate=float(row.get('平今费率', 0)) if not pd.isna(row.get('平今费率')) else None,
+                        close_today_fee=float(row.get('平今费用', 0)) if not pd.isna(row.get('平今费用')) else None,
+                        th_main_contract=row.get('同花主力合约'),
+                        current_main_contract=row.get('当前主力合约'),
+                        th_order=int(row.get('同花顺顺序', 0)) if not pd.isna(row.get('同花顺顺序')) else None,
+                        long_term_trend=row.get('长期趋势'),
+                        core_ratio=float(row.get('核心比率', 0)) if not pd.isna(row.get('核心比率')) else None
+                    )
+                    db.session.add(future_info)
+                success_count += 1
+                
+            except Exception as e:
+                error_count += 1
+                error_messages.append(f'第{i+2}行出错: {str(e)}')
+        
+        # 提交所有更改
+        db.session.commit()
+        
+        return jsonify({
+            'code': 0,
+            'msg': f'成功导入{success_count}条记录,失败{error_count}条',
+            'data': {
+                'success_count': success_count,
+                'error_count': error_count,
+                'error_messages': error_messages
+            }
+        })
+        
+    except Exception as e:
+        return jsonify({
+            'code': 1,
+            'msg': f'导入失败: {str(e)}'
+        })
+
+@bp.route('/import', methods=['GET'])
+def import_view():
+    """导入期货基础信息页面"""
+    return render_template('future_info/import.html')
+
+@bp.route('/update-data', methods=['POST'])
+def update_future_data():
+    """手动触发期货数据更新"""
+    # 获取更新模式
+    data = request.json
+    update_mode = data.get('update_mode', 'both')
+    
+    if update_mode not in ['daily', 'info', 'both']:
+        return jsonify({
+            'code': 1,
+            'msg': '无效的更新模式,有效的选项为: daily, info, both'
+        })
+    
+    # 获取当前应用实例,在线程外部获取避免上下文问题
+    app = current_app._get_current_object()
+    
+    # 在后台线程中执行更新,避免阻塞请求
+    def update_data_thread():
+        try:
+            from app.services.data_update import data_update_service
+            logger.info(f"开始后台更新期货数据,模式: {update_mode}")
+            
+            with app.app_context():
+                if update_mode in ['daily', 'both']:
+                    # 使用统一的数据更新服务
+                    result = data_update_service.manual_update()
+                    if result['code'] == 0:
+                        logger.info(f"数据更新成功: {result['msg']}")
+                        app.config['DATA_UPDATE_COMPLETE'] = True
+                    else:
+                        logger.error(f"数据更新失败: {result['msg']}")
+                        app.config['DATA_UPDATE_COMPLETE'] = False
+                        app.config['DATA_UPDATE_ERROR'] = result['msg']
+                elif update_mode == 'info':
+                    # 仅更新期货信息表
+                    scraper = FutureDataScraper()
+                    updated_count = scraper.update_future_info(db.session, FutureInfo)
+                    logger.info(f"future_info表更新完成,共更新{updated_count}条记录")
+                    app.config['DATA_UPDATE_COMPLETE'] = True
+                    
+        except Exception as e:
+            logger.error(f"更新期货数据时出错: {str(e)}")
+            # 更新失败时设置错误标记
+            try:
+                with app.app_context():
+                    app.config['DATA_UPDATE_COMPLETE'] = False
+                    app.config['DATA_UPDATE_ERROR'] = str(e)
+            except Exception as context_error:
+                logger.error(f"设置更新状态标记失败: {str(context_error)}")
+    
+    # 启动后台线程执行更新
+    thread = threading.Thread(target=update_data_thread)
+    thread.daemon = True
+    thread.start()
+    
+    # 重置完成标记和错误信息 (启动时重置)
+    current_app.config['DATA_UPDATE_COMPLETE'] = None
+    current_app.config['DATA_UPDATE_ERROR'] = None
+    
+    return jsonify({
+        'code': 0,
+        'msg': '期货数据更新已在后台启动,请稍后查看结果或等待页面自动刷新'
+    })
+
+@bp.route('/update-status', methods=['GET'])
+def get_update_status():
+    """检查后台数据更新的状态"""
+    try:
+        complete = current_app.config.get('DATA_UPDATE_COMPLETE')
+        error = current_app.config.get('DATA_UPDATE_ERROR')
+        
+        status = {
+            'code': 0,
+            'data': {
+                'complete': complete,
+                'error': error
+            }
+        }
+        
+        # 如果已完成或出错,清除标记,避免重复通知
+        if complete is not None:
+            # 清除标记的操作移到实际获取状态之后,确保前端能至少获取一次结果
+            # current_app.config['DATA_UPDATE_COMPLETE'] = None
+            # current_app.config['DATA_UPDATE_ERROR'] = None
+            pass
+            
+        return jsonify(status)
+    except Exception as e:
+        logger.error(f"获取更新状态时出错: {str(e)}")
+        return jsonify({
+            'code': 1,
+            'data': {
+                'complete': None,
+                'error': f"获取状态失败: {str(e)}"
+            }
+        })
+
+@bp.route('/daily-list', methods=['GET'])
+def get_future_daily_list():
+    """获取期货每日数据列表"""
+    # 获取查询参数
+    exchange = request.args.get('exchange')
+    product_code = request.args.get('product_code')
+    contract_code = request.args.get('contract_code')
+    is_main_contract = request.args.get('is_main_contract', type=int)
+    
+    # 构建查询
+    query = FutureDaily.query
+    
+    # 应用过滤条件
+    if exchange:
+        query = query.filter(FutureDaily.exchange.like(f'%{exchange}%'))
+    if product_code:
+        query = query.filter(FutureDaily.product_code.like(f'%{product_code}%'))
+    if contract_code:
+        query = query.filter(FutureDaily.contract_code.like(f'%{contract_code}%'))
+    if is_main_contract is not None:
+        query = query.filter(FutureDaily.is_main_contract == bool(is_main_contract))
+    
+    # 执行查询并获取结果
+    daily_data = query.all()
+    
+    # 将查询结果转换为JSON格式
+    result = {
+        'code': 0,
+        'msg': '获取成功',
+        'count': len(daily_data),
+        'data': [daily.to_dict() for daily in daily_data]
+    }
+    
+    return jsonify(result)
+
+@bp.route('/manual_update', methods=['POST'])
+def manual_update():
+    """手动触发数据更新"""
+    return jsonify(data_update_service.manual_update())
+
+@bp.route('/trends', methods=['GET'])
+def get_trend_info_list():
+    """获取趋势信息列表,用于在编辑期货信息时选择长期趋势特征"""
+    
+    # 获取查询参数
+    category = request.args.get('category', type=int)
+    
+    # 构建查询
+    query = TrendInfo.query
+    
+    # 应用过滤条件
+    if category is not None:
+        query = query.filter(TrendInfo.category == category)
+    
+    # 执行查询并获取结果
+    trends = query.all()
+    
+    # 将查询结果转换为JSON格式
+    trend_list = []
+    for trend in trends:
+        trend_data = {
+            'id': trend.id,
+            'category': trend.category,
+            'name': trend.name,
+            'time_range_id': trend.time_range_id,
+            'amplitude_id': trend.amplitude_id,
+            'position_id': trend.position_id,
+            'speed_type_id': trend.speed_type_id,
+            'trend_type_id': trend.trend_type_id,
+            'extra_info': trend.extra_info
+        }
+        trend_list.append(trend_data)
+    
+    result = {
+        'code': 0,
+        'msg': '获取成功',
+        'count': len(trends),
+        'data': trend_list
+    }
+    
+    return jsonify(result)
+
+@bp.route('/validate-trends', methods=['POST'])
+def validate_trend_names():
+    """验证趋势特征名称是否有效"""
+    data = request.json
+    
+    if not data or 'trend_names' not in data:
+        return jsonify({
+            'code': 1,
+            'msg': '缺少趋势特征名称',
+            'data': {'invalid_trends': []}
+        })
+    
+    trend_names = data['trend_names']
+    
+    # 如果为空字符串,视为有效
+    if not trend_names.strip():
+        return jsonify({
+            'code': 0,
+            'msg': '验证成功',
+            'data': {'invalid_trends': []}
+        })
+    
+    # 分割趋势特征名称
+    trend_names_list = [name.strip() for name in trend_names.split('+') if name.strip()]
+    
+    # 查询所有有效的趋势特征名称
+    valid_trends = {trend.name for trend in TrendInfo.query.all()}
+    
+    # 找出无效的趋势特征名称
+    invalid_trends = [name for name in trend_names_list if name not in valid_trends]
+    
+    if invalid_trends:
+        return jsonify({
+            'code': 1,
+            'msg': '存在无效的趋势特征名称',
+            'data': {'invalid_trends': invalid_trends}
+        })
+    
+    return jsonify({
+        'code': 0,
+        'msg': '所有趋势特征名称均有效',
+        'data': {'invalid_trends': []}
+    })
+
+@bp.route('/search', methods=['GET'])
+def search_future_info():
+    """搜索期货品种,支持模糊匹配"""
+    query_text = request.args.get('q', '').strip()
+    limit = request.args.get('limit', 10, type=int)
+    
+    if not query_text:
+        return jsonify({
+            'code': 0,
+            'msg': '成功',
+            'data': []
+        })
+    
+    # 优先精确匹配合约字母,再进行模糊匹配
+    # 首先尝试精确匹配合约字母
+    exact_match = FutureInfo.query.filter(
+        FutureInfo.contract_letter == query_text
+    ).all()
+    
+    # 如果精确匹配有结果,优先返回精确匹配的结果
+    if exact_match:
+        results = exact_match[:limit]
+    else:
+        # 精确匹配无结果时,再进行模糊匹配
+        search_query = FutureInfo.query.filter(
+            db.or_(
+                FutureInfo.name.like(f'%{query_text}%'),
+                FutureInfo.contract_letter.like(f'%{query_text}%')
+            )
+        ).order_by(db.func.length(FutureInfo.contract_letter)).limit(limit)
+        results = search_query.all()
+    
+    return jsonify({
+        'code': 0,
+        'msg': '成功',
+        'data': [
+            {
+                'id': future.id,
+                'name': future.name,
+                'contract_letter': future.contract_letter,
+                'market': future.market,
+                'current_main_contract': future.current_main_contract,
+                'display_text': f"{future.name} ({future.contract_letter})"
+            }
+            for future in results
+        ]
+    })
+
+@bp.route('/sync-main-contracts', methods=['POST'])
+def sync_main_contracts():
+    """同步主力合约:将同花主力合约更新为当前主力合约"""
+    try:
+        # 获取所有期货基础信息
+        futures = FutureInfo.query.all()
+        updated_count = 0
+        
+        for future in futures:
+            # 如果当前主力合约不为空,且与同花主力合约不同,则更新
+            if future.current_main_contract and future.current_main_contract != future.th_main_contract:
+                future.th_main_contract = future.current_main_contract
+                updated_count += 1
+                logger.debug(f"同步主力合约: {future.contract_letter} {future.th_main_contract} -> {future.current_main_contract}")
+        
+        # 提交更改
+        db.session.commit()
+        
+        return jsonify({
+            'code': 0,
+            'msg': f'同步成功,共更新{updated_count}个期货品种的主力合约',
+            'data': {
+                'updated_count': updated_count
+            }
+        })
+        
+    except Exception as e:
+        logger.error(f"同步主力合约失败: {str(e)}")
+        db.session.rollback()
+        return jsonify({
+            'code': 1,
+            'msg': f'同步失败: {str(e)}'
+        }) 

+ 649 - 0
app/routes/monitor.py

@@ -0,0 +1,649 @@
+"""
+监控记录相关路由
+"""
+
+from flask import Blueprint, jsonify, request, render_template, send_file, make_response
+from app import db
+from app.models.monitor import MonitorRecord
+from app.models.future_info import FutureInfo
+from app.models.dimension import PositionMode, CandleInfo
+import pandas as pd
+import io
+import os
+from datetime import datetime
+from werkzeug.utils import secure_filename
+from io import BytesIO
+from openpyxl.utils import get_column_letter
+
+bp = Blueprint('monitor', __name__, url_prefix='/monitor')
+
+@bp.route('/', methods=['GET'])
+def index():
+    """监控记录列表页面"""
+    return render_template('monitor/index.html')
+
+@bp.route('/add', methods=['GET'])
+def add():
+    """添加监控记录页面"""
+    return render_template('monitor/add.html')
+
+@bp.route('/list', methods=['GET'])
+def get_list():
+    """获取监控记录列表"""
+    # 获取筛选参数
+    status_list = request.args.getlist('status')
+    market = request.args.get('market')
+    names = request.args.getlist('name')
+    contract_letters = request.args.getlist('contract_letter')
+    
+    # 构建查询
+    query = MonitorRecord.query
+    
+    if status_list:
+        # 支持多个状态值筛选
+        status_ints = [int(s) for s in status_list if s.isdigit()]
+        if status_ints:
+            query = query.filter(MonitorRecord.status.in_(status_ints))
+    
+    if market is not None:
+        query = query.filter(MonitorRecord.market == int(market))
+    
+    if names:
+        query = query.filter(MonitorRecord.name.in_(names))
+    
+    if contract_letters:
+        # 假设合约代码的前1-2位是合约字母
+        query = query.filter(db.or_(*[MonitorRecord.contract.startswith(letter) for letter in contract_letters]))
+    
+    # 执行查询
+    monitors = query.all()
+    
+    # 返回结果
+    return jsonify({
+        'code': 0,
+        'msg': '成功',
+        'data': [monitor.to_dict() for monitor in monitors]
+    })
+
+@bp.route('/detail/<int:id>', methods=['GET'])
+def detail(id):
+    """监控记录详情页面"""
+    monitor = MonitorRecord.query.get_or_404(id)
+    return render_template('monitor/detail.html', monitor=monitor)
+
+@bp.route('/edit/<int:id>', methods=['GET'])
+def edit(id):
+    """编辑监控记录页面"""
+    monitor = MonitorRecord.query.get_or_404(id)
+    return render_template('monitor/edit.html', monitor=monitor)
+
+@bp.route('/api/detail/<int:id>', methods=['GET'])
+def get_detail(id):
+    """获取监控记录详情API"""
+    monitor = MonitorRecord.query.get_or_404(id)
+    return jsonify({
+        'code': 0,
+        'msg': '成功',
+        'data': monitor.to_dict()
+    })
+
+@bp.route('/create', methods=['POST'])
+def create():
+    """创建监控记录"""
+    data = request.json
+    
+    # 获取关键价格
+    key_price = data.get('key_price')
+    future_info_id = data.get('future_info_id')
+    
+    # 初始化价格字段
+    open_long_price = data.get('open_long_price')
+    open_short_price = data.get('open_short_price')
+    open_long_trigger_price = data.get('open_long_trigger_price')
+    open_short_trigger_price = data.get('open_short_trigger_price')
+    
+    # 从配置服务获取默认监控状态和价格计算系数
+    try:
+        from app.services.config_service import get_int_config, get_float_config
+        default_monitor_status = get_int_config('default_monitor_status', 0)
+        long_price_ratio = get_float_config('long_position_price_ratio', 2/3)
+        short_price_ratio = get_float_config('short_position_price_ratio', 2/3)
+    except Exception as e:
+        print(f"[WARNING] 获取配置失败,使用硬编码值: {e}")
+        default_monitor_status = 0
+        long_price_ratio = 2/3
+        short_price_ratio = 2/3
+    
+    # 如果有关键价格和期货信息ID,则进行自动计算
+    if key_price is not None and future_info_id:
+        future_info = FutureInfo.query.get(future_info_id)
+        if future_info and future_info.core_ratio is not None:
+            core_ratio = future_info.core_ratio
+            # 自动计算各个价格
+            # 做多开仓价 = 关键价格 * (1 + 核心比率 * 2/3)
+            if open_long_price is None:
+                open_long_price = key_price * (1 + core_ratio * 2/3)
+                open_long_price = round(open_long_price, 2)
+            # 做空开仓价 = 关键价格 * (1 - 核心比率 * 2/3)
+            if open_short_price is None:
+                open_short_price = key_price * (1 - core_ratio * 2/3)
+                open_short_price = round(open_short_price, 2)
+            # 做多触发价 = 关键价格 * (1 + 核心比率)
+            if open_long_trigger_price is None:
+                open_long_trigger_price = key_price * (1 + core_ratio)
+                open_long_trigger_price = round(open_long_trigger_price, 2)
+            # 做空触发价 = 关键价格 * (1 - 核心比率)
+            if open_short_trigger_price is None:
+                open_short_trigger_price = key_price * (1 - core_ratio)
+                open_short_trigger_price = round(open_short_trigger_price, 2)
+    # 创建新记录
+    monitor = MonitorRecord(
+        contract=data.get('contract'),
+        name=data.get('name'),
+        market=data.get('market'),
+        opportunity=data.get('opportunity'),
+        key_price=key_price,
+        open_long_price=open_long_price,
+        open_short_price=open_short_price,
+        status=data.get('status', default_monitor_status),
+        latest_price=data.get('latest_price'),
+        open_long_trigger_price=open_long_trigger_price,
+        open_short_trigger_price=open_short_trigger_price,
+        open_long_margin_per_unit=data.get('open_long_margin_per_unit'),
+        open_short_margin_per_unit=data.get('open_short_margin_per_unit'),
+        candle_pattern_id=data.get('candle_pattern_id'),
+        candle_pattern=data.get('candle_pattern'),
+        candle_pattern_ids=data.get('candle_pattern_ids'),
+        long_trend_ids=data.get('long_trend_ids'),
+        long_trend_name=data.get('long_trend_name'),
+        mid_trend_ids=data.get('mid_trend_ids'),
+        mid_trend_name=data.get('mid_trend_name'),
+        similarity_evaluation=data.get('similarity_evaluation'),
+        possible_trigger_price=data.get('possible_trigger_price'),
+        reference_price_type=data.get('reference_price_type'),
+        relative_ratio=data.get('relative_ratio'),
+        contract_letter=data.get('contract_letter'),
+        open_price=data.get('open_price'),
+        position_mode_id=data.get('position_mode_id')
+    )
+    
+    # 保存到数据库
+    try:
+        print(f"[DEBUG] 准备保存监控记录: contract={monitor.contract}, name={monitor.name}")
+        db.session.add(monitor)
+        db.session.commit()
+        print(f"[DEBUG] 监控记录保存成功,ID: {monitor.id}")
+        
+        return jsonify({
+            'code': 0,
+            'msg': '创建成功',
+            'data': monitor.to_dict()
+        })
+    except Exception as e:
+        print(f"[ERROR] 保存监控记录失败: {e}")
+        db.session.rollback()
+        return jsonify({
+            'code': 1,
+            'msg': f'保存失败: {str(e)}'
+        }), 500
+
+@bp.route('/add', methods=['POST'])
+def add_post():
+    """创建监控记录(通过add路由)"""
+    data = request.json
+    
+    print(f"[DEBUG] 监控添加请求数据: {data}")
+    
+    # 从配置服务获取默认监控状态
+    try:
+        from app.services.config_service import get_int_config
+        default_monitor_status = get_int_config('default_monitor_status', 0)
+    except Exception as e:
+        print(f"[WARNING] 获取默认监控状态配置失败,使用硬编码值: {e}")
+        default_monitor_status = 0
+    
+    # 获取关键价格
+    key_price = data.get('key_price')
+    future_info_id = data.get('future_info_id')
+    
+    # 初始化价格字段
+    open_long_price = data.get('open_long_price')
+    open_short_price = data.get('open_short_price')
+    open_long_trigger_price = data.get('open_long_trigger_price')
+    open_short_trigger_price = data.get('open_short_trigger_price')
+    
+    # 如果有关键价格和期货信息ID,则进行自动计算
+    if key_price is not None and future_info_id:
+        future_info = FutureInfo.query.get(future_info_id)
+        if future_info and future_info.core_ratio is not None:
+            core_ratio = future_info.core_ratio
+            # 自动计算各个价格
+            # 做多开仓价 = 关键价格 * (1 + 核心比率 * 2/3)
+            if open_long_price is None:
+                open_long_price = key_price * (1 + core_ratio * 2/3)
+                open_long_price = round(open_long_price, 2)
+            # 做空开仓价 = 关键价格 * (1 - 核心比率 * 2/3)
+            if open_short_price is None:
+                open_short_price = key_price * (1 - core_ratio * 2/3)
+                open_short_price = round(open_short_price, 2)
+            # 做多触发价 = 关键价格 * (1 + 核心比率)
+            if open_long_trigger_price is None:
+                open_long_trigger_price = key_price * (1 + core_ratio)
+                open_long_trigger_price = round(open_long_trigger_price, 2)
+            # 做空触发价 = 关键价格 * (1 - 核心比率)
+            if open_short_trigger_price is None:
+                open_short_trigger_price = key_price * (1 - core_ratio)
+                open_short_trigger_price = round(open_short_trigger_price, 2)
+    # 创建新记录
+    monitor = MonitorRecord(
+        contract=data.get('contract'),
+        name=data.get('name'),
+        market=data.get('market'),
+        opportunity=data.get('opportunity'),
+        key_price=key_price,
+        open_long_price=open_long_price,
+        open_short_price=open_short_price,
+        status=data.get('status', default_monitor_status),
+        latest_price=data.get('latest_price'),
+        open_long_trigger_price=open_long_trigger_price,
+        open_short_trigger_price=open_short_trigger_price,
+        open_long_margin_per_unit=data.get('open_long_margin_per_unit'),
+        open_short_margin_per_unit=data.get('open_short_margin_per_unit'),
+        candle_pattern_id=data.get('candle_pattern_id'),
+        candle_pattern=data.get('candle_pattern'),
+        candle_pattern_ids=data.get('candle_pattern_ids'),
+        long_trend_ids=data.get('long_trend_ids'),
+        long_trend_name=data.get('long_trend_name'),
+        mid_trend_ids=data.get('mid_trend_ids'),
+        mid_trend_name=data.get('mid_trend_name'),
+        similarity_evaluation=data.get('similarity_evaluation'),
+        possible_trigger_price=data.get('possible_trigger_price'),
+        reference_price_type=data.get('reference_price_type'),
+        relative_ratio=data.get('relative_ratio'),
+        contract_letter=data.get('contract_letter'),
+        open_price=data.get('open_price'),
+        position_mode_id=data.get('position_mode_id')
+    )
+    
+    # 保存到数据库
+    try:
+        print(f"[DEBUG] 准备保存监控记录: contract={monitor.contract}, name={monitor.name}")
+        db.session.add(monitor)
+        db.session.commit()
+        print(f"[DEBUG] 监控记录保存成功,ID: {monitor.id}")
+        
+        return jsonify({
+            'code': 0,
+            'msg': '创建成功',
+            'data': monitor.to_dict()
+        })
+    except Exception as e:
+        print(f"[ERROR] 保存监控记录失败: {e}")
+        db.session.rollback()
+        return jsonify({
+            'code': 1,
+            'msg': f'保存失败: {str(e)}'
+        }), 500
+
+@bp.route('/update/<int:id>', methods=['PUT'])
+def update(id):
+    """更新监控记录"""
+    monitor = MonitorRecord.query.get_or_404(id)
+    data = request.json
+    
+    # 更新字段
+    if 'contract' in data:
+        monitor.contract = data['contract']
+    if 'name' in data:
+        monitor.name = data['name']
+    if 'market' in data:
+        monitor.market = data['market']
+    if 'opportunity' in data:
+        monitor.opportunity = data['opportunity']
+    if 'key_price' in data:
+        monitor.key_price = data['key_price']
+    if 'open_long_price' in data:
+        monitor.open_long_price = data['open_long_price']
+    if 'open_short_price' in data:
+        monitor.open_short_price = data['open_short_price']
+    if 'status' in data:
+        monitor.status = data['status']
+    if 'latest_price' in data:
+        monitor.latest_price = data['latest_price']
+    if 'open_long_trigger_price' in data:
+        monitor.open_long_trigger_price = data['open_long_trigger_price']
+    if 'open_short_trigger_price' in data:
+        monitor.open_short_trigger_price = data['open_short_trigger_price']
+    if 'open_long_margin_per_unit' in data:
+        monitor.open_long_margin_per_unit = data['open_long_margin_per_unit']
+    if 'open_short_margin_per_unit' in data:
+        monitor.open_short_margin_per_unit = data['open_short_margin_per_unit']
+    if 'candle_pattern_id' in data:
+        monitor.candle_pattern_id = data['candle_pattern_id']
+    if 'candle_pattern' in data:
+        monitor.candle_pattern = data['candle_pattern']
+    if 'candle_pattern_ids' in data:
+        monitor.candle_pattern_ids = data['candle_pattern_ids']
+    if 'long_trend_ids' in data:
+        monitor.long_trend_ids = data['long_trend_ids']
+    if 'long_trend_name' in data:
+        monitor.long_trend_name = data['long_trend_name']
+    if 'mid_trend_ids' in data:
+        monitor.mid_trend_ids = data['mid_trend_ids']
+    if 'mid_trend_name' in data:
+        monitor.mid_trend_name = data['mid_trend_name']
+    if 'similarity_evaluation' in data:
+        monitor.similarity_evaluation = data['similarity_evaluation']
+    if 'possible_trigger_price' in data:
+        monitor.possible_trigger_price = data['possible_trigger_price']
+    if 'reference_price_type' in data:
+        monitor.reference_price_type = data['reference_price_type']
+    if 'relative_ratio' in data:
+        monitor.relative_ratio = data['relative_ratio']
+    if 'contract_letter' in data:
+        monitor.contract_letter = data['contract_letter']
+    if 'open_price' in data:
+        monitor.open_price = data['open_price']
+    if 'position_mode_id' in data:
+        monitor.position_mode_id = data['position_mode_id']
+    
+    # 保存到数据库
+    try:
+        db.session.commit()
+        print(f"[DEBUG] 监控记录更新成功,ID: {monitor.id}")
+    except Exception as e:
+        print(f"[ERROR] 更新监控记录失败: {e}")
+        db.session.rollback()
+        return jsonify({
+            'code': 1,
+            'msg': f'更新失败: {str(e)}'
+        }), 500
+    
+    return jsonify({
+        'code': 0,
+        'msg': '更新成功',
+        'data': monitor.to_dict()
+    })
+
+@bp.route('/invalidate/<int:id>', methods=['PUT'])
+def invalidate(id):
+    """标记监控记录为失效"""
+    monitor = MonitorRecord.query.get_or_404(id)
+    
+    # 更新状态为已失效(3)
+    monitor.status = 3
+    
+    try:
+        db.session.commit()
+        return jsonify({
+            'code': 0,
+            'msg': '标记失效成功',
+            'data': monitor.to_dict()
+        })
+    except Exception as e:
+        db.session.rollback()
+        return jsonify({
+            'code': 1,
+            'msg': f'标记失效失败: {str(e)}'
+        }), 500
+
+@bp.route('/delete/<int:id>', methods=['DELETE'])
+def delete(id):
+    """删除监控记录"""
+    monitor = MonitorRecord.query.get_or_404(id)
+    
+    # 从数据库删除
+    db.session.delete(monitor)
+    db.session.commit()
+    
+    return jsonify({
+        'code': 0,
+        'msg': '删除成功'
+    })
+
+@bp.route('/import', methods=['GET'])
+def import_view():
+    """导入监控记录页面"""
+    return render_template('monitor/import.html')
+
+@bp.route('/get_template', methods=['GET'])
+def get_template():
+    """Generate and return an Excel template for data import."""
+    # 创建一个DataFrame,包含需要的列
+    df = pd.DataFrame(columns=[
+        '合约代码', '名称', '市场类型', '关注原因', '关注状态', '备注'
+    ])
+    
+    # 添加示例数据(可选)
+    df.loc[0] = ['IF2212', '沪深300期货2212', '0', '价格突破', '1', '重点关注']
+    df.loc[1] = ['SC2301', '原油期货2301', '1', '季节性变化', '0', '暂时观察']
+    
+    # 创建一个字节流
+    output = BytesIO()
+    
+    # 使用ExcelWriter以便于设置列宽
+    with pd.ExcelWriter(output, engine='openpyxl') as writer:
+        df.to_excel(writer, index=False, sheet_name='监控记录导入模板')
+        worksheet = writer.sheets['监控记录导入模板']
+        
+        # 调整列宽
+        for i, col in enumerate(df.columns):
+            column_width = max(len(col) * 2, 15)
+            worksheet.column_dimensions[get_column_letter(i + 1)].width = column_width
+    
+    output.seek(0)
+    
+    # 返回Excel文件
+    return send_file(
+        output,
+        as_attachment=True,
+        download_name='监控记录导入模板.xlsx',
+        mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+    )
+
+@bp.route('/import', methods=['POST'])
+def import_excel():
+    """Import monitor records from Excel file."""
+    if 'file' not in request.files:
+        return jsonify({
+            'code': 1,
+            'msg': '没有上传文件'
+        })
+    
+    file = request.files['file']
+    if file.filename == '':
+        return jsonify({
+            'code': 1,
+            'msg': '未选择文件'
+        })
+    
+    if not file.filename.endswith('.xlsx'):
+        return jsonify({
+            'code': 1,
+            'msg': '请上传Excel文件(.xlsx)'
+        })
+    
+    try:
+        # 读取Excel文件
+        df = pd.read_excel(file)
+        
+        # 检查必需的列
+        required_columns = ['合约代码', '名称', '市场类型']
+        missing_columns = [col for col in required_columns if col not in df.columns]
+        
+        if missing_columns:
+            return jsonify({
+                'code': 1,
+                'msg': f'缺少必要的列: {", ".join(missing_columns)}'
+            })
+        
+        # 准备导入数据
+        success_count = 0
+        error_count = 0
+        error_messages = []
+        
+        for index, row in df.iterrows():
+            try:
+                # 检查必填字段
+                if pd.isna(row['合约代码']) or pd.isna(row['名称']) or pd.isna(row['市场类型']):
+                    error_count += 1
+                    error_messages.append(f"第{index+2}行: 合约代码、名称和市场类型为必填项")
+                    continue
+                
+                # 创建监控记录
+                monitor = MonitorRecord(
+                    contract=row['合约代码'],
+                    name=row['名称'],
+                    market=int(row['市场类型']),
+                    opportunity=row['关注原因'] if not pd.isna(row['关注原因']) else None,
+                    status=int(row['关注状态']) if not pd.isna(row['关注状态']) else 0
+                )
+                
+                db.session.add(monitor)
+                success_count += 1
+                
+            except Exception as e:
+                error_count += 1
+                error_messages.append(f"第{index+2}行: {str(e)}")
+        
+        # 提交事务
+        db.session.commit()
+        
+        return jsonify({
+            'code': 0,
+            'msg': f'成功导入{success_count}条记录',
+            'data': {
+                'success_count': success_count,
+                'error_count': error_count,
+                'error_messages': error_messages
+            }
+        })
+        
+    except Exception as e:
+        db.session.rollback()
+        return jsonify({
+            'code': 1,
+            'msg': f'导入失败: {str(e)}'
+        })
+
+# 新增 API 端点:根据 future_info_id 或 contract_code 查询 FutureInfo
+@bp.route('/api/future_info/lookup', methods=['GET'])
+def lookup_future_info():
+    future_info_id = request.args.get('future_info_id', type=int)
+    contract_code = request.args.get('contract_code')
+
+    future = None
+    if future_info_id:
+        future = FutureInfo.query.get(future_info_id)
+    elif contract_code:
+        # 从合约代码中提取合约字母:去掉右侧4位数字
+        import re
+        # 使用正则表达式匹配:字母开头,后面跟4位数字
+        match = re.match(r'^([A-Za-z]+)(\d{4})$', contract_code)
+        if match:
+            letter = match.group(1)  # 提取字母部分
+            # 精确匹配合约字母
+            future = FutureInfo.query.filter(
+                FutureInfo.contract_letter == letter
+            ).first()
+
+    if future:
+        return jsonify({
+            'code': 0,
+            'msg': '成功',
+            'data': {
+                'name': future.name,
+                'market_type': future.market  # 修正字段名称
+            }
+        })
+    else:
+        return jsonify({'code': 1, 'msg': '未找到匹配的期货信息', 'data': None})
+
+# 新增 API 端点:根据期货信息ID和关键价格计算各个价格
+@bp.route('/api/calculate_prices', methods=['POST'])
+def calculate_prices():
+    """根据期货信息ID和关键价格计算各个价格"""
+    data = request.json
+    future_info_id = data.get('future_info_id')
+    key_price = data.get('key_price')
+    
+    if not future_info_id or key_price is None:
+        return jsonify({
+            'code': 1,
+            'msg': '缺少必要参数:future_info_id 或 key_price'
+        })
+    
+    # 查询期货信息
+    future_info = FutureInfo.query.get(future_info_id)
+    if not future_info:
+        return jsonify({
+            'code': 1,
+            'msg': '未找到对应的期货信息'
+        })
+    
+    if future_info.core_ratio is None:
+        return jsonify({
+            'code': 1,
+            'msg': '该期货品种未设置核心比率'
+        })
+    
+    core_ratio = future_info.core_ratio
+    
+    # 计算各个价格,保留2位小数
+    calculated_prices = {
+        'open_long_price': round(key_price * (1 + core_ratio * 2/3), 2),        # 做多开仓价
+        'open_short_price': round(key_price * (1 - core_ratio * 2/3), 2),       # 做空开仓价  
+        'open_long_trigger_price': round(key_price * (1 + core_ratio), 2),      # 做多触发价
+        'open_short_trigger_price': round(key_price * (1 - core_ratio), 2),     # 做空触发价
+        'core_ratio': core_ratio                                                 # 返回核心比率用于显示
+    }
+    
+    return jsonify({
+        'code': 0,
+        'msg': '计算成功',
+        'data': calculated_prices
+    })
+
+# 新增 API 端点:获取开仓模式列表
+@bp.route('/api/position_modes', methods=['GET'])
+def get_position_modes():
+    """获取开仓模式列表"""
+    modes = PositionMode.query.all()
+    return jsonify({
+        'code': 0,
+        'msg': '成功',
+        'data': [mode.to_dict() for mode in modes]
+    })
+
+# 新增 API 端点:获取K线形态列表
+@bp.route('/api/candle_patterns', methods=['GET'])
+def get_candle_patterns():
+    """获取K线形态列表"""
+    patterns = CandleInfo.query.all()
+    return jsonify({
+        'code': 0,
+        'msg': '成功',
+        'data': [pattern.to_dict() for pattern in patterns]
+    })
+
+# 新增 API 端点:根据合约字母获取保证金信息
+@bp.route('/api/margin_info/<contract_letter>', methods=['GET'])
+def get_margin_info(contract_letter):
+    """根据合约字母获取保证金信息"""
+    future_info = FutureInfo.query.filter_by(contract_letter=contract_letter).first()
+    if future_info:
+        return jsonify({
+            'code': 0,
+            'msg': '成功',
+            'data': {
+                'long_margin_amount': future_info.long_margin_amount,
+                'short_margin_amount': future_info.short_margin_amount,
+                'contract_multiplier': future_info.contract_multiplier
+            }
+        })
+    else:
+        return jsonify({
+            'code': 1,
+            'msg': '未找到对应的期货信息',
+            'data': None
+        }) 

+ 418 - 0
app/routes/trade.py

@@ -0,0 +1,418 @@
+"""
+交易汇总记录相关路由
+"""
+
+from flask import Blueprint, jsonify, request, render_template, send_file, make_response
+from app import db
+from app.models.trade import TradeRecord, RollTradeRecord
+from app.models.transaction import TransactionRecord
+from datetime import datetime
+import pandas as pd
+import io
+import os
+from openpyxl.utils import get_column_letter
+from io import BytesIO
+from sqlalchemy import func
+from app.services.trade_logic import sync_all_trades_from_transactions
+
+bp = Blueprint('trade', __name__, url_prefix='/trade')
+
+@bp.route('/', methods=['GET'])
+def index():
+    """交易汇总记录列表页面"""
+    return render_template('trade/index.html')
+
+@bp.route('/list', methods=['GET'])
+def get_list():
+    """获取交易汇总记录列表"""
+    try:
+        print("获取交易汇总记录列表...")
+        # 获取分页参数
+        page = request.args.get('page', 1, type=int)
+        limit = request.args.get('limit', 10, type=int)
+        
+        # 获取筛选参数
+        start_time = request.args.get('start_time')
+        end_time = request.args.get('end_time')
+        names = request.args.getlist('name')
+        contract_letters = request.args.getlist('contract_letter')
+        contract_code = request.args.get('contract_code')
+        strategy_ids = request.args.getlist('strategy_id')
+        trade_type = request.args.get('trade_type')
+        position_type = request.args.get('position_type')
+        
+        # 构建查询
+        query = TradeRecord.query
+        
+        if start_time:
+            query = query.filter(TradeRecord.open_time >= datetime.strptime(start_time, '%Y-%m-%d'))
+        
+        if end_time:
+            query = query.filter(TradeRecord.open_time <= datetime.strptime(end_time, '%Y-%m-%d'))
+        
+        if names:
+            query = query.filter(TradeRecord.name.in_(names))
+        
+        if contract_letters:
+            # 假设合约代码的前1-2位是合约字母
+            query = query.filter(db.or_(*[TradeRecord.contract_code.startswith(letter) for letter in contract_letters]))
+        
+        if contract_code:
+            query = query.filter(TradeRecord.contract_code.like(f'%{contract_code}%'))
+        
+        if strategy_ids:
+            try:
+                strategy_ids = [int(i) for i in strategy_ids if i.strip()]
+                if strategy_ids:
+                    query = query.filter(TradeRecord.strategy_id.in_(strategy_ids))
+            except ValueError:
+                pass
+        
+        if trade_type is not None and trade_type.strip():
+            try:
+                query = query.filter(TradeRecord.trade_type == int(trade_type))
+            except ValueError:
+                pass
+        
+        if position_type is not None and position_type.strip():
+            try:
+                query = query.filter(TradeRecord.position_type == int(position_type))
+            except ValueError:
+                pass
+        
+        # 执行分页查询
+        pagination = query.order_by(TradeRecord.open_time.desc()).paginate(page=page, per_page=limit, error_out=False)
+        trades = pagination.items
+        total = pagination.total
+        print(f"找到{len(trades)}条交易汇总记录,总共{total}条")
+        
+        # 返回结果
+        return jsonify({
+            'code': 0,
+            'msg': '成功',
+            'count': total,
+            'data': [trade.to_dict() for trade in trades]
+        })
+    except Exception as e:
+        print(f"获取交易汇总记录列表失败: {str(e)}")
+        import traceback
+        print(traceback.format_exc())
+        return jsonify({
+            'code': 1,
+            'msg': f'获取列表失败: {str(e)}',
+            'count': 0,
+            'data': []
+        })
+
+@bp.route('/detail/<int:id>', methods=['GET'])
+def get_detail(id):
+    """获取交易汇总记录详情,改为渲染模板"""
+    trade = TradeRecord.query.get_or_404(id)
+    # 如果需要关联查询其他信息(如策略、K线形态、趋势名称),可以在这里进行
+    # 例如,查询策略名称
+    # strategy = StrategyInfo.query.get(trade.strategy_id) if trade.strategy_id else None
+    # trade_data = trade.to_dict()
+    # trade_data['strategy_name'] = strategy.name if strategy else trade.strategy_name # 优先使用查询到的名称
+    
+    return render_template('trade/detail.html', trade=trade)
+
+@bp.route('/delete/<int:id>', methods=['DELETE'])
+def delete(id):
+    """删除交易汇总记录"""
+    trade = TradeRecord.query.get_or_404(id)
+    
+    # 从数据库删除
+    db.session.delete(trade)
+    db.session.commit()
+    
+    return jsonify({
+        'code': 0,
+        'msg': '删除成功'
+    })
+
+# 换月交易记录相关路由
+@bp.route('/roll/list', methods=['GET'])
+def get_roll_list():
+    """获取换月交易记录列表"""
+    roll_trades = RollTradeRecord.query.all()
+    
+    return jsonify({
+        'code': 0,
+        'msg': '成功',
+        'data': [roll_trade.to_dict() for roll_trade in roll_trades]
+    })
+
+@bp.route('/roll/detail/<int:id>', methods=['GET'])
+def get_roll_detail(id):
+    """获取换月交易记录详情"""
+    roll_trade = RollTradeRecord.query.get_or_404(id)
+    return jsonify({
+        'code': 0,
+        'msg': '成功',
+        'data': roll_trade.to_dict()
+    })
+
+@bp.route('/roll/create', methods=['POST'])
+def create_roll():
+    """创建换月交易记录"""
+    data = request.json
+    
+    roll_trade = RollTradeRecord(
+        roll_trade_main_id=data.get('roll_trade_main_id'),
+        related_trade_ids=data.get('related_trade_ids'),
+        contract_letter=data.get('contract_letter'),
+        related_contracts=data.get('related_contracts')
+    )
+    
+    db.session.add(roll_trade)
+    db.session.commit()
+    
+    return jsonify({
+        'code': 0,
+        'msg': '创建成功',
+        'data': roll_trade.to_dict()
+    })
+
+@bp.route('/export', methods=['GET'])
+def export():
+    """导出交易汇总记录为Excel文件"""
+    # 获取所有交易记录
+    trades = TradeRecord.query.all()
+    
+    # 转换为DataFrame
+    data = []
+    for trade in trades:
+        data.append({
+            '合约代码': trade.contract_code,
+            '名称': trade.name,
+            '账户': trade.account,
+            '策略': trade.strategy_name,
+            '持仓类型': trade.position_type,
+            'K线形态': trade.candle_pattern,
+            '开仓时间': trade.open_time.strftime('%Y-%m-%d %H:%M') if trade.open_time else '',
+            '平仓时间': trade.close_time.strftime('%Y-%m-%d %H:%M') if trade.close_time else '',
+            '持仓量': trade.position_volume,
+            '合约乘数': trade.contract_multiplier,
+            '持仓成本': trade.past_position_cost,
+            '平均售价': trade.average_sale_price,
+            '单笔利润': trade.single_profit,
+            '投资利润': trade.investment_profit,
+            '投资收益率': trade.investment_profit_rate,
+            '持仓天数': trade.holding_days,
+            '年化收益率': trade.annual_profit_rate,
+            '交易类型': trade.trade_type,
+            '置信指数': trade.confidence_index,
+            '相似度评价': trade.similarity_evaluation,
+            '长期趋势': trade.long_trend_name,
+            '中期趋势': trade.mid_trend_name,
+        })
+    
+    df = pd.DataFrame(data)
+    
+    # 创建Excel文件
+    output = io.BytesIO()
+    with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
+        df.to_excel(writer, sheet_name='交易汇总', index=False)
+        
+        # 自动调整列宽
+        worksheet = writer.sheets['交易汇总']
+        for i, col in enumerate(df.columns):
+            column_width = max(df[col].astype(str).map(len).max(), len(col) + 2)
+            worksheet.set_column(i, i, column_width)
+    
+    output.seek(0)
+    
+    # 设置下载文件名
+    filename = f'交易汇总_{datetime.now().strftime("%Y%m%d%H%M%S")}.xlsx'
+    
+    return send_file(
+        output,
+        mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+        as_attachment=True,
+        download_name=filename
+    )
+
+@bp.route('/template', methods=['GET'])
+def get_template():
+    """获取交易汇总记录的Excel导入模板"""
+    # 创建DataFrame
+    columns = [
+        '换月交易主ID', '合约代码', '名称', '账户', 
+        '操作策略ID', '操作策略', '多空仓位(0-多头,1-空头)', 
+        'K线形态ID', 'K线形态', '开仓时间', '平仓时间', 
+        '持仓手数', '合约乘数', '过往持仓成本', '平均售价', 
+        '单笔收益', '投资收益', '投资收益率', '持仓天数', '年化收益率', 
+        '交易类别(0-模拟,1-真实)', '信心指数', '相似度评估', 
+        '长期趋势IDs', '长期趋势名称', '中期趋势IDs', '中期趋势名称'
+    ]
+    
+    # 创建示例数据
+    data = [
+        [None, 'CU2305', '沪铜', '华安期货', 
+         1, '趋势突破', 0, 
+         1, '突破回踩', '2023-03-29 14:30', '2023-03-30 14:30', 
+         1, 5, 68000, 68500, 
+         2500, 2500, 0.0735, 1, 26.86, 
+         0, 1.5, '80%相似', 
+         '1,2', '长期上涨+短期震荡', '3,4', '中期下跌+短期震荡']
+    ]
+    
+    df = pd.DataFrame(data, columns=columns)
+    
+    # 创建Excel文件
+    output = io.BytesIO()
+    with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
+        df.to_excel(writer, sheet_name='交易汇总导入模板', index=False)
+        
+        # 自动调整列宽
+        worksheet = writer.sheets['交易汇总导入模板']
+        for i, col in enumerate(df.columns):
+            column_width = max(df[col].astype(str).map(len).max(), len(col) + 2)
+            worksheet.set_column(i, i, column_width)
+    
+    output.seek(0)
+    
+    # 设置下载文件名
+    filename = f'交易汇总导入模板_{datetime.now().strftime("%Y%m%d%H%M%S")}.xlsx'
+    
+    return send_file(
+        output,
+        mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+        as_attachment=True,
+        download_name=filename
+    )
+
+@bp.route('/import_page', methods=['GET'])
+def import_page():
+    """Display the import page."""
+    return render_template('trade/import.html')
+
+@bp.route('/import', methods=['POST'])
+def import_excel():
+    """从Excel导入交易汇总记录"""
+    if 'file' not in request.files:
+        return jsonify({
+            'code': 1,
+            'msg': '没有上传文件'
+        })
+    
+    file = request.files['file']
+    if file.filename == '':
+        return jsonify({
+            'code': 1,
+            'msg': '没有选择文件'
+        })
+    
+    if not file.filename.endswith('.xlsx'):
+        return jsonify({
+            'code': 1,
+            'msg': '请上传Excel文件(.xlsx)'
+        })
+    
+    try:
+        # 读取Excel文件
+        df = pd.read_excel(file)
+        
+        # 验证必填列
+        required_columns = ['合约代码', '名称', '多空仓位(0-多头,1-空头)', '开仓时间', '持仓手数', '合约乘数']
+        for col in required_columns:
+            if col not in df.columns:
+                return jsonify({
+                    'code': 1,
+                    'msg': f'Excel文件缺少必填列: {col}'
+                })
+        
+        # 导入数据
+        success_count = 0
+        error_count = 0
+        error_messages = []
+        
+        for i, row in df.iterrows():
+            try:
+                # 处理日期时间
+                open_time = None
+                if '开仓时间' in row and not pd.isna(row['开仓时间']):
+                    if isinstance(row['开仓时间'], str):
+                        open_time = datetime.strptime(row['开仓时间'], '%Y-%m-%d %H:%M')
+                    else:
+                        open_time = row['开仓时间']
+                else:
+                    open_time = datetime.now()
+                
+                close_time = None
+                if '平仓时间' in row and not pd.isna(row['平仓时间']):
+                    if isinstance(row['平仓时间'], str):
+                        close_time = datetime.strptime(row['平仓时间'], '%Y-%m-%d %H:%M')
+                    else:
+                        close_time = row['平仓时间']
+                
+                # 计算持仓天数
+                holding_days = None
+                if close_time and open_time:
+                    holding_days = (close_time - open_time).days
+                
+                # 创建新记录
+                trade = TradeRecord(
+                    roll_trade_main_id=int(row['换月交易主ID']) if not pd.isna(row.get('换月交易主ID')) else None,
+                    contract_code=row['合约代码'],
+                    name=row['名称'],
+                    account=row.get('账户', '华安期货'),
+                    strategy_id=row.get('操作策略ID'),
+                    strategy_name=row.get('操作策略'),
+                    position_type=int(row['多空仓位(0-多头,1-空头)']),
+                    candle_pattern_id=row.get('K线形态ID'),
+                    candle_pattern=row.get('K线形态'),
+                    open_time=open_time,
+                    close_time=close_time,
+                    position_volume=float(row['持仓手数']),
+                    contract_multiplier=float(row['合约乘数']),
+                    past_position_cost=float(row.get('过往持仓成本', 0)) if not pd.isna(row.get('过往持仓成本')) else None,
+                    average_sale_price=float(row.get('平均售价', 0)) if not pd.isna(row.get('平均售价')) else None,
+                    single_profit=float(row.get('单笔收益', 0)) if not pd.isna(row.get('单笔收益')) else None,
+                    investment_profit=float(row.get('投资收益', 0)) if not pd.isna(row.get('投资收益')) else None,
+                    investment_profit_rate=float(row.get('投资收益率', 0)) if not pd.isna(row.get('投资收益率')) else None,
+                    holding_days=holding_days,
+                    annual_profit_rate=float(row.get('年化收益率', 0)) if not pd.isna(row.get('年化收益率')) else None,
+                    trade_type=int(row.get('交易类别(0-模拟,1-真实)', 0)) if not pd.isna(row.get('交易类别(0-模拟,1-真实)')) else 0,
+                    confidence_index=float(row.get('信心指数', 0)) if not pd.isna(row.get('信心指数')) else None,
+                    similarity_evaluation=row.get('相似度评估'),
+                    long_trend_ids=row.get('长期趋势IDs'),
+                    long_trend_name=row.get('长期趋势名称'),
+                    mid_trend_ids=row.get('中期趋势IDs'),
+                    mid_trend_name=row.get('中期趋势名称')
+                )
+                
+                # 保存到数据库
+                db.session.add(trade)
+                success_count += 1
+                
+            except Exception as e:
+                error_count += 1
+                error_messages.append(f'第{i+2}行出错: {str(e)}')
+        
+        # 提交所有更改
+        db.session.commit()
+        
+        return jsonify({
+            'code': 0,
+            'msg': f'成功导入{success_count}条记录,失败{error_count}条',
+            'data': {
+                'success_count': success_count,
+                'error_count': error_count,
+                'error_messages': error_messages
+            }
+        })
+        
+    except Exception as e:
+        return jsonify({
+            'code': 1,
+            'msg': f'导入失败: {str(e)}'
+        }) 
+
+@bp.route('/sync_all', methods=['POST'])
+def sync_all():
+    """全面同步交易汇总记录"""
+    print("接收到全面同步请求...")
+    result = sync_all_trades_from_transactions()
+    print(f"全面同步完成,结果: {result}")
+    return jsonify(result) 

+ 1048 - 0
app/routes/transaction.py

@@ -0,0 +1,1048 @@
+"""
+交易记录相关路由
+"""
+
+from flask import Blueprint, jsonify, request, render_template, send_file, make_response
+from app import db
+from app.models.transaction import TransactionRecord
+from app.models.future_info import FutureInfo
+from app.models.dimension import StrategyInfo, CandleInfo, TrendInfo
+from datetime import datetime
+import pandas as pd
+import io
+import os
+from werkzeug.utils import secure_filename
+from sqlalchemy import text
+import uuid
+import tempfile
+import logging
+
+logger = logging.getLogger(__name__)
+
+bp = Blueprint('transaction', __name__, url_prefix='/transaction')
+
+@bp.route('/', methods=['GET'])
+def index():
+    """交易记录列表页面"""
+    return render_template('transaction/index.html')
+
+@bp.route('/add', methods=['GET'])
+def add():
+    """添加交易记录页面"""
+    from app.models.trade import TradeRecord
+    
+    # 检查是否是平仓操作
+    close_for_trade_id = request.args.get('close_for')
+    close_for_transaction_id = request.args.get('close_for_transaction')
+    close_trade_data = None
+    
+    if close_for_trade_id:
+        try:
+            # 查询要平仓的交易记录
+            trade_to_close = TradeRecord.query.get(int(close_for_trade_id))
+            if trade_to_close:
+                print(f"准备为交易ID {close_for_trade_id} 创建平仓记录")
+                
+                # 根据原始持仓类型确定平仓类型
+                close_position_type = None
+                if trade_to_close.position_type == 0:  # 原来是多头
+                    close_position_type = 1  # 平多
+                elif trade_to_close.position_type == 1:  # 原来是空头
+                    close_position_type = 3  # 平空
+                
+                close_trade_data = {
+                    'original_trade_id': trade_to_close.id,
+                    'contract_code': trade_to_close.contract_code,
+                    'name': trade_to_close.name,
+                    'account': trade_to_close.account,
+                    'position_type': close_position_type,
+                    'position_volume': trade_to_close.position_volume,
+                    'contract_multiplier': trade_to_close.contract_multiplier,
+                    'strategy_name': '',  # 平仓时清空操作策略
+                    'candle_pattern': '',  # 平仓时清空K线形态
+                    'long_trend_name': trade_to_close.long_trend_name,
+                    'mid_trend_name': trade_to_close.mid_trend_name,
+                    'trade_type': trade_to_close.trade_type,
+                    'trade_status': 3  # 设置平仓记录状态为"已结束"
+                }
+                print(f"平仓数据准备完成: {close_trade_data}")
+            else:
+                print(f"未找到交易ID {close_for_trade_id} 的记录")
+        except (ValueError, TypeError) as e:
+            print(f"解析平仓交易ID失败: {e}")
+    
+    elif close_for_transaction_id:
+        try:
+            # 查询要平仓的交易记录
+            transaction_to_close = TransactionRecord.query.get(int(close_for_transaction_id))
+            if transaction_to_close:
+                print(f"准备为交易记录ID {close_for_transaction_id} 创建平仓记录")
+                
+                # 根据原始持仓类型确定平仓类型
+                close_position_type = None
+                if transaction_to_close.position_type == 0:  # 开多 -> 平多
+                    close_position_type = 1
+                elif transaction_to_close.position_type == 2:  # 开空 -> 平空
+                    close_position_type = 3
+                
+                if close_position_type is not None:
+                    close_trade_data = {
+                        'original_transaction_id': transaction_to_close.id,
+                        'contract_code': transaction_to_close.contract_code,
+                        'name': transaction_to_close.name,
+                        'account': transaction_to_close.account,
+                        'position_type': close_position_type,
+                        'position_volume': transaction_to_close.volume,
+                        'contract_multiplier': transaction_to_close.contract_multiplier,
+                        'strategy_name': '',  # 平仓时清空操作策略
+                        'candle_pattern': '',  # 平仓时清空K线形态
+                        'long_trend_name': transaction_to_close.long_trend_name,
+                        'mid_trend_name': transaction_to_close.mid_trend_name,
+                        'trade_type': transaction_to_close.trade_type,
+                        'trade_status': 3  # 设置平仓记录状态为"已结束"
+                    }
+                    print(f"基于交易记录的平仓数据准备完成: {close_trade_data}")
+                else:
+                    print(f"交易记录ID {close_for_transaction_id} 的仓位类型不支持平仓操作")
+            else:
+                print(f"未找到交易记录ID {close_for_transaction_id} 的记录")
+        except (ValueError, TypeError) as e:
+            print(f"解析平仓交易记录ID失败: {e}")
+    
+    return render_template('transaction/add.html', close_trade_data=close_trade_data)
+
+@bp.route('/edit/<int:id>', methods=['GET'])
+def edit(id):
+    """编辑交易记录页面"""
+    return render_template('transaction/edit.html', transaction_id=id)
+
+@bp.route('/detail/view/<int:id>', methods=['GET'])
+def detail(id):
+    """查看交易记录详情页面"""
+    transaction_obj = TransactionRecord.query.get_or_404(id)
+    transaction_dict = transaction_obj.to_dict() # Convert to dictionary first
+    print(f"transaction_dict: {transaction_dict}")
+    return render_template('transaction/detail.html', transaction=transaction_dict)
+
+@bp.route('/api/list', methods=['GET'])
+def get_list():
+    """获取交易记录列表"""
+    try:
+        print("\n--- [DEBUG] ---")
+        print(f"Request Args: {request.args}")
+
+        # 获取分页参数
+        page = request.args.get('page', 1, type=int)
+        
+        # 从配置服务获取默认分页大小
+        try:
+            from app.services.config_service import get_int_config
+            default_page_size = get_int_config('pagination_default_size', 10)
+        except Exception:
+            default_page_size = 10
+            
+        limit = request.args.get('limit', default_page_size, type=int)
+        
+        # 获取筛选参数
+        start_time = request.args.get('start_time')
+        end_time = request.args.get('end_time')
+        names = request.args.getlist('name')
+        contract_letters = request.args.getlist('contract_letter')
+        contract_code = request.args.get('contract_code')
+        strategy_ids = request.args.getlist('strategy_id')
+        trade_type = request.args.get('trade_type')
+        trade_statuses = request.args.getlist('trade_status[]')
+        
+        # 构建查询
+        query = TransactionRecord.query
+        
+        if start_time:
+            query = query.filter(TransactionRecord.transaction_time >= datetime.strptime(start_time, '%Y-%m-%d'))
+        
+        if end_time:
+            query = query.filter(TransactionRecord.transaction_time <= datetime.strptime(end_time, '%Y-%m-%d'))
+        
+        if names:
+            query = query.filter(TransactionRecord.name.in_(names))
+        
+        if contract_letters:
+            # 假设合约代码的前1-2位是合约字母
+            query = query.filter(db.or_(*[TransactionRecord.contract_code.startswith(letter) for letter in contract_letters]))
+        
+        if contract_code:
+            query = query.filter(TransactionRecord.contract_code.like(f'%{contract_code}%'))
+        
+        if strategy_ids:
+            try:
+                # strategy_ids 是字符串字段,需要按字符串匹配
+                strategy_ids = [i.strip() for i in strategy_ids if i.strip()]
+                if strategy_ids:
+                    # 使用 OR 条件匹配包含任一策略ID的记录
+                    conditions = []
+                    for strategy_id in strategy_ids:
+                        conditions.append(TransactionRecord.strategy_ids.contains(strategy_id))
+                    if conditions:
+                        query = query.filter(db.or_(*conditions))
+            except ValueError:
+                pass
+        
+        if trade_type is not None and trade_type.strip():
+            try:
+                query = query.filter(TransactionRecord.trade_type == int(trade_type))
+            except ValueError:
+                pass
+        
+        if trade_statuses:
+            try:
+                trade_statuses_int = [int(i) for i in trade_statuses if i.strip()]
+                if trade_statuses_int:
+                    query = query.filter(TransactionRecord.trade_status.in_(trade_statuses_int))
+            except ValueError:
+                pass
+        
+        # 执行分页查询
+        pagination = query.order_by(TransactionRecord.transaction_time.desc()).paginate(page=page, per_page=limit, error_out=False)
+        transactions = pagination.items
+        total = pagination.total
+        
+        print(f"Query returned {total} total records.")
+        print("--- [END DEBUG] ---\n")
+
+        # 返回结果
+        return jsonify({
+            'code': 0,
+            'msg': '成功',
+            'count': total, # 返回总记录数用于分页
+            'data': [transaction.to_dict() for transaction in transactions]
+        })
+    except Exception as e:
+        return jsonify({
+            'code': 1,
+            'msg': f'获取列表失败: {str(e)}',
+            'count': 0,
+            'data': []
+        })
+
+@bp.route('/api/future_info/by_letter/<string:letter>', methods=['GET'])
+def get_future_info_by_letter(letter):
+    """根据合约字母获取期货信息"""
+    if not letter:
+        return jsonify({'code': 1, 'msg': '缺少合约字母参数'})
+
+    # 统一转为大写进行查询
+    letter_upper = letter.upper()
+
+    # 查找匹配的 FutureInfo 记录
+    # 假设 future_info 表中有 contract_letter 字段存储纯字母(如 CU, ZC)
+    # 使用 ilike 可能更健壮,如果 contract_letter 存储的是完整代码的前缀
+    # future_info = FutureInfo.query.filter(FutureInfo.contract_letter.ilike(f'{letter_upper}%')).first()
+    future_info = FutureInfo.query.filter_by(contract_letter=letter_upper).first()
+
+    if future_info:
+        return jsonify({
+            'code': 0,
+            'msg': '成功',
+            'data': {
+                'name': future_info.name,
+                'open_fee': future_info.open_fee,
+                'close_fee': future_info.close_fee,
+                'contract_multiplier': future_info.contract_multiplier,
+                'long_margin_rate': future_info.long_margin_rate,
+                'short_margin_rate': future_info.short_margin_rate
+            }
+        })
+    else:
+        return jsonify({
+            'code': 1,
+            'msg': f'未找到合约字母为 {letter_upper} 的期货信息'
+        })
+
+@bp.route('/api/strategy_info/list', methods=['GET'])
+def get_strategy_info_list():
+    """获取所有策略信息列表"""
+    try:
+        strategies = StrategyInfo.query.order_by(StrategyInfo.id).all()
+        strategy_list = [{'id': s.id, 'name': s.name} for s in strategies]
+        return jsonify({
+            'code': 0,
+            'msg': '成功',
+            'data': strategy_list
+        })
+    except Exception as e:
+        return jsonify({
+            'code': 1,
+            'msg': f'获取策略列表失败: {str(e)}'
+        })
+
+@bp.route('/api/detail/<int:id>', methods=['GET'])
+def get_detail(id):
+    """获取交易记录详情"""
+    transaction = TransactionRecord.query.get_or_404(id)
+    # transaction_dict = transaction.to_dict()
+    # # 尝试根据名称查找关联的 ID
+    # account_id = None
+    # future_info_id = None
+    # if transaction.account:
+    #     account_obj = Account.query.filter_by(account_name=transaction.account).first()
+    #     if account_obj:
+    #         account_id = account_obj.id
+    # if transaction.name:
+    #     # 优先按名称精确匹配
+    #     future_obj = FutureInfo.query.filter_by(name=transaction.name).first()
+    #     if not future_obj and transaction.contract_code:
+    #         # 如果按名称找不到,尝试按合约字母匹配
+    #         letter = ''.join(filter(str.isalpha, transaction.contract_code))[:2]
+    #         if letter:
+    #              future_obj = FutureInfo.query.filter(FutureInfo.contract_letter.ilike(f'{letter}%')).first()
+    #     if future_obj:
+    #         future_info_id = future_obj.id
+    # # 将 ID 添加到返回的字典中
+    # transaction_dict['account_id'] = account_id
+    # transaction_dict['future_info_id'] = future_info_id
+
+    return jsonify({
+        'code': 0,
+        'msg': '成功',
+        'data': transaction.to_dict() # 直接返回 to_dict() 结果
+    })
+
+@bp.route('/api/create', methods=['POST'])
+def create():
+    """创建交易记录 (手动添加)"""
+    # 在函数内部导入,避免循环依赖
+    from app.models.trade import TradeRecord
+    from app.services.trade_logic import update_trade_record, generate_trade_from_transactions
+
+    data = request.json
+    try:
+        # --- 1. Process Input Data ---
+        transaction_time = datetime.strptime(data.get('transaction_time', ''), '%Y-%m-%d %H:%M') if data.get('transaction_time') else datetime.now()
+        operation_time = transaction_time
+        if 'operation_time' in data and data['operation_time']:
+             try:
+                 operation_time = datetime.strptime(data['operation_time'], '%Y-%m-%d %H:%M')
+             except ValueError:
+                 pass
+
+        # Basic data
+        name = data.get('name')
+        price = data.get('price')
+        volume = data.get('volume')
+        contract_multiplier = data.get('contract_multiplier')
+        position_type = data.get('position_type')
+        
+        # 从配置服务获取默认值
+        try:
+            from app.services.config_service import get_str_config, get_int_config
+            default_account = get_str_config('default_account_name', '华安期货')
+            default_trade_type = get_int_config('default_trade_type', 0)
+            default_trade_status = get_int_config('default_trade_status', 0)
+        except Exception as e:
+            logger.warning(f"获取业务配置失败,使用硬编码默认值: {e}")
+            default_account = '华安期货'
+            default_trade_type = 0
+            default_trade_status = 0
+            
+        account = data.get('account', default_account)
+        trade_type = data.get('trade_type', default_trade_type)
+        trade_status = data.get('trade_status', default_trade_status)
+        stop_loss_price = data.get('stop_loss_price')
+        confidence_index = data.get('confidence_index')
+        similarity_evaluation = data.get('similarity_evaluation')
+        notes = data.get('notes')
+        contract_code=data.get('contract_code')
+
+        # Calculated financial data (from frontend)
+        amount = data.get('amount')
+        fee = data.get('fee')
+        volume_change = data.get('volume_change')
+        margin = data.get('margin')
+
+        # Process names to IDs (Strategies, Candles, Trends)
+        strategy_name = (data.get('strategy_name') or '').strip()
+        strategy_ids, corrected_strategy_name = _get_ids_from_names(strategy_name, StrategyInfo)
+
+        candle_pattern_name = (data.get('candle_pattern_name') or '').strip()
+        candle_pattern_ids, corrected_candle_pattern_name = _get_ids_from_names(candle_pattern_name, CandleInfo)
+
+        long_trend_name = (data.get('long_trend_name') or '').strip()
+        long_trend_ids, corrected_long_trend_name = _get_ids_from_names(long_trend_name, TrendInfo)
+
+        mid_trend_name = (data.get('mid_trend_name') or '').strip()
+        mid_trend_ids, corrected_mid_trend_name = _get_ids_from_names(mid_trend_name, TrendInfo)
+
+        # --- 2. Create TransactionRecord (trade_id is initially None) ---
+        new_transaction = TransactionRecord(
+            transaction_time=transaction_time,
+            operation_time=operation_time,
+            contract_code=contract_code,
+            name=name,
+            account=account,
+            strategy_ids=strategy_ids,
+            strategy_name=corrected_strategy_name,
+            position_type=position_type,
+            candle_pattern_ids=candle_pattern_ids,
+            candle_pattern=corrected_candle_pattern_name,
+            price=price,
+            volume=volume,
+            contract_multiplier=contract_multiplier,
+            amount=amount,
+            fee=fee,
+            volume_change=volume_change,
+            margin=margin,
+            trade_type=trade_type,
+            trade_status=trade_status,
+            stop_loss_price=stop_loss_price,
+            confidence_index=confidence_index,
+            similarity_evaluation=similarity_evaluation,
+            long_trend_ids=long_trend_ids,
+            long_trend_name=corrected_long_trend_name,
+            mid_trend_ids=mid_trend_ids,
+            mid_trend_name=corrected_mid_trend_name,
+            # notes=notes, # Add if model has notes field
+            trade_id = None # Initial state
+        )
+
+        # --- 3. Handle Trade Logic (Find Match or Create New) ---
+        target_trade_id = None
+        final_trade_msg = ""
+
+        # Only try to find a match if it's a closing transaction
+        if position_type in [1, 3]: # 平多 or 平空
+            print("处理平仓,尝试查找匹配的未平仓 Trade...")
+            target_open_pos_type = 0 if position_type == 1 else 2
+            # Find the latest open transaction of the opposite type for the same contract/account/strategy
+            # that is linked to a TradeRecord which is currently open (close_time is null)
+            matching_open_trans = db.session.query(TransactionRecord)\
+                .join(TradeRecord, TransactionRecord.trade_id == TradeRecord.id)\
+                .filter(
+                    TradeRecord.close_time.is_(None), # Must be an open trade
+                    TransactionRecord.contract_code == new_transaction.contract_code,
+                    TransactionRecord.account == new_transaction.account,
+                    TransactionRecord.strategy_ids == new_transaction.strategy_ids, # Strategy must match
+                    TransactionRecord.position_type == target_open_pos_type
+                )\
+                .order_by(TransactionRecord.transaction_time.desc())\
+                .first()
+
+            if matching_open_trans:
+                target_trade_id = matching_open_trans.trade_id
+                new_transaction.trade_id = target_trade_id # Associate with the found trade
+                print(f"找到匹配的未平仓 Trade ID: {target_trade_id},关联此平仓记录。")
+            else:
+                # 尝试更宽松的查找:不要求策略匹配
+                print("未找到完全匹配的开仓记录,尝试更宽松的查找...")
+                loose_matching_trans = db.session.query(TransactionRecord)\
+                    .join(TradeRecord, TransactionRecord.trade_id == TradeRecord.id)\
+                    .filter(
+                        TradeRecord.close_time.is_(None), # Must be an open trade
+                        TransactionRecord.contract_code == new_transaction.contract_code,
+                        TransactionRecord.account == new_transaction.account,
+                        TransactionRecord.position_type == target_open_pos_type
+                    )\
+                    .order_by(TransactionRecord.transaction_time.desc())\
+                    .first()
+                
+                if loose_matching_trans:
+                    target_trade_id = loose_matching_trans.trade_id
+                    new_transaction.trade_id = target_trade_id
+                    print(f"找到宽松匹配的未平仓 Trade ID: {target_trade_id},关联此平仓记录。")
+                else:
+                    print("ERROR: 平仓操作找不到任何匹配的开仓记录,这是无效的平仓操作。")
+                    return jsonify({
+                        'code': 1,
+                        'msg': f'平仓操作失败:找不到对应的开仓记录。请确认有相同合约和账户的开仓交易。'
+                    })
+
+        # --- 4. Add Transaction to Session ---
+        db.session.add(new_transaction)
+        db.session.flush() # Get the ID for new_transaction
+
+        # --- 5. Create or Update Trade Record ---
+        if target_trade_id:
+            # Update existing TradeRecord
+            print(f"触发 TradeRecord 更新 ID: {target_trade_id}")
+            update_result = update_trade_record(target_trade_id) # This function handles fetching all related trans and recalculating
+            final_trade_msg = update_result.get('msg', f"尝试更新 TradeRecord ID: {target_trade_id}")
+        else:
+            # Create new TradeRecord (only for opening transactions)
+            if position_type in [0, 2]:  # 开多 or 开空
+                print("创建新的 TradeRecord...")
+                # Use the helper that returns a TradeRecord object
+                new_trade = generate_trade_from_transactions([new_transaction])
+                if new_trade:
+                     try:
+                         db.session.add(new_trade)
+                         db.session.flush() # Get the ID for the new trade
+                         new_transaction.trade_id = new_trade.id # Backfill the trade_id
+                         final_trade_msg = f"成功创建新的 TradeRecord ID: {new_trade.id}"
+                         print(final_trade_msg)
+                     except Exception as trade_create_e:
+                         final_trade_msg = f"创建 TradeRecord 实例时出错: {trade_create_e}"
+                         print(final_trade_msg)
+                         # Consider what to do if trade creation fails - maybe rollback transaction?
+                else:
+                     final_trade_msg = "创建新的 TradeRecord 失败(无法计算数据)。"
+                     print(final_trade_msg)
+            else:
+                # 平仓操作但没有找到对应的开仓记录,这个情况已经在前面处理了
+                final_trade_msg = "平仓操作已处理。"
+
+        # --- 6. 平仓后更新相关交易记录状态 ---
+        update_msg = ""
+        if (position_type in [1, 3]) and (trade_status == 3) and new_transaction.trade_id:
+            # 如果是平仓操作且状态为"已结束",更新所有相同交易ID的相关交易记录状态
+            try:
+                related_transactions = TransactionRecord.query.filter_by(trade_id=new_transaction.trade_id).all()
+                updated_count = 0
+                for trans in related_transactions:
+                    if trans.trade_status != 3:  # 只更新未结束的记录
+                        trans.trade_status = 3
+                        updated_count += 1
+                
+                if updated_count > 0:
+                    update_msg = f"已更新 {updated_count} 条相关交易记录状态为'已结束'。"
+                    print(f"平仓完成:{update_msg}")
+            except Exception as update_e:
+                print(f"更新相关交易记录状态时出错: {update_e}")
+                update_msg = "更新相关交易记录状态时出现问题。"
+
+        # --- 7. Commit and Respond ---
+        db.session.commit()
+
+        return jsonify({
+            'code': 0,
+            'msg': f'操作成功。{final_trade_msg} {update_msg}'.strip(),
+            'data': new_transaction.to_dict() # Return the transaction, possibly with updated trade_id
+        })
+
+    except Exception as e:
+        db.session.rollback()
+        import traceback
+        print(traceback.format_exc())
+        return jsonify({
+            'code': 1,
+            'msg': f'创建交易记录时出错: {str(e)}'
+        })
+
+# Helper to convert names to IDs
+def _get_ids_from_names(names_string, model):
+    ids = None
+    corrected_names = None
+    if names_string:
+        name_list = [name.strip() for name in names_string.split('+') if name.strip()]
+        if name_list:
+            records = model.query.filter(model.name.in_(name_list)).all()
+            id_map = {r.name: r.id for r in records}
+            id_list = [str(id_map[name]) for name in name_list if name in id_map]
+            matched_names = [name for name in name_list if name in id_map]
+            if id_list:
+                ids = ','.join(id_list)
+            if matched_names:
+                corrected_names = '+'.join(matched_names)
+    return ids, corrected_names
+
+@bp.route('/api/update/<int:id>', methods=['PUT'])
+def update(id):
+    """更新交易记录"""
+    # 在函数内部导入
+    from app.services.trade_logic import update_trade_record
+
+    transaction = TransactionRecord.query.get_or_404(id)
+    original_trade_id = transaction.trade_id # 记录原始 trade_id
+    data = request.json
+
+    recalculate_financials = False
+    trigger_trade_update = False # Flag to trigger trade update
+
+    # 更新字段
+    if 'transaction_time' in data:
+        try:
+            transaction.transaction_time = datetime.fromisoformat(data['transaction_time'])
+        except ValueError:
+            transaction.transaction_time = datetime.strptime(data['transaction_time'], '%Y-%m-%d %H:%M')
+        recalculate_financials = True # 时间变化影响汇总
+    if 'contract_code' in data:
+        transaction.contract_code = data['contract_code']
+        recalculate_financials = True
+    if 'name' in data:
+        transaction.name = data['name']
+        recalculate_financials = True # name 变化影响 margin 计算和汇总
+    if 'account' in data:
+        transaction.account = data['account']
+        recalculate_financials = True
+    if 'strategy_ids' in data:
+        transaction.strategy_ids = data['strategy_ids']
+        recalculate_financials = True
+    if 'strategy_name' in data:
+        transaction.strategy_name = data['strategy_name']
+    if 'position_type' in data:
+        transaction.position_type = data['position_type']
+        recalculate_financials = True # position_type 变化影响 volume_change 和 margin
+    if 'candle_pattern_ids' in data:
+        transaction.candle_pattern_ids = data['candle_pattern_ids']
+    if 'candle_pattern' in data:
+        transaction.candle_pattern = data['candle_pattern']
+    if 'price' in data:
+        transaction.price = data['price']
+        recalculate_financials = True # price 变化影响 amount, margin
+    if 'volume' in data:
+        transaction.volume = data['volume']
+        recalculate_financials = True # volume 变化影响 amount, volume_change, margin
+    if 'contract_multiplier' in data and data['contract_multiplier'] is not None:
+        transaction.contract_multiplier = data['contract_multiplier']
+        recalculate_financials = True # multiplier 变化影响 amount, margin
+    if 'fee' in data:
+        transaction.fee = data['fee']
+        # fee 变化本身不直接触发重算 amount/margin/volume_change, 但会影响最终利润计算
+    if 'trade_type' in data:
+        transaction.trade_type = data['trade_type']
+    if 'trade_status' in data and data['trade_status'] is not None:
+        try:
+            transaction.trade_status = int(data['trade_status'])
+        except (ValueError, TypeError):
+            # 如果转换失败,可以记录日志或返回错误,这里暂时忽略
+            pass
+    if 'latest_price' in data:
+        transaction.latest_price = data['latest_price']
+        # latest_price 变化影响 to_dict 中的计算,不需要在此重算存储字段
+    if 'stop_loss_price' in data:
+        transaction.stop_loss_price = data['stop_loss_price']
+        # stop_loss_price 变化影响 to_dict 中的计算
+    # 移除 is_close_today, related_open_id, notes 的更新 (根据 BRD 要求)
+    # if 'is_close_today' in data:
+    #     transaction.is_close_today = data['is_close_today']
+    # if 'related_open_id' in data:
+    #     transaction.related_open_id = data['related_open_id']
+    # if 'notes' in data:
+    #     transaction.notes = data['notes']
+    if 'operation_time' in data:
+        try:
+            transaction.operation_time = datetime.fromisoformat(data['operation_time'])
+        except ValueError:
+            transaction.operation_time = datetime.strptime(data['operation_time'], '%Y-%m-%d %H:%M')
+    if 'confidence_index' in data:
+        transaction.confidence_index = data['confidence_index']
+    if 'similarity_evaluation' in data:
+        transaction.similarity_evaluation = data['similarity_evaluation']
+    if 'long_trend_ids' in data:
+        transaction.long_trend_ids = data['long_trend_ids']
+    if 'long_trend_name' in data:
+        transaction.long_trend_name = data['long_trend_name']
+    if 'mid_trend_ids' in data:
+        transaction.mid_trend_ids = data['mid_trend_ids']
+    if 'mid_trend_name' in data:
+        transaction.mid_trend_name = data['mid_trend_name']
+
+    # 重新计算相关字段
+    if recalculate_financials:
+        # 确保必要字段存在
+        price = transaction.price
+        volume = transaction.volume
+        contract_multiplier = transaction.contract_multiplier
+        position_type = transaction.position_type
+        name = transaction.name
+
+        if price is not None and volume is not None and contract_multiplier is not None:
+            # 重新计算成交金额
+            transaction.amount = price * volume * contract_multiplier
+
+            # 重新计算手数变化
+            if position_type in [0, 3]:
+                transaction.volume_change = volume
+            elif position_type in [1, 2]:
+                transaction.volume_change = -volume
+            else:
+                transaction.volume_change = 0
+
+            # 重新计算保证金
+            margin = None
+            future_info = None
+            if name:
+                future_info = FutureInfo.query.filter_by(name=name).first()
+
+            if future_info and transaction.amount is not None:
+                margin_rate = None
+                if position_type in [0, 1]: # 多头
+                    margin_rate = future_info.long_margin_rate
+                elif position_type in [2, 3]: # 空头
+                    margin_rate = future_info.short_margin_rate
+
+                if margin_rate is not None:
+                     # 假设 margin_rate 是百分比形式存储
+                    margin = transaction.amount * (margin_rate / 100.0)
+            transaction.margin = margin
+        else:
+            # 如果计算所需字段不全,将计算结果设为 None
+            transaction.amount = None
+            transaction.volume_change = None
+            transaction.margin = None
+
+    # 保存到数据库
+    db.session.commit()
+
+    # --- Update Trade Record(s) if needed ---
+    trade_update_msg = ""
+    if trigger_trade_update:
+        ids_to_update = set()
+        if original_trade_id:
+            ids_to_update.add(original_trade_id)
+        if transaction.trade_id and transaction.trade_id != original_trade_id:
+             ids_to_update.add(transaction.trade_id)
+
+        print(f"交易记录更新触发 Trade Record 更新 IDs: {ids_to_update}")
+        for t_id in ids_to_update:
+             if t_id: # Ensure not None
+                 try:
+                     update_result = update_trade_record(t_id)
+                     trade_update_msg += f" Trade ID {t_id}: {update_result.get('msg', '尝试更新')}. "
+                 except Exception as e:
+                     trade_update_msg += f" Trade ID {t_id} 更新失败: {e}. "
+                     print(f"更新 Trade ID {t_id} 失败: {e}")
+
+    return jsonify({
+        'code': 0,
+        'msg': f'更新成功。{trade_update_msg}',
+        'data': transaction.to_dict()
+    })
+
+@bp.route('/api/delete/<int:id>', methods=['DELETE'])
+def delete(id):
+    """删除交易记录"""
+    # 在函数内部导入
+    from app.services.trade_logic import update_trade_record
+
+    transaction = TransactionRecord.query.get_or_404(id)
+    associated_trade_id = transaction.trade_id
+
+    db.session.delete(transaction)
+    db.session.commit() # Commit deletion first
+
+    # Trigger update for the associated trade record
+    trade_update_msg = ""
+    if associated_trade_id:
+        print(f"删除交易记录 ID {id} 触发 Trade Record 更新 ID: {associated_trade_id}")
+        try:
+            update_result = update_trade_record(associated_trade_id)
+            trade_update_msg = f"关联 Trade ID {associated_trade_id}: {update_result.get('msg', '尝试更新')}"
+        except Exception as e:
+            trade_update_msg = f"关联 Trade ID {associated_trade_id} 更新失败: {e}"
+            print(f"更新 Trade ID {associated_trade_id} (因删除) 失败: {e}")
+            # Consider if the trade should be deleted if it has no transactions left
+
+    return jsonify({
+        'code': 0,
+        'msg': f'删除成功。{trade_update_msg}'
+    })
+
+@bp.route('/template', methods=['GET'])
+def get_template():
+    """获取交易记录的Excel导入模板"""
+    # 创建DataFrame
+    columns = [
+        '交易ID', '换月ID', '成交时间', '合约代码', '合约名称', '账户', 
+        '操作策略', '多空仓位', 'K线形态', '成交价格', '成交手数', '单位', 
+        '成交金额', '手续费', '手数变化', '现金流', '保证金', '资金阈值判定',
+        '交易类别', '交易状态', '止损点', '操作日期',
+        '长期趋势名称', '中期趋势名称'
+    ]
+    
+    # 创建示例数据
+    data = [
+        [1, 0, '2023-03-29 14:30', 'CU2305', '沪铜', '华安期货', 
+         '趋势突破+均线突破', 0, '突破回踩+双底', 68000, 1, 5, 
+         340000, 15, 1, -340015, 34000, 0,
+         0, 0, 67500, '2023-03-29 14:30',
+         '长期上涨+短期震荡', '中期下跌+短期震荡']
+    ]
+    
+    df = pd.DataFrame(data, columns=columns)
+    
+    # 创建Excel文件
+    output = io.BytesIO()
+    with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
+        df.to_excel(writer, sheet_name='交易记录导入模板', index=False)
+        
+        # 自动调整列宽
+        worksheet = writer.sheets['交易记录导入模板']
+        for i, col in enumerate(df.columns):
+            column_width = max(df[col].astype(str).map(len).max(), len(col) + 2)
+            worksheet.set_column(i, i, column_width)
+    
+    output.seek(0)
+    
+    # 设置下载文件名
+    filename = f'交易记录导入模板_{datetime.now().strftime("%Y%m%d%H%M%S")}.xlsx'
+    
+    return send_file(
+        output,
+        mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+        as_attachment=True,
+        download_name=filename
+    )
+
+@bp.route('/api/import', methods=['POST'])
+def import_excel():
+    """从Excel导入交易记录 (修改后)"""
+    # 在函数内部导入
+    from app.services.trade_logic import sync_trades_after_import
+
+    if 'file' not in request.files:
+        return jsonify({'code': 1,'msg': '没有上传文件'})
+    file = request.files['file']
+    if file.filename == '':
+        return jsonify({'code': 1, 'msg': '没有选择文件'})
+    if not file.filename.endswith('.xlsx'):
+        return jsonify({'code': 1, 'msg': '请上传Excel文件(.xlsx)'})
+
+    try:
+        temp_dir = tempfile.gettempdir()
+        cache_buster = str(uuid.uuid4())
+        temp_path = os.path.join(temp_dir, f"transaction_import_{cache_buster}.xlsx")
+        file.save(temp_path)
+        df = pd.read_excel(temp_path)
+        try:
+            os.remove(temp_path)
+        except Exception: pass
+
+        print(f"Excel 列名: {df.columns.tolist()}")
+
+        required_columns = ['交易ID', '合约代码', '合约名称', '多空仓位', '成交价格', '成交手数']
+        missing_columns = [col for col in required_columns if col not in df.columns]
+        if missing_columns:
+            return jsonify({'code': 1, 'msg': f'Excel文件缺少必填列: {", ".join(missing_columns)}'})
+
+        # --- 验证 Trade ID 配对 ---
+        trade_id_map = {}
+        row_errors = {} # Store errors by row index
+
+        for i, row in df.iterrows():
+            row_num = i + 2 # Excel row number
+            excel_trade_id = None
+            pos_type = None
+            try:
+                 if '交易ID' in row and not pd.isna(row['交易ID']):
+                      excel_trade_id = int(row['交易ID'])
+                 else:
+                      raise ValueError("缺少必需的 '交易ID'")
+
+                 if '多空仓位' in row and not pd.isna(row['多空仓位']):
+                     pos_type = int(row['多空仓位'])
+                     if pos_type not in [0, 1, 2, 3]:
+                         raise ValueError("无效的 '多空仓位' 值")
+                 else:
+                      raise ValueError("缺少必需的 '多空仓位'")
+
+                 if excel_trade_id not in trade_id_map:
+                     trade_id_map[excel_trade_id] = []
+                 trade_id_map[excel_trade_id].append({'pos_type': pos_type, 'row_num': row_num})
+
+            except Exception as e:
+                 row_errors[row_num] = f"行预检错误: {str(e)}"
+
+        # Check pairs
+        for trade_id, items in trade_id_map.items():
+             if len(items) > 2:
+                  involved_rows = ", ".join([str(item['row_num']) for item in items])
+                  error_msg = f"交易ID {trade_id} 在行 {involved_rows} 出现超过2次。"
+                  for item in items: row_errors[item['row_num']] = error_msg # Mark all related rows
+             elif len(items) == 2:
+                  pos_types = {item['pos_type'] for item in items}
+                  if not ((0 in pos_types and 1 in pos_types) or (2 in pos_types and 3 in pos_types)):
+                       involved_rows = ", ".join([str(item['row_num']) for item in items])
+                       error_msg = f"交易ID {trade_id} 在行 {involved_rows} 的仓位类型不是有效的开平仓对。"
+                       for item in items: row_errors[item['row_num']] = error_msg
+             # Single entry is allowed, will create/update trade based on that single entry
+
+        # --- Process Rows ---
+        transactions_to_add = []
+        imported_trade_ids = set()
+        error_count = len(row_errors)
+        error_messages = list(row_errors.values()) # Collect pre-check errors
+
+        # Load dimension maps once
+        strategy_id_map, candle_pattern_id_map, trend_id_map = _load_dimension_maps()
+
+        for i, row in df.iterrows():
+            row_num = i + 2
+            if row_num in row_errors: # Skip rows with pre-check errors
+                continue
+
+            try:
+                excel_trade_id = int(row['交易ID']) # Already validated
+                position_type = int(row['多空仓位']) # Already validated
+
+                transaction_time, operation_time = _parse_excel_dates(row.get('成交时间'), row.get('操作日期'))
+
+                price = float(row['成交价格'])
+                volume = float(row['成交手数'])
+                contract_multiplier = float(row.get('单位', 1)) if not pd.isna(row.get('单位')) else 1
+                amount = float(row.get('成交金额', price * volume * contract_multiplier)) if not pd.isna(row.get('成交金额')) else price * volume * contract_multiplier
+                fee = float(row.get('手续费', 0)) if not pd.isna(row.get('手续费')) else 0
+                # Calculate volume_change based on position type
+                volume_change = volume if position_type in [0, 3] else -volume
+
+                # Margin needs calculation based on FutureInfo (similar to create logic)
+                # margin = _calculate_margin(...) # Need a helper or repeat logic
+                margin = float(row.get('保证金', 0)) if not pd.isna(row.get('保证金')) else None # Simplified: Take from Excel or None
+
+                # Get IDs from names using preloaded maps
+                strategy_ids, strategy_name = _resolve_names(row.get('操作策略', ''), strategy_id_map)
+                candle_pattern_ids, candle_pattern = _resolve_names(row.get('K线形态', ''), candle_pattern_id_map)
+                long_trend_ids, long_trend_name = _resolve_names(row.get('长期趋势名称', ''), trend_id_map)
+                mid_trend_ids, mid_trend_name = _resolve_names(row.get('中期趋势名称', ''), trend_id_map)
+
+                transaction = TransactionRecord(
+                    trade_id=excel_trade_id,
+                    roll_id=int(row.get('换月ID', 0)) if not pd.isna(row.get('换月ID')) else None,
+                    transaction_time=transaction_time,
+                    operation_time=operation_time,
+                    contract_code=row['合约代码'],
+                    name=row['合约名称'],
+                    account=row.get('账户', '华安期货'),
+                    strategy_ids=strategy_ids,
+                    strategy_name=strategy_name,
+                    position_type=position_type,
+                    candle_pattern_ids=candle_pattern_ids,
+                    candle_pattern=candle_pattern,
+                    price=price,
+                    volume=volume,
+                    contract_multiplier=contract_multiplier,
+                    amount=amount,
+                    fee=fee,
+                    volume_change=volume_change, # Use calculated value
+                    # cash_flow=... # Not directly in simpler template?
+                    margin=margin, # Use calculated or Excel value
+                    # fund_threshold=...
+                    trade_type=int(row.get('交易类别', 0)) if not pd.isna(row.get('交易类别')) else 0,
+                    trade_status=int(row.get('交易状态', 0)) if not pd.isna(row.get('交易状态')) else 0,
+                    stop_loss_price=float(row.get('止损点', 0)) if not pd.isna(row.get('止损点')) else None,
+                    confidence_index=int(row.get('信心指数', 0)) if not pd.isna(row.get('信心指数')) else None,
+                    similarity_evaluation=row.get('相似度评估'),
+                    long_trend_ids=long_trend_ids,
+                    long_trend_name=long_trend_name,
+                    mid_trend_ids=mid_trend_ids,
+                    mid_trend_name=mid_trend_name
+                    # notes=...
+                )
+                transactions_to_add.append(transaction)
+                imported_trade_ids.add(excel_trade_id)
+
+            except Exception as e:
+                error_count += 1
+                row_data_str = ", ".join([f"{k}={v}" for k, v in row.items()])
+                
+                # 从配置服务获取错误消息最大长度
+                try:
+                    from app.services.config_service import get_int_config
+                    max_length = get_int_config('error_message_max_length', 200)
+                except Exception:
+                    max_length = 200
+                    
+                error_msg = f'第{row_num}行处理错误: {str(e)}\n行数据: {row_data_str[:max_length]}...' # Limit row data length
+                print(error_msg)
+                error_messages.append(error_msg)
+                # No rollback needed here as we haven't added to session yet
+
+        # --- Add valid transactions and sync trades ---
+        sync_result = None # 初始化为 None
+        if transactions_to_add:
+            try:
+                # Check for duplicates before adding (e.g., unique constraint on id?)
+                # Add all valid Transaction Records
+                db.session.add_all(transactions_to_add)
+                db.session.flush() # Assign transaction IDs
+                print(f"已添加 {len(transactions_to_add)} 条交易记录到 session。")
+                final_success_count = len(transactions_to_add) # Update success count
+
+                # Sync Trade Records
+                print(f"开始同步 {len(imported_trade_ids)} 个关联的 Trade Records...")
+                sync_result = sync_trades_after_import(list(imported_trade_ids))
+                sync_msg = sync_result.get('msg', '交易汇总记录同步完成。')
+                print(sync_msg) # 打印同步结果
+
+                db.session.commit() # Commit transaction additions/updates and trade creations/updates
+
+            except Exception as commit_sync_e:
+                db.session.rollback() # Rollback if commit or sync fails
+                import traceback
+                print("Commit/Sync 阶段出错:")
+                print(traceback.format_exc())
+                final_success_count = 0 # Reset success count on final error
+                error_count = len(df) # Mark all as failed if commit fails
+                sync_msg = "数据库提交或同步失败,所有更改已回滚。"
+                error_messages.append(f"数据库错误: {str(commit_sync_e)}")
+
+        return jsonify({
+            'code': 0 if error_count == 0 else 1, # Adjust code based on if errors occurred
+            'msg': f'处理完成: {final_success_count} 条记录成功导入/更新, {error_count} 行存在错误。{sync_msg}',
+            'data': {
+                'success_count': final_success_count,
+                'error_count': error_count,
+                'error_messages': error_messages,
+                'sync_details': sync_result # Optional: include sync details
+            }
+        })
+
+    except Exception as e:
+        # Catch errors during file reading or initial setup
+        db.session.rollback()
+        import traceback
+        print(traceback.format_exc())
+        return jsonify({
+            'code': 1,
+            'msg': f'导入过程中发生意外错误: {str(e)}'
+        })
+
+# --- Helper functions for import ---
+def _load_dimension_maps():
+    strategy_map = {s.name: s.id for s in StrategyInfo.query.all()}
+    candle_map = {c.name: c.id for c in CandleInfo.query.all()}
+    trend_map = {t.name: t.id for t in TrendInfo.query.all()}
+    return strategy_map, candle_map, trend_map
+
+def _parse_excel_dates(time_val, op_time_val):
+    transaction_time = datetime.now() # Default
+    if not pd.isna(time_val):
+        try:
+            # Handle various possible Excel date formats
+            if isinstance(time_val, datetime): transaction_time = time_val
+            else: transaction_time = pd.to_datetime(time_val).to_pydatetime()
+        except Exception as e:
+            print(f"无法解析成交时间 '{time_val}', 使用当前时间. 错误: {e}")
+
+    operation_time = transaction_time # Default to transaction_time
+    if not pd.isna(op_time_val):
+         try:
+             if isinstance(op_time_val, datetime): operation_time = op_time_val
+             else: operation_time = pd.to_datetime(op_time_val).to_pydatetime()
+         except Exception as e:
+             print(f"无法解析操作时间 '{op_time_val}', 使用成交时间. 错误: {e}")
+
+    return transaction_time, operation_time
+
+def _resolve_names(names_string, id_map):
+    ids = None
+    corrected_names = None
+    if isinstance(names_string, str) and names_string.strip():
+        names_string = names_string.strip()
+        name_list = [name.strip() for name in names_string.split('+') if name.strip()]
+        if name_list:
+            id_list = [str(id_map[name]) for name in name_list if name in id_map]
+            matched_names = [name for name in name_list if name in id_map]
+            if id_list:
+                ids = ','.join(id_list)
+            if matched_names:
+                corrected_names = '+'.join(matched_names)
+    return ids, corrected_names
+
+@bp.route('/import', methods=['GET'])
+def import_view():
+    """导入交易记录页面"""
+    return render_template('transaction/import.html')
+
+# Remove the /generate_trades endpoint as it's replaced by logic within create/import/update
+# @bp.route('/api/generate_trades', methods=['POST'])
+# def generate_all_trades():
+#     pass
+"" 

+ 5 - 0
app/services/__init__.py

@@ -0,0 +1,5 @@
+"""
+服务模块初始化文件
+"""
+
+# 服务模块将在此处被导入 

+ 290 - 0
app/services/config_service.py

@@ -0,0 +1,290 @@
+"""
+配置管理服务
+提供统一的系统配置访问接口,支持缓存和热更新
+"""
+
+import threading
+import time
+from typing import Any, Optional, Dict
+from app.models.system import SystemConfig
+from app.database.db_manager import db
+import logging
+
+logger = logging.getLogger(__name__)
+
+class ConfigService:
+    """
+    系统配置管理服务
+    
+    特性:
+    - 缓存配置数据,减少数据库查询
+    - 支持热更新,配置变更时自动刷新缓存
+    - 类型安全的配置值获取
+    - 线程安全
+    """
+    
+    _instance = None
+    _lock = threading.Lock()
+    
+    def __new__(cls):
+        if cls._instance is None:
+            with cls._lock:
+                if cls._instance is None:
+                    cls._instance = super().__new__(cls)
+                    cls._instance._initialized = False
+        return cls._instance
+    
+    def __init__(self):
+        if not self._initialized:
+            self._config_cache: Dict[str, Any] = {}
+            self._cache_timestamp = 0
+            self._cache_ttl = 300  # 缓存5分钟
+            self._cache_lock = threading.RLock()
+            self._initialized = True
+            logger.info("配置服务已初始化")
+    
+    def _is_cache_valid(self) -> bool:
+        """检查缓存是否有效"""
+        return (time.time() - self._cache_timestamp) < self._cache_ttl
+    
+    def _refresh_cache(self) -> None:
+        """刷新配置缓存"""
+        try:
+            # 检查是否有Flask应用上下文
+            try:
+                from flask import has_app_context
+                if not has_app_context():
+                    logger.warning("没有Flask应用上下文,跳过配置缓存刷新")
+                    return
+            except ImportError:
+                logger.warning("Flask未加载,跳过配置缓存刷新")
+                return
+            
+            with self._cache_lock:
+                logger.debug("开始刷新配置缓存...")
+                
+                # 查询所有活跃的配置
+                configs = SystemConfig.query.filter_by(is_active=True).all()
+                
+                # 清空缓存并重新填充
+                self._config_cache.clear()
+                for config in configs:
+                    self._config_cache[config.parameter_name] = config.get_typed_value()
+                
+                self._cache_timestamp = time.time()
+                logger.info(f"配置缓存已刷新,共加载 {len(self._config_cache)} 个配置参数")
+                
+        except Exception as e:
+            logger.error(f"刷新配置缓存失败: {e}")
+            # 如果刷新失败,保留旧缓存
+    
+    def get_config(self, parameter_name: str, default_value: Any = None, force_refresh: bool = False) -> Any:
+        """
+        获取配置值
+        
+        Args:
+            parameter_name: 参数名称
+            default_value: 默认值,当配置不存在时返回
+            force_refresh: 是否强制刷新缓存
+            
+        Returns:
+            配置的实际类型值
+        """
+        with self._cache_lock:
+            # 检查是否需要刷新缓存
+            if force_refresh or not self._is_cache_valid() or not self._config_cache:
+                self._refresh_cache()
+            
+            # 从缓存获取值
+            if parameter_name in self._config_cache:
+                value = self._config_cache[parameter_name]
+                logger.debug(f"从缓存获取配置 {parameter_name} = {value}")
+                return value
+            else:
+                # 缓存中不存在,尝试查询数据库(可能是新添加的配置)
+                logger.debug(f"缓存中不存在配置 {parameter_name},尝试查询数据库")
+                
+                # 检查是否有Flask应用上下文
+                try:
+                    from flask import has_app_context
+                    if not has_app_context():
+                        logger.warning(f"没有Flask应用上下文,无法查询配置 {parameter_name},返回默认值 {default_value}")
+                        return default_value
+                except ImportError:
+                    logger.warning(f"Flask未加载,无法查询配置 {parameter_name},返回默认值 {default_value}")
+                    return default_value
+                
+                try:
+                    config = SystemConfig.query.filter_by(
+                        parameter_name=parameter_name,
+                        is_active=True
+                    ).first()
+                    
+                    if config:
+                        value = config.get_typed_value()
+                        # 更新缓存
+                        self._config_cache[parameter_name] = value
+                        logger.debug(f"从数据库获取配置 {parameter_name} = {value}")
+                        return value
+                    else:
+                        logger.warning(f"配置参数 {parameter_name} 不存在,返回默认值 {default_value}")
+                        return default_value
+                        
+                except Exception as e:
+                    logger.error(f"查询配置参数 {parameter_name} 失败: {e}")
+                    return default_value
+    
+    def get_str(self, parameter_name: str, default_value: str = '') -> str:
+        """获取字符串类型配置"""
+        value = self.get_config(parameter_name, default_value)
+        return str(value) if value is not None else default_value
+    
+    def get_int(self, parameter_name: str, default_value: int = 0) -> int:
+        """获取整数类型配置"""
+        value = self.get_config(parameter_name, default_value)
+        try:
+            return int(value) if value is not None else default_value
+        except (ValueError, TypeError):
+            logger.warning(f"配置 {parameter_name} 无法转换为整数,返回默认值 {default_value}")
+            return default_value
+    
+    def get_float(self, parameter_name: str, default_value: float = 0.0) -> float:
+        """获取浮点数类型配置"""
+        value = self.get_config(parameter_name, default_value)
+        try:
+            return float(value) if value is not None else default_value
+        except (ValueError, TypeError):
+            logger.warning(f"配置 {parameter_name} 无法转换为浮点数,返回默认值 {default_value}")
+            return default_value
+    
+    def get_bool(self, parameter_name: str, default_value: bool = False) -> bool:
+        """获取布尔类型配置"""
+        value = self.get_config(parameter_name, default_value)
+        if isinstance(value, bool):
+            return value
+        elif isinstance(value, str):
+            return value.upper() in ['TRUE', 'YES', '1', 'ON']
+        elif isinstance(value, (int, float)):
+            return bool(value)
+        else:
+            return default_value
+    
+    def get_list(self, parameter_name: str, default_value: list = None) -> list:
+        """获取列表类型配置"""
+        if default_value is None:
+            default_value = []
+        value = self.get_config(parameter_name, default_value)
+        return value if isinstance(value, list) else default_value
+    
+    def set_config(self, parameter_name: str, value: Any) -> bool:
+        """
+        设置配置值
+        
+        Args:
+            parameter_name: 参数名称
+            value: 要设置的值
+            
+        Returns:
+            是否设置成功
+        """
+        try:
+            config = SystemConfig.query.filter_by(parameter_name=parameter_name).first()
+            
+            if config:
+                config.set_typed_value(value)
+                db.session.commit()
+                
+                # 更新缓存
+                with self._cache_lock:
+                    self._config_cache[parameter_name] = config.get_typed_value()
+                
+                logger.info(f"配置参数 {parameter_name} 已更新为: {value}")
+                return True
+            else:
+                logger.error(f"配置参数 {parameter_name} 不存在")
+                return False
+                
+        except Exception as e:
+            logger.error(f"设置配置参数 {parameter_name} 失败: {e}")
+            db.session.rollback()
+            return False
+    
+    def reload_config(self) -> None:
+        """重新加载所有配置(强制刷新缓存)"""
+        logger.info("强制重新加载配置缓存")
+        self._refresh_cache()
+    
+    def get_config_by_category(self, category: str) -> Dict[str, Any]:
+        """
+        获取指定分类的所有配置
+        
+        Args:
+            category: 配置分类
+            
+        Returns:
+            该分类下的所有配置字典
+        """
+        try:
+            configs = SystemConfig.query.filter_by(
+                category=category,
+                is_active=True
+            ).all()
+            
+            result = {}
+            for config in configs:
+                result[config.parameter_name] = config.get_typed_value()
+            
+            return result
+            
+        except Exception as e:
+            logger.error(f"获取分类 {category} 的配置失败: {e}")
+            return {}
+    
+    def get_cache_info(self) -> Dict[str, Any]:
+        """获取缓存状态信息"""
+        with self._cache_lock:
+            return {
+                'cache_size': len(self._config_cache),
+                'cache_timestamp': self._cache_timestamp,
+                'cache_age_seconds': time.time() - self._cache_timestamp,
+                'cache_ttl_seconds': self._cache_ttl,
+                'is_cache_valid': self._is_cache_valid()
+            }
+
+
+# 创建全局配置服务实例
+config_service = ConfigService()
+
+
+# 便捷函数,方便在其他模块中使用
+def get_config(parameter_name: str, default_value: Any = None) -> Any:
+    """获取配置值的便捷函数"""
+    return config_service.get_config(parameter_name, default_value)
+
+def get_str_config(parameter_name: str, default_value: str = '') -> str:
+    """获取字符串配置的便捷函数"""
+    return config_service.get_str(parameter_name, default_value)
+
+def get_int_config(parameter_name: str, default_value: int = 0) -> int:
+    """获取整数配置的便捷函数"""
+    return config_service.get_int(parameter_name, default_value)
+
+def get_float_config(parameter_name: str, default_value: float = 0.0) -> float:
+    """获取浮点数配置的便捷函数"""
+    return config_service.get_float(parameter_name, default_value)
+
+def get_bool_config(parameter_name: str, default_value: bool = False) -> bool:
+    """获取布尔配置的便捷函数"""
+    return config_service.get_bool(parameter_name, default_value)
+
+def get_list_config(parameter_name: str, default_value: list = None) -> list:
+    """获取列表配置的便捷函数"""
+    return config_service.get_list(parameter_name, default_value or [])
+
+def set_config(parameter_name: str, value: Any) -> bool:
+    """设置配置值的便捷函数"""
+    return config_service.set_config(parameter_name, value)
+
+def reload_config() -> None:
+    """重新加载配置的便捷函数"""
+    config_service.reload_config()

+ 597 - 0
app/services/data_scraper.py

@@ -0,0 +1,597 @@
+"""
+数据爬取服务
+用于从外部网站获取期货数据
+"""
+
+import requests
+from bs4 import BeautifulSoup
+import pandas as pd
+import logging
+from datetime import datetime
+import re
+
+logger = logging.getLogger(__name__)
+
+class FutureDataScraper:
+    """期货数据爬取类"""
+    
+    def __init__(self, url=None):
+        """初始化爬虫"""
+        # 从配置服务获取数据源URL
+        if url is None:
+            try:
+                from app.services.config_service import get_str_config
+                url = get_str_config('data_source_url', 'http://121.37.80.177/fees.html')
+            except Exception as e:
+                logger.warning(f"获取配置失败,使用默认URL: {e}")
+                url = "http://121.37.80.177/fees.html"
+        
+        self.url = url
+        logger.info(f"期货数据爬虫初始化,URL: {self.url}")
+    
+    def fetch_future_daily(self):
+        """
+        从网页爬取期货每日数据
+        返回一个包含期货数据的DataFrame
+        """
+        try:
+            # 检查当前时间是否在交易时间内
+            current_time = datetime.now().time()
+            
+            # 从配置服务获取交易时间
+            try:
+                from app.services.config_service import get_str_config
+                trading_start = get_str_config('trading_start_time', '09:00')
+                trading_end = get_str_config('trading_end_time', '17:00')
+            except Exception as e:
+                logger.warning(f"获取交易时间配置失败,使用默认值: {e}")
+                trading_start = '09:00'
+                trading_end = '17:00'
+                
+            is_trading_hours = (
+                current_time >= datetime.strptime(trading_start, '%H:%M').time() and 
+                current_time <= datetime.strptime(trading_end, '%H:%M').time()
+            )
+            logger.debug(f"当前时间: {current_time}, 交易时间: {trading_start}-{trading_end}, 是否在交易时间内: {is_trading_hours}")
+            
+            # 发送HTTP请求
+            logger.debug(f"开始请求URL: {self.url}")
+            response = requests.get(self.url)
+            response.raise_for_status()  # 如果请求失败则抛出异常
+            
+            # 尝试多种编码方式
+            try:
+                from app.services.config_service import get_str_config
+                encoding_list = get_str_config('encoding_attempts', 'gb2312,gbk,gb18030,utf-8')
+                encodings = [enc.strip() for enc in encoding_list.split(',')]
+            except Exception as e:
+                logger.warning(f"获取编码配置失败,使用默认值: {e}")
+                encodings = ['gb2312', 'gbk', 'gb18030', 'utf-8']
+            
+            html_text = None
+            
+            for encoding in encodings:
+                try:
+                    html_text = response.content.decode(encoding)
+                    print(f'成功使用 {encoding} 编码解析')
+                    break
+                except UnicodeDecodeError:
+                    continue
+            
+            if html_text is None:
+                html_text = response.content.decode('utf-8', errors='ignore')
+                print('使用UTF-8忽略错误模式解析')
+            
+            # 使用BeautifulSoup解析HTML
+            soup = BeautifulSoup(html_text, 'html.parser')
+            
+            # 查找表格
+            table = soup.find('table')
+            if not table:
+                logger.error("未找到数据表格")
+                logger.debug(f"页面内容前100字符: {html_text[:100]}")
+                return None
+            
+            logger.debug("找到数据表格,开始解析")
+            
+            # 解析表格数据
+            headers = []
+            header_row = table.find('tr')
+            if header_row:
+                headers = [th.text.strip() for th in header_row.find_all('th')]
+                logger.debug(f"表头: {headers}")
+                
+                # 检查表头是否包含预期的中文字段
+                if not any(('交易所' in h or '合约' in h) for h in headers):
+                    logger.warning("表头可能存在编码问题,没有找到预期的中文字段")
+            else:
+                logger.warning("未找到表头行")
+            
+            rows = []
+            data_rows = table.find_all('tr')[1:]  # 跳过表头行
+            logger.debug(f"找到 {len(data_rows)} 行数据")
+            
+            # 用于存储每个品种的所有合约数据
+            product_contracts = {}
+            
+            # 查找持仓量列的位置
+            open_interest_col = -1
+            for i, header in enumerate(headers):
+                if '持仓量' in header:
+                    open_interest_col = i
+                    break
+            
+            # 如果没找到持仓量列,使用经验位置
+            if open_interest_col == -1:
+                try:
+                    from app.services.config_service import get_int_config
+                    default_col = get_int_config('open_interest_column_position', 21)
+                except Exception as e:
+                    logger.warning(f"获取持仓量列配置失败,使用默认值: {e}")
+                    default_col = 21
+                    
+                open_interest_col = default_col if len(headers) > default_col else len(headers) - 2
+                logger.debug(f"未找到持仓量列,使用经验位置 {open_interest_col} (配置默认值: {default_col})")
+            else:
+                logger.debug(f"找到持仓量列位置: {open_interest_col}")
+            
+            # 第一遍遍历:收集所有合约数据
+            for row in data_rows:
+                cols = row.find_all('td')
+                if not cols:
+                    continue
+                
+                # 获取合约代码和品种代码
+                contract_code = cols[1].text.strip()
+                product_code = ''.join([c for c in contract_code if c.isalpha()]).upper()
+                
+                # 获取持仓量(用于判断主力合约)
+                open_interest = 0
+                if open_interest_col >= 0 and len(cols) > open_interest_col:
+                    oi_text = cols[open_interest_col].text.strip()
+                    try:
+                        # 清理持仓量文本,移除逗号等
+                        import re
+                        oi_clean = re.sub(r'[^\d]', '', oi_text)
+                        if oi_clean:
+                            open_interest = int(oi_clean)
+                    except (ValueError, TypeError):
+                        open_interest = 0
+                
+                # 将合约信息存储到对应品种的列表中
+                if product_code not in product_contracts:
+                    product_contracts[product_code] = []
+                product_contracts[product_code].append((contract_code, open_interest))
+            
+            # 确定主力合约(根据持仓量)
+            product_main_contracts = {}
+            for product_code, contracts in product_contracts.items():
+                if contracts:
+                    # 按持仓量排序,取持仓量最大的合约
+                    main_contract = max(contracts, key=lambda x: x[1])[0]
+                    max_oi = max(contracts, key=lambda x: x[1])[1]
+                    product_main_contracts[product_code] = main_contract
+                    # logger.debug(f"根据持仓量确定主力合约: {product_code} {main_contract} (持仓量: {max_oi})")
+            
+            logger.debug(f"识别出 {len(product_main_contracts)} 个主力合约")
+            
+            # 第二遍遍历:处理所有数据行
+            for row in data_rows:
+                cols = row.find_all('td')
+                if not cols:
+                    continue
+                
+                # 获取合约代码和品种代码
+                contract_code = cols[1].text.strip()
+                product_code = ''.join([c for c in contract_code if c.isalpha()]).upper()
+                
+                # 判断是否为主力合约
+                is_main = False
+                if product_code in product_main_contracts:
+                    if contract_code == product_main_contracts[product_code]:
+                        is_main = True
+                        # logger.debug(f"标记主力合约: {product_code} {contract_code}")
+                
+                # 收集行数据
+                row_data = [col.text.strip() for col in cols]
+                row_data.append(is_main)  # 添加主力合约标志
+                rows.append(row_data)
+            
+            # 创建DataFrame
+            headers.append('is_main_contract')  # 添加主力合约标志列
+            
+            # 确保所有行的长度与表头一致
+            for i, row in enumerate(rows):
+                if len(row) != len(headers):
+                    logger.warning(f"第{i+1}行数据长度({len(row)})与表头长度({len(headers)})不一致,进行调整")
+                    # 如果行长度不够,用空字符串填充
+                    if len(row) < len(headers):
+                        row.extend([''] * (len(headers) - len(row)))
+                    # 如果行长度超过,截断
+                    elif len(row) > len(headers):
+                        row = row[:len(headers)]
+                        rows[i] = row
+            
+            df = pd.DataFrame(rows, columns=headers)
+            
+            # 记录日志
+            logger.debug(f"成功获取期货数据,共{len(df)}条记录")
+            logger.debug(f"数据前5行: {df.head()}")
+            
+            return df
+            
+        except Exception as e:
+            logger.error(f"获取期货数据失败: {str(e)}", exc_info=True)
+            return None
+    
+    def update_future_daily(self, db_session, FutureDaily):
+        """
+        更新数据库中的future_daily表
+        参数:
+            db_session: 数据库会话
+            FutureDaily: 期货日数据模型类
+        返回:
+            更新的记录数量
+        """
+        try:
+            # 获取数据
+            df = self.fetch_future_daily()
+            if df is None or df.empty:
+                logger.error("无法更新期货日数据: 未获取到数据")
+                return 0
+            
+            # 打印表头查看具体的字段名
+            logger.debug(f"表格的字段名: {list(df.columns)}")
+            
+            # 清空当前数据表
+            db_session.query(FutureDaily).delete()
+            
+            # 创建新记录
+            records = []
+            update_time = datetime.now()
+            
+            # 遍历DataFrame中的每一行
+            for idx, row in df.iterrows():
+                try:
+                    # 提取合约代码和品种代码
+                    contract_code = row.get('合约代码', '')
+                    if not contract_code:
+                        logger.warning(f"第{idx+1}行没有合约代码,跳过")
+                        continue
+                    
+                    # 提取品种代码(合约代码中的字母部分)
+                    product_code = ''.join([c for c in contract_code if c.isalpha()])
+                    
+                    # 记录处理的行号和关键字段
+                    # logger.debug(f"处理第{idx+1}行,合约代码: {contract_code}, 产品代码: {product_code}")
+                    
+                    # 创建新记录
+                    record = FutureDaily(
+                        exchange=row.get('交易所', ''),
+                        contract_code=contract_code,
+                        contract_name=row.get('合约名称', ''),
+                        product_code=product_code,
+                        product_name=row.get('品种名称', ''),
+                        contract_multiplier=self._safe_float(row.get('合约乘数', 0)),
+                        price_tick=self._safe_float(row.get('最小跳动', 0)),
+                        open_fee_rate=self._safe_float(row.get('开仓费率', 0)),
+                        open_fee=self._safe_float(row.get('开仓费用/手', 0)),
+                        close_fee_rate=self._safe_float(row.get('平仓费率', 0)),
+                        close_fee=self._safe_float(row.get('平仓费用/手', 0)),
+                        close_today_fee_rate=self._safe_float(row.get('平今费率', 0)),
+                        close_today_fee=self._safe_float(row.get('平今费用/手', 0)),
+                        long_margin_rate=self._safe_float(row.get('做多保证金率', 0)),
+                        long_margin_fee=self._safe_float(row.get('做多保证金/手', 0)),
+                        short_margin_rate=self._safe_float(row.get('做空保证金率', 0)),
+                        short_margin_fee=self._safe_float(row.get('做空保证金/手', 0)),
+                        latest_price=self._safe_float(row.get('最新价', 0)),
+                        open_interest=self._safe_int(row.get('持仓量', 0)),
+                        volume=self._safe_int(row.get('成交量', 0)),
+                        is_main_contract=row.get('is_main_contract', False),
+                        update_time=update_time
+                    )
+                    if row.get('is_main_contract', False):
+                        logger.debug(f"保存主力合约到数据库: {product_code} {contract_code}")
+                    records.append(record)
+                    
+                except Exception as e:
+                    logger.error(f"解析期货日数据行失败(行号:{idx+1}): {str(e)}", exc_info=True)
+                    continue
+            
+            # 批量添加记录
+            if records:
+                db_session.add_all(records)
+                db_session.commit()
+                logger.debug(f"成功更新期货日数据,共{len(records)}条记录")
+                return len(records)
+            else:
+                logger.warning("无期货日数据可更新")
+                return 0
+                
+        except Exception as e:
+            logger.error(f"更新期货日数据失败: {str(e)}", exc_info=True)
+            db_session.rollback()
+            return 0
+    
+    def _normalize_contract_code(self, contract_code):
+        """
+        标准化合约代码格式
+        例如:将 'AP505' 转换为 'AP2505',同时确保字母部分为大写
+        """
+        try:
+            if not contract_code:
+                return contract_code
+            
+            # 提取字母部分和数字部分,并将字母转换为大写
+            letters = ''.join(c for c in contract_code if c.isalpha()).upper()
+            numbers = ''.join(c for c in contract_code if c.isdigit())
+            
+            # 如果数字部分是3位数,在前面加上2
+            if len(numbers) == 3:
+                numbers = '2' + numbers
+            
+            return letters + numbers
+        except Exception as e:
+            logger.error(f"合约代码格式转换失败: {str(e)}")
+            return contract_code
+
+    def update_future_info_from_daily(self, db_session, FutureInfo, FutureDaily):
+        """
+        根据future_daily表更新future_info表的数据
+        参数:
+            db_session: 数据库会话
+            FutureInfo: 期货基础信息模型类
+            FutureDaily: 期货日数据模型类
+        返回:
+            更新的记录数量
+        """
+        try:
+            # 获取当前数据库中的所有期货基础信息
+            # 将contract_letter转换为大写用于统一比较
+            futures = {f.contract_letter.upper(): f for f in db_session.query(FutureInfo).all()}
+            logger.debug(f"从future_info表获取到{len(futures)}个期货品种")
+            
+            # 获取最新的future_daily数据的所有产品代码
+            product_data = {}
+            
+            # 记录每个品种的主力合约
+            product_main_contracts = {}
+            main_contract_records = db_session.query(FutureDaily).filter(FutureDaily.is_main_contract == True).all()
+            logger.debug(f"查询到{len(main_contract_records)}条主力合约记录")
+            
+            for daily in main_contract_records:
+                product_code_upper = daily.product_code.upper()
+                if product_code_upper not in product_main_contracts:
+                    product_main_contracts[product_code_upper] = self._normalize_contract_code(daily.contract_code)
+                    # logger.debug(f"从数据库获取主力合约: {product_code_upper} -> {daily.contract_code}")
+            
+            logger.debug(f"整理得到{len(product_main_contracts)}个主力合约: {product_main_contracts}")
+            
+            # 如果没有找到主力合约,尝试根据持仓量重新识别
+            if not product_main_contracts:
+                logger.debug("未找到主力合约,尝试根据持仓量重新识别...")
+                # 按品种分组,找出每个品种持仓量最大的合约
+                product_contracts = {}
+                for daily in db_session.query(FutureDaily).all():
+                    product_code_upper = daily.product_code.upper()
+                    if product_code_upper not in product_contracts:
+                        product_contracts[product_code_upper] = []
+                    product_contracts[product_code_upper].append((daily.contract_code, daily.open_interest or 0))
+                
+                # 确定每个品种的主力合约
+                for product_code, contracts in product_contracts.items():
+                    if contracts:
+                        # 按持仓量排序,取持仓量最大的合约
+                        main_contract = max(contracts, key=lambda x: x[1])[0]
+                        max_oi = max(contracts, key=lambda x: x[1])[1]
+                        product_main_contracts[product_code] = self._normalize_contract_code(main_contract)
+                        logger.debug(f"根据持仓量识别主力合约: {product_code} {main_contract} (持仓量: {max_oi})")
+                
+                logger.debug(f"重新识别出 {len(product_main_contracts)} 个主力合约")
+            
+            # 只获取主力合约的数据用于更新future_info表
+            main_daily_records = db_session.query(FutureDaily).filter(FutureDaily.is_main_contract == True).all()
+            logger.debug(f"查询主力合约数据: 找到{len(main_daily_records)}条记录")
+            
+            for daily in main_daily_records:
+                product_code_upper = daily.product_code.upper()
+                product_data[product_code_upper] = daily
+                # logger.debug(f"获取主力合约数据: {product_code_upper} -> {daily.contract_code}")
+            
+            logger.debug(f"从future_daily表获取到{len(product_data)}个主力合约的数据: {list(product_data.keys())}")
+            
+            # 更新计数器
+            updated_count = 0
+            not_found_count = 0
+            
+            # 更新期货基础信息
+            logger.debug(f"开始匹配和更新期货基础信息: future_info有{len(futures)}个品种,product_data有{len(product_data)}个品种")
+            for contract_letter, future in futures.items():
+                contract_letter_upper = contract_letter.upper()
+                # logger.debug(f"检查期货品种: {contract_letter_upper} -> 是否在product_data中: {contract_letter_upper in product_data}")
+                if contract_letter_upper in product_data:
+                    daily = product_data[contract_letter_upper]
+                    
+                    # 记录更新前的值
+                    old_values = {
+                        'exchange': future.exchange,
+                        'contract_multiplier': future.contract_multiplier,
+                        'long_margin_rate': future.long_margin_rate,
+                        'short_margin_rate': future.short_margin_rate,
+                        'open_fee': future.open_fee,
+                        'close_fee': future.close_fee,
+                        'close_today_rate': future.close_today_rate,
+                        'close_today_fee': future.close_today_fee,
+                        'current_main_contract': future.current_main_contract
+                    }
+                    
+                    # 更新字段
+                    future.exchange = daily.exchange
+                    future.contract_multiplier = daily.contract_multiplier
+                    future.long_margin_rate = daily.long_margin_rate
+                    future.short_margin_rate = daily.short_margin_rate
+                    future.open_fee = daily.open_fee
+                    future.close_fee = daily.close_fee
+                    future.close_today_rate = daily.close_today_fee_rate
+                    future.close_today_fee = daily.close_today_fee
+                    
+                    # 根据当前价格和保证金率计算保证金金额
+                    if hasattr(daily, 'latest_price') and daily.latest_price and future.contract_multiplier:
+                        if future.long_margin_rate:
+                            future.long_margin_amount = daily.latest_price * future.contract_multiplier * future.long_margin_rate
+                        if future.short_margin_rate:
+                            future.short_margin_amount = daily.latest_price * future.contract_multiplier * future.short_margin_rate
+                    
+                    # 更新主力合约
+                    if contract_letter_upper in product_main_contracts:
+                        future.current_main_contract = product_main_contracts[contract_letter_upper]
+                    
+                    # 检查是否有实际更新
+                    has_changes = False
+                    changes = []
+                    for field, old_value in old_values.items():
+                        new_value = getattr(future, field)
+                        if old_value != new_value:
+                            has_changes = True
+                            changes.append(f"{field}: {old_value} -> {new_value}")
+                    
+                    if has_changes:
+                        logger.debug(f"更新期货 {future.contract_letter} ({future.name}): {', '.join(changes)}")
+                        updated_count += 1
+                else:
+                    not_found_count += 1
+                    logger.warning(f"未找到期货 {future.contract_letter} ({future.name}) 的每日数据")
+            
+            # 更新不在future_info中的主力合约
+            for product_code, contract_code in product_main_contracts.items():
+                if product_code not in futures:
+                    # 创建新的期货基础信息记录
+                    daily = product_data.get(product_code)
+                    if daily:
+                        future_info = FutureInfo(
+                            contract_letter=product_code,
+                            name=daily.product_name,
+                            market=0,  # 默认为国内市场
+                            exchange=daily.exchange,
+                            contract_multiplier=daily.contract_multiplier,
+                            long_margin_rate=daily.long_margin_rate,
+                            short_margin_rate=daily.short_margin_rate,
+                            open_fee=daily.open_fee,
+                            close_fee=daily.close_fee,
+                            close_today_rate=daily.close_today_fee_rate,
+                            close_today_fee=daily.close_today_fee,
+                            # 计算保证金金额
+                            long_margin_amount=(daily.latest_price * daily.contract_multiplier * daily.long_margin_rate) if daily.latest_price and daily.long_margin_rate else None,
+                            short_margin_amount=(daily.latest_price * daily.contract_multiplier * daily.short_margin_rate) if daily.latest_price and daily.short_margin_rate else None,
+                            current_main_contract=contract_code
+                        )
+                        db_session.add(future_info)
+                        updated_count += 1
+                        logger.debug(f"新增期货品种 {product_code} ({daily.product_name})")
+            
+            # 提交更改
+            db_session.commit()
+            
+            logger.debug(f"根据期货日数据成功更新{updated_count}条期货基础信息,{not_found_count}个期货未找到对应数据")
+            return updated_count
+            
+        except Exception as e:
+            logger.error(f"根据期货日数据更新期货基础信息失败: {str(e)}")
+            db_session.rollback()
+            return 0
+    
+    def update_future_info(self, db_session, FutureInfo):
+        """
+        更新数据库中的期货基础信息 (直接从网页获取)
+        参数:
+            db_session: 数据库会话
+            FutureInfo: 期货基础信息模型类
+        返回:
+            更新的记录数量
+        """
+        try:
+            # 获取数据
+            df = self.fetch_future_daily()
+            if df is None or df.empty:
+                logger.error("无法更新期货基础信息: 未获取到数据")
+                return 0
+            
+            # 获取当前数据库中的所有期货基础信息
+            futures = {f.contract_letter: f for f in db_session.query(FutureInfo).all()}
+            
+            # 更新计数器
+            updated_count = 0
+            
+            # 遍历DataFrame中的每一行
+            for _, row in df.iterrows():
+                try:
+                    # 提取合约字母
+                    contract_code = row.get('合约代码', '')
+                    if not contract_code:
+                        continue
+                    
+                    # 假设合约代码的前1-2位是合约字母
+                    contract_letter = ''.join([c for c in contract_code if c.isalpha()])
+                    
+                    # 如果合约字母在数据库中存在,则更新相应字段
+                    if contract_letter in futures:
+                        future = futures[contract_letter]
+                        
+                        # 更新字段
+                        future.exchange = row.get('交易所', '')
+                        future.contract_multiplier = self._safe_float(row.get('合约乘数', 0))
+                        future.long_margin_rate = self._safe_float(row.get('做多保证金率', 0))
+                        future.short_margin_rate = self._safe_float(row.get('做空保证金率', 0))
+                        future.open_fee = self._safe_float(row.get('开仓费用/手', 0))
+                        future.close_fee = self._safe_float(row.get('平仓费用/手', 0))
+                        future.close_today_rate = self._safe_float(row.get('平今费率', 0))
+                        future.close_today_fee = self._safe_float(row.get('平今费用/手', 0))
+                        
+                        # 更新保证金金额字段(从网站的"做多1手保证金"和"做空1手保证金")
+                        future.long_margin_amount = self._safe_float(row.get('做多1手保证金', 0))
+                        future.short_margin_amount = self._safe_float(row.get('做空1手保证金', 0))
+                        
+                        # 如果网站字段名不同,尝试其他可能的字段名
+                        if future.long_margin_amount is None:
+                            future.long_margin_amount = self._safe_float(row.get('1手做多保证金', 0))
+                        if future.short_margin_amount is None:
+                            future.short_margin_amount = self._safe_float(row.get('1手做空保证金', 0))
+                        
+                        # 如果是主连合约,更新主力合约字段
+                        if row.get('is_main_contract', False):
+                            future.current_main_contract = contract_code
+                        
+                        updated_count += 1
+                        
+                except Exception as e:
+                    logger.error(f"更新单个期货信息失败: {str(e)}")
+                    continue
+            
+            # 提交更改
+            db_session.commit()
+            
+            logger.debug(f"成功更新{updated_count}条期货基础信息")
+            return updated_count
+            
+        except Exception as e:
+            logger.error(f"更新期货基础信息失败: {str(e)}")
+            db_session.rollback()
+            return 0
+    
+    def _safe_float(self, value):
+        """安全地转换为浮点数"""
+        try:
+            if pd.isna(value):
+                return None
+            return float(value)
+        except (ValueError, TypeError):
+            return None
+    
+    def _safe_int(self, value):
+        """安全地转换为整数"""
+        try:
+            if pd.isna(value):
+                return None
+            return int(value)
+        except (ValueError, TypeError):
+            return None 

+ 418 - 0
app/services/data_update.py

@@ -0,0 +1,418 @@
+"""
+数据更新服务
+负责定期更新期货数据
+"""
+
+import logging
+import threading
+import yaml
+from pathlib import Path
+from apscheduler.schedulers.background import BackgroundScheduler
+from apscheduler.triggers.cron import CronTrigger
+from app.database.db_manager import db
+from app.models.future_info import FutureInfo, FutureDaily
+from app.services.data_scraper import FutureDataScraper
+from retrying import retry
+import os
+
+logger = logging.getLogger(__name__)
+
+class DataUpdateService:
+    """
+    数据更新服务
+    用于定期更新数据库中的期货数据
+    """
+    _instance = None
+    _lock = threading.Lock()
+    
+    def __new__(cls, app=None):
+        with cls._lock:
+            if cls._instance is None:
+                cls._instance = super().__new__(cls)
+                cls._instance.app = None
+                cls._instance.scraper = None  # 延迟初始化爬虫
+                cls._instance.scheduler = None  # 延迟初始化调度器
+                cls._instance.config = None
+                cls._instance._initialized = False
+            return cls._instance
+    
+    def __init__(self, app=None):
+        with self._lock:
+            if not self._initialized:
+                logger.info("初始化数据更新服务...")
+                self.app = app
+                self.config = None  # 延迟加载配置
+                self._initialized = True
+                
+                if app is not None:
+                    self.init_app(app)
+    
+    def _load_config(self):
+        """
+        加载配置文件,优先从配置服务读取定时任务配置
+        """
+        config_path = Path("config.yaml")
+        
+        # 尝试从配置服务读取定时任务配置
+        try:
+            from app.services.config_service import get_int_config, get_str_config
+            morning_hour = get_int_config('schedule_morning_hour', 9)
+            morning_minute = get_int_config('schedule_morning_minute', 0)
+            afternoon_hour = get_int_config('schedule_afternoon_hour', 15)
+            afternoon_minute = get_int_config('schedule_afternoon_minute', 0)
+            
+            config_from_service = {
+                "data_update": {
+                    "schedule": [
+                        {"hour": str(morning_hour), "minute": str(morning_minute)},
+                        {"hour": str(afternoon_hour), "minute": str(afternoon_minute)}
+                    ]
+                }
+            }
+            logger.info(f"从配置服务加载定时任务配置: {morning_hour}:{morning_minute:02d}, {afternoon_hour}:{afternoon_minute:02d}")
+            return config_from_service
+            
+        except Exception as e:
+            logger.warning(f"从配置服务读取定时配置失败,尝试读取配置文件: {e}")
+        
+        # 如果配置服务读取失败,回退到配置文件
+        if not config_path.exists():
+            # 创建默认配置
+            default_config = {
+                "data_update": {
+                    "schedule": [
+                        {"hour": "9", "minute": "0"},
+                        {"hour": "15", "minute": "0"}
+                    ]
+                }
+            }
+            try:
+                config_filename = get_str_config('config_filename', 'config.yaml')
+                config_path = Path(config_filename)
+            except:
+                pass
+                
+            with open(config_path, "w", encoding="utf-8") as f:
+                yaml.dump(default_config, f, allow_unicode=True)
+            return default_config
+        
+        with open(config_path, "r", encoding="utf-8") as f:
+            return yaml.safe_load(f)
+    
+    def init_app(self, app):
+        """
+        初始化应用
+        
+        Args:
+            app: Flask应用实例
+        """
+        logger.info(f"初始化应用到数据更新服务,应用ID: {id(app)}")
+        self.app = app
+        
+        # 在有Flask应用上下文时加载配置和初始化组件
+        with app.app_context():
+            # 延迟加载配置
+            if self.config is None:
+                self.config = self._load_config()
+                logger.info("数据更新服务配置加载完成")
+            
+            # 延迟初始化爬虫
+            if self.scraper is None:
+                self.scraper = FutureDataScraper()
+                logger.info("数据爬虫初始化完成")
+        
+        # 在主进程中初始化调度器
+        if not self.scheduler:
+            self.scheduler = BackgroundScheduler()
+            if not app.debug or os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
+                logger.info("在主进程中启动调度器...")
+                self.start_scheduler()
+    
+    def _ensure_scraper_initialized(self):
+        """确保数据爬虫已初始化"""
+        if self.scraper is None:
+            logger.info("延迟初始化数据爬虫...")
+            self.scraper = FutureDataScraper()
+    
+    def _ensure_config_initialized(self):
+        """确保配置已加载"""
+        if self.config is None:
+            logger.info("延迟加载数据更新服务配置...")
+            self.config = self._load_config()
+    
+    def update_future_daily_data(self):
+        """
+        更新期货每日数据
+        
+        从网站爬取最新的期货数据,存入future_daily表,然后更新future_info表
+        """
+        try:
+            if not self.app:
+                logger.error("应用上下文未设置,无法更新数据")
+                return False
+                
+            with self.app.app_context():
+                logger.info("开始更新期货每日数据...")
+                # 确保爬虫已初始化
+                self._ensure_scraper_initialized()
+                # 更新future_daily表
+                records_count = self.scraper.update_future_daily(db.session, FutureDaily)
+                if records_count > 0:
+                    # 根据future_daily表更新future_info表
+                    updated_count = self.scraper.update_future_info_from_daily(db.session, FutureInfo, FutureDaily)
+                    logger.info(f"期货数据更新完成: 爬取{records_count}条记录,更新{updated_count}条期货基础信息")
+                    return True
+                else:
+                    logger.warning("期货数据更新失败: 未能爬取任何数据")
+                    return False
+        except Exception as e:
+            logger.error(f"期货数据更新出错: {str(e)}")
+            raise
+    
+    def manual_update(self):
+        """
+        手动触发数据更新
+        
+        Returns:
+            dict: 更新结果
+        """
+        try:
+            logger.info("开始手动更新数据...")
+            with self.app.app_context():
+                # 确保爬虫已初始化
+                self._ensure_scraper_initialized()
+                # 1. 更新future_daily表
+                logger.info("正在更新future_daily表...")
+                records_count = self.scraper.update_future_daily(db.session, FutureDaily)
+                
+                if records_count > 0:
+                    # 2. 更新future_info表
+                    logger.info("正在根据future_daily表更新future_info表...")
+                    updated_count = self.scraper.update_future_info_from_daily(db.session, FutureInfo, FutureDaily)
+                    
+                    return {
+                        'code': 0,
+                        'msg': f'数据更新成功:新增{records_count}条每日数据,更新{updated_count}条期货信息',
+                        'data': {
+                            'daily_count': records_count,
+                            'info_count': updated_count
+                        }
+                    }
+                else:
+                    logger.warning("未能获取到新的每日数据")
+                    return {
+                        'code': 1,
+                        'msg': '未能获取到新的数据,可能是网络问题或数据源未更新',
+                        'data': None
+                    }
+                    
+        except Exception as e:
+            error_msg = f"手动更新数据失败: {str(e)}"
+            logger.error(error_msg)
+            return {
+                'code': 1,
+                'msg': error_msg,
+                'data': None
+            }
+    
+    def start_scheduler(self):
+        """
+        启动定时任务调度器
+        """
+        if not self.scheduler:
+            logger.warning("调度器未初始化,无法启动")
+            return
+            
+        if self.scheduler.running:
+            logger.info(f"调度器已经在运行中,scheduler_id: {id(self.scheduler)}")
+            return
+        
+        try:
+            logger.info(f"开始启动调度器,scheduler_id: {id(self.scheduler)}")
+            
+            # 获取调度器配置,优先从配置服务读取
+            try:
+                from app.services.config_service import get_str_config, get_int_config, get_bool_config
+                scheduler_timezone = get_str_config('scheduler_timezone', 'Asia/Shanghai')
+                scheduler_max_instances = get_int_config('scheduler_max_instances', 1)
+                scheduler_coalesce = get_bool_config('scheduler_coalesce', True)
+                scheduler_misfire_grace_time = get_int_config('scheduler_misfire_grace_time', 60)
+                
+                logger.info(f"从配置服务读取调度器配置: timezone={scheduler_timezone}, max_instances={scheduler_max_instances}")
+            except Exception as e:
+                logger.warning(f"从配置服务读取调度器配置失败,使用默认配置: {e}")
+                # 确保配置已加载
+                self._ensure_config_initialized()
+                scheduler_config = self.config.get("data_update", {}).get("scheduler", {})
+                scheduler_timezone = scheduler_config.get("timezone", "Asia/Shanghai")
+                scheduler_max_instances = scheduler_config.get("max_instances", 1)
+                scheduler_coalesce = scheduler_config.get("coalesce", True)
+                scheduler_misfire_grace_time = scheduler_config.get("misfire_grace_time", 60)
+            
+            # 配置调度器
+            self.scheduler.configure(
+                timezone=scheduler_timezone,
+                max_instances=scheduler_max_instances,
+                coalesce=scheduler_coalesce,
+                misfire_grace_time=scheduler_misfire_grace_time
+            )
+            
+            # 从配置文件读取定时设置
+            # 确保配置已加载
+            self._ensure_config_initialized()
+            schedule_config = self.config.get("data_update", {}).get("schedule", [])
+            logger.info(f"读取到的定时配置: {schedule_config}")
+            
+            # 添加定时任务
+            for schedule in schedule_config:
+                try:
+                    hour = schedule.get("hour", "*")
+                    minute = schedule.get("minute", "0")
+                    job_id = f"update_future_data_{hour}_{minute}"
+                    
+                    # 创建触发器
+                    trigger = CronTrigger(
+                        hour=hour,
+                        minute=minute,
+                        timezone=scheduler_timezone  # 使用前面获取的配置
+                    )
+                    
+                    # 如果启用重试,创建重试装饰器
+                    if schedule.get("retry", False):
+                        # 从配置服务获取重试配置
+                        try:
+                            from app.services.config_service import get_int_config
+                            max_retries = get_int_config('retry_max_attempts', 3)
+                            retry_delay = get_int_config('retry_delay_seconds', 300)
+                        except Exception as e:
+                            logger.warning(f"获取重试配置失败,使用默认值: {e}")
+                            max_retries = schedule.get("max_retries", 3)
+                            retry_delay = schedule.get("retry_delay", 300)
+                        
+                        @retry(
+                            stop_max_attempt_number=max_retries + 1,
+                            wait_fixed=retry_delay * 1000,  # 毫秒
+                            retry_on_exception=lambda e: isinstance(e, Exception)
+                        )
+                        def wrapped_task():
+                            return self.update_future_daily_data()
+                        
+                        task_func = wrapped_task
+                    else:
+                        task_func = self.update_future_daily_data
+                    
+                    # 添加任务
+                    job = self.scheduler.add_job(
+                        task_func,
+                        trigger=trigger,
+                        id=job_id,
+                        replace_existing=True,
+                        max_instances=1,
+                        coalesce=True
+                    )
+                    
+                    # 安全地获取下次运行时间
+                    next_run = job.next_run_time.strftime('%Y-%m-%d %H:%M:%S') if job and hasattr(job, 'next_run_time') and job.next_run_time else '未知'
+                    logger.info(f"添加定时任务: {job_id}, 下次运行时间: {next_run}")
+                    
+                except Exception as e:
+                    logger.error(f"添加定时任务失败: {str(e)}")
+            
+            # 启动调度器
+            if not self.scheduler.running:
+                self.scheduler.start()
+                logger.info(f"数据更新调度器已启动,当前状态: {self.get_scheduler_status()}")
+            
+        except Exception as e:
+            logger.error(f"启动调度器失败: {str(e)}")
+            # 确保调度器被正确关闭
+            if self.scheduler and self.scheduler.running:
+                try:
+                    self.scheduler.shutdown()
+                except Exception as shutdown_error:
+                    logger.warning(f"关闭失败的调度器时出错: {str(shutdown_error)}")
+    
+    def stop_scheduler(self):
+        """
+        停止定时任务调度器
+        """
+        if not self.scheduler:
+            logger.debug("调度器未初始化,无需停止")
+            return
+            
+        if not self.scheduler.running:
+            # logger.debug(f"调度器未在运行,scheduler_id: {id(self.scheduler)}")
+            return
+        
+        logger.info(f"开始停止调度器,scheduler_id: {id(self.scheduler)}")
+        try:
+            self.scheduler.shutdown()
+            logger.info("数据更新调度器已停止")
+        except Exception as e:
+            logger.warning(f"停止调度器时出错: {str(e)}")
+
+    def get_scheduler_status(self):
+        """
+        获取调度器状态
+        """
+        if not self.scheduler:
+            return {
+                "status": "未初始化",
+                "jobs": []
+            }
+        
+        try:
+            jobs = []
+            if self.scheduler.running:
+                for job in self.scheduler.get_jobs():
+                    jobs.append({
+                        "id": job.id,
+                        "next_run_time": job.next_run_time.strftime("%Y-%m-%d %H:%M:%S") if job.next_run_time else None,
+                        "trigger": str(job.trigger)
+                    })
+            
+            return {
+                "status": "运行中" if self.scheduler.running else "已停止",
+                "jobs": jobs
+            }
+        except Exception as e:
+            logger.warning(f"获取调度器状态时出错: {str(e)}")
+            return {
+                "status": "获取状态失败",
+                "jobs": []
+            }
+
+# 创建服务实例
+data_update_service = DataUpdateService()
+
+def init_data_update_service(app):
+    """
+    初始化数据更新服务
+    
+    Args:
+        app: Flask应用实例
+    """
+    data_update_service.init_app(app)
+    
+    # 设置应用标记,用于跟踪是否已经初始化
+    app._future_data_initialized = False
+    
+    # 在Flask 2.2+中,before_first_request被移除,使用after_request替代
+    @app.after_request
+    def after_request_handler(response):
+        # 检查是否需要初始化数据
+        if not app._future_data_initialized:
+            app._future_data_initialized = True
+            # 在后台线程中执行数据更新
+            thread = threading.Thread(target=data_update_service.update_future_daily_data)
+            thread.daemon = True
+            thread.start()
+        return response
+    
+    # 启动定时任务调度器
+    data_update_service.start_scheduler()
+    
+    # 应用关闭时停止调度器
+    @app.teardown_appcontext
+    def stop_scheduler(exception=None):
+        data_update_service.stop_scheduler() 

+ 302 - 0
app/services/trade_logic.py

@@ -0,0 +1,302 @@
+# app/services/trade_logic.py
+"""
+Business logic related to TradeRecord generation and synchronization.
+"""
+from app import db
+from app.models.trade import TradeRecord
+from app.models.transaction import TransactionRecord
+from datetime import datetime
+import traceback
+
+def generate_trade_from_transactions(transactions):
+    """从交易记录生成交易汇总记录对象 (但不写入数据库)"""
+    if not transactions:
+        # print("没有交易记录可用于生成汇总")
+        return None
+
+    # print(f"从{len(transactions)}条交易记录尝试生成交易汇总")
+
+    open_trans = None
+    close_trans = []
+
+    # 找到开仓交易
+    for trans in transactions:
+        if trans.position_type is None:
+            print(f"  警告: 交易ID={trans.id}缺少position_type")
+            continue
+
+        if trans.position_type in [0, 2]:  # 开多 or 开空
+            if not open_trans:  # 只取第一个开仓交易
+                open_trans = trans
+        elif trans.position_type in [1, 3]: # 平多 or 平空
+             close_trans.append(trans)
+        else:
+            print(f"  警告: 交易ID={trans.id}的仓位类型{trans.position_type}无效")
+
+    if not open_trans:
+        # print("  没有找到有效的开仓交易")
+        return None
+
+    # 确保开仓和平仓交易匹配
+    valid_close_trans = []
+    for trans in close_trans:
+        if (open_trans.position_type == 0 and trans.position_type == 1) or \
+           (open_trans.position_type == 2 and trans.position_type == 3):
+            valid_close_trans.append(trans)
+
+    close_trans = valid_close_trans
+
+    # 计算平均售价和收益
+    total_close_amount = sum(t.price * t.volume for t in close_trans if t.price is not None and t.volume is not None)
+    total_close_volume = sum(t.volume for t in close_trans if t.volume is not None)
+    average_sale_price = total_close_amount / total_close_volume if total_close_volume > 0 else None
+
+    # 计算收益
+    single_profit = None
+    if average_sale_price is not None and open_trans.contract_multiplier is not None and open_trans.price is not None and total_close_volume is not None:
+        try:
+            if open_trans.position_type == 0:  # 多头
+                single_profit = (average_sale_price - open_trans.price) * total_close_volume * open_trans.contract_multiplier
+            else:  # 空头
+                single_profit = (open_trans.price - average_sale_price) * total_close_volume * open_trans.contract_multiplier
+        except TypeError as e:
+            print(f"  计算收益时发生类型错误: {e}. Open price: {open_trans.price}, Avg sale price: {average_sale_price}, Vol: {total_close_volume}, Multiplier: {open_trans.contract_multiplier}")
+            single_profit = None
+
+    # 计算投资额 (开仓成本)
+    investment_amount = None
+    if open_trans.price is not None and open_trans.volume is not None and open_trans.contract_multiplier is not None:
+        try:
+            investment_amount = open_trans.price * open_trans.volume * open_trans.contract_multiplier
+        except TypeError:
+             print(f"  计算投资额时发生类型错误: Price: {open_trans.price}, Vol: {open_trans.volume}, Multiplier: {open_trans.contract_multiplier}")
+             investment_amount = None
+
+    # 计算投资收益率
+    investment_profit_rate = single_profit / investment_amount if single_profit is not None and investment_amount and investment_amount != 0 else None
+
+    # 计算持仓天数
+    close_time = max(t.transaction_time for t in close_trans if t.transaction_time) if close_trans else None
+    holding_days = (close_time - open_trans.transaction_time).days if close_time and open_trans.transaction_time else None
+
+    # 计算年化收益率
+    annual_profit_rate = investment_profit_rate * 365 / holding_days if investment_profit_rate is not None and holding_days and holding_days > 0 else None
+
+    # 创建交易汇总记录对象
+    try:
+        roll_trade_main_id = getattr(open_trans, 'roll_id', None)
+
+        trade = TradeRecord(
+            roll_trade_main_id=roll_trade_main_id,
+            contract_code=open_trans.contract_code,
+            name=open_trans.name,
+            account=open_trans.account,
+            strategy_id=open_trans.strategy_ids,
+            strategy_name=open_trans.strategy_name,
+            position_type=0 if open_trans.position_type == 0 else 1,
+            candle_pattern_id=open_trans.candle_pattern_ids,
+            candle_pattern=open_trans.candle_pattern,
+            open_time=open_trans.transaction_time,
+            close_time=close_time,
+            position_volume=open_trans.volume,
+            contract_multiplier=open_trans.contract_multiplier,
+            past_position_cost=investment_amount,
+            average_sale_price=average_sale_price,
+            single_profit=single_profit,
+            investment_profit=single_profit,
+            investment_profit_rate=investment_profit_rate,
+            holding_days=holding_days,
+            annual_profit_rate=annual_profit_rate,
+            trade_type=open_trans.trade_type,
+            confidence_index=open_trans.confidence_index,
+            similarity_evaluation=open_trans.similarity_evaluation,
+            long_trend_ids=getattr(open_trans, 'long_trend_ids', None),
+            long_trend_name=getattr(open_trans, 'long_trend_name', None),
+            mid_trend_ids=getattr(open_trans, 'mid_trend_ids', None),
+            mid_trend_name=getattr(open_trans, 'mid_trend_name', None)
+        )
+        return trade
+
+    except Exception as e:
+        print(f"  创建交易汇总记录对象时出错: {str(e)}")
+        print(traceback.format_exc())
+        return None
+
+def update_trade_record(trade_id):
+    """
+    根据关联的 TransactionRecords 重新计算并更新 TradeRecord。
+    如果计算结果有效,则更新或创建 TradeRecord。
+    如果计算结果无效(例如,没有开仓交易),则删除现有的 TradeRecord。
+    """
+    if trade_id is None:
+        print("  update_trade_record 收到 None trade_id。跳过。")
+        return {"code": 1, "msg": "trade_id 为空"}
+
+    # print(f"正在更新 trade_id: {trade_id} 的 TradeRecord")
+    try:
+        existing_trade = TradeRecord.query.get(trade_id) # 使用 get 获取主键
+
+        transactions = TransactionRecord.query.filter_by(trade_id=trade_id)\
+                                            .order_by(TransactionRecord.transaction_time)\
+                                            .all()
+
+        if not transactions:
+            # print(f"  未找到 trade_id {trade_id} 的交易记录。")
+            if existing_trade:
+                # print(f"  正在删除现有的 TradeRecord {trade_id} (因无交易记录)。")
+                db.session.delete(existing_trade)
+            # else:
+                # print(f"  无需删除,TradeRecord {trade_id} 不存在。")
+            # db.session.commit() # 移除此处的 commit
+            return {"code": 0, "msg": f"已删除无交易记录的 TradeRecord {trade_id}"}
+
+        # 根据交易记录生成理论状态
+        generated_trade_obj = generate_trade_from_transactions(transactions)
+
+        if generated_trade_obj:
+            # print(f"  为 {trade_id} 生成了有效的交易数据。")
+            if existing_trade:
+                # print(f"  正在更新现有的 TradeRecord {trade_id}。")
+                # 从生成的对象更新现有记录的字段
+                existing_trade.roll_trade_main_id = generated_trade_obj.roll_trade_main_id
+                existing_trade.contract_code = generated_trade_obj.contract_code
+                existing_trade.name = generated_trade_obj.name
+                existing_trade.account = generated_trade_obj.account
+                existing_trade.strategy_id = generated_trade_obj.strategy_id
+                existing_trade.strategy_name = generated_trade_obj.strategy_name
+                existing_trade.position_type = generated_trade_obj.position_type
+                existing_trade.candle_pattern_id = generated_trade_obj.candle_pattern_id
+                existing_trade.candle_pattern = generated_trade_obj.candle_pattern
+                existing_trade.open_time = generated_trade_obj.open_time
+                existing_trade.close_time = generated_trade_obj.close_time
+                existing_trade.position_volume = generated_trade_obj.position_volume
+                existing_trade.contract_multiplier = generated_trade_obj.contract_multiplier
+                existing_trade.past_position_cost = generated_trade_obj.past_position_cost
+                existing_trade.average_sale_price = generated_trade_obj.average_sale_price
+                existing_trade.single_profit = generated_trade_obj.single_profit
+                existing_trade.investment_profit = generated_trade_obj.investment_profit
+                existing_trade.investment_profit_rate = generated_trade_obj.investment_profit_rate
+                existing_trade.holding_days = generated_trade_obj.holding_days
+                existing_trade.annual_profit_rate = generated_trade_obj.annual_profit_rate
+                existing_trade.trade_type = generated_trade_obj.trade_type
+                existing_trade.confidence_index = generated_trade_obj.confidence_index
+                existing_trade.similarity_evaluation = generated_trade_obj.similarity_evaluation
+                existing_trade.long_trend_ids = generated_trade_obj.long_trend_ids
+                existing_trade.long_trend_name = generated_trade_obj.long_trend_name
+                existing_trade.mid_trend_ids = generated_trade_obj.mid_trend_ids
+                existing_trade.mid_trend_name = generated_trade_obj.mid_trend_name
+            else:
+                # print(f"  正在为 trade_id {trade_id} 创建新的 TradeRecord。")
+                generated_trade_obj.id = trade_id # 显式设置 ID
+                db.session.add(generated_trade_obj)
+        else:
+            # 无法从交易记录生成有效的交易
+            # print(f"  为 {trade_id} 生成了无效/不完整的交易数据。")
+            if existing_trade:
+                # print(f"  正在删除现有的 TradeRecord {trade_id} (因数据无效/不完整)。")
+                db.session.delete(existing_trade)
+            # else:
+                # print(f"  无需删除,TradeRecord {trade_id} 不存在。")
+
+        # db.session.commit() # 移除此处的 commit
+        # print(f"  成功提交 TradeRecord {trade_id} 的更改。")
+        return {"code": 0, "msg": f"成功更新 TradeRecord {trade_id}"}
+
+    except Exception as e:
+        db.session.rollback()
+        print(f"  处理 TradeRecord {trade_id} 时出错: {e}")
+        print(traceback.format_exc())
+        return {"code": 1, "msg": f"处理 TradeRecord {trade_id} 时出错: {str(e)}"}
+
+def sync_trades_after_import(trade_ids):
+    """
+    为给定的 trade_id 列表同步 TradeRecords。
+    为每个唯一的 trade_id 调用 update_trade_record。
+    """
+    if not trade_ids:
+        print("未提供用于同步的 trade ID。")
+        return
+
+    valid_trade_ids = set()
+    for tid in trade_ids:
+        if tid is not None:
+            try:
+                valid_trade_ids.add(int(tid))
+            except (ValueError, TypeError):
+                 print(f"  跳过无效的 trade_id: {tid}")
+
+    if not valid_trade_ids:
+        print("过滤后未找到有效的 trade ID。")
+        return
+
+    print(f"正在为 {len(valid_trade_ids)} 个唯一的 trade ID 同步 TradeRecords...")
+    errors = []
+    success_count = 0
+    for trade_id in valid_trade_ids:
+        try:
+            update_trade_record(trade_id)
+            success_count += 1
+        except Exception as e:
+            error_msg = f"同步 trade_id {trade_id} 时发生严重错误: {e}"
+            print(f"  {error_msg}")
+            print(traceback.format_exc())
+            errors.append(error_msg)
+
+    if not errors:
+        try:
+            db.session.commit()
+            print("  成功提交所有数据库更改。")
+        except Exception as e:
+            db.session.rollback()
+            commit_error_msg = f"提交数据库事务时发生严重错误: {e}"
+            print(f"  {commit_error_msg}")
+            print(traceback.format_exc())
+            errors.append(commit_error_msg)
+    else:
+        db.session.rollback()
+        print("  检测到错误,正在回滚数据库更改。")
+
+    sync_status = "同步完成。"
+    if errors:
+        sync_status = f"同步完成,但有 {len(errors)} 个错误。"
+        print(f"同步期间的错误: {errors}")
+
+    print(sync_status)
+    # 返回同步结果
+    return {'code': 1 if errors else 0, 'msg': sync_status, 'errors': errors, 'success_count': success_count}
+
+def sync_all_trades_from_transactions():
+    """
+    从所有 TransactionRecords 中同步 TradeRecords,并清理孤立的 TradeRecords。
+    """
+    print("开始从所有交易记录中全面同步交易汇总...")
+    try:
+        # 从 TransactionRecord 获取所有非空的、唯一的 trade_id
+        transaction_trade_ids = {item[0] for item in db.session.query(TransactionRecord.trade_id).distinct() if item[0] is not None}
+        print(f"  从交易记录中找到 {len(transaction_trade_ids)} 个唯一的 trade ID。")
+
+        # 从 TradeRecord 获取所有 ID
+        trade_record_ids = {item[0] for item in db.session.query(TradeRecord.id).distinct() if item[0] is not None}
+        print(f"  从交易汇总表中找到 {len(trade_record_ids)} 个唯一的 ID。")
+
+        # 合并所有需要检查的 ID
+        all_ids_to_sync = transaction_trade_ids.union(trade_record_ids)
+
+        if not all_ids_to_sync:
+            print("  数据库中没有任何交易或交易汇总记录可供同步。")
+            return {'code': 0, 'msg': '没有需要同步的交易。', 'errors': [], 'success_count': 0}
+
+        print(f"  共计需要同步 {len(all_ids_to_sync)} 个唯一的 ID。")
+
+        # 使用现有的同步逻辑
+        result = sync_trades_after_import(list(all_ids_to_sync))
+
+        print("全面同步完成。")
+        return result
+
+    except Exception as e:
+        db.session.rollback()
+        error_msg = f"全面同步期间发生严重错误: {e}"
+        print(f"  {error_msg}")
+        print(traceback.format_exc())
+        return {'code': 1, 'msg': error_msg, 'errors': [error_msg], 'success_count': 0} 

+ 79 - 0
app/templates/base.html

@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{% block title %}期货数据管理系统{% endblock %}</title>
+    <!-- Bootstrap CSS -->
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
+    <!-- Font Awesome -->
+    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet">
+    <!-- Custom CSS -->
+    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
+    <style>
+        /* 固定表头 */
+        .table thead th {
+            position: -webkit-sticky; /* for Safari */
+            position: sticky;
+            top: 0; /* 或者根据你的导航栏高度调整 */
+            background-color: #f8f9fa; /* 表头背景色,避免下方内容透上来 */
+            z-index: 1020; /* 确保表头在其他元素之上,小于导航栏 */
+        }
+        /* 解决边框问题 */
+        .table-responsive {
+            overflow: visible; /* 允许粘性定位生效 */
+        }
+    </style>
+    {% block styles %}{% endblock %}
+</head>
+<body>
+    <!-- 导航栏 -->
+    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
+        <div class="container">
+            <a class="navbar-brand" href="{{ url_for('index') }}">期货数据管理系统</a>
+            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
+                <span class="navbar-toggler-icon"></span>
+            </button>
+            <div class="collapse navbar-collapse" id="navbarNav">
+                <ul class="navbar-nav mr-auto">
+                    <li class="nav-item">
+                        <a class="nav-link" href="{{ url_for('future_info.index') }}">期货基础信息</a>
+                    </li>
+                    <li class="nav-item">
+                        <a class="nav-link" href="{{ url_for('transaction.index') }}">交易记录</a>
+                    </li>
+                    <li class="nav-item">
+                        <a class="nav-link" href="{{ url_for('trade.index') }}">交易汇总</a>
+                    </li>
+                    <li class="nav-item">
+                        <a class="nav-link" href="{{ url_for('monitor.index') }}">标的监控</a>
+                    </li>
+                </ul>
+            </div>
+        </div>
+    </nav>
+
+    <!-- 主内容区 -->
+    <div class="container mt-4">
+        {% block content %}{% endblock %}
+    </div>
+
+    <!-- 页脚 -->
+    <footer class="footer mt-5 py-3 bg-light">
+        <div class="container">
+            <span class="text-muted">© 2023 期货数据管理系统</span>
+        </div>
+    </footer>
+
+    <!-- Toast容器 -->
+    <div id="toast-container" class="toast-container position-fixed bottom-0 end-0 p-3"></div>
+
+    <!-- jQuery -->
+    <script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
+
+    <!-- Bootstrap Bundle with Popper -->
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
+    <script src="{{ url_for('static', filename='js/main.js') }}"></script>
+    {% block scripts %}{% endblock %}
+</body>
+</html> 

+ 395 - 0
app/templates/future_info/add.html

@@ -0,0 +1,395 @@
+{% extends 'base.html' %}
+
+{% block title %}添加期货信息 - 期货数据管理系统{% endblock %}
+
+{% block content %}
+<div class="d-flex justify-content-between align-items-center mb-4">
+    <h2>添加期货信息</h2>
+    <a href="{{ url_for('future_info.index') }}" class="btn btn-secondary">返回列表</a>
+</div>
+
+<div class="card">
+    <div class="card-body">
+        <form id="future-info-form">
+            <div class="row">
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="contract_letter">合约代码</label>
+                        <input type="text" class="form-control" id="contract_letter" name="contract_letter" required>
+                    </div>
+                </div>
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="name">期货名称</label>
+                        <input type="text" class="form-control" id="name" name="name" required>
+                    </div>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="market">市场</label>
+                        <input type="text" class="form-control" id="market" name="market">
+                    </div>
+                </div>
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="exchange">交易所</label>
+                        <input type="text" class="form-control" id="exchange" name="exchange">
+                    </div>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="contract_multiplier">合约乘数</label>
+                        <input type="number" class="form-control" id="contract_multiplier" name="contract_multiplier">
+                    </div>
+                </div>
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="long_margin_rate">做多保证金率</label>
+                        <input type="number" step="0.01" class="form-control" id="long_margin_rate" name="long_margin_rate">
+                    </div>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="short_margin_rate">做空保证金率</label>
+                        <input type="number" step="0.01" class="form-control" id="short_margin_rate" name="short_margin_rate">
+                    </div>
+                </div>
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="open_fee">开仓费用(按手)</label>
+                        <input type="number" step="0.01" class="form-control" id="open_fee" name="open_fee">
+                    </div>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="close_fee">平仓费用(按手)</label>
+                        <input type="number" step="0.01" class="form-control" id="close_fee" name="close_fee">
+                    </div>
+                </div>
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="close_today_rate">平今费用(按金额)</label>
+                        <input type="number" step="0.01" class="form-control" id="close_today_rate" name="close_today_rate">
+                    </div>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="close_today_fee">平今费用(按手)</label>
+                        <input type="number" step="0.01" class="form-control" id="close_today_fee" name="close_today_fee">
+                    </div>
+                </div>
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="th_main_contract">主力合约</label>
+                        <input type="text" class="form-control" id="th_main_contract" name="th_main_contract">
+                    </div>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="current_main_contract">当前主力合约</label>
+                        <input type="text" class="form-control" id="current_main_contract" name="current_main_contract">
+                    </div>
+                </div>
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="th_order">排序</label>
+                        <input type="number" class="form-control" id="th_order" name="th_order">
+                    </div>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="long_margin_amount">做多1手保证金</label>
+                        <input type="number" step="0.01" class="form-control" id="long_margin_amount" name="long_margin_amount">
+                    </div>
+                </div>
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="short_margin_amount">做空1手保证金</label>
+                        <input type="number" step="0.01" class="form-control" id="short_margin_amount" name="short_margin_amount">
+                    </div>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="core_ratio">核心比率</label>
+                        <input type="number" step="0.01" min="0" max="1" class="form-control" id="core_ratio" name="core_ratio">
+                    </div>
+                </div>
+                <div class="col-md-6">
+                    <!-- 空列,保持布局平衡 -->
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-12">
+                    <div class="form-group">
+                        <label for="long_term_trend" class="form-label">长期趋势特征</label>
+                        
+                        <!-- 隐藏的输入框,用于存储最终的数据 -->
+                        <input type="hidden" id="long_term_trend" name="long_term_trend">
+                        
+                        <!-- 趋势特征搜索和选择区域 -->
+                        <div class="trend-selection-area">
+                            <!-- 已选择的趋势特征标签 -->
+                            <div id="selected_trends" class="selected-trends mb-2">
+                                <!-- 已选择的趋势特征标签将在这里显示 -->
+                            </div>
+                            
+                            <!-- 趋势特征搜索输入框 -->
+                            <div class="position-relative">
+                                <input type="text" class="form-control" id="trend_search_input" 
+                                       placeholder="搜索并添加趋势特征..." autocomplete="off">
+                                <div id="trend_search_results" class="dropdown-menu w-100" style="display: none; max-height: 200px; overflow-y: auto;">
+                                    <!-- 搜索结果将在这里显示 -->
+                                </div>
+                            </div>
+                        </div>
+                        
+                        <div id="trend_validation_feedback" class="invalid-feedback" style="display:none;"></div>
+                        <div class="form-text">搜索并选择趋势特征,支持添加多个</div>
+                    </div>
+                </div>
+            </div>
+            <div class="form-group mt-4">
+                <button type="submit" class="btn btn-primary">保存</button>
+                <button type="reset" class="btn btn-secondary">重置</button>
+            </div>
+        </form>
+    </div>
+</div>
+
+<script>
+document.addEventListener('DOMContentLoaded', function() {
+    const form = document.getElementById('future-info-form');
+    const longTermTrendInput = document.getElementById('long_term_trend');
+    const trendSearchInput = document.getElementById('trend_search_input');
+    const trendSearchResults = document.getElementById('trend_search_results');
+    const selectedTrendsContainer = document.getElementById('selected_trends');
+    
+    let allTrends = []; // 存储所有趋势特征数据
+    let selectedTrends = []; // 存储已选择的趋势特征
+    let searchTimeout;
+    
+    // 加载所有趋势特征列表
+    function loadTrendOptions() {
+        fetch('/api/future_info/trends')
+            .then(response => response.json())
+            .then(data => {
+                if (data.code === 0) {
+                    allTrends = data.data;
+                    console.log('加载趋势特征列表:', allTrends.length, '个');
+                }
+            })
+            .catch(error => {
+                console.error('获取趋势特征失败:', error);
+            });
+    }
+
+    // 初始化页面时加载趋势特征列表
+    loadTrendOptions();
+    
+    // 搜索趋势特征
+    function searchTrends(query) {
+        if (!query.trim()) {
+            hideTrendSearchResults();
+            return;
+        }
+
+        // 过滤已选择的趋势特征
+        const selectedTrendNames = selectedTrends.map(trend => trend.name);
+        const filteredTrends = allTrends.filter(trend => 
+            trend.name.toLowerCase().includes(query.toLowerCase()) &&
+            !selectedTrendNames.includes(trend.name)
+        );
+
+        showTrendSearchResults(filteredTrends);
+    }
+
+    // 显示搜索结果
+    function showTrendSearchResults(results) {
+        trendSearchResults.innerHTML = '';
+        
+        if (results.length === 0) {
+            trendSearchResults.innerHTML = '<div class="dropdown-item-text text-muted">未找到匹配的趋势特征</div>';
+        } else {
+            results.slice(0, 10).forEach(trend => { // 限制显示前10个结果
+                const item = document.createElement('div');
+                item.className = 'dropdown-item cursor-pointer';
+                item.style.cursor = 'pointer';
+                item.textContent = trend.name;
+                item.addEventListener('click', () => selectTrend(trend));
+                trendSearchResults.appendChild(item);
+            });
+        }
+        
+        trendSearchResults.style.display = 'block';
+    }
+
+    // 隐藏搜索结果
+    function hideTrendSearchResults() {
+        trendSearchResults.style.display = 'none';
+    }
+
+    // 选择趋势特征
+    function selectTrend(trend) {
+        // 检查是否已经选择
+        if (selectedTrends.find(t => t.name === trend.name)) {
+            return;
+        }
+
+        selectedTrends.push(trend);
+        updateSelectedTrendsDisplay();
+        updateHiddenInput();
+        
+        // 清空搜索框
+        trendSearchInput.value = '';
+        hideTrendSearchResults();
+        
+        console.log('选择了趋势特征:', trend.name);
+    }
+
+    // 删除趋势特征
+    function removeTrend(index) {
+        selectedTrends.splice(index, 1);
+        updateSelectedTrendsDisplay();
+        updateHiddenInput();
+        
+        console.log('删除趋势特征,剩余:', selectedTrends.length, '个');
+    }
+
+    // 更新已选择趋势特征的显示
+    function updateSelectedTrendsDisplay() {
+        selectedTrendsContainer.innerHTML = '';
+        
+        selectedTrends.forEach((trend, index) => {
+            const badge = document.createElement('span');
+            badge.className = 'badge bg-primary me-2 mb-2 d-inline-flex align-items-center';
+            badge.innerHTML = `
+                ${trend.name}
+                <button type="button" class="btn-close btn-close-white ms-2" style="font-size: 0.7em;" aria-label="Remove"></button>
+            `;
+            
+            // 添加删除事件
+            const closeBtn = badge.querySelector('.btn-close');
+            closeBtn.addEventListener('click', () => removeTrend(index));
+            
+            selectedTrendsContainer.appendChild(badge);
+        });
+    }
+
+    // 更新隐藏输入框的值
+    function updateHiddenInput() {
+        const trendNames = selectedTrends.map(trend => trend.name);
+        longTermTrendInput.value = trendNames.join('+');
+        
+        console.log('更新隐藏输入框值:', longTermTrendInput.value);
+    }
+
+    // 监听趋势特征搜索输入
+    trendSearchInput.addEventListener('input', function() {
+        const query = this.value.trim();
+        
+        // 使用防抖进行搜索
+        clearTimeout(searchTimeout);
+        searchTimeout = setTimeout(() => {
+            searchTrends(query);
+        }, 300);
+    });
+
+    // 监听焦点离开事件,隐藏搜索结果
+    trendSearchInput.addEventListener('blur', function() {
+        // 延迟隐藏,允许用户点击搜索结果
+        setTimeout(() => {
+            hideTrendSearchResults();
+        }, 200);
+    });
+
+    // 监听点击外部区域隐藏搜索结果
+    document.addEventListener('click', function(event) {
+        if (!event.target.closest('.trend-selection-area')) {
+            hideTrendSearchResults();
+        }
+    });
+    
+    // 表单提交处理
+    form.addEventListener('submit', function(event) {
+        event.preventDefault();
+        
+        if (!form.checkValidity()) {
+            event.stopPropagation();
+            form.classList.add('was-validated');
+            return;
+        }
+
+        // 收集表单数据
+        const formData = {
+            contract_letter: document.getElementById('contract_letter').value,
+            name: document.getElementById('name').value,
+            market: parseInt(document.getElementById('market').value),
+            exchange: document.getElementById('exchange').value,
+            contract_multiplier: document.getElementById('contract_multiplier').value ? Number(document.getElementById('contract_multiplier').value) : null,
+            long_margin_rate: document.getElementById('long_margin_rate').value !== '' ? Number(document.getElementById('long_margin_rate').value) : null,
+            short_margin_rate: document.getElementById('short_margin_rate').value !== '' ? Number(document.getElementById('short_margin_rate').value) : null,
+            open_fee: document.getElementById('open_fee').value !== '' ? Number(document.getElementById('open_fee').value) : null,
+            close_fee: document.getElementById('close_fee').value !== '' ? Number(document.getElementById('close_fee').value) : null,
+            close_today_rate: document.getElementById('close_today_rate').value !== '' ? Number(document.getElementById('close_today_rate').value) : null,
+            close_today_fee: document.getElementById('close_today_fee').value !== '' ? Number(document.getElementById('close_today_fee').value) : null,
+            long_margin_amount: document.getElementById('long_margin_amount').value !== '' ? Number(document.getElementById('long_margin_amount').value) : null,
+            short_margin_amount: document.getElementById('short_margin_amount').value !== '' ? Number(document.getElementById('short_margin_amount').value) : null,
+            th_main_contract: document.getElementById('th_main_contract').value,
+            current_main_contract: document.getElementById('current_main_contract').value,
+            th_order: document.getElementById('th_order').value ? parseInt(document.getElementById('th_order').value) : null,
+            long_term_trend: document.getElementById('long_term_trend').value,
+            core_ratio: document.getElementById('core_ratio').value ? Number(document.getElementById('core_ratio').value) : null
+        };
+
+        console.log('准备提交的数据:', formData);
+
+        // 发送添加请求
+        fetch('/api/future_info/add', {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify(formData)
+        })
+        .then(response => response.json())
+        .then(data => {
+            if (data.code === 0) {
+                alert('添加成功');
+                window.location.href = "{{ url_for('future_info.index') }}";
+            } else {
+                alert('添加失败:' + data.msg);
+            }
+        })
+        .catch(error => {
+            console.error('Error:', error);
+            alert('添加失败');
+        });
+    });
+
+    // 重置表单时重置趋势特征选择
+    form.addEventListener('reset', function() {
+        selectedTrends = [];
+        updateSelectedTrendsDisplay();
+        updateHiddenInput();
+        trendSearchInput.value = '';
+        hideTrendSearchResults();
+    });
+});
+</script>
+{% endblock %} 

+ 181 - 0
app/templates/future_info/detail.html

@@ -0,0 +1,181 @@
+{% extends 'base.html' %}
+
+{% block title %}期货信息详情 - 期货数据管理系统{% endblock %}
+
+{% block content %}
+<div class="container-fluid">
+    <div class="row mb-3">
+        <div class="col">
+            <h2>期货详情 - {{ future.name }}</h2>
+        </div>
+        <div class="col-auto">
+            <a href="{{ url_for('future_info.index') }}" class="btn btn-secondary">
+                <i class="fas fa-arrow-left"></i> 返回列表
+            </a>
+            <a href="{{ url_for('future_info.edit', id=future.id) }}" class="btn btn-warning">
+                <i class="fas fa-edit"></i> 编辑
+            </a>
+        </div>
+    </div>
+
+    <div class="row">
+        <div class="col-md-6">
+            <div class="card mb-3">
+                <div class="card-header">
+                    <h5 class="card-title mb-0">基本信息</h5>
+                </div>
+                <div class="card-body">
+                    <table class="table table-borderless">
+                        <tbody>
+                            <tr>
+                                <th style="width: 200px">合约字母</th>
+                                <td>{{ future.contract_letter }}</td>
+                            </tr>
+                            <tr>
+                                <th>期货名称</th>
+                                <td>{{ future.name }}</td>
+                            </tr>
+                            <tr>
+                                <th>市场</th>
+                                <td>{{ '国内' if future.market == 0 else '国外' }}</td>
+                            </tr>
+                            <tr>
+                                <th>交易所</th>
+                                <td>{{ future.exchange or '未设置' }}</td>
+                            </tr>
+                            <tr>
+                                <th>合约乘数</th>
+                                <td>{{ future.contract_multiplier or '未设置' }}</td>
+                            </tr>
+                            <tr>
+                                <th>同花主力合约</th>
+                                <td>{{ future.th_main_contract or '未设置' }}</td>
+                            </tr>
+                            <tr>
+                                <th>当前主力合约</th>
+                                <td>{{ future.current_main_contract or '未设置' }}</td>
+                            </tr>
+                            <tr>
+                                <th>同花顺顺序</th>
+                                <td>{{ future.th_order or '未设置' }}</td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+        </div>
+
+        <div class="col-md-6">
+            <div class="card mb-3">
+                <div class="card-header">
+                    <h5 class="card-title mb-0">交易参数</h5>
+                </div>
+                <div class="card-body">
+                    <table class="table table-borderless">
+                        <tbody>
+                            <tr>
+                                <th style="width: 200px">做多保证金率</th>
+                                <td>{{ '{:.2%}'.format(future.long_margin_rate) if future.long_margin_rate is not none else '未设置' }}</td>
+                            </tr>
+                            <tr>
+                                <th>做空保证金率</th>
+                                <td>{{ '{:.2%}'.format(future.short_margin_rate) if future.short_margin_rate is not none else '未设置' }}</td>
+                            </tr>
+                            <tr>
+                                <th>开仓费用(按手)</th>
+                                <td>{{ future.open_fee if future.open_fee is not none else '未设置' }}</td>
+                            </tr>
+                            <tr>
+                                <th>平仓费用(按手)</th>
+                                <td>{{ future.close_fee if future.close_fee is not none else '未设置' }}</td>
+                            </tr>
+                            <tr>
+                                <th>平今费率(按金额)</th>
+                                <td>{{ '{:.4%}'.format(future.close_today_rate) if future.close_today_rate is not none else '未设置' }}</td>
+                            </tr>
+                            <tr>
+                                <th>平今费用(按手)</th>
+                                <td>{{ future.close_today_fee if future.close_today_fee is not none else '未设置' }}</td>
+                            </tr>
+                            <tr>
+                                <th>做多1手保证金</th>
+                                <td>{{ future.long_margin_amount if future.long_margin_amount is not none else '未设置' }}</td>
+                            </tr>
+                            <tr>
+                                <th>做空1手保证金</th>
+                                <td>{{ future.short_margin_amount if future.short_margin_amount is not none else '未设置' }}</td>
+                            </tr>
+                            <tr>
+                                <th>核心比率</th>
+                                <td>{{ future.core_ratio or '未设置' }}</td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+
+            <div class="card">
+                <div class="card-header">
+                    <h5 class="card-title mb-0">趋势信息</h5>
+                </div>
+                <div class="card-body">
+                    <div class="form-group">
+                        <label>长期趋势特征</label>
+                        <p class="form-control-plaintext">{{ future.long_term_trend or '未设置' }}</p>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    {% if daily_data %}
+    <div class="row mt-3">
+        <div class="col-12">
+            <div class="card">
+                <div class="card-header">
+                    <h5 class="card-title mb-0">最新每日数据</h5>
+                </div>
+                <div class="card-body">
+                    <div class="table-responsive">
+                        <table class="table table-striped">
+                            <thead>
+                                <tr>
+                                    <th>合约代码</th>
+                                    <th>最新价格</th>
+                                    <th>成交量</th>
+                                    <th>持仓量</th>
+                                    <th>是否主力</th>
+                                    <th>更新时间</th>
+                                </tr>
+                            </thead>
+                            <tbody>
+                                {% for daily in daily_data %}
+                                <tr>
+                                    <td>{{ daily.contract_code }}</td>
+                                    <td>{{ daily.latest_price }}</td>
+                                    <td>{{ daily.volume }}</td>
+                                    <td>{{ daily.open_interest }}</td>
+                                    <td>
+                                        {% if daily.is_main_contract %}
+                                        <span class="badge bg-success">是</span>
+                                        {% else %}
+                                        <span class="badge bg-secondary">否</span>
+                                        {% endif %}
+                                    </td>
+                                    <td>{{ daily.update_time.strftime('%Y-%m-%d %H:%M:%S') }}</td>
+                                </tr>
+                                {% else %}
+                                <tr>
+                                    <td colspan="6" class="text-center">暂无每日数据</td>
+                                </tr>
+                                {% endfor %}
+                            </tbody>
+                        </table>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    {% endif %}
+</div>
+{% endblock %} 

+ 473 - 0
app/templates/future_info/edit.html

@@ -0,0 +1,473 @@
+{% extends 'base.html' %}
+
+{% block title %}编辑期货信息 - 期货数据管理系统{% endblock %}
+
+{% block content %}
+<div class="container-fluid">
+    <div class="row mb-3">
+        <div class="col">
+            <h2>编辑期货信息</h2>
+        </div>
+        <div class="col-auto">
+            <a href="{{ url_for('future_info.index') }}" class="btn btn-secondary">
+                <i class="fas fa-arrow-left"></i> 返回列表
+            </a>
+        </div>
+    </div>
+
+    <div class="card">
+        <div class="card-body">
+            <form id="editForm" class="needs-validation" novalidate>
+                <div class="row">
+                    <!-- 基本信息 -->
+                    <div class="col-md-6">
+                        <h5 class="mb-3">基本信息</h5>
+                        
+                        <div class="mb-3">
+                            <label for="contract_letter" class="form-label">合约字母 <span class="text-danger">*</span></label>
+                            <input type="text" class="form-control" id="contract_letter" name="contract_letter" required>
+                            <div class="invalid-feedback">请输入合约字母</div>
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="name" class="form-label">期货名称 <span class="text-danger">*</span></label>
+                            <input type="text" class="form-control" id="name" name="name" required>
+                            <div class="invalid-feedback">请输入期货名称</div>
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="market" class="form-label">市场 <span class="text-danger">*</span></label>
+                            <select class="form-select" id="market" name="market" required>
+                                <option value="0">国内</option>
+                                <option value="1">国外</option>
+                            </select>
+                            <div class="invalid-feedback">请选择市场</div>
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="exchange" class="form-label">交易所</label>
+                            <input type="text" class="form-control" id="exchange" name="exchange">
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="contract_multiplier" class="form-label">合约乘数</label>
+                            <input type="number" class="form-control" id="contract_multiplier" name="contract_multiplier" step="1">
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="th_main_contract" class="form-label">同花主力合约</label>
+                            <input type="text" class="form-control" id="th_main_contract" name="th_main_contract">
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="current_main_contract" class="form-label">当前主力合约</label>
+                            <input type="text" class="form-control" id="current_main_contract" name="current_main_contract">
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="th_order" class="form-label">同花顺顺序</label>
+                            <input type="number" class="form-control" id="th_order" name="th_order" step="1">
+                        </div>
+                    </div>
+
+                    <!-- 交易参数和趋势信息 -->
+                    <div class="col-md-6">
+                        <h5 class="mb-3">交易参数</h5>
+
+                        <div class="mb-3">
+                            <label for="long_margin_rate" class="form-label">做多保证金率</label>
+                            <div class="input-group">
+                                <input type="number" class="form-control" id="long_margin_rate" name="long_margin_rate" step="0.01" min="0" max="1">
+                                <span class="input-group-text">%</span>
+                            </div>
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="short_margin_rate" class="form-label">做空保证金率</label>
+                            <div class="input-group">
+                                <input type="number" class="form-control" id="short_margin_rate" name="short_margin_rate" step="0.01" min="0" max="1">
+                                <span class="input-group-text">%</span>
+                            </div>
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="open_fee" class="form-label">开仓费用(按手)</label>
+                            <input type="number" class="form-control" id="open_fee" name="open_fee" step="0.01">
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="close_fee" class="form-label">平仓费用(按手)</label>
+                            <input type="number" class="form-control" id="close_fee" name="close_fee" step="0.01">
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="close_today_rate" class="form-label">平今费率(按金额)</label>
+                            <div class="input-group">
+                                <input type="number" class="form-control" id="close_today_rate" name="close_today_rate" step="0.000001" min="0" max="1">
+                                <span class="input-group-text">%</span>
+                            </div>
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="close_today_fee" class="form-label">平今费用(按手)</label>
+                            <input type="number" class="form-control" id="close_today_fee" name="close_today_fee" step="0.01">
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="long_margin_amount" class="form-label">做多1手保证金</label>
+                            <input type="number" class="form-control" id="long_margin_amount" name="long_margin_amount" step="0.01">
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="short_margin_amount" class="form-label">做空1手保证金</label>
+                            <input type="number" class="form-control" id="short_margin_amount" name="short_margin_amount" step="0.01">
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="core_ratio" class="form-label">核心比率</label>
+                            <input type="number" class="form-control" id="core_ratio" name="core_ratio" step="0.01" min="0" max="1">
+                        </div>
+
+                        <h5 class="mb-3 mt-4">趋势信息</h5>
+                        <div class="mb-3">
+                            <label for="long_term_trend" class="form-label">长期趋势特征</label>
+                            
+                            <!-- 隐藏的输入框,用于存储最终的数据 -->
+                            <input type="hidden" id="long_term_trend" name="long_term_trend">
+                            
+                            <!-- 趋势特征搜索和选择区域 -->
+                            <div class="trend-selection-area">
+                                <!-- 已选择的趋势特征标签 -->
+                                <div id="selected_trends" class="selected-trends mb-2">
+                                    <!-- 已选择的趋势特征标签将在这里显示 -->
+                                </div>
+                                
+                                <!-- 趋势特征搜索输入框 -->
+                                <div class="position-relative">
+                                    <input type="text" class="form-control" id="trend_search_input" 
+                                           placeholder="搜索并添加趋势特征..." autocomplete="off">
+                                    <div id="trend_search_results" class="dropdown-menu w-100" style="display: none; max-height: 200px; overflow-y: auto;">
+                                        <!-- 搜索结果将在这里显示 -->
+                                    </div>
+                                </div>
+                            </div>
+                            
+                            <div class="form-text mt-2">搜索并选择趋势特征,支持添加多个</div>
+                            <div id="trend_validation_feedback" class="invalid-feedback"></div>
+                        </div>
+                    </div>
+                </div>
+
+                <div class="row mt-4">
+                    <div class="col">
+                        <button type="submit" class="btn btn-primary">保存更改</button>
+                        <button type="button" class="btn btn-secondary" onclick="window.history.back()">取消</button>
+                    </div>
+                </div>
+            </form>
+        </div>
+    </div>
+</div>
+{% endblock %}
+
+{% block scripts %}
+<script>
+document.addEventListener('DOMContentLoaded', function() {
+    // 从Flask传递的ID,转换为数字
+    const futureId = parseInt("{{ future_id }}");
+    const form = document.getElementById('editForm');
+    const longTermTrendInput = document.getElementById('long_term_trend');
+    const trendSearchInput = document.getElementById('trend_search_input');
+    const trendSearchResults = document.getElementById('trend_search_results');
+    const selectedTrendsContainer = document.getElementById('selected_trends');
+    
+    let allTrends = []; // 存储所有趋势特征数据
+    let selectedTrends = []; // 存储已选择的趋势特征
+    let searchTimeout;
+
+    // 加载趋势信息数据
+    fetch('/api/future_info/trends')
+        .then(response => response.json())
+        .then(data => {
+            if (data.code === 0) {
+                allTrends = data.data;
+                console.log('加载趋势特征列表:', allTrends.length, '个');
+                
+                // 加载期货信息
+                loadFutureInfo();
+            }
+        })
+        .catch(error => {
+            console.error('加载趋势信息失败:', error);
+            // 即使趋势信息加载失败,也加载期货信息
+            loadFutureInfo();
+        });
+
+    // 加载期货信息
+    function loadFutureInfo() {
+        fetch(`/api/future_info/get/${futureId}`)
+            .then(response => response.json())
+            .then(data => {
+                if (data.code === 0 && data.data) {
+                    const future = data.data;
+                    console.log('从服务器获取的数据:', future);
+                    
+                    // 填充表单数据
+                    document.getElementById('contract_letter').value = future.contract_letter || '';
+                    document.getElementById('name').value = future.name || '';
+                    document.getElementById('market').value = future.market;
+                    document.getElementById('exchange').value = future.exchange || '';
+                    document.getElementById('contract_multiplier').value = future.contract_multiplier || '';
+                    document.getElementById('long_margin_rate').value = future.long_margin_rate || '';
+                    document.getElementById('short_margin_rate').value = future.short_margin_rate || '';
+                    document.getElementById('open_fee').value = future.open_fee || '';
+                    document.getElementById('close_fee').value = future.close_fee || '';
+                    
+                    // 特别处理平今费率
+                    const closeTodayRateInput = document.getElementById('close_today_rate');
+                    closeTodayRateInput.value = future.close_today_rate || '';
+                    console.log('平今费率值:', {
+                        original: future.close_today_rate,
+                        formatted: closeTodayRateInput.value
+                    });
+                    
+                    document.getElementById('close_today_fee').value = future.close_today_fee || '';
+                    document.getElementById('long_margin_amount').value = future.long_margin_amount || '';
+                    document.getElementById('short_margin_amount').value = future.short_margin_amount || '';
+                    document.getElementById('th_main_contract').value = future.th_main_contract || '';
+                    document.getElementById('current_main_contract').value = future.current_main_contract || '';
+                    document.getElementById('th_order').value = future.th_order || '';
+                    document.getElementById('core_ratio').value = future.core_ratio || '';
+                    
+                    // 设置长期趋势特征
+                    const trendValue = future.long_term_trend || '';
+                    longTermTrendInput.value = trendValue;
+                    
+                    // 解析已有的趋势特征并设置到选择组件中
+                    if (trendValue.trim()) {
+                        const trendNames = trendValue.split('+').map(name => name.trim()).filter(name => name);
+                        selectedTrends = [];
+                        
+                        trendNames.forEach(trendName => {
+                            const trend = allTrends.find(t => t.name === trendName);
+                            if (trend) {
+                                selectedTrends.push(trend);
+                            }
+                        });
+                        
+                        updateSelectedTrendsDisplay();
+                    }
+                } else {
+                    alert('加载期货信息失败:未找到对应的期货信息');
+                }
+            })
+            .catch(error => {
+                console.error('Error:', error);
+                alert('加载期货信息失败');
+            });
+    }
+
+    // 监听平今费率输入变化
+    document.getElementById('close_today_rate').addEventListener('input', function(e) {
+        console.log('平今费率输入值变化:', {
+            value: e.target.value,
+            valueAsNumber: e.target.valueAsNumber
+        });
+    });
+
+    // 趋势特征搜索和选择功能
+    
+    // 搜索趋势特征
+    function searchTrends(query) {
+        if (!query.trim()) {
+            hideTrendSearchResults();
+            return;
+        }
+
+        // 过滤已选择的趋势特征
+        const selectedTrendNames = selectedTrends.map(trend => trend.name);
+        const filteredTrends = allTrends.filter(trend => 
+            trend.name.toLowerCase().includes(query.toLowerCase()) &&
+            !selectedTrendNames.includes(trend.name)
+        );
+
+        showTrendSearchResults(filteredTrends);
+    }
+
+    // 显示搜索结果
+    function showTrendSearchResults(results) {
+        trendSearchResults.innerHTML = '';
+        
+        if (results.length === 0) {
+            trendSearchResults.innerHTML = '<div class="dropdown-item-text text-muted">未找到匹配的趋势特征</div>';
+        } else {
+            results.slice(0, 10).forEach(trend => { // 限制显示前10个结果
+                const item = document.createElement('div');
+                item.className = 'dropdown-item cursor-pointer';
+                item.style.cursor = 'pointer';
+                item.textContent = trend.name;
+                item.addEventListener('click', () => selectTrend(trend));
+                trendSearchResults.appendChild(item);
+            });
+        }
+        
+        trendSearchResults.style.display = 'block';
+    }
+
+    // 隐藏搜索结果
+    function hideTrendSearchResults() {
+        trendSearchResults.style.display = 'none';
+    }
+
+    // 选择趋势特征
+    function selectTrend(trend) {
+        // 检查是否已经选择
+        if (selectedTrends.find(t => t.name === trend.name)) {
+            return;
+        }
+
+        selectedTrends.push(trend);
+        updateSelectedTrendsDisplay();
+        updateHiddenInput();
+        
+        // 清空搜索框
+        trendSearchInput.value = '';
+        hideTrendSearchResults();
+        
+        console.log('选择了趋势特征:', trend.name);
+    }
+
+    // 删除趋势特征
+    function removeTrend(index) {
+        selectedTrends.splice(index, 1);
+        updateSelectedTrendsDisplay();
+        updateHiddenInput();
+        
+        console.log('删除趋势特征,剩余:', selectedTrends.length, '个');
+    }
+
+    // 更新已选择趋势特征的显示
+    function updateSelectedTrendsDisplay() {
+        selectedTrendsContainer.innerHTML = '';
+        
+        selectedTrends.forEach((trend, index) => {
+            const badge = document.createElement('span');
+            badge.className = 'badge bg-primary me-2 mb-2 d-inline-flex align-items-center';
+            badge.innerHTML = `
+                ${trend.name}
+                <button type="button" class="btn-close btn-close-white ms-2" style="font-size: 0.7em;" aria-label="Remove"></button>
+            `;
+            
+            // 添加删除事件
+            const closeBtn = badge.querySelector('.btn-close');
+            closeBtn.addEventListener('click', () => removeTrend(index));
+            
+            selectedTrendsContainer.appendChild(badge);
+        });
+    }
+
+    // 更新隐藏输入框的值
+    function updateHiddenInput() {
+        const trendNames = selectedTrends.map(trend => trend.name);
+        longTermTrendInput.value = trendNames.join('+');
+        
+        console.log('更新隐藏输入框值:', longTermTrendInput.value);
+    }
+
+    // 监听趋势特征搜索输入
+    trendSearchInput.addEventListener('input', function() {
+        const query = this.value.trim();
+        
+        // 使用防抖进行搜索
+        clearTimeout(searchTimeout);
+        searchTimeout = setTimeout(() => {
+            searchTrends(query);
+        }, 300);
+    });
+
+    // 监听焦点离开事件,隐藏搜索结果
+    trendSearchInput.addEventListener('blur', function() {
+        // 延迟隐藏,允许用户点击搜索结果
+        setTimeout(() => {
+            hideTrendSearchResults();
+        }, 200);
+    });
+
+    // 监听点击外部区域隐藏搜索结果
+    document.addEventListener('click', function(event) {
+        if (!event.target.closest('.trend-selection-area')) {
+            hideTrendSearchResults();
+        }
+    });
+
+    // 表单提交处理
+    form.addEventListener('submit', function(event) {
+        event.preventDefault();
+        
+        if (!form.checkValidity()) {
+            event.stopPropagation();
+            form.classList.add('was-validated');
+            return;
+        }
+
+        // 直接提交,因为趋势特征已通过多选组件验证
+
+        // 获取平今费率的值
+        const closeTodayRateInput = document.getElementById('close_today_rate');
+        const closeTodayRateValue = closeTodayRateInput.value;
+        console.log('提交前平今费率值:', {
+            inputValue: closeTodayRateValue,
+            inputValueAsNumber: closeTodayRateInput.valueAsNumber,
+            inputStep: closeTodayRateInput.step
+        });
+
+        // 收集表单数据
+        const formData = {
+            contract_letter: document.getElementById('contract_letter').value,
+            name: document.getElementById('name').value,
+            market: parseInt(document.getElementById('market').value),
+            exchange: document.getElementById('exchange').value,
+            contract_multiplier: document.getElementById('contract_multiplier').value ? Number(document.getElementById('contract_multiplier').value) : null,
+            long_margin_rate: document.getElementById('long_margin_rate').value !== '' ? Number(document.getElementById('long_margin_rate').value) : null,
+            short_margin_rate: document.getElementById('short_margin_rate').value !== '' ? Number(document.getElementById('short_margin_rate').value) : null,
+            open_fee: document.getElementById('open_fee').value !== '' ? Number(document.getElementById('open_fee').value) : null,
+            close_fee: document.getElementById('close_fee').value !== '' ? Number(document.getElementById('close_fee').value) : null,
+            close_today_rate: closeTodayRateValue !== '' ? Number(closeTodayRateValue) : null,
+            close_today_fee: document.getElementById('close_today_fee').value !== '' ? Number(document.getElementById('close_today_fee').value) : null,
+            long_margin_amount: document.getElementById('long_margin_amount').value !== '' ? Number(document.getElementById('long_margin_amount').value) : null,
+            short_margin_amount: document.getElementById('short_margin_amount').value !== '' ? Number(document.getElementById('short_margin_amount').value) : null,
+            th_main_contract: document.getElementById('th_main_contract').value,
+            current_main_contract: document.getElementById('current_main_contract').value,
+            th_order: document.getElementById('th_order').value ? parseInt(document.getElementById('th_order').value) : null,
+            long_term_trend: document.getElementById('long_term_trend').value,
+            core_ratio: document.getElementById('core_ratio').value ? Number(document.getElementById('core_ratio').value) : null
+        };
+
+        console.log('准备提交的数据:', formData);
+
+        // 发送更新请求
+        fetch(`/api/future_info/update/${futureId}`, {
+            method: 'PUT',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify(formData)
+        })
+        .then(response => response.json())
+        .then(data => {
+            console.log('服务器响应:', data);
+            if (data.code === 0) {
+                alert('更新成功');
+                window.location.href = "{{ url_for('future_info.index') }}";
+            } else {
+                alert('更新失败:' + data.msg);
+            }
+        })
+        .catch(error => {
+            console.error('Error:', error);
+            alert('更新失败');
+        });
+    });
+});
+</script>
+{% endblock %} 

+ 121 - 0
app/templates/future_info/import.html

@@ -0,0 +1,121 @@
+{% extends 'base.html' %}
+
+{% block title %}导入期货基础信息 - 期货数据管理系统{% endblock %}
+
+{% block content %}
+<div class="d-flex justify-content-between align-items-center mb-4">
+    <h2>导入期货基础信息</h2>
+    <a href="{{ url_for('future_info.index') }}" class="btn btn-secondary">返回列表</a>
+</div>
+
+<div class="card">
+    <div class="card-body">
+        <div class="row mb-4">
+            <div class="col-md-12">
+                <p>请按照以下步骤操作:</p>
+                <ol>
+                    <li>下载导入模板</li>
+                    <li>按照模板格式填写数据</li>
+                    <li>上传Excel文件</li>
+                </ol>
+                <div class="alert alert-info">
+                    <strong>提示:</strong> 必填字段包括"合约字母"、"名称"和"市场(0-国内,1-国外)"。
+                </div>
+            </div>
+        </div>
+        
+        <div class="row mb-4">
+            <div class="col-md-6">
+                <button id="download-template" class="btn btn-primary">
+                    <i class="fas fa-download"></i> 下载导入模板
+                </button>
+            </div>
+        </div>
+        
+        <form id="import-form" enctype="multipart/form-data">
+            <div class="row mb-4">
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="file">选择Excel文件</label>
+                        <input type="file" class="form-control" id="file" name="file" accept=".xlsx" required>
+                    </div>
+                </div>
+            </div>
+            
+            <div class="form-group">
+                <button type="submit" class="btn btn-success">
+                    <i class="fas fa-upload"></i> 导入数据
+                </button>
+            </div>
+        </form>
+        
+        <div id="result" class="mt-4" style="display: none;">
+        </div>
+    </div>
+</div>
+
+<script>
+document.addEventListener('DOMContentLoaded', function() {
+    const form = document.getElementById('import-form');
+    const resultDiv = document.getElementById('result');
+    const downloadBtn = document.getElementById('download-template');
+    
+    // 处理下载模板按钮点击
+    downloadBtn.addEventListener('click', function(e) {
+        // 禁用按钮防止重复点击
+        downloadBtn.disabled = true;
+        
+        // 创建一个隐藏的iframe来处理下载
+        const iframe = document.createElement('iframe');
+        iframe.style.display = 'none';
+        iframe.src = "{{ url_for('future_info.get_template') }}";
+        document.body.appendChild(iframe);
+        
+        // 3秒后重新启用按钮
+        setTimeout(() => {
+            downloadBtn.disabled = false;
+            document.body.removeChild(iframe);
+        }, 3000);
+    });
+    
+    form.addEventListener('submit', function(e) {
+        e.preventDefault();
+        
+        const formData = new FormData(form);
+        
+        // 显示加载状态
+        resultDiv.innerHTML = '<div class="alert alert-info">正在导入数据,请稍候...</div>';
+        resultDiv.style.display = 'block';
+        
+        fetch('/api/future_info/import', {
+            method: 'POST',
+            body: formData
+        })
+        .then(response => response.json())
+        .then(data => {
+            if (data.code === 0) {
+                // 成功导入
+                let html = `<div class="alert alert-success">${data.msg}</div>`;
+                
+                if (data.data.error_count > 0) {
+                    html += '<div class="alert alert-warning"><strong>导入过程中出现以下错误:</strong><ul>';
+                    data.data.error_messages.forEach(msg => {
+                        html += `<li>${msg}</li>`;
+                    });
+                    html += '</ul></div>';
+                }
+                
+                resultDiv.innerHTML = html;
+            } else {
+                // 导入失败
+                resultDiv.innerHTML = `<div class="alert alert-danger">${data.msg}</div>`;
+            }
+        })
+        .catch(error => {
+            console.error('Error:', error);
+            resultDiv.innerHTML = '<div class="alert alert-danger">导入失败,请查看控制台了解详情</div>';
+        });
+    });
+});
+</script>
+{% endblock %} 

+ 455 - 0
app/templates/future_info/index.html

@@ -0,0 +1,455 @@
+{% extends 'base.html' %}
+
+{% block title %}期货基础信息 - 期货数据管理系统{% endblock %}
+
+{% block content %}
+<div class="container-fluid">
+    <div class="card">
+        <div class="card-header">
+             <!-- 标题和操作按钮 -->
+            <div class="row mb-0 align-items-center"> <!-- mb-3 removed, header adds padding -->
+                <div class="col-md-6">
+                    <h2>期货基础信息</h2>
+                </div>
+                <div class="col-md-6 text-end">
+                    <button id="updateDataBtn" type="button" class="btn btn-primary btn-sm me-1">
+                        <i class="fas fa-sync-alt"></i> 更新
+                    </button>
+                    <button id="syncMainContractsBtn" type="button" class="btn btn-warning btn-sm me-1">
+                        <i class="fas fa-link"></i> 同步
+                    </button>
+                    <button id="sortMainContract" class="btn btn-info btn-sm me-1">
+                        <i class="fas fa-sort"></i> 不一致
+                    </button>
+                    <a href="{{ url_for('future_info.import_view') }}" class="btn btn-success btn-sm me-1">
+                        <i class="fas fa-file-import"></i> 导入
+                    </a>
+                    <a href="{{ url_for('future_info.add') }}" class="btn btn-primary btn-sm">
+                        <i class="fas fa-plus"></i> 添加
+                    </a>
+                </div>
+            </div>
+        </div>
+        <div class="card-body">
+             <!-- 筛选/搜索区域 -->
+            <form id="searchForm" class="mb-3">
+                <div class="row g-2 align-items-center justify-content-end">
+                     <div class="col-md-2">
+                         <select class="form-select form-select-sm" name="market_filter" id="marketFilter">
+                            <option value="">所有市场</option>
+                            <option value="0">国内</option>
+                            <option value="1">国外</option>
+                        </select>
+                    </div>
+                     <div class="col-md-3">
+                        <input type="text" class="form-control form-control-sm" placeholder="按长期趋势筛选..." name="trend_filter" id="trendFilter">
+                    </div>
+                    <div class="col-md-4">
+                        <div class="input-group input-group-sm">
+                            <input type="text" class="form-control" placeholder="搜索合约字母或名称..." name="search" id="searchInput">
+                            <button class="btn btn-outline-secondary btn-sm" type="submit">搜索</button>
+                        </div>
+                    </div>
+                </div>
+            </form>
+
+            <!-- 数据表格 -->
+             <div class="table-responsive">
+                <table class="table table-striped table-hover">
+                    <thead>
+                        <tr>
+                            <th>合约字母</th>
+                            <th>名称</th>
+                            <th>市场</th>
+                            <!-- <th>交易所</th> -->
+                            <!-- <th>合约乘数</th> -->
+                            <th>做多保证金率</th>
+                            <th>做空保证金率</th>
+                            <th>同花主力合约</th>
+                            <th>当前主力合约</th>
+                            <th>长期趋势</th>
+                            <th>操作</th>
+                        </tr>
+                    </thead>
+                    <tbody id="futuresTableBody">
+                        <!-- 数据将通过JavaScript动态加载 -->
+                    </tbody>
+                </table>
+            </div>
+            <!-- 分页控件和每页数量选择器 -->
+            <div class="d-flex justify-content-center align-items-center mt-3">
+                <nav aria-label="Page navigation" class="me-3">
+                    <ul class="pagination mb-0" id="pagination">
+                        <!-- 分页按钮将通过JavaScript动态加载 -->
+                    </ul>
+                </nav>
+                <div class="d-flex align-items-center" id="itemsPerPageContainer" style="display: none;">
+                    <label for="itemsPerPageSelect" class="col-form-label me-2 mb-0">每页:</label>
+                    <select class="form-select form-select-sm" id="itemsPerPageSelect" style="width: auto;">
+                        <option value="10" selected>10</option>
+                        <option value="20">20</option>
+                        <option value="50">50</option>
+                    </select>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+
+<!-- Toast容器 -->
+<div id="toast-container" class="toast-container position-fixed bottom-0 end-0 p-3"></div>
+
+<!-- 引入 jQuery -->
+<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
+<!-- 引入 Bootstrap JS (如果尚未在base.html中引入) -->
+<!-- <script src="https://cdn.staticfile.org/twitter-bootstrap/5.1.1/js/bootstrap.bundle.min.js"></script> -->
+
+<script>
+$(document).ready(function() {
+    let currentPage = 1;
+    let currentItemsPerPage = parseInt($('#itemsPerPageSelect').val()) || 10;
+    let currentSearch = '';
+    let currentMarketFilter = '';
+    let currentTrendFilter = '';
+    let isSorting = false; // 标记是否处于排序状态
+
+    // --- 数据加载与渲染 ---
+    function loadFutures(page = 1, filters = {}) {
+        currentPage = page;
+        filters.page = page;
+        filters.limit = currentItemsPerPage;
+        filters.search = currentSearch;
+        filters.market = currentMarketFilter; // 添加市场筛选参数
+        filters.long_term_trend = currentTrendFilter; // 添加趋势筛选参数
+
+        // 添加加载提示
+        $('#futuresTableBody').html('<tr><td colspan="9" class="text-center">数据加载中...</td></tr>');
+        $('#pagination').empty();
+        $('#itemsPerPageContainer').hide();
+
+        $.get("{{ url_for('future_info.get_future_info_list') }}", filters, function(response) {
+            if (response.code === 0) {
+                renderTable(response.data);
+                renderPagination(response.count, currentPage, currentItemsPerPage);
+                if (isSorting) {
+                    applySortingAndHighlighting(); // 如果处于排序状态,重新应用排序和高亮
+                }
+            } else {
+                $('#futuresTableBody').html('<tr><td colspan="9" class="text-center text-danger">加载失败:' + response.msg + '</td></tr>');
+            }
+        }).fail(function(jqXHR, textStatus, errorThrown) {
+            $('#futuresTableBody').html('<tr><td colspan="9" class="text-center text-danger">加载失败:' + textStatus + '</td></tr>');
+        });
+    }
+
+    function renderTable(data) {
+        let html = '';
+        if (data && data.length > 0) {
+            data.forEach(function(item) {
+                const longMarginRate = (item.long_margin_rate !== null && item.long_margin_rate !== undefined) ? (item.long_margin_rate * 100).toFixed(2) + '%' : '';
+                const shortMarginRate = (item.short_margin_rate !== null && item.short_margin_rate !== undefined) ? (item.short_margin_rate * 100).toFixed(2) + '%' : '';
+                const marketDisplay = item.market === 0 ? '国内' : (item.market === 1 ? '国外' : '未知'); // 转换市场显示
+                html += `
+                    <tr data-id="${item.id}" data-th-main="${item.th_main_contract || ''}" data-current-main="${item.current_main_contract || ''}">
+                        <td>${item.contract_letter || ''}</td>
+                        <td>${item.name || ''}</td>
+                        <td>${marketDisplay}</td>
+                        <td>${longMarginRate}</td>
+                        <td>${shortMarginRate}</td>
+                        <td>${item.th_main_contract || ''}</td>
+                        <td>${item.current_main_contract || ''}</td>
+                        <td>${item.long_term_trend || ''}</td>
+                        <td>
+                            <div class="btn-group">
+                                <button type="button" class="btn btn-secondary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
+                                    操作
+                                </button>
+                                <ul class="dropdown-menu">
+                                    <li><a class="dropdown-item" href="/api/future_info/detail/${item.id}">查看</a></li>
+                                    <li><a class="dropdown-item" href="/api/future_info/edit/${item.id}">编辑</a></li>
+                                    <li><hr class="dropdown-divider"></li>
+                                    <li><button class="dropdown-item text-danger delete-future" data-id="${item.id}">删除</button></li>
+                                </ul>
+                            </div>
+                        </td>
+                    </tr>
+                `;
+            });
+        } else {
+            html = '<tr><td colspan="9" class="text-center">暂无期货信息</td></tr>';
+        }
+        $('#futuresTableBody').html(html);
+    }
+
+    function renderPagination(totalItems, page, itemsPerPage) {
+        const totalPages = Math.ceil(totalItems / itemsPerPage);
+        let paginationHtml = '';
+
+        if (totalPages <= 1) {
+            $('#pagination').empty();
+            $('#itemsPerPageContainer').hide();
+            return;
+        }
+        $('#itemsPerPageContainer').show();
+
+        // 上一页按钮
+        paginationHtml += `<li class="page-item ${page === 1 ? 'disabled' : ''}">
+                            <a class="page-link" href="#" data-page="${page - 1}" aria-label="Previous">
+                                <span aria-hidden="true">&laquo;</span>
+                            </a>
+                           </li>`;
+
+        // 页码按钮 (只显示部分页码)
+        const maxPagesToShow = 5;
+        let startPage = Math.max(1, page - Math.floor(maxPagesToShow / 2));
+        let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
+        if (endPage - startPage + 1 < maxPagesToShow) {
+            startPage = Math.max(1, endPage - maxPagesToShow + 1);
+        }
+        if (startPage > 1) {
+            paginationHtml += `<li class="page-item"><a class="page-link" href="#" data-page="1">1</a></li>`;
+            if (startPage > 2) {
+                paginationHtml += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
+            }
+        }
+        for (let i = startPage; i <= endPage; i++) {
+            paginationHtml += `<li class="page-item ${i === page ? 'active' : ''}">
+                                <a class="page-link" href="#" data-page="${i}">${i}</a>
+                               </li>`;
+        }
+        if (endPage < totalPages) {
+            if (endPage < totalPages - 1) {
+                paginationHtml += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
+            }
+            paginationHtml += `<li class="page-item"><a class="page-link" href="#" data-page="${totalPages}">${totalPages}</a></li>`;
+        }
+
+        // 下一页按钮
+        paginationHtml += `<li class="page-item ${page === totalPages ? 'disabled' : ''}">
+                            <a class="page-link" href="#" data-page="${page + 1}" aria-label="Next">
+                                <span aria-hidden="true">&raquo;</span>
+                            </a>
+                           </li>`;
+
+        $('#pagination').html(paginationHtml);
+    }
+
+    // --- 事件绑定 ---
+    // 分页点击
+    $('#pagination').on('click', 'a.page-link', function(e) {
+        e.preventDefault();
+        const page = $(this).data('page');
+        if (page && page !== currentPage) {
+            loadFutures(page);
+        }
+    });
+
+    // 每页数量变化
+    $('#itemsPerPageSelect').on('change', function() {
+        currentItemsPerPage = parseInt($(this).val());
+        loadFutures(1); // 回到第一页
+    });
+
+    // 搜索表单提交 (包括筛选)
+    $('#searchForm').on('submit', function(e) {
+        e.preventDefault();
+        currentSearch = $('#searchInput').val();
+        currentMarketFilter = $('#marketFilter').val(); // 获取市场筛选值
+        currentTrendFilter = $('#trendFilter').val(); // 获取趋势筛选值
+        isSorting = false; // 清除排序状态
+        loadFutures(1); // 搜索后回到第一页
+    });
+
+    // 删除按钮 (使用事件委托)
+    $('#futuresTableBody').on('click', '.delete-future', function() {
+        const futureId = $(this).data('id');
+        if (confirm('确定要删除此期货信息吗?')) {
+            $.ajax({
+                url: `/api/future_info/delete/${futureId}`,
+                type: 'DELETE',
+                success: function(response) {
+                    if (response.code === 0) {
+                        showToast('success', '删除成功', '期货信息已删除。');
+                        loadFutures(currentPage); // 重新加载当前页
+                    } else {
+                        showToast('error', '删除失败', response.msg);
+                    }
+                },
+                error: function() {
+                    showToast('error', '请求失败', '删除请求发送失败。');
+                }
+            });
+        }
+    });
+
+    // 手动更新数据按钮
+    const updateDataBtn = $('#updateDataBtn');
+    let pollInterval;
+    let pollCount = 0;
+    const maxPollCount = 60; // 最多轮询60次
+
+    updateDataBtn.on('click', function() {
+        const $btn = $(this);
+        const icon = $btn.find('i');
+
+        $btn.prop('disabled', true);
+        icon.addClass('fa-spin');
+        $btn.html(`<i class="fas fa-sync-alt fa-spin"></i> 正在启动更新...`);
+
+        $.ajax({
+            url: "{{ url_for('future_info.update_future_data') }}",
+            type: 'POST',
+            contentType: 'application/json',
+            data: JSON.stringify({ update_mode: 'both' }),
+            success: function(response) {
+                if (response.code === 0) {
+                    showToast('info', '更新已启动', response.msg);
+                    checkUpdateStatus();
+                } else {
+                    showToast('error', '启动更新失败', response.msg);
+                    restoreUpdateBtn();
+                }
+            },
+            error: function() {
+                showToast('error', '请求失败', '启动更新请求失败');
+                restoreUpdateBtn();
+            }
+        });
+    });
+
+    function checkUpdateStatus() {
+        pollCount = 0;
+        if (pollInterval) {
+            clearInterval(pollInterval);
+        }
+        updateDataBtn.html(`<i class="fas fa-sync-alt fa-spin"></i> 数据更新中...`);
+
+        pollInterval = setInterval(function() {
+            pollCount++;
+            if (pollCount > maxPollCount) {
+                clearInterval(pollInterval);
+                showToast('warning', '更新超时', '数据更新超时,请稍后手动刷新页面。');
+                restoreUpdateBtn();
+                return;
+            }
+
+            $.get("{{ url_for('future_info.get_update_status') }}", function(response) {
+                if (response.code === 0 && response.data) {
+                    if (response.data.complete === true) {
+                        clearInterval(pollInterval);
+                        showToast('success', '更新完成', '数据更新成功!页面将自动刷新。');
+                        setTimeout(() => { location.reload(); }, 2000);
+                    } else if (response.data.complete === false) {
+                        clearInterval(pollInterval);
+                        showToast('error', '更新失败', `数据更新失败: ${response.data.error || '未知错误'}`);
+                        restoreUpdateBtn();
+                    }
+                }
+            }).fail(function() {
+                 console.error("检查更新状态请求失败");
+            });
+        }, 5000);
+    }
+
+    function restoreUpdateBtn() {
+        updateDataBtn.prop('disabled', false);
+        updateDataBtn.html(`<i class="fas fa-sync-alt"></i> 手动更新数据`);
+    }
+
+    // 同步主力合约功能
+    $('#syncMainContractsBtn').on('click', function() {
+        const $btn = $(this);
+        const icon = $btn.find('i');
+        
+        if (!confirm('确定要同步主力合约吗?这将把"同花主力合约"更新为"当前主力合约"。')) {
+            return;
+        }
+        
+        $btn.prop('disabled', true);
+        icon.addClass('fa-spin');
+        $btn.html(`<i class="fas fa-link fa-spin"></i> 同步中...`);
+        
+        $.ajax({
+            url: "{{ url_for('future_info.sync_main_contracts') }}",
+            type: 'POST',
+            contentType: 'application/json',
+            success: function(response) {
+                if (response.code === 0) {
+                    showToast('success', '同步成功', response.msg);
+                    loadFutures(currentPage); // 重新加载当前页数据
+                } else {
+                    showToast('error', '同步失败', response.msg);
+                }
+            },
+            error: function() {
+                showToast('error', '请求失败', '同步请求发送失败。');
+            },
+            complete: function() {
+                $btn.prop('disabled', false);
+                $btn.html(`<i class="fas fa-link"></i> 同步`);
+            }
+        });
+    });
+
+    // 主力合约排序功能
+    $('#sortMainContract').on('click', function() {
+        isSorting = true; // 标记为排序状态
+        applySortingAndHighlighting();
+    });
+
+    function applySortingAndHighlighting() {
+        const rows = $('#futuresTableBody tr').get(); // 获取所有行DOM元素
+        rows.sort((a, b) => {
+            const aThMain = $(a).data('th-main') || '';
+            const aCurrentMain = $(a).data('current-main') || '';
+            const bThMain = $(b).data('th-main') || '';
+            const bCurrentMain = $(b).data('current-main') || '';
+            const aNotMatch = aThMain !== aCurrentMain;
+            const bNotMatch = bThMain !== bCurrentMain;
+            if (aNotMatch === bNotMatch) return 0;
+            return aNotMatch ? -1 : 1;
+        });
+
+        const tbody = $('#futuresTableBody');
+        tbody.empty(); // 清空tbody
+        $.each(rows, function(index, row) {
+            const $row = $(row);
+            const thMain = $row.data('th-main') || '';
+            const currentMain = $row.data('current-main') || '';
+            if (thMain !== currentMain) {
+                $row.addClass('table-warning');
+            } else {
+                $row.removeClass('table-warning');
+            }
+            tbody.append($row); // 重新添加排序和高亮后的行
+        });
+    }
+
+    // --- Toast 显示 ---
+    function showToast(type, title, message) {
+        const bgClass = type === 'success' ? 'bg-success' : (type === 'error' ? 'bg-danger' : (type === 'warning' ? 'bg-warning' : 'bg-info'));
+        const toastHtml = `
+            <div class="toast align-items-center text-white ${bgClass} border-0" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="true" data-bs-delay="5000">
+              <div class="d-flex">
+                <div class="toast-body">
+                  <strong>${title}:</strong> ${message}
+                </div>
+                <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
+              </div>
+            </div>
+        `;
+        const $toastElement = $(toastHtml);
+        $('#toast-container').append($toastElement);
+        const toast = new bootstrap.Toast($toastElement[0]);
+        toast.show();
+         // Optional: Remove the toast from DOM after it's hidden
+        $toastElement.on('hidden.bs.toast', function () {
+            $(this).remove();
+        });
+    }
+
+    // --- 初始加载 ---
+    loadFutures();
+});
+</script>
+{% endblock %} 

+ 51 - 0
app/templates/index.html

@@ -0,0 +1,51 @@
+{% extends 'base.html' %}
+
+{% block title %}首页 - 期货数据管理系统{% endblock %}
+
+{% block content %}
+<div class="jumbotron">
+    <h1 class="display-4">欢迎使用期货数据管理系统</h1>
+    <p class="lead">这是一个用于管理期货数据的系统,包括期货基础信息、交易记录、交易汇总和标的监控等功能。</p>
+    <hr class="my-4">
+    <p>请从上方导航栏选择要使用的功能。</p>
+</div>
+
+<div class="row">
+    <div class="col-md-3 mb-4">
+        <div class="card">
+            <div class="card-body">
+                <h5 class="card-title">期货基础信息</h5>
+                <p class="card-text">管理期货合约的基础信息,包括合约代码、名称、交易所等。</p>
+                <a href="{{ url_for('future_info.index') }}" class="btn btn-primary">进入</a>
+            </div>
+        </div>
+    </div>
+    <div class="col-md-3 mb-4">
+        <div class="card">
+            <div class="card-body">
+                <h5 class="card-title">交易记录</h5>
+                <p class="card-text">记录期货交易的详细信息,包括买入、卖出记录等。</p>
+                <a href="{{ url_for('transaction.index') }}" class="btn btn-primary">进入</a>
+            </div>
+        </div>
+    </div>
+    <div class="col-md-3 mb-4">
+        <div class="card">
+            <div class="card-body">
+                <h5 class="card-title">交易汇总</h5>
+                <p class="card-text">汇总交易信息,统计盈亏情况和交易表现。</p>
+                <a href="{{ url_for('trade.index') }}" class="btn btn-primary">进入</a>
+            </div>
+        </div>
+    </div>
+    <div class="col-md-3 mb-4">
+        <div class="card">
+            <div class="card-body">
+                <h5 class="card-title">标的监控</h5>
+                <p class="card-text">监控关注的期货标的,包括价格、持仓量等信息。</p>
+                <a href="{{ url_for('monitor.index') }}" class="btn btn-primary">进入</a>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock %} 

+ 639 - 0
app/templates/monitor/add.html

@@ -0,0 +1,639 @@
+{% extends 'base.html' %}
+
+{% block title %}添加监控记录 - 期货数据管理系统{% endblock %}
+
+{% block styles %}
+<!-- 引入 Select2 CSS -->
+<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css">
+<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ttskch/select2-bootstrap4-theme@x.x.x/dist/select2-bootstrap4.min.css">
+<style>
+    .select2-container--bootstrap4 .select2-selection--multiple {
+        min-height: calc(1.5em + .75rem + 2px);
+    }
+</style>
+{% endblock %}
+
+{% block content %}
+<div class="d-flex justify-content-between align-items-center mb-4">
+    <h2>添加监控记录</h2>
+    <a href="{{ url_for('monitor.index') }}" class="btn btn-secondary">返回列表</a>
+</div>
+
+<div class="card">
+    <div class="card-body">
+        <form id="monitor-form" class="needs-validation" novalidate>
+            <div class="row">
+                <div class="col-md-6 mb-3">
+                    <label for="future_info_id" class="form-label">期货品种 <span class="text-danger">*</span></label>
+                    <div class="position-relative">
+                        <input type="text" class="form-control" id="future_search_input" name="future_search_input" 
+                               placeholder="输入期货品种名称或代码..." autocomplete="off" required>
+                        <input type="hidden" id="future_info_id" name="future_info_id" required>
+                        <div id="future_search_results" class="dropdown-menu w-100" style="display: none; max-height: 200px; overflow-y: auto;">
+                            <!-- 搜索结果将在这里显示 -->
+                        </div>
+                    </div>
+                    <div class="invalid-feedback">请选择期货品种</div>
+                </div>
+                 <div class="col-md-6 mb-3">
+                    <label for="contract" class="form-label">合约代码 <span class="text-danger">*</span></label>
+                    <input type="text" class="form-control" id="contract" name="contract" required>
+                     <div class="invalid-feedback">请输入合约代码</div>
+                </div>
+            </div>
+             <div class="row">
+                <div class="col-md-6 mb-3">
+                    <label for="name" class="form-label">名称</label>
+                    <input type="text" class="form-control" id="name" name="name" readonly required>
+                     <div class="invalid-feedback">名称将根据期货品种自动填充</div>
+                </div>
+                 <div class="col-md-6 mb-3">
+                    <label for="market" class="form-label">市场类型</label>
+                    <select class="form-select" id="market" name="market" readonly disabled> <!-- 设置为 readonly 和 disabled -->
+                        <option value="" selected disabled>请选择市场类型</option>
+                        <option value="0">国内</option>
+                        <option value="1">国外</option>
+                    </select>
+                     <div class="invalid-feedback">市场类型将根据期货品种自动填充</div>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-6 mb-3">
+                    <label for="opportunity" class="form-label">关注原因</label>
+                    <select class="form-control" id="opportunity" name="opportunity">
+                        <option value="">请选择关注原因</option>
+                        <option value="支撑位">支撑位</option>
+                        <option value="压力位">压力位</option>
+                        <option value="手画支撑位">手画支撑位</option>
+                        <option value="手画压力位">手画压力位</option>
+                        <option value="5K支撑位">5K支撑位</option>
+                        <option value="5K压力位">5K压力位</option>
+                        <option value="10K支撑位">10K支撑位</option>
+                        <option value="10K压力位">10K压力位</option>
+                        <option value="20K支撑位">20K支撑位</option>
+                        <option value="20K压力位">20K压力位</option>
+                        <option value="30K支撑位">30K支撑位</option>
+                        <option value="30K压力位">30K压力位</option>
+                    </select>
+                </div>
+                <div class="col-md-6 mb-3">
+                    <label for="status" class="form-label">关注状态</label>
+                    <select class="form-select" id="status" name="status">
+                        <option value="0" selected>观察中</option>
+                        <option value="1">重点关注</option>
+                        <option value="2">已触发</option>
+                        <option value="3">已失效</option>
+                    </select>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-6 mb-3">
+                    <label for="key_price" class="form-label">关键价格</label>
+                    <input type="number" step="any" class="form-control" id="key_price" name="key_price">
+                </div>
+                <div class="col-md-6 mb-3">
+                    <label for="latest_price" class="form-label">最新价格</label>
+                    <input type="number" step="any" class="form-control" id="latest_price" name="latest_price">
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-6 mb-3">
+                    <label for="open_price" class="form-label">开仓价格</label>
+                    <input type="number" step="any" class="form-control" id="open_price" name="open_price">
+                </div>
+                <div class="col-md-6 mb-3">
+                    <label for="position_mode_id" class="form-label">开仓模式</label>
+                    <select class="form-select" id="position_mode_id" name="position_mode_id">
+                        <option value="">请选择开仓模式</option>
+                    </select>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-12 mb-3">
+                    <label for="candle_pattern" class="form-label">K线形态</label>
+                    <select class="form-control select2" id="candle_pattern" name="candle_pattern" multiple="multiple" data-placeholder="选择K线形态">
+                        <!-- 选项将通过API加载 -->
+                    </select>
+                    <input type="hidden" id="candle_pattern_ids" name="candle_pattern_ids">
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-6 mb-3">
+                    <label for="open_long_price" class="form-label">做多开仓价</label>
+                    <input type="number" step="any" class="form-control" id="open_long_price" name="open_long_price" readonly>
+                </div>
+                <div class="col-md-6 mb-3">
+                    <label for="open_short_price" class="form-label">做空开仓价</label>
+                    <input type="number" step="any" class="form-control" id="open_short_price" name="open_short_price" readonly>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-6 mb-3">
+                    <label for="open_long_trigger_price" class="form-label">做多触发价</label>
+                    <input type="number" step="any" class="form-control" id="open_long_trigger_price" name="open_long_trigger_price" readonly>
+                </div>
+                <div class="col-md-6 mb-3">
+                    <label for="open_short_trigger_price" class="form-label">做空触发价</label>
+                    <input type="number" step="any" class="form-control" id="open_short_trigger_price" name="open_short_trigger_price" readonly>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-6 mb-3">
+                    <label for="open_long_margin_per_unit" class="form-label">开多一手保证金</label>
+                    <input type="number" step="any" class="form-control" id="open_long_margin_per_unit" name="open_long_margin_per_unit" readonly>
+                </div>
+                <div class="col-md-6 mb-3">
+                    <label for="open_short_margin_per_unit" class="form-label">开空一手保证金</label>
+                    <input type="number" step="any" class="form-control" id="open_short_margin_per_unit" name="open_short_margin_per_unit" readonly>
+                </div>
+            </div>
+             <!-- 可以根据需要添加更多字段 -->
+            <div class="form-group mt-4">
+                <button type="submit" class="btn btn-primary">保存</button>
+                <button type="reset" class="btn btn-secondary">重置</button>
+            </div>
+        </form>
+    </div>
+</div>
+{% endblock %}
+
+{% block scripts %}
+<!-- 引入 jQuery -->
+<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
+<!-- 引入 Select2 JS -->
+<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
+<script>
+document.addEventListener('DOMContentLoaded', function() {
+    const form = document.getElementById('monitor-form');
+    const futureSearchInput = document.getElementById('future_search_input');
+    const futureIdInput = document.getElementById('future_info_id');
+    const searchResults = document.getElementById('future_search_results');
+    const contractInput = document.getElementById('contract');
+    const nameInput = document.getElementById('name');
+    const marketSelect = document.getElementById('market');
+
+    let searchTimeout;
+    let currentFutureData = null;
+
+    // 存储开仓模式数据
+    let positionModesData = [];
+    
+    // 存储K线形态选择顺序
+    let candlePatternOrder = [];
+    let candlePatternIdOrder = [];
+
+    // 加载开仓模式选项
+    function loadPositionModes() {
+        fetch('/monitor/api/position_modes')
+            .then(response => response.json())
+            .then(data => {
+                if (data.code === 0) {
+                    positionModesData = data.data; // 存储完整数据
+                    const select = document.getElementById('position_mode_id');
+                    data.data.forEach(mode => {
+                        const option = document.createElement('option');
+                        option.value = mode.id;
+                        option.textContent = mode.name;
+                        select.appendChild(option);
+                    });
+                } else {
+                    console.error('加载开仓模式失败:', data.msg);
+                }
+            })
+            .catch(error => {
+                console.error('加载开仓模式出错:', error);
+            });
+    }
+
+    // 加载K线形态选项
+    function loadCandlePatterns() {
+        fetch('/monitor/api/candle_patterns')
+            .then(response => response.json())
+            .then(data => {
+                if (data.code === 0) {
+                    const select = $('#candle_pattern');
+                    select.empty(); // 清空现有选项
+                    data.data.forEach(pattern => {
+                        const option = new Option(pattern.name, pattern.id, false, false);
+                        select.append(option);
+                    });
+                    // 初始化Select2
+                    select.select2({
+                        theme: 'bootstrap4',
+                        placeholder: '选择K线形态',
+                        allowClear: true,
+                        closeOnSelect: false // 选择后不关闭,方便多选
+                    });
+                } else {
+                    console.error('加载K线形态失败:', data.msg);
+                }
+            })
+            .catch(error => {
+                console.error('加载K线形态出错:', error);
+            });
+    }
+
+    // 根据合约字母加载保证金信息
+    function loadMarginInfo(contractLetter) {
+        if (!contractLetter) return;
+        
+        fetch(`/monitor/api/margin_info/${contractLetter}`)
+            .then(response => response.json())
+            .then(data => {
+                if (data.code === 0) {
+                    const marginInfo = data.data;
+                    // 计算保证金(需要价格信息)
+                    calculateMargins(marginInfo);
+                } else {
+                    console.warn('获取保证金信息失败:', data.msg);
+                }
+            })
+            .catch(error => {
+                console.error('获取保证金信息出错:', error);
+            });
+    }
+
+    // 设置保证金(直接从future_info获取固定金额)
+    function calculateMargins(marginInfo) {
+        // 直接使用从future_info查询到的固定保证金金额
+        if (marginInfo.long_margin_amount) {
+            document.getElementById('open_long_margin_per_unit').value = marginInfo.long_margin_amount.toFixed(2);
+        }
+        
+        if (marginInfo.short_margin_amount) {
+            document.getElementById('open_short_margin_per_unit').value = marginInfo.short_margin_amount.toFixed(2);
+        }
+    }
+
+    // 期货品种搜索功能
+    function searchFutures(query) {
+        if (!query.trim()) {
+            hideSearchResults();
+            return;
+        }
+
+        fetch(`/api/future_info/search?q=${encodeURIComponent(query)}&limit=10`)
+            .then(response => response.json())
+            .then(data => {
+                if (data.code === 0) {
+                    showSearchResults(data.data);
+                } else {
+                    console.error('搜索期货品种失败:', data.msg);
+                    hideSearchResults();
+                }
+            })
+            .catch(error => {
+                console.error('搜索期货品种失败:', error);
+                hideSearchResults();
+            });
+    }
+
+    // 显示搜索结果
+    function showSearchResults(results) {
+        searchResults.innerHTML = '';
+        
+        if (results.length === 0) {
+            searchResults.innerHTML = '<div class="dropdown-item-text text-muted">未找到匹配的期货品种</div>';
+        } else {
+            results.forEach(future => {
+                const item = document.createElement('div');
+                item.className = 'dropdown-item cursor-pointer';
+                item.style.cursor = 'pointer';
+                item.textContent = future.display_text;
+                item.addEventListener('click', () => selectFuture(future));
+                searchResults.appendChild(item);
+            });
+        }
+        
+        searchResults.style.display = 'block';
+    }
+
+    // 隐藏搜索结果
+    function hideSearchResults() {
+        searchResults.style.display = 'none';
+    }
+
+    // 选择期货品种
+    function selectFuture(future) {
+        currentFutureData = future;
+        futureSearchInput.value = future.display_text;
+        futureIdInput.value = future.id;
+        hideSearchResults();
+
+        // 自动填充相关字段
+        fillFutureDetails(future);
+        
+        // 如果已经有关键价格,则自动计算
+        setTimeout(() => {
+            calculatePrices();
+        }, 100);
+        
+        console.log('选择了期货品种:', future);
+    }
+
+    // 自动填充期货详情
+    function fillFutureDetails(future) {
+        // 填充名称
+        nameInput.value = future.name || '';
+        
+        // 填充市场类型
+        if (future.market !== null && future.market !== undefined) {
+            marketSelect.value = future.market.toString();
+            marketSelect.removeAttribute('disabled');
+        } else {
+            marketSelect.value = '';
+            marketSelect.setAttribute('disabled', '');
+        }
+
+        // 不自动填充合约代码,允许用户手动输入非主力合约
+        // 可以作为提示显示在placeholder中
+        if (future.current_main_contract) {
+            contractInput.placeholder = `主力合约: ${future.current_main_contract}`;
+        }
+
+        // 自动填充合约字母并加载保证金信息
+        if (future.contract_letter) {
+            loadMarginInfo(future.contract_letter);
+        }
+    }
+
+    // 监听期货品种输入
+    futureSearchInput.addEventListener('input', function() {
+        const query = this.value.trim();
+        
+        // 清空隐藏的ID字段
+        futureIdInput.value = '';
+        currentFutureData = null;
+        
+        // 清空其他字段
+        nameInput.value = '';
+        marketSelect.value = '';
+        marketSelect.setAttribute('disabled', '');
+        
+        // 使用防抖进行搜索
+        clearTimeout(searchTimeout);
+        searchTimeout = setTimeout(() => {
+            searchFutures(query);
+        }, 300);
+    });
+
+    // 监听焦点离开事件,隐藏搜索结果
+    futureSearchInput.addEventListener('blur', function() {
+        // 延迟隐藏,允许用户点击搜索结果
+        setTimeout(() => {
+            hideSearchResults();
+        }, 200);
+    });
+
+    // 监听点击外部区域隐藏搜索结果
+    document.addEventListener('click', function(event) {
+        if (!event.target.closest('.position-relative')) {
+            hideSearchResults();
+        }
+    });
+
+    // 兼容性:监听合约代码输入变化进行反向查询
+    let contractDebounceTimer;
+    contractInput.addEventListener('input', function() {
+        clearTimeout(contractDebounceTimer);
+        const code = this.value.trim();
+        
+        // 如果已经通过期货品种选择填充了合约代码,则不进行反向查询
+        if (currentFutureData && currentFutureData.current_main_contract === code) {
+            return;
+        }
+        
+        if (code) {
+            // 清空期货品种选择
+            futureSearchInput.value = '';
+            futureIdInput.value = '';
+            currentFutureData = null;
+            
+            contractDebounceTimer = setTimeout(() => {
+                lookupByContract(code);
+            }, 500);
+        } else {
+            nameInput.value = '';
+            marketSelect.value = '';
+            marketSelect.setAttribute('disabled', '');
+        }
+    });
+
+    // 根据合约代码查询期货信息
+    function lookupByContract(contractCode) {
+        fetch(`/monitor/api/future_info/lookup?contract_code=${encodeURIComponent(contractCode)}`)
+            .then(response => response.json())
+            .then(data => {
+                if (data.code === 0 && data.data) {
+                    nameInput.value = data.data.name || '';
+                    marketSelect.value = data.data.market_type !== null ? data.data.market_type.toString() : '';
+                    marketSelect.removeAttribute('disabled');
+                } else {
+                    nameInput.value = '';
+                    marketSelect.value = '';
+                    marketSelect.setAttribute('disabled', '');
+                    console.warn('未能自动填充期货信息:', data.msg);
+                }
+            })
+            .catch(error => {
+                nameInput.value = '';
+                marketSelect.value = '';
+                marketSelect.setAttribute('disabled', '');
+                console.error('查询期货信息失败:', error);
+            });
+    }
+
+    // 自动计算价格
+    function calculatePrices() {
+        const keyPrice = document.getElementById('key_price').value;
+        const futureInfoId = futureIdInput.value;
+        
+        // 清空所有计算字段
+        const calculatedFields = ['open_long_price', 'open_short_price', 'open_long_trigger_price', 'open_short_trigger_price'];
+        calculatedFields.forEach(fieldId => {
+            document.getElementById(fieldId).value = '';
+        });
+        
+        if (!keyPrice || !futureInfoId) {
+            console.log('缺少关键价格或期货信息ID,跳过自动计算');
+            return;
+        }
+        
+        // 调用后端API进行计算
+        fetch('/monitor/api/calculate_prices', {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify({
+                future_info_id: parseInt(futureInfoId),
+                key_price: parseFloat(keyPrice)
+            })
+        })
+        .then(response => response.json())
+        .then(data => {
+            if (data.code === 0) {
+                const prices = data.data;
+                // 填充计算结果,保留2位小数
+                document.getElementById('open_long_price').value = prices.open_long_price.toFixed(2);
+                document.getElementById('open_short_price').value = prices.open_short_price.toFixed(2);
+                document.getElementById('open_long_trigger_price').value = prices.open_long_trigger_price.toFixed(2);
+                document.getElementById('open_short_trigger_price').value = prices.open_short_trigger_price.toFixed(2);
+                console.log('自动计算完成,核心比率:', prices.core_ratio);
+            } else {
+                console.warn('自动计算失败:', data.msg);
+            }
+        })
+        .catch(error => {
+            console.error('自动计算出错:', error);
+        });
+    }
+    
+    // 监听关键价格变化
+    let keyPriceTimeout;
+    document.getElementById('key_price').addEventListener('input', function() {
+        // 使用防抖,避免频繁计算
+        clearTimeout(keyPriceTimeout);
+        keyPriceTimeout = setTimeout(() => {
+            calculatePrices();
+        }, 500);
+    });
+
+    // K线形态选择事件 - 跟踪真实的用户选择顺序
+    $('#candle_pattern').on('select2:select', function(e) {
+        const selectedData = e.params.data;
+        const selectedId = selectedData.id;
+        const selectedText = selectedData.text;
+        
+        // 添加到用户选择顺序数组
+        if (!candlePatternIdOrder.includes(selectedId)) {
+            candlePatternIdOrder.push(selectedId);
+            candlePatternOrder.push(selectedText);
+            
+            // 更新隐藏的ID字段
+            $('#candle_pattern_ids').val(candlePatternIdOrder.join(','));
+            console.log('选择的K线形态ID(按用户点击顺序):', candlePatternIdOrder);
+            console.log('选择的K线形态名称(按用户点击顺序):', candlePatternOrder);
+        }
+    });
+    
+    $('#candle_pattern').on('select2:unselect', function(e) {
+        const unselectedData = e.params.data;
+        const unselectedId = unselectedData.id;
+        const unselectedText = unselectedData.text;
+        
+        // 从用户选择顺序数组中移除
+        const idIndex = candlePatternIdOrder.indexOf(unselectedId);
+        const textIndex = candlePatternOrder.indexOf(unselectedText);
+        
+        if (idIndex > -1) {
+            candlePatternIdOrder.splice(idIndex, 1);
+        }
+        if (textIndex > -1) {
+            candlePatternOrder.splice(textIndex, 1);
+        }
+        
+        // 更新隐藏的ID字段
+        $('#candle_pattern_ids').val(candlePatternIdOrder.join(','));
+        console.log('取消选择后的K线形态ID(按用户点击顺序):', candlePatternIdOrder);
+        console.log('取消选择后的K线形态名称(按用户点击顺序):', candlePatternOrder);
+    });
+
+    // 开仓模式改变时自动填入相应的触发价到开仓价格
+    document.getElementById('position_mode_id').addEventListener('change', function() {
+        const selectedModeId = parseInt(this.value);
+        if (selectedModeId) {
+            const selectedMode = positionModesData.find(mode => mode.id === selectedModeId);
+            if (selectedMode) {
+                // 根据方向自动填入相应的触发价
+                if (selectedMode.direction === 0) {
+                    // 做多方向,填入做多触发价
+                    const longTriggerPrice = document.getElementById('open_long_trigger_price').value;
+                    if (longTriggerPrice) {
+                        document.getElementById('open_price').value = longTriggerPrice;
+                    }
+                } else if (selectedMode.direction === 1) {
+                    // 做空方向,填入做空触发价
+                    const shortTriggerPrice = document.getElementById('open_short_trigger_price').value;
+                    if (shortTriggerPrice) {
+                        document.getElementById('open_price').value = shortTriggerPrice;
+                    }
+                }
+                // direction为NULL的情况(虚拟、执行、无)不自动填入
+                console.log('开仓模式选择:', selectedMode.name, '方向:', selectedMode.direction);
+            }
+        }
+    });
+
+    // 表单提交处理
+    form.addEventListener('submit', function(event) {
+        event.preventDefault();
+
+        if (!form.checkValidity()) {
+            event.stopPropagation();
+            form.classList.add('was-validated');
+            return;
+        }
+
+        // 收集表单数据
+        const formData = {
+            future_info_id: parseInt(futureIdInput.value) || null,
+            contract: contractInput.value.trim(),
+            name: nameInput.value.trim(),
+            market: marketSelect.value !== '' ? parseInt(marketSelect.value) : null,
+            status: parseInt(document.getElementById('status').value),
+            opportunity: document.getElementById('opportunity').value || null,
+            key_price: document.getElementById('key_price').value ? parseFloat(document.getElementById('key_price').value) : null,
+            open_long_price: document.getElementById('open_long_price').value ? parseFloat(document.getElementById('open_long_price').value) : null,
+            open_short_price: document.getElementById('open_short_price').value ? parseFloat(document.getElementById('open_short_price').value) : null,
+            latest_price: document.getElementById('latest_price').value ? parseFloat(document.getElementById('latest_price').value) : null,
+            open_long_trigger_price: document.getElementById('open_long_trigger_price').value ? parseFloat(document.getElementById('open_long_trigger_price').value) : null,
+            open_short_trigger_price: document.getElementById('open_short_trigger_price').value ? parseFloat(document.getElementById('open_short_trigger_price').value) : null,
+            contract_letter: currentFutureData ? currentFutureData.contract_letter : null,
+            open_price: document.getElementById('open_price').value ? parseFloat(document.getElementById('open_price').value) : null,
+            position_mode_id: document.getElementById('position_mode_id').value ? parseInt(document.getElementById('position_mode_id').value) : null,
+            candle_pattern_ids: $('#candle_pattern_ids').val() || null,
+            candle_pattern: candlePatternOrder.join('+') || null,
+            open_long_margin_per_unit: document.getElementById('open_long_margin_per_unit').value ? parseFloat(document.getElementById('open_long_margin_per_unit').value) : null,
+            open_short_margin_per_unit: document.getElementById('open_short_margin_per_unit').value ? parseFloat(document.getElementById('open_short_margin_per_unit').value) : null
+        };
+
+        console.log('准备提交的数据:', formData);
+
+        // 发送创建请求
+        fetch('/monitor/add', {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify(formData)
+        })
+        .then(response => response.json())
+        .then(data => {
+            console.log('服务器响应:', data);
+            if (data.code === 0) {
+                alert('添加成功!');
+                window.location.href = "{{ url_for('monitor.index') }}";
+            } else {
+                alert('添加失败:' + (data.msg || '未知错误'));
+            }
+        })
+        .catch(error => {
+            console.error('Error:', error);
+            alert('发生错误,请查看控制台了解详情');
+        });
+    });
+
+    // 初始化验证监听器
+    Array.from(form.elements).forEach(element => {
+        element.addEventListener('invalid', () => {
+            form.classList.add('was-validated');
+        });
+    });
+
+    // 初始化加载数据
+    loadPositionModes();
+    loadCandlePatterns();
+});
+</script>
+{% endblock %} 

+ 191 - 0
app/templates/monitor/detail.html

@@ -0,0 +1,191 @@
+{% extends 'base.html' %}
+
+{% block title %}监控记录详情 - 期货数据管理系统{% endblock %}
+
+{% block content %}
+<div class="d-flex justify-content-between align-items-center mb-4">
+    <h2>监控记录详情</h2>
+    <div>
+        <a href="{{ url_for('monitor.edit', id=monitor.id) }}" class="btn btn-primary">编辑</a>
+        <a href="{{ url_for('monitor.index') }}" class="btn btn-secondary">返回列表</a>
+    </div>
+</div>
+
+<div class="row">
+    <div class="col-lg-8">
+        <div class="card">
+            <div class="card-header">
+                <h3 class="card-title">基本信息</h3>
+            </div>
+            <div class="card-body">
+                <div class="row mb-3">
+                    <label class="col-sm-3 col-form-label">合约代码:</label>
+                    <div class="col-sm-9">
+                        <p class="form-control-plaintext">{{ monitor.contract or '-' }}</p>
+                    </div>
+                </div>
+                <div class="row mb-3">
+                    <label class="col-sm-3 col-form-label">名称:</label>
+                    <div class="col-sm-9">
+                        <p class="form-control-plaintext">{{ monitor.name or '-' }}</p>
+                    </div>
+                </div>
+                <div class="row mb-3">
+                    <label class="col-sm-3 col-form-label">市场:</label>
+                    <div class="col-sm-9">
+                        <p class="form-control-plaintext">
+                            {% if monitor.market == 0 %}国内{% elif monitor.market == 1 %}国外{% else %}未知{% endif %}
+                        </p>
+                    </div>
+                </div>
+                <div class="row mb-3">
+                    <label class="col-sm-3 col-form-label">合约字母:</label>
+                    <div class="col-sm-9">
+                        <p class="form-control-plaintext">{{ monitor.contract_letter or '-' }}</p>
+                    </div>
+                </div>
+                <div class="row mb-3">
+                    <label class="col-sm-3 col-form-label">关注原因:</label>
+                    <div class="col-sm-9">
+                        <p class="form-control-plaintext">{{ monitor.opportunity or '-' }}</p>
+                    </div>
+                </div>
+                <div class="row mb-3">
+                    <label class="col-sm-3 col-form-label">关注状态:</label>
+                    <div class="col-sm-9">
+                        <p class="form-control-plaintext">
+                            {% set status_map = {0: '观察中', 1: '重点关注', 2: '已触发', 3: '已失效'} %}
+                            <span class="badge 
+                                {% if monitor.status == 0 %}bg-secondary{% elif monitor.status == 1 %}bg-warning{% elif monitor.status == 2 %}bg-success{% elif monitor.status == 3 %}bg-danger{% else %}bg-light{% endif %}">
+                                {{ status_map.get(monitor.status, '未知状态') }}
+                            </span>
+                        </p>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    
+    <div class="col-lg-4">
+        <div class="card">
+            <div class="card-header">
+                <h3 class="card-title">价格信息</h3>
+            </div>
+            <div class="card-body">
+                <div class="row mb-3">
+                    <label class="col-sm-6 col-form-label">关键价格:</label>
+                    <div class="col-sm-6">
+                        <p class="form-control-plaintext">
+                            {{ '%.2f'|format(monitor.key_price) if monitor.key_price else '-' }}
+                        </p>
+                    </div>
+                </div>
+                <div class="row mb-3">
+                    <label class="col-sm-6 col-form-label">最新价格:</label>
+                    <div class="col-sm-6">
+                        <p class="form-control-plaintext">
+                            {{ '%.2f'|format(monitor.latest_price) if monitor.latest_price else '-' }}
+                        </p>
+                    </div>
+                </div>
+                <div class="row mb-3">
+                    <label class="col-sm-6 col-form-label">开仓价格:</label>
+                    <div class="col-sm-6">
+                        <p class="form-control-plaintext">
+                            {{ '%.2f'|format(monitor.open_price) if monitor.open_price else '-' }}
+                        </p>
+                    </div>
+                </div>
+                <div class="row mb-3">
+                    <label class="col-sm-6 col-form-label">做多开仓价:</label>
+                    <div class="col-sm-6">
+                        <p class="form-control-plaintext">
+                            {{ '%.2f'|format(monitor.open_long_price) if monitor.open_long_price else '-' }}
+                        </p>
+                    </div>
+                </div>
+                <div class="row mb-3">
+                    <label class="col-sm-6 col-form-label">做空开仓价:</label>
+                    <div class="col-sm-6">
+                        <p class="form-control-plaintext">
+                            {{ '%.2f'|format(monitor.open_short_price) if monitor.open_short_price else '-' }}
+                        </p>
+                    </div>
+                </div>
+                <div class="row mb-3">
+                    <label class="col-sm-6 col-form-label">做多触发价:</label>
+                    <div class="col-sm-6">
+                        <p class="form-control-plaintext">
+                            {{ '%.2f'|format(monitor.open_long_trigger_price) if monitor.open_long_trigger_price else '-' }}
+                        </p>
+                    </div>
+                </div>
+                <div class="row mb-3">
+                    <label class="col-sm-6 col-form-label">做空触发价:</label>
+                    <div class="col-sm-6">
+                        <p class="form-control-plaintext">
+                            {{ '%.2f'|format(monitor.open_short_trigger_price) if monitor.open_short_trigger_price else '-' }}
+                        </p>
+                    </div>
+                </div>
+            </div>
+        </div>
+        
+        <div class="card mt-3">
+            <div class="card-header">
+                <h3 class="card-title">保证金信息</h3>
+            </div>
+            <div class="card-body">
+                <div class="row mb-3">
+                    <label class="col-sm-7 col-form-label">做多一手保证金:</label>
+                    <div class="col-sm-5">
+                        <p class="form-control-plaintext">
+                            {{ '%.2f'|format(monitor.open_long_margin_per_unit) if monitor.open_long_margin_per_unit else '-' }}
+                        </p>
+                    </div>
+                </div>
+                <div class="row mb-3">
+                    <label class="col-sm-7 col-form-label">做空一手保证金:</label>
+                    <div class="col-sm-5">
+                        <p class="form-control-plaintext">
+                            {{ '%.2f'|format(monitor.open_short_margin_per_unit) if monitor.open_short_margin_per_unit else '-' }}
+                        </p>
+                    </div>
+                </div>
+            </div>
+        </div>
+        
+        <div class="card mt-3">
+            <div class="card-header">
+                <h3 class="card-title">技术分析</h3>
+            </div>
+            <div class="card-body">
+                <div class="row mb-3">
+                    <label class="col-sm-5 col-form-label">K线形态:</label>
+                    <div class="col-sm-7">
+                        <p class="form-control-plaintext">{{ monitor.candle_pattern or '-' }}</p>
+                    </div>
+                </div>
+                <div class="row mb-3">
+                    <label class="col-sm-5 col-form-label">长期趋势:</label>
+                    <div class="col-sm-7">
+                        <p class="form-control-plaintext">{{ monitor.long_trend_name or '-' }}</p>
+                    </div>
+                </div>
+                <div class="row mb-3">
+                    <label class="col-sm-5 col-form-label">中期趋势:</label>
+                    <div class="col-sm-7">
+                        <p class="form-control-plaintext">{{ monitor.mid_trend_name or '-' }}</p>
+                    </div>
+                </div>
+                <div class="row mb-3">
+                    <label class="col-sm-5 col-form-label">相似度评估:</label>
+                    <div class="col-sm-7">
+                        <p class="form-control-plaintext">{{ monitor.similarity_evaluation or '-' }}</p>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock %}

+ 498 - 0
app/templates/monitor/edit.html

@@ -0,0 +1,498 @@
+{% extends 'base.html' %}
+
+{% block title %}编辑监控记录 - 期货数据管理系统{% endblock %}
+
+{% block styles %}
+<!-- 引入 Select2 CSS -->
+<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css">
+<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ttskch/select2-bootstrap4-theme@x.x.x/dist/select2-bootstrap4.min.css">
+<style>
+    .select2-container--bootstrap4 .select2-selection--multiple {
+        min-height: calc(1.5em + .75rem + 2px);
+    }
+</style>
+{% endblock %}
+
+{% block content %}
+<div class="d-flex justify-content-between align-items-center mb-4">
+    <h2>编辑监控记录</h2>
+    <div>
+        <a href="{{ url_for('monitor.detail', id=monitor.id) }}" class="btn btn-info">查看详情</a>
+        <a href="{{ url_for('monitor.index') }}" class="btn btn-secondary">返回列表</a>
+    </div>
+</div>
+
+<div class="card">
+    <div class="card-body">
+        <form id="monitor-edit-form" class="needs-validation" novalidate>
+            <input type="hidden" id="monitor_id" value="{{ monitor.id }}">
+            <div id="monitor-data" 
+                 data-position-mode-id="{{ monitor.position_mode_id or '' }}"
+                 data-candle-pattern-ids="{{ monitor.candle_pattern_ids or '' }}"
+                 data-candle-pattern="{{ monitor.candle_pattern or '' }}"
+                 data-contract-letter="{{ monitor.contract_letter or '' }}" 
+                 style="display: none;"></div>
+            
+            <div class="row">
+                <div class="col-md-6 mb-3">
+                    <label for="future_info_id" class="form-label">期货品种 <span class="text-danger">*</span></label>
+                    <div class="position-relative">
+                        <input type="text" class="form-control" id="future_search_input" name="future_search_input" 
+                               placeholder="输入期货品种名称或代码..." autocomplete="off" required>
+                        <input type="hidden" id="future_info_id" name="future_info_id" required>
+                        <div id="future_search_results" class="dropdown-menu w-100" style="display: none; max-height: 200px; overflow-y: auto;">
+                            <!-- 搜索结果将在这里显示 -->
+                        </div>
+                    </div>
+                    <div class="invalid-feedback">请选择期货品种</div>
+                </div>
+                <div class="col-md-6 mb-3">
+                    <label for="contract" class="form-label">合约代码 <span class="text-danger">*</span></label>
+                    <input type="text" class="form-control" id="contract" name="contract" value="{{ monitor.contract or '' }}" required>
+                    <div class="invalid-feedback">请输入合约代码</div>
+                </div>
+            </div>
+            
+            <div class="row">
+                <div class="col-md-6 mb-3">
+                    <label for="name" class="form-label">名称</label>
+                    <input type="text" class="form-control" id="name" name="name" value="{{ monitor.name or '' }}" readonly required>
+                    <div class="invalid-feedback">名称将根据期货品种自动填充</div>
+                </div>
+                <div class="col-md-6 mb-3">
+                    <label for="market" class="form-label">市场类型</label>
+                    <select class="form-select" id="market" name="market" readonly disabled>
+                        <option value="" selected disabled>请选择市场类型</option>
+                        <option value="0" {% if monitor.market == 0 %}selected{% endif %}>国内</option>
+                        <option value="1" {% if monitor.market == 1 %}selected{% endif %}>国外</option>
+                    </select>
+                    <div class="invalid-feedback">市场类型将根据期货品种自动填充</div>
+                </div>
+            </div>
+            
+            <div class="row">
+                <div class="col-md-6 mb-3">
+                    <label for="opportunity" class="form-label">关注原因</label>
+                    <select class="form-control" id="opportunity" name="opportunity">
+                        <option value="">请选择关注原因</option>
+                        <option value="支撑位" {% if monitor.opportunity == '支撑位' %}selected{% endif %}>支撑位</option>
+                        <option value="压力位" {% if monitor.opportunity == '压力位' %}selected{% endif %}>压力位</option>
+                        <option value="手画支撑位" {% if monitor.opportunity == '手画支撑位' %}selected{% endif %}>手画支撑位</option>
+                        <option value="手画压力位" {% if monitor.opportunity == '手画压力位' %}selected{% endif %}>手画压力位</option>
+                        <option value="5K支撑位" {% if monitor.opportunity == '5K支撑位' %}selected{% endif %}>5K支撑位</option>
+                        <option value="5K压力位" {% if monitor.opportunity == '5K压力位' %}selected{% endif %}>5K压力位</option>
+                        <option value="10K支撑位" {% if monitor.opportunity == '10K支撑位' %}selected{% endif %}>10K支撑位</option>
+                        <option value="10K压力位" {% if monitor.opportunity == '10K压力位' %}selected{% endif %}>10K压力位</option>
+                        <option value="20K支撑位" {% if monitor.opportunity == '20K支撑位' %}selected{% endif %}>20K支撑位</option>
+                        <option value="20K压力位" {% if monitor.opportunity == '20K压力位' %}selected{% endif %}>20K压力位</option>
+                        <option value="30K支撑位" {% if monitor.opportunity == '30K支撑位' %}selected{% endif %}>30K支撑位</option>
+                        <option value="30K压力位" {% if monitor.opportunity == '30K压力位' %}selected{% endif %}>30K压力位</option>
+                    </select>
+                </div>
+                <div class="col-md-6 mb-3">
+                    <label for="status" class="form-label">关注状态</label>
+                    <select class="form-select" id="status" name="status">
+                        <option value="0" {% if monitor.status == 0 %}selected{% endif %}>观察中</option>
+                        <option value="1" {% if monitor.status == 1 %}selected{% endif %}>重点关注</option>
+                        <option value="2" {% if monitor.status == 2 %}selected{% endif %}>已触发</option>
+                        <option value="3" {% if monitor.status == 3 %}selected{% endif %}>已失效</option>
+                    </select>
+                </div>
+            </div>
+            
+            <div class="row">
+                <div class="col-md-6 mb-3">
+                    <label for="key_price" class="form-label">关键价格</label>
+                    <input type="number" step="any" class="form-control" id="key_price" name="key_price" 
+                           value="{% if monitor.key_price %}{{ '%.2f'|format(monitor.key_price) }}{% endif %}">
+                </div>
+                <div class="col-md-6 mb-3">
+                    <label for="latest_price" class="form-label">最新价格</label>
+                    <input type="number" step="any" class="form-control" id="latest_price" name="latest_price"
+                           value="{% if monitor.latest_price %}{{ '%.2f'|format(monitor.latest_price) }}{% endif %}">
+                </div>
+            </div>
+            
+            <div class="row">
+                <div class="col-md-6 mb-3">
+                    <label for="open_price" class="form-label">开仓价格</label>
+                    <input type="number" step="any" class="form-control" id="open_price" name="open_price"
+                           value="{% if monitor.open_price %}{{ '%.2f'|format(monitor.open_price) }}{% endif %}">
+                </div>
+                <div class="col-md-6 mb-3">
+                    <label for="position_mode_id" class="form-label">开仓模式</label>
+                    <select class="form-select" id="position_mode_id" name="position_mode_id">
+                        <option value="">请选择开仓模式</option>
+                    </select>
+                </div>
+            </div>
+            
+            <div class="row">
+                <div class="col-md-12 mb-3">
+                    <label for="candle_pattern" class="form-label">K线形态</label>
+                    <select class="form-control select2" id="candle_pattern" name="candle_pattern" multiple="multiple" data-placeholder="选择K线形态">
+                        <!-- 选项将通过API加载 -->
+                    </select>
+                    <input type="hidden" id="candle_pattern_ids" name="candle_pattern_ids" value="{{ monitor.candle_pattern_ids or '' }}">
+                </div>
+            </div>
+            
+            <div class="row">
+                <div class="col-md-6 mb-3">
+                    <label for="open_long_price" class="form-label">做多开仓价</label>
+                    <input type="number" step="any" class="form-control" id="open_long_price" name="open_long_price" 
+                           value="{% if monitor.open_long_price %}{{ '%.2f'|format(monitor.open_long_price) }}{% endif %}" readonly>
+                </div>
+                <div class="col-md-6 mb-3">
+                    <label for="open_short_price" class="form-label">做空开仓价</label>
+                    <input type="number" step="any" class="form-control" id="open_short_price" name="open_short_price" 
+                           value="{% if monitor.open_short_price %}{{ '%.2f'|format(monitor.open_short_price) }}{% endif %}" readonly>
+                </div>
+            </div>
+            
+            <div class="row">
+                <div class="col-md-6 mb-3">
+                    <label for="open_long_trigger_price" class="form-label">做多触发价</label>
+                    <input type="number" step="any" class="form-control" id="open_long_trigger_price" name="open_long_trigger_price" 
+                           value="{% if monitor.open_long_trigger_price %}{{ '%.2f'|format(monitor.open_long_trigger_price) }}{% endif %}" readonly>
+                </div>
+                <div class="col-md-6 mb-3">
+                    <label for="open_short_trigger_price" class="form-label">做空触发价</label>
+                    <input type="number" step="any" class="form-control" id="open_short_trigger_price" name="open_short_trigger_price" 
+                           value="{% if monitor.open_short_trigger_price %}{{ '%.2f'|format(monitor.open_short_trigger_price) }}{% endif %}" readonly>
+                </div>
+            </div>
+            
+            <div class="row">
+                <div class="col-md-6 mb-3">
+                    <label for="open_long_margin_per_unit" class="form-label">开多一手保证金</label>
+                    <input type="number" step="any" class="form-control" id="open_long_margin_per_unit" name="open_long_margin_per_unit" 
+                           value="{% if monitor.open_long_margin_per_unit %}{{ '%.2f'|format(monitor.open_long_margin_per_unit) }}{% endif %}" readonly>
+                </div>
+                <div class="col-md-6 mb-3">
+                    <label for="open_short_margin_per_unit" class="form-label">开空一手保证金</label>
+                    <input type="number" step="any" class="form-control" id="open_short_margin_per_unit" name="open_short_margin_per_unit" 
+                           value="{% if monitor.open_short_margin_per_unit %}{{ '%.2f'|format(monitor.open_short_margin_per_unit) }}{% endif %}" readonly>
+                </div>
+            </div>
+            
+            <div class="form-group mt-4">
+                <button type="submit" class="btn btn-primary">保存修改</button>
+                <button type="reset" class="btn btn-secondary">重置</button>
+                <a href="{{ url_for('monitor.detail', id=monitor.id) }}" class="btn btn-info">取消</a>
+            </div>
+        </form>
+    </div>
+</div>
+{% endblock %}
+
+{% block scripts %}
+<!-- 引入 jQuery -->
+<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
+<!-- 引入 Select2 JS -->
+<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
+<script>
+document.addEventListener('DOMContentLoaded', function() {
+    const form = document.getElementById('monitor-edit-form');
+    const futureSearchInput = document.getElementById('future_search_input');
+    const futureIdInput = document.getElementById('future_info_id');
+    const searchResults = document.getElementById('future_search_results');
+    const contractInput = document.getElementById('contract');
+    const nameInput = document.getElementById('name');
+    const marketSelect = document.getElementById('market');
+    const monitorId = document.getElementById('monitor_id').value;
+    
+    // 从DOM数据属性获取监控数据
+    const monitorDataElement = document.getElementById('monitor-data');
+    const monitorData = {
+        positionModeId: monitorDataElement.getAttribute('data-position-mode-id') || null,
+        candlePatternIds: monitorDataElement.getAttribute('data-candle-pattern-ids') || '',
+        candlePattern: monitorDataElement.getAttribute('data-candle-pattern') || '',
+        contractLetter: monitorDataElement.getAttribute('data-contract-letter') || ''
+    };
+
+    let searchTimeout;
+    let currentFutureData = null;
+    
+    // 存储开仓模式数据
+    let positionModesData = [];
+    
+    // 存储K线形态选择顺序
+    let candlePatternOrder = [];
+    let candlePatternIdOrder = [];
+
+    // 加载开仓模式选项
+    function loadPositionModes() {
+        fetch('/monitor/api/position_modes')
+            .then(response => response.json())
+            .then(data => {
+                if (data.code === 0) {
+                    positionModesData = data.data; // 存储完整数据
+                    const select = document.getElementById('position_mode_id');
+                    select.innerHTML = '<option value="">请选择开仓模式</option>';
+                    data.data.forEach(mode => {
+                        const option = document.createElement('option');
+                        option.value = mode.id;
+                        option.textContent = mode.name;
+                        if (monitorData.positionModeId && mode.id == monitorData.positionModeId) {
+                            option.selected = true;
+                        }
+                        select.appendChild(option);
+                    });
+                } else {
+                    console.error('加载开仓模式失败:', data.msg);
+                }
+            })
+            .catch(error => {
+                console.error('加载开仓模式出错:', error);
+            });
+    }
+
+    // 加载K线形态选项
+    function loadCandlePatterns() {
+        fetch('/monitor/api/candle_patterns')
+            .then(response => response.json())
+            .then(data => {
+                if (data.code === 0) {
+                    const select = $('#candle_pattern');
+                    select.empty(); // 清空现有选项
+                    data.data.forEach(pattern => {
+                        const option = new Option(pattern.name, pattern.id, false, false);
+                        select.append(option);
+                    });
+                    
+                    // 初始化Select2
+                    select.select2({
+                        theme: 'bootstrap4',
+                        placeholder: '选择K线形态',
+                        allowClear: true,
+                        closeOnSelect: false // 选择后不关闭,方便多选
+                    });
+                    
+                    // 设置已选择的值和顺序
+                    const existingIds = monitorData.candlePatternIds || $('#candle_pattern_ids').val();
+                    if (existingIds) {
+                        const idsArray = existingIds.split(',').map(id => id.trim()).filter(id => id);
+                        
+                        // 初始化K线形态顺序(基于数据库中保存的顺序)
+                        if (monitorData.candlePattern) {
+                            candlePatternOrder = monitorData.candlePattern.split('+').map(name => name.trim()).filter(name => name);
+                            candlePatternIdOrder = idsArray; // 按相同顺序保存ID
+                        }
+                        
+                        // 设置选中的值(这会触发select2:select事件,但我们需要避免重复添加)
+                        select.val(idsArray).trigger('change');
+                    }
+                } else {
+                    console.error('加载K线形态失败:', data.msg);
+                }
+            })
+            .catch(error => {
+                console.error('加载K线形态出错:', error);
+            });
+    }
+
+    // K线形态选择事件 - 跟踪真实的用户选择顺序
+    $('#candle_pattern').on('select2:select', function(e) {
+        const selectedData = e.params.data;
+        const selectedId = selectedData.id;
+        const selectedText = selectedData.text;
+        
+        // 只有在用户主动选择时才添加到顺序数组(避免初始化时重复添加)
+        if (!candlePatternIdOrder.includes(selectedId)) {
+            candlePatternIdOrder.push(selectedId);
+            candlePatternOrder.push(selectedText);
+            
+            // 更新隐藏的ID字段
+            $('#candle_pattern_ids').val(candlePatternIdOrder.join(','));
+            console.log('选择的K线形态ID(按用户点击顺序):', candlePatternIdOrder);
+            console.log('选择的K线形态名称(按用户点击顺序):', candlePatternOrder);
+        }
+    });
+    
+    $('#candle_pattern').on('select2:unselect', function(e) {
+        const unselectedData = e.params.data;
+        const unselectedId = unselectedData.id;
+        const unselectedText = unselectedData.text;
+        
+        // 从用户选择顺序数组中移除
+        const idIndex = candlePatternIdOrder.indexOf(unselectedId);
+        const textIndex = candlePatternOrder.indexOf(unselectedText);
+        
+        if (idIndex > -1) {
+            candlePatternIdOrder.splice(idIndex, 1);
+        }
+        if (textIndex > -1) {
+            candlePatternOrder.splice(textIndex, 1);
+        }
+        
+        // 更新隐藏的ID字段
+        $('#candle_pattern_ids').val(candlePatternIdOrder.join(','));
+        console.log('取消选择后的K线形态ID(按用户点击顺序):', candlePatternIdOrder);
+        console.log('取消选择后的K线形态名称(按用户点击顺序):', candlePatternOrder);
+    });
+
+    // 表单提交处理
+    form.addEventListener('submit', function(event) {
+        event.preventDefault();
+
+        if (!form.checkValidity()) {
+            event.stopPropagation();
+            form.classList.add('was-validated');
+            return;
+        }
+
+        // 收集表单数据
+        const formData = {
+            contract: contractInput.value.trim(),
+            name: nameInput.value.trim(),
+            market: marketSelect.value !== '' ? parseInt(marketSelect.value) : null,
+            status: parseInt(document.getElementById('status').value),
+            opportunity: document.getElementById('opportunity').value || null,
+            key_price: document.getElementById('key_price').value ? parseFloat(document.getElementById('key_price').value) : null,
+            open_price: document.getElementById('open_price').value ? parseFloat(document.getElementById('open_price').value) : null,
+            latest_price: document.getElementById('latest_price').value ? parseFloat(document.getElementById('latest_price').value) : null,
+            position_mode_id: document.getElementById('position_mode_id').value ? parseInt(document.getElementById('position_mode_id').value) : null,
+            candle_pattern_ids: $('#candle_pattern_ids').val() || null,
+            candle_pattern: candlePatternOrder.join('+') || null,
+            open_long_margin_per_unit: document.getElementById('open_long_margin_per_unit').value ? parseFloat(document.getElementById('open_long_margin_per_unit').value) : null,
+            open_short_margin_per_unit: document.getElementById('open_short_margin_per_unit').value ? parseFloat(document.getElementById('open_short_margin_per_unit').value) : null
+        };
+
+        console.log('准备提交的编辑数据:', formData);
+
+        // 发送更新请求
+        fetch(`/monitor/update/${monitorId}`, {
+            method: 'PUT',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify(formData)
+        })
+        .then(response => response.json())
+        .then(data => {
+            console.log('服务器响应:', data);
+            if (data.code === 0) {
+                // 直接跳转,不需要手动关闭弹窗
+                window.location.href = `/monitor/`;
+            } else {
+                alert('更新失败:' + (data.msg || '未知错误'));
+            }
+        })
+        .catch(error => {
+            console.error('Error:', error);
+            alert('发生错误,请查看控制台了解详情');
+        });
+    });
+
+    // 根据合约字母加载保证金信息
+    function loadMarginInfo(contractLetter) {
+        if (!contractLetter) return;
+        
+        fetch(`/monitor/api/margin_info/${contractLetter}`)
+            .then(response => response.json())
+            .then(data => {
+                if (data.code === 0) {
+                    const marginInfo = data.data;
+                    // 计算保证金(需要价格信息)
+                    calculateMargins(marginInfo);
+                } else {
+                    console.warn('获取保证金信息失败:', data.msg);
+                }
+            })
+            .catch(error => {
+                console.error('获取保证金信息出错:', error);
+            });
+    }
+
+    // 设置保证金(直接从future_info获取固定金额)
+    function calculateMargins(marginInfo) {
+        // 直接使用从future_info查询到的固定保证金金额
+        if (marginInfo.long_margin_amount) {
+            document.getElementById('open_long_margin_per_unit').value = marginInfo.long_margin_amount.toFixed(2);
+        }
+        
+        if (marginInfo.short_margin_amount) {
+            document.getElementById('open_short_margin_per_unit').value = marginInfo.short_margin_amount.toFixed(2);
+        }
+    }
+
+    // 初始化期货品种字段
+    function initializeFutureField() {
+        const contractCode = document.getElementById('contract').value;
+        const name = document.getElementById('name').value;
+        
+        if (contractCode && name) {
+            // 提取合约字母:去掉右侧4位数字
+            const match = contractCode.match(/^([A-Za-z]+)(\d{4})$/);
+            const contractLetter = match ? match[1] : contractCode.replace(/\d+/g, ''); // 后备方案
+            
+            // 通过合约字母查询期货品种信息
+            if (contractLetter) {
+                fetch(`/api/future_info/search?q=${encodeURIComponent(contractLetter)}&limit=1`)
+                    .then(response => response.json())
+                    .then(data => {
+                        if (data.code === 0 && data.data && data.data.length > 0) {
+                            const future = data.data[0];
+                            futureSearchInput.value = future.display_text;
+                            futureIdInput.value = future.id;
+                            currentFutureData = future;
+                            console.log('已自动填充期货品种:', future);
+                            
+                            // 自动计算保证金
+                            if (future.contract_letter) {
+                                loadMarginInfo(future.contract_letter);
+                            }
+                        } else {
+                            // 如果API查询失败,使用合约代码和名称拼接
+                            futureSearchInput.value = `${contractCode} - ${name}`;
+                            console.log('使用合约代码和名称拼接期货品种字段');
+                            
+                            // 尝试通过合约字母直接计算保证金
+                            if (contractLetter) {
+                                loadMarginInfo(contractLetter);
+                            }
+                        }
+                    })
+                    .catch(error => {
+                        console.error('查询期货品种失败:', error);
+                        futureSearchInput.value = `${contractCode} - ${name}`;
+                        console.log('查询失败,使用合约代码和名称拼接');
+                    });
+            }
+        }
+    }
+
+    // 开仓模式改变时自动填入相应的触发价到开仓价格
+    document.getElementById('position_mode_id').addEventListener('change', function() {
+        const selectedModeId = parseInt(this.value);
+        if (selectedModeId) {
+            const selectedMode = positionModesData.find(mode => mode.id === selectedModeId);
+            if (selectedMode) {
+                // 根据方向自动填入相应的触发价
+                if (selectedMode.direction === 0) {
+                    // 做多方向,填入做多触发价
+                    const longTriggerPrice = document.getElementById('open_long_trigger_price').value;
+                    if (longTriggerPrice) {
+                        document.getElementById('open_price').value = longTriggerPrice;
+                    }
+                } else if (selectedMode.direction === 1) {
+                    // 做空方向,填入做空触发价
+                    const shortTriggerPrice = document.getElementById('open_short_trigger_price').value;
+                    if (shortTriggerPrice) {
+                        document.getElementById('open_price').value = shortTriggerPrice;
+                    }
+                }
+                // direction为NULL的情况(虚拟、执行、无)不自动填入
+                console.log('开仓模式选择:', selectedMode.name, '方向:', selectedMode.direction);
+            }
+        }
+    });
+
+    // 初始化加载数据
+    loadPositionModes();
+    loadCandlePatterns();
+    initializeFutureField();
+});
+</script>
+{% endblock %}

+ 121 - 0
app/templates/monitor/import.html

@@ -0,0 +1,121 @@
+{% extends 'base.html' %}
+
+{% block title %}导入监控记录 - 期货数据管理系统{% endblock %}
+
+{% block content %}
+<div class="d-flex justify-content-between align-items-center mb-4">
+    <h2>导入监控记录</h2>
+    <a href="{{ url_for('monitor.index') }}" class="btn btn-secondary">返回列表</a>
+</div>
+
+<div class="card">
+    <div class="card-body">
+        <div class="row mb-4">
+            <div class="col-md-12">
+                <p>请按照以下步骤操作:</p>
+                <ol>
+                    <li>下载导入模板</li>
+                    <li>按照模板格式填写数据</li>
+                    <li>上传Excel文件</li>
+                </ol>
+                <div class="alert alert-info">
+                    <strong>提示:</strong> 必填字段包括"合约"、"名称"和"市场(0-国内,1-国外)"。
+                </div>
+            </div>
+        </div>
+        
+        <div class="row mb-4">
+            <div class="col-md-6">
+                <button id="download-template" class="btn btn-primary">
+                    <i class="fas fa-download"></i> 下载导入模板
+                </button>
+            </div>
+        </div>
+        
+        <form id="import-form" enctype="multipart/form-data">
+            <div class="row mb-4">
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="file">选择Excel文件</label>
+                        <input type="file" class="form-control" id="file" name="file" accept=".xlsx" required>
+                    </div>
+                </div>
+            </div>
+            
+            <div class="form-group">
+                <button type="submit" class="btn btn-success">
+                    <i class="fas fa-upload"></i> 导入数据
+                </button>
+            </div>
+        </form>
+        
+        <div id="result" class="mt-4" style="display: none;">
+        </div>
+    </div>
+</div>
+
+<script>
+document.addEventListener('DOMContentLoaded', function() {
+    const form = document.getElementById('import-form');
+    const resultDiv = document.getElementById('result');
+    const downloadBtn = document.getElementById('download-template');
+    
+    // 处理下载模板按钮点击
+    downloadBtn.addEventListener('click', function(e) {
+        // 禁用按钮防止重复点击
+        downloadBtn.disabled = true;
+        
+        // 创建一个隐藏的iframe来处理下载
+        const iframe = document.createElement('iframe');
+        iframe.style.display = 'none';
+        iframe.src = "{{ url_for('monitor.get_template') }}";
+        document.body.appendChild(iframe);
+        
+        // 3秒后重新启用按钮
+        setTimeout(() => {
+            downloadBtn.disabled = false;
+            document.body.removeChild(iframe);
+        }, 3000);
+    });
+    
+    form.addEventListener('submit', function(e) {
+        e.preventDefault();
+        
+        const formData = new FormData(form);
+        
+        // 显示加载状态
+        resultDiv.innerHTML = '<div class="alert alert-info">正在导入数据,请稍候...</div>';
+        resultDiv.style.display = 'block';
+        
+        fetch('/api/monitor/import', {
+            method: 'POST',
+            body: formData
+        })
+        .then(response => response.json())
+        .then(data => {
+            if (data.code === 0) {
+                // 成功导入
+                let html = `<div class="alert alert-success">${data.msg}</div>`;
+                
+                if (data.data.error_count > 0) {
+                    html += '<div class="alert alert-warning"><strong>导入过程中出现以下错误:</strong><ul>';
+                    data.data.error_messages.forEach(msg => {
+                        html += `<li>${msg}</li>`;
+                    });
+                    html += '</ul></div>';
+                }
+                
+                resultDiv.innerHTML = html;
+            } else {
+                // 导入失败
+                resultDiv.innerHTML = `<div class="alert alert-danger">${data.msg}</div>`;
+            }
+        })
+        .catch(error => {
+            console.error('Error:', error);
+            resultDiv.innerHTML = '<div class="alert alert-danger">导入失败,请查看控制台了解详情</div>';
+        });
+    });
+});
+</script>
+{% endblock %} 

+ 571 - 0
app/templates/monitor/index.html

@@ -0,0 +1,571 @@
+{% extends 'base.html' %}
+
+{% block title %}监控记录 - 期货数据管理系统{% endblock %}
+
+{% block styles %}
+<style>
+    /* 表格水平滚动样式 */
+    .table-responsive {
+        border: 1px solid #dee2e6;
+        border-radius: 0.375rem;
+        overflow-x: auto;
+        max-height: 70vh; /* 限制最大高度,让表头可以固定 */
+    }
+    .table-responsive::-webkit-scrollbar { height: 8px; }
+    .table-responsive::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; }
+    .table-responsive::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 4px; }
+    .table-responsive::-webkit-scrollbar-thumb:hover { background: #a8a8a8; }
+    
+    /* 列冻结样式 */
+    .sticky-table { position: relative; width: 100%; }
+    .sticky-table thead th { position: sticky; top: 0; z-index: 1; background-color: #f8f9fa; }
+    .sticky-table td, .sticky-table th { white-space: nowrap; }
+
+    /* 冻结列的 z-index 和 position */
+    .sticky-table .sticky-left,
+    .sticky-table .sticky-right {
+        position: sticky;
+        z-index: 2; /* 高于普通表头 */
+    }
+    
+    /* 冻结列和表头的不透明背景 */
+    .sticky-table th.sticky-left,
+    .sticky-table th.sticky-right {
+        background-color: #f8f9fa; /* 表头背景色 */
+    }
+
+    .sticky-table td.sticky-left,
+    .sticky-table td.sticky-right {
+        background-color: #ffffff; /* 单元格背景色 */
+    }
+
+    /* 修复斑马纹表格下的背景色问题 */
+    .table-striped > tbody > tr:nth-of-type(odd) > td.sticky-left,
+    .table-striped > tbody > tr:nth-of-type(odd) > td.sticky-right {
+        background-color: #f9f9f9; /* 斑马纹行背景色 */
+    }
+
+    /* 左侧冻结列位置 */
+    .sticky-left:nth-child(1) { left: 0; } /* 合约 */
+    .sticky-left:nth-child(2) { left: 120px; } /* 名称 */
+
+    /* 右侧冻结列 */
+    .sticky-right {
+        right: 0;
+        border-left: 2px solid #dee2e6;
+    }
+    
+    /* 左侧冻结列分隔线 */
+    th.sticky-left:nth-child(2),
+    td.sticky-left:nth-child(2) {
+        border-right: 2px solid #dee2e6;
+    }
+    
+    /* 开仓模式行背景色 */
+    .row-virtual {
+        background-color: #e3f2fd !important; /* 浅蓝色 */
+    }
+    
+    .row-virtual td {
+        background-color: #e3f2fd !important;
+    }
+    
+    .row-execute {
+        background-color: #e8f5e8 !important; /* 中等浅绿色 */
+    }
+    
+    .row-execute td {
+        background-color: #e8f5e8 !important;
+    }
+    
+    /* 确保固定列也应用背景色 */
+    .row-virtual .sticky-left,
+    .row-virtual .sticky-right {
+        background-color: #e3f2fd !important;
+    }
+    
+    .row-execute .sticky-left,
+    .row-execute .sticky-right {
+        background-color: #e8f5e8 !important;
+    }
+    
+    /* 确保虚拟和执行样式始终生效,不被斑马纹覆盖 */
+    .table-striped > tbody > .row-virtual,
+    .table-striped > tbody > .row-virtual:nth-of-type(odd) {
+        background-color: #e3f2fd !important;
+    }
+    
+    .table-striped > tbody > .row-virtual > td,
+    .table-striped > tbody > .row-virtual:nth-of-type(odd) > td {
+        background-color: #e3f2fd !important;
+    }
+    
+    .table-striped > tbody > .row-execute,
+    .table-striped > tbody > .row-execute:nth-of-type(odd) {
+        background-color: #e8f5e8 !important;
+    }
+    
+    .table-striped > tbody > .row-execute > td,
+    .table-striped > tbody > .row-execute:nth-of-type(odd) > td {
+        background-color: #e8f5e8 !important;
+    }
+</style>
+{% endblock %}
+
+{% block content %}
+<div class="container-fluid">
+    <div class="card">
+        <div class="card-header">
+            <div class="row mb-0 align-items-center">
+                <div class="col-md-6">
+                    <h2>监控记录</h2>
+                </div>
+                <div class="col-md-6 text-end">
+                    <a href="{{ url_for('monitor.add') }}" class="btn btn-primary btn-sm me-1">添加监控记录</a>
+                    <a href="{{ url_for('monitor.import_view') }}" class="btn btn-success btn-sm">导入监控记录</a>
+                </div>
+            </div>
+        </div>
+        <div class="card-body">
+            <form method="get" class="mb-3">
+                <div class="row justify-content-start">
+                    <div class="col-md-auto">
+                        <div class="dropdown">
+                            <button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="statusFilter" data-bs-toggle="dropdown" aria-expanded="false">
+                                状态筛选
+                            </button>
+                            <div class="dropdown-menu p-3" aria-labelledby="statusFilter" style="min-width: 200px;" onclick="event.stopPropagation();">
+                                <div class="form-check">
+                                    <input class="form-check-input" type="checkbox" value="0" id="status0" name="status" checked>
+                                    <label class="form-check-label" for="status0">观察中</label>
+                                </div>
+                                <div class="form-check">
+                                    <input class="form-check-input" type="checkbox" value="1" id="status1" name="status" checked>
+                                    <label class="form-check-label" for="status1">重点关注</label>
+                                </div>
+                                <div class="form-check">
+                                    <input class="form-check-input" type="checkbox" value="2" id="status2" name="status" checked>
+                                    <label class="form-check-label" for="status2">已触发</label>
+                                </div>
+                                <div class="form-check">
+                                    <input class="form-check-input" type="checkbox" value="3" id="status3" name="status">
+                                    <label class="form-check-label" for="status3">已失效</label>
+                                </div>
+                                <hr class="my-2">
+                                <button type="button" class="btn btn-sm btn-primary" onclick="applyStatusFilter()">应用筛选</button>
+                                <button type="button" class="btn btn-sm btn-secondary ms-1" onclick="resetStatusFilter()">重置</button>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="col-md-auto">
+                        <select name="market" class="form-select form-select-sm">
+                            <option value="">所有市场</option>
+                            <option value="0" {% if request.args.get('market') == '0' %}selected{% endif %}>国内</option>
+                            <option value="1" {% if request.args.get('market') == '1' %}selected{% endif %}>国际</option>
+                        </select>
+                    </div>
+                    <div class="col-md-auto">
+                        <button type="submit" class="btn btn-secondary btn-sm">筛选</button>
+                    </div>
+                </div>
+            </form>
+
+            <div class="table-responsive">
+                <table class="table table-striped table-bordered table-sm sticky-table" style="min-width: 1400px;">
+                    <thead>
+                        <tr>
+                            <th class="sticky-left" style="min-width: 120px;">合约</th>
+                            <th class="sticky-left" style="min-width: 150px;">名称</th>
+                            <th style="min-width: 120px;">开仓模式</th>
+                            <th style="min-width: 150px;">K线形态</th>
+                            <th style="min-width: 100px;">开仓价格</th>
+                            <th style="min-width: 100px;">关键价格</th>
+                            <th style="min-width: 120px;">做多保证金</th>
+                            <th style="min-width: 120px;">做空保证金</th>
+                            <th style="min-width: 100px;">状态</th>
+                            <th class="sticky-right" style="min-width: 120px;">操作</th>
+                        </tr>
+                    </thead>
+                    <tbody id="monitor-table-body">
+                        <tr>
+                            <td colspan="10" class="text-center">正在加载...</td>
+                        </tr>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
+<!-- 自定义确认对话框 -->
+<div class="modal fade" id="confirmModal" tabindex="-1" aria-labelledby="confirmModalLabel" aria-hidden="true">
+    <div class="modal-dialog modal-dialog-centered">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title" id="confirmModalLabel">确认操作</h5>
+                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+            </div>
+            <div class="modal-body" id="confirmModalBody">
+                确定要执行此操作吗?
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
+                <button type="button" class="btn btn-primary" id="confirmModalOK">确定</button>
+            </div>
+        </div>
+    </div>
+</div>
+
+<!-- 错误提示模态框 -->
+<div class="modal fade" id="errorModal" tabindex="-1" aria-labelledby="errorModalLabel" aria-hidden="true">
+    <div class="modal-dialog modal-dialog-centered">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title text-danger" id="errorModalLabel">操作失败</h5>
+                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+            </div>
+            <div class="modal-body" id="errorModalBody">
+                操作失败,请稍后重试
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-primary" data-bs-dismiss="modal">确定</button>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock %}
+
+{% block scripts %}
+<script>
+document.addEventListener('DOMContentLoaded', function() {
+    const tableBody = document.getElementById('monitor-table-body');
+    
+    // 自定义确认对话框函数
+    function showConfirmDialog(message, onConfirm) {
+        const modal = new bootstrap.Modal(document.getElementById('confirmModal'));
+        const body = document.getElementById('confirmModalBody');
+        const okBtn = document.getElementById('confirmModalOK');
+        
+        body.textContent = message;
+        
+        // 移除旧的事件监听器
+        okBtn.replaceWith(okBtn.cloneNode(true));
+        const newOkBtn = document.getElementById('confirmModalOK');
+        
+        // 添加确认事件
+        newOkBtn.addEventListener('click', function() {
+            modal.hide();
+            onConfirm();
+        });
+        
+        // 监听回车键
+        const keyHandler = function(e) {
+            if (e.key === 'Enter') {
+                e.preventDefault();
+                modal.hide();
+                document.removeEventListener('keydown', keyHandler);
+                onConfirm();
+            }
+        };
+        
+        document.addEventListener('keydown', keyHandler);
+        
+        // 模态框关闭时清理事件监听器
+        document.getElementById('confirmModal').addEventListener('hidden.bs.modal', function() {
+            document.removeEventListener('keydown', keyHandler);
+        }, { once: true });
+        
+        modal.show();
+        
+        // 聚焦到确定按钮,以便回车键生效
+        setTimeout(() => {
+            newOkBtn.focus();
+        }, 150);
+    }
+    
+    // 显示错误对话框
+    function showErrorDialog(message) {
+        const modal = new bootstrap.Modal(document.getElementById('errorModal'));
+        const body = document.getElementById('errorModalBody');
+        const okBtn = document.querySelector('#errorModal .btn-primary');
+        
+        body.textContent = message;
+        
+        // 监听回车键
+        const keyHandler = function(e) {
+            if (e.key === 'Enter') {
+                e.preventDefault();
+                modal.hide();
+                document.removeEventListener('keydown', keyHandler);
+            }
+        };
+        
+        document.addEventListener('keydown', keyHandler);
+        
+        // 模态框关闭时清理事件监听器
+        document.getElementById('errorModal').addEventListener('hidden.bs.modal', function() {
+            document.removeEventListener('keydown', keyHandler);
+        }, { once: true });
+        
+        modal.show();
+        
+        // 聚焦到确定按钮,以便回车键生效
+        setTimeout(() => {
+            okBtn.focus();
+        }, 150);
+    }
+    
+    // 将函数附加到window对象,以便在其他函数中使用
+    window.showConfirmDialog = showConfirmDialog;
+    window.showErrorDialog = showErrorDialog;
+    
+    // 状态映射
+    const statusMap = {
+        0: '观察中',
+        1: '重点关注', 
+        2: '已触发',
+        3: '已失效'
+    };
+    
+    // 市场映射
+    const marketMap = {
+        0: '国内',
+        1: '国外'
+    };
+
+    // 开仓模式数据缓存
+    let positionModes = [];
+    
+    // 加载开仓模式数据
+    function loadPositionModes() {
+        return fetch('/monitor/api/position_modes')
+            .then(response => response.json())
+            .then(data => {
+                if (data.code === 0) {
+                    positionModes = data.data;
+                    console.log('开仓模式数据加载成功:', positionModes);
+                } else {
+                    console.error('加载开仓模式失败:', data.msg);
+                }
+            })
+            .catch(error => {
+                console.error('加载开仓模式出错:', error);
+            });
+    }
+    
+    // 获取开仓模式名称
+    function getPositionModeName(modeId) {
+        if (!modeId) return '-';
+        const mode = positionModes.find(m => m.id === modeId);
+        return mode ? mode.name : '-';
+    }
+    
+    // 加载监控记录数据
+    function loadMonitorRecords() {
+        // 获取筛选参数
+        const selectedStatuses = getSelectedStatuses();
+        const urlParams = new URLSearchParams(window.location.search);
+        const market = urlParams.get('market') || '';
+        
+        // 构建查询参数
+        const params = new URLSearchParams();
+        selectedStatuses.forEach(status => params.append('status', status));
+        if (market) params.append('market', market);
+        
+        console.log('正在加载监控记录,参数:', params.toString());
+        
+        // 如果开仓模式数据未加载,先加载
+        const loadPromise = positionModes.length > 0 ? Promise.resolve() : loadPositionModes();
+        
+        loadPromise.then(() => {
+            return fetch(`/monitor/list?${params.toString()}`);
+        })
+        .then(response => response.json())
+        .then(data => {
+            console.log('监控记录API响应:', data);
+            if (data.code === 0) {
+                renderMonitorTable(data.data);
+            } else {
+                console.error('获取监控记录失败:', data.msg);
+                tableBody.innerHTML = `<tr><td colspan="10" class="text-center text-danger">加载失败: ${data.msg}</td></tr>`;
+            }
+        })
+        .catch(error => {
+            console.error('获取监控记录出错:', error);
+            tableBody.innerHTML = '<tr><td colspan="10" class="text-center text-danger">网络错误,请刷新重试</td></tr>';
+        });
+    }
+    
+    // 渲染监控记录表格
+    function renderMonitorTable(monitors) {
+        console.log('渲染监控记录数据:', monitors);
+        
+        if (!monitors || monitors.length === 0) {
+            tableBody.innerHTML = '<tr><td colspan="10" class="text-center text-muted">暂无监控记录</td></tr>';
+            return;
+        }
+        
+        const rows = monitors.map(monitor => {
+            const statusText = statusMap[monitor.status] || '未知状态';
+            const positionModeText = getPositionModeName(monitor.position_mode_id);
+            
+            // 根据开仓模式确定行样式类
+            let rowClass = '';
+            if (positionModeText && positionModeText !== '无' && positionModeText.includes('虚拟')) {
+                rowClass = 'row-virtual';
+            } else if (positionModeText && positionModeText !== '无' && positionModeText.includes('执行')) {
+                rowClass = 'row-execute';
+            }
+            
+            return `
+                <tr${rowClass ? ' class="' + rowClass + '"' : ''}>
+                    <td class="sticky-left">${monitor.contract || '-'}</td>
+                    <td class="sticky-left">${monitor.name || '-'}</td>
+                    <td>${positionModeText}</td>
+                    <td title="${monitor.candle_pattern || ''}">${truncateText(monitor.candle_pattern, 15)}</td>
+                    <td>${formatPrice(monitor.open_price)}</td>
+                    <td>${formatPrice(monitor.key_price)}</td>
+                    <td>${formatPrice(monitor.open_long_margin_per_unit)}</td>
+                    <td>${formatPrice(monitor.open_short_margin_per_unit)}</td>
+                    <td>
+                        <span class="badge ${getStatusBadgeClass(monitor.status)}">${statusText}</span>
+                    </td>
+                    <td class="sticky-right">
+                        <div class="btn-group dropstart">
+                            <button type="button" class="btn btn-secondary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
+                                操作
+                            </button>
+                            <ul class="dropdown-menu">
+                                <li><button class="dropdown-item" onclick="viewDetail(${monitor.id})">查看详情</button></li>
+                                <li><button class="dropdown-item" onclick="editMonitor(${monitor.id})">编辑</button></li>
+                                ${monitor.status !== 3 ? '<li><button class="dropdown-item text-warning" onclick="invalidateMonitor(' + monitor.id + ')">失效</button></li>' : ''}
+                                <li><hr class="dropdown-divider"></li>
+                                <li><button class="dropdown-item text-danger" onclick="deleteMonitor(${monitor.id})">删除</button></li>
+                            </ul>
+                        </div>
+                    </td>
+                </tr>
+            `;
+        }).join('');
+        
+        tableBody.innerHTML = rows;
+    }
+    
+    // 格式化价格显示
+    function formatPrice(price) {
+        if (price === null || price === undefined || price === '') {
+            return '-';
+        }
+        return parseFloat(price).toFixed(2);
+    }
+    
+    // 截断长文本
+    function truncateText(text, maxLength) {
+        if (!text) return '-';
+        return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
+    }
+    
+    // 获取状态徽章的CSS类
+    function getStatusBadgeClass(status) {
+        const classes = {
+            0: 'bg-secondary',  // 观察中
+            1: 'bg-warning',    // 重点关注
+            2: 'bg-success',    // 已触发
+            3: 'bg-danger'      // 已失效
+        };
+        return classes[status] || 'bg-light';
+    }
+    
+    // 获取选中的状态值
+    function getSelectedStatuses() {
+        const checkboxes = document.querySelectorAll('input[name="status"]:checked');
+        return Array.from(checkboxes).map(cb => cb.value);
+    }
+    
+    // 应用状态筛选
+    window.applyStatusFilter = function() {
+        loadMonitorRecords();
+        // 关闭dropdown
+        const dropdown = bootstrap.Dropdown.getInstance(document.getElementById('statusFilter'));
+        if (dropdown) dropdown.hide();
+    };
+    
+    // 重置状态筛选(默认选中除"已失效"外的所有状态)
+    window.resetStatusFilter = function() {
+        document.getElementById('status0').checked = true; // 观察中
+        document.getElementById('status1').checked = true; // 重点关注  
+        document.getElementById('status2').checked = true; // 已触发
+        document.getElementById('status3').checked = false; // 已失效
+        loadMonitorRecords();
+    };
+
+    // 查看详情
+    window.viewDetail = function(id) {
+        // 实现查看详情逻辑
+        console.log('查看监控详情:', id);
+        // 这里可以跳转到详情页面或打开模态框
+        window.location.href = `/monitor/detail/${id}`;
+    };
+    
+    // 编辑监控
+    window.editMonitor = function(id) {
+        console.log('编辑监控记录:', id);
+        // 跳转到编辑页面
+        window.location.href = `/monitor/edit/${id}`;
+    };
+    
+    // 失效监控记录
+    window.invalidateMonitor = function(id) {
+        showConfirmDialog('确定要将这条监控记录标记为失效吗?', function() {
+            fetch(`/monitor/invalidate/${id}`, {
+                method: 'PUT',
+                headers: {
+                    'Content-Type': 'application/json'
+                }
+            })
+            .then(response => response.json())
+            .then(data => {
+                if (data.code === 0) {
+                    // 成功时不显示提示,直接刷新数据
+                    loadMonitorRecords();
+                } else {
+                    // 失败时显示错误对话框
+                    showErrorDialog('标记失效失败:' + data.msg);
+                }
+            })
+            .catch(error => {
+                console.error('标记失效失败:', error);
+                showErrorDialog('标记失效失败,请稍后重试');
+            });
+        });
+    };
+
+    // 删除监控
+    window.deleteMonitor = function(id) {
+        showConfirmDialog('确定要删除这条监控记录吗?此操作不可撤销!', function() {
+            fetch(`/monitor/delete/${id}`, {
+                method: 'DELETE'
+            })
+            .then(response => response.json())
+            .then(data => {
+                if (data.code === 0) {
+                    // 成功时不显示提示,直接刷新数据
+                    loadMonitorRecords();
+                } else {
+                    // 失败时显示错误对话框
+                    showErrorDialog('删除失败:' + data.msg);
+                }
+            })
+            .catch(error => {
+                console.error('删除失败:', error);
+                showErrorDialog('删除失败,请稍后重试');
+            });
+        });
+    };
+    
+    // 初始化时先加载开仓模式数据,再加载监控记录
+    loadPositionModes().then(() => {
+        loadMonitorRecords();
+    });
+});
+</script>
+{% endblock %} 

+ 96 - 0
app/templates/trade/detail.html

@@ -0,0 +1,96 @@
+{% extends "base.html" %}
+
+{% block title %}交易汇总详情 - {{ trade.name }} ({{ trade.contract_code }}){% endblock %}
+
+{% block content %}
+<div class="container mt-4">
+    <div class="card">
+        <div class="card-header">
+            <h4 class="card-title mb-0">交易汇总详情</h4>
+        </div>
+        <div class="card-body">
+            <dl class="row">
+                <dt class="col-sm-3">ID</dt>
+                <dd class="col-sm-9">{{ trade.id }}</dd>
+
+                <dt class="col-sm-3">合约代码</dt>
+                <dd class="col-sm-9">{{ trade.contract_code }}</dd>
+
+                <dt class="col-sm-3">名称</dt>
+                <dd class="col-sm-9">{{ trade.name }}</dd>
+
+                <dt class="col-sm-3">账户</dt>
+                <dd class="col-sm-9">{{ trade.account }}</dd>
+
+                <dt class="col-sm-3">开仓时间</dt>
+                <dd class="col-sm-9">{{ trade.open_time.strftime('%Y-%m-%d %H:%M:%S') if trade.open_time else '-' }}</dd>
+
+                <dt class="col-sm-3">平仓时间</dt>
+                <dd class="col-sm-9">{{ trade.close_time.strftime('%Y-%m-%d %H:%M:%S') if trade.close_time else '-' }}</dd>
+
+                <dt class="col-sm-3">持仓方向</dt>
+                <dd class="col-sm-9">{{ '多头' if trade.position_type == 0 else ('空头' if trade.position_type == 1 else '未知') }}</dd>
+
+                <dt class="col-sm-3">持仓手数</dt>
+                <dd class="col-sm-9">{{ trade.position_volume }}</dd>
+
+                <dt class="col-sm-3">合约乘数</dt>
+                <dd class="col-sm-9">{{ trade.contract_multiplier }}</dd>
+
+                <dt class="col-sm-3">持仓成本</dt>
+                <dd class="col-sm-9">{{ '{:,.2f}'.format(trade.past_position_cost) if trade.past_position_cost is not none else '-' }}</dd>
+
+                <dt class="col-sm-3">平均售价</dt>
+                <dd class="col-sm-9">{{ '{:,.2f}'.format(trade.average_sale_price) if trade.average_sale_price is not none else '-' }}</dd>
+
+                <dt class="col-sm-3">单笔收益</dt>
+                <dd class="col-sm-9">{{ '{:,.0f}'.format(trade.single_profit) if trade.single_profit is not none else '-' }}</dd>
+
+                <dt class="col-sm-3">投资收益率</dt>
+                <dd class="col-sm-9">{{ '{:.2%}'.format(trade.investment_profit_rate) if trade.investment_profit_rate is not none else '-' }}</dd>
+
+                <dt class="col-sm-3">持仓天数</dt>
+                <dd class="col-sm-9">{{ trade.holding_days if trade.holding_days is not none else '-' }}</dd>
+
+                <dt class="col-sm-3">年化收益率</dt>
+                <dd class="col-sm-9">{{ '{:.2%}'.format(trade.annual_profit_rate) if trade.annual_profit_rate is not none else '-' }}</dd>
+
+                <dt class="col-sm-3">操作策略</dt>
+                <dd class="col-sm-9">{{ trade.strategy_name or '-' }} (ID: {{ trade.strategy_ids or '-' }})</dd>
+
+                <dt class="col-sm-3">K线形态</dt>
+                <dd class="col-sm-9">{{ trade.candle_pattern or '-' }} (ID: {{ trade.candle_pattern_id or '-' }})</dd>
+
+                <dt class="col-sm-3">长期趋势</dt>
+                <dd class="col-sm-9">{{ trade.long_trend_name or '-' }} (IDs: {{ trade.long_trend_ids or '-' }})</dd>
+
+                <dt class="col-sm-3">中期趋势</dt>
+                <dd class="col-sm-9">{{ trade.mid_trend_name or '-' }} (IDs: {{ trade.mid_trend_ids or '-' }})</dd>
+
+                <dt class="col-sm-3">交易类型</dt>
+                <dd class="col-sm-9">{{ '模拟交易' if trade.trade_type == 0 else ('实盘交易' if trade.trade_type == 1 else '未知') }}</dd>
+
+                <dt class="col-sm-3">换月交易主ID</dt>
+                <dd class="col-sm-9">{{ trade.roll_trade_main_id if trade.roll_trade_main_id is not none else '-' }}</dd>
+
+                <dt class="col-sm-3">信心指数</dt>
+                <dd class="col-sm-9">{{ trade.confidence_index if trade.confidence_index is not none else '-' }}</dd>
+
+                <dt class="col-sm-3">相似度评价</dt>
+                <dd class="col-sm-9">{{ trade.similarity_evaluation or '-' }}</dd>
+
+            </dl>
+        </div>
+        <div class="card-footer text-end">
+            <a href="{{ url_for('trade.index') }}" class="btn btn-secondary">返回列表</a>
+            {% if not trade.close_time %}
+                <a href="{{ url_for('transaction.add') }}?close_for={{ trade.id }}" class="btn btn-warning ms-2">
+                    <i class="fas fa-times-circle"></i> 平仓
+                </a>
+            {% endif %}
+            {# Add Edit button if needed #}
+            {# <a href="{{ url_for('trade.edit', id=trade.id) }}" class="btn btn-primary">编辑</a> #}
+        </div>
+    </div>
+</div>
+{% endblock %}

+ 121 - 0
app/templates/trade/import.html

@@ -0,0 +1,121 @@
+{% extends 'base.html' %}
+
+{% block title %}导入换月交易记录 - 期货数据管理系统{% endblock %}
+
+{% block content %}
+<div class="d-flex justify-content-between align-items-center mb-4">
+    <h2>导入换月交易记录</h2>
+    <a href="{{ url_for('trade.index') }}" class="btn btn-secondary">返回列表</a>
+</div>
+
+<div class="card">
+    <div class="card-body">
+        <div class="row mb-4">
+            <div class="col-md-12">
+                <p>请按照以下步骤操作:</p>
+                <ol>
+                    <li>下载导入模板</li>
+                    <li>按照模板格式填写数据</li>
+                    <li>上传Excel文件</li>
+                </ol>
+                <div class="alert alert-info">
+                    <strong>提示:</strong> 必填字段包括"换月交易主ID"、"合约代码"、"名称"、"开仓时间"、"平仓时间"等。
+                </div>
+            </div>
+        </div>
+        
+        <div class="row mb-4">
+            <div class="col-md-6">
+                <button id="download-template" class="btn btn-primary">
+                    <i class="fas fa-download"></i> 下载导入模板
+                </button>
+            </div>
+        </div>
+        
+        <form id="import-form" enctype="multipart/form-data">
+            <div class="row mb-4">
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="file">选择Excel文件</label>
+                        <input type="file" class="form-control" id="file" name="file" accept=".xlsx" required>
+                    </div>
+                </div>
+            </div>
+            
+            <div class="form-group">
+                <button type="submit" class="btn btn-success">
+                    <i class="fas fa-upload"></i> 导入数据
+                </button>
+            </div>
+        </form>
+        
+        <div id="result" class="mt-4" style="display: none;">
+        </div>
+    </div>
+</div>
+
+<script>
+document.addEventListener('DOMContentLoaded', function() {
+    const form = document.getElementById('import-form');
+    const resultDiv = document.getElementById('result');
+    const downloadBtn = document.getElementById('download-template');
+    
+    // 处理下载模板按钮点击
+    downloadBtn.addEventListener('click', function(e) {
+        // 禁用按钮防止重复点击
+        downloadBtn.disabled = true;
+        
+        // 创建一个隐藏的iframe来处理下载
+        const iframe = document.createElement('iframe');
+        iframe.style.display = 'none';
+        iframe.src = "{{ url_for('trade.get_template') }}";
+        document.body.appendChild(iframe);
+        
+        // 3秒后重新启用按钮
+        setTimeout(() => {
+            downloadBtn.disabled = false;
+            document.body.removeChild(iframe);
+        }, 3000);
+    });
+    
+    form.addEventListener('submit', function(e) {
+        e.preventDefault();
+        
+        const formData = new FormData(form);
+        
+        // 显示加载状态
+        resultDiv.innerHTML = '<div class="alert alert-info">正在导入数据,请稍候...</div>';
+        resultDiv.style.display = 'block';
+        
+        fetch('/api/trade/import', {
+            method: 'POST',
+            body: formData
+        })
+        .then(response => response.json())
+        .then(data => {
+            if (data.code === 0) {
+                // 成功导入
+                let html = `<div class="alert alert-success">${data.msg}</div>`;
+                
+                if (data.data.error_count > 0) {
+                    html += '<div class="alert alert-warning"><strong>导入过程中出现以下错误:</strong><ul>';
+                    data.data.error_messages.forEach(msg => {
+                        html += `<li>${msg}</li>`;
+                    });
+                    html += '</ul></div>';
+                }
+                
+                resultDiv.innerHTML = html;
+            } else {
+                // 导入失败
+                resultDiv.innerHTML = `<div class="alert alert-danger">${data.msg}</div>`;
+            }
+        })
+        .catch(error => {
+            console.error('Error:', error);
+            resultDiv.innerHTML = '<div class="alert alert-danger">导入失败,请查看控制台了解详情</div>';
+        });
+    });
+});
+</script>
+{% endblock %} 

+ 393 - 0
app/templates/trade/index.html

@@ -0,0 +1,393 @@
+{% extends "base.html" %}
+
+{% block title %}交易汇总列表{% endblock %}
+
+{% block styles %}
+<style>
+    /* 表格水平滚动样式 */
+    .table-responsive {
+        border: 1px solid #dee2e6;
+        border-radius: 0.375rem;
+        overflow-x: auto;
+    }
+    .table-responsive::-webkit-scrollbar { height: 8px; }
+    .table-responsive::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; }
+    .table-responsive::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 4px; }
+    .table-responsive::-webkit-scrollbar-thumb:hover { background: #a8a8a8; }
+    
+
+</style>
+{% endblock %}
+
+{% block content %}
+<div class="container-fluid">
+    <div class="row">
+        <div class="col-12">
+            <div class="card">
+                <div class="card-header">
+                    <h3 class="card-title">交易汇总列表</h3>
+                    <div class="card-tools">
+                        <button id="syncDataBtn" class="btn btn-primary btn-sm">同步数据</button>
+                        <a href="{{ url_for('trade.export') }}" class="btn btn-success btn-sm">导出Excel</a>
+                    </div>
+                </div>
+                <div class="card-body">
+                    <!-- 筛选表单 -->
+                    <form id="filterForm" class="mb-3">
+                        <div class="row">
+                            <div class="col-md-3">
+                                <div class="form-group">
+                                    <label>时间范围</label>
+                                    <div class="input-group">
+                                        <input type="date" class="form-control" name="start_time">
+                                        <div class="input-group-append">
+                                            <span class="input-group-text">至</span>
+                                        </div>
+                                        <input type="date" class="form-control" name="end_time">
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="col-md-2">
+                                <div class="form-group">
+                                    <label>合约名称</label>
+                                    <input type="text" class="form-control" name="name">
+                                </div>
+                            </div>
+                            <div class="col-md-2">
+                                <div class="form-group">
+                                    <label>合约代码</label>
+                                    <input type="text" class="form-control" name="contract_code">
+                                </div>
+                            </div>
+                            <div class="col-md-2">
+                                <div class="form-group">
+                                    <label>交易类型</label>
+                                    <select class="form-control" name="trade_type">
+                                        <option value="">全部</option>
+                                        <option value="0">模拟交易</option>
+                                        <option value="1">实盘交易</option>
+                                    </select>
+                                </div>
+                            </div>
+                            <div class="col-md-2">
+                                <div class="form-group">
+                                    <label>持仓方向</label>
+                                    <select class="form-control" name="position_type">
+                                        <option value="">全部</option>
+                                        <option value="0">多头</option>
+                                        <option value="1">空头</option>
+                                    </select>
+                                </div>
+                            </div>
+                            <div class="col-md-1">
+                                <div class="form-group">
+                                    <label>&nbsp;</label>
+                                    <button type="submit" class="btn btn-primary form-control">查询</button>
+                                </div>
+                            </div>
+                        </div>
+                    </form>
+
+                    <!-- 数据表格 -->
+                    <div class="table-responsive">
+                        <table class="table table-striped table-hover">
+                            <thead>
+                                <tr>
+                                    <th>ID</th>
+                                    <th>开仓时间</th>
+                                    <th>平仓时间</th>
+                                    <th>合约代码</th>
+                                    <th>名称</th>
+                                    <th>账户</th>
+                                    <th>操作策略</th>
+                                    <th>持仓方向</th>
+                                    <th>持仓手数</th>
+                                    <th>持仓成本</th>
+                                    <th>平均售价</th>
+                                    <th>单笔收益</th>
+                                    <th>投资收益率</th>
+                                    <th>持仓天数</th>
+                                    <th>年化收益率</th>
+                                    <th>操作</th>
+                                </tr>
+                            </thead>
+                            <tbody id="tradeList">
+                                <!-- 数据将通过JavaScript动态加载 -->
+                            </tbody>
+                        </table>
+                    </div>
+                    <!-- 分页控件和每页数量选择器 -->
+                    <div class="d-flex justify-content-center align-items-center mt-3">
+                        <nav aria-label="Page navigation" class="me-3">
+                            <ul class="pagination mb-0" id="pagination">
+                                <!-- 分页按钮将通过JavaScript动态加载 -->
+                            </ul>
+                        </nav>
+                        <div class="d-flex align-items-center">
+                            <label for="itemsPerPageSelect" class="col-form-label me-2 mb-0">每页:</label>
+                            <select class="form-select form-select-sm" id="itemsPerPageSelect" style="width: auto;">
+                                <option value="10" selected>10</option>
+                                <option value="20">20</option>
+                                <option value="50">50</option>
+                            </select>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock %}
+
+{% block scripts %}
+<script>
+$(document).ready(function() {
+    let currentPage = 1;
+    let currentItemsPerPage = parseInt($('#itemsPerPageSelect').val()) || 10;
+
+    // 从 sessionStorage 恢复状态
+    const savedFiltersJSON = sessionStorage.getItem('tradeFilters');
+    if (savedFiltersJSON) {
+        const savedFilters = JSON.parse(savedFiltersJSON);
+        Object.keys(savedFilters).forEach(key => {
+            $(`[name="${key}"]`).val(savedFilters[key]);
+        });
+    }
+
+    const savedPage = sessionStorage.getItem('tradeCurrentPage');
+    if (savedPage) {
+        currentPage = parseInt(savedPage, 10);
+    }
+
+    const savedItemsPerPage = sessionStorage.getItem('tradeItemsPerPage');
+    if (savedItemsPerPage) {
+        currentItemsPerPage = parseInt(savedItemsPerPage, 10);
+        $('#itemsPerPageSelect').val(currentItemsPerPage);
+    }
+
+    // 加载交易汇总列表
+    function loadTrades(page = 1, filters = {}) {
+        currentPage = page;
+        filters.page = page;
+        filters.limit = currentItemsPerPage;
+
+        // 保存当前状态到 sessionStorage
+        sessionStorage.setItem('tradeFilters', JSON.stringify(getFilters()));
+        sessionStorage.setItem('tradeCurrentPage', currentPage);
+        sessionStorage.setItem('tradeItemsPerPage', currentItemsPerPage);
+
+        console.log("开始加载交易汇总数据...");
+        console.log("筛选条件:", filters);
+        // 显示加载提示
+        $('#tradeList').html('<tr><td colspan="16" class="text-center">数据加载中...</td></tr>');
+        $('#pagination').empty(); // 清空分页
+        
+        $.ajax({
+            url: "{{ url_for('trade.get_list') }}",
+            type: "GET",
+            data: filters,
+            dataType: "json",
+            success: function(response) {
+                console.log("加载交易汇总数据成功", response);
+                if (response.code === 0) {
+                    let html = '';
+                    if (response.data && response.data.length > 0) {
+                        console.log(`找到${response.data.length}条交易汇总记录`);
+                        response.data.forEach(function(item) {
+                            // 调试信息:检查close_time值
+                            if (item.id <= 5) { // 只对前5条记录输出调试信息,避免控制台过于冗长
+                                console.log(`交易ID ${item.id}: close_time = ${item.close_time}, 是否显示平仓按钮: ${!item.close_time}`);
+                            }
+                            
+                            html += `
+                                <tr>
+                                    <td>${item.id || ''}</td>
+                                    <td>${item.open_time || ''}</td>
+                                    <td>${item.close_time || '-'}</td>
+                                    <td>${item.contract_code || ''}</td>
+                                    <td>${item.name || ''}</td>
+                                    <td>${item.account || ''}</td>
+                                    <td>${item.strategy_name || '-'}</td>
+                                    <td>${getPositionTypeText(item.position_type)}</td>
+                                    <td>${item.position_volume || ''}</td>
+                                    <td>${item.past_position_cost !== undefined ? item.past_position_cost : '-'}</td>
+                                    <td>${item.average_sale_price !== undefined ? item.average_sale_price : '-'}</td>
+                                    <td>${item.single_profit !== undefined ? Math.round(item.single_profit) : '-'}</td>
+                                    <td>${item.investment_profit_rate ? (item.investment_profit_rate * 100).toFixed(2) + '%' : '-'}</td>
+                                    <td>${item.holding_days !== undefined ? item.holding_days : '-'}</td>
+                                    <td>${item.annual_profit_rate ? (item.annual_profit_rate * 100).toFixed(2) + '%' : '-'}</td>
+                                    <td>
+                                        <div class="btn-group">
+                                            <button type="button" class="btn btn-secondary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                                                操作
+                                            </button>
+                                            <div class="dropdown-menu">
+                                                <a class="dropdown-item" href="/trade/detail/${item.id}">详情</a>
+                                                ${!item.close_time ? `
+                                                    <div class="dropdown-divider"></div>
+                                                    <a class="dropdown-item text-warning" href="/transaction/add?close_for=${item.id}">
+                                                        <i class="fas fa-times-circle"></i> 平仓
+                                                    </a>
+                                                ` : ''}
+                                            </div>
+                                        </div>
+                                    </td>
+                                </tr>
+                            `;
+                        });
+                    } else {
+                        console.log("没有找到交易汇总记录");
+                        html = '<tr><td colspan="16" class="text-center">暂无数据</td></tr>';
+                    }
+                    $('#tradeList').html(html);
+                    renderPagination(response.count, page, currentItemsPerPage);
+                } else {
+                    console.error('加载交易汇总失败:', response.msg);
+                    $('#tradeList').html(`<tr><td colspan="16" class="text-center text-danger">加载失败: ${response.msg}</td></tr>`);
+                }
+            },
+            error: function(xhr, status, error) {
+                console.error('加载交易汇总异常:', error);
+                console.error('状态:', status);
+                console.error('响应:', xhr.responseText);
+                $('#tradeList').html(`<tr><td colspan="16" class="text-center text-danger">加载异常,请查看控制台</td></tr>`);
+            }
+        });
+    }
+
+    // 渲染分页控件
+    function renderPagination(totalItems, currentPage, itemsPerPage) {
+        const totalPages = Math.ceil(totalItems / itemsPerPage);
+        let paginationHtml = '';
+
+        if (totalPages <= 1) {
+            $('#pagination').empty();
+            // 如果只有一页或没有数据,也隐藏每页数量选择器
+            $('#itemsPerPageSelect').closest('div').hide();
+            return;
+        } else {
+            // 确保选择器可见
+            $('#itemsPerPageSelect').closest('div').show();
+        }
+
+        // 上一页按钮
+        paginationHtml += `<li class="page-item ${currentPage === 1 ? 'disabled' : ''}">
+                            <a class="page-link" href="#" data-page="${currentPage - 1}" aria-label="Previous">
+                                <span aria-hidden="true">&laquo;</span>
+                            </a>
+                           </li>`;
+
+        // 页码按钮 (只显示部分页码)
+        const maxPagesToShow = 5;
+        let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
+        let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
+
+        if (endPage - startPage + 1 < maxPagesToShow) {
+            startPage = Math.max(1, endPage - maxPagesToShow + 1);
+        }
+
+        if (startPage > 1) {
+            paginationHtml += `<li class="page-item"><a class="page-link" href="#" data-page="1">1</a></li>`;
+            if (startPage > 2) {
+                paginationHtml += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
+            }
+        }
+
+        for (let i = startPage; i <= endPage; i++) {
+            paginationHtml += `<li class="page-item ${i === currentPage ? 'active' : ''}">
+                                <a class="page-link" href="#" data-page="${i}">${i}</a>
+                               </li>`;
+        }
+
+        if (endPage < totalPages) {
+            if (endPage < totalPages - 1) {
+                paginationHtml += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
+            }
+            paginationHtml += `<li class="page-item"><a class="page-link" href="#" data-page="${totalPages}">${totalPages}</a></li>`;
+        }
+
+        // 下一页按钮
+        paginationHtml += `<li class="page-item ${currentPage === totalPages ? 'disabled' : ''}">
+                            <a class="page-link" href="#" data-page="${currentPage + 1}" aria-label="Next">
+                                <span aria-hidden="true">&raquo;</span>
+                            </a>
+                           </li>`;
+
+        $('#pagination').html(paginationHtml);
+
+        // 绑定分页按钮点击事件
+        $('#pagination .page-link').on('click', function(e) {
+            e.preventDefault();
+            const page = $(this).data('page');
+            if (page && page !== currentPage) {
+                const filters = getFilters();
+                loadTrades(page, filters);
+            }
+        });
+    }
+
+    // 获取当前筛选条件
+    function getFilters() {
+        const filters = {};
+        $('#filterForm').serializeArray().forEach(function(item) {
+            if (item.value) {
+                filters[item.name] = item.value;
+            }
+        });
+        return filters;
+    }
+
+    // 获取持仓方向文本
+    function getPositionTypeText(type) {
+        const types = {
+            0: '多头',
+            1: '空头'
+        };
+        return types[type] || '未知';
+    }
+
+    // 表单提交处理
+    $('#filterForm').on('submit', function(e) {
+        e.preventDefault();
+        const filters = getFilters();
+        loadTrades(1, filters); // 筛选后总是回到第一页
+    });
+
+    // 每页显示数量变化处理
+    $('#itemsPerPageSelect').on('change', function() {
+        currentItemsPerPage = parseInt($(this).val());
+        const filters = getFilters();
+        loadTrades(1, filters); // 更改每页数量后回到第一页
+    });
+
+    // 同步数据按钮点击事件
+    $('#syncDataBtn').on('click', function() {
+        if (confirm('确定要从交易明细中全面同步交易汇总数据吗?这可能需要一些时间。')) {
+            const loadingIndicator = $('<div class="overlay"><i class="fas fa-2x fa-sync-alt fa-spin"></i></div>');
+            $('.card').append(loadingIndicator);
+
+            $.ajax({
+                url: "{{ url_for('trade.sync_all') }}",
+                type: 'POST',
+                success: function(res) {
+                    loadingIndicator.remove();
+                    if (res.code === 0) {
+                        alert(res.msg || '同步成功!');
+                        loadTrades(currentPage, getFilters()); // 刷新当前页
+                    } else {
+                        alert('同步失败: ' + (res.msg || '未知错误'));
+                    }
+                },
+                error: function() {
+                    loadingIndicator.remove();
+                    alert('请求失败,请检查网络或联系管理员。');
+                }
+            });
+        }
+    });
+
+    // 初始加载
+    loadTrades(currentPage, getFilters());
+});
+</script>
+{% endblock %} 

+ 669 - 0
app/templates/transaction/add.html

@@ -0,0 +1,669 @@
+{% extends 'base.html' %}
+
+{% block title %}添加交易记录 - 期货数据管理系统{% endblock %}
+
+{% block styles %}
+<!-- 如果使用 Select2 或其他库,在此处添加 CSS 链接 -->
+<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
+<style>
+    /* 调整 Select2 样式以匹配 Bootstrap */
+    .select2-container .select2-selection--single {
+        height: calc(1.5em + .75rem + 2px); /* 匹配 Bootstrap 输入框高度 */
+        padding: .375rem .75rem;
+        border: 1px solid #ced4da;
+    }
+    .select2-container--default .select2-selection--single .select2-selection__rendered {
+        line-height: 1.5;
+        padding-left: 0;
+        padding-right: 0;
+    }
+    .select2-container--default .select2-selection--single .select2-selection__arrow {
+        height: calc(1.5em + .75rem); /* 匹配 Bootstrap 输入框高度 */
+    }
+    .select2-container .select2-selection--multiple {
+        min-height: calc(1.5em + .75rem + 2px); /* 匹配 Bootstrap 输入框高度 */
+        border: 1px solid #ced4da;
+    }
+    .select2-container .select2-search--inline .select2-search__field {
+        margin-top: 0.5rem;
+    }
+</style>
+{% endblock %}
+
+{% block content %}
+<div class="d-flex justify-content-between align-items-center mb-4">
+    <h2>{{ '平仓交易记录' if close_trade_data else '添加交易记录' }}</h2>
+    <a href="{{ url_for('transaction.index') }}" class="btn btn-secondary">返回列表</a>
+</div>
+
+{% if close_trade_data %}
+<div class="alert alert-info">
+    <i class="fas fa-info-circle"></i>
+    正在为交易ID <strong>{{ close_trade_data.original_trade_id }}</strong> 创建平仓记录。
+    相关信息已自动填入表单,请确认价格和数量后提交。
+</div>
+{% endif %}
+
+<div class="card">
+    <div class="card-body">
+        <form id="transaction-form">
+            <!-- 隐藏字段存储从API获取的期货信息 -->
+            <input type="hidden" id="contract_multiplier" name="contract_multiplier">
+            <input type="hidden" id="open_fee_rate" name="open_fee_rate">
+            <input type="hidden" id="close_fee_rate" name="close_fee_rate">
+            <input type="hidden" id="long_margin_rate" name="long_margin_rate">
+            <input type="hidden" id="short_margin_rate" name="short_margin_rate">
+
+            <div class="row">
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="transaction_datetime_input">成交日期和时间</label>
+                        <input type="datetime-local" class="form-control" id="transaction_datetime_input" name="transaction_datetime_input" required>
+                    </div>
+                </div>
+                 <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="operation_time_input">操作时间</label>
+                        <input type="datetime-local" class="form-control" id="operation_time_input" name="operation_time_input">
+                         <small class="form-text text-muted">默认为成交时间。</small>
+                    </div>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="account">账户</label>
+                        <input type="text" class="form-control" id="account" name="account" value="华安期货">
+                    </div>
+                </div>
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="contract_code">合约代码</label>
+                        <input type="text" class="form-control" id="contract_code" name="contract_code" placeholder="例如: CU2305" required>
+                        <small class="form-text text-muted">输入合约代码后将自动填充名称、乘数和费率。</small>
+                    </div>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="name">合约名称</label>
+                        <input type="text" class="form-control" id="name" name="name" readonly required>
+                    </div>
+                </div>
+            </div>
+            <div class="row">
+                 <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="position_type">仓位操作类型</label>
+                        <select class="form-control" id="position_type" name="position_type" required>
+                            <option value="">请选择...</option>
+                            <option value="0">开多</option>
+                            <option value="1">平多</option>
+                            <option value="2">开空</option>
+                            <option value="3">平空</option>
+                        </select>
+                    </div>
+                </div>
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="volume">成交手数</label>
+                        <input type="number" step="1" min="1" class="form-control" id="volume" name="volume" required>
+                    </div>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="price">成交价格</label>
+                        <input type="number" step="any" class="form-control" id="price" name="price" required>
+                    </div>
+                </div>
+                 <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="strategy_names">操作策略</label>
+                        <select class="form-control" id="strategy_names" name="strategy_names" multiple="multiple">
+                            <!-- Options will be loaded via JS -->
+                        </select>
+                         <small class="form-text text-muted">可多选。</small>
+                    </div>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="candle_pattern_names">K线形态</label>
+                        <select class="form-control" id="candle_pattern_names" name="candle_pattern_names" multiple="multiple">
+                            <!-- Options will be loaded via JS -->
+                        </select>
+                        <small class="form-text text-muted">可多选。</small>
+                    </div>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="long_trend_names">长期趋势</label>
+                        <select class="form-control" id="long_trend_names" name="long_trend_names" multiple="multiple">
+                            <!-- Options will be loaded via JS -->
+                        </select>
+                        <small class="form-text text-muted">可多选。</small>
+                    </div>
+                </div>
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="mid_trend_names">中期趋势</label>
+                        <select class="form-control" id="mid_trend_names" name="mid_trend_names" multiple="multiple">
+                            <!-- Options will be loaded via JS -->
+                        </select>
+                        <small class="form-text text-muted">可多选。</small>
+                    </div>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="trade_type">交易类别</label>
+                        <select class="form-control" id="trade_type" name="trade_type">
+                            <option value="0">模拟交易</option>
+                            <option value="1">真实交易</option>
+                        </select>
+                    </div>
+                </div>
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="trade_status">交易状态</label>
+                        <select class="form-control" id="trade_status" name="trade_status">
+                            <option value="0" selected>进行中</option>
+                            <option value="1">已暂停</option>
+                            <option value="2">暂停进行</option>
+                            <option value="3">已结束</option>
+                        </select>
+                    </div>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="stop_loss_price">止损价格</label>
+                        <input type="number" step="0.001" class="form-control" id="stop_loss_price" name="stop_loss_price">
+                    </div>
+                </div>
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="confidence_index">信心指数</label>
+                        <input type="number" step="1" min="0" class="form-control" id="confidence_index" name="confidence_index">
+                    </div>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-12">
+                    <div class="form-group">
+                        <label for="notes">备注</label>
+                        <textarea class="form-control" id="notes" name="notes" rows="3"></textarea>
+                    </div>
+                </div>
+            </div>
+            <div class="form-group mt-4">
+                <button type="submit" class="btn btn-primary">保存</button>
+                <button type="reset" class="btn btn-secondary">重置</button>
+            </div>
+        </form>
+    </div>
+</div>
+
+{% endblock %}
+
+{% block scripts %}
+<!-- 数据传递脚本 -->
+<script type="application/json" id="close-trade-data">
+    {% if close_trade_data %}
+    {{ close_trade_data | tojson | safe }}
+    {% else %}
+    null
+    {% endif %}
+</script>
+
+<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
+<script>
+$(document).ready(function() {
+    const form = document.getElementById('transaction-form');
+    const contractCodeInput = document.getElementById('contract_code');
+    const nameInput = document.getElementById('name');
+    const multiplierInput = document.getElementById('contract_multiplier');
+    const openFeeRateInput = document.getElementById('open_fee_rate');
+    const closeFeeRateInput = document.getElementById('close_fee_rate');
+    const longMarginRateInput = document.getElementById('long_margin_rate');
+    const shortMarginRateInput = document.getElementById('short_margin_rate');
+    const strategySelect = $('#strategy_names'); // jQuery object for Select2
+    const candlePatternSelect = $('#candle_pattern_names');
+    const longTrendSelect = $('#long_trend_names');
+    const midTrendSelect = $('#mid_trend_names');
+    const transactionDateTimeInput = document.getElementById('transaction_datetime_input');
+    const operationTimeInput = document.getElementById('operation_time_input');
+
+    // 存储K线形态选择顺序
+    let candlePatternOrder = [];
+    let candlePatternIdOrder = [];
+    
+    // 从JSON script标签中读取平仓数据
+    let closeTradeData = null;
+    try {
+        const dataScript = document.getElementById('close-trade-data');
+        if (dataScript) {
+            const dataText = dataScript.textContent.trim();
+            if (dataText !== 'null') {
+                closeTradeData = JSON.parse(dataText);
+                console.log('检测到平仓数据,开始预填充表单:', closeTradeData);
+            }
+        }
+    } catch (e) {
+        console.error('解析平仓数据失败:', e);
+        closeTradeData = null;
+    }
+
+    // 初始化 Select2 多选下拉框
+    strategySelect.select2({
+        placeholder: "请选择...",
+        allowClear: true,
+        width: '100%'
+    });
+    
+    candlePatternSelect.select2({
+        placeholder: "请选择...",
+        allowClear: true,
+        width: '100%',
+        closeOnSelect: false // 选择后不关闭,方便多选
+    });
+    
+    longTrendSelect.select2({
+        placeholder: "请选择...",
+        allowClear: true,
+        width: '100%'
+    });
+    
+    midTrendSelect.select2({
+        placeholder: "请选择...",
+        allowClear: true,
+        width: '100%'
+    });
+
+    // --- 辅助函数:加载 Select2 选项 --- 
+    function loadSelect2Options(selectElement, apiUrl, nameField = 'name', idField = 'id') {
+        fetch(apiUrl)
+            .then(response => response.json())
+            .then(data => {
+                if (data.code === 0) {
+                    data.data.forEach(item => {
+                        // 使用名称作为值和显示文本,简化处理
+                        const option = new Option(item[nameField], item[nameField], false, false);
+                        selectElement.append(option);
+                    });
+                    selectElement.trigger('change');
+                } else {
+                    console.error(`加载 ${selectElement.attr('id')} 失败: ${data.msg}`);
+                }
+            })
+            .catch(error => console.error(`加载 ${selectElement.attr('id')} 出错: ${error}`));
+    }
+
+    // 加载所有 Select2 选项
+    let loadedOptionsCount = 0;
+    const totalOptions = 4;
+    
+    function checkIfAllLoaded() {
+        loadedOptionsCount++;
+        if (loadedOptionsCount === totalOptions && closeTradeData) {
+            // 所有选项都加载完毕,现在可以预填充数据
+            prefillFormData();
+        }
+    }
+    
+    function loadSelect2OptionsWithCallback(selectElement, apiUrl, nameField = 'name', idField = 'id') {
+        fetch(apiUrl)
+            .then(response => response.json())
+            .then(data => {
+                if (data.code === 0) {
+                    data.data.forEach(item => {
+                        const option = new Option(item[nameField], item[nameField], false, false);
+                        selectElement.append(option);
+                    });
+                    selectElement.trigger('change');
+                } else {
+                    console.error(`加载 ${selectElement.attr('id')} 失败: ${data.msg}`);
+                }
+                checkIfAllLoaded();
+            })
+            .catch(error => {
+                console.error(`加载 ${selectElement.attr('id')} 出错: ${error}`);
+                checkIfAllLoaded();
+            });
+    }
+    
+    loadSelect2OptionsWithCallback(strategySelect, '/api/dimension/strategy/list_all');
+    loadSelect2OptionsWithCallback(candlePatternSelect, '/api/dimension/candle/list_all');
+    loadSelect2OptionsWithCallback(longTrendSelect, '/api/dimension/trend/list_all');
+    loadSelect2OptionsWithCallback(midTrendSelect, '/api/dimension/trend/list_all');
+    
+    // 预填充表单数据函数
+    function prefillFormData() {
+        if (!closeTradeData) return;
+        
+        console.log('开始预填充表单数据...');
+        
+        // 填充基本字段
+        contractCodeInput.value = closeTradeData.contract_code || '';
+        nameInput.value = closeTradeData.name || '';
+        document.getElementById('account').value = closeTradeData.account || '华安期货';
+        document.getElementById('volume').value = closeTradeData.position_volume || '';
+        multiplierInput.value = closeTradeData.contract_multiplier || '';
+        document.getElementById('trade_type').value = closeTradeData.trade_type || '0';
+        
+        // 设置仓位操作类型(平仓类型)
+        if (closeTradeData.position_type !== null && closeTradeData.position_type !== undefined) {
+            document.getElementById('position_type').value = closeTradeData.position_type;
+        }
+        
+        // 设置交易状态为"已结束"
+        if (closeTradeData.trade_status !== null && closeTradeData.trade_status !== undefined) {
+            document.getElementById('trade_status').value = closeTradeData.trade_status;
+        }
+        
+        // 平仓时策略和K线形态字段保持为空,不进行预填充
+        // 这样用户可以选择是否为平仓操作设置新的策略或形态
+        
+        // 预填充选择框数据(仅填充趋势信息,策略和K线形态保持为空)
+        // if (closeTradeData.strategy_name) {
+        //     const strategyNames = closeTradeData.strategy_name.split('+');
+        //     strategySelect.val(strategyNames).trigger('change');
+        // }
+        
+        // if (closeTradeData.candle_pattern) {
+        //     const candlePatterns = closeTradeData.candle_pattern.split('+');
+        //     candlePatternSelect.val(candlePatterns).trigger('change');
+        //     candlePatternOrder = [...candlePatterns]; // 保持顺序
+        // }
+        
+        if (closeTradeData.long_trend_name) {
+            const longTrendNames = closeTradeData.long_trend_name.split('+');
+            longTrendSelect.val(longTrendNames).trigger('change');
+        }
+        
+        if (closeTradeData.mid_trend_name) {
+            const midTrendNames = closeTradeData.mid_trend_name.split('+');
+            midTrendSelect.val(midTrendNames).trigger('change');
+        }
+        
+        // 设置当前时间为交易时间
+        const now = new Date();
+        const dateTimeString = now.toISOString().slice(0, 16); // Format: YYYY-MM-DDTHH:MM
+        transactionDateTimeInput.value = dateTimeString;
+        operationTimeInput.value = dateTimeString;
+        
+        console.log('表单预填充完成');
+    }
+    
+    // K线形态选择事件 - 跟踪真实的用户选择顺序
+    candlePatternSelect.on('select2:select', function(e) {
+        const selectedData = e.params.data;
+        const selectedId = selectedData.id;
+        const selectedText = selectedData.text;
+        
+        // 添加到用户选择顺序数组
+        if (!candlePatternIdOrder.includes(selectedId)) {
+            candlePatternIdOrder.push(selectedId);
+            candlePatternOrder.push(selectedText);
+            console.log('选择的K线形态ID(按用户点击顺序):', candlePatternIdOrder);
+            console.log('选择的K线形态名称(按用户点击顺序):', candlePatternOrder);
+        }
+    });
+    
+    candlePatternSelect.on('select2:unselect', function(e) {
+        const unselectedData = e.params.data;
+        const unselectedId = unselectedData.id;
+        const unselectedText = unselectedData.text;
+        
+        // 从用户选择顺序数组中移除
+        const idIndex = candlePatternIdOrder.indexOf(unselectedId);
+        const textIndex = candlePatternOrder.indexOf(unselectedText);
+        
+        if (idIndex > -1) {
+            candlePatternIdOrder.splice(idIndex, 1);
+        }
+        if (textIndex > -1) {
+            candlePatternOrder.splice(textIndex, 1);
+        }
+        
+        console.log('取消选择后的K线形态ID(按用户点击顺序):', candlePatternIdOrder);
+        console.log('取消选择后的K线形态名称(按用户点击顺序):', candlePatternOrder);
+    });
+
+    // 标记用户是否手动修改过操作时间
+    let operationTimeManuallySet = false;
+    
+    // --- 监听成交日期和时间变化,智能更新操作时间 --- 
+    function updateOperationTimeDefault() {
+        const transactionDateTime = transactionDateTimeInput.value;
+        if (transactionDateTime) {
+            // 如果操作时间为空或未被用户手动修改过,则自动同步
+            if (!operationTimeInput.value || !operationTimeManuallySet) {
+                operationTimeInput.value = transactionDateTime;
+                console.log('操作时间已自动同步为成交时间:', transactionDateTime);
+            }
+        }
+    }
+    
+    // 监听操作时间的手动修改
+    operationTimeInput.addEventListener('change', function() {
+        if (this.value !== transactionDateTimeInput.value) {
+            operationTimeManuallySet = true;
+            console.log('检测到用户手动修改操作时间,自动同步已停用');
+        } else {
+            operationTimeManuallySet = false;
+            console.log('操作时间与成交时间一致,自动同步重新启用');
+        }
+    });
+    
+    transactionDateTimeInput.addEventListener('change', updateOperationTimeDefault);
+
+    // --- 监听合约代码输入变化 (自动填充名称等) --- 
+    contractCodeInput.addEventListener('blur', function() {
+        const code = this.value.trim();
+        if (code.length > 4) {
+            // 提取合约字母 (前缀,去除最后4位数字)
+            const letter = code.substring(0, code.length - 4).replace(/[^a-zA-Z]/g, ''); // 仅保留字母
+            if (letter) {
+                fetch(`/transaction/api/future_info/by_letter/${letter}`)
+                    .then(response => response.json())
+                    .then(data => {
+                        if (data.code === 0 && data.data) {
+                            nameInput.value = data.data.name || '';
+                            multiplierInput.value = data.data.contract_multiplier || '';
+                            openFeeRateInput.value = data.data.open_fee || '';
+                            closeFeeRateInput.value = data.data.close_fee || '';
+                            longMarginRateInput.value = data.data.long_margin_rate || '';
+                            shortMarginRateInput.value = data.data.short_margin_rate || '';
+                        } else {
+                            console.warn(`Could not find info for letter ${letter}: ${data.msg}`);
+                            // 清空相关字段
+                            nameInput.value = '';
+                            multiplierInput.value = '';
+                            openFeeRateInput.value = '';
+                            closeFeeRateInput.value = '';
+                            longMarginRateInput.value = '';
+                            shortMarginRateInput.value = '';
+                        }
+                    })
+                    .catch(error => {
+                        console.error('Error fetching future info:', error);
+                        // 清空相关字段
+                        nameInput.value = '';
+                        multiplierInput.value = '';
+                        openFeeRateInput.value = '';
+                        closeFeeRateInput.value = '';
+                        longMarginRateInput.value = '';
+                        shortMarginRateInput.value = '';
+                    });
+            }
+        } else {
+             // 如果代码无效,清空
+             nameInput.value = '';
+             multiplierInput.value = '';
+             openFeeRateInput.value = '';
+             closeFeeRateInput.value = '';
+             longMarginRateInput.value = '';
+             shortMarginRateInput.value = '';
+        }
+    });
+
+    form.addEventListener('submit', function(e) {
+        e.preventDefault();
+
+        // 获取表单基础数据
+        const transactionDateTimeValue = transactionDateTimeInput.value;
+        // 格式化为 YYYY-MM-DD HH:MM
+        const transactionDatetime = transactionDateTimeValue ? transactionDateTimeValue.replace('T', ' ') : null;
+        const contractCode = contractCodeInput.value;
+        const name = nameInput.value;
+        const positionType = parseInt(document.getElementById('position_type').value);
+        const volume = parseFloat(document.getElementById('volume').value);
+        const price = parseFloat(document.getElementById('price').value);
+        const notesValue = document.getElementById('notes').value;
+        const accountValue = document.getElementById('account').value;
+        const tradeTypeValue = document.getElementById('trade_type').value;
+        const tradeStatusValue = document.getElementById('trade_status').value;
+        const stopLossPriceValue = document.getElementById('stop_loss_price').value;
+        const confidenceIndexValue = document.getElementById('confidence_index').value;
+        const operationTimeValue = operationTimeInput.value; // 直接获取操作时间的值
+
+        // 获取存储的期货信息
+        const multiplier = parseFloat(multiplierInput.value) || 1; // Default to 1 if not found
+        const openFeeRate = parseFloat(openFeeRateInput.value) || 0;
+        const closeFeeRate = parseFloat(closeFeeRateInput.value) || 0;
+        const longMarginRate = parseFloat(longMarginRateInput.value) || 0;
+        const shortMarginRate = parseFloat(shortMarginRateInput.value) || 0;
+
+        // 检查必要数据是否获取成功
+        if (!name || !multiplierInput.value) {
+            alert('请确保输入了有效的合约代码并已自动获取合约信息。');
+            return;
+        }
+        if (isNaN(volume) || isNaN(price) || isNaN(positionType)) {
+             alert('请输入有效的数量、价格和仓位操作类型。');
+             return;
+        }
+
+        // --- 计算衍生字段 ---
+        // 1. 计算手续费 (fee)
+        let fee = 0;
+        if (positionType === 0 || positionType === 2) { // 开仓 (开多/开空)
+            fee = openFeeRate * volume;
+        } else if (positionType === 1 || positionType === 3) { // 平仓 (平多/平空)
+            fee = closeFeeRate * volume;
+        }
+
+        // 2. 计算成交金额 (amount)
+        const amount = price * volume * multiplier;
+
+        // 3. 计算手数变化 (volume_change)
+        let volumeChange = 0;
+        if (positionType === 0 || positionType === 3) { // 开多 或 平空 (手数增加或恢复)
+            volumeChange = volume;
+        } else if (positionType === 1 || positionType === 2) { // 平多 或 开空 (手数减少或建立空头)
+            volumeChange = -volume;
+        }
+
+        // 4. 计算保证金 (margin)
+        let margin = 0;
+        let marginRate = 0;
+        if (positionType === 0 || positionType === 1) { // 多头仓位
+            marginRate = longMarginRate;
+        } else if (positionType === 2 || positionType === 3) { // 空头仓位
+            marginRate = shortMarginRate;
+        }
+        if (marginRate > 0) {
+             // 保证金按金额 * 比例 计算
+             margin = amount * (marginRate / 100.0);
+        }
+
+        // --- 获取 Select2 多选值并保留顺序 ---
+        const getOrderedSelectionText = (selectElement) => {
+            const selectedData = selectElement.select2('data');
+            return selectedData && selectedData.length > 0 ? selectedData.map(item => item.text).join('+') : null;
+        };
+
+        const strategyNameString = getOrderedSelectionText(strategySelect);
+        // 使用真正的用户选择顺序
+        const candlePatternNameString = candlePatternOrder.length > 0 ? candlePatternOrder.join('+') : null;
+        const longTrendNameString = getOrderedSelectionText(longTrendSelect);
+        const midTrendNameString = getOrderedSelectionText(midTrendSelect);
+
+        // 构建发送到后端的数据
+        const formData = {
+            transaction_time: transactionDatetime,
+            operation_time: operationTimeValue ? operationTimeValue.replace('T', ' ') : transactionDatetime, // 格式化操作时间
+            contract_code: contractCode,
+            name: name,
+            account: accountValue || '华安期货', // 如果为空则使用默认值
+            position_type: positionType,
+            volume: volume,
+            price: price,
+            contract_multiplier: multiplier,
+            amount: amount,         // 发送计算好的值
+            fee: fee,             // 发送计算好的值
+            volume_change: volumeChange, // 发送计算好的值
+            margin: margin,         // 发送计算好的值
+            strategy_name: strategyNameString, // 发送名称字符串
+            candle_pattern_name: candlePatternNameString, // K线形态名称
+            long_trend_name: longTrendNameString,       // 长期趋势名称
+            mid_trend_name: midTrendNameString,        // 中期趋势名称
+            trade_type: parseInt(tradeTypeValue),       // 交易类型
+            trade_status: parseInt(tradeStatusValue),     // 交易状态
+            stop_loss_price: stopLossPriceValue ? parseFloat(stopLossPriceValue) : null, // 止损价
+            confidence_index: confidenceIndexValue ? parseInt(confidenceIndexValue) : null, // 信心指数
+            notes: notesValue      // 包含备注
+            // similarity_evaluation: similarity_evaluation, // 如果添加了该字段
+        };
+
+        // 发送数据到后端 /api/create 接口
+        fetch('/transaction/api/create', {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify(formData)
+        })
+        .then(response => response.json())
+        .then(data => {
+            if (data.code === 0) {
+                alert('添加成功!\n' + data.msg); // 显示后端返回的完整消息
+                // 成功后可以跳转回列表页
+                window.location.href = "{{ url_for('transaction.index') }}";
+            } else {
+                alert('添加失败:' + data.msg);
+            }
+        })
+        .catch(error => {
+            console.error('Error:', error);
+            alert('发生错误,请查看控制台了解详情');
+        });
+    });
+
+    form.addEventListener('reset', function() {
+         // 清空 Select2
+         strategySelect.val(null).trigger('change');
+         candlePatternSelect.val(null).trigger('change');
+         longTrendSelect.val(null).trigger('change');
+         midTrendSelect.val(null).trigger('change');
+         // 清空只读和隐藏字段
+         nameInput.value = '';
+         multiplierInput.value = '';
+         openFeeRateInput.value = '';
+         closeFeeRateInput.value = '';
+         longMarginRateInput.value = '';
+         shortMarginRateInput.value = '';
+    });
+});
+</script>
+{% endblock %} 

+ 267 - 0
app/templates/transaction/detail.html

@@ -0,0 +1,267 @@
+{% extends 'base.html' %}
+
+{% block title %}交易记录详情 - 期货数据管理系统{% endblock %}
+
+{% block content %}
+<div class="container-fluid">
+    <div class="row mb-3">
+        <div class="col">
+            <h2>交易记录详情 - ID: {{ transaction.id }}</h2>
+        </div>
+        <div class="col-auto">
+            <a href="{{ url_for('transaction.index') }}" class="btn btn-secondary">
+                <i class="fas fa-arrow-left"></i> 返回列表
+            </a>
+            <a href="{{ url_for('transaction.edit', id=transaction.id) }}" class="btn btn-warning">
+                <i class="fas fa-edit"></i> 编辑
+            </a>
+        </div>
+    </div>
+
+    <div class="card">
+        <div class="card-header">
+            <h5 class="card-title mb-0">交易信息</h5>
+        </div>
+        <div class="card-body">
+            <table class="table table-bordered table-striped table-sm">
+                <tbody>
+                    <tr>
+                        <th style="width: 200px">ID</th>
+                        <td>{{ transaction.id }}</td>
+                    </tr>
+                    <tr>
+                        <th>交易 ID (trade_id)</th>
+                        <td>{{ transaction.trade_id or 'N/A' }}</td>
+                    </tr>
+                    <tr>
+                        <th>成交时间</th>
+                        <td>{{ transaction.transaction_time }}</td>
+                    </tr>
+                    <tr>
+                        <th>合约代码</th>
+                        <td>{{ transaction.contract_code or 'N/A' }}</td>
+                    </tr>
+                    <tr>
+                        <th>名称</th>
+                        <td>{{ transaction.name or 'N/A' }}</td>
+                    </tr>
+                    <tr>
+                        <th>账户</th>
+                        <td>{{ transaction.account or 'N/A' }}</td>
+                    </tr>
+                    <tr>
+                        <th>操作策略</th>
+                        <td>{{ transaction.strategy_name or 'N/A' }}</td>
+                    </tr>
+                    <tr>
+                        <th>多空仓位</th>
+                        <td id="detail-position_type">{{ transaction.position_type }}</td>
+                    </tr>
+                    <tr>
+                        <th>K线形态</th>
+                        <td>{{ transaction.candle_pattern or 'N/A' }}</td>
+                    </tr>
+                    <tr>
+                        <th>成交价格</th>
+                        <td id="detail-price">{{ transaction.price }}</td>
+                    </tr>
+                     <tr>
+                        <th>成交手数</th>
+                        <td id="detail-volume">{{ transaction.volume }}</td>
+                    </tr>
+                     <tr>
+                        <th>成交金额</th>
+                        <td id="detail-amount">{{ transaction.amount }}</td>
+                    </tr>
+                     <tr>
+                        <th>保证金</th>
+                        <td id="detail-margin">{{ transaction.margin }}</td>
+                    </tr>
+                     <tr>
+                        <th>交易类别</th>
+                        <td id="detail-trade_type">{{ transaction.trade_type }}</td>
+                    </tr>
+                     <tr>
+                        <th>交易状态</th>
+                        <td id="detail-trade_status">{{ transaction.trade_status }}</td>
+                    </tr>
+                     <tr>
+                        <th>最新价格</th>
+                        <td id="detail-latest_price">{{ transaction.latest_price }}</td>
+                    </tr>
+                     <tr>
+                        <th>实际收益率</th>
+                        <td id="detail-actual_profit_rate" class="profit-loss">{{ transaction.actual_profit_rate }}</td>
+                    </tr>
+                     <tr>
+                        <th>实际收益</th>
+                        <td id="detail-actual_profit" class="profit-loss">{{ transaction.actual_profit }}</td>
+                    </tr>
+                     <tr>
+                        <th>止损价格</th>
+                        <td id="detail-stop_loss_price">{{ transaction.stop_loss_price }}</td>
+                    </tr>
+                     <tr>
+                        <th>止损比例</th>
+                        <td id="detail-stop_loss_rate" class="profit-loss">{{ transaction.stop_loss_rate }}</td>
+                    </tr>
+                     <tr>
+                        <th>止损收益</th>
+                        <td id="detail-stop_loss_profit" class="profit-loss">{{ transaction.stop_loss_profit }}</td>
+                    </tr>
+                    <tr>
+                        <th>操作时间</th>
+                        <td>{{ transaction.operation_time }}</td>
+                    </tr>
+                    <tr>
+                        <th>信心指数</th>
+                        <td id="detail-confidence_index">{{ transaction.confidence_index }}</td>
+                    </tr>
+                    <tr>
+                        <th>相似度评估</th>
+                        <td id="detail-similarity_evaluation">{{ transaction.similarity_evaluation }}</td>
+                    </tr>
+                    <tr>
+                        <th>长期趋势名称</th>
+                        <td>{{ transaction.long_trend_name or 'N/A' }}</td>
+                    </tr>
+                    <tr>
+                        <th>中期趋势名称</th>
+                        <td>{{ transaction.mid_trend_name or 'N/A' }}</td>
+                    </tr>
+                    <!-- 移除 BRD 未提及的字段 -->
+                    <!-- 
+                    <tr>
+                        <th>是否平今</th>
+                        <td>{{ '是' if transaction.is_close_today else '否' }}</td>
+                    </tr>
+                    <tr>
+                        <th>关联开仓记录ID</th>
+                        <td>{{ transaction.related_open_id or 'N/A' }}</td>
+                    </tr>
+                    <tr>
+                        <th>备注</th>
+                        <td>{{ transaction.notes or '无' }}</td>
+                    </tr>
+                    <tr>
+                        <th>创建时间</th>
+                        <td>{{ transaction.created_at.strftime('%Y-%m-%d %H:%M:%S') if transaction.created_at else 'N/A' }}</td>
+                    </tr>
+                    <tr>
+                        <th>更新时间</th>
+                        <td>{{ transaction.updated_at.strftime('%Y-%m-%d %H:%M:%S') if transaction.updated_at else 'N/A' }}</td>
+                    </tr> 
+                    -->
+                </tbody>
+            </table>
+        </div>
+    </div>
+</div>
+{% endblock %}
+
+{% block scripts %}
+<script>
+// 复用 index 页面的格式化函数
+function formatNumber(num, precision = 3) {
+    if (num === null || num === undefined || isNaN(num)) {
+        return 'N/A'; // 详情页用 N/A
+    }
+    let fixedNum = Number(parseFloat(num).toFixed(precision)); 
+    return fixedNum.toString();
+}
+
+function formatPercentage(num, precision = 2) {
+    if (num === null || num === undefined || isNaN(num)) {
+        return 'N/A';
+    }
+    let percentage = parseFloat(num) * 100;
+    return formatNumber(percentage, precision) + '%';
+}
+
+function getPositionTypeText(type) {
+    const types = { 0: '开多', 1: '平多', 2: '开空', 3: '平空' };
+    return types[type] !== undefined ? types[type] : '未知';
+}
+
+function getTradeTypeText(type) {
+    return type === 1 ? '真实交易' : (type === 0 ? '模拟交易' : '未知');
+}
+
+function getTradeStatusText(status) {
+    const statuses = { 0: '进行中', 1: '已暂停', 2: '暂停进行', 3: '已结束' };
+    return statuses[status] !== undefined ? statuses[status] : '未知';
+}
+
+document.addEventListener('DOMContentLoaded', function() {
+    // 将 applyProfitLossClass 移到内部,确保在 DOM 加载后可用
+    function applyProfitLossClass(elementId) {
+        const element = document.getElementById(elementId);
+        if (!element) return;
+        const rawValue = element.textContent.replace('%', ''); // 去掉百分号(如果存在)
+        if (rawValue === 'N/A' || isNaN(parseFloat(rawValue))) {
+            element.classList.remove('text-danger', 'text-success');
+            return;
+        }
+        const num = parseFloat(rawValue);
+        element.classList.remove('text-danger', 'text-success'); // 先移除旧样式
+        if (num > 0) element.classList.add('text-danger');
+        else if (num < 0) element.classList.add('text-success');
+    }
+
+    // 获取原始数据 - 不再需要,将从 DOM 读取
+    // const transactionData = {{ transaction | tojson | safe }};
+
+    // 格式化显示 - 直接读取并更新 DOM 元素内容
+    const positionTypeElement = document.getElementById('detail-position_type');
+    if (positionTypeElement) positionTypeElement.textContent = getPositionTypeText(parseFloat(positionTypeElement.textContent));
+
+    const priceElement = document.getElementById('detail-price');
+    if (priceElement) priceElement.textContent = formatNumber(parseFloat(priceElement.textContent), 3);
+
+    const volumeElement = document.getElementById('detail-volume');
+    if (volumeElement) volumeElement.textContent = formatNumber(parseFloat(volumeElement.textContent), 0);
+
+    const amountElement = document.getElementById('detail-amount');
+    if (amountElement) amountElement.textContent = formatNumber(parseFloat(amountElement.textContent), 2);
+
+    const marginElement = document.getElementById('detail-margin');
+    if (marginElement) marginElement.textContent = formatNumber(parseFloat(marginElement.textContent), 2);
+    
+    const tradeTypeElement = document.getElementById('detail-trade_type');
+    if (tradeTypeElement) tradeTypeElement.textContent = getTradeTypeText(parseFloat(tradeTypeElement.textContent));
+
+    const tradeStatusElement = document.getElementById('detail-trade_status');
+    if (tradeStatusElement) tradeStatusElement.textContent = getTradeStatusText(parseFloat(tradeStatusElement.textContent));
+
+    const latestPriceElement = document.getElementById('detail-latest_price');
+    if (latestPriceElement) latestPriceElement.textContent = formatNumber(parseFloat(latestPriceElement.textContent), 3);
+
+    const actualProfitRateElement = document.getElementById('detail-actual_profit_rate');
+    if (actualProfitRateElement) actualProfitRateElement.textContent = formatPercentage(parseFloat(actualProfitRateElement.textContent), 2);
+
+    const actualProfitElement = document.getElementById('detail-actual_profit');
+    if (actualProfitElement) actualProfitElement.textContent = formatNumber(parseFloat(actualProfitElement.textContent), 2);
+
+    const stopLossPriceElement = document.getElementById('detail-stop_loss_price');
+    if (stopLossPriceElement) stopLossPriceElement.textContent = formatNumber(parseFloat(stopLossPriceElement.textContent), 3);
+
+    const stopLossRateElement = document.getElementById('detail-stop_loss_rate');
+    if (stopLossRateElement) stopLossRateElement.textContent = formatPercentage(parseFloat(stopLossRateElement.textContent), 2);
+
+    const stopLossProfitElement = document.getElementById('detail-stop_loss_profit');
+    if (stopLossProfitElement) stopLossProfitElement.textContent = formatNumber(parseFloat(stopLossProfitElement.textContent), 2);
+
+    const confidenceIndexElement = document.getElementById('detail-confidence_index');
+    if (confidenceIndexElement) confidenceIndexElement.textContent = formatNumber(parseFloat(confidenceIndexElement.textContent), 0);
+
+    const similarityEvaluationElement = document.getElementById('detail-similarity_evaluation');
+    if (similarityEvaluationElement) similarityEvaluationElement.textContent = formatPercentage(parseFloat(similarityEvaluationElement.textContent), 1);
+
+    // 应用盈亏颜色
+    applyProfitLossClass('detail-actual_profit_rate');
+    applyProfitLossClass('detail-actual_profit');
+    applyProfitLossClass('detail-stop_loss_rate');
+    applyProfitLossClass('detail-stop_loss_profit');
+});
+</script>
+{% endblock %} 

+ 563 - 0
app/templates/transaction/edit.html

@@ -0,0 +1,563 @@
+{% extends 'base.html' %}
+
+{% block title %}编辑交易记录 - 期货数据管理系统{% endblock %}
+
+{% block styles %}
+<!-- 引入 Select2 CSS -->
+<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css">
+<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ttskch/select2-bootstrap4-theme@x.x.x/dist/select2-bootstrap4.min.css"> <!-- Select2 Bootstrap 4 主题 -->
+<style>
+    .select2-container--bootstrap4 .select2-selection--multiple {
+        min-height: calc(1.5em + .75rem + 2px);
+    }
+</style>
+{% endblock %}
+
+{% block content %}
+<div class="container-fluid">
+    <div class="row mb-3">
+        <div class="col">
+            <h2>编辑交易记录</h2>
+        </div>
+        <div class="col-auto">
+            <a href="{{ url_for('transaction.index') }}" class="btn btn-secondary">
+                <i class="fas fa-arrow-left"></i> 返回列表
+            </a>
+        </div>
+    </div>
+
+    <div class="card">
+        <div class="card-body">
+            <form id="editForm" class="needs-validation" novalidate>
+                 <input type="hidden" id="id" name="id">
+
+                 <div class="row">
+                     <!-- 第一列 -->
+                     <div class="col-md-6">
+                         <div class="mb-3">
+                             <label for="trade_id" class="form-label">交易 ID (Trade Record ID)</label>
+                             <input type="number" class="form-control" id="trade_id" name="trade_id">
+                             <div class="form-text">关联的交易汇总记录 ID。同一个 ID 最多出现两次。</div>
+                         </div>
+                         <div class="mb-3">
+                             <label for="transaction_time" class="form-label">成交时间 <span class="text-danger">*</span></label>
+                             <input type="datetime-local" class="form-control" id="transaction_time" name="transaction_time" required>
+                             <div class="invalid-feedback">请选择成交时间</div>
+                         </div>
+                         <div class="mb-3">
+                             <label for="operation_time" class="form-label">操作时间</label>
+                             <input type="datetime-local" class="form-control" id="operation_time" name="operation_time">
+                         </div>
+                         <div class="mb-3">
+                             <label for="contract_code" class="form-label">合约代码</label>
+                             <input type="text" class="form-control" id="contract_code" name="contract_code">
+                         </div>
+                         <div class="mb-3">
+                             <label for="name" class="form-label">名称</label>
+                             <input type="text" class="form-control" id="name" name="name" readonly>
+                             <div class="form-text">根据合约代码自动生成,不可编辑。</div>
+                         </div>
+                         <div class="mb-3">
+                            <label for="account" class="form-label">账户</label>
+                            <input type="text" class="form-control" id="account" name="account" value="华安期货"> <!-- 默认值 -->
+                         </div>
+                         <div class="mb-3">
+                            <label for="position_type" class="form-label">多空仓位 <span class="text-danger">*</span></label>
+                            <select class="form-select" id="position_type" name="position_type" required>
+                                <option value="0">开多</option>
+                                <option value="1">平多</option>
+                                <option value="2">开空</option>
+                                <option value="3">平空</option>
+                            </select>
+                            <div class="invalid-feedback">请选择多空仓位</div>
+                         </div>
+                         <div class="mb-3">
+                             <label for="price" class="form-label">成交价格 <span class="text-danger">*</span></label>
+                             <input type="number" class="form-control" id="price" name="price" required step="0.001"> <!-- 允许最多3位小数 -->
+                             <div class="invalid-feedback">请输入成交价格</div>
+                         </div>
+                         <div class="mb-3">
+                             <label for="volume" class="form-label">成交手数 <span class="text-danger">*</span></label>
+                             <input type="number" class="form-control" id="volume" name="volume" required step="1" min="0">
+                             <div class="invalid-feedback">请输入有效的整数成交手数</div>
+                         </div>
+                          <div class="mb-3">
+                             <label for="contract_multiplier" class="form-label">合约乘数</label>
+                             <input type="number" class="form-control" id="contract_multiplier" name="contract_multiplier" step="any">
+                         </div>
+                          <div class="mb-3">
+                             <label for="fee" class="form-label">手续费</label>
+                             <input type="number" class="form-control" id="fee" name="fee" step="any" min="0">
+                         </div>
+                         <div class="mb-3">
+                             <label for="amount" class="form-label">成交金额</label>
+                             <input type="number" class="form-control" id="amount" name="amount" readonly step="any">
+                             <div class="form-text">根据价格、手数、乘数自动计算,不可编辑。</div>
+                         </div>
+                         <div class="mb-3">
+                             <label for="margin" class="form-label">保证金</label>
+                             <input type="number" class="form-control" id="margin" name="margin" readonly step="any">
+                             <div class="form-text">根据成交金额和保证金率自动计算,不可编辑。</div>
+                         </div>
+                     </div>
+                     <!-- 第二列 -->
+                     <div class="col-md-6">
+                         <div class="mb-3">
+                             <label for="strategy_name" class="form-label">操作策略</label>
+                             <select class="form-control select2" id="strategy_name" name="strategy_name" multiple="multiple" data-placeholder="选择策略">
+                                 <!-- 选项将通过API加载 -->
+                             </select>
+                             <input type="hidden" id="strategy_ids" name="strategy_ids">
+                         </div>
+                         <div class="mb-3">
+                             <label for="candle_pattern" class="form-label">K线形态</label>
+                             <select class="form-control select2" id="candle_pattern" name="candle_pattern" multiple="multiple" data-placeholder="选择K线形态">
+                                 <!-- 选项将通过API加载 -->
+                             </select>
+                             <input type="hidden" id="candle_pattern_ids" name="candle_pattern_ids">
+                         </div>
+                         <div class="mb-3">
+                             <label for="trade_type" class="form-label">交易类别</label>
+                             <select class="form-select" id="trade_type" name="trade_type">
+                                 <option value="0">模拟交易</option>
+                                 <option value="1">真实交易</option>
+                             </select>
+                         </div>
+                         <div class="mb-3">
+                             <label for="trade_status" class="form-label">交易状态</label>
+                             <select class="form-select" id="trade_status" name="trade_status">
+                                 <option value="0">进行中</option>
+                                 <option value="1">已暂停</option>
+                                 <option value="2">暂停进行</option>
+                                 <option value="3">已结束</option>
+                             </select>
+                         </div>
+                         <div class="mb-3">
+                              <label for="latest_price" class="form-label">最新价格</label>
+                              <input type="number" class="form-control" id="latest_price" name="latest_price" step="0.001">
+                          </div>
+                          <div class="mb-3">
+                             <label for="actual_profit_rate" class="form-label">实际收益率</label>
+                             <input type="text" class="form-control" id="actual_profit_rate" name="actual_profit_rate" readonly>
+                             <div class="form-text">根据最新价格和成交价格计算,不可编辑。</div>
+                         </div>
+                          <div class="mb-3">
+                              <label for="actual_profit" class="form-label">实际收益</label>
+                              <input type="number" class="form-control" id="actual_profit" name="actual_profit" readonly step="any">
+                              <div class="form-text">根据最新价格和成交价格计算,不可编辑。</div>
+                          </div>
+                          <div class="mb-3">
+                              <label for="stop_loss_price" class="form-label">止损价格</label>
+                              <input type="number" class="form-control" id="stop_loss_price" name="stop_loss_price" step="0.001">
+                          </div>
+                          <div class="mb-3">
+                             <label for="stop_loss_rate" class="form-label">止损比例</label>
+                             <input type="text" class="form-control" id="stop_loss_rate" name="stop_loss_rate" readonly>
+                             <div class="form-text">根据止损价格和成交价格计算,不可编辑。</div>
+                         </div>
+                         <div class="mb-3">
+                             <label for="stop_loss_profit" class="form-label">止损收益</label>
+                             <input type="number" class="form-control" id="stop_loss_profit" name="stop_loss_profit" readonly step="any">
+                             <div class="form-text">根据止损价格和成交价格计算,不可编辑。</div>
+                         </div>
+                          <div class="mb-3">
+                             <label for="confidence_index" class="form-label">信心指数</label>
+                             <input type="number" class="form-control" id="confidence_index" name="confidence_index" step="1" min="0">
+                         </div>
+                         <div class="mb-3">
+                             <label for="similarity_evaluation" class="form-label">相似度评估 (%)</label>
+                             <input type="number" class="form-control" id="similarity_evaluation" name="similarity_evaluation" step="0.1">
+                         </div>
+                         <div class="mb-3">
+                             <label for="long_trend_name" class="form-label">长期趋势</label>
+                             <select class="form-control select2" id="long_trend_name" name="long_trend_name" multiple="multiple" data-placeholder="选择长期趋势">
+                                 <!-- 选项将通过API加载 -->
+                             </select>
+                             <input type="hidden" id="long_trend_ids" name="long_trend_ids">
+                         </div>
+                         <div class="mb-3">
+                             <label for="mid_trend_name" class="form-label">中期趋势</label>
+                             <select class="form-control select2" id="mid_trend_name" name="mid_trend_name" multiple="multiple" data-placeholder="选择中期趋势">
+                                 <!-- 选项将通过API加载 -->
+                             </select>
+                             <input type="hidden" id="mid_trend_ids" name="mid_trend_ids">
+                         </div>
+                     </div>
+                 </div>
+
+                 <div class="row mt-3">
+                     <!-- 按钮行 -->
+                     <div class="col-12 text-end">
+                           <button type="submit" class="btn btn-primary">保存更改</button>
+                           <button type="button" id="cancelBtn" class="btn btn-secondary">取消</button>
+                      </div>
+                 </div>
+            </form>
+        </div>
+    </div>
+</div>
+{% endblock %}
+
+{% block scripts %}
+<!-- 引入 Select2 JS -->
+<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
+<!-- 引入 dayjs 处理日期时间 -->
+<script src="https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/utc.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/timezone.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/customParseFormat.js"></script>
+<script>
+    dayjs.extend(dayjs_plugin_utc);
+    dayjs.extend(dayjs_plugin_timezone);
+    dayjs.extend(dayjs_plugin_customParseFormat);
+    // 设置默认时区(如果需要)
+    // dayjs.tz.setDefault("Asia/Shanghai");
+
+// --- 复用格式化函数 ---
+function formatNumber(num, precision = 3) {
+    if (num === null || num === undefined || isNaN(num)) return '';
+    let fixedNum = Number(parseFloat(num).toFixed(precision));
+    return fixedNum.toString();
+}
+function formatPercentage(num, precision = 2) {
+    if (num === null || num === undefined || isNaN(num)) return '';
+    let percentage = parseFloat(num) * 100;
+    return formatNumber(percentage, precision) + '%';
+}
+
+document.addEventListener('DOMContentLoaded', function() {
+    const transactionId = "{{ transaction_id }}";
+    const form = document.getElementById('editForm');
+
+    // --- 辅助函数:加载 Select2 选项 --- 
+    function loadSelect2Options(selectElementId, apiUrl, idField = 'id', nameField = 'name', processResults = null) {
+        // Return the fetch promise
+        return fetch(apiUrl)
+            .then(response => response.json())
+            .then(data => {
+                if (data.code === 0 && data.data) {
+                    const selectElement = $(`#${selectElementId}`);
+                    selectElement.empty(); // 清空现有选项
+                    let results = data.data;
+                    if (processResults) {
+                        results = processResults(results);
+                    }
+                    results.forEach(item => {
+                        // Ensure the option value is a string
+                        const option = new Option(item[nameField], String(item[idField]));
+                        selectElement.append(option);
+                    });
+                    selectElement.trigger('change'); // 更新 Select2 显示
+                    return true; // Indicate success
+                } else {
+                    console.error(`加载 ${selectElementId} 选项失败:`, data.msg);
+                    return false; // Indicate failure
+                }
+            })
+            .catch(error => {
+                console.error(`加载 ${selectElementId} 选项请求失败:`, error);
+                return false; // Indicate failure
+            });
+    }
+
+    // --- 辅助函数:填充表单 --- 
+    function populateForm(data) {
+        if (!data) return;
+        console.log("Populating form with data:", data);
+        document.getElementById('id').value = data.id || '';
+        document.getElementById('trade_id').value = data.trade_id || '';
+        
+        // 处理日期时间 (从 YYYY-MM-DD HH:MM 转换为 datetime-local 需要的 YYYY-MM-DDTHH:mm)
+        if (data.transaction_time) {
+            const transactionTime = dayjs(data.transaction_time, 'YYYY-MM-DD HH:mm').format('YYYY-MM-DDTHH:mm');
+            document.getElementById('transaction_time').value = transactionTime;
+        }
+         if (data.operation_time) {
+            const operationTime = dayjs(data.operation_time, 'YYYY-MM-DD HH:mm').format('YYYY-MM-DDTHH:mm');
+            document.getElementById('operation_time').value = operationTime;
+        } else if (data.transaction_time) { // 如果操作时间为空,默认使用成交时间
+             const transactionTime = dayjs(data.transaction_time, 'YYYY-MM-DD HH:mm').format('YYYY-MM-DDTHH:mm');
+            document.getElementById('operation_time').value = transactionTime;
+        }
+
+        document.getElementById('contract_code').value = data.contract_code || '';
+        document.getElementById('name').value = data.name || ''; // 名称不可编辑
+        document.getElementById('account').value = data.account || '华安期货';
+        document.getElementById('position_type').value = data.position_type !== null ? data.position_type.toString() : '0';
+        document.getElementById('price').value = formatNumber(data.price, 3);
+        document.getElementById('volume').value = formatNumber(data.volume, 0);
+        document.getElementById('contract_multiplier').value = data.contract_multiplier || '';
+        document.getElementById('amount').value = formatNumber(data.amount, 2); // 不可编辑
+        document.getElementById('margin').value = formatNumber(data.margin, 2); // 不可编辑
+        document.getElementById('fee').value = data.fee || '';
+        document.getElementById('trade_type').value = data.trade_type !== null ? data.trade_type.toString() : '0';
+        document.getElementById('trade_status').value = data.trade_status !== null ? data.trade_status.toString() : '0';
+        document.getElementById('latest_price').value = formatNumber(data.latest_price, 3);
+        document.getElementById('actual_profit_rate').value = formatPercentage(data.actual_profit_rate, 2); // 不可编辑
+        document.getElementById('actual_profit').value = formatNumber(data.actual_profit, 2); // 不可编辑
+        document.getElementById('stop_loss_price').value = formatNumber(data.stop_loss_price, 3);
+        document.getElementById('stop_loss_rate').value = formatPercentage(data.stop_loss_rate, 2); // 不可编辑
+        document.getElementById('stop_loss_profit').value = formatNumber(data.stop_loss_profit, 2); // 不可编辑
+        document.getElementById('confidence_index').value = formatNumber(data.confidence_index, 0);
+        document.getElementById('similarity_evaluation').value = data.similarity_evaluation !== null && data.similarity_evaluation !== undefined ? data.similarity_evaluation : '';
+
+        // 填充 Select2 多选框 (在选项加载完成后进行)
+        if (data.strategy_ids) {
+            const ids = data.strategy_ids.split(',').map(id => String(id.trim())).filter(id => id);
+            $('#strategy_name').val(ids).trigger('change');
+        }
+        if (data.candle_pattern_ids) {
+            const ids = data.candle_pattern_ids.split(',').map(id => String(id.trim())).filter(id => id);
+            $('#candle_pattern').val(ids).trigger('change');
+        }
+        if (data.long_trend_ids) {
+            const ids = data.long_trend_ids.split(',').map(id => String(id.trim())).filter(id => id);
+            $('#long_trend_name').val(ids).trigger('change');
+        }
+         if (data.mid_trend_ids) {
+            const ids = data.mid_trend_ids.split(',').map(id => String(id.trim())).filter(id => id);
+            $('#mid_trend_name').val(ids).trigger('change');
+        }
+    }
+
+    // --- 初始化页面 --- 
+    function initializePage() {
+        const transactionId = "{{ transaction_id }}";
+
+        // 1. 初始化 Select2 (同步)
+        $('.select2').select2({
+            theme: 'bootstrap4',
+            width: '100%',
+            tags: false
+        });
+
+        // 2. 加载 Select2 选项 (异步)
+        const loadStrategyOptions = loadSelect2Options('strategy_name', '/api/dimension/strategy/list_all', 'id', 'name');
+        const loadCandleOptions = loadSelect2Options('candle_pattern', '/api/dimension/candle/list_all', 'id', 'name');
+        const loadLongTrendOptions = loadSelect2Options('long_trend_name', '/api/dimension/trend/list_all', 'id', 'name');
+        const loadMidTrendOptions = loadSelect2Options('mid_trend_name', '/api/dimension/trend/list_all', 'id', 'name');
+
+        // 3. 等待所有选项加载完成后,加载并填充表单数据 (异步)
+        Promise.all([loadStrategyOptions, loadCandleOptions, loadLongTrendOptions, loadMidTrendOptions])
+            .then(loadResults => {
+                console.log("All Select2 options loaded successfully:", loadResults);
+                if (transactionId && transactionId !== '0') {
+                    console.log(`[Edit Page] Fetching data for transaction ID: ${transactionId}`);
+                    return fetch(`/transaction/api/detail/${transactionId}`);
+                } else {
+                    console.log("[Edit Page] No valid transaction ID provided, treating as new or error.");
+                    // 设置默认值(如果不是编辑现有记录)
+                    document.getElementById('account').value = '华安期货';
+                    document.getElementById('operation_time').value = dayjs().format('YYYY-MM-DDTHH:mm');
+                    document.getElementById('transaction_time').value = dayjs().format('YYYY-MM-DDTHH:mm');
+                    return Promise.reject('No Transaction ID'); // Skip fetching and populating
+                }
+            })
+            .then(response => response.json()) // 只在有 transactionId 时执行
+            .then(responseData => {
+                console.log("[Edit Page] Received data from API:", responseData);
+                if (responseData.code === 0 && responseData.data) {
+                    console.log("[Edit Page] Data is valid, calling populateForm...");
+                    populateForm(responseData.data);
+                } else if (responseData.code !== undefined) { // Check if it's a valid API response
+                    console.error('[Edit Page] API returned error or no data:', responseData.msg);
+                    alert('加载交易信息失败:' + (responseData.msg || '未知错误'));
+                } // Else: 'No Transaction ID' case was handled by Promise.reject
+            })
+            .catch(error => {
+                if (error === 'No Transaction ID') {
+                    // Expected case when creating a new record or ID is missing
+                } else {
+                    console.error('[Edit Page] Error during initialization:', error);
+                    alert('页面初始化或加载交易信息时出错: ' + error.message);
+                }
+            });
+    }
+
+    initializePage(); // 执行初始化
+
+    // --- 表单提交处理 --- 
+    form.addEventListener('submit', function(event) {
+        event.preventDefault();
+
+        if (!form.checkValidity()) {
+            event.stopPropagation();
+            form.classList.add('was-validated');
+            return;
+        }
+
+        // 收集表单数据
+        const formData = new FormData(form);
+        const data = {};
+        formData.forEach((value, key) => {
+             // 处理 Select2 多选字段 (取 ID)
+             if (key === 'strategy_name') {
+                 data['strategy_ids'] = $('#strategy_name').val() ? $('#strategy_name').val().join(',') : null;
+                 data['strategy_name'] = $('#strategy_name').select2('data').map(item => item.text).join('+');
+                 return; // 跳过原始 key
+             }
+             if (key === 'candle_pattern') {
+                 data['candle_pattern_ids'] = $('#candle_pattern').val() ? $('#candle_pattern').val().join(',') : null;
+                 data['candle_pattern'] = $('#candle_pattern').select2('data').map(item => item.text).join('+');
+                 return;
+             }
+             if (key === 'long_trend_name') {
+                 data['long_trend_ids'] = $('#long_trend_name').val() ? $('#long_trend_name').val().join(',') : null;
+                 data['long_trend_name'] = $('#long_trend_name').select2('data').map(item => item.text).join('+');
+                 return;
+             }
+             if (key === 'mid_trend_name') {
+                 data['mid_trend_ids'] = $('#mid_trend_name').val() ? $('#mid_trend_name').val().join(',') : null;
+                 data['mid_trend_name'] = $('#mid_trend_name').select2('data').map(item => item.text).join('+');
+                 return;
+             }
+             // 对于空字符串,根据需要转为 null 或保持空字符串
+             // 这里我们将空字符串转为 null,除非它是某些特定字段 (如 account 可以为空但不能是 null)
+             if (value === '' && key !== 'account' && key !== 'contract_code' /* Add other keys that can be empty string */) {
+                 data[key] = null;
+             } else {
+                  // 转换数字类型
+                 if ([ 'trade_id', 'position_type', 'volume', 'trade_type', 'trade_status', 'confidence_index'].includes(key)) {
+                     data[key] = value ? parseInt(value) : null;
+                 } else if ([ 'price', 'contract_multiplier', 'fee', 'latest_price', 'stop_loss_price', 'similarity_evaluation'].includes(key)) {
+                     data[key] = value ? parseFloat(value) : null;
+                 } else {
+                     data[key] = value;
+                 }
+             }
+        });
+        
+        // 移除只读字段,防止提交
+        delete data.amount;
+        delete data.margin;
+        delete data.actual_profit_rate;
+        delete data.actual_profit;
+        delete data.stop_loss_rate;
+        delete data.stop_loss_profit;
+        delete data.name; // 名称不可编辑
+
+        // 移除隐藏的ID字段的键名,避免冲突
+        delete data.strategy_ids; 
+        delete data.candle_pattern_ids;
+        delete data.long_trend_ids;
+        delete data.mid_trend_ids;
+
+        console.log('Submitting data:', data);
+
+        // 发送更新请求
+        fetch(`/transaction/api/update/${transactionId}`, {
+            method: 'PUT',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify(data)
+        })
+        .then(response => response.json())
+        .then(responseData => {
+            console.log('Server response:', responseData);
+            if (responseData.code === 0) {
+                alert('更新成功: ' + responseData.msg);
+                window.location.href = "{{ url_for('transaction.index') }}";
+            } else {
+                alert('更新失败:' + responseData.msg);
+            }
+        })
+        .catch(error => {
+            console.error('Error:', error);
+            alert('更新请求失败。');
+        });
+    });
+
+    // --- Cancel Button Handler ---
+    const cancelButton = document.getElementById('cancelBtn');
+    if (cancelButton) {
+        cancelButton.addEventListener('click', function() {
+            window.location.href = "{{ url_for('transaction.index') | safe }}"; // Added safe filter just in case
+        });
+    }
+
+    // --- 动态计算与更新 (可选,如果需要实时反馈) ---
+    function updateCalculatedFields() {
+        const price = parseFloat(document.getElementById('price').value) || 0;
+        const volume = parseInt(document.getElementById('volume').value) || 0;
+        const multiplier = parseFloat(document.getElementById('contract_multiplier').value) || 1;
+        const latestPrice = parseFloat(document.getElementById('latest_price').value);
+        const stopLossPrice = parseFloat(document.getElementById('stop_loss_price').value);
+        const positionType = parseInt(document.getElementById('position_type').value);
+
+        // 计算 Amount
+        const amount = price * volume * multiplier;
+        document.getElementById('amount').value = formatNumber(amount, 2);
+
+        // 计算 Volume Change (用于后续计算)
+        let volumeChange = 0;
+        if (positionType === 0 || positionType === 3) volumeChange = volume;
+        else if (positionType === 1 || positionType === 2) volumeChange = -volume;
+
+        // 保证金计算需要异步获取费率,这里暂时不清空或显示 N/A
+        // document.getElementById('margin').value = 'N/A'; 
+
+        // 计算实际收益率和收益
+        let actualProfitRate = null, actualProfit = null;
+        if (!isNaN(latestPrice) && price !== 0) {
+             const directionMultiplier = (positionType === 2 || positionType === 3) ? -1 : 1;
+             actualProfitRate = directionMultiplier * (latestPrice - price) / price;
+             actualProfit = (latestPrice - price) * volumeChange * multiplier;
+        }
+        document.getElementById('actual_profit_rate').value = formatPercentage(actualProfitRate, 2);
+        document.getElementById('actual_profit').value = formatNumber(actualProfit, 2);
+        
+        // 计算止损比例和收益
+        let stopLossRate = null, stopLossProfit = null;
+         if (!isNaN(stopLossPrice) && price !== 0) {
+             const directionMultiplier = (positionType === 2 || positionType === 3) ? -1 : 1;
+             stopLossRate = directionMultiplier * (stopLossPrice - price) / price;
+             stopLossProfit = (stopLossPrice - price) * volumeChange * multiplier;
+        }
+        document.getElementById('stop_loss_rate').value = formatPercentage(stopLossRate, 2);
+        document.getElementById('stop_loss_profit').value = formatNumber(stopLossProfit, 2);
+    }
+
+    // 监听相关字段变化,触发计算更新
+    ['price', 'volume', 'contract_multiplier', 'latest_price', 'stop_loss_price', 'position_type'].forEach(id => {
+        const element = document.getElementById(id);
+        if (element) {
+            element.addEventListener('input', updateCalculatedFields);
+            element.addEventListener('change', updateCalculatedFields); // For select
+        }
+    });
+
+    // 合约代码变化时,尝试更新名称 (需要后端API支持)
+    const contractCodeInput = document.getElementById('contract_code');
+    if (contractCodeInput) {
+        contractCodeInput.addEventListener('change', function() {
+            const code = this.value.trim();
+            if (code) {
+                // 示例: /api/future_info/get_name_by_code?code=CU2305
+                fetch(`/api/future_info/get_name_by_code?code=${encodeURIComponent(code)}`)
+                    .then(res => res.json())
+                    .then(resData => {
+                        if (resData.code === 0 && resData.data && resData.data.name) {
+                            document.getElementById('name').value = resData.data.name;
+                            // 如果还需要更新乘数等信息,可以在这里处理
+                             if (resData.data.contract_multiplier !== undefined) {
+                                document.getElementById('contract_multiplier').value = resData.data.contract_multiplier;
+                                updateCalculatedFields(); // 乘数变化,更新计算
+                            }
+                        } else {
+                            // 找不到或出错,可以清空名称或提示用户
+                            document.getElementById('name').value = ''; 
+                        }
+                    })
+                    .catch(err => {
+                        console.error('获取名称失败:', err);
+                        document.getElementById('name').value = ''; 
+                    });
+            } else {
+                 document.getElementById('name').value = ''; 
+            }
+        });
+    }
+});
+</script>
+{% endblock %} 

+ 211 - 0
app/templates/transaction/import.html

@@ -0,0 +1,211 @@
+{% extends 'base.html' %}
+
+{% block title %}导入交易记录 - 期货数据管理系统{% endblock %}
+
+{% block content %}
+<div class="d-flex justify-content-between align-items-center mb-4">
+    <h2>导入交易记录</h2>
+    <a href="{{ url_for('transaction.index') }}" class="btn btn-secondary">返回列表</a>
+</div>
+
+<div class="card">
+    <div class="card-body">
+        <div class="row mb-4">
+            <div class="col-md-12">
+                <p>请按照以下步骤操作:</p>
+                <ol>
+                    <li>下载导入模板</li>
+                    <li>按照模板格式填写数据</li>
+                    <li>上传Excel文件</li>
+                </ol>
+                <div class="alert alert-info">
+                    <strong>提示:</strong> 必填字段为交易ID、合约代码、合约名称、多空仓位、成交价格、成交手数。多空仓位取值:0-开多,1-平多,2-开空,3-平空。
+                </div>
+                
+                <div class="alert alert-warning">
+                    <strong>重要说明:</strong>
+                    <ol>
+                        <li>每个<strong>交易ID</strong>最多只能出现两次,一次开仓一次平仓</li>
+                        <li>具有相同交易ID的记录必须组成有效的开平仓配对:
+                            <ul>
+                                <li>开多(0)必须与平多(1)配对</li>
+                                <li>开空(2)必须与平空(3)配对</li>
+                            </ul>
+                        </li>
+                        <li>如果导入失败,请尝试<a href="#" onclick="clearCache()">清除浏览器缓存</a>或使用新的浏览器窗口重试</li>
+                    </ol>
+                </div>
+            </div>
+        </div>
+        
+        <div class="row mb-4">
+            <div class="col-md-6">
+                <button id="download-template" class="btn btn-primary">
+                    <i class="fas fa-download"></i> 下载导入模板
+                </button>
+            </div>
+        </div>
+        
+        <form id="import-form" enctype="multipart/form-data">
+            <div class="row mb-4">
+                <div class="col-md-6">
+                    <div class="form-group">
+                        <label for="file">选择Excel文件</label>
+                        <input type="file" class="form-control" id="file" name="file" accept=".xlsx" required>
+                    </div>
+                </div>
+            </div>
+            
+            <div class="form-group">
+                <button type="submit" class="btn btn-success">
+                    <i class="fas fa-upload"></i> 导入数据
+                </button>
+            </div>
+        </form>
+        
+        <div id="result" class="mt-4" style="display: none;">
+        </div>
+    </div>
+</div>
+
+<script>
+// 清除浏览器缓存的函数
+function clearCache() {
+    // 添加一个随机参数来强制刷新页面
+    const timestamp = new Date().getTime();
+    window.location.href = window.location.href.split('?')[0] + '?cache_buster=' + timestamp;
+}
+
+// 添加切换行数据显示/隐藏的函数
+function toggleRowData(btn) {
+    const rowData = btn.nextElementSibling;
+    if (rowData.style.display === 'none') {
+        rowData.style.display = 'block';
+        btn.textContent = '隐藏行数据';
+    } else {
+        rowData.style.display = 'none';
+        btn.textContent = '显示行数据';
+    }
+}
+
+// 切换元素显示/隐藏的通用函数
+function toggleElement(id) {
+    const element = document.getElementById(id);
+    if (element) {
+        if (element.style.display === 'none') {
+            element.style.display = 'block';
+        } else {
+            element.style.display = 'none';
+        }
+    }
+}
+
+document.addEventListener('DOMContentLoaded', function() {
+    const form = document.getElementById('import-form');
+    const resultDiv = document.getElementById('result');
+    const downloadBtn = document.getElementById('download-template');
+    
+    // 处理下载模板按钮点击
+    downloadBtn.addEventListener('click', function(e) {
+        // 禁用按钮防止重复点击
+        downloadBtn.disabled = true;
+        
+        // 创建一个隐藏的iframe来处理下载
+        const iframe = document.createElement('iframe');
+        iframe.style.display = 'none';
+        iframe.src = "{{ url_for('transaction.get_template') }}";
+        document.body.appendChild(iframe);
+        
+        // 3秒后重新启用按钮
+        setTimeout(() => {
+            downloadBtn.disabled = false;
+            document.body.removeChild(iframe);
+        }, 3000);
+    });
+    
+    // 处理表单提交(导入文件)
+    form.addEventListener('submit', function(e) {
+        e.preventDefault();
+        
+        const formData = new FormData(form);
+        
+        // 显示加载状态
+        resultDiv.innerHTML = '<div class="alert alert-info">正在导入数据,请稍候...</div>';
+        resultDiv.style.display = 'block';
+        
+        // 添加随机参数避免缓存
+        const cacheBuster = new Date().getTime();
+        
+        fetch(`/transaction/api/import?cache_buster=${cacheBuster}`, {
+            method: 'POST',
+            body: formData
+        })
+        .then(response => response.json())
+        .then(data => {
+            if (data.code === 0) {
+                // 成功导入
+                let html = `<div class="alert alert-success">${data.msg}</div>`;
+                
+                if (data.data.error_count > 0) {
+                    html += '<div class="alert alert-warning">';
+                    html += '<strong>导入过程中出现以下错误:</strong>';
+                    html += '<button class="btn btn-sm btn-link float-right" id="toggle-errors">显示/隐藏详情</button>';
+                    html += '<div id="error-details" style="display: none; margin-top: 10px;">';
+                    
+                    if (data.data.error_messages.length > 0) {
+                        html += '<ul class="list-group">';
+                        data.data.error_messages.forEach(msg => {
+                            let errorMsg = msg;
+                            if (typeof msg === 'string') {
+                                // 分离错误消息和行数据
+                                const parts = msg.split('\n');
+                                if (parts.length > 1) {
+                                    // 格式化显示
+                                    html += `<li class="list-group-item list-group-item-danger">${parts[0]}`;
+                                    html += `<button class="btn btn-sm btn-link" onclick="toggleRowData(this)">显示行数据</button>`;
+                                    html += `<div class="row-data" style="display:none;margin-top:10px;"><pre>${parts[1]}</pre></div>`;
+                                    html += `</li>`;
+                                } else {
+                                    html += `<li class="list-group-item list-group-item-danger">${msg}</li>`;
+                                }
+                            } else {
+                                html += `<li class="list-group-item list-group-item-danger">${msg}</li>`;
+                            }
+                        });
+                        html += '</ul>';
+                    } else {
+                        html += '<p>没有详细错误信息</p>';
+                    }
+                    
+                    html += '</div></div>';
+                }
+                
+                resultDiv.innerHTML = html;
+                
+                // 添加错误切换按钮的事件监听
+                const toggleErrorsBtn = document.getElementById('toggle-errors');
+                if (toggleErrorsBtn) {
+                    toggleErrorsBtn.addEventListener('click', function() {
+                        const errorDetails = document.getElementById('error-details');
+                        if (errorDetails.style.display === 'none') {
+                            errorDetails.style.display = 'block';
+                            this.textContent = '隐藏详情';
+                        } else {
+                            errorDetails.style.display = 'none';
+                            this.textContent = '显示详情';
+                        }
+                    });
+                }
+            } else {
+                // 导入失败
+                resultDiv.innerHTML = `<div class="alert alert-danger">${data.msg}</div>`;
+            }
+        })
+        .catch(error => {
+            console.error('Error:', error);
+            resultDiv.innerHTML = '<div class="alert alert-danger">导入失败,请查看控制台了解详情</div>';
+        });
+    });
+});
+</script>
+{% endblock %} 

+ 198 - 0
app/templates/transaction/import.html.bak

@@ -0,0 +1,198 @@
+{% extends "base.html" %}
+
+{% block title %}导入交易记录{% endblock %}
+
+{% block content %}
+<div class="container-fluid">
+    <div class="row">
+        <div class="col-12">
+            <div class="card">
+                <div class="card-header">
+                    <h3 class="card-title">导入交易记录</h3>
+                </div>
+                <div class="card-body">
+                    <div class="alert alert-info">
+                        <h5><i class="icon fas fa-info"></i> 导入说明</h5>
+                        <ol>
+                            <li>下载导入模板Excel文件</li>
+                            <li>按照模板格式填写交易记录数据</li>
+                            <li>上传填写好的Excel文件</li>
+                        </ol>
+                        <p class="mb-0">
+                            <strong>必填字段:</strong>合约代码、名称、多空仓位(0-开多,1-平多,2-开空,3-平空)、成交价格、成交手数
+                        </p>
+                    </div>
+
+                    <div class="row">
+                        <div class="col-md-6">
+                            <!-- 下载模板按钮 -->
+                            <div class="form-group">
+                                <a href="{{ url_for('transaction.get_template') }}" class="btn btn-info">
+                                    <i class="fas fa-download"></i> 下载导入模板
+                                </a>
+                            </div>
+
+                            <!-- 上传表单 -->
+                            <form id="uploadForm" enctype="multipart/form-data">
+                                <div class="form-group">
+                                    <label for="file">选择文件</label>
+                                    <div class="custom-file">
+                                        <input type="file" class="custom-file-input" id="file" name="file" accept=".xlsx">
+                                        <label class="custom-file-label" for="file">选择Excel文件</label>
+                                    </div>
+                                </div>
+                                <div class="form-group">
+                                    <button type="submit" class="btn btn-primary">
+                                        <i class="fas fa-upload"></i> 开始导入
+                                    </button>
+                                    <a href="{{ url_for('transaction.index') }}" class="btn btn-default">
+                                        <i class="fas fa-arrow-left"></i> 返回列表
+                                    </a>
+                                </div>
+                            </form>
+                        </div>
+                    </div>
+
+                    <!-- 导入结果 -->
+                    <div id="importResult" style="display: none;">
+                        <div class="alert" role="alert">
+                            <h5><i class="icon fas fa-info"></i> 导入结果</h5>
+                            <p id="resultMessage" class="mb-0"></p>
+                            <div id="errorDetails" style="display: none;">
+                                <hr>
+                                <p class="mb-1">错误详情:</p>
+                                <ul id="errorList" class="mb-0"></ul>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock %}
+
+{% block scripts %}
+<script>
+$(document).ready(function() {
+    // 文件选择处理
+    $('.custom-file-input').on('change', function() {
+        let fileName = $(this).val().split('\\').pop();
+        $(this).next('.custom-file-label').html(fileName || '选择Excel文件');
+    });
+
+    // 表单提交处理
+    $('#uploadForm').on('submit', function(e) {
+        e.preventDefault();
+        
+        // 检查文件
+        let fileInput = $('#file')[0];
+        if (!fileInput.files || !fileInput.files[0]) {
+            showResult('请选择要导入的文件', 'warning');
+            return;
+        }
+
+        // 检查文件类型
+        let fileName = fileInput.files[0].name;
+        if (!fileName.endsWith('.xlsx')) {
+            showResult('请上传Excel文件(.xlsx)', 'warning');
+            return;
+        }
+
+        // 显示加载状态
+        let submitBtn = $(this).find('button[type="submit"]');
+        let originalText = submitBtn.html();
+        submitBtn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> 导入中...');
+
+        // 创建FormData对象
+        let formData = new FormData();
+        formData.append('file', fileInput.files[0]);
+
+        // 发送请求
+        $.ajax({
+            url: "{{ url_for('transaction.import_excel') }}",
+            type: 'POST',
+            data: formData,
+            processData: false,
+            contentType: false,
+            success: function(response) {
+                if (response.code === 0) {
+                    // 导入成功
+                    let msg = response.msg;
+                    showResult(msg, 'success');
+                    
+                    // 显示错误详情(如果有)
+                    if (response.data.error_messages && response.data.error_messages.length > 0) {
+                        showErrorDetails(response.data.error_messages);
+                    }
+                    
+                    // 清空文件选择
+                    $('#file').val('').next('.custom-file-label').html('选择Excel文件');
+                } else {
+                    // 导入失败
+                    showResult(response.msg, 'danger');
+                }
+            },
+            error: function(jqXHR, textStatus, errorThrown) {
+                showResult('导入失败:' + textStatus, 'danger');
+            },
+            complete: function() {
+                // 恢复按钮状态
+                submitBtn.prop('disabled', false).html(originalText);
+            }
+        });
+    });
+
+    // 显示结果
+    function showResult(message, type) {
+        let resultDiv = $('#importResult');
+        let alertDiv = resultDiv.find('.alert');
+        let messageP = $('#resultMessage');
+        
+        // 设置消息和样式
+        messageP.text(message);
+        alertDiv.removeClass('alert-success alert-warning alert-danger')
+               .addClass('alert-' + type);
+        
+        // 设置图标
+        let icon = alertDiv.find('.icon');
+        icon.removeClass('fa-info fa-check fa-times fa-exclamation-triangle');
+        switch (type) {
+            case 'success':
+                icon.addClass('fa-check');
+                break;
+            case 'warning':
+                icon.addClass('fa-exclamation-triangle');
+                break;
+            case 'danger':
+                icon.addClass('fa-times');
+                break;
+            default:
+                icon.addClass('fa-info');
+        }
+        
+        // 显示结果区域
+        resultDiv.show();
+        
+        // 隐藏错误详情
+        $('#errorDetails').hide();
+        $('#errorList').empty();
+        
+        // 滚动到结果区域
+        resultDiv[0].scrollIntoView({ behavior: 'smooth' });
+    }
+
+    // 显示错误详情
+    function showErrorDetails(errors) {
+        let errorList = $('#errorList');
+        errorList.empty();
+        
+        errors.forEach(function(error) {
+            errorList.append(`<li>${error}</li>`);
+        });
+        
+        $('#errorDetails').show();
+    }
+});
+</script>
+{% endblock %} 

+ 505 - 0
app/templates/transaction/index.html

@@ -0,0 +1,505 @@
+{% extends "base.html" %}
+
+{% block title %}交易记录列表{% endblock %}
+
+{% block content %}
+<div class="container-fluid">
+    <div class="row">
+        <div class="col-12">
+            <div class="card">
+                <div class="card-header">
+                    <h3 class="card-title">交易记录列表</h3>
+                    <div class="card-tools">
+                        <a href="{{ url_for('transaction.add') }}" class="btn btn-primary btn-sm">新增交易</a>
+                        <a href="{{ url_for('transaction.import_view') }}" class="btn btn-success btn-sm">导入交易</a>
+                    </div>
+                </div>
+                <div class="card-body">
+                    <!-- 筛选表单 -->
+                    <form id="filterForm" class="mb-3">
+                        <div class="row">
+                            <div class="col-md-3">
+                                <div class="form-group">
+                                    <label>时间范围</label>
+                                    <div class="input-group">
+                                        <input type="date" class="form-control" name="start_time">
+                                        <div class="input-group-append">
+                                            <span class="input-group-text">至</span>
+                                        </div>
+                                        <input type="date" class="form-control" name="end_time">
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="col-md-2">
+                                <div class="form-group">
+                                    <label>合约名称</label>
+                                    <input type="text" class="form-control" name="name">
+                                </div>
+                            </div>
+                            <div class="col-md-2">
+                                <div class="form-group">
+                                    <label>合约代码</label>
+                                    <input type="text" class="form-control" name="contract_code">
+                                </div>
+                            </div>
+                            <div class="col-md-2">
+                                <div class="form-group">
+                                    <label>交易类型</label>
+                                    <select class="form-control" name="trade_type">
+                                        <option value="">全部</option>
+                                        <option value="0">模拟交易</option>
+                                        <option value="1">实盘交易</option>
+                                    </select>
+                                </div>
+                            </div>
+                            <div class="col-md-3">
+                                <div class="form-group">
+                                    <label>交易状态 (可多选)</label>
+                                    <select class="form-control select2" name="trade_status[]" multiple="multiple" data-placeholder="选择状态">
+                                        <option value="0">进行中</option>
+                                        <option value="1">已暂停</option>
+                                        <option value="2">暂停进行</option>
+                                        <option value="3">已结束</option>
+                                    </select>
+                                </div>
+                            </div>
+                        </div>
+                        <div class="row">
+                            <div class="col-md-12 text-end">
+                                <button type="submit" class="btn btn-primary">查询</button>
+                            </div>
+                        </div>
+                    </form>
+
+                    <!-- 数据表格 - 添加列冻结功能 -->
+                    <div class="table-responsive" style="overflow-x: auto;">
+                        <table class="table table-bordered table-striped table-sm sticky-table" style="min-width: 1800px;">
+                            <thead>
+                                <tr>
+                                    <th class="sticky-left-1" style="min-width: 60px;">ID</th>
+                                    <th class="sticky-left-2" style="min-width: 140px;">成交时间</th>
+                                    <th class="sticky-left-3" style="min-width: 100px;">合约代码</th>
+                                    <th style="min-width: 80px;">名称</th>
+                                    <th style="min-width: 100px;">账户</th>
+                                    <th style="min-width: 80px;">交易类别</th>
+                                    <th style="min-width: 120px;">操作策略</th>
+                                    <th style="min-width: 80px;">多空仓位</th>
+                                    <th style="min-width: 100px;">K线形态</th>
+                                    <th style="min-width: 100px;">成交价格</th>
+                                    <th style="min-width: 100px;">成交手数</th>
+                                    <th style="min-width: 100px;">保证金</th>
+                                    <th style="min-width: 100px;">最新价格</th>
+                                    <th style="min-width: 100px;">实际收益率</th>
+                                    <th style="min-width: 100px;">实际收益</th>
+                                    <th style="min-width: 100px;">止损价格</th>
+                                    <th style="min-width: 100px;">止损比例</th>
+                                    <th style="min-width: 100px;">止损收益</th>
+                                    <th class="sticky-right" style="min-width: 120px;">操作</th>
+                                </tr>
+                            </thead>
+                            <tbody id="transactionList">
+                                <!-- 数据将通过JavaScript动态加载 -->
+                            </tbody>
+                        </table>
+                    </div>
+                    <!-- 分页控件和每页数量选择器 -->
+                    <div class="d-flex justify-content-center align-items-center mt-3">
+                        <nav aria-label="Page navigation" class="me-3">
+                            <ul class="pagination mb-0" id="pagination">
+                                <!-- 分页按钮将通过JavaScript动态加载 -->
+                            </ul>
+                        </nav>
+                        <div class="d-flex align-items-center">
+                            <label for="itemsPerPageSelect" class="col-form-label me-2 mb-0">每页:</label>
+                            <select class="form-select form-select-sm" id="itemsPerPageSelect" style="width: auto;">
+                                <option value="10">10</option>
+                                <option value="20" selected>20</option>
+                                <option value="50">50</option>
+                                <option value="100">100</option>
+                            </select>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock %}
+
+{% block styles %}
+<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css">
+<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ttskch/select2-bootstrap4-theme@x.x.x/dist/select2-bootstrap4.min.css">
+<style>
+    .select2-container--bootstrap4 .select2-selection--multiple {
+        min-height: calc(1.5em + .75rem + 2px);
+    }
+    .select2-container--bootstrap4 .select2-selection--multiple .select2-selection__rendered {
+        padding-bottom: 0;
+    }
+    
+    /* 表格水平滚动样式 */
+    .table-responsive {
+        border: 1px solid #dee2e6;
+        border-radius: 0.375rem;
+    }
+    
+    .table-responsive::-webkit-scrollbar {
+        height: 8px;
+    }
+    
+    .table-responsive::-webkit-scrollbar-track {
+        background: #f1f1f1;
+        border-radius: 4px;
+    }
+    
+    .table-responsive::-webkit-scrollbar-thumb {
+        background: #c1c1c1;
+        border-radius: 4px;
+    }
+    
+    .table-responsive::-webkit-scrollbar-thumb:hover {
+        background: #a8a8a8;
+    }
+    
+    /* 列冻结样式 */
+    .sticky-table {
+        position: relative;
+    }
+
+    .sticky-table thead th {
+        position: sticky;
+        top: 0;
+        z-index: 10;
+        background-color: #f8f9fa; /* 统一表头背景色 */
+    }
+
+    /* 冻结列统一样式 */
+    .sticky-table .sticky-left-1,
+    .sticky-table .sticky-left-2,
+    .sticky-table .sticky-left-3,
+    .sticky-table .sticky-right {
+        position: sticky;
+        z-index: 11; /* 确保冻结列在普通表头之上 */
+    }
+
+    /* 左侧冻结列 */
+    .sticky-left-1 { left: 0; }
+    .sticky-left-2 { left: 60px; }
+    .sticky-left-3 { left: 200px; }
+    
+    /* 右侧冻结列 */
+    .sticky-right { right: 0; }
+    
+    /* 冻结列表头和内容单元格背景色 */
+    .sticky-table th.sticky-left-1,
+    .sticky-table th.sticky-left-2,
+    .sticky-table th.sticky-left-3,
+    .sticky-table th.sticky-right {
+        background-color: #f8f9fa; /* 冻结列表头背景色 */
+    }
+
+    .sticky-table td.sticky-left-1,
+    .sticky-table td.sticky-left-2,
+    .sticky-table td.sticky-left-3,
+    .sticky-table td.sticky-right {
+        background-color: #fff; /* 冻结内容列背景色 */
+    }
+    
+    /* 修复斑马纹表格下的背景色问题 */
+    .table-striped > tbody > tr:nth-of-type(odd) > td.sticky-left-1,
+    .table-striped > tbody > tr:nth-of-type(odd) > td.sticky-left-2,
+    .table-striped > tbody > tr:nth-of-type(odd) > td.sticky-left-3,
+    .table-striped > tbody > tr:nth-of-type(odd) > td.sticky-right {
+        background-color: #f9f9f9; /* 斑马纹行冻结列背景色 */
+    }
+    
+    /* 添加边框以示区分 */
+    .sticky-left-3 { border-right: 2px solid #dee2e6; }
+    .sticky-right { border-left: 2px solid #dee2e6; }
+
+</style>
+{% endblock %}
+
+{% block scripts %}
+<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
+<script>
+function formatNumber(num, precision = 3) {
+    if (num === null || num === undefined || isNaN(num)) {
+        return '-';
+    }
+    let fixedNum = Number(parseFloat(num).toFixed(precision)); 
+    return fixedNum.toString();
+}
+
+function formatPercentage(num, precision = 2) {
+    if (num === null || num === undefined || isNaN(num)) {
+        return '-';
+    }
+    let percentage = parseFloat(num) * 100;
+    return formatNumber(percentage, precision) + '%';
+}
+
+// 判断是否可以显示平仓选项
+function canShowCloseOption(item) {
+    // 只有开仓交易(position_type: 0=开多, 2=开空)且状态不是已结束(3)的情况下才显示平仓选项
+    return (item.position_type === 0 || item.position_type === 2) && item.trade_status !== 3;
+}
+
+$(document).ready(function() {
+    $('.select2').select2({
+        theme: 'bootstrap4',
+        width: '100%'
+    });
+
+    let currentPage = 1;
+    let currentItemsPerPage = parseInt($('#itemsPerPageSelect').val()) || 20;
+
+    // Restore state from sessionStorage on page load or set defaults
+    const savedFiltersJSON = sessionStorage.getItem('transactionFilters');
+    if (savedFiltersJSON) {
+        const savedFilters = JSON.parse(savedFiltersJSON);
+        $('#filterForm [name="start_time"]').val(savedFilters.start_time);
+        $('#filterForm [name="end_time"]').val(savedFilters.end_time);
+        $('#filterForm [name="name"]').val(savedFilters.name);
+        $('#filterForm [name="contract_code"]').val(savedFilters.contract_code);
+        $('#filterForm [name="trade_type"]').val(savedFilters.trade_type);
+        if (savedFilters.trade_status) {
+            $('#filterForm [name="trade_status[]"]').val(savedFilters.trade_status).trigger('change');
+        } else {
+            // 如果没有保存的交易状态,设置默认选中"进行中"和"暂停进行"
+            $('#filterForm [name="trade_status[]"]').val(['0', '2']).trigger('change');
+        }
+    } else {
+        // 如果没有保存的筛选条件,设置默认选中"进行中"和"暂停进行"
+        $('#filterForm [name="trade_status[]"]').val(['0', '2']).trigger('change');
+    }
+
+    const savedPage = sessionStorage.getItem('transactionCurrentPage');
+    if (savedPage) {
+        currentPage = parseInt(savedPage, 10);
+    }
+
+    const savedItemsPerPage = sessionStorage.getItem('transactionItemsPerPage');
+    if (savedItemsPerPage) {
+        currentItemsPerPage = parseInt(savedItemsPerPage, 10);
+        $('#itemsPerPageSelect').val(currentItemsPerPage);
+    }
+
+    function loadTransactions(page = 1, filters = {}) {
+        currentPage = page;
+        filters.page = page;
+        filters.limit = currentItemsPerPage;
+
+        // Save current state to sessionStorage
+        sessionStorage.setItem('transactionFilters', JSON.stringify(getFilters()));
+        sessionStorage.setItem('transactionCurrentPage', currentPage);
+        sessionStorage.setItem('transactionItemsPerPage', currentItemsPerPage);
+
+        $('#transactionList').html(`<tr><td colspan="19" class="text-center">数据加载中...</td></tr>`);
+        $('#pagination').empty();
+
+        $.get("{{ url_for('transaction.get_list') }}", filters, function(response) {
+            if (response.code === 0) {
+                let html = '';
+                if (response.data && response.data.length > 0) {
+                    response.data.forEach(function(item) {
+                        html += `
+                            <tr>
+                                <td class="sticky-left-1">${item.id || '-'}</td>
+                                <td class="sticky-left-2">${item.transaction_time || '-'}</td>
+                                <td class="sticky-left-3">${item.contract_code || '-'}</td>
+                                <td>${item.name || '-'}</td>
+                                <td>${item.account || '-'}</td>
+                                <td>${getTradeTypeText(item.trade_type)}</td>
+                                <td>${item.strategy_name || '-'}</td>
+                                <td>${getPositionTypeText(item.position_type)}</td>
+                                <td>${item.candle_pattern || '-'}</td>
+                                <td>${formatNumber(item.price, 3)}</td>
+                                <td>${formatNumber(item.volume, 0)}</td>
+                                <td>${formatNumber(item.margin, 2)}</td>
+                                <td>${formatNumber(item.latest_price, 3)}</td>
+                                <td class="${getProfitLossClass(item.actual_profit_rate)}">${formatPercentage(item.actual_profit_rate, 2)}</td>
+                                <td class="${getProfitLossClass(item.actual_profit)}">${formatNumber(item.actual_profit, 2)}</td>
+                                <td>${formatNumber(item.stop_loss_price, 3)}</td>
+                                <td class="${getProfitLossClass(item.stop_loss_rate)}">${formatPercentage(item.stop_loss_rate, 2)}</td>
+                                <td class="${getProfitLossClass(item.stop_loss_profit)}">${formatNumber(item.stop_loss_profit, 2)}</td>
+                                <td class="sticky-right">
+                                    <div class="btn-group">
+                                        <button type="button" class="btn btn-secondary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                                            操作
+                                        </button>
+                                        <div class="dropdown-menu">
+                                            <a class="dropdown-item" href="/transaction/detail/view/${item.id}">详情</a>
+                                            <a class="dropdown-item" href="/transaction/edit/${item.id}">编辑</a>
+                                            ${canShowCloseOption(item) ? `
+                                                <div class="dropdown-divider"></div>
+                                                <a class="dropdown-item text-warning" href="/transaction/add?close_for_transaction=${item.id}">
+                                                    <i class="fas fa-times-circle"></i> 平仓
+                                                </a>
+                                            ` : ''}
+                                            <div class="dropdown-divider"></div>
+                                            <button class="dropdown-item text-danger" onclick="deleteTransaction(${item.id})">删除</button>
+                                        </div>
+                                    </div>
+                                </td>
+                            </tr>
+                        `;
+                    });
+                } else {
+                    html = `<tr><td colspan="19" class="text-center">暂无数据</td></tr>`;
+                }
+                $('#transactionList').html(html);
+                renderPagination(response.count, page, currentItemsPerPage);
+            } else {
+                $('#transactionList').html(`<tr><td colspan="19" class="text-center text-danger">加载失败:${response.msg}</td></tr>`);
+            }
+        }).fail(function(jqXHR, textStatus, errorThrown) {
+            $('#transactionList').html(`<tr><td colspan="19" class="text-center text-danger">加载失败:${textStatus}</td></tr>`);
+        });
+    }
+
+    function renderPagination(totalItems, currentPage, itemsPerPage) {
+        const totalPages = Math.ceil(totalItems / itemsPerPage);
+        let paginationHtml = '';
+
+        if (totalPages <= 1) {
+            $('#pagination').empty();
+            $('#itemsPerPageSelect').closest('div').hide();
+            return;
+        } else {
+            $('#itemsPerPageSelect').closest('div').show();
+        }
+
+        paginationHtml += `<li class="page-item ${currentPage === 1 ? 'disabled' : ''}">
+                            <a class="page-link" href="#" data-page="${currentPage - 1}" aria-label="Previous">
+                                <span aria-hidden="true">&laquo;</span>
+                            </a>
+                           </li>`;
+
+        const maxPagesToShow = 5;
+        let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
+        let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
+
+        if (endPage - startPage + 1 < maxPagesToShow) {
+            startPage = Math.max(1, endPage - maxPagesToShow + 1);
+        }
+
+        if (startPage > 1) {
+            paginationHtml += `<li class="page-item"><a class="page-link" href="#" data-page="1">1</a></li>`;
+            if (startPage > 2) {
+                paginationHtml += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
+            }
+        }
+
+        for (let i = startPage; i <= endPage; i++) {
+            paginationHtml += `<li class="page-item ${i === currentPage ? 'active' : ''}">
+                                <a class="page-link" href="#" data-page="${i}">${i}</a>
+                               </li>`;
+        }
+
+        if (endPage < totalPages) {
+            if (endPage < totalPages - 1) {
+                paginationHtml += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
+            }
+            paginationHtml += `<li class="page-item"><a class="page-link" href="#" data-page="${totalPages}">${totalPages}</a></li>`;
+        }
+
+        paginationHtml += `<li class="page-item ${currentPage === totalPages ? 'disabled' : ''}">
+                            <a class="page-link" href="#" data-page="${currentPage + 1}" aria-label="Next">
+                                <span aria-hidden="true">&raquo;</span>
+                            </a>
+                           </li>`;
+
+        $('#pagination').html(paginationHtml);
+
+        $('#pagination .page-link').on('click', function(e) {
+            e.preventDefault();
+            const page = $(this).data('page');
+            if (page && page !== currentPage) {
+                const filters = getFilters();
+                loadTransactions(page, filters);
+            }
+        });
+    }
+
+    function getFilters() {
+        const filters = {};
+        $('#filterForm').serializeArray().forEach(function(item) {
+            if (item.value) {
+                if (item.name === 'trade_status[]') {
+                    if (!filters['trade_status']) {
+                        filters['trade_status'] = [];
+                    }
+                    filters['trade_status'].push(item.value);
+                } else {
+                    filters[item.name] = item.value;
+                }
+            }
+        });
+        return filters;
+    }
+
+    function getPositionTypeText(type) {
+        const types = {
+            0: '开多',
+            1: '平多',
+            2: '开空',
+            3: '平空'
+        };
+        return types[type] || '未知';
+    }
+
+    function getTradeTypeText(type) {
+        return type === 1 ? '实盘' : (type === 0 ? '模拟' : '未知');
+    }
+
+    function getProfitLossClass(value) {
+        if (value === null || value === undefined || isNaN(value)) {
+            return '';
+        }
+        const num = parseFloat(value);
+        if (num > 0) return 'text-danger';
+        if (num < 0) return 'text-success';
+        return '';
+    }
+
+    window.deleteTransaction = function(id) {
+        if (!id) return;
+        if (confirm('确认要删除这条交易记录吗?(将同时删除关联汇总记录)')) {
+            $.ajax({
+                url: "/transaction/api/delete/" + id,
+                type: 'DELETE',
+                success: function(response) {
+                    if (response.code === 0) {
+                        alert('删除成功');
+                        const filters = getFilters();
+                        loadTransactions(currentPage, filters);
+                    } else {
+                        alert('删除失败:' + response.msg);
+                    }
+                },
+                error: function(jqXHR, textStatus, errorThrown) {
+                    alert('删除失败:' + textStatus);
+                }
+            });
+        }
+    }
+
+    $('#filterForm').on('submit', function(e) {
+        e.preventDefault();
+        const filters = getFilters();
+        loadTransactions(1, filters);
+    });
+
+    $('#itemsPerPageSelect').on('change', function() {
+        currentItemsPerPage = parseInt($(this).val());
+        const filters = getFilters();
+        loadTransactions(1, filters);
+    });
+
+    // Initial load with restored state
+    const initialFilters = getFilters();
+    loadTransactions(currentPage, initialFilters);
+});
+</script>
+{% endblock %} 

+ 6 - 0
config.yaml

@@ -0,0 +1,6 @@
+data_update:
+  schedule:
+  - hour: '9'
+    minute: '0'
+  - hour: '15'
+    minute: '0'

+ 2 - 0
data/README.md

@@ -0,0 +1,2 @@
+# 数据目录
+此目录用于存放数据库文件,数据库文件不会被版本控制。

+ 6 - 0
main.py

@@ -0,0 +1,6 @@
+def main():
+    print("Hello from inv-monitor!")
+
+
+if __name__ == "__main__":
+    main()

+ 22 - 0
pyproject.toml

@@ -0,0 +1,22 @@
+[project]
+name = "inv-monitor"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.11"
+dependencies = [
+    "apscheduler>=3.11.0",
+    "beautifulsoup4>=4.13.4",
+    "flask-migrate>=4.1.0",
+    "flask-sqlalchemy>=3.1.1",
+    "openpyxl>=3.1.5",
+    "pandas>=2.3.1",
+    "pyyaml>=6.0.2",
+    "requests>=2.32.4",
+    "retrying>=1.4.1",
+    "xlsxwriter>=3.2.5",
+]
+
+[[tool.uv.index]]
+url = "https://pypi.tuna.tsinghua.edu.cn/simple"
+default = true

+ 655 - 0
uv.lock

@@ -0,0 +1,655 @@
+version = 1
+revision = 2
+requires-python = ">=3.11"
+resolution-markers = [
+    "python_full_version >= '3.12'",
+    "python_full_version < '3.12'",
+]
+
+[[package]]
+name = "alembic"
+version = "1.16.4"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+dependencies = [
+    { name = "mako" },
+    { name = "sqlalchemy" },
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/52/72e791b75c6b1efa803e491f7cbab78e963695e76d4ada05385252927e76/alembic-1.16.4.tar.gz", hash = "sha256:efab6ada0dd0fae2c92060800e0bf5c1dc26af15a10e02fb4babff164b4725e2", size = 1968161, upload-time = "2025-07-10T16:17:20.192Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/62/96b5217b742805236614f05904541000f55422a6060a90d7fd4ce26c172d/alembic-1.16.4-py3-none-any.whl", hash = "sha256:b05e51e8e82efc1abd14ba2af6392897e145930c3e0a2faf2b0da2f7f7fd660d", size = 247026, upload-time = "2025-07-10T16:17:21.845Z" },
+]
+
+[[package]]
+name = "apscheduler"
+version = "3.11.0"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+dependencies = [
+    { name = "tzlocal" },
+]
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4e/00/6d6814ddc19be2df62c8c898c4df6b5b1914f3bd024b780028caa392d186/apscheduler-3.11.0.tar.gz", hash = "sha256:4c622d250b0955a65d5d0eb91c33e6d43fd879834bf541e0a18661ae60460133", size = 107347, upload-time = "2024-11-24T19:39:26.463Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/ae/9a053dd9229c0fde6b1f1f33f609ccff1ee79ddda364c756a924c6d8563b/APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da", size = 64004, upload-time = "2024-11-24T19:39:24.442Z" },
+]
+
+[[package]]
+name = "beautifulsoup4"
+version = "4.13.4"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+dependencies = [
+    { name = "soupsieve" },
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" },
+]
+
+[[package]]
+name = "blinker"
+version = "1.9.0"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.7.14"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.2"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.2.1"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+dependencies = [
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "et-xmlfile"
+version = "2.0.0"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" },
+]
+
+[[package]]
+name = "flask"
+version = "3.1.1"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+dependencies = [
+    { name = "blinker" },
+    { name = "click" },
+    { name = "itsdangerous" },
+    { name = "jinja2" },
+    { name = "markupsafe" },
+    { name = "werkzeug" },
+]
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/de/e47735752347f4128bcf354e0da07ef311a78244eba9e3dc1d4a5ab21a98/flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e", size = 753440, upload-time = "2025-05-13T15:01:17.447Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", size = 103305, upload-time = "2025-05-13T15:01:15.591Z" },
+]
+
+[[package]]
+name = "flask-migrate"
+version = "4.1.0"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+dependencies = [
+    { name = "alembic" },
+    { name = "flask" },
+    { name = "flask-sqlalchemy" },
+]
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/8e/47c7b3c93855ceffc2eabfa271782332942443321a07de193e4198f920cf/flask_migrate-4.1.0.tar.gz", hash = "sha256:1a336b06eb2c3ace005f5f2ded8641d534c18798d64061f6ff11f79e1434126d", size = 21965, upload-time = "2025-01-10T18:51:11.848Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d2/c4/3f329b23d769fe7628a5fc57ad36956f1fb7132cf8837be6da762b197327/Flask_Migrate-4.1.0-py3-none-any.whl", hash = "sha256:24d8051af161782e0743af1b04a152d007bad9772b2bca67b7ec1e8ceeb3910d", size = 21237, upload-time = "2025-01-10T18:51:09.527Z" },
+]
+
+[[package]]
+name = "flask-sqlalchemy"
+version = "3.1.1"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+dependencies = [
+    { name = "flask" },
+    { name = "sqlalchemy" },
+]
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/53/b0a9fcc1b1297f51e68b69ed3b7c3c40d8c45be1391d77ae198712914392/flask_sqlalchemy-3.1.1.tar.gz", hash = "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1d/6a/89963a5c6ecf166e8be29e0d1bf6806051ee8fe6c82e232842e3aeac9204/flask_sqlalchemy-3.1.1-py3-none-any.whl", hash = "sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0" },
+]
+
+[[package]]
+name = "greenlet"
+version = "3.2.3"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752, upload-time = "2025-06-05T16:16:09.955Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/2e/d4fcb2978f826358b673f779f78fa8a32ee37df11920dc2bb5589cbeecef/greenlet-3.2.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:784ae58bba89fa1fa5733d170d42486580cab9decda3484779f4759345b29822", size = 270219, upload-time = "2025-06-05T16:10:10.414Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/16/24/929f853e0202130e4fe163bc1d05a671ce8dcd604f790e14896adac43a52/greenlet-3.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0921ac4ea42a5315d3446120ad48f90c3a6b9bb93dd9b3cf4e4d84a66e42de83", size = 630383, upload-time = "2025-06-05T16:38:51.785Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/b2/0320715eb61ae70c25ceca2f1d5ae620477d246692d9cc284c13242ec31c/greenlet-3.2.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d2971d93bb99e05f8c2c0c2f4aa9484a18d98c4c3bd3c62b65b7e6ae33dfcfaf", size = 642422, upload-time = "2025-06-05T16:41:35.259Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/49/445fd1a210f4747fedf77615d941444349c6a3a4a1135bba9701337cd966/greenlet-3.2.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c667c0bf9d406b77a15c924ef3285e1e05250948001220368e039b6aa5b5034b", size = 638375, upload-time = "2025-06-05T16:48:18.235Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/c8/ca19760cf6eae75fa8dc32b487e963d863b3ee04a7637da77b616703bc37/greenlet-3.2.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:592c12fb1165be74592f5de0d70f82bc5ba552ac44800d632214b76089945147", size = 637627, upload-time = "2025-06-05T16:13:02.858Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/89/77acf9e3da38e9bcfca881e43b02ed467c1dedc387021fc4d9bd9928afb8/greenlet-3.2.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29e184536ba333003540790ba29829ac14bb645514fbd7e32af331e8202a62a5", size = 585502, upload-time = "2025-06-05T16:12:49.642Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/97/c6/ae244d7c95b23b7130136e07a9cc5aadd60d59b5951180dc7dc7e8edaba7/greenlet-3.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:93c0bb79844a367782ec4f429d07589417052e621aa39a5ac1fb99c5aa308edc", size = 1114498, upload-time = "2025-06-05T16:36:46.598Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/5f/b16dec0cbfd3070658e0d744487919740c6d45eb90946f6787689a7efbce/greenlet-3.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:751261fc5ad7b6705f5f76726567375bb2104a059454e0226e1eef6c756748ba", size = 1139977, upload-time = "2025-06-05T16:12:38.262Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/77/d48fb441b5a71125bcac042fc5b1494c806ccb9a1432ecaa421e72157f77/greenlet-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:83a8761c75312361aa2b5b903b79da97f13f556164a7dd2d5448655425bd4c34", size = 297017, upload-time = "2025-06-05T16:25:05.225Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/94/ad0d435f7c48debe960c53b8f60fb41c2026b1d0fa4a99a1cb17c3461e09/greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d", size = 271992, upload-time = "2025-06-05T16:11:23.467Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/93/5d/7c27cf4d003d6e77749d299c7c8f5fd50b4f251647b5c2e97e1f20da0ab5/greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b", size = 638820, upload-time = "2025-06-05T16:38:52.882Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/7e/807e1e9be07a125bb4c169144937910bf59b9d2f6d931578e57f0bce0ae2/greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d", size = 653046, upload-time = "2025-06-05T16:41:36.343Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/ab/158c1a4ea1068bdbc78dba5a3de57e4c7aeb4e7fa034320ea94c688bfb61/greenlet-3.2.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264", size = 647701, upload-time = "2025-06-05T16:48:19.604Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/0d/93729068259b550d6a0288da4ff72b86ed05626eaf1eb7c0d3466a2571de/greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688", size = 649747, upload-time = "2025-06-05T16:13:04.628Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/f6/c82ac1851c60851302d8581680573245c8fc300253fc1ff741ae74a6c24d/greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb", size = 605461, upload-time = "2025-06-05T16:12:50.792Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/82/d022cf25ca39cf1200650fc58c52af32c90f80479c25d1cbf57980ec3065/greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c", size = 1121190, upload-time = "2025-06-05T16:36:48.59Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/e1/25297f70717abe8104c20ecf7af0a5b82d2f5a980eb1ac79f65654799f9f/greenlet-3.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163", size = 1149055, upload-time = "2025-06-05T16:12:40.457Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/8f/8f9e56c5e82eb2c26e8cde787962e66494312dc8cb261c460e1f3a9c88bc/greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849", size = 297817, upload-time = "2025-06-05T16:29:49.244Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/cf/f5c0b23309070ae93de75c90d29300751a5aacefc0a3ed1b1d8edb28f08b/greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad", size = 270732, upload-time = "2025-06-05T16:10:08.26Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/ae/91a957ba60482d3fecf9be49bc3948f341d706b52ddb9d83a70d42abd498/greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef", size = 639033, upload-time = "2025-06-05T16:38:53.983Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/df/20ffa66dd5a7a7beffa6451bdb7400d66251374ab40b99981478c69a67a8/greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3", size = 652999, upload-time = "2025-06-05T16:41:37.89Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/b4/ebb2c8cb41e521f1d72bf0465f2f9a2fd803f674a88db228887e6847077e/greenlet-3.2.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95", size = 647368, upload-time = "2025-06-05T16:48:21.467Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/6a/1e1b5aa10dced4ae876a322155705257748108b7fd2e4fae3f2a091fe81a/greenlet-3.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb", size = 650037, upload-time = "2025-06-05T16:13:06.402Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b", size = 608402, upload-time = "2025-06-05T16:12:51.91Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/bc/862bd2083e6b3aff23300900a956f4ea9a4059de337f5c8734346b9b34fc/greenlet-3.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0", size = 1119577, upload-time = "2025-06-05T16:36:49.787Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/94/1fc0cc068cfde885170e01de40a619b00eaa8f2916bf3541744730ffb4c3/greenlet-3.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36", size = 1147121, upload-time = "2025-06-05T16:12:42.527Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/27/1a/199f9587e8cb08a0658f9c30f3799244307614148ffe8b1e3aa22f324dea/greenlet-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3", size = 297603, upload-time = "2025-06-05T16:20:12.651Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/ca/accd7aa5280eb92b70ed9e8f7fd79dc50a2c21d8c73b9a0856f5b564e222/greenlet-3.2.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3d04332dddb10b4a211b68111dabaee2e1a073663d117dc10247b5b1642bac86", size = 271479, upload-time = "2025-06-05T16:10:47.525Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/71/01ed9895d9eb49223280ecc98a557585edfa56b3d0e965b9fa9f7f06b6d9/greenlet-3.2.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8186162dffde068a465deab08fc72c767196895c39db26ab1c17c0b77a6d8b97", size = 683952, upload-time = "2025-06-05T16:38:55.125Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/61/638c4bdf460c3c678a0a1ef4c200f347dff80719597e53b5edb2fb27ab54/greenlet-3.2.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728", size = 696917, upload-time = "2025-06-05T16:41:38.959Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/cc/0bd1a7eb759d1f3e3cc2d1bc0f0b487ad3cc9f34d74da4b80f226fde4ec3/greenlet-3.2.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:ed6cfa9200484d234d8394c70f5492f144b20d4533f69262d530a1a082f6ee9a", size = 692443, upload-time = "2025-06-05T16:48:23.113Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/10/b2a4b63d3f08362662e89c103f7fe28894a51ae0bc890fabf37d1d780e52/greenlet-3.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892", size = 692995, upload-time = "2025-06-05T16:13:07.972Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/c6/ad82f148a4e3ce9564056453a71529732baf5448ad53fc323e37efe34f66/greenlet-3.2.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141", size = 655320, upload-time = "2025-06-05T16:12:53.453Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/4f/aab73ecaa6b3086a4c89863d94cf26fa84cbff63f52ce9bc4342b3087a06/greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a", size = 301236, upload-time = "2025-06-05T16:15:20.111Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
+]
+
+[[package]]
+name = "inv-monitor"
+version = "0.1.0"
+source = { virtual = "." }
+dependencies = [
+    { name = "apscheduler" },
+    { name = "beautifulsoup4" },
+    { name = "flask-migrate" },
+    { name = "flask-sqlalchemy" },
+    { name = "openpyxl" },
+    { name = "pandas" },
+    { name = "pyyaml" },
+    { name = "requests" },
+    { name = "retrying" },
+    { name = "xlsxwriter" },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "apscheduler", specifier = ">=3.11.0" },
+    { name = "beautifulsoup4", specifier = ">=4.13.4" },
+    { name = "flask-migrate", specifier = ">=4.1.0" },
+    { name = "flask-sqlalchemy", specifier = ">=3.1.1" },
+    { name = "openpyxl", specifier = ">=3.1.5" },
+    { name = "pandas", specifier = ">=2.3.1" },
+    { name = "pyyaml", specifier = ">=6.0.2" },
+    { name = "requests", specifier = ">=2.32.4" },
+    { name = "retrying", specifier = ">=1.4.1" },
+    { name = "xlsxwriter", specifier = ">=3.2.5" },
+]
+
+[[package]]
+name = "itsdangerous"
+version = "2.2.0"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+dependencies = [
+    { name = "markupsafe" },
+]
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
+]
+
+[[package]]
+name = "mako"
+version = "1.3.10"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+dependencies = [
+    { name = "markupsafe" },
+]
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.2"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "2.3.1"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/19/d7c972dfe90a353dbd3efbbe1d14a5951de80c99c9dc1b93cd998d51dc0f/numpy-2.3.1.tar.gz", hash = "sha256:1ec9ae20a4226da374362cca3c62cd753faf2f951440b0e3b98e93c235441d2b", size = 20390372, upload-time = "2025-06-21T12:28:33.469Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/c7/87c64d7ab426156530676000c94784ef55676df2f13b2796f97722464124/numpy-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ea9e48336a402551f52cd8f593343699003d2353daa4b72ce8d34f66b722070", size = 21199346, upload-time = "2025-06-21T11:47:47.57Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/0e/0966c2f44beeac12af8d836e5b5f826a407cf34c45cb73ddcdfce9f5960b/numpy-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ccb7336eaf0e77c1635b232c141846493a588ec9ea777a7c24d7166bb8533ae", size = 14361143, upload-time = "2025-06-21T11:48:10.766Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/31/6e35a247acb1bfc19226791dfc7d4c30002cd4e620e11e58b0ddf836fe52/numpy-2.3.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0bb3a4a61e1d327e035275d2a993c96fa786e4913aa089843e6a2d9dd205c66a", size = 5378989, upload-time = "2025-06-21T11:48:19.998Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/25/93b621219bb6f5a2d4e713a824522c69ab1f06a57cd571cda70e2e31af44/numpy-2.3.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:e344eb79dab01f1e838ebb67aab09965fb271d6da6b00adda26328ac27d4a66e", size = 6912890, upload-time = "2025-06-21T11:48:31.376Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ef/60/6b06ed98d11fb32e27fb59468b42383f3877146d3ee639f733776b6ac596/numpy-2.3.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:467db865b392168ceb1ef1ffa6f5a86e62468c43e0cfb4ab6da667ede10e58db", size = 14569032, upload-time = "2025-06-21T11:48:52.563Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/75/c9/9bec03675192077467a9c7c2bdd1f2e922bd01d3a69b15c3a0fdcd8548f6/numpy-2.3.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:afed2ce4a84f6b0fc6c1ce734ff368cbf5a5e24e8954a338f3bdffa0718adffb", size = 16930354, upload-time = "2025-06-21T11:49:17.473Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/e2/5756a00cabcf50a3f527a0c968b2b4881c62b1379223931853114fa04cda/numpy-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0025048b3c1557a20bc80d06fdeb8cc7fc193721484cca82b2cfa072fec71a93", size = 15879605, upload-time = "2025-06-21T11:49:41.161Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/86/a471f65f0a86f1ca62dcc90b9fa46174dd48f50214e5446bc16a775646c5/numpy-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5ee121b60aa509679b682819c602579e1df14a5b07fe95671c8849aad8f2115", size = 18666994, upload-time = "2025-06-21T11:50:08.516Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/43/a6/482a53e469b32be6500aaf61cfafd1de7a0b0d484babf679209c3298852e/numpy-2.3.1-cp311-cp311-win32.whl", hash = "sha256:a8b740f5579ae4585831b3cf0e3b0425c667274f82a484866d2adf9570539369", size = 6603672, upload-time = "2025-06-21T11:50:19.584Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/fb/bb613f4122c310a13ec67585c70e14b03bfc7ebabd24f4d5138b97371d7c/numpy-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:d4580adadc53311b163444f877e0789f1c8861e2698f6b2a4ca852fda154f3ff", size = 13024015, upload-time = "2025-06-21T11:50:39.139Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/58/2d842825af9a0c041aca246dc92eb725e1bc5e1c9ac89712625db0c4e11c/numpy-2.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:ec0bdafa906f95adc9a0c6f26a4871fa753f25caaa0e032578a30457bff0af6a", size = 10456989, upload-time = "2025-06-21T11:50:55.616Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/56/71ad5022e2f63cfe0ca93559403d0edef14aea70a841d640bd13cdba578e/numpy-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2959d8f268f3d8ee402b04a9ec4bb7604555aeacf78b360dc4ec27f1d508177d", size = 20896664, upload-time = "2025-06-21T12:15:30.845Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/65/2db52ba049813670f7f987cc5db6dac9be7cd95e923cc6832b3d32d87cef/numpy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:762e0c0c6b56bdedfef9a8e1d4538556438288c4276901ea008ae44091954e29", size = 14131078, upload-time = "2025-06-21T12:15:52.23Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/dd/28fa3c17b0e751047ac928c1e1b6990238faad76e9b147e585b573d9d1bd/numpy-2.3.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:867ef172a0976aaa1f1d1b63cf2090de8b636a7674607d514505fb7276ab08fc", size = 5112554, upload-time = "2025-06-21T12:16:01.434Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/fc/84ea0cba8e760c4644b708b6819d91784c290288c27aca916115e3311d17/numpy-2.3.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:4e602e1b8682c2b833af89ba641ad4176053aaa50f5cacda1a27004352dde943", size = 6646560, upload-time = "2025-06-21T12:16:11.895Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/b2/512b0c2ddec985ad1e496b0bd853eeb572315c0f07cd6997473ced8f15e2/numpy-2.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8e333040d069eba1652fb08962ec5b76af7f2c7bce1df7e1418c8055cf776f25", size = 14260638, upload-time = "2025-06-21T12:16:32.611Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/45/c51cb248e679a6c6ab14b7a8e3ead3f4a3fe7425fc7a6f98b3f147bec532/numpy-2.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e7cbf5a5eafd8d230a3ce356d892512185230e4781a361229bd902ff403bc660", size = 16632729, upload-time = "2025-06-21T12:16:57.439Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/ff/feb4be2e5c09a3da161b412019caf47183099cbea1132fd98061808c2df2/numpy-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1b8f26d1086835f442286c1d9b64bb3974b0b1e41bb105358fd07d20872952", size = 15565330, upload-time = "2025-06-21T12:17:20.638Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/6d/ceafe87587101e9ab0d370e4f6e5f3f3a85b9a697f2318738e5e7e176ce3/numpy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ee8340cb48c9b7a5899d1149eece41ca535513a9698098edbade2a8e7a84da77", size = 18361734, upload-time = "2025-06-21T12:17:47.938Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/19/0fb49a3ea088be691f040c9bf1817e4669a339d6e98579f91859b902c636/numpy-2.3.1-cp312-cp312-win32.whl", hash = "sha256:e772dda20a6002ef7061713dc1e2585bc1b534e7909b2030b5a46dae8ff077ab", size = 6320411, upload-time = "2025-06-21T12:17:58.475Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/3e/e28f4c1dd9e042eb57a3eb652f200225e311b608632bc727ae378623d4f8/numpy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cfecc7822543abdea6de08758091da655ea2210b8ffa1faf116b940693d3df76", size = 12734973, upload-time = "2025-06-21T12:18:17.601Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/a8/8a5e9079dc722acf53522b8f8842e79541ea81835e9b5483388701421073/numpy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:7be91b2239af2658653c5bb6f1b8bccafaf08226a258caf78ce44710a0160d30", size = 10191491, upload-time = "2025-06-21T12:18:33.585Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/bd/35ad97006d8abff8631293f8ea6adf07b0108ce6fec68da3c3fcca1197f2/numpy-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25a1992b0a3fdcdaec9f552ef10d8103186f5397ab45e2d25f8ac51b1a6b97e8", size = 20889381, upload-time = "2025-06-21T12:19:04.103Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/4f/df5923874d8095b6062495b39729178eef4a922119cee32a12ee1bd4664c/numpy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7dea630156d39b02a63c18f508f85010230409db5b2927ba59c8ba4ab3e8272e", size = 14152726, upload-time = "2025-06-21T12:19:25.599Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/0f/a1f269b125806212a876f7efb049b06c6f8772cf0121139f97774cd95626/numpy-2.3.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bada6058dd886061f10ea15f230ccf7dfff40572e99fef440a4a857c8728c9c0", size = 5105145, upload-time = "2025-06-21T12:19:34.782Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6d/63/a7f7fd5f375b0361682f6ffbf686787e82b7bbd561268e4f30afad2bb3c0/numpy-2.3.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:a894f3816eb17b29e4783e5873f92faf55b710c2519e5c351767c51f79d8526d", size = 6639409, upload-time = "2025-06-21T12:19:45.228Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/0d/1854a4121af895aab383f4aa233748f1df4671ef331d898e32426756a8a6/numpy-2.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:18703df6c4a4fee55fd3d6e5a253d01c5d33a295409b03fda0c86b3ca2ff41a1", size = 14257630, upload-time = "2025-06-21T12:20:06.544Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/30/af1b277b443f2fb08acf1c55ce9d68ee540043f158630d62cef012750f9f/numpy-2.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5902660491bd7a48b2ec16c23ccb9124b8abfd9583c5fdfa123fe6b421e03de1", size = 16627546, upload-time = "2025-06-21T12:20:31.002Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/ec/3b68220c277e463095342d254c61be8144c31208db18d3fd8ef02712bcd6/numpy-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:36890eb9e9d2081137bd78d29050ba63b8dab95dff7912eadf1185e80074b2a0", size = 15562538, upload-time = "2025-06-21T12:20:54.322Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/2b/4014f2bcc4404484021c74d4c5ee8eb3de7e3f7ac75f06672f8dcf85140a/numpy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a780033466159c2270531e2b8ac063704592a0bc62ec4a1b991c7c40705eb0e8", size = 18360327, upload-time = "2025-06-21T12:21:21.053Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/8d/2ddd6c9b30fcf920837b8672f6c65590c7d92e43084c25fc65edc22e93ca/numpy-2.3.1-cp313-cp313-win32.whl", hash = "sha256:39bff12c076812595c3a306f22bfe49919c5513aa1e0e70fac756a0be7c2a2b8", size = 6312330, upload-time = "2025-06-21T12:25:07.447Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/c8/beaba449925988d415efccb45bf977ff8327a02f655090627318f6398c7b/numpy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d5ee6eec45f08ce507a6570e06f2f879b374a552087a4179ea7838edbcbfa42", size = 12731565, upload-time = "2025-06-21T12:25:26.444Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/c3/5c0c575d7ec78c1126998071f58facfc124006635da75b090805e642c62e/numpy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c4d9e0a8368db90f93bd192bfa771ace63137c3488d198ee21dfb8e7771916e", size = 10190262, upload-time = "2025-06-21T12:25:42.196Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/19/a029cd335cf72f79d2644dcfc22d90f09caa86265cbbde3b5702ccef6890/numpy-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b0b5397374f32ec0649dd98c652a1798192042e715df918c20672c62fb52d4b8", size = 20987593, upload-time = "2025-06-21T12:21:51.664Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/91/8ea8894406209107d9ce19b66314194675d31761fe2cb3c84fe2eeae2f37/numpy-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c5bdf2015ccfcee8253fb8be695516ac4457c743473a43290fd36eba6a1777eb", size = 14300523, upload-time = "2025-06-21T12:22:13.583Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/7f/06187b0066eefc9e7ce77d5f2ddb4e314a55220ad62dd0bfc9f2c44bac14/numpy-2.3.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d70f20df7f08b90a2062c1f07737dd340adccf2068d0f1b9b3d56e2038979fee", size = 5227993, upload-time = "2025-06-21T12:22:22.53Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/ec/a926c293c605fa75e9cfb09f1e4840098ed46d2edaa6e2152ee35dc01ed3/numpy-2.3.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:2fb86b7e58f9ac50e1e9dd1290154107e47d1eef23a0ae9145ded06ea606f992", size = 6736652, upload-time = "2025-06-21T12:22:33.629Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/62/d68e52fb6fde5586650d4c0ce0b05ff3a48ad4df4ffd1b8866479d1d671d/numpy-2.3.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:23ab05b2d241f76cb883ce8b9a93a680752fbfcbd51c50eff0b88b979e471d8c", size = 14331561, upload-time = "2025-06-21T12:22:55.056Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/ec/b74d3f2430960044bdad6900d9f5edc2dc0fb8bf5a0be0f65287bf2cbe27/numpy-2.3.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ce2ce9e5de4703a673e705183f64fd5da5bf36e7beddcb63a25ee2286e71ca48", size = 16693349, upload-time = "2025-06-21T12:23:20.53Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/15/def96774b9d7eb198ddadfcbd20281b20ebb510580419197e225f5c55c3e/numpy-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c4913079974eeb5c16ccfd2b1f09354b8fed7e0d6f2cab933104a09a6419b1ee", size = 15642053, upload-time = "2025-06-21T12:23:43.697Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/57/c3203974762a759540c6ae71d0ea2341c1fa41d84e4971a8e76d7141678a/numpy-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:010ce9b4f00d5c036053ca684c77441f2f2c934fd23bee058b4d6f196efd8280", size = 18434184, upload-time = "2025-06-21T12:24:10.708Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/8a/ccdf201457ed8ac6245187850aff4ca56a79edbea4829f4e9f14d46fa9a5/numpy-2.3.1-cp313-cp313t-win32.whl", hash = "sha256:6269b9edfe32912584ec496d91b00b6d34282ca1d07eb10e82dfc780907d6c2e", size = 6440678, upload-time = "2025-06-21T12:24:21.596Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/7e/7f431d8bd8eb7e03d79294aed238b1b0b174b3148570d03a8a8a8f6a0da9/numpy-2.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:2a809637460e88a113e186e87f228d74ae2852a2e0c44de275263376f17b5bdc", size = 12870697, upload-time = "2025-06-21T12:24:40.644Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/ca/af82bf0fad4c3e573c6930ed743b5308492ff19917c7caaf2f9b6f9e2e98/numpy-2.3.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eccb9a159db9aed60800187bc47a6d3451553f0e1b08b068d8b277ddfbb9b244", size = 10260376, upload-time = "2025-06-21T12:24:56.884Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/34/facc13b9b42ddca30498fc51f7f73c3d0f2be179943a4b4da8686e259740/numpy-2.3.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ad506d4b09e684394c42c966ec1527f6ebc25da7f4da4b1b056606ffe446b8a3", size = 21070637, upload-time = "2025-06-21T12:26:12.518Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/b6/41b705d9dbae04649b529fc9bd3387664c3281c7cd78b404a4efe73dcc45/numpy-2.3.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ebb8603d45bc86bbd5edb0d63e52c5fd9e7945d3a503b77e486bd88dde67a19b", size = 5304087, upload-time = "2025-06-21T12:26:22.294Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/b4/fe3ac1902bff7a4934a22d49e1c9d71a623204d654d4cc43c6e8fe337fcb/numpy-2.3.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:15aa4c392ac396e2ad3d0a2680c0f0dee420f9fed14eef09bdb9450ee6dcb7b7", size = 6817588, upload-time = "2025-06-21T12:26:32.939Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/ee/89bedf69c36ace1ac8f59e97811c1f5031e179a37e4821c3a230bf750142/numpy-2.3.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c6e0bf9d1a2f50d2b65a7cf56db37c095af17b59f6c132396f7c6d5dd76484df", size = 14399010, upload-time = "2025-06-21T12:26:54.086Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/08/e00e7070ede29b2b176165eba18d6f9784d5349be3c0c1218338e79c27fd/numpy-2.3.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:eabd7e8740d494ce2b4ea0ff05afa1b7b291e978c0ae075487c51e8bd93c0c68", size = 16752042, upload-time = "2025-06-21T12:27:19.018Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/6b/1c6b515a83d5564b1698a61efa245727c8feecf308f4091f565988519d20/numpy-2.3.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e610832418a2bc09d974cc9fecebfa51e9532d6190223bc5ef6a7402ebf3b5cb", size = 12927246, upload-time = "2025-06-21T12:27:38.618Z" },
+]
+
+[[package]]
+name = "openpyxl"
+version = "3.1.5"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+dependencies = [
+    { name = "et-xmlfile" },
+]
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
+]
+
+[[package]]
+name = "pandas"
+version = "2.3.1"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+dependencies = [
+    { name = "numpy" },
+    { name = "python-dateutil" },
+    { name = "pytz" },
+    { name = "tzdata" },
+]
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/6f/75aa71f8a14267117adeeed5d21b204770189c0a0025acbdc03c337b28fc/pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2", size = 4487493, upload-time = "2025-07-07T19:20:04.079Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/1c/ccf70029e927e473a4476c00e0d5b32e623bff27f0402d0a92b7fc29bb9f/pandas-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2b0540963d83431f5ce8870ea02a7430adca100cec8a050f0811f8e31035541b", size = 11566608, upload-time = "2025-07-07T19:18:33.86Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/d3/3c37cb724d76a841f14b8f5fe57e5e3645207cc67370e4f84717e8bb7657/pandas-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fe7317f578c6a153912bd2292f02e40c1d8f253e93c599e82620c7f69755c74f", size = 10823181, upload-time = "2025-07-07T19:18:36.151Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/4c/367c98854a1251940edf54a4df0826dcacfb987f9068abf3e3064081a382/pandas-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6723a27ad7b244c0c79d8e7007092d7c8f0f11305770e2f4cd778b3ad5f9f85", size = 11793570, upload-time = "2025-07-07T19:18:38.385Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/07/5f/63760ff107bcf5146eee41b38b3985f9055e710a72fdd637b791dea3495c/pandas-2.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3462c3735fe19f2638f2c3a40bd94ec2dc5ba13abbb032dd2fa1f540a075509d", size = 12378887, upload-time = "2025-07-07T19:18:41.284Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/53/f31a9b4dfe73fe4711c3a609bd8e60238022f48eacedc257cd13ae9327a7/pandas-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:98bcc8b5bf7afed22cc753a28bc4d9e26e078e777066bc53fac7904ddef9a678", size = 13230957, upload-time = "2025-07-07T19:18:44.187Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/94/6fce6bf85b5056d065e0a7933cba2616dcb48596f7ba3c6341ec4bcc529d/pandas-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d544806b485ddf29e52d75b1f559142514e60ef58a832f74fb38e48d757b299", size = 13883883, upload-time = "2025-07-07T19:18:46.498Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c8/7b/bdcb1ed8fccb63d04bdb7635161d0ec26596d92c9d7a6cce964e7876b6c1/pandas-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b3cd4273d3cb3707b6fffd217204c52ed92859533e31dc03b7c5008aa933aaab", size = 11340212, upload-time = "2025-07-07T19:18:49.293Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/46/de/b8445e0f5d217a99fe0eeb2f4988070908979bec3587c0633e5428ab596c/pandas-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:689968e841136f9e542020698ee1c4fbe9caa2ed2213ae2388dc7b81721510d3", size = 11588172, upload-time = "2025-07-07T19:18:52.054Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/e0/801cdb3564e65a5ac041ab99ea6f1d802a6c325bb6e58c79c06a3f1cd010/pandas-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:025e92411c16cbe5bb2a4abc99732a6b132f439b8aab23a59fa593eb00704232", size = 10717365, upload-time = "2025-07-07T19:18:54.785Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/a5/c76a8311833c24ae61a376dbf360eb1b1c9247a5d9c1e8b356563b31b80c/pandas-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b7ff55f31c4fcb3e316e8f7fa194566b286d6ac430afec0d461163312c5841e", size = 11280411, upload-time = "2025-07-07T19:18:57.045Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/01/e383018feba0a1ead6cf5fe8728e5d767fee02f06a3d800e82c489e5daaf/pandas-2.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dcb79bf373a47d2a40cf7232928eb7540155abbc460925c2c96d2d30b006eb4", size = 11988013, upload-time = "2025-07-07T19:18:59.771Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/14/cec7760d7c9507f11c97d64f29022e12a6cc4fc03ac694535e89f88ad2ec/pandas-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56a342b231e8862c96bdb6ab97170e203ce511f4d0429589c8ede1ee8ece48b8", size = 12767210, upload-time = "2025-07-07T19:19:02.944Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/b9/6e2d2c6728ed29fb3d4d4d302504fb66f1a543e37eb2e43f352a86365cdf/pandas-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ca7ed14832bce68baef331f4d7f294411bed8efd032f8109d690df45e00c4679", size = 13440571, upload-time = "2025-07-07T19:19:06.82Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/a5/3a92893e7399a691bad7664d977cb5e7c81cf666c81f89ea76ba2bff483d/pandas-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ac942bfd0aca577bef61f2bc8da8147c4ef6879965ef883d8e8d5d2dc3e744b8", size = 10987601, upload-time = "2025-07-07T19:19:09.589Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/32/ed/ff0a67a2c5505e1854e6715586ac6693dd860fbf52ef9f81edee200266e7/pandas-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9026bd4a80108fac2239294a15ef9003c4ee191a0f64b90f170b40cfb7cf2d22", size = 11531393, upload-time = "2025-07-07T19:19:12.245Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c7/db/d8f24a7cc9fb0972adab0cc80b6817e8bef888cfd0024eeb5a21c0bb5c4a/pandas-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6de8547d4fdb12421e2d047a2c446c623ff4c11f47fddb6b9169eb98ffba485a", size = 10668750, upload-time = "2025-07-07T19:19:14.612Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/b0/80f6ec783313f1e2356b28b4fd8d2148c378370045da918c73145e6aab50/pandas-2.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782647ddc63c83133b2506912cc6b108140a38a37292102aaa19c81c83db2928", size = 11342004, upload-time = "2025-07-07T19:19:16.857Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/e2/20a317688435470872885e7fc8f95109ae9683dec7c50be29b56911515a5/pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba6aff74075311fc88504b1db890187a3cd0f887a5b10f5525f8e2ef55bfdb9", size = 12050869, upload-time = "2025-07-07T19:19:19.265Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/79/20d746b0a96c67203a5bee5fb4e00ac49c3e8009a39e1f78de264ecc5729/pandas-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5635178b387bd2ba4ac040f82bc2ef6e6b500483975c4ebacd34bec945fda12", size = 12750218, upload-time = "2025-07-07T19:19:21.547Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/0f/145c8b41e48dbf03dd18fdd7f24f8ba95b8254a97a3379048378f33e7838/pandas-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f3bf5ec947526106399a9e1d26d40ee2b259c66422efdf4de63c848492d91bb", size = 13416763, upload-time = "2025-07-07T19:19:23.939Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/c0/54415af59db5cdd86a3d3bf79863e8cc3fa9ed265f0745254061ac09d5f2/pandas-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c78cf43c8fde236342a1cb2c34bcff89564a7bfed7e474ed2fffa6aed03a956", size = 10987482, upload-time = "2025-07-07T19:19:42.699Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/64/2fd2e400073a1230e13b8cd604c9bc95d9e3b962e5d44088ead2e8f0cfec/pandas-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8dfc17328e8da77be3cf9f47509e5637ba8f137148ed0e9b5241e1baf526e20a", size = 12029159, upload-time = "2025-07-07T19:19:26.362Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/0a/d84fd79b0293b7ef88c760d7dca69828d867c89b6d9bc52d6a27e4d87316/pandas-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ec6c851509364c59a5344458ab935e6451b31b818be467eb24b0fe89bd05b6b9", size = 11393287, upload-time = "2025-07-07T19:19:29.157Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/ae/ff885d2b6e88f3c7520bb74ba319268b42f05d7e583b5dded9837da2723f/pandas-2.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911580460fc4884d9b05254b38a6bfadddfcc6aaef856fb5859e7ca202e45275", size = 11309381, upload-time = "2025-07-07T19:19:31.436Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/86/1fa345fc17caf5d7780d2699985c03dbe186c68fee00b526813939062bb0/pandas-2.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f4d6feeba91744872a600e6edbbd5b033005b431d5ae8379abee5bcfa479fab", size = 11883998, upload-time = "2025-07-07T19:19:34.267Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/aa/e58541a49b5e6310d89474333e994ee57fea97c8aaa8fc7f00b873059bbf/pandas-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fe37e757f462d31a9cd7580236a82f353f5713a80e059a29753cf938c6775d96", size = 12704705, upload-time = "2025-07-07T19:19:36.856Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/f9/07086f5b0f2a19872554abeea7658200824f5835c58a106fa8f2ae96a46c/pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444", size = 13189044, upload-time = "2025-07-07T19:19:39.999Z" },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+dependencies = [
+    { name = "six" },
+]
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
+[[package]]
+name = "pytz"
+version = "2025.2"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.2"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.4"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+dependencies = [
+    { name = "certifi" },
+    { name = "charset-normalizer" },
+    { name = "idna" },
+    { name = "urllib3" },
+]
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
+]
+
+[[package]]
+name = "retrying"
+version = "1.4.1"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/2e/90b236e496810c23eb428a1f3e2723849eb219d6196a4f7afe16f4981b5c/retrying-1.4.1.tar.gz", hash = "sha256:4d206e0ed2aff5ef2f3cd867abb9511e9e8f31127c5aca20f1d5246e476903b0", size = 11344, upload-time = "2025-07-19T09:39:01.906Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/25/f3b628e123699139b959551ed922f35af97fa1505e195ae3e6537a14fbc3/retrying-1.4.1-py3-none-any.whl", hash = "sha256:d736050c1adfc0a71fa022d9198ee130b0e66be318678a3fdd8b1b8872dc0997", size = 12184, upload-time = "2025-07-19T09:39:00.574Z" },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[package]]
+name = "soupsieve"
+version = "2.7"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" },
+]
+
+[[package]]
+name = "sqlalchemy"
+version = "2.0.41"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+dependencies = [
+    { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" },
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/37/4e/b00e3ffae32b74b5180e15d2ab4040531ee1bef4c19755fe7926622dc958/sqlalchemy-2.0.41-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f", size = 2121232, upload-time = "2025-05-14T17:48:20.444Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ef/30/6547ebb10875302074a37e1970a5dce7985240665778cfdee2323709f749/sqlalchemy-2.0.41-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560", size = 2110897, upload-time = "2025-05-14T17:48:21.634Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/21/59df2b41b0f6c62da55cd64798232d7349a9378befa7f1bb18cf1dfd510a/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f", size = 3273313, upload-time = "2025-05-14T17:51:56.205Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/e4/b9a7a0e5c6f79d49bcd6efb6e90d7536dc604dab64582a9dec220dab54b6/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6", size = 3273807, upload-time = "2025-05-14T17:55:26.928Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/39/d8/79f2427251b44ddee18676c04eab038d043cff0e764d2d8bb08261d6135d/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04", size = 3209632, upload-time = "2025-05-14T17:51:59.384Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/16/730a82dda30765f63e0454918c982fb7193f6b398b31d63c7c3bd3652ae5/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582", size = 3233642, upload-time = "2025-05-14T17:55:29.901Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/61/c0d4607f7799efa8b8ea3c49b4621e861c8f5c41fd4b5b636c534fcb7d73/sqlalchemy-2.0.41-cp311-cp311-win32.whl", hash = "sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8", size = 2086475, upload-time = "2025-05-14T17:56:02.095Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/8e/8344f8ae1cb6a479d0741c02cd4f666925b2bf02e2468ddaf5ce44111f30/sqlalchemy-2.0.41-cp311-cp311-win_amd64.whl", hash = "sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504", size = 2110903, upload-time = "2025-05-14T17:56:03.499Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/2a/f1f4e068b371154740dd10fb81afb5240d5af4aa0087b88d8b308b5429c2/sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9", size = 2119645, upload-time = "2025-05-14T17:55:24.854Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/e8/c664a7e73d36fbfc4730f8cf2bf930444ea87270f2825efbe17bf808b998/sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1", size = 2107399, upload-time = "2025-05-14T17:55:28.097Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/78/8a9cf6c5e7135540cb682128d091d6afa1b9e48bd049b0d691bf54114f70/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70", size = 3293269, upload-time = "2025-05-14T17:50:38.227Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/35/f74add3978c20de6323fb11cb5162702670cc7a9420033befb43d8d5b7a4/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e", size = 3303364, upload-time = "2025-05-14T17:51:49.829Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/d4/c990f37f52c3f7748ebe98883e2a0f7d038108c2c5a82468d1ff3eec50b7/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078", size = 3229072, upload-time = "2025-05-14T17:50:39.774Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/69/cab11fecc7eb64bc561011be2bd03d065b762d87add52a4ca0aca2e12904/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", size = 3268074, upload-time = "2025-05-14T17:51:51.736Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/ca/0c19ec16858585d37767b167fc9602593f98998a68a798450558239fb04a/sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6", size = 2084514, upload-time = "2025-05-14T17:55:49.915Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/23/4c2833d78ff3010a4e17f984c734f52b531a8c9060a50429c9d4b0211be6/sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0", size = 2111557, upload-time = "2025-05-14T17:55:51.349Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload-time = "2025-05-14T17:55:31.177Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload-time = "2025-05-14T17:55:34.921Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload-time = "2025-05-14T17:50:41.418Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045, upload-time = "2025-05-14T17:51:54.722Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357, upload-time = "2025-05-14T17:50:43.483Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511, upload-time = "2025-05-14T17:51:57.308Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420, upload-time = "2025-05-14T17:55:52.69Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329, upload-time = "2025-05-14T17:55:54.495Z" },
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.14.1"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
+]
+
+[[package]]
+name = "tzdata"
+version = "2025.2"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
+]
+
+[[package]]
+name = "tzlocal"
+version = "5.3.1"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+dependencies = [
+    { name = "tzdata", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.5.0"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
+]
+
+[[package]]
+name = "werkzeug"
+version = "3.1.3"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+dependencies = [
+    { name = "markupsafe" },
+]
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" },
+]
+
+[[package]]
+name = "xlsxwriter"
+version = "3.2.5"
+source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
+sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/47/7704bac42ac6fe1710ae099b70e6a1e68ed173ef14792b647808c357da43/xlsxwriter-3.2.5.tar.gz", hash = "sha256:7e88469d607cdc920151c0ab3ce9cf1a83992d4b7bc730c5ffdd1a12115a7dbe", size = 213306, upload-time = "2025-06-17T08:59:14.619Z" }
+wheels = [
+    { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fa/34/a22e6664211f0c8879521328000bdcae9bf6dbafa94a923e531f6d5b3f73/xlsxwriter-3.2.5-py3-none-any.whl", hash = "sha256:4f4824234e1eaf9d95df9a8fe974585ff91d0f5e3d3f12ace5b71e443c1c6abd", size = 172347, upload-time = "2025-06-17T08:59:13.453Z" },
+]