commit
b167d953db
@ -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
|
||||
@ -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
|
||||
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.
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,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
|
||||
@ -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()
|
||||
Binary file not shown.
@ -0,0 +1,405 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ name }} - 分析卡片</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/antd@5.12.8/dist/reset.css" rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #f0f2f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
padding: 6px 12px;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.symbol-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.symbol-name {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.symbol-code {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.price-info {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.current-price {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.direction-indicator {
|
||||
display: inline-block;
|
||||
padding: 6px 16px;
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.direction-up {
|
||||
background-color: #f6ffed;
|
||||
color: #52c41a;
|
||||
border: 1px solid #b7eb8f;
|
||||
}
|
||||
|
||||
.direction-down {
|
||||
background-color: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
border: 1px solid #ffccc7;
|
||||
}
|
||||
|
||||
.direction-sideways {
|
||||
background-color: #f0f5ff;
|
||||
color: #1890ff;
|
||||
border: 1px solid #adc6ff;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section-title::before {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background-color: #f9fafb;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.win-rate {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.fund-flow {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.rollover-warning {
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
.indicator-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.indicator-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
background-color: #f9fafb;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.indicator-name {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.indicator-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.recommendation {
|
||||
background-color: #f0f5ff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
border-left: 4px solid #1890ff;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.recommendation-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1890ff;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.recommendation-content {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
text-align: center;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-link {
|
||||
color: #1890ff;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.detail-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.price-info {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.indicator-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- 头部 -->
|
||||
<div class="header">
|
||||
<div class="header-content">
|
||||
<h1>分析卡片</h1>
|
||||
<a href="/" class="back-link">返回多品种面板</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分析卡片 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="symbol-info">
|
||||
<div class="symbol-name">{{ name }}</div>
|
||||
<div class="symbol-code">{{ symbol }}</div>
|
||||
</div>
|
||||
<div class="price-info">
|
||||
<div class="current-price">{{ current_price }}</div>
|
||||
<div class="direction-indicator
|
||||
{% if direction == 'bullish' or direction == 'strong_bullish' or direction == 'weak_bullish' %}direction-up{% elif direction == 'bearish' or direction == 'strong_bearish' or direction == 'weak_bearish' %}direction-down{% else %}direction-sideways{% endif %}">
|
||||
{% 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 %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- 核心指标 -->
|
||||
<div class="section">
|
||||
<div class="section-title">核心指标</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">胜率</div>
|
||||
<div class="stat-value win-rate">{{ win_rate }}%</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">资金流向</div>
|
||||
<div class="stat-value fund-flow">{{ fund_flow.fund_signal | capitalize }}</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">换月预警</div>
|
||||
<div class="stat-value rollover-warning">{{ rollover_warning.warning_level | capitalize }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 技术指标 -->
|
||||
<div class="section">
|
||||
<div class="section-title">技术指标</div>
|
||||
<div class="indicator-list">
|
||||
<div class="indicator-item">
|
||||
<div class="indicator-name">MACD</div>
|
||||
<div class="indicator-value">{{ technical_indicators.macd.signal }}</div>
|
||||
</div>
|
||||
<div class="indicator-item">
|
||||
<div class="indicator-name">RSI</div>
|
||||
<div class="indicator-value">{{ technical_indicators.rsi }}</div>
|
||||
</div>
|
||||
<div class="indicator-item">
|
||||
<div class="indicator-name">布林带</div>
|
||||
<div class="indicator-value">{{ technical_indicators.bollinger.position }}</div>
|
||||
</div>
|
||||
<div class="indicator-item">
|
||||
<div class="indicator-name">KDJ</div>
|
||||
<div class="indicator-value">{{ technical_indicators.kdj.signal }}</div>
|
||||
</div>
|
||||
<div class="indicator-item">
|
||||
<div class="indicator-name">ATR</div>
|
||||
<div class="indicator-value">{{ technical_indicators.atr | round(2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI 建议 -->
|
||||
<div class="section">
|
||||
<div class="section-title">AI交易建议</div>
|
||||
<div class="recommendation">
|
||||
<div class="recommendation-title">建议</div>
|
||||
<div class="recommendation-content">{{ recommendation }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<a href="/symbol/{{ symbol }}" class="detail-link">查看详细分析 →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部 -->
|
||||
<div class="footer">
|
||||
<p>© 2024 AI期货分析系统 | 数据更新时间: {{ now() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 获取当前时间
|
||||
function now() {
|
||||
const date = new Date();
|
||||
return date.toLocaleString('zh-CN');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,117 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>错误 - AI期货分析系统</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/antd@5.12.8/dist/reset.css" rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #f0f2f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.error-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-color: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40px;
|
||||
margin: 0 auto 24px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 32px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: inline-block;
|
||||
padding: 10px 24px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.error-card {
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="error-card">
|
||||
<div class="error-icon">⚠️</div>
|
||||
<h1 class="error-title">发生错误</h1>
|
||||
<p class="error-message">{{ message }}</p>
|
||||
<a href="/" class="back-button">返回首页</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,272 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI期货分析系统 - 多品种分析面板</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/antd@5.12.8/dist/reset.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #f0f2f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.panel-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.symbol-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.symbol-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.symbol-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.symbol-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.symbol-code {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.price-info {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.current-price {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.direction-indicator {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.direction-up {
|
||||
background-color: #f6ffed;
|
||||
color: #52c41a;
|
||||
border: 1px solid #b7eb8f;
|
||||
}
|
||||
|
||||
.direction-down {
|
||||
background-color: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
border: 1px solid #ffccc7;
|
||||
}
|
||||
|
||||
.direction-sideways {
|
||||
background-color: #f0f5ff;
|
||||
color: #1890ff;
|
||||
border: 1px solid #adc6ff;
|
||||
}
|
||||
|
||||
.analysis-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.analysis-item {
|
||||
background-color: #f9fafb;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.analysis-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.analysis-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.win-rate {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.rollover-warning {
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
.fund-flow {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.panel-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.analysis-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- 头部 -->
|
||||
<div class="header">
|
||||
<h1>AI期货分析系统</h1>
|
||||
<p>基于DeepSeek大模型和量化分析算法的智能决策平台</p>
|
||||
</div>
|
||||
|
||||
<!-- 多品种分析面板 -->
|
||||
{% if data_unavailable %}
|
||||
<div style="text-align: center; padding: 60px; background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);">
|
||||
<div style="font-size: 48px; margin-bottom: 20px;">⚠️</div>
|
||||
<h2 style="font-size: 24px; font-weight: 600; color: #333; margin-bottom: 16px;">数据不可用</h2>
|
||||
<p style="font-size: 16px; color: #666; margin-bottom: 32px;">{{ message }}</p>
|
||||
<div style="display: flex; justify-content: center; gap: 16px;">
|
||||
<a href="/" style="padding: 10px 24px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; text-decoration: none; border-radius: 6px; font-size: 14px; font-weight: 500;">重试</a>
|
||||
<a href="javascript:location.reload()" style="padding: 10px 24px; background: #f0f0f0; color: #333; text-decoration: none; border-radius: 6px; font-size: 14px; font-weight: 500;">刷新</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="panel-grid">
|
||||
{% for symbol_data in symbols_data %}
|
||||
<div class="symbol-card" onclick="window.location.href='/symbol/{{ symbol_data.symbol }}'">
|
||||
<div class="symbol-header">
|
||||
<div>
|
||||
<div class="symbol-name">{{ symbol_data.name }}</div>
|
||||
<div class="symbol-code">{{ symbol_data.symbol }}</div>
|
||||
</div>
|
||||
<div class="price-info">
|
||||
<div class="current-price">{{ symbol_data.current_price }}</div>
|
||||
<div class="direction-indicator
|
||||
{% if symbol_data.direction == 'bullish' or symbol_data.direction == 'strong_bullish' or symbol_data.direction == 'weak_bullish' %}direction-up{% elif symbol_data.direction == 'bearish' or symbol_data.direction == 'strong_bearish' or symbol_data.direction == 'weak_bearish' %}direction-down{% else %}direction-sideways{% endif %}">
|
||||
{% 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 %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="analysis-grid">
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">胜率</div>
|
||||
<div class="analysis-value win-rate">{{ symbol_data.win_rate }}%</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">趋势强度</div>
|
||||
<div class="analysis-value">{{ symbol_data.trend_strength }}</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">周期</div>
|
||||
<div class="analysis-value">{{ symbol_data.cycle }}</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">换月预警</div>
|
||||
<div class="analysis-value rollover-warning">{{ symbol_data.rollover_warning }}</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">资金流向</div>
|
||||
<div class="analysis-value fund-flow">{{ symbol_data.fund_flow }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 底部 -->
|
||||
<div class="footer">
|
||||
<p>© 2024 AI期货分析系统 | 数据更新时间: {{ now() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 获取当前时间
|
||||
function now() {
|
||||
const date = new Date();
|
||||
return date.toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
// 定时刷新数据
|
||||
setInterval(() => {
|
||||
window.location.reload();
|
||||
}, 60000); // 每分钟刷新一次
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,630 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ name }} - AI期货分析系统</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/antd@5.12.8/dist/reset.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #f0f2f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
padding: 8px 16px;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.price-section {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.price-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.price-main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.price-change {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
|
||||
}
|
||||
|
||||
.price-change.up {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.price-change.down {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.price-stats {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.chart-section {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.analysis-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.analysis-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.analysis-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f9f9f9;
|
||||
}
|
||||
|
||||
.analysis-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.analysis-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.direction-up {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.direction-down {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
.ai-analysis {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.ai-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ai-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ai-content {
|
||||
background-color: #f9fafb;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.recommendation {
|
||||
background-color: #f0f5ff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
border-left: 4px solid #1890ff;
|
||||
}
|
||||
|
||||
.recommendation-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1890ff;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.recommendation-content {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.price-info {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.price-stats {
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.analysis-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- 头部 -->
|
||||
<div class="header">
|
||||
<div class="header-content">
|
||||
<div>
|
||||
<h1>{{ name }}</h1>
|
||||
<p>品种详细分析</p>
|
||||
</div>
|
||||
<a href="/" class="back-link">返回多品种面板</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 价格信息 -->
|
||||
<div class="price-section">
|
||||
<div class="price-info">
|
||||
<div class="price-main">
|
||||
<div class="price-value">{{ current_price }}</div>
|
||||
<div class="price-change
|
||||
{% if price_change > 0 %}up{% else %}down{% endif %}">
|
||||
{{ price_change }} ({{ price_change_pct }}%)
|
||||
</div>
|
||||
</div>
|
||||
<div class="price-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">胜率</div>
|
||||
<div class="stat-value">{{ win_rate }}%</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">趋势</div>
|
||||
<div class="stat-value
|
||||
{% if trend_analysis.overall_trend == 'bullish' or trend_analysis.overall_trend == 'strong_bullish' or trend_analysis.overall_trend == 'weak_bullish' %}direction-up{% elif trend_analysis.overall_trend == 'bearish' or trend_analysis.overall_trend == 'strong_bearish' or trend_analysis.overall_trend == 'weak_bearish' %}direction-down{% endif %}">
|
||||
{% 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 %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">周期</div>
|
||||
<div class="stat-value">{{ cycle }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- K线图表 -->
|
||||
<div class="chart-section">
|
||||
<div class="chart-title">K线走势图</div>
|
||||
<div id="kline-chart" class="chart-container"></div>
|
||||
</div>
|
||||
|
||||
<!-- 分析网格 -->
|
||||
<div class="analysis-grid">
|
||||
<!-- 趋势分析 -->
|
||||
<div class="analysis-card">
|
||||
<div class="card-title">趋势分析</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">趋势方向</div>
|
||||
<div class="analysis-value
|
||||
{% if trend_analysis.overall_trend == 'bullish' or trend_analysis.overall_trend == 'strong_bullish' or trend_analysis.overall_trend == 'weak_bullish' %}direction-up{% elif trend_analysis.overall_trend == 'bearish' or trend_analysis.overall_trend == 'strong_bearish' or trend_analysis.overall_trend == 'weak_bearish' %}direction-down{% endif %}">
|
||||
{% 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 %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">趋势强度</div>
|
||||
<div class="analysis-value">{{ trend_analysis.trend_strength }}</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">ADX指标</div>
|
||||
<div class="analysis-value">
|
||||
{% if trend_analysis.adx is defined and trend_analysis.adx is not none %}
|
||||
{{ trend_analysis.adx | round(2) }}
|
||||
{% else %}
|
||||
0
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">MA关系</div>
|
||||
<div class="analysis-value">{{ trend_analysis.ma_relationship }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 风险分析 -->
|
||||
<div class="analysis-card">
|
||||
<div class="card-title">风险分析</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">ATR</div>
|
||||
<div class="analysis-value">{{ risk_analysis.atr }}</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">多头止损</div>
|
||||
<div class="analysis-value">{{ risk_analysis.stop_loss_long }}</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">空头止损</div>
|
||||
<div class="analysis-value">{{ risk_analysis.stop_loss_short }}</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">仓位大小</div>
|
||||
<div class="analysis-value">{{ risk_analysis.position_size }}</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">风险比例</div>
|
||||
<div class="analysis-value">{{ risk_analysis.risk_ratio }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 资金流向 -->
|
||||
<div class="analysis-card">
|
||||
<div class="card-title">资金流向</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">资金信号</div>
|
||||
<div class="analysis-value
|
||||
{% if fund_flow_analysis.fund_signal == 'positive' %}direction-up{% elif fund_flow_analysis.fund_signal == 'negative' %}direction-down{% endif %}">
|
||||
{{ fund_flow_analysis.fund_signal | capitalize }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">持仓变化</div>
|
||||
<div class="analysis-value">
|
||||
{% 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 %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">量价关系</div>
|
||||
<div class="analysis-value">{{ fund_flow_analysis.volume_price_relationship }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 换月分析 -->
|
||||
<div class="analysis-card">
|
||||
<div class="card-title">换月分析</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">距离交割</div>
|
||||
<div class="analysis-value warning">{{ rollover_analysis.days_to_delivery }} 天</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">预警级别</div>
|
||||
<div class="analysis-value warning">{{ rollover_analysis.warning_level | capitalize }}</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">流动性风险</div>
|
||||
<div class="analysis-value warning">{{ rollover_analysis.liquidity_risk }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI分析 -->
|
||||
<div class="ai-analysis">
|
||||
<div class="ai-title">
|
||||
<div class="ai-icon">AI</div>
|
||||
AI智能分析
|
||||
</div>
|
||||
<div class="ai-content">
|
||||
<div class="analysis-grid">
|
||||
<!-- 趋势分析 -->
|
||||
<div class="analysis-card">
|
||||
<div class="card-title">趋势判断</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">趋势方向</div>
|
||||
<div class="analysis-value
|
||||
{% if ai_analysis.trend_judgment and ('多' in ai_analysis.trend_judgment) %}direction-up{% elif ai_analysis.trend_judgment and ('空' in ai_analysis.trend_judgment) %}direction-down{% endif %}">
|
||||
{{ ai_analysis.trend_judgment | default('未知') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">胜率评估</div>
|
||||
<div class="analysis-value">{{ ai_analysis.win_rate_assessment | default('未知') }}</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">风险预警</div>
|
||||
<div class="analysis-value warning">{{ ai_analysis.risk_warning | default('无') }}</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">交易建议</div>
|
||||
<div class="analysis-value
|
||||
{% if ai_analysis.trade_recommendation and ('多' in ai_analysis.trade_recommendation) %}direction-up{% elif ai_analysis.trade_recommendation and ('空' in ai_analysis.trade_recommendation) %}direction-down{% endif %}">
|
||||
{{ ai_analysis.trade_recommendation | default('未知') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分析逻辑 -->
|
||||
<div class="analysis-card">
|
||||
<div class="card-title">分析逻辑</div>
|
||||
<div style="padding: 8px 0;">
|
||||
{{ ai_analysis.analysis_logic | default('无详细分析') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 交易建议 -->
|
||||
<div class="recommendation" style="margin-top: 20px;">
|
||||
<div class="recommendation-title">详细交易建议</div>
|
||||
<div class="analysis-grid" style="margin-top: 16px;">
|
||||
<!-- 交易参数 -->
|
||||
<div class="analysis-card">
|
||||
<div class="card-title">交易参数</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">交易方向</div>
|
||||
<div class="analysis-value
|
||||
{% if recommendation.direction == 'long' %}direction-up{% elif recommendation.direction == 'short' %}direction-down{% endif %}">
|
||||
{% if recommendation.direction == 'long' %}多头
|
||||
{% elif recommendation.direction == 'short' %}空头
|
||||
{% else %}{{ recommendation.direction | default('未知') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">入场价格</div>
|
||||
<div class="analysis-value">{{ recommendation.entry_price | default('未知') }}</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">止损价格</div>
|
||||
<div class="analysis-value warning">{{ recommendation.stop_loss | default('未知') }}</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">目标价格</div>
|
||||
<div class="analysis-value direction-up">{{ recommendation.target_price | default('未知') }}</div>
|
||||
</div>
|
||||
<div class="analysis-item">
|
||||
<div class="analysis-label">仓位大小</div>
|
||||
<div class="analysis-value">{{ recommendation.position_size | default('未知') }}手</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 执行计划 -->
|
||||
<div class="analysis-card">
|
||||
<div class="card-title">执行计划</div>
|
||||
<div style="padding: 8px 0;">
|
||||
{{ recommendation.execution_plan | default('无详细计划') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 风险提示 -->
|
||||
<div class="analysis-card">
|
||||
<div class="card-title">风险提示</div>
|
||||
<div style="padding: 8px 0; color: #ff4d4f;">
|
||||
{{ recommendation.risk_tips | default('无风险提示') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部 -->
|
||||
<div class="footer">
|
||||
<p>© 2024 AI期货分析系统 | 数据更新时间: {{ now() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 获取当前时间
|
||||
function now() {
|
||||
const date = new Date();
|
||||
return date.toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
// 初始化K线图表
|
||||
window.onload = function() {
|
||||
const chart = echarts.init(document.getElementById('kline-chart'));
|
||||
|
||||
// 准备K线数据
|
||||
const klineData = [
|
||||
{% for item in kline_data %}
|
||||
["{{ item.date }}", {{ item.open }}, {{ item.close }}, {{ item.low }}, {{ item.high }}],
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['K线']
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: klineData.map(item => item[0]),
|
||||
boundaryGap: false
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
scale: true
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'K线',
|
||||
type: 'candlestick',
|
||||
data: klineData.map(item => [item[1], item[4], item[3], item[2]]),
|
||||
itemStyle: {
|
||||
color: '#52c41a',
|
||||
color0: '#ff4d4f',
|
||||
borderColor: '#52c41a',
|
||||
borderColor0: '#ff4d4f'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
|
||||
// 响应式调整
|
||||
window.addEventListener('resize', function() {
|
||||
chart.resize();
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in new issue