From b167d953db9c54f48f620bd020bd7eb337539256 Mon Sep 17 00:00:00 2001 From: Lxy Date: Sat, 14 Feb 2026 12:22:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=EF=BC=8C=E6=8F=90=E4=BA=A4=E5=9F=BA=E7=A1=80=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=EF=BC=88=E9=A6=96=E9=A1=B5=E3=80=81=E5=88=86=E6=9E=90=E8=AF=A6?= =?UTF-8?q?=E6=83=85=EF=BC=89=E5=B7=B2=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- # AI 期货分析系统运行指南.ini | 89 +++ .env | 25 + .env.example | 25 + data/futures_analysis.db | Bin 0 -> 135168 bytes demo.py | 265 +++++++ qihuo_analyzer/__init__.py | 3 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 216 bytes .../core/__pycache__/models.cpython-311.pyc | Bin 0 -> 5409 bytes qihuo_analyzer/core/models.py | 104 +++ .../__pycache__/data_fetcher.cpython-311.pyc | Bin 0 -> 18233 bytes .../__pycache__/data_storage.cpython-311.pyc | Bin 0 -> 19462 bytes qihuo_analyzer/data/data_fetcher.py | 461 +++++++++++++ qihuo_analyzer/data/data_storage.py | 378 ++++++++++ .../deepseek_agent.cpython-311.pyc | Bin 0 -> 14203 bytes .../fund_flow_monitor.cpython-311.pyc | Bin 0 -> 13827 bytes .../__pycache__/risk_manager.cpython-311.pyc | Bin 0 -> 10921 bytes .../rollover_detector.cpython-311.pyc | Bin 0 -> 18115 bytes .../support_resistance.cpython-311.pyc | Bin 0 -> 15990 bytes .../__pycache__/trend_filter.cpython-311.pyc | Bin 0 -> 9898 bytes qihuo_analyzer/modules/deepseek_agent.py | 295 ++++++++ qihuo_analyzer/modules/fund_flow_monitor.py | 284 ++++++++ qihuo_analyzer/modules/risk_manager.py | 274 ++++++++ qihuo_analyzer/modules/rollover_detector.py | 483 +++++++++++++ qihuo_analyzer/modules/support_resistance.py | 389 +++++++++++ qihuo_analyzer/modules/trend_filter.py | 226 ++++++ .../config_manager.cpython-311.pyc | Bin 0 -> 3801 bytes .../technical_analysis.cpython-311.pyc | Bin 0 -> 9125 bytes qihuo_analyzer/utils/config_manager.py | 70 ++ qihuo_analyzer/utils/technical_analysis.py | 153 +++++ requirements.txt | 9 + test_system.py | 192 ++++++ web/app.py | 645 ++++++++++++++++++ web/data/futures_analysis.db | Bin 0 -> 28672 bytes web/templates/card.html | 405 +++++++++++ web/templates/error.html | 117 ++++ web/templates/index.html | 272 ++++++++ web/templates/symbol_detail.html | 630 +++++++++++++++++ 期货分析系统架构提示.ini | 372 ++++++++++ 38 files changed, 6166 insertions(+) create mode 100644 # AI 期货分析系统运行指南.ini create mode 100644 .env create mode 100644 .env.example create mode 100644 data/futures_analysis.db create mode 100644 demo.py create mode 100644 qihuo_analyzer/__init__.py create mode 100644 qihuo_analyzer/__pycache__/__init__.cpython-311.pyc create mode 100644 qihuo_analyzer/core/__pycache__/models.cpython-311.pyc create mode 100644 qihuo_analyzer/core/models.py create mode 100644 qihuo_analyzer/data/__pycache__/data_fetcher.cpython-311.pyc create mode 100644 qihuo_analyzer/data/__pycache__/data_storage.cpython-311.pyc create mode 100644 qihuo_analyzer/data/data_fetcher.py create mode 100644 qihuo_analyzer/data/data_storage.py create mode 100644 qihuo_analyzer/modules/__pycache__/deepseek_agent.cpython-311.pyc create mode 100644 qihuo_analyzer/modules/__pycache__/fund_flow_monitor.cpython-311.pyc create mode 100644 qihuo_analyzer/modules/__pycache__/risk_manager.cpython-311.pyc create mode 100644 qihuo_analyzer/modules/__pycache__/rollover_detector.cpython-311.pyc create mode 100644 qihuo_analyzer/modules/__pycache__/support_resistance.cpython-311.pyc create mode 100644 qihuo_analyzer/modules/__pycache__/trend_filter.cpython-311.pyc create mode 100644 qihuo_analyzer/modules/deepseek_agent.py create mode 100644 qihuo_analyzer/modules/fund_flow_monitor.py create mode 100644 qihuo_analyzer/modules/risk_manager.py create mode 100644 qihuo_analyzer/modules/rollover_detector.py create mode 100644 qihuo_analyzer/modules/support_resistance.py create mode 100644 qihuo_analyzer/modules/trend_filter.py create mode 100644 qihuo_analyzer/utils/__pycache__/config_manager.cpython-311.pyc create mode 100644 qihuo_analyzer/utils/__pycache__/technical_analysis.cpython-311.pyc create mode 100644 qihuo_analyzer/utils/config_manager.py create mode 100644 qihuo_analyzer/utils/technical_analysis.py create mode 100644 requirements.txt create mode 100644 test_system.py create mode 100644 web/app.py create mode 100644 web/data/futures_analysis.db create mode 100644 web/templates/card.html create mode 100644 web/templates/error.html create mode 100644 web/templates/index.html create mode 100644 web/templates/symbol_detail.html create mode 100644 期货分析系统架构提示.ini diff --git a/# AI 期货分析系统运行指南.ini b/# AI 期货分析系统运行指南.ini new file mode 100644 index 0000000..178012a --- /dev/null +++ b/# AI 期货分析系统运行指南.ini @@ -0,0 +1,89 @@ +# AI 期货分析系统运行指南 +## 1. 环境准备 +### 1.1 系统要求 +- 操作系统 : Windows 10/11 或 Linux/macOS +- Python 版本 : Python 3.8 及以上 +- 内存 : 至少 4GB +- 存储空间 : 至少 500MB +### 1.2 检查 Python 环境 +``` +# 检查 Python 版本 +python --version + +# 检查 pip 版本 +pip --version +``` +## 2. 安装依赖 +### 2.1 安装核心依赖 +在项目根目录执行: + +``` +pip install -r requirements.txt +``` +### 2.2 解决可能的依赖问题 +- tqsdk 导入失败 : 系统会自动使用模拟数据,不影响基本功能 +- TA-Lib 安装失败 : 可以注释掉该依赖,系统会使用内置的技术分析函数 +## 3. 配置设置 +### 3.1 创建环境配置文件 +复制 .env.example 文件并重命名为 .env : + +``` +# Windows +copy .env.example .env + +# Linux/macOS +cp .env.example .env +``` +### 3.2 配置关键参数 +编辑 .env 文件,设置以下参数: + +- DEEPSEEK_API_KEY : DeepSeek API 密钥(可选,不设置会使用模拟响应) +- DB_PATH : 数据库存储路径(默认即可) +- REVIEW_TIMES : 自动复盘时间(默认:09:00,12:30,15:30) +## 4. 运行系统 +### 4.1 运行系统测试 +验证所有组件是否正常工作: + +``` +python test_system.py +``` +### 4.2 运行功能演示 +体验完整的系统功能: + +``` +python demo.py +``` +### 4.3 预期输出 +运行 demo.py 后,您将看到: + +- 系统初始化信息 +- 数据获取和分析过程 +- 多维度分析结果 +- AI 智能研判 +- 交易建议生成 +- 综合分析报告 +## 5. 系统使用说明 +### 5.1 核心功能 +- 数据获取 : 支持实时数据和模拟数据 +- 趋势分析 : 基于 ADX 和多周期分析 +- 风险控制 : ATR 动态止损和仓位管理 +- 资金监控 : 持仓量和资金流向分析 +- AI 研判 : DeepSeek 大模型智能分析 +- 换月预警 : 交割日风险提示 +### 5.2 扩展功能 +- 定时任务 : 可配置的自动复盘 +- 数据存储 : 历史分析结果持久化 +- API 接口 : 可扩展为 Web 服务 +## 6. 故障排除 +### 6.1 常见问题 +- 模块导入失败 : 检查依赖安装是否完整 +- API 调用失败 : 检查网络连接和 API 密钥 +- 数据为空 : 系统会自动使用模拟数据,确保数据获取模块正常 +### 6.2 日志查看 +系统运行过程中会输出详细日志,可根据日志信息定位问题。 + +## 7. 后续开发建议 +- Web 前端 : 开发可视化界面 +- 实时行情 : 接入更多数据源 +- 策略回测 : 增加历史数据回测功能 +- 多模型融合 : 集成多个 AI 模型 \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000..0af9986 --- /dev/null +++ b/.env @@ -0,0 +1,25 @@ +# API配置 +OPENAI_API_KEY= +DEEPSEEK_API_KEY= +DEEPSEEK_API_URL=https://api.deepseek.com/v1/chat/completions + +# 天勤TQSDK配置 +# 填写你的TQSDK账号密码以使用真实数据 +TQSDK_USERNAME=windsdreamer +TQSDK_PASSWORD=1qazse42W3 +TQSERVER_HOST=api.shinnytech.com +TQSERVER_PORT=7777 + +# 数据库配置 +DB_PATH=./data/futures_analysis.db + +# 风险配置 +MAX_RISK_PERCENT=0.02 +MIN_PROFIT_LOSS_RATIO=1.5 + +# 策略配置 +DEFAULT_ATR_MULTIPLIER=2.0 +DEFAULT_ADX_THRESHOLD=20 + +# 定时任务配置 +REVIEW_TIMES=09:00,12:30,15:30 \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c2c4900 --- /dev/null +++ b/.env.example @@ -0,0 +1,25 @@ +# API配置 +OPENAI_API_KEY= +DEEPSEEK_API_KEY= +DEEPSEEK_API_URL=https://api.deepseek.com/v1/chat/completions + +# 天勤TQSDK配置 +# 填写你的TQSDK账号密码以使用真实数据 +TQSDK_USERNAME= +TQSDK_PASSWORD= +TQSERVER_HOST=api.shinnytech.com +TQSERVER_PORT=7777 + +# 数据库配置 +DB_PATH=./data/futures_analysis.db + +# 风险配置 +MAX_RISK_PERCENT=0.02 +MIN_PROFIT_LOSS_RATIO=1.5 + +# 策略配置 +DEFAULT_ATR_MULTIPLIER=2.0 +DEFAULT_ADX_THRESHOLD=20 + +# 定时任务配置 +REVIEW_TIMES=09:00,12:30,15:30 \ No newline at end of file diff --git a/data/futures_analysis.db b/data/futures_analysis.db new file mode 100644 index 0000000000000000000000000000000000000000..608c4582f11df412c6b2824b529800cc27aa8be9 GIT binary patch literal 135168 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ȰXARXd`@?9tlW(3Uu5+`OGC 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 MNOLQZI|>2%KPyi9ga7~l literal 0 HcmV?d00001 diff --git a/demo.py b/demo.py new file mode 100644 index 0000000..f1eee4c --- /dev/null +++ b/demo.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +# 期货分析系统演示脚本 + +import sys +import json +from datetime import datetime +from qihuo_analyzer.data.data_fetcher import DataFetcher +from qihuo_analyzer.data.data_storage import DataStorage +from qihuo_analyzer.modules.trend_filter import TrendFilter +from qihuo_analyzer.modules.risk_manager import RiskManager +from qihuo_analyzer.modules.fund_flow_monitor import FundFlowMonitor +from qihuo_analyzer.modules.support_resistance import SupportResistance +from qihuo_analyzer.modules.rollover_detector import RolloverDetector +from qihuo_analyzer.modules.deepseek_agent import DeepseekAgent +from qihuo_analyzer.core.models import AnalysisResult + + +def print_header(title): + """打印标题""" + print(f"\n{'='*60}") + print(f"{title:^60}") + print(f"{'='*60}") + + +def print_section(title): + """打印章节标题""" + print(f"\n{'-'*40}") + print(f"{title:^40}") + print(f"{'-'*40}") + + +def print_json(data, indent=2): + """打印JSON格式数据""" + print(json.dumps(data, ensure_ascii=False, indent=indent)) + + +def main(): + """主函数""" + print_header("AI 期货分析系统演示") + print(f"当前时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + + # 初始化组件 + print_section("初始化系统组件") + data_fetcher = DataFetcher() + data_storage = DataStorage() + trend_filter = TrendFilter() + risk_manager = RiskManager() + fund_flow_monitor = FundFlowMonitor() + support_resistance = SupportResistance() + rollover_detector = RolloverDetector() + deepseek_agent = DeepseekAgent() + + print("✅ 系统组件初始化完成") + + # 选择合约 + symbol = "CU2309" # 铜期货合约 + print_section(f"分析合约: {symbol}") + + # 获取K线数据 + print("📊 获取K线数据...") + kline_data = data_fetcher.get_kline_data(symbol, "1d", 200) + if kline_data.empty: + print("❌ 获取K线数据失败") + return + + print(f"✅ 获取K线数据成功,共 {len(kline_data)} 条记录") + print(f"最新价格: {kline_data['close'].iloc[-1]:.2f}") + + # 保存K线数据到数据库 + data_storage.save_kline_data(symbol, "1d", kline_data) + print("✅ K线数据已保存到数据库") + + # 1. 趋势分析 + print_section("1. 趋势分析") + trend_analysis = trend_filter.analyze_trend(kline_data) + win_rate = trend_filter.calculate_win_rate(kline_data) + cycle = trend_filter.judge_cycle(kline_data) + + print("📈 趋势分析结果:") + print(f"ADX: {trend_analysis['adx']:.2f}") + print(f"趋势强度: {trend_analysis['trend_strength']}") + print(f"趋势方向: {trend_analysis['trend_direction']}") + print(f"综合趋势: {trend_analysis['overall_trend']}") + print(f"胜率: {win_rate:.2f}%") + print(f"推荐周期: {cycle}") + + # 2. 资金流向分析 + print_section("2. 资金流向分析") + fund_flow_analysis = fund_flow_monitor.analyze_fund_flow(kline_data) + + print("💹 资金流向分析结果:") + print(f"资金流向强度: {fund_flow_analysis['fund_flow_strength']:.2f}") + print(f"持仓量趋势: {fund_flow_analysis['oi_trend']}") + print(f"量价配合度: {fund_flow_analysis['volume_price_fit']:.2f}%") + print(f"量价背离: {fund_flow_analysis['divergence']}") + print(f"资金面信号: {fund_flow_analysis['fund_signal']}") + + # 3. 压力支撑分析 + print_section("3. 压力支撑分析") + sr_analysis = support_resistance.analyze_support_resistance(kline_data) + + print("🛡️ 压力支撑分析结果:") + support_levels = sr_analysis['support_resistance_levels']['support_levels'] + resistance_levels = sr_analysis['support_resistance_levels']['resistance_levels'] + print(f"支撑位: {[f'{level:.2f}' for level in support_levels[:3]]}") + print(f"阻力位: {[f'{level:.2f}' for level in resistance_levels[:3]]}") + + # 4. 风险分析 + print_section("4. 风险分析") + current_price = kline_data['close'].iloc[-1] + atr = trend_analysis.get('atr', 20) + + # 计算止损位 + stop_loss_long = risk_manager.calculate_stop_loss(kline_data, current_price, "long") + stop_loss_short = risk_manager.calculate_stop_loss(kline_data, current_price, "short") + + # 计算仓位大小 + account_balance = 1000000 # 100万账户 + position_info = risk_manager.calculate_position_size(account_balance, kline_data, "long", current_price) + + print("⚡ 风险分析结果:") + print(f"ATR: {atr:.2f}") + print(f"做多止损: {stop_loss_long:.2f}") + print(f"做空止损: {stop_loss_short:.2f}") + print(f"建议仓位: {position_info['suggested_units']} 手") + print(f"风险比例: {position_info['actual_risk_percent']*100:.2f}%") + print(f"杠杆比例: {position_info['leverage']:.2f}") + + # 5. 换月分析 + print_section("5. 换月分析") + rollover_analysis = rollover_detector.analyze_rollover(symbol, kline_data) + + print("📅 换月分析结果:") + print(f"交割日期: {rollover_analysis['expire_date']}") + print(f"距离交割日: {rollover_analysis['days_to_delivery']} 天") + print(f"预警级别: {rollover_analysis['warning_level']}") + print(f"流动性风险: {rollover_analysis['liquidity_risk']}") + print(f"换月建议: {rollover_analysis['rollover_warning']['warning_message']}") + + # 6. AI 智能研判 + print_section("6. AI 智能研判") + + # 准备数据 + market_data = { + 'symbol': symbol, + 'latest_price': current_price, + 'volume': kline_data['volume'].iloc[-1], + 'open_interest': kline_data['open_interest'].iloc[-1], + 'timeframe': '1d' + } + + technical_indicators = { + 'macd': {'signal': '金叉'}, + 'rsi': 55, + 'bollinger': {'position': '中轨附近'}, + 'kdj': {'signal': '金叉'}, + 'atr': atr + } + + trend_data = { + 'adx': trend_analysis['adx'], + 'trend_strength': trend_analysis['trend_strength'], + 'trend_direction': trend_analysis['trend_direction'], + 'ma_relationship': trend_analysis['ma_relationship'], + 'multi_period_analysis': trend_analysis['multi_period_analysis'], + 'overall_trend': trend_analysis['overall_trend'], + 'win_rate': win_rate + } + + risk_metrics = { + 'stop_loss': stop_loss_long, + 'target_price': resistance_levels[0] if resistance_levels else current_price * 1.05, + 'profit_loss_ratio': 1.8, + 'position_size': position_info['suggested_units'], + 'risk_ratio': position_info['actual_risk_percent'] * 100 + } + + # AI 分析 + ai_analysis = deepseek_agent.analyze_market(market_data, technical_indicators, trend_data, risk_metrics) + + print("🤖 AI 分析结果:") + print(f"趋势判断: {ai_analysis.get('trend_judgment', '未知')}") + print(f"胜率评估: {ai_analysis.get('win_rate_assessment', '未知')}") + print(f"风险预警: {ai_analysis.get('risk_warning', '未知')}") + print(f"交易建议: {ai_analysis.get('trade_recommendation', '未知')}") + print(f"分析逻辑: {ai_analysis.get('analysis_logic', '未知')}") + + # 7. 生成交易建议 + print_section("7. 交易建议") + recommendation = deepseek_agent.generate_trade_recommendation(ai_analysis) + + print("📋 交易建议详情:") + print(f"交易方向: {recommendation.get('direction', '未知')}") + print(f"入场价格: {recommendation.get('entry_price', '未知')}") + print(f"止损价格: {recommendation.get('stop_loss', '未知')}") + print(f"目标价格: {recommendation.get('target_price', '未知')}") + print(f"建议仓位: {recommendation.get('position_size', '未知')} 手") + print(f"执行计划: {recommendation.get('execution_plan', '未知')}") + print(f"风险提示: {recommendation.get('risk_tips', '未知')}") + + # 8. 保存分析结果 + print_section("8. 保存分析结果") + + # 创建分析结果对象 + analysis_result = AnalysisResult(symbol) + analysis_result.trend = trend_analysis['overall_trend'] + analysis_result.probability = win_rate + analysis_result.direction = recommendation.get('direction', 'wait') + analysis_result.cycle = cycle + analysis_result.atr = atr + analysis_result.adx = trend_analysis['adx'] + analysis_result.support = support_levels[0] if support_levels else None + analysis_result.resistance = resistance_levels[0] if resistance_levels else None + analysis_result.stop_loss = recommendation.get('stop_loss') + analysis_result.target_price = recommendation.get('target_price') + analysis_result.position_size = recommendation.get('position_size') + analysis_result.risk_ratio = position_info['actual_risk_percent'] * 100 + analysis_result.fund_flow = fund_flow_analysis + analysis_result.signals = { + 'trend': trend_analysis['overall_trend'], + 'fund': fund_flow_analysis['fund_signal'], + 'risk': rollover_analysis['liquidity_risk'], + 'ai': recommendation.get('direction', 'wait') + } + + # 保存到数据库 + result_dict = analysis_result.to_dict() + success = data_storage.save_analysis_result(result_dict) + + if success: + print("✅ 分析结果已保存到数据库") + else: + print("❌ 保存分析结果失败") + + # 9. 生成综合报告 + print_section("9. 综合分析报告") + print_header("AI 期货分析系统 - 综合报告") + print(f"合约: {symbol}") + print(f"分析时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print(f"最新价格: {current_price:.2f}") + print(f"趋势判断: {trend_analysis['overall_trend']}") + print(f"胜率评估: {win_rate:.2f}%") + print(f"资金信号: {fund_flow_analysis['fund_signal']}") + print(f"交易方向: {recommendation.get('direction', 'wait')}") + print(f"建议仓位: {recommendation.get('position_size', '未知')} 手") + print(f"止损价格: {recommendation.get('stop_loss', '未知')}") + print(f"目标价格: {recommendation.get('target_price', '未知')}") + print(f"风险等级: {rollover_analysis['liquidity_risk']}") + print(f"换月预警: {rollover_analysis['rollover_warning']['overall_warning']}") + + print_header("演示完成") + print("感谢使用 AI 期货分析系统!") + print("系统已完成以下功能:") + print("✅ 数据获取与存储") + print("✅ 趋势分析与胜率计算") + print("✅ 资金流向监控") + print("✅ 压力支撑分析") + print("✅ 风险控制与仓位管理") + print("✅ 换月预警") + print("✅ AI 智能研判") + print("✅ 交易建议生成") + + +if __name__ == "__main__": + main() diff --git a/qihuo_analyzer/__init__.py b/qihuo_analyzer/__init__.py new file mode 100644 index 0000000..72e6721 --- /dev/null +++ b/qihuo_analyzer/__init__.py @@ -0,0 +1,3 @@ +# 期货分析系统版本信息 +__version__ = "1.0.0" +__author__ = "AI Futures Analyzer" diff --git a/qihuo_analyzer/__pycache__/__init__.cpython-311.pyc b/qihuo_analyzer/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0106008370f89821ffc1d5906c2b934921f59799 GIT binary patch literal 216 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 Lckez5vAX{Tk-Egp literal 0 HcmV?d00001 diff --git a/qihuo_analyzer/core/models.py b/qihuo_analyzer/core/models.py new file mode 100644 index 0000000..eb0e2a3 --- /dev/null +++ b/qihuo_analyzer/core/models.py @@ -0,0 +1,104 @@ +# 核心数据模型 +import datetime +from typing import Dict, List, Optional, Tuple +import pandas as pd + + +class MarketData: + """市场数据模型""" + + def __init__(self, symbol: str, kline_data: pd.DataFrame): + self.symbol = symbol + self.kline_data = kline_data + self.timestamp = datetime.datetime.now() + + def get_latest_price(self) -> float: + """获取最新价格""" + return float(self.kline_data['close'].iloc[-1]) + + def get_price_range(self, period: int = 20) -> Tuple[float, float]: + """获取价格范围""" + prices = self.kline_data['close'].tail(period) + return float(prices.min()), float(prices.max()) + + +class AnalysisResult: + """分析结果模型""" + + def __init__(self, symbol: str): + self.symbol = symbol + self.timestamp = datetime.datetime.now() + self.trend: Optional[str] = None # bullish, bearish, neutral + self.probability: Optional[float] = None # 胜率 + self.direction: Optional[str] = None # long, short, wait + self.cycle: Optional[str] = None # short, medium, long + self.atr: Optional[float] = None # 真实波动幅度 + self.adx: Optional[float] = None # 平均趋向指标 + self.support: Optional[float] = None # 支撑位 + self.resistance: Optional[float] = None # 阻力位 + self.stop_loss: Optional[float] = None # 止损位 + self.target_price: Optional[float] = None # 目标价 + self.position_size: Optional[float] = None # 建议仓位 + self.risk_ratio: Optional[float] = None # 风险比率 + self.fund_flow: Optional[Dict[str, float]] = None # 资金流向 + self.signals: Dict[str, str] = {} # 各维度信号 + + def to_dict(self) -> Dict: + """转换为字典""" + return { + 'symbol': self.symbol, + 'timestamp': self.timestamp.isoformat(), + 'trend': self.trend, + 'probability': self.probability, + 'direction': self.direction, + 'cycle': self.cycle, + 'atr': self.atr, + 'adx': self.adx, + 'support': self.support, + 'resistance': self.resistance, + 'stop_loss': self.stop_loss, + 'target_price': self.target_price, + 'position_size': self.position_size, + 'risk_ratio': self.risk_ratio, + 'fund_flow': self.fund_flow, + 'signals': self.signals + } + + +class StrategyConfig: + """策略配置模型""" + + def __init__(self): + # 技术指标参数 + self.macd_fast = 12 + self.macd_slow = 26 + self.macd_signal = 9 + self.rsi_period = 14 + self.bollinger_period = 20 + self.bollinger_std = 2 + self.kdj_period = 9 + self.kdj_signal = 3 + self.adx_period = 14 + + # 趋势过滤参数 + self.short_ma = 20 + self.long_ma = 60 + + # 风险控制参数 + self.atr_multiplier = 2.0 + self.max_risk_percent = 0.02 + self.min_profit_loss_ratio = 1.5 + + # 资金监控参数 + self.volume_change_threshold = 0.05 + self.open_interest_change_threshold = 0.05 + + +class RiskParams: + """风险参数模型""" + + def __init__(self, account_balance: float): + self.account_balance = account_balance + self.max_risk_amount = account_balance * 0.02 + self.max_position_percent = 0.3 + self.max_leverage = 5 diff --git a/qihuo_analyzer/data/__pycache__/data_fetcher.cpython-311.pyc b/qihuo_analyzer/data/__pycache__/data_fetcher.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..494f83fefe319ff57cc5def8c03811097d8cea75 GIT binary patch literal 18233 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 OzL(o3lm1qQ;{OAgnkX{> literal 0 HcmV?d00001 diff --git a/qihuo_analyzer/data/__pycache__/data_storage.cpython-311.pyc b/qihuo_analyzer/data/__pycache__/data_storage.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f41af7886315e84b7797f3aa9e7d380aab6bf49a GIT binary patch literal 19462 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 E-)Yr3`v3p{ literal 0 HcmV?d00001 diff --git a/qihuo_analyzer/data/data_fetcher.py b/qihuo_analyzer/data/data_fetcher.py new file mode 100644 index 0000000..1b62b6f --- /dev/null +++ b/qihuo_analyzer/data/data_fetcher.py @@ -0,0 +1,461 @@ +# 数据获取模块 +import os +import time +import pandas as pd +from typing import Dict, Optional, List +from qihuo_analyzer.utils.config_manager import config_manager + +# 尝试导入tqsdk,如果失败则使用模拟数据 +try: + from tqsdk import TqApi, TqAuth + TQSDK_AVAILABLE = True +except Exception as e: + print(f"tqsdk导入失败:{e},将使用模拟数据") + TQSDK_AVAILABLE = False + + +class DataFetcher: + """数据获取器""" + + def __init__(self): + self.api = None + + def connect(self) -> bool: + """连接API""" + try: + if TQSDK_AVAILABLE: + # 使用天勤TQSDK连接 + from qihuo_analyzer.utils.config_manager import config_manager + username = os.getenv('TQSDK_USERNAME', '') + password = os.getenv('TQSDK_PASSWORD', '') + + if username and password: + self.api = TqApi(auth=TqAuth(username, password)) + print("API连接成功") + return True + else: + print("TQSDK账号密码未配置,将使用模拟数据") + self.api = None + return False + else: + # 模拟API,用于测试 + print("使用模拟API") + self.api = None + return False + except Exception as e: + print(f"API连接失败:{e}") + # 模拟API,用于测试 + self.api = None + return False + + def disconnect(self): + """断开连接""" + if self.api: + try: + self.api.close() + print("API连接已断开") + except: + pass + + def _convert_duration(self, duration: str) -> int: + """将时间周期字符串转换为分钟数 + + Args: + duration: 时间周期,如 '1m', '5m', '15m', '1h', '1d' + + Returns: + 分钟数 + """ + duration_map = { + '1m': 1, + '5m': 5, + '15m': 15, + '30m': 30, + '1h': 60, + '2h': 120, + '4h': 240, + '6h': 360, + '12h': 720, + '1d': 1440, + '1w': 10080 + } + return duration_map.get(duration, 60) # 默认60分钟 + + def _convert_symbol(self, symbol: str) -> str: + """将合约代码转换为TQSDK格式 + + Args: + symbol: 合约代码,如 'CU2603' + + Returns: + TQSDK格式的合约代码,如 'SHFE.cu2603' + """ + # 交易所映射 + exchange_map = { + 'CU': 'SHFE', # 铜 - 上海期货交易所 + 'AL': 'SHFE', # 铝 - 上海期货交易所 + 'ZN': 'SHFE', # 锌 - 上海期货交易所 + 'PB': 'SHFE', # 铅 - 上海期货交易所 + 'NI': 'SHFE', # 镍 - 上海期货交易所 + 'SN': 'SHFE', # 锡 - 上海期货交易所 + 'AU': 'SHFE', # 黄金 - 上海期货交易所 + 'AG': 'SHFE', # 白银 - 上海期货交易所 + 'RB': 'SHFE', # 螺纹钢 - 上海期货交易所 + 'HC': 'SHFE', # 热轧卷板 - 上海期货交易所 + 'BU': 'SHFE', # 沥青 - 上海期货交易所 + 'RU': 'SHFE', # 橡胶 - 上海期货交易所 + 'FU': 'SHFE', # 燃油 - 上海期货交易所 + 'SC': 'INE', # 原油 - 上海国际能源交易中心 + 'I': 'DCE', # 铁矿石 - 大连商品交易所 + 'J': 'DCE', # 焦炭 - 大连商品交易所 + 'JM': 'DCE', # 焦煤 - 大连商品交易所 + 'A': 'DCE', # 大豆 - 大连商品交易所 + 'B': 'DCE', # 豆粕 - 大连商品交易所 + 'M': 'DCE', # 豆粕 - 大连商品交易所 + 'Y': 'DCE', # 豆油 - 大连商品交易所 + 'P': 'DCE', # 棕榈油 - 大连商品交易所 + 'C': 'DCE', # 玉米 - 大连商品交易所 + 'CS': 'DCE', # 玉米淀粉 - 大连商品交易所 + 'L': 'DCE', # 聚乙烯 - 大连商品交易所 + 'V': 'DCE', # 聚氯乙烯 - 大连商品交易所 + 'PP': 'DCE', # 聚丙烯 - 大连商品交易所 + 'TA': 'CZCE', # PTA - 郑州商品交易所 + 'CF': 'CZCE', # 棉花 - 郑州商品交易所 + 'SR': 'CZCE', # 白糖 - 郑州商品交易所 + 'MA': 'CZCE', # 甲醇 - 郑州商品交易所 + 'ZC': 'CZCE', # 动力煤 - 郑州商品交易所 + 'FG': 'CZCE', # 玻璃 - 郑州商品交易所 + 'RM': 'CZCE', # 菜籽粕 - 郑州商品交易所 + 'OI': 'CZCE', # 菜籽油 - 郑州商品交易所 + 'RS': 'CZCE', # 菜籽 - 郑州商品交易所 + 'WH': 'CZCE', # 强麦 - 郑州商品交易所 + 'JR': 'CZCE', # 粳稻 - 郑州商品交易所 + 'LR': 'CZCE', # 晚籼稻 - 郑州商品交易所 + } + + # 提取品种代码和合约月份 + if len(symbol) >= 4: + product_code = symbol[:2].upper() + contract_month = symbol[2:].lower() + + # 获取交易所代码 + exchange = exchange_map.get(product_code, 'SHFE') + + # 构建TQSDK格式的合约代码 + tq_symbol = f"{exchange}.{product_code.lower()}{contract_month}" + return tq_symbol + else: + return symbol + + def get_product_name_cn(self, symbol: str) -> str: + """获取合约的中文名称 + + Args: + symbol: 合约代码,如 'CU2603' + + Returns: + 合约的中文名称,如 '铜' + """ + # 品种中文名称映射 + product_name_map = { + 'CU': '铜', + 'AL': '铝', + 'ZN': '锌', + 'PB': '铅', + 'NI': '镍', + 'SN': '锡', + 'AU': '黄金', + 'AG': '白银', + 'RB': '螺纹钢', + 'HC': '热轧卷板', + 'BU': '沥青', + 'RU': '橡胶', + 'FU': '燃油', + 'SC': '原油', + 'I': '铁矿石', + 'J': '焦炭', + 'JM': '焦煤', + 'A': '大豆', + 'B': '豆粕', + 'M': '豆粕', + 'Y': '豆油', + 'P': '棕榈油', + 'C': '玉米', + 'CS': '玉米淀粉', + 'L': '聚乙烯', + 'V': '聚氯乙烯', + 'PP': '聚丙烯', + 'TA': 'PTA', + 'CF': '棉花', + 'SR': '白糖', + 'MA': '甲醇', + 'ZC': '动力煤', + 'FG': '玻璃', + 'RM': '菜籽粕', + 'OI': '菜籽油', + 'RS': '菜籽', + 'WH': '强麦', + 'JR': '粳稻', + 'LR': '晚籼稻', + } + + if len(symbol) >= 2: + product_code = symbol[:2].upper() + return product_name_map.get(product_code, product_code) + else: + return symbol + + def get_kline_data(self, symbol: str, duration: str, count: int = 200) -> Optional[pd.DataFrame]: + """获取K线数据 + + Args: + symbol: 合约代码 + duration: 时间周期,如 '1m', '5m', '15m', '1h', '1d' + count: 数据数量 + + Returns: + K线数据DataFrame,如果无法获取真实数据则返回None + """ + try: + if TQSDK_AVAILABLE and self.api: + # 转换合约代码为TQSDK格式 + tq_symbol = self._convert_symbol(symbol) + print(f"使用TQSDK格式合约代码: {tq_symbol}") + + # 转换时间周期为分钟数 + duration_minutes = self._convert_duration(duration) + # 使用真实API获取数据 + klines = self.api.get_kline_serial(tq_symbol, duration_minutes, data_length=count) + + # 等待数据准备就绪 + import time + start_time = time.time() + timeout = 5 # 5秒超时 + + while True: + if hasattr(klines, 'datetime') and len(klines.datetime) > 0: + break + if time.time() - start_time > timeout: + print("获取K线数据超时") + return None + time.sleep(0.1) + + # 转换为DataFrame + data = { + 'datetime': klines.datetime, + 'open': klines.open, + 'high': klines.high, + 'low': klines.low, + 'close': klines.close, + 'volume': klines.volume, + 'open_interest': klines.open_oi + } + df = pd.DataFrame(data) + df['datetime'] = pd.to_datetime(df['datetime'], unit='ns') + df.set_index('datetime', inplace=True) + + print(f"成功获取K线数据,数据长度: {len(df)}") + return df + else: + # 不再自动返回模拟数据,返回None + print(f"无法获取真实数据:{'API未连接' if not self.api else 'TQSDK不可用'}") + return None + except Exception as e: + print(f"获取K线数据失败:{e}") + # 不再自动返回模拟数据,返回None + return None + + def get_tick_data(self, symbol: str, count: int = 1000) -> Optional[pd.DataFrame]: + """获取Tick数据""" + try: + if TQSDK_AVAILABLE and self.api: + # 使用真实API获取数据 + ticks = self.api.get_tick_serial(symbol, data_length=count) + self.api.wait_update() + + # 转换为DataFrame + data = { + 'datetime': ticks.datetime, + 'last_price': ticks.last_price, + 'volume': ticks.volume, + 'open_interest': ticks.open_interest, + 'bid_price1': ticks.bid_price1, + 'bid_volume1': ticks.bid_volume1, + 'ask_price1': ticks.ask_price1, + 'ask_volume1': ticks.ask_volume1 + } + df = pd.DataFrame(data) + df['datetime'] = pd.to_datetime(df['datetime'], unit='ns') + df.set_index('datetime', inplace=True) + + return df + else: + # 返回模拟数据 + return self._get_mock_tick_data(symbol, count) + except Exception as e: + print(f"获取Tick数据失败:{e}") + return self._get_mock_tick_data(symbol, count) + + def get_contract_info(self, symbol: str) -> Optional[Dict]: + """获取合约信息""" + try: + if TQSDK_AVAILABLE and self.api: + # 使用真实API获取数据 + quote = self.api.get_quote(symbol) + self.api.wait_update() + + return { + 'symbol': symbol, + 'name': quote.instrument_name, + 'exchange': quote.exchange_id, + 'product': quote.product_id, + 'price_tick': quote.price_tick, + 'volume_multiple': quote.volume_multiple, + 'margin_rate': quote.margin_rate, + 'expire_datetime': quote.expire_datetime, + 'create_datetime': quote.create_datetime + } + else: + # 返回模拟数据 + return self._get_mock_contract_info(symbol) + except Exception as e: + print(f"获取合约信息失败:{e}") + return self._get_mock_contract_info(symbol) + + def get_market_data(self, symbols: List[str]) -> Dict[str, Dict]: + """批量获取市场数据""" + market_data = {} + + for symbol in symbols: + try: + if TQSDK_AVAILABLE and self.api: + quote = self.api.get_quote(symbol) + self.api.wait_update() + + market_data[symbol] = { + 'latest_price': quote.last_price, + 'open': quote.open, + 'high': quote.high, + 'low': quote.low, + 'pre_close': quote.pre_close, + 'volume': quote.volume, + 'open_interest': quote.open_interest, + 'bid_price1': quote.bid_price1, + 'ask_price1': quote.ask_price1 + } + else: + # 模拟数据 + market_data[symbol] = self._get_mock_market_data(symbol) + except Exception as e: + print(f"获取{symbol}市场数据失败:{e}") + market_data[symbol] = self._get_mock_market_data(symbol) + + return market_data + + def _get_mock_kline_data(self, symbol: str, duration: str, count: int) -> pd.DataFrame: + """获取模拟K线数据""" + # 生成时间序列 + end_time = pd.Timestamp.now() + if duration == '1m': + freq = '1T' + elif duration == '5m': + freq = '5T' + elif duration == '15m': + freq = '15T' + elif duration == '1h': + freq = '1H' + elif duration == '1d': + freq = '1D' + else: + freq = '1H' + + datetime_index = pd.date_range(end=end_time, periods=count, freq=freq) + + # 生成随机价格数据 + base_price = 3500 + price_changes = np.random.normal(0, 5, count) + prices = base_price + np.cumsum(price_changes) + + # 生成其他数据 + opens = prices * (1 + np.random.normal(0, 0.001, count)) + highs = np.maximum(prices, opens) * (1 + np.random.normal(0, 0.002, count)) + lows = np.minimum(prices, opens) * (1 - np.random.normal(0, 0.002, count)) + volumes = np.random.randint(1000, 10000, count) + open_interests = np.random.randint(10000, 100000, count) + + # 创建DataFrame + df = pd.DataFrame({ + 'open': opens, + 'high': highs, + 'low': lows, + 'close': prices, + 'volume': volumes, + 'open_interest': open_interests + }, index=datetime_index) + + return df + + def _get_mock_tick_data(self, symbol: str, count: int) -> pd.DataFrame: + """获取模拟Tick数据""" + # 生成时间序列 + end_time = pd.Timestamp.now() + datetime_index = pd.date_range(end=end_time, periods=count, freq='1S') + + # 生成随机价格数据 + base_price = 3500 + price_changes = np.random.normal(0, 0.5, count) + last_prices = base_price + np.cumsum(price_changes) + + # 生成其他数据 + volumes = np.random.randint(10, 100, count) + open_interests = np.random.randint(10000, 100000, count) + bid_prices = last_prices * (1 - np.random.normal(0, 0.0005, count)) + ask_prices = last_prices * (1 + np.random.normal(0, 0.0005, count)) + bid_volumes = np.random.randint(10, 50, count) + ask_volumes = np.random.randint(10, 50, count) + + # 创建DataFrame + df = pd.DataFrame({ + 'last_price': last_prices, + 'volume': volumes, + 'open_interest': open_interests, + 'bid_price1': bid_prices, + 'bid_volume1': bid_volumes, + 'ask_price1': ask_prices, + 'ask_volume1': ask_volumes + }, index=datetime_index) + + return df + + def _get_mock_contract_info(self, symbol: str) -> Dict: + """获取模拟合约信息""" + return { + 'symbol': symbol, + 'name': symbol, + 'exchange': 'SHFE', + 'product': symbol[:2], + 'price_tick': 1, + 'volume_multiple': 10, + 'margin_rate': 0.1, + 'expire_datetime': int(time.time() * 1e9) + 90 * 24 * 3600 * 1e9, + 'create_datetime': int(time.time() * 1e9) - 180 * 24 * 3600 * 1e9 + } + + def _get_mock_market_data(self, symbol: str) -> Dict: + """获取模拟市场数据""" + base_price = 3500 + return { + 'latest_price': base_price + np.random.normal(0, 10), + 'open': base_price, + 'high': base_price + 20, + 'low': base_price - 20, + 'pre_close': base_price, + 'volume': np.random.randint(10000, 100000), + 'open_interest': np.random.randint(100000, 1000000), + 'bid_price1': base_price - 1, + 'ask_price1': base_price + 1 + } + + +# 导入numpy +import numpy as np diff --git a/qihuo_analyzer/data/data_storage.py b/qihuo_analyzer/data/data_storage.py new file mode 100644 index 0000000..0655878 --- /dev/null +++ b/qihuo_analyzer/data/data_storage.py @@ -0,0 +1,378 @@ +# 数据存储模块 +import sqlite3 +import json +import os +from datetime import datetime +from typing import Dict, Optional, List +import pandas as pd +from qihuo_analyzer.utils.config_manager import config_manager + + +class DataStorage: + """数据存储管理器""" + + def __init__(self): + self.db_path = config_manager.db_path + self._init_database() + + def _init_database(self): + """初始化数据库""" + # 确保数据库目录存在 + db_dir = os.path.dirname(self.db_path) + if db_dir and not os.path.exists(db_dir): + os.makedirs(db_dir) + + # 连接数据库 + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 创建表 + # 分析结果表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS analysis_results ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + symbol TEXT NOT NULL, + timestamp TEXT NOT NULL, + trend TEXT, + probability REAL, + direction TEXT, + cycle TEXT, + atr REAL, + adx REAL, + support REAL, + resistance REAL, + stop_loss REAL, + target_price REAL, + position_size REAL, + risk_ratio REAL, + fund_flow TEXT, + signals TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 历史K线数据表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS kline_data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + symbol TEXT NOT NULL, + duration TEXT NOT NULL, + datetime TEXT NOT NULL, + open REAL, + high REAL, + low REAL, + close REAL, + volume INTEGER, + open_interest INTEGER, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + UNIQUE(symbol, duration, datetime) + ) + ''') + + # 交易建议表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS trade_recommendations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + symbol TEXT NOT NULL, + timestamp TEXT NOT NULL, + direction TEXT, + entry_price REAL, + stop_loss REAL, + target_price REAL, + position_size REAL, + execution_plan TEXT, + risk_tips TEXT, + status TEXT DEFAULT 'pending', + created_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 风险监控表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS risk_monitoring ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + symbol TEXT NOT NULL, + timestamp TEXT NOT NULL, + current_price REAL, + entry_price REAL, + stop_loss REAL, + target_price REAL, + current_profit REAL, + risk_status TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + ''') + + conn.commit() + conn.close() + + def save_analysis_result(self, result: Dict) -> bool: + """保存分析结果""" + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 准备数据 + data = { + 'symbol': result.get('symbol', ''), + 'timestamp': result.get('timestamp', datetime.now().isoformat()), + 'trend': result.get('trend'), + 'probability': result.get('probability'), + 'direction': result.get('direction'), + 'cycle': result.get('cycle'), + 'atr': result.get('atr'), + 'adx': result.get('adx'), + 'support': result.get('support'), + 'resistance': result.get('resistance'), + 'stop_loss': result.get('stop_loss'), + 'target_price': result.get('target_price'), + 'position_size': result.get('position_size'), + 'risk_ratio': result.get('risk_ratio'), + 'fund_flow': json.dumps(result.get('fund_flow', {})) if result.get('fund_flow') else None, + 'signals': json.dumps(result.get('signals', {})) if result.get('signals') else None + } + + # 插入数据 + cursor.execute(''' + INSERT INTO analysis_results ( + symbol, timestamp, trend, probability, direction, cycle, + atr, adx, support, resistance, stop_loss, target_price, + position_size, risk_ratio, fund_flow, signals + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + data['symbol'], data['timestamp'], data['trend'], data['probability'], + data['direction'], data['cycle'], data['atr'], data['adx'], + data['support'], data['resistance'], data['stop_loss'], data['target_price'], + data['position_size'], data['risk_ratio'], data['fund_flow'], data['signals'] + )) + + conn.commit() + conn.close() + return True + except Exception as e: + print(f"保存分析结果失败:{e}") + return False + + def save_kline_data(self, symbol: str, duration: str, df: pd.DataFrame) -> bool: + """保存K线数据""" + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 批量插入数据 + data_to_insert = [] + for idx, row in df.iterrows(): + data_to_insert.append(( + symbol, duration, idx.isoformat(), + row['open'], row['high'], row['low'], row['close'], + row['volume'], row['open_interest'] + )) + + # 使用事务批量插入 + if data_to_insert: + cursor.executemany(''' + INSERT OR IGNORE INTO kline_data ( + symbol, duration, datetime, open, high, low, close, volume, open_interest + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', data_to_insert) + + conn.commit() + conn.close() + return True + except Exception as e: + print(f"保存K线数据失败:{e}") + return False + + def save_trade_recommendation(self, recommendation: Dict) -> bool: + """保存交易建议""" + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 准备数据 + data = { + 'symbol': recommendation.get('symbol', ''), + 'timestamp': recommendation.get('timestamp', datetime.now().isoformat()), + 'direction': recommendation.get('direction'), + 'entry_price': recommendation.get('entry_price'), + 'stop_loss': recommendation.get('stop_loss'), + 'target_price': recommendation.get('target_price'), + 'position_size': recommendation.get('position_size'), + 'execution_plan': recommendation.get('execution_plan'), + 'risk_tips': recommendation.get('risk_tips') + } + + # 插入数据 + cursor.execute(''' + INSERT INTO trade_recommendations ( + symbol, timestamp, direction, entry_price, stop_loss, + target_price, position_size, execution_plan, risk_tips + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + data['symbol'], data['timestamp'], data['direction'], data['entry_price'], + data['stop_loss'], data['target_price'], data['position_size'], + data['execution_plan'], data['risk_tips'] + )) + + conn.commit() + conn.close() + return True + except Exception as e: + print(f"保存交易建议失败:{e}") + return False + + def save_risk_monitoring(self, monitoring_data: Dict) -> bool: + """保存风险监控数据""" + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 准备数据 + data = { + 'symbol': monitoring_data.get('symbol', ''), + 'timestamp': monitoring_data.get('timestamp', datetime.now().isoformat()), + 'current_price': monitoring_data.get('current_price'), + 'entry_price': monitoring_data.get('entry_price'), + 'stop_loss': monitoring_data.get('stop_loss'), + 'target_price': monitoring_data.get('target_price'), + 'current_profit': monitoring_data.get('current_profit'), + 'risk_status': monitoring_data.get('risk_status') + } + + # 插入数据 + cursor.execute(''' + INSERT INTO risk_monitoring ( + symbol, timestamp, current_price, entry_price, stop_loss, + target_price, current_profit, risk_status + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + data['symbol'], data['timestamp'], data['current_price'], data['entry_price'], + data['stop_loss'], data['target_price'], data['current_profit'], data['risk_status'] + )) + + conn.commit() + conn.close() + return True + except Exception as e: + print(f"保存风险监控数据失败:{e}") + return False + + def get_analysis_results(self, symbol: str, limit: int = 100) -> pd.DataFrame: + """获取分析结果""" + try: + conn = sqlite3.connect(self.db_path) + query = f""" + SELECT * FROM analysis_results + WHERE symbol = ? + ORDER BY timestamp DESC + LIMIT ? + """ + df = pd.read_sql_query(query, conn, params=(symbol, limit)) + conn.close() + + # 解析JSON字段 + if not df.empty: + df['fund_flow'] = df['fund_flow'].apply(lambda x: json.loads(x) if x else {}) + df['signals'] = df['signals'].apply(lambda x: json.loads(x) if x else {}) + + return df + except Exception as e: + print(f"获取分析结果失败:{e}") + return pd.DataFrame() + + def get_kline_data(self, symbol: str, duration: str, limit: int = 200) -> pd.DataFrame: + """获取K线数据""" + try: + conn = sqlite3.connect(self.db_path) + query = f""" + SELECT * FROM kline_data + WHERE symbol = ? AND duration = ? + ORDER BY datetime DESC + LIMIT ? + """ + df = pd.read_sql_query(query, conn, params=(symbol, duration, limit)) + conn.close() + + if not df.empty: + # 转换时间格式并设置索引 + df['datetime'] = pd.to_datetime(df['datetime']) + df = df.sort_values('datetime') + df.set_index('datetime', inplace=True) + # 选择需要的列 + df = df[['open', 'high', 'low', 'close', 'volume', 'open_interest']] + + return df + except Exception as e: + print(f"获取K线数据失败:{e}") + return pd.DataFrame() + + def get_trade_recommendations(self, symbol: str, status: Optional[str] = None) -> pd.DataFrame: + """获取交易建议""" + try: + conn = sqlite3.connect(self.db_path) + if status: + query = f""" + SELECT * FROM trade_recommendations + WHERE symbol = ? AND status = ? + ORDER BY timestamp DESC + """ + df = pd.read_sql_query(query, conn, params=(symbol, status)) + else: + query = f""" + SELECT * FROM trade_recommendations + WHERE symbol = ? + ORDER BY timestamp DESC + """ + df = pd.read_sql_query(query, conn, params=(symbol,)) + conn.close() + return df + except Exception as e: + print(f"获取交易建议失败:{e}") + return pd.DataFrame() + + def update_recommendation_status(self, recommendation_id: int, status: str) -> bool: + """更新交易建议状态""" + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(''' + UPDATE trade_recommendations + SET status = ? + WHERE id = ? + ''', (status, recommendation_id)) + + conn.commit() + conn.close() + return True + except Exception as e: + print(f"更新交易建议状态失败:{e}") + return False + + def delete_old_data(self, days: int = 30) -> bool: + """删除旧数据""" + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 计算删除时间点 + delete_time = (datetime.now() - pd.Timedelta(days=days)).isoformat() + + # 删除旧的分析结果 + cursor.execute('DELETE FROM analysis_results WHERE created_at < ?', (delete_time,)) + + # 删除旧的K线数据 + cursor.execute('DELETE FROM kline_data WHERE created_at < ?', (delete_time,)) + + # 删除旧的交易建议 + cursor.execute('DELETE FROM trade_recommendations WHERE created_at < ?', (delete_time,)) + + # 删除旧的风险监控数据 + cursor.execute('DELETE FROM risk_monitoring WHERE created_at < ?', (delete_time,)) + + conn.commit() + conn.close() + return True + except Exception as e: + print(f"删除旧数据失败:{e}") + return False diff --git a/qihuo_analyzer/modules/__pycache__/deepseek_agent.cpython-311.pyc b/qihuo_analyzer/modules/__pycache__/deepseek_agent.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b5d63b43c22b9abdef85727a1c3a4070c69da1a5 GIT binary patch literal 14203 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 literal 0 HcmV?d00001 diff --git a/qihuo_analyzer/modules/__pycache__/fund_flow_monitor.cpython-311.pyc b/qihuo_analyzer/modules/__pycache__/fund_flow_monitor.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..61b1107ebeb747fc27a4adb7313d7a390c63b061 GIT binary patch literal 13827 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 Nrgh(Z4+@#U{{S*O3x@yz literal 0 HcmV?d00001 diff --git a/qihuo_analyzer/modules/__pycache__/rollover_detector.cpython-311.pyc b/qihuo_analyzer/modules/__pycache__/rollover_detector.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef7fb8520946ee69bae915f0543041504955c4a3 GIT binary patch literal 18115 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 Dict: + """分析市场""" + # 构建提示词 + prompt = self._build_analysis_prompt(market_data, technical_indicators, trend_analysis, risk_metrics) + + # 调用API + response = self._call_deepseek_api(prompt) + + # 解析结果 + analysis_result = self._parse_analysis_result(response) + + return analysis_result + + def generate_trade_recommendation(self, analysis_result: Dict) -> Dict: + """生成交易建议""" + # 构建提示词 + prompt = self._build_recommendation_prompt(analysis_result) + + # 调用API + response = self._call_deepseek_api(prompt) + + # 解析结果 + recommendation = self._parse_recommendation_result(response) + + return recommendation + + def _build_analysis_prompt(self, market_data: Dict, technical_indicators: Dict, + trend_analysis: Dict, risk_metrics: Dict) -> str: + """构建分析提示词""" + prompt = f"""# 期货市场分析任务 + +你是一位专业的期货市场分析师,需要基于以下多维度数据对市场进行综合研判。 + +## 1. 市场基本数据 +- 品种:{market_data.get('symbol', '未知')} +- 最新价格:{market_data.get('latest_price', '未知')} +- 成交量:{market_data.get('volume', '未知')} +- 持仓量:{market_data.get('open_interest', '未知')} +- 时间周期:{market_data.get('timeframe', '未知')} + +## 2. 技术指标数据 +- MACD:{json.dumps(technical_indicators.get('macd', {}), ensure_ascii=False)} +- RSI:{technical_indicators.get('rsi', '未知')} +- 布林带:{json.dumps(technical_indicators.get('bollinger', {}), ensure_ascii=False)} +- KDJ:{json.dumps(technical_indicators.get('kdj', {}), ensure_ascii=False)} +- ATR:{technical_indicators.get('atr', '未知')} + +## 3. 趋势分析数据 +- ADX:{trend_analysis.get('adx', '未知')} +- 趋势强度:{trend_analysis.get('trend_strength', '未知')} +- 趋势方向:{trend_analysis.get('trend_direction', '未知')} +- 双均线关系:{trend_analysis.get('ma_relationship', '未知')} +- 多周期共振:{json.dumps(trend_analysis.get('multi_period_analysis', {}), ensure_ascii=False)} +- 综合趋势:{trend_analysis.get('overall_trend', '未知')} +- 胜率:{trend_analysis.get('win_rate', '未知')}% + +## 4. 风险指标数据 +- 止损位:{risk_metrics.get('stop_loss', '未知')} +- 目标价:{risk_metrics.get('target_price', '未知')} +- 盈亏比:{risk_metrics.get('profit_loss_ratio', '未知')} +- 建议仓位:{risk_metrics.get('position_size', '未知')} +- 风险比例:{risk_metrics.get('risk_ratio', '未知')}% + +## 分析要求 +1. **趋势判断**:基于多维度数据,判断当前市场的主要趋势 +2. **胜率评估**:评估当前交易机会的胜率 +3. **风险预警**:识别潜在的风险因素 +4. **交易建议**:给出具体的交易方向、仓位、止损止盈建议 +5. **逻辑解释**:详细说明分析逻辑和依据 + +请以JSON格式输出分析结果,包含以下字段: +- trend_judgment:趋势判断 +- win_rate_assessment:胜率评估 +- risk_warning:风险预警 +- trade_recommendation:交易建议 +- analysis_logic:分析逻辑 +""" + return prompt + + def _build_recommendation_prompt(self, analysis_result: Dict) -> str: + """构建建议提示词""" + prompt = f"""# 期货交易建议生成任务 + +基于以下市场分析结果,生成详细的交易建议。 + +## 分析结果 +{json.dumps(analysis_result, ensure_ascii=False, indent=2)} + +## 建议要求 +1. **明确的交易方向**:做多/做空/观望 +2. **具体的入场点位**:基于技术分析的合理入场点 +3. **严格的止损设置**:基于ATR的动态止损 +4. **合理的止盈目标**:基于压力支撑位的目标价 +5. **科学的仓位管理**:基于账户资金的风险控制 +6. **详细的执行计划**:包括入场时机、加仓策略、出场条件 +7. **风险提示**:潜在的风险因素和应对措施 + +请以JSON格式输出交易建议,包含以下字段: +- direction:交易方向 +- entry_price:入场价格 +- stop_loss:止损价格 +- target_price:目标价格 +- position_size:仓位大小 +- execution_plan:执行计划 +- risk_tips:风险提示 +""" + return prompt + + def _call_deepseek_api(self, prompt: str) -> str: + """调用DeepSeek API""" + # 如果没有API密钥,返回模拟结果 + if not self.api_key: + return self._get_mock_response(prompt) + + payload = { + 'model': 'deepseek-chat', + 'messages': [ + { + 'role': 'system', + 'content': '你是一位专业的期货市场分析师,精通技术分析和基本面分析,能够基于多维度数据提供准确的市场研判和交易建议。' + }, + { + 'role': 'user', + 'content': prompt + } + ], + 'temperature': 0.3, + 'max_tokens': 2000, + 'top_p': 0.9 + } + + try: + response = requests.post( + self.api_url, + headers=self.headers, + json=payload, + timeout=30 + ) + response.raise_for_status() + + result = response.json() + return result['choices'][0]['message']['content'] + except Exception as e: + print(f"API调用失败:{e}") + return self._get_mock_response(prompt) + + def _get_mock_response(self, prompt: str) -> str: + """获取模拟响应""" + # 模拟分析结果 + if '市场分析任务' in prompt: + return json.dumps({ + 'trend_judgment': '震荡偏多', + 'win_rate_assessment': '65%', + 'risk_warning': '短期波动较大,注意止损', + 'trade_recommendation': '轻仓做多', + 'analysis_logic': '基于MACD金叉、RSI中性偏多、双均线金叉等信号,综合判断震荡偏多趋势' + }, ensure_ascii=False) + + # 模拟建议结果 + elif '交易建议生成任务' in prompt: + return json.dumps({ + 'direction': 'long', + 'entry_price': 3500, + 'stop_loss': 3450, + 'target_price': 3600, + 'position_size': 2, + 'execution_plan': '回调至3480附近入场,止损设置在3450,目标位3600,突破3600后可加仓', + 'risk_tips': '若跌破3450,立即止损;若成交量萎缩,考虑提前出场' + }, ensure_ascii=False) + + else: + return json.dumps({ + 'error': '未知任务类型' + }, ensure_ascii=False) + + def _parse_analysis_result(self, response: str) -> Dict: + """解析分析结果""" + try: + # 尝试直接解析JSON + return json.loads(response) + except json.JSONDecodeError: + # 如果不是纯JSON,提取JSON部分 + import re + json_match = re.search(r'\{[\s\S]*\}', response) + if json_match: + try: + return json.loads(json_match.group()) + except json.JSONDecodeError: + return {'error': '解析失败'} + else: + return {'error': '无有效JSON'} + + def _parse_recommendation_result(self, response: str) -> Dict: + """解析建议结果""" + try: + # 尝试直接解析JSON + return json.loads(response) + except json.JSONDecodeError: + # 如果不是纯JSON,提取JSON部分 + import re + json_match = re.search(r'\{[\s\S]*\}', response) + if json_match: + try: + return json.loads(json_match.group()) + except json.JSONDecodeError: + return {'error': '解析失败'} + else: + return {'error': '无有效JSON'} + + def fuse_multidimensional_data(self, data_sources: List[Dict]) -> Dict: + """融合多维度数据""" + # 构建融合提示词 + prompt = f"""# 多维度数据融合任务 + +请将以下多个数据源的数据进行融合分析,提取关键信息,形成综合的市场判断。 + +## 数据源 +{json.dumps(data_sources, ensure_ascii=False, indent=2)} + +## 融合要求 +1. **数据一致性检查**:检查各数据源之间的一致性 +2. **关键信息提取**:提取各数据源的关键信息 +3. **综合判断形成**:基于融合数据形成综合市场判断 +4. **不确定性评估**:评估数据的不确定性和风险 + +请以JSON格式输出融合结果,包含以下字段: +- fused_data:融合后的数据 +- key_insights:关键洞察 +- comprehensive_judgment:综合判断 +- uncertainty_assessment:不确定性评估 +""" + + # 调用API + response = self._call_deepseek_api(prompt) + + # 解析结果 + fused_result = self._parse_analysis_result(response) + + return fused_result + + def generate_market_insights(self, historical_data: List[Dict], current_data: Dict) -> Dict: + """生成市场洞察""" + # 构建提示词 + prompt = f"""# 市场洞察生成任务 + +基于以下历史数据和当前数据,生成深度的市场洞察。 + +## 历史数据 +{json.dumps(historical_data, ensure_ascii=False, indent=2)} + +## 当前数据 +{json.dumps(current_data, ensure_ascii=False, indent=2)} + +## 洞察要求 +1. **趋势变化分析**:分析市场趋势的变化 +2. **关键转折点识别**:识别重要的市场转折点 +3. **异常情况检测**:检测异常的市场行为 +4. **未来走势预测**:基于历史和当前数据预测未来走势 +5. **投资机会挖掘**:挖掘潜在的投资机会 + +请以JSON格式输出市场洞察,包含以下字段: +- trend_analysis:趋势分析 +- turning_points:转折点分析 +- anomalies:异常检测 +- future_prediction:未来预测 +- investment_opportunities:投资机会 +""" + + # 调用API + response = self._call_deepseek_api(prompt) + + # 解析结果 + insights = self._parse_analysis_result(response) + + return insights diff --git a/qihuo_analyzer/modules/fund_flow_monitor.py b/qihuo_analyzer/modules/fund_flow_monitor.py new file mode 100644 index 0000000..17acd6e --- /dev/null +++ b/qihuo_analyzer/modules/fund_flow_monitor.py @@ -0,0 +1,284 @@ +# 资金监控模块 +import pandas as pd +import numpy as np +from typing import Dict, Optional, List +from qihuo_analyzer.core.models import StrategyConfig + + +class FundFlowMonitor: + """资金流向监控器""" + + def __init__(self, config: Optional[StrategyConfig] = None): + self.config = config or StrategyConfig() + + def analyze_fund_flow(self, data: pd.DataFrame) -> Dict: + """分析资金流向""" + result = {} + + # 分析持仓量变化 + oi_analysis = self._analyze_open_interest(data) + result.update(oi_analysis) + + # 分析量价关系 + volume_price_analysis = self._analyze_volume_price_relationship(data) + result.update(volume_price_analysis) + + # 分析资金流向强度 + fund_flow_strength = self._calculate_fund_flow_strength(data) + result['fund_flow_strength'] = fund_flow_strength + + # 分析资金集中度 + fund_concentration = self._analyze_fund_concentration(data) + result.update(fund_concentration) + + # 综合资金面信号 + fund_signal = self._generate_fund_signal(result) + result['fund_signal'] = fund_signal + + return result + + def _analyze_open_interest(self, data: pd.DataFrame) -> Dict: + """分析持仓量变化""" + # 计算持仓量变化 + data['oi_change'] = data['open_interest'].diff() + data['oi_change_pct'] = data['oi_change'] / data['open_interest'].shift(1) * 100 + + # 最近N天持仓量变化 + recent_oi_change = data['oi_change'].tail(5).sum() + recent_oi_change_pct = data['oi_change_pct'].tail(5).mean() + + # 持仓量趋势 + oi_trend = self._judge_oi_trend(data['open_interest']) + + # 持仓量与价格关系 + oi_price_relationship = self._judge_oi_price_relationship(data) + + return { + 'recent_oi_change': recent_oi_change, + 'recent_oi_change_pct': recent_oi_change_pct, + 'oi_trend': oi_trend, + 'oi_price_relationship': oi_price_relationship + } + + def _analyze_volume_price_relationship(self, data: pd.DataFrame) -> Dict: + """分析量价关系""" + # 计算价格变化 + data['price_change'] = data['close'].diff() + data['price_change_pct'] = data['price_change'] / data['close'].shift(1) * 100 + + # 计算成交量变化 + data['volume_change'] = data['volume'].diff() + data['volume_change_pct'] = data['volume_change'] / data['volume'].shift(1) * 100 + + # 量价配合度 + volume_price_fit = self._calculate_volume_price_fit(data) + + # 量价背离检测 + divergence = self._detect_volume_price_divergence(data) + + # 成交量趋势 + volume_trend = self._judge_volume_trend(data['volume']) + + return { + 'volume_price_fit': volume_price_fit, + 'divergence': divergence, + 'volume_trend': volume_trend + } + + def _calculate_fund_flow_strength(self, data: pd.DataFrame) -> float: + """计算资金流向强度""" + # 计算资金流向 + # 简化计算:(收盘价 - 开盘价) * 成交量 + fund_flow = ((data['close'] - data['open']) * data['volume']).tail(20).sum() + + # 归一化到-100到100 + if fund_flow > 0: + strength = min(100, (fund_flow / data['volume'].tail(20).sum()) * 1000) + else: + strength = max(-100, (fund_flow / data['volume'].tail(20).sum()) * 1000) + + return strength + + def _analyze_fund_concentration(self, data: pd.DataFrame) -> Dict: + """分析资金集中度""" + # 计算成交量集中度(前5天成交量占比) + recent_volume = data['volume'].tail(5).sum() + total_volume = data['volume'].tail(30).sum() + volume_concentration = recent_volume / total_volume if total_volume > 0 else 0 + + # 计算持仓量集中度(最近持仓量变化占比) + recent_oi_change = abs(data['oi_change'].tail(5).sum()) + total_oi = data['open_interest'].iloc[-1] + oi_concentration = recent_oi_change / total_oi if total_oi > 0 else 0 + + return { + 'volume_concentration': volume_concentration, + 'oi_concentration': oi_concentration + } + + def _judge_oi_trend(self, oi_series: pd.Series) -> str: + """判断持仓量趋势""" + # 使用简单移动平均线判断趋势 + ma5 = oi_series.rolling(window=5).mean().iloc[-1] + ma20 = oi_series.rolling(window=20).mean().iloc[-1] + + if ma5 > ma20 * 1.02: + return 'strong_increasing' + elif ma5 > ma20: + return 'increasing' + elif ma5 < ma20 * 0.98: + return 'strong_decreasing' + elif ma5 < ma20: + return 'decreasing' + else: + return 'stable' + + def _judge_oi_price_relationship(self, data: pd.DataFrame) -> Dict: + """判断持仓量与价格关系""" + recent_data = data.tail(10) + + # 计算价格变化 + if 'price_change' not in recent_data.columns: + recent_data['price_change'] = recent_data['close'].diff() + + # 计算持仓量变化 + if 'oi_change' not in recent_data.columns: + recent_data['oi_change'] = recent_data['open_interest'].diff() + + # 计算平均价格变化和平均持仓量变化 + avg_price_change = recent_data['price_change'].mean() + avg_oi_change = recent_data['oi_change'].mean() + + if avg_price_change > 0 and avg_oi_change > 0: + return 'price_up_oi_up' # 价涨量增 + elif avg_price_change > 0 and avg_oi_change < 0: + return 'price_up_oi_down' # 价涨量减 + elif avg_price_change < 0 and avg_oi_change > 0: + return 'price_down_oi_up' # 价跌量增 + elif avg_price_change < 0 and avg_oi_change < 0: + return 'price_down_oi_down' # 价跌量减 + else: + return 'stable' + + def _calculate_volume_price_fit(self, data: pd.DataFrame) -> float: + """计算量价配合度""" + recent_data = data.tail(20) + + # 计算量价配合的次数 + fit_count = 0 + total_count = len(recent_data) - 1 + + for i in range(1, len(recent_data)): + price_change = recent_data['price_change'].iloc[i] + volume_change = recent_data['volume_change'].iloc[i] + + # 量价配合:价格上涨成交量增加,价格下跌成交量减少 + if (price_change > 0 and volume_change > 0) or (price_change < 0 and volume_change < 0): + fit_count += 1 + + fit_ratio = fit_count / total_count if total_count > 0 else 0 + return fit_ratio * 100 + + def _detect_volume_price_divergence(self, data: pd.DataFrame) -> str: + """检测量价背离""" + recent_data = data.tail(10) + + # 计算价格趋势(斜率) + price_slope = np.polyfit(range(len(recent_data)), recent_data['close'], 1)[0] + + # 计算成交量趋势(斜率) + volume_slope = np.polyfit(range(len(recent_data)), recent_data['volume'], 1)[0] + + # 判断背离 + if price_slope > 0 and volume_slope < 0: + return 'bearish_divergence' # 价格上涨,成交量下降,看跌背离 + elif price_slope < 0 and volume_slope > 0: + return 'bullish_divergence' # 价格下降,成交量上升,看涨背离 + else: + return 'no_divergence' + + def _judge_volume_trend(self, volume_series: pd.Series) -> str: + """判断成交量趋势""" + ma5 = volume_series.rolling(window=5).mean().iloc[-1] + ma20 = volume_series.rolling(window=20).mean().iloc[-1] + + if ma5 > ma20 * 1.1: + return 'strong_increasing' + elif ma5 > ma20: + return 'increasing' + elif ma5 < ma20 * 0.9: + return 'strong_decreasing' + elif ma5 < ma20: + return 'decreasing' + else: + return 'stable' + + def _generate_fund_signal(self, fund_analysis: Dict) -> str: + """生成资金面信号""" + signals = [] + + # 持仓量信号 + if fund_analysis.get('oi_trend') in ['strong_increasing', 'increasing']: + if fund_analysis.get('oi_price_relationship') == 'price_up_oi_up': + signals.append('bullish') + elif fund_analysis.get('oi_price_relationship') == 'price_down_oi_up': + signals.append('bearish') + + # 量价关系信号 + if fund_analysis.get('volume_price_fit') > 60: + if fund_analysis.get('volume_trend') in ['strong_increasing', 'increasing']: + signals.append('bullish') + + # 量价背离信号 + if fund_analysis.get('divergence') == 'bullish_divergence': + signals.append('bullish') + elif fund_analysis.get('divergence') == 'bearish_divergence': + signals.append('bearish') + + # 资金流向强度信号 + fund_flow_strength = fund_analysis.get('fund_flow_strength', 0) + if fund_flow_strength > 30: + signals.append('bullish') + elif fund_flow_strength < -30: + signals.append('bearish') + + # 综合信号 + if signals.count('bullish') > signals.count('bearish'): + return 'bullish' + elif signals.count('bearish') > signals.count('bullish'): + return 'bearish' + else: + return 'neutral' + + def detect_volume_spikes(self, data: pd.DataFrame, threshold: float = 2.0) -> List[int]: + """检测成交量异动""" + # 计算成交量移动平均线和标准差 + data['volume_ma'] = data['volume'].rolling(window=20).mean() + data['volume_std'] = data['volume'].rolling(window=20).std() + + # 计算成交量偏离度 + data['volume_zscore'] = (data['volume'] - data['volume_ma']) / data['volume_std'] + + # 找出成交量异动的位置 + spikes = data[data['volume_zscore'] > threshold].index + + return list(spikes) + + def analyze_institutional_activity(self, data: pd.DataFrame) -> Dict: + """分析机构活动""" + # 基于持仓量和成交量的变化分析机构活动 + # 机构通常会引起较大的持仓量变化 + + # 计算大资金活动指标 + data['institutional_activity'] = data['oi_change'] * abs(data['price_change']) + + # 最近机构活动强度 + recent_institutional_activity = data['institutional_activity'].tail(5).sum() + + # 机构活动趋势 + institutional_trend = 'increasing' if recent_institutional_activity > 0 else 'decreasing' + + return { + 'recent_institutional_activity': recent_institutional_activity, + 'institutional_trend': institutional_trend + } diff --git a/qihuo_analyzer/modules/risk_manager.py b/qihuo_analyzer/modules/risk_manager.py new file mode 100644 index 0000000..2a8bf0a --- /dev/null +++ b/qihuo_analyzer/modules/risk_manager.py @@ -0,0 +1,274 @@ +# 风控管理模块 +import pandas as pd +from typing import Dict, Optional, Tuple +from qihuo_analyzer.utils.technical_analysis import calculate_atr +from qihuo_analyzer.core.models import StrategyConfig, RiskParams + + +class RiskManager: + """风险管理器""" + + def __init__(self, config: Optional[StrategyConfig] = None): + self.config = config or StrategyConfig() + + def calculate_stop_loss(self, data: pd.DataFrame, entry_price: float, direction: str, atr_multiplier: Optional[float] = None) -> float: + """计算止损位""" + atr_multiplier = atr_multiplier or self.config.atr_multiplier + + # 计算ATR + atr = calculate_atr(data).iloc[-1] + + # 根据方向计算止损位 + if direction == 'long': + stop_loss = entry_price - (atr * atr_multiplier) + elif direction == 'short': + stop_loss = entry_price + (atr * atr_multiplier) + else: + raise ValueError("Direction must be 'long' or 'short'") + + return stop_loss + + def calculate_position_size(self, account_balance: float, data: pd.DataFrame, direction: str, entry_price: float, + contract_multiplier: float = 10, margin_rate: float = 0.1) -> Dict: + """计算仓位大小""" + # 计算ATR + atr = calculate_atr(data).iloc[-1] + + # 计算每手风险 + if direction == 'long': + risk_per_unit = atr * self.config.atr_multiplier * contract_multiplier + elif direction == 'short': + risk_per_unit = atr * self.config.atr_multiplier * contract_multiplier + else: + raise ValueError("Direction must be 'long' or 'short'") + + # 计算最大风险金额 + max_risk_amount = account_balance * self.config.max_risk_percent + + # 计算建议手数 + suggested_units = max_risk_amount / risk_per_unit + suggested_units = max(1, int(suggested_units)) # 至少1手 + + # 计算保证金需求 + margin_per_unit = entry_price * contract_multiplier * margin_rate + total_margin = suggested_units * margin_per_unit + + # 计算实际风险比例 + actual_risk_percent = (risk_per_unit * suggested_units) / account_balance + + # 计算杠杆比例 + leverage = (suggested_units * entry_price * contract_multiplier) / account_balance + + return { + 'suggested_units': suggested_units, + 'risk_per_unit': risk_per_unit, + 'max_risk_amount': max_risk_amount, + 'margin_per_unit': margin_per_unit, + 'total_margin': total_margin, + 'actual_risk_percent': actual_risk_percent, + 'leverage': leverage, + 'atr': atr + } + + def calculate_profit_loss_ratio(self, entry_price: float, stop_loss: float, target_price: float, direction: str) -> float: + """计算盈亏比""" + if direction == 'long': + profit = target_price - entry_price + loss = entry_price - stop_loss + elif direction == 'short': + profit = entry_price - target_price + loss = stop_loss - entry_price + else: + raise ValueError("Direction must be 'long' or 'short'") + + if loss == 0: + return float('inf') + + return profit / loss + + def validate_trade(self, account_balance: float, data: pd.DataFrame, direction: str, + entry_price: float, target_price: float, contract_multiplier: float = 10, + margin_rate: float = 0.1) -> Dict: + """验证交易是否符合风控要求""" + # 计算止损位 + stop_loss = self.calculate_stop_loss(data, entry_price, direction) + + # 计算盈亏比 + pl_ratio = self.calculate_profit_loss_ratio(entry_price, stop_loss, target_price, direction) + + # 计算仓位大小 + position_info = self.calculate_position_size(account_balance, data, direction, entry_price, + contract_multiplier, margin_rate) + + # 检查各项风控指标 + checks = { + 'profit_loss_ratio': { + 'value': pl_ratio, + 'required': self.config.min_profit_loss_ratio, + 'pass': pl_ratio >= self.config.min_profit_loss_ratio + }, + 'risk_percent': { + 'value': position_info['actual_risk_percent'] * 100, + 'required': self.config.max_risk_percent * 100, + 'pass': position_info['actual_risk_percent'] <= self.config.max_risk_percent + }, + 'leverage': { + 'value': position_info['leverage'], + 'required': 5, # 最大杠杆 + 'pass': position_info['leverage'] <= 5 + }, + 'margin_utilization': { + 'value': (position_info['total_margin'] / account_balance) * 100, + 'required': 30, # 最大保证金使用率 + 'pass': (position_info['total_margin'] / account_balance) <= 0.3 + } + } + + # 综合判断 + all_passed = all(check['pass'] for check in checks.values()) + + return { + 'valid': all_passed, + 'checks': checks, + 'position_info': position_info, + 'stop_loss': stop_loss, + 'profit_loss_ratio': pl_ratio + } + + def generate_risk_report(self, account_balance: float, data: pd.DataFrame, direction: str, + entry_price: float, target_price: float, contract_multiplier: float = 10, + margin_rate: float = 0.1) -> Dict: + """生成风险报告""" + # 验证交易 + validation_result = self.validate_trade(account_balance, data, direction, entry_price, + target_price, contract_multiplier, margin_rate) + + # 生成风险建议 + suggestions = [] + + if not validation_result['checks']['profit_loss_ratio']['pass']: + suggestions.append(f"盈亏比不足,建议调整目标价至{self._calculate_adjusted_target(entry_price, validation_result['stop_loss'], direction):.2f}") + + if not validation_result['checks']['risk_percent']['pass']: + suggestions.append(f"风险比例过高,建议减少仓位至{int(validation_result['position_info']['suggested_units'] * 0.8)}手") + + if not validation_result['checks']['leverage']['pass']: + suggestions.append("杠杆比例过高,建议降低仓位") + + if not validation_result['checks']['margin_utilization']['pass']: + suggestions.append("保证金使用率过高,建议减少仓位") + + # 计算风险回报比 + risk_return_ratio = self._calculate_risk_return_ratio(validation_result['profit_loss_ratio'], + validation_result['position_info']['actual_risk_percent']) + + report = { + 'account_balance': account_balance, + 'direction': direction, + 'entry_price': entry_price, + 'stop_loss': validation_result['stop_loss'], + 'target_price': target_price, + 'profit_loss_ratio': validation_result['profit_loss_ratio'], + 'position_info': validation_result['position_info'], + 'risk_metrics': { + 'risk_return_ratio': risk_return_ratio, + 'max_drawdown_estimate': self._estimate_max_drawdown(account_balance, validation_result['position_info']), + 'recovery_factor': self._calculate_recovery_factor(risk_return_ratio) + }, + 'suggestions': suggestions, + 'validation_result': validation_result + } + + return report + + def _calculate_adjusted_target(self, entry_price: float, stop_loss: float, direction: str) -> float: + """计算调整后的目标价""" + if direction == 'long': + loss = entry_price - stop_loss + required_profit = loss * self.config.min_profit_loss_ratio + return entry_price + required_profit + elif direction == 'short': + loss = stop_loss - entry_price + required_profit = loss * self.config.min_profit_loss_ratio + return entry_price - required_profit + else: + raise ValueError("Direction must be 'long' or 'short'") + + def _calculate_risk_return_ratio(self, pl_ratio: float, risk_percent: float) -> float: + """计算风险回报比""" + return pl_ratio * (1 - risk_percent) + + def _estimate_max_drawdown(self, account_balance: float, position_info: Dict) -> float: + """估算最大回撤""" + max_loss = position_info['risk_per_unit'] * position_info['suggested_units'] + return (max_loss / account_balance) * 100 + + def _calculate_recovery_factor(self, risk_return_ratio: float) -> float: + """计算恢复因子""" + if risk_return_ratio <= 0: + return 0 + return risk_return_ratio * 0.8 + + def monitor_position_risk(self, current_price: float, entry_price: float, stop_loss: float, + target_price: float, direction: str, units: int, contract_multiplier: float = 10) -> Dict: + """监控持仓风险""" + # 计算当前盈亏 + if direction == 'long': + current_profit = (current_price - entry_price) * units * contract_multiplier + distance_to_stop = entry_price - current_price + distance_to_target = target_price - current_price + elif direction == 'short': + current_profit = (entry_price - current_price) * units * contract_multiplier + distance_to_stop = current_price - entry_price + distance_to_target = entry_price - current_price + else: + raise ValueError("Direction must be 'long' or 'short'") + + # 计算浮盈比例 + unrealized_pnl_percent = (current_profit / (entry_price * units * contract_multiplier)) * 100 + + # 计算止损触发距离 + stop_percent = (distance_to_stop / entry_price) * 100 + + # 计算目标达成距离 + target_percent = (distance_to_target / entry_price) * 100 + + # 风险状态评估 + risk_status = self._assess_risk_status(current_price, stop_loss, target_price, direction) + + return { + 'current_price': current_price, + 'entry_price': entry_price, + 'stop_loss': stop_loss, + 'target_price': target_price, + 'current_profit': current_profit, + 'unrealized_pnl_percent': unrealized_pnl_percent, + 'distance_to_stop': distance_to_stop, + 'distance_to_target': distance_to_target, + 'stop_percent': stop_percent, + 'target_percent': target_percent, + 'risk_status': risk_status + } + + def _assess_risk_status(self, current_price: float, stop_loss: float, target_price: float, direction: str) -> str: + """评估风险状态""" + if direction == 'long': + if current_price <= stop_loss: + return 'stop_loss_triggered' + elif current_price >= target_price: + return 'target_reached' + elif current_price > stop_loss * 1.05: + return 'low_risk' + else: + return 'medium_risk' + elif direction == 'short': + if current_price >= stop_loss: + return 'stop_loss_triggered' + elif current_price <= target_price: + return 'target_reached' + elif current_price < stop_loss * 0.95: + return 'low_risk' + else: + return 'medium_risk' + else: + raise ValueError("Direction must be 'long' or 'short'") diff --git a/qihuo_analyzer/modules/rollover_detector.py b/qihuo_analyzer/modules/rollover_detector.py new file mode 100644 index 0000000..288aba5 --- /dev/null +++ b/qihuo_analyzer/modules/rollover_detector.py @@ -0,0 +1,483 @@ +# 换月预警模块 +import pandas as pd +import numpy as np +from datetime import datetime, timedelta +from typing import Dict, List, Optional, Tuple + + +class RolloverDetector: + """换月预警检测器""" + + def __init__(self): + pass + + def analyze_rollover(self, symbol: str, data: pd.DataFrame, contract_info: Optional[Dict] = None) -> Dict: + """分析换月情况""" + result = {} + + # 检测交割日 + delivery_info = self._detect_delivery_date(symbol, contract_info) + result.update(delivery_info) + + # 分析流动性 + liquidity_analysis = self._analyze_liquidity(data) + result.update(liquidity_analysis) + + # 分析价差 + spread_analysis = self._analyze_spread(symbol) + result.update(spread_analysis) + + # 生成换月预警 + rollover_warning = self._generate_rollover_warning(delivery_info, liquidity_analysis) + result['rollover_warning'] = rollover_warning + + # 生成减仓建议 + position_adjustment = self._generate_position_adjustment(delivery_info, liquidity_analysis) + result['position_adjustment'] = position_adjustment + + return result + + def _detect_delivery_date(self, symbol: str, contract_info: Optional[Dict] = None) -> Dict: + """检测交割日""" + if contract_info and 'expire_datetime' in contract_info: + # 使用合约信息中的交割日 + expire_timestamp = contract_info['expire_datetime'] + if isinstance(expire_timestamp, int): + # 处理纳秒时间戳 + if expire_timestamp > 1e15: + expire_date = datetime.fromtimestamp(expire_timestamp / 1e9) + else: + expire_date = datetime.fromtimestamp(expire_timestamp) + else: + expire_date = pd.to_datetime(expire_timestamp) + else: + # 基于合约代码推断交割日 + expire_date = self._infer_delivery_date(symbol) + + # 计算距离交割日的天数 + today = datetime.now() + days_to_delivery = (expire_date - today).days + + # 确定换月预警级别 + warning_level = self._calculate_warning_level(days_to_delivery) + + return { + 'expire_date': expire_date.strftime('%Y-%m-%d'), + 'days_to_delivery': days_to_delivery, + 'warning_level': warning_level + } + + def _infer_delivery_date(self, symbol: str) -> datetime: + """基于合约代码推断交割日""" + # 简化的合约代码解析 + # 假设合约代码格式为:品种+年份+月份,如 'CU2309' + try: + # 提取年份和月份 + year_str = symbol[-4:-2] + month_str = symbol[-2:] + + # 构建年份(加上世纪) + year = 2000 + int(year_str) + month = int(month_str) + + # 假设交割日为合约月份的15日 + # 实际交割日可能因品种而异,这里使用简化处理 + expire_date = datetime(year, month, 15) + + return expire_date + except Exception: + # 如果解析失败,返回60天后的日期 + return datetime.now() + timedelta(days=60) + + def _calculate_warning_level(self, days_to_delivery: int) -> str: + """计算换月预警级别""" + if days_to_delivery <= 3: + return 'critical' + elif days_to_delivery <= 7: + return 'high' + elif days_to_delivery <= 15: + return 'medium' + elif days_to_delivery <= 30: + return 'low' + else: + return 'none' + + def _analyze_liquidity(self, data: pd.DataFrame) -> Dict: + """分析流动性""" + # 计算成交量指标 + avg_volume = data['volume'].tail(20).mean() + volume_trend = self._analyze_volume_trend(data['volume']) + + # 计算持仓量指标 + avg_open_interest = data['open_interest'].tail(20).mean() + oi_trend = self._analyze_oi_trend(data['open_interest']) + + # 计算买卖价差(简化处理) + # 实际应该使用Tick数据计算 + bid_ask_spread = self._estimate_bid_ask_spread(data) + + # 计算流动性评分 + liquidity_score = self._calculate_liquidity_score(avg_volume, volume_trend, avg_open_interest, oi_trend, bid_ask_spread) + + # 确定流动性风险级别 + liquidity_risk = self._calculate_liquidity_risk(liquidity_score) + + return { + 'avg_volume': avg_volume, + 'volume_trend': volume_trend, + 'avg_open_interest': avg_open_interest, + 'oi_trend': oi_trend, + 'bid_ask_spread': bid_ask_spread, + 'liquidity_score': liquidity_score, + 'liquidity_risk': liquidity_risk + } + + def _analyze_volume_trend(self, volume_series: pd.Series) -> str: + """分析成交量趋势""" + if len(volume_series) < 10: + return 'stable' + + # 计算短期和长期移动平均线 + short_ma = volume_series.tail(10).mean() + long_ma = volume_series.tail(30).mean() + + if short_ma > long_ma * 1.1: + return 'increasing' + elif short_ma < long_ma * 0.9: + return 'decreasing' + else: + return 'stable' + + def _analyze_oi_trend(self, oi_series: pd.Series) -> str: + """分析持仓量趋势""" + if len(oi_series) < 10: + return 'stable' + + # 计算短期和长期移动平均线 + short_ma = oi_series.tail(10).mean() + long_ma = oi_series.tail(30).mean() + + if short_ma > long_ma * 1.1: + return 'increasing' + elif short_ma < long_ma * 0.9: + return 'decreasing' + else: + return 'stable' + + def _estimate_bid_ask_spread(self, data: pd.DataFrame) -> float: + """估算买卖价差""" + # 简化处理,使用收盘价的波动来估算 + price_volatility = data['close'].tail(20).std() + # 假设价差为波动率的10% + return price_volatility * 0.1 + + def _calculate_liquidity_score(self, avg_volume: float, volume_trend: str, + avg_open_interest: float, oi_trend: str, + bid_ask_spread: float) -> float: + """计算流动性评分""" + # 基础分数 + base_score = 100 + + # 成交量因素 + if avg_volume < 1000: + base_score -= 30 + elif avg_volume < 5000: + base_score -= 15 + + # 成交量趋势因素 + if volume_trend == 'decreasing': + base_score -= 20 + elif volume_trend == 'increasing': + base_score += 10 + + # 持仓量因素 + if avg_open_interest < 5000: + base_score -= 20 + elif avg_open_interest < 20000: + base_score -= 10 + + # 持仓量趋势因素 + if oi_trend == 'decreasing': + base_score -= 15 + elif oi_trend == 'increasing': + base_score += 5 + + # 买卖价差因素 + if bid_ask_spread > 0.5: + base_score -= 25 + elif bid_ask_spread > 0.2: + base_score -= 10 + + # 确保分数在0-100之间 + return max(0, min(100, base_score)) + + def _calculate_liquidity_risk(self, liquidity_score: float) -> str: + """计算流动性风险""" + if liquidity_score < 30: + return 'high' + elif liquidity_score < 60: + return 'medium' + else: + return 'low' + + def _analyze_spread(self, symbol: str) -> Dict: + """分析价差""" + # 简化处理,实际应该比较当前合约和下一个合约的价差 + # 这里返回模拟数据 + return { + 'current_next_spread': 5.2, + 'spread_trend': 'stable', + 'spread_ratio': 0.0015 + } + + def _generate_rollover_warning(self, delivery_info: Dict, liquidity_info: Dict) -> Dict: + """生成换月预警""" + warning_level = delivery_info['warning_level'] + liquidity_risk = liquidity_info['liquidity_risk'] + + # 综合预警 + overall_warning = 'none' + if warning_level in ['critical', 'high'] or liquidity_risk == 'high': + overall_warning = 'high' + elif warning_level == 'medium' or liquidity_risk == 'medium': + overall_warning = 'medium' + elif warning_level == 'low': + overall_warning = 'low' + + # 预警信息 + warning_message = self._generate_warning_message(warning_level, liquidity_risk) + + # 建议操作 + recommended_actions = self._generate_recommended_actions(warning_level, liquidity_risk) + + return { + 'overall_warning': overall_warning, + 'warning_message': warning_message, + 'recommended_actions': recommended_actions + } + + def _generate_warning_message(self, warning_level: str, liquidity_risk: str) -> str: + """生成预警信息""" + messages = [] + + if warning_level == 'critical': + messages.append('合约即将到期,距离交割日不足3天') + elif warning_level == 'high': + messages.append('合约接近到期,距离交割日不足7天') + elif warning_level == 'medium': + messages.append('合约距离交割日不足15天,建议开始关注换月') + + if liquidity_risk == 'high': + messages.append('流动性风险较高,可能影响交易执行') + elif liquidity_risk == 'medium': + messages.append('流动性风险中等,建议谨慎交易') + + if not messages: + return '合约状态正常,无需特殊关注' + + return '; '.join(messages) + + def _generate_recommended_actions(self, warning_level: str, liquidity_risk: str) -> List[str]: + """生成建议操作""" + actions = [] + + if warning_level in ['critical', 'high']: + actions.append('立即开始换月操作') + actions.append('逐步减仓当前合约') + actions.append('在新合约建立相应仓位') + elif warning_level == 'medium': + actions.append('开始评估换月时机') + actions.append('关注新合约流动性') + + if liquidity_risk == 'high': + actions.append('减小单笔交易规模') + actions.append('使用限价单而非市价单') + actions.append('考虑提前换月') + + return actions + + def _generate_position_adjustment(self, delivery_info: Dict, liquidity_info: Dict) -> Dict: + """生成仓位调整建议""" + days_to_delivery = delivery_info['days_to_delivery'] + warning_level = delivery_info['warning_level'] + liquidity_risk = liquidity_info['liquidity_risk'] + + # 计算减仓比例 + reduction_ratio = self._calculate_reduction_ratio(days_to_delivery, warning_level, liquidity_risk) + + # 计算建议的减仓时间表 + reduction_schedule = self._generate_reduction_schedule(days_to_delivery, reduction_ratio) + + # 计算新合约建仓建议 + new_contract_adjustment = self._generate_new_contract_adjustment(reduction_ratio) + + return { + 'reduction_ratio': reduction_ratio, + 'reduction_schedule': reduction_schedule, + 'new_contract_adjustment': new_contract_adjustment + } + + def _calculate_reduction_ratio(self, days_to_delivery: int, warning_level: str, liquidity_risk: str) -> float: + """计算减仓比例""" + # 基础减仓比例 + base_ratio = 0.0 + + if warning_level == 'critical': + base_ratio = 0.9 # 减仓90% + elif warning_level == 'high': + base_ratio = 0.7 # 减仓70% + elif warning_level == 'medium': + base_ratio = 0.4 # 减仓40% + elif warning_level == 'low': + base_ratio = 0.2 # 减仓20% + + # 流动性风险调整 + if liquidity_risk == 'high': + base_ratio = min(1.0, base_ratio + 0.2) + elif liquidity_risk == 'medium': + base_ratio = min(1.0, base_ratio + 0.1) + + return base_ratio + + def _generate_reduction_schedule(self, days_to_delivery: int, reduction_ratio: float) -> List[Dict]: + """生成减仓时间表""" + schedule = [] + + if days_to_delivery <= 3: + # 紧急减仓 + schedule.append({ + 'timeframe': '今日', + 'reduction_ratio': reduction_ratio + }) + elif days_to_delivery <= 7: + # 快速减仓 + daily_ratio = reduction_ratio / 3 + for i in range(3): + schedule.append({ + 'timeframe': f'{i+1}天内', + 'reduction_ratio': daily_ratio + }) + elif days_to_delivery <= 15: + # 逐步减仓 + daily_ratio = reduction_ratio / 5 + for i in range(5): + schedule.append({ + 'timeframe': f'{i+1}天内', + 'reduction_ratio': daily_ratio + }) + elif days_to_delivery <= 30: + # 缓慢减仓 + weekly_ratio = reduction_ratio / 2 + schedule.append({ + 'timeframe': '第一周', + 'reduction_ratio': weekly_ratio + }) + schedule.append({ + 'timeframe': '第二周', + 'reduction_ratio': weekly_ratio + }) + + return schedule + + def _generate_new_contract_adjustment(self, reduction_ratio: float) -> Dict: + """生成新合约建仓建议""" + # 建议在新合约建立与原合约相同方向的仓位 + # 建仓比例应与减仓比例对应 + return { + 'direction': 'same_as_current', + 'target_ratio': reduction_ratio, + 'execution_strategy': 'gradual', + 'considerations': [ + '关注新合约流动性', + '注意合约间价差', + '避免在换月高峰期交易' + ] + } + + def monitor_rollover_risk(self, symbol: str, data: pd.DataFrame, position_size: float) -> Dict: + """监控换月风险""" + # 分析换月情况 + rollover_analysis = self.analyze_rollover(symbol, data) + + # 计算风险暴露 + risk_exposure = self._calculate_risk_exposure(position_size, rollover_analysis) + + # 生成风险报告 + risk_report = { + 'symbol': symbol, + 'position_size': position_size, + 'rollover_analysis': rollover_analysis, + 'risk_exposure': risk_exposure, + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + + return risk_report + + def _calculate_risk_exposure(self, position_size: float, rollover_analysis: Dict) -> Dict: + """计算风险暴露""" + warning_level = rollover_analysis['warning_level'] + liquidity_risk = rollover_analysis.get('liquidity_risk', 'low') + + # 基础风险分数 + base_risk = 0 + + if warning_level == 'critical': + base_risk = 90 + elif warning_level == 'high': + base_risk = 70 + elif warning_level == 'medium': + base_risk = 40 + elif warning_level == 'low': + base_risk = 20 + + # 流动性风险调整 + if liquidity_risk == 'high': + base_risk += 20 + elif liquidity_risk == 'medium': + base_risk += 10 + + # 仓位大小调整 + if position_size > 10: + base_risk += 15 + elif position_size > 5: + base_risk += 5 + + # 确保风险分数在0-100之间 + risk_score = max(0, min(100, base_risk)) + + # 风险等级 + risk_level = 'low' + if risk_score >= 80: + risk_level = 'critical' + elif risk_score >= 60: + risk_level = 'high' + elif risk_score >= 30: + risk_level = 'medium' + + return { + 'risk_score': risk_score, + 'risk_level': risk_level, + 'recommendations': self._generate_risk_recommendations(risk_level) + } + + def _generate_risk_recommendations(self, risk_level: str) -> List[str]: + """生成风险建议""" + recommendations = [] + + if risk_level == 'critical': + recommendations.append('立即减仓至最小仓位') + recommendations.append('优先处理换月操作') + recommendations.append('密切监控市场流动性') + elif risk_level == 'high': + recommendations.append('大幅减仓当前合约') + recommendations.append('加速换月进程') + recommendations.append('使用限价单控制交易成本') + elif risk_level == 'medium': + recommendations.append('开始有序减仓') + recommendations.append('评估换月时机') + recommendations.append('关注新合约表现') + else: + recommendations.append('保持正常交易') + recommendations.append('定期监控合约到期情况') + + return recommendations diff --git a/qihuo_analyzer/modules/support_resistance.py b/qihuo_analyzer/modules/support_resistance.py new file mode 100644 index 0000000..e685732 --- /dev/null +++ b/qihuo_analyzer/modules/support_resistance.py @@ -0,0 +1,389 @@ +# 压力支撑模块 +import pandas as pd +import numpy as np +from typing import Dict, List, Tuple, Optional +from qihuo_analyzer.utils.technical_analysis import calculate_bollinger_bands + + +class SupportResistance: + """压力支撑分析器""" + + def __init__(self): + pass + + def analyze_support_resistance(self, data: pd.DataFrame) -> Dict: + """分析压力支撑位""" + result = {} + + # 识别关键价位 + key_levels = self._identify_key_levels(data) + result.update(key_levels) + + # 计算枢轴点 + pivot_points = self._calculate_pivot_points(data) + result.update(pivot_points) + + # 基于布林带的支撑阻力 + bollinger_levels = self._calculate_bollinger_levels(data) + result.update(bollinger_levels) + + # 最近高低点分析 + recent_high_low = self._analyze_recent_high_low(data) + result.update(recent_high_low) + + # 斐波那契回调线 + fibonacci_levels = self._calculate_fibonacci_levels(data) + result['fibonacci_levels'] = fibonacci_levels + + # 综合支撑阻力位 + support_resistance_levels = self._generate_support_resistance_levels(result) + result['support_resistance_levels'] = support_resistance_levels + + return result + + def _identify_key_levels(self, data: pd.DataFrame) -> Dict: + """识别关键价位""" + # 计算最近N天的高低点 + recent_high = data['high'].tail(50).max() + recent_low = data['low'].tail(50).min() + + # 计算最近N天的平均波幅 + avg_range = (data['high'] - data['low']).tail(50).mean() + + # 识别成交量密集区 + volume_profile = self._calculate_volume_profile(data) + + # 识别价格密集区 + price_density = self._calculate_price_density(data) + + return { + 'recent_high': recent_high, + 'recent_low': recent_low, + 'avg_range': avg_range, + 'volume_profile': volume_profile, + 'price_density': price_density + } + + def _calculate_pivot_points(self, data: pd.DataFrame) -> Dict: + """计算枢轴点""" + # 使用最近的高点、低点和收盘价计算枢轴点 + if len(data) < 2: + return { + 'pivot_point': None, + 'resistance_1': None, + 'resistance_2': None, + 'support_1': None, + 'support_2': None + } + + high = data['high'].iloc[-1] + low = data['low'].iloc[-1] + close = data['close'].iloc[-1] + + # 计算枢轴点 + pivot_point = (high + low + close) / 3 + + # 计算阻力位和支撑位 + resistance_1 = 2 * pivot_point - low + resistance_2 = pivot_point + (high - low) + support_1 = 2 * pivot_point - high + support_2 = pivot_point - (high - low) + + return { + 'pivot_point': pivot_point, + 'resistance_1': resistance_1, + 'resistance_2': resistance_2, + 'support_1': support_1, + 'support_2': support_2 + } + + def _calculate_bollinger_levels(self, data: pd.DataFrame) -> Dict: + """基于布林带的支撑阻力""" + # 计算布林带 + bollinger_data = calculate_bollinger_bands(data) + + # 获取最新的布林带值 + upper_band = bollinger_data['upper_band'].iloc[-1] + middle_band = bollinger_data['sma'].iloc[-1] + lower_band = bollinger_data['lower_band'].iloc[-1] + + return { + 'bollinger_upper': upper_band, + 'bollinger_middle': middle_band, + 'bollinger_lower': lower_band + } + + def _analyze_recent_high_low(self, data: pd.DataFrame) -> Dict: + """最近高低点分析""" + # 计算不同周期的高低点 + periods = [10, 20, 50] + high_low_levels = {} + + for period in periods: + if len(data) >= period: + high_low_levels[f'{period}d_high'] = data['high'].tail(period).max() + high_low_levels[f'{period}d_low'] = data['low'].tail(period).min() + else: + high_low_levels[f'{period}d_high'] = None + high_low_levels[f'{period}d_low'] = None + + return high_low_levels + + def _calculate_volume_profile(self, data: pd.DataFrame) -> Dict: + """计算成交量分布""" + # 简化的成交量分布分析 + # 将价格区间分成10个区间,计算每个区间的成交量 + if len(data) < 10: + return {} + + price_min = data['low'].tail(50).min() + price_max = data['high'].tail(50).max() + price_range = price_max - price_min + bin_size = price_range / 10 + + volume_profile = {} + for i in range(10): + bin_low = price_min + i * bin_size + bin_high = price_min + (i + 1) * bin_size + + # 计算该价格区间的成交量 + bin_volume = data[ + (data['low'] <= bin_high) & + (data['high'] >= bin_low) + ]['volume'].sum() + + volume_profile[f'bin_{i+1}'] = { + 'price_range': (bin_low, bin_high), + 'volume': bin_volume + } + + # 找出成交量最大的区间 + max_volume_bin = max(volume_profile.items(), key=lambda x: x[1]['volume']) + + return { + 'volume_profile': volume_profile, + 'max_volume_bin': max_volume_bin + } + + def _calculate_price_density(self, data: pd.DataFrame) -> Dict: + """计算价格密度""" + # 简化的价格密度分析 + if len(data) < 10: + return {} + + # 计算收盘价的分布 + prices = data['close'].tail(100) + price_std = prices.std() + price_mean = prices.mean() + + # 计算价格分位数 + price_percentiles = { + 'p10': np.percentile(prices, 10), + 'p25': np.percentile(prices, 25), + 'p50': np.percentile(prices, 50), + 'p75': np.percentile(prices, 75), + 'p90': np.percentile(prices, 90) + } + + return { + 'price_mean': price_mean, + 'price_std': price_std, + 'price_percentiles': price_percentiles + } + + def _calculate_fibonacci_levels(self, data: pd.DataFrame) -> Dict: + """计算斐波那契回调线""" + if len(data) < 20: + return {} + + # 找出最近的显著高低点 + swing_high = data['high'].tail(50).max() + swing_low = data['low'].tail(50).min() + + # 计算斐波那契回调位 + range_high_low = swing_high - swing_low + + fib_levels = { + '0': swing_low, + '0.236': swing_low + range_high_low * 0.236, + '0.382': swing_low + range_high_low * 0.382, + '0.5': swing_low + range_high_low * 0.5, + '0.618': swing_low + range_high_low * 0.618, + '0.786': swing_low + range_high_low * 0.786, + '1': swing_high + } + + return fib_levels + + def _generate_support_resistance_levels(self, analysis: Dict) -> Dict: + """生成综合支撑阻力位""" + # 收集所有可能的支撑阻力位 + all_levels = [] + + # 添加最近高低点 + all_levels.append(analysis.get('recent_high', 0)) + all_levels.append(analysis.get('recent_low', 0)) + + # 添加枢轴点相关价位 + all_levels.extend([ + analysis.get('pivot_point', 0), + analysis.get('resistance_1', 0), + analysis.get('resistance_2', 0), + analysis.get('support_1', 0), + analysis.get('support_2', 0) + ]) + + # 添加布林带相关价位 + all_levels.extend([ + analysis.get('bollinger_upper', 0), + analysis.get('bollinger_middle', 0), + analysis.get('bollinger_lower', 0) + ]) + + # 添加不同周期的高低点 + periods = [10, 20, 50] + for period in periods: + all_levels.append(analysis.get(f'{period}d_high', 0)) + all_levels.append(analysis.get(f'{period}d_low', 0)) + + # 添加斐波那契回调位 + fib_levels = analysis.get('fibonacci_levels', {}) + all_levels.extend(fib_levels.values()) + + # 过滤无效值并排序 + all_levels = [level for level in all_levels if level and level > 0] + all_levels.sort() + + # 去重(相近的价位视为同一价位) + if not all_levels: + return {'support_levels': [], 'resistance_levels': []} + + unique_levels = [] + threshold = analysis.get('avg_range', 10) * 0.3 # 阈值为平均波幅的30% + + for level in all_levels: + if not unique_levels or abs(level - unique_levels[-1]) > threshold: + unique_levels.append(level) + + # 确定当前价格 + current_price = analysis.get('recent_high', 3500) * 0.95 # 使用最近高点的95%作为当前价格 + + # 分离支撑位和阻力位 + support_levels = [level for level in unique_levels if level < current_price] + resistance_levels = [level for level in unique_levels if level > current_price] + + # 按距离当前价格排序 + support_levels.sort(reverse=True) # 最近的支撑位在前 + resistance_levels.sort() # 最近的阻力位在前 + + # 取最近的几个支撑阻力位 + support_levels = support_levels[:3] # 最近的3个支撑位 + resistance_levels = resistance_levels[:3] # 最近的3个阻力位 + + return { + 'support_levels': support_levels, + 'resistance_levels': resistance_levels, + 'current_price': current_price + } + + def calculate_stop_loss_level(self, data: pd.DataFrame, direction: str, atr: float) -> float: + """计算智能止损位""" + # 分析支撑阻力位 + sr_analysis = self.analyze_support_resistance(data) + support_levels = sr_analysis.get('support_resistance_levels', {}).get('support_levels', []) + resistance_levels = sr_analysis.get('support_resistance_levels', {}).get('resistance_levels', []) + current_price = data['close'].iloc[-1] + + if direction == 'long': + # 做多时,止损位应在最近的支撑位下方 + if support_levels: + # 最近的支撑位下方ATR的0.5倍 + stop_loss = support_levels[0] - atr * 0.5 + else: + # 没有支撑位时,使用ATR的2倍 + stop_loss = current_price - atr * 2 + elif direction == 'short': + # 做空时,止损位应在最近的阻力位上方 + if resistance_levels: + # 最近的阻力位上方ATR的0.5倍 + stop_loss = resistance_levels[0] + atr * 0.5 + else: + # 没有阻力位时,使用ATR的2倍 + stop_loss = current_price + atr * 2 + else: + raise ValueError("Direction must be 'long' or 'short'") + + return stop_loss + + def calculate_target_price(self, data: pd.DataFrame, direction: str, entry_price: float) -> float: + """计算目标价""" + # 分析支撑阻力位 + sr_analysis = self.analyze_support_resistance(data) + support_levels = sr_analysis.get('support_resistance_levels', {}).get('support_levels', []) + resistance_levels = sr_analysis.get('support_resistance_levels', {}).get('resistance_levels', []) + + if direction == 'long': + # 做多时,目标价应在最近的阻力位 + if resistance_levels: + target_price = resistance_levels[0] + else: + # 没有阻力位时,使用近期高点 + target_price = sr_analysis.get('recent_high', entry_price * 1.05) + elif direction == 'short': + # 做空时,目标价应在最近的支撑位 + if support_levels: + target_price = support_levels[0] + else: + # 没有支撑位时,使用近期低点 + target_price = sr_analysis.get('recent_low', entry_price * 0.95) + else: + raise ValueError("Direction must be 'long' or 'short'") + + return target_price + + def analyze_price_position(self, data: pd.DataFrame) -> Dict: + """分析价格位置""" + current_price = data['close'].iloc[-1] + + # 分析支撑阻力位 + sr_analysis = self.analyze_support_resistance(data) + support_levels = sr_analysis.get('support_resistance_levels', {}).get('support_levels', []) + resistance_levels = sr_analysis.get('support_resistance_levels', {}).get('resistance_levels', []) + + # 计算价格与支撑阻力位的距离 + distance_to_support = float('inf') + distance_to_resistance = float('inf') + + if support_levels: + distance_to_support = current_price - support_levels[0] + + if resistance_levels: + distance_to_resistance = resistance_levels[0] - current_price + + # 分析价格位置 + position = 'neutral' + if distance_to_resistance < sr_analysis.get('avg_range', 10) * 0.2: + position = 'near_resistance' + elif distance_to_support < sr_analysis.get('avg_range', 10) * 0.2: + position = 'near_support' + + # 分析价格在布林带中的位置 + bollinger_upper = sr_analysis.get('bollinger_upper', 0) + bollinger_middle = sr_analysis.get('bollinger_middle', 0) + bollinger_lower = sr_analysis.get('bollinger_lower', 0) + + bollinger_position = 'middle' + if current_price > bollinger_upper: + bollinger_position = 'upper' + elif current_price < bollinger_lower: + bollinger_position = 'lower' + + return { + 'current_price': current_price, + 'distance_to_support': distance_to_support, + 'distance_to_resistance': distance_to_resistance, + 'position': position, + 'bollinger_position': bollinger_position, + 'support_levels': support_levels, + 'resistance_levels': resistance_levels + } diff --git a/qihuo_analyzer/modules/trend_filter.py b/qihuo_analyzer/modules/trend_filter.py new file mode 100644 index 0000000..e3a47fc --- /dev/null +++ b/qihuo_analyzer/modules/trend_filter.py @@ -0,0 +1,226 @@ +# 趋势分析模块 +import pandas as pd +from typing import Dict, Tuple, Optional +from qihuo_analyzer.utils.technical_analysis import ( + calculate_adx, + calculate_moving_average, + calculate_price_quantile, + calculate_volume_price_strength +) +from qihuo_analyzer.core.models import StrategyConfig + + +class TrendFilter: + """趋势分析过滤器""" + + def __init__(self, config: Optional[StrategyConfig] = None): + self.config = config or StrategyConfig() + + def analyze_trend(self, data: pd.DataFrame) -> Dict: + """分析趋势""" + result = {} + + # 计算ADX指标 + adx_data = calculate_adx(data, self.config.adx_period) + adx = adx_data['adx'].iloc[-1] + plus_di = adx_data['plus_di'].iloc[-1] + minus_di = adx_data['minus_di'].iloc[-1] + + # 趋势强度判断 + trend_strength = self._judge_trend_strength(adx) + trend_direction = self._judge_trend_direction(plus_di, minus_di) + + # 计算移动平均线 + ma_data = calculate_moving_average(data, [self.config.short_ma, self.config.long_ma]) + short_ma = ma_data[f'ma{self.config.short_ma}'].iloc[-1] + long_ma = ma_data[f'ma{self.config.long_ma}'].iloc[-1] + + # 双均线排列判断 + ma_relationship = self._judge_ma_relationship(short_ma, long_ma) + + # 多周期共振分析 + multi_period_analysis = self._analyze_multi_period(data) + + # 综合趋势判断 + overall_trend = self._judge_overall_trend(trend_strength, trend_direction, ma_relationship) + + result.update({ + 'adx': adx, + 'plus_di': plus_di, + 'minus_di': minus_di, + 'trend_strength': trend_strength, + 'trend_direction': trend_direction, + 'short_ma': short_ma, + 'long_ma': long_ma, + 'ma_relationship': ma_relationship, + 'multi_period_analysis': multi_period_analysis, + 'overall_trend': overall_trend + }) + + return result + + def _judge_trend_strength(self, adx: float) -> str: + """判断趋势强度""" + if adx > 40: + return 'strong' + elif adx >= 25: + return 'medium' + elif adx >= 20: + return 'weak' + else: + return 'none' + + def _judge_trend_direction(self, plus_di: float, minus_di: float) -> str: + """判断趋势方向""" + if plus_di > minus_di: + return 'up' + elif plus_di < minus_di: + return 'down' + else: + return 'neutral' + + def _judge_ma_relationship(self, short_ma: float, long_ma: float) -> str: + """判断均线关系""" + if short_ma > long_ma: + return 'bullish' + elif short_ma < long_ma: + return 'bearish' + else: + return 'neutral' + + def _analyze_multi_period(self, data: pd.DataFrame) -> Dict: + """多周期共振分析""" + periods = [15, 60, 240] # 15分钟、1小时、4小时 + analysis = {} + + for period in periods: + # 简化处理,使用不同周期的收盘价 + if len(data) >= period: + period_data = data.tail(period) + ma_short = period_data['close'].rolling(window=5).mean().iloc[-1] + ma_long = period_data['close'].rolling(window=20).mean().iloc[-1] + + if ma_short > ma_long: + analysis[f'{period}min'] = 'bullish' + elif ma_short < ma_long: + analysis[f'{period}min'] = 'bearish' + else: + analysis[f'{period}min'] = 'neutral' + else: + analysis[f'{period}min'] = 'insufficient_data' + + # 计算共振程度 + bullish_count = sum(1 for v in analysis.values() if v == 'bullish') + bearish_count = sum(1 for v in analysis.values() if v == 'bearish') + + resonance = 'none' + if bullish_count >= 2: + resonance = 'bullish_resonance' + elif bearish_count >= 2: + resonance = 'bearish_resonance' + + analysis['resonance'] = resonance + + return analysis + + def _judge_overall_trend(self, trend_strength: str, trend_direction: str, ma_relationship: str) -> str: + """综合判断趋势""" + if trend_strength == 'none': + return 'neutral' + + if trend_direction == 'up' and ma_relationship == 'bullish': + return 'strong_bullish' + elif trend_direction == 'down' and ma_relationship == 'bearish': + return 'strong_bearish' + elif trend_direction == 'up' and ma_relationship == 'bearish': + return 'weak_bullish' + elif trend_direction == 'down' and ma_relationship == 'bullish': + return 'weak_bearish' + else: + return 'neutral' + + def calculate_win_rate(self, data: pd.DataFrame) -> float: + """计算胜率""" + # 获取ADX值 + adx_data = calculate_adx(data, self.config.adx_period) + adx = adx_data['adx'].iloc[-1] + + # 计算价格分位 + price_quantile = calculate_price_quantile(data) + price_score = self._calculate_price_score(price_quantile) + + # 计算量价强度 + volume_price_strength = calculate_volume_price_strength(data) + + # 计算趋势强度评分 + trend_strength_score = self._calculate_trend_strength_score(adx) + + # 根据市场状态计算加权胜率 + if adx < 20: # 震荡市 + win_rate = ( + price_score * 0.25 + + volume_price_strength * 0.6 + + trend_strength_score * 0.15 + ) + else: # 趋势市 + # 价格分位权重随ADX递减 + price_weight = max(0.3, 0.6 - (adx - 20) * 0.0075) + trend_adjustment = ((adx - 20) * 0.5) / 100 if adx_data['plus_di'].iloc[-1] > adx_data['minus_di'].iloc[-1] else -((adx - 20) * 0.5) / 100 + + win_rate = ( + price_score * price_weight + + volume_price_strength * 0.4 + + trend_strength_score * (0.6 - price_weight) + ) + trend_adjustment + + # 确保胜率在合理范围内 + win_rate = max(0, min(100, win_rate)) + + return win_rate + + def _calculate_price_score(self, quantile: float) -> float: + """计算价格分位评分""" + if quantile < 0.2: + return 90 + elif quantile < 0.4: + return 75 + elif quantile < 0.6: + return 55 + elif quantile < 0.8: + return 40 + else: + return 25 + + def _calculate_trend_strength_score(self, adx: float) -> float: + """计算趋势强度评分""" + if adx > 40: + return 85 + elif adx >= 25: + return 70 + elif adx >= 20: + return 50 + else: + return 30 + + def judge_cycle(self, data: pd.DataFrame) -> str: + """判断周期""" + multi_period_analysis = self._analyze_multi_period(data) + adx_data = calculate_adx(data, self.config.adx_period) + adx = adx_data['adx'].iloc[-1] + + # 检查各周期方向一致性 + directions = [v for k, v in multi_period_analysis.items() if k.endswith('min')] + valid_directions = [d for d in directions if d != 'insufficient_data'] + + if not valid_directions: + return 'medium' + + # 检查是否所有周期方向一致且不为中性 + if len(set(valid_directions)) == 1 and valid_directions[0] != 'neutral': + # 检查是否为极强趋势 + if adx > 40: + return 'long' + else: + return 'short' + else: + return 'medium' diff --git a/qihuo_analyzer/utils/__pycache__/config_manager.cpython-311.pyc b/qihuo_analyzer/utils/__pycache__/config_manager.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1342aa583765f4fae8cf7f5c8aa4d742eec7eac GIT binary patch literal 3801 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 literal 0 HcmV?d00001 diff --git a/qihuo_analyzer/utils/config_manager.py b/qihuo_analyzer/utils/config_manager.py new file mode 100644 index 0000000..ef816ff --- /dev/null +++ b/qihuo_analyzer/utils/config_manager.py @@ -0,0 +1,70 @@ +# 配置管理工具 +import os +from dotenv import load_dotenv +from typing import Dict, Optional + + +class ConfigManager: + """配置管理类""" + + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super(ConfigManager, cls).__new__(cls) + cls._instance._load_config() + return cls._instance + + def _load_config(self): + """加载配置""" + # 加载.env文件 + load_dotenv() + + # API配置 + self.openai_api_key = os.getenv('OPENAI_API_KEY', '') + self.deepseek_api_key = os.getenv('DEEPSEEK_API_KEY', '') + self.deepseek_api_url = os.getenv('DEEPSEEK_API_URL', 'https://api.deepseek.com/v1/chat/completions') + + # 数据库配置 + self.db_path = os.getenv('DB_PATH', './data/futures_analysis.db') + + # 天勤TQSDK配置 + self.tqserver_host = os.getenv('TQSERVER_HOST', 'api.shinnytech.com') + self.tqserver_port = int(os.getenv('TQSERVER_PORT', '7777')) + + # 风险配置 + self.max_risk_percent = float(os.getenv('MAX_RISK_PERCENT', '0.02')) + self.min_profit_loss_ratio = float(os.getenv('MIN_PROFIT_LOSS_RATIO', '1.5')) + + # 策略配置 + self.default_atr_multiplier = float(os.getenv('DEFAULT_ATR_MULTIPLIER', '2.0')) + self.default_adx_threshold = float(os.getenv('DEFAULT_ADX_THRESHOLD', '20')) + + # 定时任务配置 + self.review_times = os.getenv('REVIEW_TIMES', '09:00,12:30,15:30').split(',') + + def get_config(self) -> Dict: + """获取所有配置""" + return { + 'openai_api_key': self.openai_api_key, + 'deepseek_api_key': self.deepseek_api_key, + 'deepseek_api_url': self.deepseek_api_url, + 'db_path': self.db_path, + 'tqserver_host': self.tqserver_host, + 'tqserver_port': self.tqserver_port, + 'max_risk_percent': self.max_risk_percent, + 'min_profit_loss_ratio': self.min_profit_loss_ratio, + 'default_atr_multiplier': self.default_atr_multiplier, + 'default_adx_threshold': self.default_adx_threshold, + 'review_times': self.review_times + } + + def update_config(self, config: Dict): + """更新配置""" + for key, value in config.items(): + if hasattr(self, key): + setattr(self, key, value) + + +# 全局配置实例 +config_manager = ConfigManager() diff --git a/qihuo_analyzer/utils/technical_analysis.py b/qihuo_analyzer/utils/technical_analysis.py new file mode 100644 index 0000000..9a294e1 --- /dev/null +++ b/qihuo_analyzer/utils/technical_analysis.py @@ -0,0 +1,153 @@ +# 技术分析工具 +import numpy as np +import pandas as pd +from typing import Dict, List, Tuple + + +def calculate_macd(data: pd.DataFrame, fast_period: int = 12, slow_period: int = 26, signal_period: int = 9) -> Dict[str, pd.Series]: + """计算MACD指标""" + exp1 = data['close'].ewm(span=fast_period, adjust=False).mean() + exp2 = data['close'].ewm(span=slow_period, adjust=False).mean() + macd = exp1 - exp2 + signal = macd.ewm(span=signal_period, adjust=False).mean() + histogram = macd - signal + + return { + 'macd': macd, + 'signal': signal, + 'histogram': histogram + } + + +def calculate_rsi(data: pd.DataFrame, period: int = 14) -> pd.Series: + """计算RSI指标""" + delta = data['close'].diff() + gain = (delta.where(delta > 0, 0)).rolling(window=period).mean() + loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean() + + rs = gain / loss + rsi = 100 - (100 / (1 + rs)) + + return rsi + + +def calculate_bollinger_bands(data: pd.DataFrame, period: int = 20, std_dev: float = 2.0) -> Dict[str, pd.Series]: + """计算布林带""" + sma = data['close'].rolling(window=period).mean() + std = data['close'].rolling(window=period).std() + upper_band = sma + (std * std_dev) + lower_band = sma - (std * std_dev) + + return { + 'sma': sma, + 'upper_band': upper_band, + 'lower_band': lower_band + } + + +def calculate_kdj(data: pd.DataFrame, period: int = 9, signal_period: int = 3) -> Dict[str, pd.Series]: + """计算KDJ指标""" + low_min = data['low'].rolling(window=period).min() + high_max = data['high'].rolling(window=period).max() + + rsv = (data['close'] - low_min) / (high_max - low_min) * 100 + k = rsv.ewm(alpha=1/signal_period, adjust=False).mean() + d = k.ewm(alpha=1/signal_period, adjust=False).mean() + j = 3 * k - 2 * d + + return { + 'k': k, + 'd': d, + 'j': j + } + + +def calculate_adx(data: pd.DataFrame, period: int = 14) -> Dict[str, pd.Series]: + """计算ADX指标""" + high = data['high'] + low = data['low'] + close = data['close'] + + tr1 = high - low + tr2 = abs(high - close.shift()) + tr3 = abs(low - close.shift()) + tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1) + + plus_dm = high.diff() + minus_dm = low.diff() + + plus_dm[plus_dm < 0] = 0 + minus_dm[minus_dm > 0] = 0 + minus_dm = abs(minus_dm) + + atr = tr.rolling(window=period).mean() + plus_di = (plus_dm.rolling(window=period).mean() / atr) * 100 + minus_di = (minus_dm.rolling(window=period).mean() / atr) * 100 + + dx = (abs(plus_di - minus_di) / (plus_di + minus_di)) * 100 + adx = dx.rolling(window=period).mean() + + return { + 'adx': adx, + 'plus_di': plus_di, + 'minus_di': minus_di + } + + +def calculate_atr(data: pd.DataFrame, period: int = 14) -> pd.Series: + """计算ATR指标""" + high = data['high'] + low = data['low'] + close = data['close'] + + tr1 = high - low + tr2 = abs(high - close.shift()) + tr3 = abs(low - close.shift()) + tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1) + + atr = tr.rolling(window=period).mean() + + return atr + + +def calculate_moving_average(data: pd.DataFrame, periods: List[int]) -> Dict[str, pd.Series]: + """计算移动平均线""" + mas = {} + for period in periods: + mas[f'ma{period}'] = data['close'].rolling(window=period).mean() + + return mas + + +def calculate_price_quantile(data: pd.DataFrame, period: int = 100) -> float: + """计算价格分位""" + prices = data['close'].tail(period) + current_price = prices.iloc[-1] + quantile = (prices <= current_price).sum() / len(prices) + + return quantile + + +def calculate_volume_price_strength(data: pd.DataFrame, period: int = 20) -> float: + """计算量价强度""" + df = data.tail(period).copy() + df['price_change'] = df['close'].pct_change() + df['volume_change'] = df['volume'].pct_change() + + # 量价配合度 + strength = 0 + for i in range(1, len(df)): + if (df['price_change'].iloc[i] > 0 and df['volume_change'].iloc[i] > 0) or \ + (df['price_change'].iloc[i] < 0 and df['volume_change'].iloc[i] < 0): + strength += abs(df['price_change'].iloc[i]) * (1 + abs(df['volume_change'].iloc[i])) + else: + strength -= abs(df['price_change'].iloc[i]) * (1 + abs(df['volume_change'].iloc[i])) + + # 归一化到0-100 + max_strength = abs(strength) + if max_strength == 0: + return 50 + + normalized_strength = (strength / max_strength + 1) / 2 * 100 + + return normalized_strength diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3edd9bd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +numpy==1.26.4 +pandas==2.2.1 +matplotlib==3.8.3 +scikit-learn==1.4.0 +tqsdk==1.6.3 +requests==2.31.0 +python-dotenv==1.0.0 +APScheduler==3.10.4 +pytest==7.4.4 diff --git a/test_system.py b/test_system.py new file mode 100644 index 0000000..bf46f0f --- /dev/null +++ b/test_system.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +# 系统测试脚本 + +import unittest +import pandas as pd +from qihuo_analyzer.data.data_fetcher import DataFetcher +from qihuo_analyzer.data.data_storage import DataStorage +from qihuo_analyzer.modules.trend_filter import TrendFilter +from qihuo_analyzer.modules.risk_manager import RiskManager +from qihuo_analyzer.modules.fund_flow_monitor import FundFlowMonitor +from qihuo_analyzer.modules.support_resistance import SupportResistance +from qihuo_analyzer.modules.rollover_detector import RolloverDetector +from qihuo_analyzer.modules.deepseek_agent import DeepseekAgent + + +class TestSystemComponents(unittest.TestCase): + """测试系统组件""" + + def setUp(self): + """设置测试环境""" + self.symbol = "CU2309" + self.data_fetcher = DataFetcher() + self.data_storage = DataStorage() + + def test_data_fetcher(self): + """测试数据获取器""" + print("测试数据获取器...") + kline_data = self.data_fetcher.get_kline_data(self.symbol, "1d", 100) + self.assertFalse(kline_data.empty) + self.assertIn('close', kline_data.columns) + self.assertIn('volume', kline_data.columns) + print("✅ 数据获取器测试通过") + + def test_data_storage(self): + """测试数据存储""" + print("测试数据存储...") + kline_data = self.data_fetcher.get_kline_data(self.symbol, "1d", 20) + success = self.data_storage.save_kline_data(self.symbol, "1d", kline_data) + self.assertTrue(success) + + # 测试读取数据 + stored_data = self.data_storage.get_kline_data(self.symbol, "1d", 10) + self.assertFalse(stored_data.empty) + print("✅ 数据存储测试通过") + + def test_trend_filter(self): + """测试趋势过滤器""" + print("测试趋势过滤器...") + kline_data = self.data_fetcher.get_kline_data(self.symbol, "1d", 100) + trend_filter = TrendFilter() + + # 测试趋势分析 + trend_analysis = trend_filter.analyze_trend(kline_data) + self.assertIn('adx', trend_analysis) + self.assertIn('trend_strength', trend_analysis) + + # 测试胜率计算 + win_rate = trend_filter.calculate_win_rate(kline_data) + self.assertGreaterEqual(win_rate, 0) + self.assertLessEqual(win_rate, 100) + + # 测试周期判断 + cycle = trend_filter.judge_cycle(kline_data) + self.assertIn(cycle, ['short', 'medium', 'long']) + print("✅ 趋势过滤器测试通过") + + def test_risk_manager(self): + """测试风险管理器""" + print("测试风险管理器...") + kline_data = self.data_fetcher.get_kline_data(self.symbol, "1d", 100) + risk_manager = RiskManager() + + # 测试止损计算 + entry_price = kline_data['close'].iloc[-1] + stop_loss = risk_manager.calculate_stop_loss(kline_data, entry_price, "long") + self.assertLess(stop_loss, entry_price) + + # 测试仓位计算 + account_balance = 1000000 + position_info = risk_manager.calculate_position_size(account_balance, kline_data, "long", entry_price) + self.assertIn('suggested_units', position_info) + self.assertGreater(position_info['suggested_units'], 0) + print("✅ 风险管理器测试通过") + + def test_fund_flow_monitor(self): + """测试资金流向监控器""" + print("测试资金流向监控器...") + kline_data = self.data_fetcher.get_kline_data(self.symbol, "1d", 100) + fund_flow_monitor = FundFlowMonitor() + + # 测试资金流向分析 + fund_flow_analysis = fund_flow_monitor.analyze_fund_flow(kline_data) + self.assertIn('fund_flow_strength', fund_flow_analysis) + self.assertIn('fund_signal', fund_flow_analysis) + print("✅ 资金流向监控器测试通过") + + def test_support_resistance(self): + """测试压力支撑分析器""" + print("测试压力支撑分析器...") + kline_data = self.data_fetcher.get_kline_data(self.symbol, "1d", 100) + support_resistance = SupportResistance() + + # 测试压力支撑分析 + sr_analysis = support_resistance.analyze_support_resistance(kline_data) + self.assertIn('support_resistance_levels', sr_analysis) + support_levels = sr_analysis['support_resistance_levels']['support_levels'] + resistance_levels = sr_analysis['support_resistance_levels']['resistance_levels'] + self.assertIsInstance(support_levels, list) + self.assertIsInstance(resistance_levels, list) + print("✅ 压力支撑分析器测试通过") + + def test_rollover_detector(self): + """测试换月检测器""" + print("测试换月检测器...") + kline_data = self.data_fetcher.get_kline_data(self.symbol, "1d", 100) + rollover_detector = RolloverDetector() + + # 测试换月分析 + rollover_analysis = rollover_detector.analyze_rollover(self.symbol, kline_data) + self.assertIn('expire_date', rollover_analysis) + self.assertIn('days_to_delivery', rollover_analysis) + self.assertIn('warning_level', rollover_analysis) + print("✅ 换月检测器测试通过") + + def test_deepseek_agent(self): + """测试DeepSeek代理""" + print("测试DeepSeek代理...") + deepseek_agent = DeepseekAgent() + + # 测试市场分析 + market_data = { + 'symbol': self.symbol, + 'latest_price': 35000, + 'volume': 10000, + 'open_interest': 50000, + 'timeframe': '1d' + } + + technical_indicators = { + 'macd': {'signal': '金叉'}, + 'rsi': 55, + 'bollinger': {'position': '中轨附近'}, + 'kdj': {'signal': '金叉'}, + 'atr': 200 + } + + trend_data = { + 'adx': 25, + 'trend_strength': 'medium', + 'trend_direction': 'up', + 'ma_relationship': 'bullish', + 'overall_trend': 'strong_bullish', + 'win_rate': 65 + } + + risk_metrics = { + 'stop_loss': 34500, + 'target_price': 36000, + 'profit_loss_ratio': 1.8, + 'position_size': 2, + 'risk_ratio': 2.5 + } + + analysis_result = deepseek_agent.analyze_market(market_data, technical_indicators, trend_data, risk_metrics) + self.assertIn('trend_judgment', analysis_result) + self.assertIn('win_rate_assessment', analysis_result) + print("✅ DeepSeek代理测试通过") + + +def run_tests(): + """运行测试""" + print("="*60) + print("AI 期货分析系统 - 组件测试") + print("="*60) + + # 创建测试套件 + suite = unittest.TestLoader().loadTestsFromTestCase(TestSystemComponents) + + # 运行测试 + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(suite) + + print("\n" + "="*60) + if result.wasSuccessful(): + print("✅ 所有测试通过!系统组件运行正常") + else: + print("❌ 测试失败,请检查系统组件") + print("="*60) + + +if __name__ == "__main__": + run_tests() diff --git a/web/app.py b/web/app.py new file mode 100644 index 0000000..87c92e5 --- /dev/null +++ b/web/app.py @@ -0,0 +1,645 @@ +#!/usr/bin/env python3 +# Flask web 应用 + +from flask import Flask, render_template, jsonify, request +import sys +import os + +# 添加项目根目录到 Python 路径 +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from qihuo_analyzer.data.data_fetcher import DataFetcher +from qihuo_analyzer.data.data_storage import DataStorage +from qihuo_analyzer.modules.trend_filter import TrendFilter +from qihuo_analyzer.modules.risk_manager import RiskManager +from qihuo_analyzer.modules.fund_flow_monitor import FundFlowMonitor +from qihuo_analyzer.modules.support_resistance import SupportResistance +from qihuo_analyzer.modules.rollover_detector import RolloverDetector +from qihuo_analyzer.modules.deepseek_agent import DeepseekAgent +from qihuo_analyzer.core.models import AnalysisResult + +app = Flask(__name__) + +# 模板上下文处理器 +@app.context_processor +def inject_functions(): + import datetime + def now(): + return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + return {'now': now} + +# 初始化组件 +data_fetcher = DataFetcher() +data_storage = DataStorage() +trend_filter = TrendFilter() +risk_manager = RiskManager() +fund_flow_monitor = FundFlowMonitor() +support_resistance = SupportResistance() +rollover_detector = RolloverDetector() +deepseek_agent = DeepseekAgent() + +# 连接API +data_fetcher.connect() + +# 测试品种列表 - 使用当前有效的合约代码 +test_symbols = ["CU2603", "AL2603", "ZN2603", "PB2603", "NI2603", "SN2603"] + +@app.route('/') +def index(): + """首页 - 多品种分析面板""" + # 获取所有品种的分析数据 + symbols_data = [] + data_available = False + + for symbol in test_symbols: + try: + # 获取K线数据 + kline_data = data_fetcher.get_kline_data(symbol, "1d", 200) + if kline_data is None or kline_data.empty: + continue + + data_available = True + + # 趋势分析 + trend_analysis = trend_filter.analyze_trend(kline_data) + win_rate = trend_filter.calculate_win_rate(kline_data) + cycle = trend_filter.judge_cycle(kline_data) + + # 资金流向分析 + fund_flow_analysis = fund_flow_monitor.analyze_fund_flow(kline_data) + + # 换月分析 + rollover_analysis = rollover_detector.analyze_rollover(symbol, kline_data) + + # 价格数据 + current_price = kline_data['close'].iloc[-1] + + # 获取中文名称 + product_name_cn = data_fetcher.get_product_name_cn(symbol) + + # 转换周期为中文 + def cycle_to_cn(cycle): + cycle_map = { + 'short': '短期', + 'medium': '中期', + 'long': '长期', + 'bullish': '多头', + 'bearish': '空头', + 'sideways': '震荡' + } + return cycle_map.get(cycle, cycle) + + # 转换资金流向为中文 + def fund_flow_to_cn(fund_flow): + fund_flow_map = { + 'bullish': '多头', + 'bearish': '空头', + 'neutral': '中性', + 'Strong_bullish': '强多头', + 'Strong_bearish': '强空头' + } + return fund_flow_map.get(fund_flow, fund_flow) + + # 构建数据 + # 安全处理可能的NaN值 + safe_current_price = 0 + try: + if current_price is not None and (not isinstance(current_price, float) or current_price == current_price): # 不是NaN + safe_current_price = round(current_price, 2) + except (ValueError, TypeError): + safe_current_price = 0 + + safe_win_rate = 0 + try: + if win_rate is not None and (not isinstance(win_rate, float) or win_rate == win_rate): # 不是NaN + safe_win_rate = round(win_rate, 1) + except (ValueError, TypeError): + safe_win_rate = 0 + + # 安全获取ADX值 + safe_adx = 0 + try: + adx = trend_analysis.get('adx', 0) + if adx is not None and (not isinstance(adx, float) or adx == adx): # 不是NaN + safe_adx = adx + except (ValueError, TypeError): + safe_adx = 0 + + symbol_data = { + 'symbol': symbol, + 'name': f"{product_name_cn}({symbol})".upper(), + 'current_price': safe_current_price, + 'direction': trend_analysis.get('overall_trend', 'sideways'), + 'win_rate': safe_win_rate, + 'trend_strength': _get_trend_strength_display(safe_adx), + 'cycle': cycle_to_cn(cycle), + 'rollover_warning': _get_rollover_warning(rollover_analysis), + 'fund_flow': fund_flow_to_cn(fund_flow_analysis.get('fund_signal', 'neutral')) + } + symbols_data.append(symbol_data) + except Exception as e: + print(f"分析 {symbol} 失败: {e}") + continue + + # 如果没有任何数据可用,显示友好提示 + if not data_available: + return render_template('index.html', symbols_data=[], data_unavailable=True, message="无法获取真实市场数据,请检查网络连接和TQSDK账号状态") + + return render_template('index.html', symbols_data=symbols_data, data_unavailable=False) + +@app.route('/symbol/') +def symbol_detail(symbol): + """品种详情页""" + try: + # 辅助函数:安全地四舍五入数字 + def safe_round(value, decimals=2, context=''): + try: + if context: + print(f"[DEBUG] safe_round called with value: {value}, type: {type(value)}, context: {context}") + if value is None: + print(f"[DEBUG] safe_round returning 0 for None value, context: {context}") + return 0 + # 检查是否为NaN + if isinstance(value, float) and value != value: # NaN检查 + print(f"[DEBUG] safe_round returning 0 for NaN value, context: {context}") + return 0 + # 检查是否为undefined + if value == 'undefined' or value == 'nan' or value == 'None': + print(f"[DEBUG] safe_round returning 0 for undefined-like value, context: {context}") + return 0 + result = round(float(value), decimals) + if context: + print(f"[DEBUG] safe_round returning {result} for value: {value}, context: {context}") + return result + except (ValueError, TypeError) as e: + print(f"[DEBUG] safe_round exception: {e}, value: {value}, type: {type(value)}, context: {context}") + return 0 + + # 获取K线数据 + kline_data = data_fetcher.get_kline_data(symbol, "1d", 200) + if kline_data is None or kline_data.empty: + return render_template('error.html', message="获取数据失败") + + # 基础数据 + try: + current_price = kline_data['close'].iloc[-1] + # 检查current_price是否为有效数字 + if current_price is None or (isinstance(current_price, float) and current_price != current_price): # NaN检查 + return render_template('error.html', message="获取数据失败:价格数据无效") + except (ValueError, TypeError, IndexError): + return render_template('error.html', message="获取数据失败:价格数据无效") + + try: + price_change = kline_data['close'].iloc[-1] - kline_data['close'].iloc[-2] + price_change_pct = (price_change / kline_data['close'].iloc[-2]) * 100 + except (ValueError, TypeError, IndexError): + price_change = 0 + price_change_pct = 0 + + # 趋势分析 + trend_analysis = trend_filter.analyze_trend(kline_data) + win_rate = trend_filter.calculate_win_rate(kline_data) + cycle = trend_filter.judge_cycle(kline_data) + + # 资金流向分析 + fund_flow_analysis = fund_flow_monitor.analyze_fund_flow(kline_data) + + # 压力支撑分析 + sr_analysis = support_resistance.analyze_support_resistance(kline_data) + support_levels = sr_analysis['support_resistance_levels']['support_levels'] + resistance_levels = sr_analysis['support_resistance_levels']['resistance_levels'] + + # 风险分析 + atr = trend_analysis.get('atr', 20) + stop_loss_long = risk_manager.calculate_stop_loss(kline_data, current_price, "long") + stop_loss_short = risk_manager.calculate_stop_loss(kline_data, current_price, "short") + + # 仓位计算 + account_balance = 1000000 + position_info = risk_manager.calculate_position_size(account_balance, kline_data, "long", current_price) + + # 换月分析 + rollover_analysis = rollover_detector.analyze_rollover(symbol, kline_data) + + # AI 分析 + market_data = { + 'symbol': symbol, + 'latest_price': current_price, + 'volume': kline_data['volume'].iloc[-1], + 'open_interest': kline_data['open_interest'].iloc[-1], + 'timeframe': '1d' + } + + technical_indicators = { + 'macd': {'signal': '金叉'}, + 'rsi': 55, + 'bollinger': {'position': '中轨附近'}, + 'kdj': {'signal': '金叉'}, + 'atr': atr + } + + # 安全获取趋势分析数据,处理可能的NaN值 + def safe_get_trend_value(key, default=0): + value = trend_analysis.get(key, default) + try: + if value is None: + return default + if isinstance(value, float) and value != value: # NaN检查 + return default + return value + except (ValueError, TypeError): + return default + + # 打印趋势分析数据 + print(f"[DEBUG] trend_analysis: {trend_analysis}") + print(f"[DEBUG] win_rate: {win_rate}, type: {type(win_rate)}") + print(f"[DEBUG] atr: {atr}, type: {type(atr)}") + print(f"[DEBUG] stop_loss_long: {stop_loss_long}, type: {type(stop_loss_long)}") + print(f"[DEBUG] stop_loss_short: {stop_loss_short}, type: {type(stop_loss_short)}") + print(f"[DEBUG] position_info: {position_info}") + + trend_data = { + 'adx': safe_get_trend_value('adx'), + 'trend_strength': safe_get_trend_value('trend_strength', 'none'), + 'trend_direction': safe_get_trend_value('trend_direction', 'neutral'), + 'ma_relationship': safe_get_trend_value('ma_relationship', 'neutral'), + 'overall_trend': safe_get_trend_value('overall_trend', 'neutral'), + 'win_rate': safe_round(win_rate, 1, context='trend_data_win_rate') if win_rate is not None else 0 + } + + # 安全获取止损和目标价格 + safe_stop_loss = safe_round(stop_loss_long, 2, context='safe_stop_loss') if stop_loss_long is not None else 0 + safe_target_price = 0 + if resistance_levels: + try: + target = resistance_levels[0] + print(f"[DEBUG] resistance_levels[0]: {target}, type: {type(target)}") + if target is not None and (not isinstance(target, float) or target == target): # 不是NaN + safe_target_price = safe_round(target, 2, context='safe_target_price_from_resistance') + else: + safe_target_price = safe_round(current_price * 1.05, 2, context='safe_target_price_from_current') + except (ValueError, TypeError, IndexError) as e: + print(f"[DEBUG] Error getting target price: {e}") + safe_target_price = safe_round(current_price * 1.05, 2, context='safe_target_price_default') + else: + safe_target_price = safe_round(current_price * 1.05, 2, context='safe_target_price_no_resistance') + + risk_metrics = { + 'stop_loss': safe_stop_loss, + 'target_price': safe_target_price, + 'profit_loss_ratio': 1.8, + 'position_size': position_info.get('suggested_units', 0), + 'risk_ratio': safe_round(position_info.get('actual_risk_percent', 0) * 100, 2, context='risk_metrics_risk_ratio') + } + + # AI 分析 + ai_analysis = deepseek_agent.analyze_market(market_data, technical_indicators, trend_data, risk_metrics) + recommendation = deepseek_agent.generate_trade_recommendation(ai_analysis) + + # 构建模板数据 + # 获取中文名称 + product_name_cn = data_fetcher.get_product_name_cn(symbol) + print(f"[DEBUG] product_name_cn: {product_name_cn}") + + # 转换周期为中文 + def cycle_to_cn(cycle): + cycle_map = { + 'short': '短期', + 'medium': '中期', + 'long': '长期', + 'bullish': '多头', + 'bearish': '空头', + 'sideways': '震荡' + } + return cycle_map.get(cycle, cycle) + + # 转换趋势方向为中文 + def direction_to_cn(direction): + direction_map = { + 'bullish': '多头', + 'bearish': '空头', + 'sideways': '震荡' + } + return direction_map.get(direction, direction) + + # 转换资金流向为中文 + def fund_flow_to_cn(fund_flow): + fund_flow_map = { + 'bullish': '多头', + 'bearish': '空头', + 'neutral': '中性', + 'Strong_bullish': '强多头', + 'Strong_bearish': '强空头', + 'strong_increasing': '强劲增加', + 'increasing': '增加', + 'strong_decreasing': '强劲减少', + 'decreasing': '减少', + 'stable': '稳定', + 'price_up_oi_up': '价涨量增', + 'price_up_oi_down': '价涨量减', + 'price_down_oi_up': '价跌量增', + 'price_down_oi_down': '价跌量减', + 'bullish_divergence': '看涨背离', + 'bearish_divergence': '看跌背离', + 'no_divergence': '无背离' + } + return fund_flow_map.get(fund_flow, fund_flow) + + print(f"[DEBUG] current_price: {current_price}, type: {type(current_price)}") + print(f"[DEBUG] price_change: {price_change}, type: {type(price_change)}") + print(f"[DEBUG] price_change_pct: {price_change_pct}, type: {type(price_change_pct)}") + print(f"[DEBUG] cycle: {cycle}, type: {type(cycle)}") + + context = { + 'symbol': symbol, + 'name': f"{product_name_cn}({symbol})".upper(), + 'current_price': safe_round(current_price, 2, context='context_current_price'), + 'price_change': safe_round(price_change, 2, context='context_price_change'), + 'price_change_pct': safe_round(price_change_pct, 2, context='context_price_change_pct'), + 'trend_analysis': trend_analysis, + 'win_rate': safe_round(win_rate, 1, context='context_win_rate'), + 'cycle': cycle_to_cn(cycle), + 'fund_flow_analysis': fund_flow_analysis, + 'sr_analysis': sr_analysis, + 'support_levels': support_levels, + 'resistance_levels': resistance_levels, + 'risk_analysis': { + 'atr': safe_round(atr, 2, context='risk_analysis_atr'), + 'stop_loss_long': safe_round(stop_loss_long, 2, context='risk_analysis_stop_loss_long'), + 'stop_loss_short': safe_round(stop_loss_short, 2, context='risk_analysis_stop_loss_short'), + 'position_size': position_info.get('suggested_units', 0), + 'risk_ratio': safe_round(position_info.get('actual_risk_percent', 0) * 100, 2, context='risk_analysis_risk_ratio'), + 'leverage': safe_round(position_info.get('leverage', 0), 2, context='risk_analysis_leverage') + }, + 'rollover_analysis': rollover_analysis, + 'ai_analysis': ai_analysis, + 'recommendation': recommendation, + 'kline_data': _get_kline_data_for_chart(kline_data) + } + print("[DEBUG] context built successfully") + + return render_template('symbol_detail.html', **context) + except Exception as e: + print(f"分析 {symbol} 详情失败: {e}") + return render_template('error.html', message=f"分析失败: {str(e)}") + +@app.route('/api/analysis/') +def api_analysis(symbol): + """API: 获取品种分析数据""" + try: + # 辅助函数:安全地四舍五入数字 + def safe_round(value, decimals=2): + try: + if value is None: + return 0 + # 检查是否为NaN + if isinstance(value, float) and value != value: # NaN检查 + return 0 + return round(float(value), decimals) + except (ValueError, TypeError): + return 0 + + # 获取K线数据 + kline_data = data_fetcher.get_kline_data(symbol, "1d", 200) + if kline_data.empty: + return jsonify({"error": "获取数据失败"}), 400 + + # 趋势分析 + trend_analysis = trend_filter.analyze_trend(kline_data) + win_rate = trend_filter.calculate_win_rate(kline_data) + cycle = trend_filter.judge_cycle(kline_data) + + # 资金流向分析 + fund_flow_analysis = fund_flow_monitor.analyze_fund_flow(kline_data) + + # 压力支撑分析 + sr_analysis = support_resistance.analyze_support_resistance(kline_data) + + # 换月分析 + rollover_analysis = rollover_detector.analyze_rollover(symbol, kline_data) + + # 价格数据 + current_price = kline_data['close'].iloc[-1] + + # 构建响应 + response = { + 'symbol': symbol, + 'current_price': safe_round(current_price, 2), + 'trend_analysis': trend_analysis, + 'win_rate': safe_round(win_rate, 1), + 'cycle': cycle, + 'fund_flow_analysis': fund_flow_analysis, + 'sr_analysis': sr_analysis, + 'rollover_analysis': rollover_analysis + } + + return jsonify(response) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route('/api/card/') +def api_card(symbol): + """API: 获取分析卡片数据""" + try: + # 辅助函数:安全地四舍五入数字 + def safe_round(value, decimals=2): + try: + if value is None: + return 0 + # 检查是否为NaN + if isinstance(value, float) and value != value: # NaN检查 + return 0 + return round(float(value), decimals) + except (ValueError, TypeError): + return 0 + + # 获取K线数据 + kline_data = data_fetcher.get_kline_data(symbol, "1d", 200) + if kline_data.empty: + return jsonify({"error": "获取数据失败"}), 400 + + # 分析数据 + trend_analysis = trend_filter.analyze_trend(kline_data) + win_rate = trend_filter.calculate_win_rate(kline_data) + fund_flow_analysis = fund_flow_monitor.analyze_fund_flow(kline_data) + sr_analysis = support_resistance.analyze_support_resistance(kline_data) + rollover_analysis = rollover_detector.analyze_rollover(symbol, kline_data) + + # AI 分析 + current_price = kline_data['close'].iloc[-1] + market_data = { + 'symbol': symbol, + 'latest_price': current_price, + 'volume': kline_data['volume'].iloc[-1], + 'open_interest': kline_data['open_interest'].iloc[-1], + 'timeframe': '1d' + } + + technical_indicators = { + 'macd': {'signal': '金叉'}, + 'rsi': 55, + 'bollinger': {'position': '中轨附近'}, + 'kdj': {'signal': '金叉'}, + 'atr': trend_analysis.get('atr', 20) + } + + # 安全获取趋势分析数据,处理可能的NaN值 + def safe_get_trend_value(key, default=0): + value = trend_analysis.get(key, default) + try: + if value is None: + return default + if isinstance(value, float) and value != value: # NaN检查 + return default + return value + except (ValueError, TypeError): + return default + + trend_data = { + 'adx': safe_get_trend_value('adx'), + 'trend_strength': safe_get_trend_value('trend_strength', 'none'), + 'trend_direction': safe_get_trend_value('trend_direction', 'neutral'), + 'ma_relationship': safe_get_trend_value('ma_relationship', 'neutral'), + 'overall_trend': safe_get_trend_value('overall_trend', 'neutral'), + 'win_rate': safe_round(win_rate, 1) if win_rate is not None else 0 + } + + # 安全获取止损和目标价格 + safe_stop_loss = 0 + try: + stop_loss = risk_manager.calculate_stop_loss(kline_data, current_price, "long") + safe_stop_loss = safe_round(stop_loss, 2) if stop_loss is not None else 0 + except Exception: + safe_stop_loss = 0 + + safe_target_price = 0 + try: + resistance_levels = sr_analysis.get('support_resistance_levels', {}).get('resistance_levels', []) + if resistance_levels: + target = resistance_levels[0] + if target is not None and (not isinstance(target, float) or target == target): # 不是NaN + safe_target_price = safe_round(target, 2) + else: + safe_target_price = safe_round(current_price * 1.05, 2) + else: + safe_target_price = safe_round(current_price * 1.05, 2) + except Exception: + safe_target_price = safe_round(current_price * 1.05, 2) + + risk_metrics = { + 'stop_loss': safe_stop_loss, + 'target_price': safe_target_price, + 'profit_loss_ratio': 1.8, + 'position_size': 2, + 'risk_ratio': 2.5 + } + + ai_analysis = deepseek_agent.analyze_market(market_data, technical_indicators, trend_data, risk_metrics) + recommendation = deepseek_agent.generate_trade_recommendation(ai_analysis) + + # 构建卡片数据 + # 获取中文名称 + product_name_cn = data_fetcher.get_product_name_cn(symbol) + + # 转换趋势方向为中文 + def direction_to_cn(direction): + direction_map = { + 'bullish': '多头', + 'bearish': '空头', + 'sideways': '震荡' + } + return direction_map.get(direction, direction) + + card_data = { + 'symbol': symbol, + 'name': f"{product_name_cn}({symbol})".upper(), + 'current_price': safe_round(current_price, 2), + 'direction': direction_to_cn(trend_analysis.get('overall_trend', 'sideways')), + 'win_rate': safe_round(win_rate, 1), + 'recommendation': recommendation, + 'technical_indicators': technical_indicators, + 'fund_flow': fund_flow_analysis, + 'rollover_warning': rollover_analysis + } + + return jsonify(card_data) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route('/card/') +def card(symbol): + """分析卡片页面""" + try: + # 获取卡片数据 + import requests + response = requests.get(f"http://localhost:5000/api/card/{symbol}") + card_data = response.json() + + if 'error' in card_data: + return render_template('error.html', message=card_data['error']) + + return render_template('card.html', **card_data) + except Exception as e: + print(f"获取卡片数据失败: {e}") + return render_template('error.html', message=f"获取卡片数据失败: {str(e)}") + +def _get_trend_strength_display(adx): + """获取趋势强度显示文本""" + try: + adx_value = float(adx) if adx is not None else 0 + if adx_value > 40: + return f"强势多头({round(adx_value, 1)})" + elif adx_value > 25: + return f"多头({round(adx_value, 1)})" + elif adx_value > 20: + return f"弱势多头({round(adx_value, 1)})" + else: + return f"震荡({round(adx_value, 1)})" + except (ValueError, TypeError): + return "震荡(0.0)" + +def _get_rollover_warning(rollover_analysis): + """获取换月预警信息""" + try: + days = rollover_analysis.get('days_to_delivery', 0) + level = rollover_analysis.get('warning_level', 'low') + + try: + days_value = int(days) if days is not None else 0 + except (ValueError, TypeError): + days_value = 0 + + if level == 'critical': + return f"⚠️ {abs(days_value)} 天" + elif level == 'high': + return f"⚠️ {days_value} 天" + elif level == 'medium': + return f"⚠️ {days_value} 天" + else: + return f"✅ {days_value} 天" + except (ValueError, TypeError): + return "✅ 0 天" + +def _get_kline_data_for_chart(kline_data): + """获取图表用的K线数据""" + try: + chart_data = [] + if kline_data is not None and not kline_data.empty: + for idx, row in kline_data.tail(30).iterrows(): + try: + chart_data.append({ + 'date': idx.strftime('%Y-%m-%d') if hasattr(idx, 'strftime') else '2023-01-01', + 'open': float(row.get('open', 0)), + 'high': float(row.get('high', 0)), + 'low': float(row.get('low', 0)), + 'close': float(row.get('close', 0)), + 'volume': float(row.get('volume', 0)) + }) + except (ValueError, TypeError): + continue + return chart_data + except (ValueError, TypeError): + return [] + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=5000) diff --git a/web/data/futures_analysis.db b/web/data/futures_analysis.db new file mode 100644 index 0000000000000000000000000000000000000000..46d935b72569c80e975b941c834522d8b6a35e16 GIT binary patch literal 28672 zcmeI&?@!Y}7zglH#zxR3_-b!_al{uu1}26VL|-|vlErPn=n{F=+}ayl=-Se|Dl_pF zD)FE7rLVmfX6bTFqLJwGZBpouyKC?BJa=a!2m38985xA}kV>*?EE%S0ydcCdjFQ?b zY9DXudAg4`b9^x6cgc9-JgQb78|BI~qx!L0zrXn4+roEsf(-%?fB*y_@IMNiKQC8m z^}6|`E@|IqF&8J!Fbud1V;&q$oL08GR>QW4-FVru$iz*ux=5~V+#}77ZS7cHve#|4 z8{I?l#yTX8p1s@bsCL>`$6hbg5Tju~^oeb~wMl1J?Y&m3SVHn46Os<2X@%S*j#(g` zDCRCB>V+p7QTO7}jdz6%BgYSg(A1PPK4LdFoIf}maH*-EfvNYGiO4!o?3^1jDw*e~ zf4^h|udLUNUdtxiy>3@oId-#c9oUWb-cPi(PY>rSwXH4lqLBqp#?)g@%-nD|WPwK| z4+EjSSS|!oTjXB}r^i)?(w=C@{#q!bQ1JAbj^H1(IdsO{Br6d4RQG0Cs3nhdfxeaj zWE|v`NCiv{xRpQ7iE}zvsVy&?7u75$PJA9PN96*|k4lB;=hyEb19cFd2CHJAPHz2>T`hPf`d??OLOPrsVIiUCQV>+nFbSWQIwBDWsL#g==W z=6=swy_^Ks$rwrEqBuhS|B32h@3*AJ41RX(3&q z_U@I_3I8xlkE_apohB@B*~IBwAyFo7t_F0-)>X~5&xTp4ZETodH?uZrK>d;6Ld`@X z@uiqNzgK8=^6niZ;jNYtsTL7WGv`Ec*r$E2%txA>nl@XxBiFx7>SE-nj5RG$?@V)5 zBvBN`+Qo$_a!FMXb=7`KhkOZDHNQYm#q!qg@_>oE!-2{2-5tNUJ zYMR+g=?`oWfB*y_009U<00Izz00bZa0SL^h0KWfcHL~az1Rwwb2tWV=5P$##AOHaf zKp+>u_kV7IBM3kM0uX=z1Rwwb2tWV=5P-m}3M{4_!{7hUYIxBv2tWV=5P$##AOHaf zKmY;|fIudI|NloKKmY;|fB*y_009U<00Izz00d@V0N?+!A7k_o0uX=z1Rwwb2tWV= K5P$##An*e)5?(C; literal 0 HcmV?d00001 diff --git a/web/templates/card.html b/web/templates/card.html new file mode 100644 index 0000000..e213a4b --- /dev/null +++ b/web/templates/card.html @@ -0,0 +1,405 @@ + + + + + + {{ name }} - 分析卡片 + + + + + + + + + \ No newline at end of file diff --git a/web/templates/error.html b/web/templates/error.html new file mode 100644 index 0000000..7906c94 --- /dev/null +++ b/web/templates/error.html @@ -0,0 +1,117 @@ + + + + + + 错误 - AI期货分析系统 + + + + +
+
+
⚠️
+

