This commit is contained in:
aaron 2026-02-19 19:32:46 +08:00
parent dfb0eb9477
commit 7e8178b674
8 changed files with 694 additions and 43 deletions

View File

@ -1,20 +1,147 @@
# Tushare API # ============================================================================
# Stock Agent 环境变量配置文件
# ============================================================================
# 复制此文件为 .env 并填入你的实际配置值
# ============================================================================
# ----------------------------------------------------------------------------
# API 密钥配置
# ----------------------------------------------------------------------------
# Tushare API用于获取A股数据
TUSHARE_TOKEN=your_tushare_token_here TUSHARE_TOKEN=your_tushare_token_here
# 智谱AI GLM-4 API # 智谱AI GLM-4 API
ZHIPUAI_API_KEY=your_zhipuai_key_here 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 DATABASE_URL=sqlite:///./stock_agent.db
# API Settings # ----------------------------------------------------------------------------
# API 服务配置
# ----------------------------------------------------------------------------
API_HOST=0.0.0.0 API_HOST=0.0.0.0
API_PORT=8000 API_PORT=8000
DEBUG=True DEBUG=True
# Security # ----------------------------------------------------------------------------
# 安全配置
# ----------------------------------------------------------------------------
# JWT 密钥(生产环境必须修改)
SECRET_KEY=your_secret_key_here_change_in_production SECRET_KEY=your_secret_key_here_change_in_production
# JWT 算法
JWT_ALGORITHM=HS256
# JWT 过期天数
JWT_EXPIRE_DAYS=7
# API 访问频率限制
RATE_LIMIT=100/minute RATE_LIMIT=100/minute
# CORS # ----------------------------------------------------------------------------
# 跨域配置 (CORS)
# ----------------------------------------------------------------------------
CORS_ORIGINS=http://localhost:8000,http://127.0.0.1:8000 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

View File

@ -118,6 +118,17 @@ class Settings(BaseSettings):
paper_trading_max_orders: int = 10 # 最大持仓+挂单总数 paper_trading_max_orders: int = 10 # 最大持仓+挂单总数
paper_trading_auto_close_opposite: bool = False # 是否自动平掉反向持仓(智能策略) paper_trading_auto_close_opposite: bool = False # 是否自动平掉反向持仓(智能策略)
paper_trading_breakeven_threshold: float = 1 # 保本止损触发阈值盈利百分比0表示禁用 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_a: float = 1000 # A级信号仓位 (USDT)
paper_trading_position_b: float = 500 # B级信号仓位 (USDT) paper_trading_position_b: float = 500 # B级信号仓位 (USDT)

View File

@ -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. **量价优先** - 任何信号都必须有量能配合才可靠 1. **量价优先** - 任何信号都必须有量能配合才可靠
2. **积极但不冒进** - 有合理依据就给出信号不要过于保守 2. **积极但不冒进** - 有合理依据就给出信号不要过于保守
3. 每种类型最多输出一个信号 3. 每种类型最多输出一个信号
4. 止损必须明确风险收益比至少 1:1.5 4. 止损必须基于关键支撑/阻力位前低前高MA20不要用固定百分比
5. reason 字段必须包含量价分析"放量突破+RSI=45量比1.8确认有效" 5. 止盈设置为保险价位做多+15%做空-15%实际靠移动止损锁定利润
6. entry_type 必须明确信号已触发用 market等待更好价位用 limit 6. reason 字段必须包含量价分析"放量突破+RSI=45量比1.8确认有效"
7. 短线信号止损控制在 1-2%中线信号止损控制在 2-4% 7. entry_type 必须明确信号已触发用 market等待更好价位用 limit
8. **position_size 必须明确**根据信号质量和持仓情况给出 heavy/medium/light""" 8. **position_size 必须明确**根据信号质量和持仓情况给出 heavy/medium/light"""
def __init__(self): def __init__(self):

View File

