update
This commit is contained in:
parent
dfb0eb9477
commit
7e8178b674
137
.env.example
137
.env.example
@ -1,20 +1,147 @@
|
||||
# Tushare API
|
||||
# ============================================================================
|
||||
# Stock Agent 环境变量配置文件
|
||||
# ============================================================================
|
||||
# 复制此文件为 .env 并填入你的实际配置值
|
||||
# ============================================================================
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# API 密钥配置
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# Tushare API(用于获取A股数据)
|
||||
TUSHARE_TOKEN=your_tushare_token_here
|
||||
|
||||
# 智谱AI GLM-4 API
|
||||
ZHIPUAI_API_KEY=your_zhipuai_key_here
|
||||
|
||||
# Database (使用SQLite,无需额外配置)
|
||||
# DeepSeek API(推荐用于 SmartAgent 和 CryptoAgent)
|
||||
DEEPSEEK_API_KEY=your_deepseek_key_here
|
||||
|
||||
# Brave Search API(用于搜索实时新闻和市场信息)
|
||||
BRAVE_API_KEY=your_brave_api_key_here
|
||||
|
||||
# Binance API(公开数据不需要,私有交易需要)
|
||||
BINANCE_API_KEY=
|
||||
BINANCE_API_SECRET=
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 数据库配置
|
||||
# ----------------------------------------------------------------------------
|
||||
DATABASE_URL=sqlite:///./stock_agent.db
|
||||
|
||||
# API Settings
|
||||
# ----------------------------------------------------------------------------
|
||||
# API 服务配置
|
||||
# ----------------------------------------------------------------------------
|
||||
API_HOST=0.0.0.0
|
||||
API_PORT=8000
|
||||
DEBUG=True
|
||||
|
||||
# Security
|
||||
# ----------------------------------------------------------------------------
|
||||
# 安全配置
|
||||
# ----------------------------------------------------------------------------
|
||||
# JWT 密钥(生产环境必须修改)
|
||||
SECRET_KEY=your_secret_key_here_change_in_production
|
||||
# JWT 算法
|
||||
JWT_ALGORITHM=HS256
|
||||
# JWT 过期天数
|
||||
JWT_EXPIRE_DAYS=7
|
||||
# API 访问频率限制
|
||||
RATE_LIMIT=100/minute
|
||||
|
||||
# CORS
|
||||
# ----------------------------------------------------------------------------
|
||||
# 跨域配置 (CORS)
|
||||
# ----------------------------------------------------------------------------
|
||||
CORS_ORIGINS=http://localhost:8000,http://127.0.0.1:8000
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 腾讯云短信配置
|
||||
# ----------------------------------------------------------------------------
|
||||
TENCENT_SMS_APP_ID=1400961527
|
||||
TENCENT_SMS_SECRET_ID=your_tencent_secret_id_here
|
||||
TENCENT_SMS_SECRET_KEY=your_tencent_secret_key_here
|
||||
TENCENT_SMS_SIGN_ID=629073
|
||||
TENCENT_SMS_TEMPLATE_ID=2353142
|
||||
|
||||
# 验证码配置
|
||||
CODE_EXPIRE_MINUTES=5
|
||||
CODE_RESEND_SECONDS=60
|
||||
CODE_MAX_PER_HOUR=10
|
||||
|
||||
# 白名单手机号(无需验证码即可登录,逗号分隔)
|
||||
WHITELIST_PHONES=18583366860,18583926860
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 通知配置
|
||||
# ----------------------------------------------------------------------------
|
||||
# 飞书机器人
|
||||
FEISHU_WEBHOOK_URL=https://open.feishu.cn/open-apis/bot/v2/hook/8a1dcf69-6753-41e2-a393-edc4f7822db0
|
||||
FEISHU_ENABLED=true
|
||||
|
||||
# Telegram 机器人
|
||||
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
|
||||
TELEGRAM_CHANNEL_ID=your_telegram_channel_id_here
|
||||
TELEGRAM_ENABLED=true
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 加密货币交易智能体配置
|
||||
# ----------------------------------------------------------------------------
|
||||
# 监控的交易对(逗号分隔)
|
||||
CRYPTO_SYMBOLS=BTCUSDT,ETHUSDT
|
||||
# 分析间隔(秒)
|
||||
CRYPTO_ANALYSIS_INTERVAL=60
|
||||
# 触发 LLM 分析的置信度阈值(0-1)
|
||||
CRYPTO_LLM_THRESHOLD=0.70
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 模拟交易配置
|
||||
# ----------------------------------------------------------------------------
|
||||
# 是否启用模拟交易
|
||||
PAPER_TRADING_ENABLED=true
|
||||
# 初始本金 (USDT)
|
||||
PAPER_TRADING_INITIAL_BALANCE=10000
|
||||
# 杠杆倍数(全仓模式下的最大杠杆)
|
||||
PAPER_TRADING_LEVERAGE=20
|
||||
# 每单保证金 (USDT)
|
||||
PAPER_TRADING_MARGIN_PER_ORDER=1000
|
||||
# 最大持仓+挂单总数
|
||||
PAPER_TRADING_MAX_ORDERS=10
|
||||
# 是否自动平掉反向持仓(智能策略)
|
||||
PAPER_TRADING_AUTO_CLOSE_OPPOSITE=false
|
||||
# 保本止损触发阈值(盈利百分比),0表示禁用
|
||||
PAPER_TRADING_BREAKEVEN_THRESHOLD=1
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 移动止损配置
|
||||
# ----------------------------------------------------------------------------
|
||||
# 是否启用移动止损
|
||||
PAPER_TRADING_TRAILING_STOP_ENABLED=true
|
||||
# 移动止损触发倍数(相对于保本阈值)
|
||||
PAPER_TRADING_TRAILING_STOP_THRESHOLD_MULTIPLIER=2
|
||||
# 移动止损跟随比例(0-1之间,1表示完全跟随)
|
||||
PAPER_TRADING_TRAILING_STOP_RATIO=0.5
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 动态止盈配置(趋势过滤)
|
||||
# ----------------------------------------------------------------------------
|
||||
# 是否启用动态止盈
|
||||
PAPER_TRADING_DYNAMIC_TP_ENABLED=true
|
||||
# 强趋势时移动止损跟随比例(70%)
|
||||
PAPER_TRADING_STRONG_TREND_RATIO=0.7
|
||||
# 弱趋势时移动止损跟随比例(30%)
|
||||
PAPER_TRADING_WEAK_TREND_RATIO=0.3
|
||||
# 震荡市固定止盈百分比(3%)
|
||||
PAPER_TRADING_SIDEWAYS_TP_PERCENT=3
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 仓位配置(废弃,保留兼容性)
|
||||
# ----------------------------------------------------------------------------
|
||||
PAPER_TRADING_POSITION_A=1000
|
||||
PAPER_TRADING_POSITION_B=500
|
||||
PAPER_TRADING_POSITION_C=200
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Agent 模型配置
|
||||
# ----------------------------------------------------------------------------
|
||||
# 可选值: zhipu, deepseek
|
||||
SMART_AGENT_MODEL=deepseek
|
||||
CRYPTO_AGENT_MODEL=deepseek
|
||||
|
||||
@ -118,6 +118,17 @@ class Settings(BaseSettings):
|
||||
paper_trading_max_orders: int = 10 # 最大持仓+挂单总数
|
||||
paper_trading_auto_close_opposite: bool = False # 是否自动平掉反向持仓(智能策略)
|
||||
paper_trading_breakeven_threshold: float = 1 # 保本止损触发阈值(盈利百分比),0表示禁用
|
||||
|
||||
# 移动止损配置
|
||||
paper_trading_trailing_stop_enabled: bool = True # 是否启用移动止损
|
||||
paper_trading_trailing_stop_threshold_multiplier: float = 2 # 移动止损触发倍数(相对于保本阈值)
|
||||
paper_trading_trailing_stop_ratio: float = 0.5 # 移动止损跟随比例(0-1之间,1表示完全跟随)
|
||||
|
||||
# 动态止盈配置(趋势过滤)
|
||||
paper_trading_dynamic_tp_enabled: bool = True # 是否启用动态止盈
|
||||
paper_trading_strong_trend_ratio: float = 0.7 # 强趋势时移动止损跟随比例(70%)
|
||||
paper_trading_weak_trend_ratio: float = 0.3 # 弱趋势时移动止损跟随比例(30%)
|
||||
paper_trading_sideways_tp_percent: float = 3 # 震荡市固定止盈百分比(3%)
|
||||
# 废弃的配置(保留兼容性)
|
||||
paper_trading_position_a: float = 1000 # A级信号仓位 (USDT)
|
||||
paper_trading_position_b: float = 500 # B级信号仓位 (USDT)
|
||||
|
||||
@ -155,14 +155,48 @@ class LLMSignalAnalyzer:
|
||||
- 单一交易对持仓不宜过大
|
||||
- 如果当前持仓已经较重,即使有好机会也要控制仓位
|
||||
|
||||
## 八、止损止盈策略(重要更新)
|
||||
|
||||
### 止损设置原则(结构化止损)
|
||||
**不要使用固定百分比或固定ATR倍数!止损必须基于关键价位:**
|
||||
|
||||
1. **做多止损**:
|
||||
- 优先放在最近支撑位(前低)下方 0.3-0.5%
|
||||
- 如果有 MA20 支撑,可放在 MA20 下方 0.5%
|
||||
- 如果最近低点距离过近(<1%),则使用 ATR 1.2-1.5倍
|
||||
- 避免止损距离过大(>4%ATR)
|
||||
|
||||
2. **做空止损**:
|
||||
- 优先放在最近阻力位(前高)上方 0.3-0.5%
|
||||
- 如果有 MA20 阻力,可放在 MA20 上方 0.5%
|
||||
- 如果最近高点距离过近(<1%),则使用 ATR 1.2-1.5倍
|
||||
- 避免止损距离过大(>4%ATR)
|
||||
|
||||
### 止盈设置(移动止盈策略)
|
||||
**不设固定止盈位,让利润奔跑!**
|
||||
|
||||
1. **take_profit 设置为保险价位**:
|
||||
- 做多:入场价 + 15%(作为极端情况的保险止盈)
|
||||
- 做空:入场价 - 15%
|
||||
- 这个价位只是"保险",正常情况下不会触及
|
||||
|
||||
2. **实际止盈靠移动止损**:
|
||||
- 系统会通过移动止损自动锁定利润
|
||||
- 盈利 2% 后开始移动止损,锁定部分利润
|
||||
- 盈利越多,止损跟随移动,确保吃到趋势
|
||||
|
||||
### 风险收益比
|
||||
- 虽然不设固定止盈,但仍要确保止损合理
|
||||
- 理想情况下,潜在风险(止损距离)应控制在 2-3% 以内
|
||||
|
||||
## 重要原则
|
||||
1. **量价优先** - 任何信号都必须有量能配合才可靠
|
||||
2. **积极但不冒进** - 有合理依据就给出信号,不要过于保守
|
||||
3. 每种类型最多输出一个信号
|
||||
4. 止损必须明确,风险收益比至少 1:1.5
|
||||
5. reason 字段必须包含量价分析(如"放量突破+RSI=45,量比1.8确认有效")
|
||||
6. entry_type 必须明确:信号已触发用 market,等待更好价位用 limit
|
||||
7. 短线信号止损控制在 1-2%,中线信号止损控制在 2-4%
|
||||
4. 止损必须基于关键支撑/阻力位(前低前高、MA20),不要用固定百分比
|
||||
5. 止盈设置为保险价位(做多+15%,做空-15%),实际靠移动止损锁定利润
|
||||
6. reason 字段必须包含量价分析(如"放量突破+RSI=45,量比1.8确认有效")
|
||||
7. entry_type 必须明确:信号已触发用 market,等待更好价位用 limit
|
||||
8. **position_size 必须明确**:根据信号质量和持仓情况给出 heavy/medium/light"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@ -1249,29 +1249,156 @@ class SignalAnalyzer:
|
||||
return "\n".join(parts)
|
||||
|
||||
def calculate_stop_loss_take_profit(self, price: float, action: str,
|
||||
atr: float) -> Dict[str, float]:
|
||||
atr: float, df: pd.DataFrame = None) -> Dict[str, float]:
|
||||
"""
|
||||
计算止损止盈位置
|
||||
计算止损止盈位置 - 结构化止损 + 移动止盈策略
|
||||
|
||||
策略:
|
||||
- 止损:基于关键支撑/阻力位(前高前低、均线)
|
||||
- 止盈:不设固定止盈,通过移动止损锁定利润
|
||||
|
||||
Args:
|
||||
price: 当前价格
|
||||
action: 'buy' 或 'sell'
|
||||
atr: ATR 值
|
||||
df: K线数据(用于计算关键价位)
|
||||
|
||||
Returns:
|
||||
{'stop_loss': float, 'take_profit': float}
|
||||
{'stop_loss': float, 'take_profit': float, 'method': str}
|
||||
"""
|
||||
if df is not None and len(df) >= 20:
|
||||
# 使用结构化止损
|
||||
result = self._calculate_structured_stops(price, action, atr, df)
|
||||
if result['stop_loss'] > 0:
|
||||
return result
|
||||
|
||||
# 回退到 ATR 止损
|
||||
if action == 'buy':
|
||||
stop_loss = price - atr * 2
|
||||
take_profit = price + atr * 3
|
||||
stop_loss = price - atr * 1.5 # 降低到 1.5 倍 ATR
|
||||
# 止盈设置得很远,主要靠移动止损
|
||||
take_profit = price * 1.15 # 15% 作为一个"保险"止盈位
|
||||
method = 'atr'
|
||||
elif action == 'sell':
|
||||
stop_loss = price + atr * 2
|
||||
take_profit = price - atr * 3
|
||||
stop_loss = price + atr * 1.5
|
||||
# 止盈设置得很远,主要靠移动止损
|
||||
take_profit = price * 0.85 # 15% 作为一个"保险"止盈位
|
||||
method = 'atr'
|
||||
else:
|
||||
stop_loss = 0
|
||||
take_profit = 0
|
||||
return {'stop_loss': 0, 'take_profit': 0, 'method': 'none'}
|
||||
|
||||
return {
|
||||
'stop_loss': round(stop_loss, 2),
|
||||
'take_profit': round(take_profit, 2)
|
||||
'take_profit': round(take_profit, 2),
|
||||
'method': method
|
||||
}
|
||||
|
||||
def _calculate_structured_stops(self, price: float, action: str,
|
||||
atr: float, df: pd.DataFrame) -> Dict[str, float]:
|
||||
"""
|
||||
基于关键价位计算结构化止损(无固定止盈,靠移动止损)
|
||||
|
||||
原则:
|
||||
- 做多止损在前低/MA20下方
|
||||
- 做空止损在前高/MA20上方
|
||||
- 止损距离控制在 1.5-3% ATR 范围内
|
||||
- 止盈设置得很远,主要靠移动止损锁定利润
|
||||
"""
|
||||
stop_loss = 0
|
||||
take_profit = 0
|
||||
|
||||
# 获取关键价位
|
||||
recent_data = df.tail(50)
|
||||
|
||||
if action == 'buy':
|
||||
# === 做多止损 ===
|
||||
# 1. 查找近期支撑位(前低)
|
||||
recent_lows = recent_data['low'].values
|
||||
# 找出低于当前价的低点
|
||||
valid_lows = [low for low in recent_lows if low < price * 0.99]
|
||||
if valid_lows:
|
||||
nearest_low = max(valid_lows) # 最近的低点
|
||||
# 止损放在前低下方 0.3%
|
||||
sl_candidate = nearest_low * 0.997
|
||||
else:
|
||||
sl_candidate = 0
|
||||
|
||||
# 2. 检查 MA20 作为支撑
|
||||
ma20 = df['ma20'].iloc[-1] if 'ma20' in df.columns else 0
|
||||
if pd.notna(ma20) and ma20 < price * 0.98:
|
||||
sl_ma20 = ma20 * 0.995 # MA20 下方 0.5%
|
||||
# 选择更保守的止损(更高的止损位)
|
||||
if sl_candidate > 0:
|
||||
sl_candidate = max(sl_candidate, sl_ma20)
|
||||
else:
|
||||
sl_candidate = sl_ma20
|
||||
|
||||
# 3. 验证止损距离合理性(1.5%-3% ATR)
|
||||
if sl_candidate > 0:
|
||||
sl_distance = (price - sl_candidate) / price * 100
|
||||
atr_percent = atr / price * 100
|
||||
|
||||
# 如果止损距离太小(< 1%ATR),使用 ATR 止损
|
||||
if sl_distance < atr_percent * 1.0:
|
||||
sl_candidate = price - atr * 1.2
|
||||
# 如果止损距离过大(> 4%ATR),限制在合理范围
|
||||
elif sl_distance > atr_percent * 4:
|
||||
sl_candidate = price - atr * 2.5
|
||||
|
||||
# 4. 回退到 ATR 止损
|
||||
if sl_candidate <= 0:
|
||||
sl_candidate = price - atr * 1.5
|
||||
|
||||
stop_loss = sl_candidate
|
||||
|
||||
# === 做多止盈(设置得很远,主要靠移动止损)===
|
||||
take_profit = price * 1.15 # 15% 作为保险止盈位
|
||||
|
||||
elif action == 'sell':
|
||||
# === 做空止损 ===
|
||||
# 1. 查找近期阻力位(前高)
|
||||
recent_highs = recent_data['high'].values
|
||||
# 找出高于当前价的高点
|
||||
valid_highs = [high for high in recent_highs if high > price * 1.01]
|
||||
if valid_highs:
|
||||
nearest_high = min(valid_highs) # 最近的高点
|
||||
# 止损放在前高上方 0.3%
|
||||
sl_candidate = nearest_high * 1.003
|
||||
else:
|
||||
sl_candidate = 0
|
||||
|
||||
# 2. 检查 MA20 作为阻力
|
||||
ma20 = df['ma20'].iloc[-1] if 'ma20' in df.columns else 0
|
||||
if pd.notna(ma20) and ma20 > price * 1.02:
|
||||
sl_ma20 = ma20 * 1.005 # MA20 上方 0.5%
|
||||
# 选择更保守的止损(更低的止损位)
|
||||
if sl_candidate > 0:
|
||||
sl_candidate = min(sl_candidate, sl_ma20)
|
||||
else:
|
||||
sl_candidate = sl_ma20
|
||||
|
||||
# 3. 验证止损距离合理性
|
||||
if sl_candidate > 0:
|
||||
sl_distance = (sl_candidate - price) / price * 100
|
||||
atr_percent = atr / price * 100
|
||||
|
||||
# 如果止损距离太小(< 1%ATR),使用 ATR 止损
|
||||
if sl_distance < atr_percent * 1.0:
|
||||
sl_candidate = price + atr * 1.2
|
||||
# 如果止损距离过大(> 4%ATR),限制在合理范围
|
||||
elif sl_distance > atr_percent * 4:
|
||||
sl_candidate = price + atr * 2.5
|
||||
|
||||
# 4. 回退到 ATR 止损
|
||||
if sl_candidate <= 0:
|
||||
sl_candidate = price + atr * 1.5
|
||||
|
||||
stop_loss = sl_candidate
|
||||
|
||||
# === 做空止盈(设置得很远,主要靠移动止损)===
|
||||
take_profit = price * 0.85 # 15% 作为保险止盈位
|
||||
|
||||
return {
|
||||
'stop_loss': round(stop_loss, 2),
|
||||
'take_profit': round(take_profit, 2),
|
||||
'method': 'structured'
|
||||
}
|
||||
|
||||
@ -80,6 +80,10 @@ class PaperOrder(Base):
|
||||
max_profit = Column(Float, default=0) # 持仓期间最大盈利
|
||||
breakeven_triggered = Column(Integer, default=0) # 是否触发过保本止损(0=否,1=是)
|
||||
|
||||
# 移动止损相关
|
||||
trailing_stop_triggered = Column(Integer, default=0) # 是否触发过移动止损(0=否,1=是)
|
||||
trailing_stop_base_profit = Column(Float, default=0) # 移动止损触发时的盈利百分比
|
||||
|
||||
# 时间戳
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
opened_at = Column(DateTime, nullable=True) # 开仓时间
|
||||
@ -114,6 +118,8 @@ class PaperOrder(Base):
|
||||
'max_drawdown': self.max_drawdown,
|
||||
'max_profit': self.max_profit,
|
||||
'breakeven_triggered': self.breakeven_triggered,
|
||||
'trailing_stop_triggered': getattr(self, 'trailing_stop_triggered', 0),
|
||||
'trailing_stop_base_profit': getattr(self, 'trailing_stop_base_profit', 0),
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'opened_at': self.opened_at.isoformat() if self.opened_at else None,
|
||||
'closed_at': self.closed_at.isoformat() if self.closed_at else None,
|
||||
|
||||
@ -76,9 +76,21 @@ class BinanceService:
|
||||
Returns:
|
||||
包含 5m, 15m, 1h, 4h 数据的字典
|
||||
"""
|
||||
# 不同周期使用不同的数据量,平衡分析深度和性能
|
||||
# 5m: 200根 = 16.7小时(日内分析)
|
||||
# 15m: 200根 = 2.1天(短线分析)
|
||||
# 1h: 300根 = 12.5天(中线分析)
|
||||
# 4h: 200根 = 33.3天(趋势分析)
|
||||
limits = {
|
||||
'5m': 200,
|
||||
'15m': 200,
|
||||
'1h': 300,
|
||||
'4h': 200
|
||||
}
|
||||
|
||||
data = {}
|
||||
for interval in ['5m', '15m', '1h', '4h']:
|
||||
df = self.get_klines(symbol, interval, limit=100)
|
||||
df = self.get_klines(symbol, interval, limit=limits.get(interval, 100))
|
||||
if not df.empty:
|
||||
df = self.calculate_indicators(df)
|
||||
data[interval] = df
|
||||
|
||||
@ -27,13 +27,28 @@ class PaperTradingService:
|
||||
self.auto_close_opposite = self.settings.paper_trading_auto_close_opposite # 是否自动平掉反向持仓
|
||||
self.breakeven_threshold = self.settings.paper_trading_breakeven_threshold # 保本止损触发阈值
|
||||
|
||||
# 移动止损配置
|
||||
self.trailing_stop_enabled = self.settings.paper_trading_trailing_stop_enabled
|
||||
self.trailing_stop_threshold_multiplier = self.settings.paper_trading_trailing_stop_threshold_multiplier
|
||||
self.trailing_stop_ratio = self.settings.paper_trailing_stop_ratio
|
||||
|
||||
# 动态止盈配置
|
||||
self.dynamic_tp_enabled = self.settings.paper_trading_dynamic_tp_enabled
|
||||
self.strong_trend_ratio = self.settings.paper_trading_strong_trend_ratio
|
||||
self.weak_trend_ratio = self.settings.paper_trading_weak_trend_ratio
|
||||
self.sideways_tp_percent = self.settings.paper_trading_sideways_tp_percent
|
||||
|
||||
# 确保表已创建
|
||||
self._ensure_table_exists()
|
||||
|
||||
# 加载活跃订单到内存
|
||||
self._load_active_orders()
|
||||
|
||||
logger.info(f"模拟交易服务初始化完成(自动平反向持仓: {'启用' if self.auto_close_opposite else '禁用'},保本止损阈值: {self.breakeven_threshold}%)")
|
||||
logger.info(f"模拟交易服务初始化完成(自动平反向持仓: {'启用' if self.auto_close_opposite else '禁用'},"
|
||||
f"保本止损阈值: {self.breakeven_threshold}%,"
|
||||
f"移动止损: {'启用' if self.trailing_stop_enabled else '禁用'},"
|
||||
f"触发倍数: {self.trailing_stop_threshold_multiplier}x,"
|
||||
f"跟随比例: {self.trailing_stop_ratio * 100}%)")
|
||||
|
||||
def _ensure_table_exists(self):
|
||||
"""确保数据表已创建,并迁移新字段"""
|
||||
@ -42,19 +57,32 @@ class PaperTradingService:
|
||||
from sqlalchemy import text
|
||||
Base.metadata.create_all(bind=db_service.engine)
|
||||
|
||||
# 检查并添加新字段 breakeven_triggered
|
||||
db = db_service.get_session()
|
||||
try:
|
||||
# 尝试查询新字段,如果失败则添加
|
||||
db.execute(text("SELECT breakeven_triggered FROM paper_orders LIMIT 1"))
|
||||
except Exception:
|
||||
# 检查并添加新字段 breakeven_triggered
|
||||
try:
|
||||
db.execute(text("ALTER TABLE paper_orders ADD COLUMN breakeven_triggered INTEGER DEFAULT 0"))
|
||||
db.commit()
|
||||
logger.info("数据库迁移: 添加 breakeven_triggered 字段")
|
||||
except Exception as e:
|
||||
logger.warning(f"添加 breakeven_triggered 字段失败(可能已存在): {e}")
|
||||
db.rollback()
|
||||
db.execute(text("SELECT breakeven_triggered FROM paper_orders LIMIT 1"))
|
||||
except Exception:
|
||||
try:
|
||||
db.execute(text("ALTER TABLE paper_orders ADD COLUMN breakeven_triggered INTEGER DEFAULT 0"))
|
||||
db.commit()
|
||||
logger.info("数据库迁移: 添加 breakeven_triggered 字段")
|
||||
except Exception as e:
|
||||
logger.warning(f"添加 breakeven_triggered 字段失败(可能已存在): {e}")
|
||||
db.rollback()
|
||||
|
||||
# 检查并添加移动止损相关字段
|
||||
try:
|
||||
db.execute(text("SELECT trailing_stop_triggered FROM paper_orders LIMIT 1"))
|
||||
except Exception:
|
||||
try:
|
||||
db.execute(text("ALTER TABLE paper_orders ADD COLUMN trailing_stop_triggered INTEGER DEFAULT 0"))
|
||||
db.execute(text("ALTER TABLE paper_orders ADD COLUMN trailing_stop_base_profit REAL DEFAULT 0"))
|
||||
db.commit()
|
||||
logger.info("数据库迁移: 添加 trailing_stop_triggered 和 trailing_stop_base_profit 字段")
|
||||
except Exception as e:
|
||||
logger.warning(f"添加移动止损字段失败(可能已存在): {e}")
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@ -541,8 +569,79 @@ class PaperTradingService:
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def _assess_trend_strength(self, order: PaperOrder, current_price: float) -> str:
|
||||
"""
|
||||
评估当前趋势强度(用于动态止盈)
|
||||
|
||||
Returns:
|
||||
'strong' - 强趋势(移动止损跟随比例高)
|
||||
'weak' - 弱趋势(移动止损跟随比例低)
|
||||
'sideways' - 震荡市(考虑固定止盈)
|
||||
"""
|
||||
# 基于订单的最大盈利和当前盈亏判断趋势强度
|
||||
max_profit = order.max_profit
|
||||
current_pnl_percent = 0
|
||||
|
||||
if order.side == OrderSide.LONG:
|
||||
current_pnl_percent = ((current_price - order.filled_price) / order.filled_price) * 100
|
||||
else:
|
||||
current_pnl_percent = ((order.filled_price - current_price) / order.filled_price) * 100
|
||||
|
||||
# 判断1:盈利是否持续创新高
|
||||
profit_streak = (current_pnl_percent >= max_profit * 0.8) # 当前盈利接近最大盈利
|
||||
|
||||
# 判断2:最大盈利是否足够大(说明有趋势)
|
||||
significant_profit = max_profit >= 3 # 至少3%盈利
|
||||
|
||||
# 判断3:当前盈利是否回撤较多
|
||||
profit_pullback = (max_profit - current_pnl_percent) / max_profit if max_profit > 0 else 0
|
||||
|
||||
# 综合判断
|
||||
if profit_streak and significant_profit and profit_pullback < 0.3:
|
||||
return 'strong'
|
||||
elif significant_profit and profit_pullback >= 0.5:
|
||||
# 盈利回撤超过50%,可能是震荡或反转
|
||||
return 'sideways'
|
||||
elif max_profit < 1.5:
|
||||
# 盈利不到1.5%,趋势尚未确立
|
||||
return 'weak'
|
||||
else:
|
||||
return 'weak'
|
||||
|
||||
def _get_dynamic_ratio(self, order: PaperOrder, current_price: float) -> float:
|
||||
"""
|
||||
根据趋势强度动态计算移动止损跟随比例
|
||||
|
||||
Returns:
|
||||
跟随比例(0-1之间)
|
||||
"""
|
||||
if not self.dynamic_tp_enabled:
|
||||
return self.trailing_stop_ratio
|
||||
|
||||
trend_strength = self._assess_trend_strength(order, current_price)
|
||||
|
||||
if trend_strength == 'strong':
|
||||
# 强趋势:高跟随比例,锁定更多利润
|
||||
return self.strong_trend_ratio
|
||||
elif trend_strength == 'sideways':
|
||||
# 震荡市:低跟随比例,避免被来回扫
|
||||
return self.weak_trend_ratio
|
||||
else:
|
||||
# 弱趋势:中等跟随比例
|
||||
return self.trailing_stop_ratio
|
||||
|
||||
def _update_order_extremes(self, order: PaperOrder, current_price: float) -> Optional[Dict[str, Any]]:
|
||||
"""更新订单的最大回撤和最大盈利,并检查是否需要移动止损到保本"""
|
||||
"""
|
||||
更新订单的最大回撤和最大盈利,并检查是否需要移动止损
|
||||
|
||||
止损策略(按优先级):
|
||||
1. 移动止损:盈利达到阈值倍数后,止损按比例跟随盈利移动
|
||||
2. 保本止损:盈利达到阈值时,将止损移动到开仓价
|
||||
|
||||
Returns:
|
||||
如果触发了止损移动,返回通知字典;否则返回 None
|
||||
"""
|
||||
if order.side == OrderSide.LONG:
|
||||
current_pnl_percent = ((current_price - order.filled_price) / order.filled_price) * 100
|
||||
else:
|
||||
@ -550,7 +649,9 @@ class PaperTradingService:
|
||||
|
||||
# 检查是否需要更新极值
|
||||
needs_update = False
|
||||
breakeven_triggered = False
|
||||
stop_moved = False
|
||||
stop_move_type = "" # "breakeven" 或 "trailing"
|
||||
new_stop_loss = None
|
||||
|
||||
if current_pnl_percent > order.max_profit:
|
||||
order.max_profit = current_pnl_percent
|
||||
@ -560,17 +661,93 @@ class PaperTradingService:
|
||||
order.max_drawdown = current_pnl_percent
|
||||
needs_update = True
|
||||
|
||||
# 保本止损逻辑:当盈利达到阈值时,将止损移动到开仓价
|
||||
# === 移动止损逻辑(优先级高于保本止损) ===
|
||||
if self.trailing_stop_enabled and self.trailing_stop_threshold_multiplier > 0:
|
||||
trailing_threshold = self.breakeven_threshold * self.trailing_stop_threshold_multiplier
|
||||
|
||||
# 检查是否达到移动止损触发阈值
|
||||
if current_pnl_percent >= trailing_threshold:
|
||||
trailing_triggered = getattr(order, 'trailing_stop_triggered', 0) == 1
|
||||
|
||||
if not trailing_triggered:
|
||||
# 首次触发移动止损:记录基准盈利,并移动止损
|
||||
order.trailing_stop_triggered = 1
|
||||
order.trailing_stop_base_profit = current_pnl_percent
|
||||
needs_update = True
|
||||
stop_moved = True
|
||||
stop_move_type = "trailing_first"
|
||||
|
||||
# 计算新的止损价(锁定部分利润)
|
||||
if order.side == OrderSide.LONG:
|
||||
# 做多:新止损价 = 开仓价 * (1 + 盈利 * 跟随比例)
|
||||
locked_profit_percent = current_pnl_percent * self._get_dynamic_ratio(order, current_price)
|
||||
new_stop_loss = order.filled_price * (1 + locked_profit_percent / 100)
|
||||
order.stop_loss = new_stop_loss
|
||||
else:
|
||||
# 做空:新止损价 = 开仓价 * (1 - 盈利 * 跟随比例)
|
||||
locked_profit_percent = current_pnl_percent * self._get_dynamic_ratio(order, current_price)
|
||||
new_stop_loss = order.filled_price * (1 - locked_profit_percent / 100)
|
||||
order.stop_loss = new_stop_loss
|
||||
|
||||
logger.info(f"移动止损首次触发: {order.order_id} | {order.symbol} | 盈利 {current_pnl_percent:.2f}% >= {trailing_threshold:.2f}% | "
|
||||
f"锁定利润 {locked_profit_percent:.2f}% | 止损移至 ${order.stop_loss:,.2f}")
|
||||
|
||||
elif getattr(order, 'trailing_stop_triggered', 0) == 1:
|
||||
# 已触发过移动止损,持续跟随
|
||||
base_profit = getattr(order, 'trailing_stop_base_profit', 0)
|
||||
additional_profit = current_pnl_percent - base_profit
|
||||
|
||||
if additional_profit > 0:
|
||||
# 有额外盈利,移动止损以锁定更多利润
|
||||
if order.side == OrderSide.LONG:
|
||||
# 做多:当前止损对应的盈利
|
||||
current_stop_profit = ((order.stop_loss - order.filled_price) / order.filled_price) * 100
|
||||
# 新的止损盈利 = 基准 + 额外盈利 * 跟随比例
|
||||
new_stop_profit = base_profit * self._get_dynamic_ratio(order, current_price)
|
||||
new_stop_loss = order.filled_price * (1 + new_stop_profit / 100)
|
||||
|
||||
# 只有当新止损高于当前止损时才更新(做多止损只能上移)
|
||||
if new_stop_loss > order.stop_loss:
|
||||
old_stop = order.stop_loss
|
||||
order.stop_loss = new_stop_loss
|
||||
needs_update = True
|
||||
stop_moved = True
|
||||
stop_move_type = "trailing_update"
|
||||
logger.info(f"移动止损更新: {order.order_id} | {order.symbol} | "
|
||||
f"盈利 {current_pnl_percent:.2f}% | 止损 ${old_stop:,.2f} -> ${new_stop_loss:,.2f}")
|
||||
else:
|
||||
# 做空:当前止损对应的盈利
|
||||
current_stop_profit = ((order.filled_price - order.stop_loss) / order.filled_price) * 100
|
||||
# 新的止损盈利 = 基准 + 额外盈利 * 跟随比例
|
||||
new_stop_profit = base_profit * self._get_dynamic_ratio(order, current_price)
|
||||
new_stop_loss = order.filled_price * (1 - new_stop_profit / 100)
|
||||
|
||||
# 只有当新止损低于当前止损时才更新(做空止损只能下移)
|
||||
if new_stop_loss < order.stop_loss:
|
||||
old_stop = order.stop_loss
|
||||
order.stop_loss = new_stop_loss
|
||||
needs_update = True
|
||||
stop_moved = True
|
||||
stop_move_type = "trailing_update"
|
||||
logger.info(f"移动止损更新: {order.order_id} | {order.symbol} | "
|
||||
f"盈利 {current_pnl_percent:.2f}% | 止损 ${old_stop:,.2f} -> ${new_stop_loss:,.2f}")
|
||||
|
||||
# === 保本止损逻辑(仅在未触发移动止损时生效) ===
|
||||
if self.breakeven_threshold > 0 and current_pnl_percent >= self.breakeven_threshold:
|
||||
# 检查止损是否还没有移动到保本位(通过标记判断)
|
||||
if getattr(order, 'breakeven_triggered', 0) != 1:
|
||||
# 检查是否还没有触发过保本止损,也没有触发过移动止损
|
||||
breakeven_not_triggered = getattr(order, 'breakeven_triggered', 0) != 1
|
||||
trailing_not_triggered = getattr(order, 'trailing_stop_triggered', 0) != 1
|
||||
|
||||
if breakeven_not_triggered and trailing_not_triggered:
|
||||
if order.side == OrderSide.LONG:
|
||||
# 做多:止损应该低于开仓价,如果止损还在开仓价下方,则移动到开仓价
|
||||
if order.stop_loss < order.filled_price:
|
||||
order.stop_loss = order.filled_price
|
||||
order.breakeven_triggered = 1
|
||||
needs_update = True
|
||||
breakeven_triggered = True
|
||||
stop_moved = True
|
||||
stop_move_type = "breakeven"
|
||||
new_stop_loss = order.stop_loss
|
||||
logger.info(f"保本止损触发: {order.order_id} | {order.symbol} | 盈利 {current_pnl_percent:.2f}% >= {self.breakeven_threshold}% | 止损移至开仓价 ${order.filled_price:,.2f}")
|
||||
else:
|
||||
# 做空:止损应该高于开仓价,如果止损还在开仓价上方,则移动到开仓价
|
||||
@ -578,7 +755,9 @@ class PaperTradingService:
|
||||
order.stop_loss = order.filled_price
|
||||
order.breakeven_triggered = 1
|
||||
needs_update = True
|
||||
breakeven_triggered = True
|
||||
stop_moved = True
|
||||
stop_move_type = "breakeven"
|
||||
new_stop_loss = order.stop_loss
|
||||
logger.info(f"保本止损触发: {order.order_id} | {order.symbol} | 盈利 {current_pnl_percent:.2f}% >= {self.breakeven_threshold}% | 止损移至开仓价 ${order.filled_price:,.2f}")
|
||||
|
||||
# 如果有更新,持久化到数据库
|
||||
@ -590,7 +769,12 @@ class PaperTradingService:
|
||||
db_order.max_profit = order.max_profit
|
||||
db_order.max_drawdown = order.max_drawdown
|
||||
db_order.stop_loss = order.stop_loss
|
||||
db_order.breakeven_triggered = order.breakeven_triggered
|
||||
db_order.breakeven_triggered = getattr(order, 'breakeven_triggered', 0)
|
||||
# 更新移动止损相关字段
|
||||
if hasattr(order, 'trailing_stop_triggered'):
|
||||
db_order.trailing_stop_triggered = order.trailing_stop_triggered
|
||||
if hasattr(order, 'trailing_stop_base_profit'):
|
||||
db_order.trailing_stop_base_profit = getattr(order, 'trailing_stop_base_profit', 0)
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
logger.error(f"更新订单极值失败: {e}")
|
||||
@ -598,15 +782,16 @@ class PaperTradingService:
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# 如果触发了保本止损,返回通知信息
|
||||
if breakeven_triggered:
|
||||
# 如果触发了止损移动,返回通知信息
|
||||
if stop_moved and new_stop_loss is not None:
|
||||
return {
|
||||
'event_type': 'breakeven_triggered',
|
||||
'event_type': 'stop_loss_moved',
|
||||
'order_id': order.order_id,
|
||||
'symbol': order.symbol,
|
||||
'side': order.side.value,
|
||||
'move_type': stop_move_type,
|
||||
'filled_price': order.filled_price,
|
||||
'new_stop_loss': order.stop_loss,
|
||||
'new_stop_loss': new_stop_loss,
|
||||
'current_pnl_percent': current_pnl_percent
|
||||
}
|
||||
|
||||
|
||||
149
backend/migrate_db.py
Normal file
149
backend/migrate_db.py
Normal file
@ -0,0 +1,149 @@
|
||||
"""
|
||||
数据库迁移脚本 - 添加移动止损字段
|
||||
用于为已有的 paper_trading 表添加新字段
|
||||
"""
|
||||
import sqlite3
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
def migrate_database():
|
||||
"""执行数据库迁移"""
|
||||
|
||||
# 查找数据库文件
|
||||
db_paths = [
|
||||
"stock_agent.db",
|
||||
"backend/stock_agent.db",
|
||||
"../stock_agent.db"
|
||||
]
|
||||
|
||||
db_path = None
|
||||
for path in db_paths:
|
||||
if os.path.exists(path):
|
||||
db_path = path
|
||||
break
|
||||
|
||||
if not db_path:
|
||||
print("❌ 未找到数据库文件 stock_agent.db")
|
||||
print("请确保在项目根目录或 backend 目录下运行此脚本")
|
||||
return False
|
||||
|
||||
print(f"📁 找到数据库文件: {db_path}")
|
||||
|
||||
# 连接数据库
|
||||
try:
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 检查表是否存在
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='paper_orders'")
|
||||
if not cursor.fetchone():
|
||||
print("⚠️ paper_orders 表不存在,将在首次启动时自动创建")
|
||||
return False
|
||||
|
||||
# 检查字段是否已存在
|
||||
cursor.execute("PRAGMA table_info(paper_orders)")
|
||||
columns = [column[1] for column in cursor.fetchall()]
|
||||
|
||||
# 需要添加的新字段
|
||||
new_columns = {
|
||||
'trailing_stop_triggered': 'INTEGER DEFAULT 0',
|
||||
'trailing_stop_base_profit': 'REAL DEFAULT 0'
|
||||
}
|
||||
|
||||
columns_to_add = []
|
||||
for col_name, col_type in new_columns.items():
|
||||
if col_name not in columns:
|
||||
columns_to_add.append((col_name, col_type))
|
||||
|
||||
if not columns_to_add:
|
||||
print("✅ 数据库已是最新版本,无需迁移")
|
||||
return True
|
||||
|
||||
# 执行迁移
|
||||
print(f"📝 开始迁移,将添加 {len(columns_to_add)} 个新字段...")
|
||||
|
||||
for col_name, col_type in columns_to_add:
|
||||
try:
|
||||
sql = f"ALTER TABLE paper_orders ADD COLUMN {col_name} {col_type}"
|
||||
cursor.execute(sql)
|
||||
print(f" ✅ 添加字段: {col_name}")
|
||||
except sqlite3.OperationalError as e:
|
||||
print(f" ⚠️ 添加字段 {col_name} 失败: {e}")
|
||||
|
||||
# 提交更改
|
||||
conn.commit()
|
||||
print("✅ 数据库迁移完成!")
|
||||
return True
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"❌ 数据库错误: {e}")
|
||||
return False
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def verify_migration():
|
||||
"""验证迁移结果"""
|
||||
db_paths = [
|
||||
"stock_agent.db",
|
||||
"backend/stock_agent.db",
|
||||
"../stock_agent.db"
|
||||
]
|
||||
|
||||
db_path = None
|
||||
for path in db_paths:
|
||||
if os.path.exists(path):
|
||||
db_path = path
|
||||
break
|
||||
|
||||
if not db_path:
|
||||
return
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("PRAGMA table_info(paper_orders)")
|
||||
columns = {column[1]: column[2] for column in cursor.fetchall()}
|
||||
|
||||
print("\n📋 paper_orders 表字段列表:")
|
||||
print("-" * 60)
|
||||
|
||||
required_fields = ['trailing_stop_triggered', 'trailing_stop_base_profit']
|
||||
all_present = True
|
||||
|
||||
for field in required_fields:
|
||||
if field in columns:
|
||||
print(f" ✅ {field}: {columns[field]}")
|
||||
else:
|
||||
print(f" ❌ {field}: 缺失")
|
||||
all_present = False
|
||||
|
||||
if all_present:
|
||||
print("\n✅ 所有必需字段都已存在!")
|
||||
else:
|
||||
print("\n⚠️ 部分字段缺失,请检查迁移结果")
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"❌ 验证失败: {e}")
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("🔄 Stock Agent 数据库迁移工具")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
success = migrate_database()
|
||||
|
||||
if success:
|
||||
verify_migration()
|
||||
else:
|
||||
print("\n💡 如果数据库文件不存在,启动服务时会自动创建")
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
Loading…
Reference in New Issue
Block a user