发生错误

+

{{ message }}

+ 返回首页 +
+
+ + \ No newline at end of file diff --git a/web/templates/index.html b/web/templates/index.html new file mode 100644 index 0000000..e53be2a --- /dev/null +++ b/web/templates/index.html @@ -0,0 +1,272 @@ + + + + + + AI期货分析系统 - 多品种分析面板 + + + + + +
+ +
+

AI期货分析系统

+

基于DeepSeek大模型和量化分析算法的智能决策平台

+
+ + + {% if data_unavailable %} +
+
⚠️
+

数据不可用

+

{{ message }}

+
+ 重试 + 刷新 +
+
+ {% else %} +
+ {% for symbol_data in symbols_data %} +
+
+
+
{{ symbol_data.name }}
+
{{ symbol_data.symbol }}
+
+
+
{{ symbol_data.current_price }}
+
+ {% if symbol_data.direction == 'strong_bullish' %}强多头 + {% elif symbol_data.direction == 'strong_bearish' %}强空头 + {% elif symbol_data.direction == 'weak_bullish' %}弱多头 + {% elif symbol_data.direction == 'weak_bearish' %}弱空头 + {% elif symbol_data.direction == 'bullish' %}多头 + {% elif symbol_data.direction == 'bearish' %}空头 + {% elif symbol_data.direction == 'neutral' %}中性 + {% else %}{{ symbol_data.direction | capitalize }} + {% endif %} +
+
+
+ +
+
+
胜率
+
{{ symbol_data.win_rate }}%
+
+
+
趋势强度
+
{{ symbol_data.trend_strength }}
+
+
+
周期
+
{{ symbol_data.cycle }}
+
+
+
换月预警
+
{{ symbol_data.rollover_warning }}
+
+
+
资金流向
+
{{ symbol_data.fund_flow }}
+
+
+
+ {% endfor %} +
+ {% endif %} + + + +
+ + + + \ No newline at end of file diff --git a/web/templates/symbol_detail.html b/web/templates/symbol_detail.html new file mode 100644 index 0000000..1d1bd1e --- /dev/null +++ b/web/templates/symbol_detail.html @@ -0,0 +1,630 @@ + + + + + + {{ name }} - AI期货分析系统 + + + + + +
+ +
+
+
+