@ -1249,29 +1249,156 @@ class SignalAnalyzer:
return "\n".join(parts) return "\n".join(parts)
def calculate_stop_loss_take_profit(self, price: float, action: str, 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: Args:
price: 当前价格 price: 当前价格
action: 'buy' 'sell' action: 'buy' 'sell'
atr: ATR atr: ATR
df: K线数据用于计算关键价位
Returns: 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': if action == 'buy':
stop_loss = price - atr * 2 stop_loss = price - atr * 1.5 # 降低到 1.5 倍 ATR
take_profit = price + atr * 3 # 止盈设置得很远,主要靠移动止损
take_profit = price * 1.15 # 15% 作为一个"保险"止盈位
method = 'atr'
elif action == 'sell': elif action == 'sell':
stop_loss = price + atr * 2 stop_loss = price + atr * 1.5
take_profit = price - atr * 3 # 止盈设置得很远,主要靠移动止损
take_profit = price * 0.85 # 15% 作为一个"保险"止盈位
method = 'atr'
else: else:
stop_loss = 0 return {'stop_loss': 0, 'take_profit': 0, 'method': 'none'}
take_profit = 0
return { return {
'stop_loss': round(stop_loss, 2), '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'
} }

View File

@ -80,6 +80,10 @@ class PaperOrder(Base):
max_profit = Column(Float, default=0) # 持仓期间最大盈利 max_profit = Column(Float, default=0) # 持仓期间最大盈利
breakeven_triggered = Column(Integer, default=0) # 是否触发过保本止损0=否1=是) 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) created_at = Column(DateTime, default=datetime.utcnow)
opened_at = Column(DateTime, nullable=True) # 开仓时间 opened_at = Column(DateTime, nullable=True) # 开仓时间
@ -114,6 +118,8 @@ class PaperOrder(Base):
'max_drawdown': self.max_drawdown, 'max_drawdown': self.max_drawdown,
'max_profit': self.max_profit, 'max_profit': self.max_profit,
'breakeven_triggered': self.breakeven_triggered, '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, 'created_at': self.created_at.isoformat() if self.created_at else None,
'opened_at': self.opened_at.isoformat() if self.opened_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, 'closed_at': self.closed_at.isoformat() if self.closed_at else None,

View File

@ -76,9 +76,21 @@ class BinanceService:
Returns: Returns:
包含 5m, 15m, 1h, 4h 数据的字典 包含 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 = {} data = {}
for interval in ['5m', '15m', '1h', '4h']: 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: if not df.empty:
df = self.calculate_indicators(df) df = self.calculate_indicators(df)
data[interval] = df data[interval] = df

View File

@ -27,13 +27,28 @@ class PaperTradingService:
self.auto_close_opposite = self.settings.paper_trading_auto_close_opposite # 是否自动平掉反向持仓 self.auto_close_opposite = self.settings.paper_trading_auto_close_opposite # 是否自动平掉反向持仓
self.breakeven_threshold = self.settings.paper_trading_breakeven_threshold # 保本止损触发阈值 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._ensure_table_exists()
# 加载活跃订单到内存 # 加载活跃订单到内存
self._load_active_orders() 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): def _ensure_table_exists(self):
"""确保数据表已创建,并迁移新字段""" """确保数据表已创建,并迁移新字段"""
@ -42,19 +57,32 @@ class PaperTradingService:
from sqlalchemy import text from sqlalchemy import text
Base.metadata.create_all(bind=db_service.engine) Base.metadata.create_all(bind=db_service.engine)
# 检查并添加新字段 breakeven_triggered
db = db_service.get_session() db = db_service.get_session()
try: try:
# 尝试查询新字段,如果失败则添加 # 检查并添加新字段 breakeven_triggered
db.execute(text("SELECT breakeven_triggered FROM paper_orders LIMIT 1"))
except Exception:
try: try:
db.execute(text("ALTER TABLE paper_orders ADD COLUMN breakeven_triggered INTEGER DEFAULT 0")) db.execute(text("SELECT breakeven_triggered FROM paper_orders LIMIT 1"))
db.commit() except Exception:
logger.info("数据库迁移: 添加 breakeven_triggered 字段") try:
except Exception as e: db.execute(text("ALTER TABLE paper_orders ADD COLUMN breakeven_triggered INTEGER DEFAULT 0"))
logger.warning(f"添加 breakeven_triggered 字段失败(可能已存在): {e}") db.commit()
db.rollback() 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: finally:
db.close() db.close()
@ -541,8 +569,79 @@ class PaperTradingService:
finally: finally:
db.close() 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]]: def _update_order_extremes(self, order: PaperOrder, current_price: float) -> Optional[Dict[str, Any]]:
"""更新订单的最大回撤和最大盈利,并检查是否需要移动止损到保本""" """
更新订单的最大回撤和最大盈利并检查是否需要移动止损
止损策略按优先级
1. 移动止损盈利达到阈值倍数后止损按比例跟随盈利移动
2. 保本止损盈利达到阈值时将止损移动到开仓价
Returns:
如果触发了止损移动返回通知字典否则返回 None
"""
if order.side == OrderSide.LONG: if order.side == OrderSide.LONG:
current_pnl_percent = ((current_price - order.filled_price) / order.filled_price) * 100 current_pnl_percent = ((current_price - order.filled_price) / order.filled_price) * 100
else: else:
@ -550,7 +649,9 @@ class PaperTradingService:
# 检查是否需要更新极值 # 检查是否需要更新极值
needs_update = False 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: if current_pnl_percent > order.max_profit:
order.max_profit = current_pnl_percent order.max_profit = current_pnl_percent
@ -560,17 +661,93 @@ class PaperTradingService:
order.max_drawdown = current_pnl_percent order.max_drawdown = current_pnl_percent
needs_update = True 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 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.side == OrderSide.LONG:
# 做多:止损应该低于开仓价,如果止损还在开仓价下方,则移动到开仓价 # 做多:止损应该低于开仓价,如果止损还在开仓价下方,则移动到开仓价
if order.stop_loss < order.filled_price: if order.stop_loss < order.filled_price:
order.stop_loss = order.filled_price order.stop_loss = order.filled_price
order.breakeven_triggered = 1 order.breakeven_triggered = 1
needs_update = True 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}") logger.info(f"保本止损触发: {order.order_id} | {order.symbol} | 盈利 {current_pnl_percent:.2f}% >= {self.breakeven_threshold}% | 止损移至开仓价 ${order.filled_price:,.2f}")
else: else:
# 做空:止损应该高于开仓价,如果止损还在开仓价上方,则移动到开仓价 # 做空:止损应该高于开仓价,如果止损还在开仓价上方,则移动到开仓价
@ -578,7 +755,9 @@ class PaperTradingService:
order.stop_loss = order.filled_price order.stop_loss = order.filled_price
order.breakeven_triggered = 1 order.breakeven_triggered = 1
needs_update = True 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}") 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_profit = order.max_profit
db_order.max_drawdown = order.max_drawdown db_order.max_drawdown = order.max_drawdown
db_order.stop_loss = order.stop_loss 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() db.commit()
except Exception as e: except Exception as e:
logger.error(f"更新订单极值失败: {e}") logger.error(f"更新订单极值失败: {e}")
@ -598,15 +782,16 @@ class PaperTradingService:
finally: finally:
db.close() db.close()
# 如果触发了保本止损,返回通知信息 # 如果触发了止损移动,返回通知信息
if breakeven_triggered: if stop_moved and new_stop_loss is not None:
return { return {
'event_type': 'breakeven_triggered', 'event_type': 'stop_loss_moved',
'order_id': order.order_id, 'order_id': order.order_id,
'symbol': order.symbol, 'symbol': order.symbol,
'side': order.side.value, 'side': order.side.value,
'move_type': stop_move_type,
'filled_price': order.filled_price, 'filled_price': order.filled_price,
'new_stop_loss': order.stop_loss, 'new_stop_loss': new_stop_loss,
'current_pnl_percent': current_pnl_percent 'current_pnl_percent': current_pnl_percent
} }

149
backend/migrate_db.py Normal file
View 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)