parent
6d2cb15bfc
commit
90d9ab53ad
@ -0,0 +1,93 @@
|
|||||||
|
{
|
||||||
|
"database": {
|
||||||
|
"mongoDB": {
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 10000,
|
||||||
|
"database": "aaa",
|
||||||
|
"username": "aaa",
|
||||||
|
"password": "aaaa",
|
||||||
|
"authSource": "aaa",
|
||||||
|
"ssl": false,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"postgreSQL": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 5432,
|
||||||
|
"database": "alpha-futures",
|
||||||
|
"username": "postgres",
|
||||||
|
"password": "password",
|
||||||
|
"ssl": false,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"redis": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 6379,
|
||||||
|
"password": "",
|
||||||
|
"db": 0,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"influxDB": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 8086,
|
||||||
|
"database": "alpha-futures",
|
||||||
|
"username": "",
|
||||||
|
"password": "",
|
||||||
|
"ssl": false,
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"port": 3007,
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"environment": "development",
|
||||||
|
"debug": true,
|
||||||
|
"timeout": 30000,
|
||||||
|
"maxBodySize": "10mb"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"jwtSecret": "your-secret-key",
|
||||||
|
"jwtExpiresIn": "7d",
|
||||||
|
"rateLimit": {
|
||||||
|
"windowMs": 60000,
|
||||||
|
"max": 120
|
||||||
|
},
|
||||||
|
"cors": {
|
||||||
|
"origin": "*",
|
||||||
|
"methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||||
|
"allowedHeaders": "Content-Type, Authorization"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dataSource": {
|
||||||
|
"test": {
|
||||||
|
"enabled": false,
|
||||||
|
"timeout": 10000,
|
||||||
|
"retries": 3,
|
||||||
|
"refreshInterval": 60000
|
||||||
|
},
|
||||||
|
"tqsdk": {
|
||||||
|
"enabled": true,
|
||||||
|
"username": "windsdreamer",
|
||||||
|
"password": "1qazse42W3",
|
||||||
|
"pythonPort": 8001 ,
|
||||||
|
"timeout": 10000,
|
||||||
|
"retries": 10,
|
||||||
|
"maxConnections": 20
|
||||||
|
},
|
||||||
|
"wind": {
|
||||||
|
"enabled": false,
|
||||||
|
"apiKey": "",
|
||||||
|
"apiSecret": "",
|
||||||
|
"url": "https://api.wind.com.cn",
|
||||||
|
"timeout": 30000,
|
||||||
|
"retries": 3
|
||||||
|
},
|
||||||
|
"sina": {
|
||||||
|
"enabled": false,
|
||||||
|
"url": "https://finance.sina.com.cn",
|
||||||
|
"timeout": 10000,
|
||||||
|
"retries": 3,
|
||||||
|
"refreshInterval": 60000
|
||||||
|
},
|
||||||
|
"defaultDataSource": "tqsdk"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@ -0,0 +1,3 @@
|
|||||||
|
# 期货分析系统版本信息
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
__author__ = "AI Futures Analyzer"
|
||||||
Binary file not shown.
Binary file not shown.
@ -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
|
||||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1,12 @@
|
|||||||
|
# API适配器包初始化文件
|
||||||
|
from qihuo_analyzer.data.api_adapters.base_adapter import BaseDataAdapter
|
||||||
|
from qihuo_analyzer.data.api_adapters.tqsdk_adapter import TqSdkAdapter
|
||||||
|
from qihuo_analyzer.data.api_adapters.rqdata_adapter import RqDataAdapter
|
||||||
|
from qihuo_analyzer.data.api_adapters.adapter_factory import DataAdapterFactory
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'BaseDataAdapter',
|
||||||
|
'TqSdkAdapter',
|
||||||
|
'RqDataAdapter',
|
||||||
|
'DataAdapterFactory'
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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'")
|
||||||
@ -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'
|
||||||
Binary file not shown.
Binary file not shown.
@ -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()
|
||||||
@ -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
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
# Service dependencies
|
||||||
|
Flask==2.0.1
|
||||||
|
pandas==1.3.3
|
||||||
|
python-dotenv==0.19.0
|
||||||
@ -0,0 +1 @@
|
|||||||
|
# Service module initialization
|
||||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1,226 @@
|
|||||||
|
# Service main application
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
# 添加项目根目录到 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.deepseek_agent import DeepseekAgent
|
||||||
|
from qihuo_analyzer.utils.config_manager import config_manager
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# 初始化组件
|
||||||
|
data_fetcher = DataFetcher()
|
||||||
|
data_storage = DataStorage()
|
||||||
|
deepseek_agent = DeepseekAgent()
|
||||||
|
|
||||||
|
# 连接 API
|
||||||
|
print("正在连接 API...")
|
||||||
|
connect_success = data_fetcher.connect()
|
||||||
|
if connect_success:
|
||||||
|
print("API 连接成功,可以获取真实数据")
|
||||||
|
else:
|
||||||
|
print("API 连接失败,将使用模拟数据")
|
||||||
|
|
||||||
|
# 健康检查接口
|
||||||
|
@app.route('/health', methods=['GET'])
|
||||||
|
def health_check():
|
||||||
|
return jsonify({'status': 'ok', 'message': 'Service is running'})
|
||||||
|
|
||||||
|
# 合约数据获取接口
|
||||||
|
@app.route('/api/contracts', methods=['GET'])
|
||||||
|
def get_contracts():
|
||||||
|
try:
|
||||||
|
exchange = request.args.get('exchange', '')
|
||||||
|
symbol = request.args.get('symbol', '')
|
||||||
|
|
||||||
|
contracts = data_fetcher.get_contracts(exchange=exchange, symbol=symbol)
|
||||||
|
return jsonify({'status': 'success', 'data': contracts})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||||
|
|
||||||
|
# K线数据获取接口
|
||||||
|
@app.route('/api/kline', methods=['GET'])
|
||||||
|
def get_kline():
|
||||||
|
try:
|
||||||
|
symbol = request.args.get('symbol', '')
|
||||||
|
duration = request.args.get('duration', '1m')
|
||||||
|
limit = int(request.args.get('limit', 100))
|
||||||
|
|
||||||
|
if not symbol:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Symbol is required'}), 400
|
||||||
|
|
||||||
|
# 尝试从数据库获取,如果没有则从数据源获取
|
||||||
|
df = data_storage.get_kline_data(symbol, duration, limit)
|
||||||
|
|
||||||
|
if df.empty:
|
||||||
|
# 从数据源获取
|
||||||
|
df = data_fetcher.get_kline_data(symbol, duration, limit)
|
||||||
|
# 保存到数据库
|
||||||
|
data_storage.save_kline_data(symbol, duration, df)
|
||||||
|
|
||||||
|
# 转换为字典格式
|
||||||
|
kline_data = []
|
||||||
|
for idx, row in df.iterrows():
|
||||||
|
kline_data.append({
|
||||||
|
'datetime': idx.isoformat(),
|
||||||
|
'open': float(row['open']),
|
||||||
|
'high': float(row['high']),
|
||||||
|
'low': float(row['low']),
|
||||||
|
'close': float(row['close']),
|
||||||
|
'volume': int(row['volume']),
|
||||||
|
'open_interest': int(row['open_interest'])
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify({'status': 'success', 'data': kline_data})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||||
|
|
||||||
|
# DeepSeek 分析接口
|
||||||
|
@app.route('/api/analyze', methods=['POST'])
|
||||||
|
def analyze():
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
symbol = data.get('symbol', '')
|
||||||
|
duration = data.get('duration', '1m')
|
||||||
|
analysis_type = data.get('analysis_type', 'technical')
|
||||||
|
|
||||||
|
if not symbol:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Symbol is required'}), 400
|
||||||
|
|
||||||
|
# 获取K线数据
|
||||||
|
df = data_fetcher.get_kline_data(symbol, duration, 1000)
|
||||||
|
|
||||||
|
# 保存到数据库
|
||||||
|
data_storage.save_kline_data(symbol, duration, df)
|
||||||
|
|
||||||
|
# 执行分析
|
||||||
|
analysis_result = deepseek_agent.analyze_market(symbol, df)
|
||||||
|
|
||||||
|
# 保存分析结果
|
||||||
|
data_storage.save_analysis_result(analysis_result)
|
||||||
|
|
||||||
|
return jsonify({'status': 'success', 'data': analysis_result})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||||
|
|
||||||
|
# 交易建议接口
|
||||||
|
@app.route('/api/recommendations', methods=['GET'])
|
||||||
|
def get_recommendations():
|
||||||
|
try:
|
||||||
|
symbol = request.args.get('symbol', '')
|
||||||
|
status = request.args.get('status', '')
|
||||||
|
|
||||||
|
if not symbol:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Symbol is required'}), 400
|
||||||
|
|
||||||
|
df = data_storage.get_trade_recommendations(symbol, status)
|
||||||
|
|
||||||
|
# 转换为字典格式
|
||||||
|
recommendations = []
|
||||||
|
for _, row in df.iterrows():
|
||||||
|
recommendations.append({
|
||||||
|
'id': int(row['id']),
|
||||||
|
'symbol': row['symbol'],
|
||||||
|
'timestamp': row['timestamp'],
|
||||||
|
'direction': row['direction'],
|
||||||
|
'entry_price': float(row['entry_price']) if not pd.isna(row['entry_price']) else None,
|
||||||
|
'stop_loss': float(row['stop_loss']) if not pd.isna(row['stop_loss']) else None,
|
||||||
|
'target_price': float(row['target_price']) if not pd.isna(row['target_price']) else None,
|
||||||
|
'position_size': float(row['position_size']) if not pd.isna(row['position_size']) else None,
|
||||||
|
'execution_plan': row['execution_plan'],
|
||||||
|
'risk_tips': row['risk_tips'],
|
||||||
|
'status': row['status'],
|
||||||
|
'created_at': row['created_at']
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify({'status': 'success', 'data': recommendations})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||||
|
|
||||||
|
# 风险监控接口
|
||||||
|
@app.route('/api/risk', methods=['POST'])
|
||||||
|
def monitor_risk():
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
symbol = data.get('symbol', '')
|
||||||
|
current_price = data.get('current_price', 0)
|
||||||
|
entry_price = data.get('entry_price', 0)
|
||||||
|
stop_loss = data.get('stop_loss', 0)
|
||||||
|
target_price = data.get('target_price', 0)
|
||||||
|
|
||||||
|
if not symbol:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Symbol is required'}), 400
|
||||||
|
|
||||||
|
# 计算当前利润
|
||||||
|
current_profit = current_price - entry_price
|
||||||
|
|
||||||
|
# 评估风险状态
|
||||||
|
risk_status = 'normal'
|
||||||
|
if abs(current_profit) > (entry_price * 0.05):
|
||||||
|
risk_status = 'high'
|
||||||
|
|
||||||
|
# 保存风险监控数据
|
||||||
|
risk_data = {
|
||||||
|
'symbol': symbol,
|
||||||
|
'current_price': current_price,
|
||||||
|
'entry_price': entry_price,
|
||||||
|
'stop_loss': stop_loss,
|
||||||
|
'target_price': target_price,
|
||||||
|
'current_profit': current_profit,
|
||||||
|
'risk_status': risk_status
|
||||||
|
}
|
||||||
|
|
||||||
|
data_storage.save_risk_monitoring(risk_data)
|
||||||
|
|
||||||
|
return jsonify({'status': 'success', 'data': risk_data})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||||
|
|
||||||
|
# 分析历史接口
|
||||||
|
@app.route('/api/analysis/history', methods=['GET'])
|
||||||
|
def get_analysis_history():
|
||||||
|
try:
|
||||||
|
symbol = request.args.get('symbol', '')
|
||||||
|
limit = int(request.args.get('limit', 100))
|
||||||
|
|
||||||
|
if not symbol:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Symbol is required'}), 400
|
||||||
|
|
||||||
|
df = data_storage.get_analysis_results(symbol, limit)
|
||||||
|
|
||||||
|
# 转换为字典格式
|
||||||
|
history = []
|
||||||
|
for _, row in df.iterrows():
|
||||||
|
history.append({
|
||||||
|
'id': int(row['id']),
|
||||||
|
'symbol': row['symbol'],
|
||||||
|
'timestamp': row['timestamp'],
|
||||||
|
'trend': row['trend'],
|
||||||
|
'probability': float(row['probability']) if not pd.isna(row['probability']) else None,
|
||||||
|
'direction': row['direction'],
|
||||||
|
'cycle': row['cycle'],
|
||||||
|
'atr': float(row['atr']) if not pd.isna(row['atr']) else None,
|
||||||
|
'adx': float(row['adx']) if not pd.isna(row['adx']) else None,
|
||||||
|
'support': float(row['support']) if not pd.isna(row['support']) else None,
|
||||||
|
'resistance': float(row['resistance']) if not pd.isna(row['resistance']) else None,
|
||||||
|
'stop_loss': float(row['stop_loss']) if not pd.isna(row['stop_loss']) else None,
|
||||||
|
'target_price': float(row['target_price']) if not pd.isna(row['target_price']) else None,
|
||||||
|
'position_size': float(row['position_size']) if not pd.isna(row['position_size']) else None,
|
||||||
|
'risk_ratio': float(row['risk_ratio']) if not pd.isna(row['risk_ratio']) else None,
|
||||||
|
'fund_flow': row['fund_flow'],
|
||||||
|
'signals': row['signals'],
|
||||||
|
'created_at': row['created_at']
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify({'status': 'success', 'data': history})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||||
Binary file not shown.
@ -0,0 +1,183 @@
|
|||||||
|
# Service API tests
|
||||||
|
import unittest
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
# 添加项目根目录到 Python 路径
|
||||||
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
# 直接导入 app 模块
|
||||||
|
from service.app import app
|
||||||
|
|
||||||
|
class ServiceAPITest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
# 创建测试客户端
|
||||||
|
self.client = app.test_client()
|
||||||
|
self.client.testing = True
|
||||||
|
|
||||||
|
@patch('service.app.DataFetcher')
|
||||||
|
def test_health_check(self, mock_data_fetcher):
|
||||||
|
"""测试健康检查接口"""
|
||||||
|
response = self.client.get('/health')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = json.loads(response.data)
|
||||||
|
self.assertEqual(data['status'], 'ok')
|
||||||
|
self.assertEqual(data['message'], 'Service is running')
|
||||||
|
|
||||||
|
@patch('service.app.DataFetcher')
|
||||||
|
def test_get_contracts(self, mock_data_fetcher):
|
||||||
|
"""测试合约数据获取接口"""
|
||||||
|
# 配置 mock
|
||||||
|
mock_fetcher_instance = MagicMock()
|
||||||
|
mock_data_fetcher.return_value = mock_fetcher_instance
|
||||||
|
|
||||||
|
# 模拟 get_contracts 方法
|
||||||
|
mock_fetcher_instance.get_contracts.return_value = [
|
||||||
|
{'symbol': 'CU2603', 'product': 'CU', 'product_name': '铜', 'exchange': 'SHFE', 'month': '2603'},
|
||||||
|
{'symbol': 'AL2603', 'product': 'AL', 'product_name': '铝', 'exchange': 'SHFE', 'month': '2603'}
|
||||||
|
]
|
||||||
|
|
||||||
|
# 测试获取所有合约
|
||||||
|
response = self.client.get('/api/contracts')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = json.loads(response.data)
|
||||||
|
self.assertEqual(data['status'], 'success')
|
||||||
|
self.assertIsInstance(data['data'], list)
|
||||||
|
self.assertGreater(len(data['data']), 0)
|
||||||
|
|
||||||
|
@patch('service.app.DataStorage')
|
||||||
|
@patch('service.app.DataFetcher')
|
||||||
|
def test_get_kline(self, mock_data_fetcher, mock_data_storage):
|
||||||
|
"""测试K线数据获取接口"""
|
||||||
|
# 配置 mock
|
||||||
|
mock_fetcher_instance = MagicMock()
|
||||||
|
mock_data_fetcher.return_value = mock_fetcher_instance
|
||||||
|
|
||||||
|
mock_storage_instance = MagicMock()
|
||||||
|
mock_data_storage.return_value = mock_storage_instance
|
||||||
|
|
||||||
|
# 模拟数据
|
||||||
|
mock_df = MagicMock()
|
||||||
|
mock_df.empty = False
|
||||||
|
mock_df.iterrows.return_value = [(MagicMock(isoformat=lambda: '2026-02-22T00:00:00'), \
|
||||||
|
{'open': 35000, 'high': 35100, 'low': 34900, 'close': 35050, 'volume': 1000, 'open_interest': 10000})]
|
||||||
|
|
||||||
|
mock_storage_instance.get_kline_data.return_value = mock_df
|
||||||
|
mock_fetcher_instance.get_kline_data.return_value = mock_df
|
||||||
|
mock_storage_instance.save_kline_data.return_value = True
|
||||||
|
|
||||||
|
# 测试获取K线数据
|
||||||
|
response = self.client.get('/api/kline?symbol=CU2603&duration=1m&limit=10')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = json.loads(response.data)
|
||||||
|
self.assertEqual(data['status'], 'success')
|
||||||
|
self.assertIsInstance(data['data'], list)
|
||||||
|
self.assertGreater(len(data['data']), 0)
|
||||||
|
|
||||||
|
@patch('service.app.DataStorage')
|
||||||
|
@patch('service.app.DeepseekAgent')
|
||||||
|
@patch('service.app.DataFetcher')
|
||||||
|
def test_analyze(self, mock_data_fetcher, mock_deepseek_agent, mock_data_storage):
|
||||||
|
"""测试DeepSeek分析接口"""
|
||||||
|
# 配置 mock
|
||||||
|
mock_fetcher_instance = MagicMock()
|
||||||
|
mock_data_fetcher.return_value = mock_fetcher_instance
|
||||||
|
|
||||||
|
mock_agent_instance = MagicMock()
|
||||||
|
mock_deepseek_agent.return_value = mock_agent_instance
|
||||||
|
|
||||||
|
mock_storage_instance = MagicMock()
|
||||||
|
mock_data_storage.return_value = mock_storage_instance
|
||||||
|
|
||||||
|
# 模拟数据
|
||||||
|
mock_df = MagicMock()
|
||||||
|
mock_df.empty = False
|
||||||
|
mock_fetcher_instance.get_kline_data.return_value = mock_df
|
||||||
|
|
||||||
|
# 模拟分析结果
|
||||||
|
mock_agent_instance.analyze_market.return_value = {
|
||||||
|
'symbol': 'CU2603',
|
||||||
|
'timestamp': '2026-02-22T00:00:00',
|
||||||
|
'trend': 'up',
|
||||||
|
'probability': 0.8,
|
||||||
|
'direction': 'buy'
|
||||||
|
}
|
||||||
|
|
||||||
|
mock_storage_instance.save_kline_data.return_value = True
|
||||||
|
mock_storage_instance.save_analysis_result.return_value = True
|
||||||
|
|
||||||
|
# 测试分析接口
|
||||||
|
test_data = {
|
||||||
|
'symbol': 'CU2603',
|
||||||
|
'duration': '1m',
|
||||||
|
'analysis_type': 'technical'
|
||||||
|
}
|
||||||
|
response = self.client.post('/api/analyze', json=test_data)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = json.loads(response.data)
|
||||||
|
self.assertEqual(data['status'], 'success')
|
||||||
|
self.assertIn('data', data)
|
||||||
|
|
||||||
|
@patch('service.app.DataStorage')
|
||||||
|
def test_get_recommendations(self, mock_data_storage):
|
||||||
|
"""测试交易建议接口"""
|
||||||
|
# 配置 mock
|
||||||
|
mock_storage_instance = MagicMock()
|
||||||
|
mock_data_storage.return_value = mock_storage_instance
|
||||||
|
|
||||||
|
# 模拟数据
|
||||||
|
mock_df = MagicMock()
|
||||||
|
mock_storage_instance.get_trade_recommendations.return_value = mock_df
|
||||||
|
|
||||||
|
# 测试获取交易建议
|
||||||
|
response = self.client.get('/api/recommendations?symbol=CU2603')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = json.loads(response.data)
|
||||||
|
self.assertEqual(data['status'], 'success')
|
||||||
|
self.assertIsInstance(data['data'], list)
|
||||||
|
|
||||||
|
@patch('service.app.DataStorage')
|
||||||
|
def test_monitor_risk(self, mock_data_storage):
|
||||||
|
"""测试风险监控接口"""
|
||||||
|
# 配置 mock
|
||||||
|
mock_storage_instance = MagicMock()
|
||||||
|
mock_data_storage.return_value = mock_storage_instance
|
||||||
|
mock_storage_instance.save_risk_monitoring.return_value = True
|
||||||
|
|
||||||
|
# 测试风险监控
|
||||||
|
test_data = {
|
||||||
|
'symbol': 'CU2603',
|
||||||
|
'current_price': 36000,
|
||||||
|
'entry_price': 35000,
|
||||||
|
'stop_loss': 34500,
|
||||||
|
'target_price': 37000
|
||||||
|
}
|
||||||
|
response = self.client.post('/api/risk', json=test_data)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = json.loads(response.data)
|
||||||
|
self.assertEqual(data['status'], 'success')
|
||||||
|
self.assertIn('data', data)
|
||||||
|
self.assertEqual(data['data']['symbol'], 'CU2603')
|
||||||
|
|
||||||
|
@patch('service.app.DataStorage')
|
||||||
|
def test_get_analysis_history(self, mock_data_storage):
|
||||||
|
"""测试分析历史接口"""
|
||||||
|
# 配置 mock
|
||||||
|
mock_storage_instance = MagicMock()
|
||||||
|
mock_data_storage.return_value = mock_storage_instance
|
||||||
|
|
||||||
|
# 模拟数据
|
||||||
|
mock_df = MagicMock()
|
||||||
|
mock_storage_instance.get_analysis_results.return_value = mock_df
|
||||||
|
|
||||||
|
# 测试获取分析历史
|
||||||
|
response = self.client.get('/api/analysis/history?symbol=CU2603&limit=10')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = json.loads(response.data)
|
||||||
|
self.assertEqual(data['status'], 'success')
|
||||||
|
self.assertIsInstance(data['data'], list)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Loading…
Reference in new issue