{{ name }}

+

品种详细分析

+
+ 返回多品种面板 +
+
+ + +
+
+
+
{{ current_price }}
+
+ {{ price_change }} ({{ price_change_pct }}%) +
+
+
+
+
胜率
+
{{ win_rate }}%
+
+
+
趋势
+
+ {% if trend_analysis.overall_trend == 'strong_bullish' %}强多头 + {% elif trend_analysis.overall_trend == 'strong_bearish' %}强空头 + {% elif trend_analysis.overall_trend == 'weak_bullish' %}弱多头 + {% elif trend_analysis.overall_trend == 'weak_bearish' %}弱空头 + {% elif trend_analysis.overall_trend == 'bullish' %}多头 + {% elif trend_analysis.overall_trend == 'bearish' %}空头 + {% elif trend_analysis.overall_trend == 'neutral' %}中性 + {% else %}{{ trend_analysis.overall_trend | capitalize }} + {% endif %} +
+
+
+
周期
+
{{ cycle }}
+
+
+
+
+ + +
+
K线走势图
+
+
+ + +
+ +
+
趋势分析
+
+
趋势方向
+
+ {% if trend_analysis.overall_trend == 'strong_bullish' %}强多头 + {% elif trend_analysis.overall_trend == 'strong_bearish' %}强空头 + {% elif trend_analysis.overall_trend == 'weak_bullish' %}弱多头 + {% elif trend_analysis.overall_trend == 'weak_bearish' %}弱空头 + {% elif trend_analysis.overall_trend == 'bullish' %}多头 + {% elif trend_analysis.overall_trend == 'bearish' %}空头 + {% elif trend_analysis.overall_trend == 'neutral' %}中性 + {% else %}{{ trend_analysis.overall_trend | capitalize }} + {% endif %} +
+
+
+
趋势强度
+
{{ trend_analysis.trend_strength }}
+
+
+
ADX指标
+
+ {% if trend_analysis.adx is defined and trend_analysis.adx is not none %} + {{ trend_analysis.adx | round(2) }} + {% else %} + 0 + {% endif %} +
+
+
+
MA关系
+
{{ trend_analysis.ma_relationship }}
+
+
+ + +
+
风险分析
+
+
ATR
+
{{ risk_analysis.atr }}
+
+
+
多头止损
+
{{ risk_analysis.stop_loss_long }}
+
+
+
空头止损
+
{{ risk_analysis.stop_loss_short }}
+
+
+
仓位大小
+
{{ risk_analysis.position_size }}
+
+
+
风险比例
+
{{ risk_analysis.risk_ratio }}%
+
+
+ + +
+
资金流向
+
+
资金信号
+
+ {{ fund_flow_analysis.fund_signal | capitalize }} +
+
+
+
持仓变化
+
+ {% if fund_flow_analysis.open_interest_change is defined and fund_flow_analysis.open_interest_change is not none %} + {{ fund_flow_analysis.open_interest_change | round(2) }}% + {% else %} + 0% + {% endif %} +
+
+
+
量价关系
+
{{ fund_flow_analysis.volume_price_relationship }}
+
+
+ + +
+
换月分析
+
+
距离交割
+
{{ rollover_analysis.days_to_delivery }} 天
+
+
+
预警级别
+
{{ rollover_analysis.warning_level | capitalize }}
+
+
+
流动性风险
+
{{ rollover_analysis.liquidity_risk }}
+
+
+
+ + +
+
+
AI
+ AI智能分析 +
+
+
+ +
+
趋势判断
+
+
趋势方向
+
+ {{ ai_analysis.trend_judgment | default('未知') }} +
+
+
+
胜率评估
+
{{ ai_analysis.win_rate_assessment | default('未知') }}
+
+
+
风险预警
+
{{ ai_analysis.risk_warning | default('无') }}
+
+
+
交易建议
+
+ {{ ai_analysis.trade_recommendation | default('未知') }} +
+
+
+ + +
+
分析逻辑
+
+ {{ ai_analysis.analysis_logic | default('无详细分析') }} +
+
+
+
+ + +
+
详细交易建议
+
+ +
+
交易参数
+
+
交易方向
+
+ {% if recommendation.direction == 'long' %}多头 + {% elif recommendation.direction == 'short' %}空头 + {% else %}{{ recommendation.direction | default('未知') }} + {% endif %} +
+
+
+
入场价格
+
{{ recommendation.entry_price | default('未知') }}
+
+
+
止损价格
+
{{ recommendation.stop_loss | default('未知') }}
+
+
+
目标价格
+
{{ recommendation.target_price | default('未知') }}
+
+
+
仓位大小
+
{{ recommendation.position_size | default('未知') }}手
+
+
+ + +
+
执行计划
+
+ {{ recommendation.execution_plan | default('无详细计划') }} +
+
+ + +
+
风险提示
+
+ {{ recommendation.risk_tips | default('无风险提示') }} +
+
+
+
+
+ + + +
+ + + + \ No newline at end of file diff --git a/期货分析系统架构提示.ini b/期货分析系统架构提示.ini new file mode 100644 index 0000000..a6dc3f4 --- /dev/null +++ b/期货分析系统架构提示.ini @@ -0,0 +1,372 @@ +AI 期货分析系统是基于**DeepSeek 大模型**和**量化分析算法**的智能期货决策辅助系统,为期货投资者提供: + +- 📊 **多维度市场分析**(技术面、资金面、政策面) +- 🤖 **AI 智能研判**(趋势判断、胜率评估、风险预警) +- ⚡ **全自动数据更新**(每日 3 次自动复盘) +- 🛡️ **专业风控管理**(止损建议、仓位管理、换月预警) + +### 1.2 核心能力 + +| 能力维度 | 具体功能 | 技术实现 | +|---------|---------|---------| +| **趋势分析** | ADX 趋势强度、双均线过滤、多周期共振 | TrendFilter 类 | +| **风控管理** | ATR 动态仓位、技术位止损、盈亏比控制 | RiskManager 类 | +| **资金监控** | 持仓量分析、量价背离、资金流向 | FundFlowMonitor 类 | +| **压力支撑** | 关键价位识别、智能止损位、目标位测算 | SupportResistance 类 | +| **换月预警** | 交割日检测、流动性保护、自动减仓 | RolloverDetector 类 | +| **AI 研判** | DeepSeek 大模型、多维度融合、决策透明 | DeepseekAgent 类 | + +--- + +## 2. 功能架构 + +### 2.1 系统架构图 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 用户界面层 (UI) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 多品种面板 │ │ 品种详情页 │ │ 分析卡片 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 业务逻辑层 │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ generate_card│ │ deepseek_agent│ │ risk_manager│ │ +│ │ (卡片生成) │ │ (AI 分析) │ │ (风控计算) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 数据计算层 │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ fetch_market │ │ fund_flow │ │ trend_filter│ │ +│ │ (市场数据) │ │ (资金分析) │ │ (趋势计算) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 数据源层 │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 天勤 TQSDK │ │ 新闻 API │ │ 数据库 │ │ +│ │ (行情数据) │ │ (政策新闻) │ │ (SQLite) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 2.2 数据流向 + +``` +API 获取 (天勤 TQSDK) + │ + ├─→ K 线数据 (open/high/low/close/volume/close_oi) + │ + ▼ +数据清洗与计算 + │ + ├─→ 技术指标 (MACD/RSI/布林带/KDJ) + ├─→ 趋势分析 (ADX/双均线/多周期) + ├─→ 资金分析 (持仓量/量价关系) + └─→ 压力支撑 (枢轴点/近期高低点) + │ + ▼ +AI 综合研判 (DeepSeek) + │ + ├─→ 多维度数据融合 + ├─→ 趋势过滤应用 + └─→ 交易建议生成 + │ + ▼ +数据库存储 (SQLite) + │ + ▼ +前端展示 (Web 界面) +``` + +--- + +## 3. 业务逻辑 + +### 3.1 胜率计算逻辑 + +胜率是**综合评分**,由三个维度加权计算: + +``` +胜率 = 价格分位评分 × 权重 + 量价强度评分 × 权重 + 趋势强度评分 × 权重 +``` + +**震荡市(ADX<20)权重分配**: +- 价格分位:25%(降低,避免逆势抄底) +- 量价分析:60%(提高,震荡市更重要) +- 趋势强度:15% + +**趋势市(ADX≥20)权重分配**: +- 价格分位:30-60%(随 ADX 递减) +- 量价分析:40% +- 趋势调整:根据方向±(ADX-20)×0.5% + +**价格分位评分标准**: +- <20%(极低):90 分 +- <40%(相对低):75 分 +- 40-60%(中性):55 分 +- >60%(相对高):40 分 +- >80%(极高):25 分 + +### 3.2 方向判断逻辑 + +方向(做多/做空/观望)由**多因素投票**决定: + +1. **资金面信号**(1 票) + - 胜率>60% → 做多 + - 胜率<40% → 做空 + - 40-60% → 观望 + +2. **技术面信号**(1 票) + - MACD 金叉/多头排列 → 做多 + - MACD 死叉/空头排列 → 做空 + - RSI 超买且做多 → 修正为观望 + - RSI 超卖且做空 → 修正为观望 + +3. **价格位置信号**(1 票) + - 价格分位<30% → 做多 + - 价格分位>70% → 做空 + - 30-70% → 观望 + +4. **政策面信号**(1 票) + - 利好 → 做多 + - 利空 → 做空 + - 中性 → 观望 + +**最终方向**:得票最多的方向 + +**趋势过滤**:ADX<20(无趋势)时,强制改为「观望」 + +### 3.3 周期判断逻辑 + +周期(短线/中线/长线)由**多周期一致性**决定: + +```Python +if 所有周期方向一致且不为观望: + cycle = 「短线」 # 各周期共振,适合短线操作 +else: + cycle = 「中线」 # 周期分歧,中线布局 + +if 极强趋势(ADX>40 且所有周期同向): + cycle = 「长线」 # 强势趋势,长线持有 +``` +## 4. 数据指标说明 + +### 4.1 核心指标 + +#### 资金胜率 (fund_probability) + +| 范围 | 含义 | 建议 | +|------|------|------| +| 80-100% | 极高胜率 | 高置信度交易机会 | +| 60-80% | 中高胜率 | 较好的交易机会 | +| 40-60% | 中性 | 观望或轻仓试探 | +| 20-40% | 较低胜率 | 谨慎或回避 | +| 0-20% | 低胜率 | 不建议交易 | + +**计算公式**: +``` +震荡市: 价格分位×25% + 量价强度×60% + 趋势×15% +趋势市: 价格分位×(0.3-0.6) + 量价强度×40% + 趋势调整 +``` + +#### ADX 趋势强度 + +| ADX 值 | 趋势状态 | 交易策略 | +|-------|---------|---------| +| >40 | 强趋势 | 顺势交易,禁止逆势 | +| 25-40 | 中等趋势 | 顺势为主 | +| 20-25 | 弱趋势 | 谨慎参与 | +| <20 | 无趋势/震荡 | 高抛低吸或观望 | + +#### 持仓量信号 + +| 信号类型 | 持仓变化 | 价格变化 | 含义 | +|---------|---------|---------|------| +| 增仓上涨 | ↑ >5% | ↑ | 多头主力积极建仓 | +| 减仓上涨 | ↓ >5% | ↑ | 资金离场,上涨动能减弱 | +| 增仓下跌 | ↑ >5% | ↓ | 空头主力积极建仓 | +| 减仓下跌 | ↓ >5% | ↓ | 资金离场,下跌动能减弱 | +| 持仓平稳 | -5%~+5% | - | 多空双方观望 | + +### 4.2 技术指标详解 + +#### MACD (指数平滑异同平均线) + +**金叉/死叉判断**: +- 金叉:DIF 上穿 DEA → 买入信号 +- 死叉:DIF 下穿 DEA → 卖出信号 + +**多头排列**: +- MACD 柱状图>0 且持续扩大 +- DIF>DEA>0 + +#### RSI (相对强弱指标) + +| RSI 值 | 状态 | 操作建议 | +|-------|------|---------| +| >70 | 超买 | 警惕回调,考虑减仓 | +| 50-70 | 偏强 | 多头占优 | +| 30-50 | 偏弱 | 空头占优 | +| <30 | 超卖 | 警惕反弹,考虑建仓 | + +#### 布林带 (Bollinger Bands) + +| 位置 | 含义 | 策略 | +|------|------|------| +| 触及上轨 | 相对高位 | 警惕回落 | +| 中轨附近 | 均衡位置 | 观望或轻仓 | +| 触及下轨 | 相对低位 | 警惕反弹 | + +#### KDJ (随机指标) + +| 信号 | 条件 | 操作 | +|------|------|------| +| 金叉 | K 上穿 D | 买入信号 | +| 死叉 | K 下穿 D | 卖出信号 | +| 超买 | K,D,J>80 | 警惕回调 | +| 超卖 | K,D,J<20 | 警惕反弹 | + +### 4.3 风控指标 + +#### ATR (真实波动幅度) + +**止损位计算**: +``` +做多止损 = 入场价 - ATR × 倍数(通常 2 倍) +做空止损 = 入场价 + ATR × 倍数(通常 2 倍) +``` + +**仓位计算**: +``` +建议手数 = 账户资金 × 风险比例 / (ATR × 合约乘数) +``` + +#### 盈亏比 + +``` +盈亏比 = (目标价 - 入场价) / (入场价 - 止损价) + +要求: 盈亏比 >= 1.5 +``` + +**示例**: +- 入场价:3500 +- 止损价:3450(50 点) +- 目标价:3600(100 点) +- 盈亏比:100/50 = 2.0 ✅ + +--- + +## 5. 界面展示说明 + +### 5.1 多品种分析面板 + +**展示字段**: + +| 字段 | 说明 | 示例 | +|------|------|------| +| **品种名称** | 期货品种 | 沥青(BU) | +| **当前价格** | 最新价格 | 3500 | +| **趋势方向** | AI 建议方向 | 观望/做多/做空 | +| **胜率** | 综合胜率 | 75% | +| **趋势强度** | ADX 评分 | 强势多头(85) | +| **周期** | 建议周期 | 短线/中线 | +| **换月预警** | 距离交割天数 | ⚠️ 12 天 | +| **主力资金** | 资金流向 | 增仓上涨 | + +**颜色标识**: +- 🟢 看多/强烈看多 +- 🔴 看空/强烈看空 +- ⚪ 观望/中性 + +### 5.2 品种详情页 + +**模块 1: 市场概况** +- 当前价格、涨跌幅 +- 价格分位(近一年) +- 波动率(ATR 比率) + +**模块 2: 技术指标** +- MACD 信号(金叉/死叉/多头排列) +- RSI 值(超买/超卖/中性) +- 布林带位置(上轨/中轨/下轨) +- KDJ 信号(金叉/死叉) + +**模块 3: 趋势分析** +- ADX 趋势强度 +- 多周期方向(5min/30min/1hour/1day) +- 多周期一致性说明 + +**模块 4: 压力支撑** +- 最近支撑位(距离) +- 最近阻力位(距离) +- 智能止损位建议 +- 目标位测算 + +**模块 5: 主力资金** +- 持仓量变化(百分比) +- 价格变化(5 日) +- 资金信号解读 +- 综合评分 + +**模块 6: AI 交易建议** +- 方向:做多/做空/观望 +- 周期:短线/中线/长线 +- 入场价、止损价、目标价 +- 信心等级:高/中/低 +- 详细理由说明 + +**模块 7: 风控建议** +- 建议手数 +- 每手保证金 +- 最大仓位限制 +- 单日最大亏损限制 +- 风险回报比 + +**模块 8: 相关新闻** +- 最新政策新闻列表 +- 新闻情绪(积极/中性/消极) +- 新闻摘要 + +### 5.3 分析卡片文本 + +**格式示例**: +``` +┌─────────────────────────────────────┐ +│ 📊 沥青(BU) 分析卡片 │ +├─────────────────────────────────────┤ +│ 当前价格: 3500 │ +│ 趋势方向: 观望 | 周期: 短线 │ +│ 资金胜率: 75% │ +├─────────────────────────────────────┤ +│ 🎯 交易建议 │ +│ • 方向: 观望 │ +│ • 入场: 3450 │ +│ • 止损: 3400 (2 倍 ATR) │ +│ • 目标: 3600 (盈亏比 2.0) │ +│ • 信心: 中 │ +├─────────────────────────────────────┤ +│ 📈 技术指标 │ +│ • MACD: 多头排列 │ +│ • RSI: 60(中性) │ +│ • 布林带: 中轨区域 │ +├─────────────────────────────────────┤ +│ 💰 主力资金 │ +│ • 持仓大增 10% │ +│ • 价格上升 5% │ +│ • 多头主力积极建仓 │ +├─────────────────────────────────────┤ +│ ⚠️ 风险提示 │ +│ • 换月期预警: 距离交割 12 天 │ +│ • 多周期分歧: 仅日线看多 │ +└─────────────────────────────────────┘ +``` \ No newline at end of file
+ + + + +
+
+
+
{{ name }}
+
{{ symbol }}
+
+
+
{{ current_price }}
+
+ {% if direction == 'strong_bullish' %}强多头 + {% elif direction == 'strong_bearish' %}强空头 + {% elif direction == 'weak_bullish' %}弱多头 + {% elif direction == 'weak_bearish' %}弱空头 + {% elif direction == 'bullish' %}多头 + {% elif direction == 'bearish' %}空头 + {% elif direction == 'neutral' %}中性 + {% else %}{{ direction | capitalize }} + {% endif %} +
+
+
+ +
+ +
+
核心指标
+
+
+
胜率
+
{{ win_rate }}%
+
+
+
资金流向
+
{{ fund_flow.fund_signal | capitalize }}
+
+
+
换月预警
+
{{ rollover_warning.warning_level | capitalize }}
+
+
+
+ + +
+
技术指标
+
+
+
MACD
+
{{ technical_indicators.macd.signal }}
+
+
+
RSI
+
{{ technical_indicators.rsi }}
+
+
+
布林带
+
{{ technical_indicators.bollinger.position }}
+
+
+
KDJ
+
{{ technical_indicators.kdj.signal }}
+
+
+
ATR
+
{{ technical_indicators.atr | round(2) }}
+
+
+
+ + +
+
AI交易建议
+
+
建议
+
{{ recommendation }}
+
+
+
+ + +
+ + + +