feat(config): 添加市场状态方案以优化交易策略
在配置管理中引入市场状态方案,允许在不同市场条件下快速切换策略(如熊市、牛市、正常、保守)。更新相关参数以自动覆盖止损、仓位和趋势过滤设置,增强策略灵活性。同时,前端组件更新以支持市场状态方案的展示与选择,提升用户体验。
This commit is contained in:
parent
3b0526f392
commit
452e40bdf5
|
|
@ -814,7 +814,7 @@ class ConfigManager:
|
||||||
short_filter_default = False if is_fast else True
|
short_filter_default = False if is_fast else True
|
||||||
max_trend_move_default = 0.08 if is_fast else 0.05
|
max_trend_move_default = 0.08 if is_fast else 0.05
|
||||||
|
|
||||||
return {
|
result = {
|
||||||
# 仓位控制
|
# 仓位控制
|
||||||
'MAX_POSITION_PERCENT': eff_get('MAX_POSITION_PERCENT', 0.12), # 单笔最大保证金占比(12%,加大单笔盈利空间)
|
'MAX_POSITION_PERCENT': eff_get('MAX_POSITION_PERCENT', 0.12), # 单笔最大保证金占比(12%,加大单笔盈利空间)
|
||||||
'MAX_TOTAL_POSITION_PERCENT': eff_get('MAX_TOTAL_POSITION_PERCENT', 0.40), # 总保证金占比上限
|
'MAX_TOTAL_POSITION_PERCENT': eff_get('MAX_TOTAL_POSITION_PERCENT', 0.40), # 总保证金占比上限
|
||||||
|
|
@ -947,8 +947,47 @@ class ConfigManager:
|
||||||
'SYMBOL_LOSS_COOLDOWN_SEC': eff_get('SYMBOL_LOSS_COOLDOWN_SEC', 3600),
|
'SYMBOL_LOSS_COOLDOWN_SEC': eff_get('SYMBOL_LOSS_COOLDOWN_SEC', 3600),
|
||||||
# 第一目标止盈最小盈亏比(相对止损距离)
|
# 第一目标止盈最小盈亏比(相对止损距离)
|
||||||
'MIN_RR_FOR_TP1': eff_get('MIN_RR_FOR_TP1', 1.5), # 2026-02-12:保证 TP1 至少 1.5 倍止损距离,改善盈亏比
|
'MIN_RR_FOR_TP1': eff_get('MIN_RR_FOR_TP1', 1.5), # 2026-02-12:保证 TP1 至少 1.5 倍止损距离,改善盈亏比
|
||||||
|
|
||||||
|
# 市场状态方案(便于在不同行情间切换)
|
||||||
|
'MARKET_SCHEME': str(eff_get('MARKET_SCHEME', 'normal') or 'normal').lower(),
|
||||||
|
'BLOCK_LONG_WHEN_4H_DOWN': eff_get('BLOCK_LONG_WHEN_4H_DOWN', False), # 4H 下跌时禁止开多(熊市/保守用)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 根据市场方案覆盖关键参数(便于快速切换熊市/牛市/保守等预设)
|
||||||
|
_SCHEME_PRESETS = {
|
||||||
|
'normal': {
|
||||||
|
'MIN_STOP_LOSS_PRICE_PCT': 0.03,
|
||||||
|
'MAX_POSITION_PERCENT': 0.12,
|
||||||
|
'ATR_STOP_LOSS_MULTIPLIER': 2.5,
|
||||||
|
'BLOCK_LONG_WHEN_4H_DOWN': False,
|
||||||
|
},
|
||||||
|
'bear': {
|
||||||
|
'MIN_STOP_LOSS_PRICE_PCT': 0.05, # 放宽止损约 -5%
|
||||||
|
'MAX_POSITION_PERCENT': 0.08, # 单仓 ≤ 8%
|
||||||
|
'ATR_STOP_LOSS_MULTIPLIER': 2.5,
|
||||||
|
'BLOCK_LONG_WHEN_4H_DOWN': True, # 4H 下跌不开多
|
||||||
|
'BETA_FILTER_ENABLED': True,
|
||||||
|
},
|
||||||
|
'bull': {
|
||||||
|
'MIN_STOP_LOSS_PRICE_PCT': 0.03,
|
||||||
|
'MAX_POSITION_PERCENT': 0.12,
|
||||||
|
'ATR_STOP_LOSS_MULTIPLIER': 2.0,
|
||||||
|
'BLOCK_LONG_WHEN_4H_DOWN': False,
|
||||||
|
},
|
||||||
|
'conservative': {
|
||||||
|
'MIN_STOP_LOSS_PRICE_PCT': 0.06, # 最宽松止损
|
||||||
|
'MAX_POSITION_PERCENT': 0.06, # 最小仓位
|
||||||
|
'ATR_STOP_LOSS_MULTIPLIER': 2.5,
|
||||||
|
'BLOCK_LONG_WHEN_4H_DOWN': True,
|
||||||
|
'BETA_FILTER_ENABLED': True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
scheme = result.get('MARKET_SCHEME', 'normal') or 'normal'
|
||||||
|
if scheme in _SCHEME_PRESETS:
|
||||||
|
for k, v in _SCHEME_PRESETS[scheme].items():
|
||||||
|
result[k] = v
|
||||||
|
return result
|
||||||
|
|
||||||
def _sync_to_redis(self):
|
def _sync_to_redis(self):
|
||||||
"""将配置同步到Redis缓存(账号维度)"""
|
"""将配置同步到Redis缓存(账号维度)"""
|
||||||
if not self._redis_connected or not self._redis_client:
|
if not self._redis_connected or not self._redis_client:
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,13 @@ DEFAULTS_TO_SYNC = [
|
||||||
"description": "止盈按保证金封顶,避免过远"},
|
"description": "止盈按保证金封顶,避免过远"},
|
||||||
{"config_key": "USE_MARGIN_CAP_FOR_SL", "config_value": "true", "config_type": "boolean", "category": "risk",
|
{"config_key": "USE_MARGIN_CAP_FOR_SL", "config_value": "true", "config_type": "boolean", "category": "risk",
|
||||||
"description": "止损按保证金封顶,避免扛单"},
|
"description": "止损按保证金封顶,避免扛单"},
|
||||||
|
# 市场状态方案(2026-02 三项优化 + 方案切换)
|
||||||
|
{"config_key": "MARKET_SCHEME", "config_value": "normal", "config_type": "string", "category": "strategy",
|
||||||
|
"description": "市场方案:normal / bear / bull / conservative。切换后自动覆盖止损、仓位、趋势过滤等参数。"},
|
||||||
|
{"config_key": "BLOCK_LONG_WHEN_4H_DOWN", "config_value": "false", "config_type": "boolean", "category": "strategy",
|
||||||
|
"description": "4H 趋势下跌时禁止开多。bear / conservative 方案下自动为 true。"},
|
||||||
|
{"config_key": "AUTO_MARKET_SCHEME_ENABLED", "config_value": "false", "config_type": "boolean", "category": "strategy",
|
||||||
|
"description": "开启后,crontab 定时运行 scripts/update_market_scheme.py --apply 时自动更新 MARKET_SCHEME(根据 BTC 行情识别牛/熊/正常)。"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -381,6 +381,57 @@ const GlobalConfig = () => {
|
||||||
FIXED_RISK_PERCENT: 0.01, // 1% 风险
|
FIXED_RISK_PERCENT: 0.01, // 1% 风险
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// 市场状态方案:便于在不同行情间一键切换(熊市/正常/牛市/保守)
|
||||||
|
scheme_bear: {
|
||||||
|
name: '熊市',
|
||||||
|
desc: '【熊市专用】放宽止损5%、单仓≤8%、4H下跌禁止开多、大盘过滤开启。避免逆势抄底、减少回撤。',
|
||||||
|
signatureKeys: ['MARKET_SCHEME'],
|
||||||
|
configs: {
|
||||||
|
MARKET_SCHEME: 'bear',
|
||||||
|
MIN_STOP_LOSS_PRICE_PCT: 0.05,
|
||||||
|
MAX_POSITION_PERCENT: 0.08,
|
||||||
|
ATR_STOP_LOSS_MULTIPLIER: 2.5,
|
||||||
|
BLOCK_LONG_WHEN_4H_DOWN: true,
|
||||||
|
BETA_FILTER_ENABLED: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scheme_normal: {
|
||||||
|
name: '正常',
|
||||||
|
desc: '【常规市场】止损3%、单仓12%、4H下跌可开多。适合震荡或温和行情。',
|
||||||
|
signatureKeys: ['MARKET_SCHEME'],
|
||||||
|
configs: {
|
||||||
|
MARKET_SCHEME: 'normal',
|
||||||
|
MIN_STOP_LOSS_PRICE_PCT: 0.03,
|
||||||
|
MAX_POSITION_PERCENT: 0.12,
|
||||||
|
ATR_STOP_LOSS_MULTIPLIER: 2.5,
|
||||||
|
BLOCK_LONG_WHEN_4H_DOWN: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scheme_bull: {
|
||||||
|
name: '牛市',
|
||||||
|
desc: '【牛市专用】止损3%、单仓12%、4H下跌可开多、ATR倍数略低。偏积极。',
|
||||||
|
signatureKeys: ['MARKET_SCHEME'],
|
||||||
|
configs: {
|
||||||
|
MARKET_SCHEME: 'bull',
|
||||||
|
MIN_STOP_LOSS_PRICE_PCT: 0.03,
|
||||||
|
MAX_POSITION_PERCENT: 0.12,
|
||||||
|
ATR_STOP_LOSS_MULTIPLIER: 2.0,
|
||||||
|
BLOCK_LONG_WHEN_4H_DOWN: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scheme_conservative: {
|
||||||
|
name: '保守',
|
||||||
|
desc: '【极保守】止损6%、单仓6%、4H下跌禁止开多。最小仓位、最严趋势过滤。',
|
||||||
|
signatureKeys: ['MARKET_SCHEME'],
|
||||||
|
configs: {
|
||||||
|
MARKET_SCHEME: 'conservative',
|
||||||
|
MIN_STOP_LOSS_PRICE_PCT: 0.06,
|
||||||
|
MAX_POSITION_PERCENT: 0.06,
|
||||||
|
ATR_STOP_LOSS_MULTIPLIER: 2.5,
|
||||||
|
BLOCK_LONG_WHEN_4H_DOWN: true,
|
||||||
|
BETA_FILTER_ENABLED: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
aggressive: {
|
aggressive: {
|
||||||
name: '激进短线(高风险)',
|
name: '激进短线(高风险)',
|
||||||
desc: '针对晚间高波动:2%风险/单,交易频率高,止损较紧但止盈合理。注意:回撤风险较高。',
|
desc: '针对晚间高波动:2%风险/单,交易频率高,止损较紧但止盈合理。注意:回撤风险较高。',
|
||||||
|
|
@ -445,6 +496,9 @@ const GlobalConfig = () => {
|
||||||
SYNC_RECOVER_MISSING_POSITIONS: { value: true, type: 'boolean', category: 'position', description: '同步时补建「币安有仓、DB 无记录」的交易记录。' },
|
SYNC_RECOVER_MISSING_POSITIONS: { value: true, type: 'boolean', category: 'position', description: '同步时补建「币安有仓、DB 无记录」的交易记录。' },
|
||||||
SYNC_RECOVER_ONLY_WHEN_HAS_SLTP: { value: true, type: 'boolean', category: 'position', description: '仅当该持仓存在止损/止盈单时才补建(未配置 SYSTEM_ORDER_ID_PREFIX 时生效)。' },
|
SYNC_RECOVER_ONLY_WHEN_HAS_SLTP: { value: true, type: 'boolean', category: 'position', description: '仅当该持仓存在止损/止盈单时才补建(未配置 SYSTEM_ORDER_ID_PREFIX 时生效)。' },
|
||||||
SYSTEM_ORDER_ID_PREFIX: { value: 'SYS', type: 'string', category: 'position', description: '系统单标识:下单时写入 newClientOrderId 前缀,同步时仅对「开仓订单 clientOrderId 以此前缀开头」的持仓补建;设空则用「是否有止损止盈单」判断。' },
|
SYSTEM_ORDER_ID_PREFIX: { value: 'SYS', type: 'string', category: 'position', description: '系统单标识:下单时写入 newClientOrderId 前缀,同步时仅对「开仓订单 clientOrderId 以此前缀开头」的持仓补建;设空则用「是否有止损止盈单」判断。' },
|
||||||
|
MARKET_SCHEME: { value: 'normal', type: 'string', category: 'strategy', description: '市场方案:normal / bear / bull / conservative。切换后自动覆盖止损、仓位、4H趋势过滤等参数。' },
|
||||||
|
BLOCK_LONG_WHEN_4H_DOWN: { value: false, type: 'boolean', category: 'strategy', description: '4H 趋势下跌时禁止开多。bear / conservative 方案下自动为 true。' },
|
||||||
|
AUTO_MARKET_SCHEME_ENABLED: { value: false, type: 'boolean', category: 'strategy', description: '开启后,crontab 定时运行 update_market_scheme.py --apply 时自动更新 MARKET_SCHEME(根据 BTC 行情识别牛/熊/正常)。' },
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadConfigs = async () => {
|
const loadConfigs = async () => {
|
||||||
|
|
@ -967,9 +1021,13 @@ const GlobalConfig = () => {
|
||||||
swing: { group: 'backup', tag: '纯限价' },
|
swing: { group: 'backup', tag: '纯限价' },
|
||||||
strict: { group: 'backup', tag: '精选低频' },
|
strict: { group: 'backup', tag: '精选低频' },
|
||||||
conservative: { group: 'backup', tag: '保守' },
|
conservative: { group: 'backup', tag: '保守' },
|
||||||
|
scheme_bear: { group: 'scheme', tag: '熊市' },
|
||||||
|
scheme_normal: { group: 'scheme', tag: '正常' },
|
||||||
|
scheme_bull: { group: 'scheme', tag: '牛市' },
|
||||||
|
scheme_conservative: { group: 'scheme', tag: '保守' },
|
||||||
}
|
}
|
||||||
|
|
||||||
// 快速方案:当前推荐(含增加机会/盈利放大/之前盈利)+ 备选
|
// 快速方案:当前推荐(含增加机会/盈利放大/之前盈利)+ 备选 + 市场状态方案
|
||||||
const presetGroups = [
|
const presetGroups = [
|
||||||
{
|
{
|
||||||
key: 'altcoin',
|
key: 'altcoin',
|
||||||
|
|
@ -983,6 +1041,12 @@ const GlobalConfig = () => {
|
||||||
desc: '需要更少出单或纯限价时可选:波段回归、精选低频、保守传统。',
|
desc: '需要更少出单或纯限价时可选:波段回归、精选低频、保守传统。',
|
||||||
presetKeys: ['swing', 'strict', 'conservative'],
|
presetKeys: ['swing', 'strict', 'conservative'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'scheme',
|
||||||
|
title: '市场状态方案',
|
||||||
|
desc: '按当前行情切换:熊市(放宽止损、降仓、4H 下跌不开多)→ 正常 → 牛市 → 保守(最小仓位、最严过滤)。',
|
||||||
|
presetKeys: ['scheme_bear', 'scheme_normal', 'scheme_bull', 'scheme_conservative'],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
62
scripts/UPDATE_MARKET_SCHEME_README.md
Normal file
62
scripts/UPDATE_MARKET_SCHEME_README.md
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
# 市场行情自动识别与 MARKET_SCHEME 更新
|
||||||
|
|
||||||
|
## 1. 功能说明
|
||||||
|
|
||||||
|
`update_market_scheme.py` 基于 Binance 公开 API 自动识别当前市场行情(牛/熊/正常),并可选更新全局配置 `MARKET_SCHEME`,从而自动切换止损、仓位、4H 趋势过滤等参数。
|
||||||
|
|
||||||
|
**数据来源(无需 API Key):**
|
||||||
|
|
||||||
|
- 24h 涨跌幅:`/fapi/v1/ticker/24hr`
|
||||||
|
- 资金费率:`/fapi/v1/premiumIndex`
|
||||||
|
- 大户多空比:`/futures/data/topLongShortPositionRatio`
|
||||||
|
- 4H K 线:`/fapi/v1/klines`(计算 trend_4h)
|
||||||
|
|
||||||
|
**打分规则:**
|
||||||
|
|
||||||
|
| 指标 | 偏多 (+1) | 偏空 (-1) |
|
||||||
|
|--------------|-------------|--------------|
|
||||||
|
| 24h 涨跌幅 | ≥ 2% | ≤ -2% |
|
||||||
|
| 4H 趋势 | up | down |
|
||||||
|
| 资金费率 | ≥ 0.00005 | ≤ -0.00005 |
|
||||||
|
| 大户多空比 | ≥ 1.1 | ≤ 0.9 |
|
||||||
|
|
||||||
|
总分 ≥ 2 → bull,≤ -2 → bear,否则 normal。
|
||||||
|
|
||||||
|
## 2. 使用方式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 仅检测并打印(不更新 DB)
|
||||||
|
python scripts/update_market_scheme.py
|
||||||
|
|
||||||
|
# 若开启 AUTO_MARKET_SCHEME_ENABLED,则更新 DB
|
||||||
|
python scripts/update_market_scheme.py --apply
|
||||||
|
|
||||||
|
# 强制更新(忽略 AUTO_MARKET_SCHEME_ENABLED 开关)
|
||||||
|
python scripts/update_market_scheme.py --apply --force
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 启用自动更新
|
||||||
|
|
||||||
|
1. 在「全局配置」中设置 `AUTO_MARKET_SCHEME_ENABLED` 为 `true`
|
||||||
|
2. 或执行一次同步默认配置:`cd backend && python sync_global_config_defaults.py`,然后在全局配置页手动改为 true
|
||||||
|
3. 配置 crontab 定时执行 `update_market_scheme.py --apply`
|
||||||
|
|
||||||
|
## 4. Crontab 配置示例
|
||||||
|
|
||||||
|
每 4 小时执行一次(0 点、4 点、8 点…):
|
||||||
|
|
||||||
|
```cron
|
||||||
|
0 */4 * * * cd /path/to/auto_trade_sys && /path/to/.venv/bin/python scripts/update_market_scheme.py --apply >> /tmp/update_market_scheme.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
或每 6 小时:
|
||||||
|
|
||||||
|
```cron
|
||||||
|
0 */6 * * * cd /path/to/auto_trade_sys && /path/to/.venv/bin/python scripts/update_market_scheme.py --apply >> /tmp/update_market_scheme.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 注意事项
|
||||||
|
|
||||||
|
- 脚本会清除 `global_strategy_config_v5` Redis 缓存,使交易进程下次读取时从 DB 获取最新配置
|
||||||
|
- 若希望立即生效,可在前端点击「清除全局缓存」并「重启所有账号交易」
|
||||||
|
- 若不开启 `AUTO_MARKET_SCHEME_ENABLED`,脚本仅打印识别结果,不修改 DB;可用 `--force` 临时强制更新
|
||||||
117
scripts/update_market_scheme.py
Normal file
117
scripts/update_market_scheme.py
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
定时任务:自动识别市场行情并更新 MARKET_SCHEME(需开启 AUTO_MARKET_SCHEME_ENABLED)。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
cd /path/to/auto_trade_sys && python scripts/update_market_scheme.py # 仅检测并打印
|
||||||
|
python scripts/update_market_scheme.py --apply # 若开启自动模式则更新 DB
|
||||||
|
python scripts/update_market_scheme.py --apply --force # 强制更新(忽略 AUTO 开关)
|
||||||
|
|
||||||
|
crontab 示例(每 4 小时执行):
|
||||||
|
0 */4 * * * cd /path/to/auto_trade_sys && python scripts/update_market_scheme.py --apply >> /tmp/update_market_scheme.log 2>&1
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 项目根目录
|
||||||
|
proj = Path(__file__).resolve().parent.parent
|
||||||
|
sys.path.insert(0, str(proj / "backend"))
|
||||||
|
sys.path.insert(0, str(proj))
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_auto_enabled() -> bool:
|
||||||
|
"""从 DB 读取 AUTO_MARKET_SCHEME_ENABLED,默认 False。"""
|
||||||
|
try:
|
||||||
|
from database.models import GlobalStrategyConfig
|
||||||
|
val = GlobalStrategyConfig.get_value("AUTO_MARKET_SCHEME_ENABLED", False)
|
||||||
|
if isinstance(val, bool):
|
||||||
|
return val
|
||||||
|
if isinstance(val, str):
|
||||||
|
return val.lower() in ("true", "1", "yes")
|
||||||
|
return bool(val)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"读取 AUTO_MARKET_SCHEME_ENABLED 失败: {e},默认不自动更新")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _update_market_scheme(scheme: str) -> bool:
|
||||||
|
"""更新 MARKET_SCHEME 到 DB,并清除全局配置 Redis 缓存。"""
|
||||||
|
try:
|
||||||
|
from database.models import GlobalStrategyConfig
|
||||||
|
except ImportError as e:
|
||||||
|
logger.error(f"导入 GlobalStrategyConfig 失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
GlobalStrategyConfig.set(
|
||||||
|
"MARKET_SCHEME",
|
||||||
|
scheme,
|
||||||
|
"string",
|
||||||
|
"strategy",
|
||||||
|
"市场方案:normal / bear / bull / conservative。由 update_market_scheme 定时任务自动更新。",
|
||||||
|
updated_by="update_market_scheme",
|
||||||
|
)
|
||||||
|
logger.info(f"已更新 MARKET_SCHEME = {scheme}")
|
||||||
|
|
||||||
|
# 清除全局配置 Redis 缓存,让 trading_system 下次读取时从 DB 获取
|
||||||
|
try:
|
||||||
|
import redis
|
||||||
|
redis_url = os.getenv("REDIS_URL", "redis://localhost:6379")
|
||||||
|
redis_use_tls = os.getenv("REDIS_USE_TLS", "False").lower() == "true"
|
||||||
|
redis_username = os.getenv("REDIS_USERNAME", None)
|
||||||
|
redis_password = os.getenv("REDIS_PASSWORD", None)
|
||||||
|
if redis_use_tls and not redis_url.startswith("rediss://"):
|
||||||
|
redis_url = redis_url.replace("redis://", "rediss://", 1)
|
||||||
|
r = redis.from_url(
|
||||||
|
redis_url,
|
||||||
|
username=redis_username,
|
||||||
|
password=redis_password,
|
||||||
|
decode_responses=True,
|
||||||
|
)
|
||||||
|
r.ping()
|
||||||
|
r.delete("global_strategy_config_v5")
|
||||||
|
logger.info("已清除全局配置 Redis 缓存")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"清除 Redis 缓存失败(不影响 DB 更新): {e}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"更新 MARKET_SCHEME 失败: {e}", exc_info=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="自动识别市场行情并可选更新 MARKET_SCHEME")
|
||||||
|
parser.add_argument("--apply", action="store_true", help="若开启自动模式则更新 DB")
|
||||||
|
parser.add_argument("--force", action="store_true", help="强制更新(忽略 AUTO_MARKET_SCHEME_ENABLED)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
from trading_system.market_regime_detector import detect_market_regime
|
||||||
|
|
||||||
|
regime, details = detect_market_regime()
|
||||||
|
logger.info(f"市场行情识别: {regime} | 详情: {details}")
|
||||||
|
|
||||||
|
if not args.apply:
|
||||||
|
print(f"REGIME={regime} DETAILS={details}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if not args.force and not _get_auto_enabled():
|
||||||
|
logger.info("AUTO_MARKET_SCHEME_ENABLED 未开启,跳过更新。如需强制更新请加 --force")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
ok = _update_market_scheme(regime)
|
||||||
|
return 0 if ok else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
257
trading_system/market_regime_detector.py
Normal file
257
trading_system/market_regime_detector.py
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
"""
|
||||||
|
市场行情自动识别模块
|
||||||
|
|
||||||
|
基于 Binance 公开 API 判断当前市场状态(牛/熊/正常),用于自动切换 MARKET_SCHEME。
|
||||||
|
可被 crontab 定时任务调用(如每 4 小时执行一次)。
|
||||||
|
|
||||||
|
使用的接口(无需 API Key):
|
||||||
|
- GET /fapi/v1/ticker/24hr:24h 涨跌幅
|
||||||
|
- GET /fapi/v1/premiumIndex:资金费率
|
||||||
|
- GET /futures/data/topLongShortPositionRatio:大户多空比
|
||||||
|
- GET /fapi/v1/klines:4H K 线(计算 trend_4h)
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import ssl
|
||||||
|
import urllib.request
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
BINANCE_FUTURES_BASE = "https://fapi.binance.com"
|
||||||
|
BINANCE_FUTURES_DATA = "https://fapi.binance.com/futures/data"
|
||||||
|
REQUEST_TIMEOUT = 15
|
||||||
|
|
||||||
|
|
||||||
|
def _http_get(url: str, params: Optional[dict] = None) -> Optional[dict]:
|
||||||
|
"""发起 GET 请求,返回 JSON 或 None。"""
|
||||||
|
if params:
|
||||||
|
qs = "&".join(f"{k}={v}" for k, v in params.items())
|
||||||
|
url = f"{url}?{qs}"
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(url, headers={"Accept": "application/json"})
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT, context=ctx) as resp:
|
||||||
|
raw = resp.read().decode("utf-8")
|
||||||
|
data = json.loads(raw)
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"HTTP GET 失败 {url[:80]}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_btc_24h_change() -> Optional[float]:
|
||||||
|
"""获取 BTCUSDT 24h 涨跌幅百分比。"""
|
||||||
|
data = _http_get(f"{BINANCE_FUTURES_BASE}/fapi/v1/ticker/24hr", {"symbol": "BTCUSDT"})
|
||||||
|
if not data or not isinstance(data, dict):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(data.get("priceChangePercent", 0))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_btc_funding_rate() -> Optional[float]:
|
||||||
|
"""获取 BTCUSDT 最近资金费率。正=多头付费,负=空头付费。"""
|
||||||
|
data = _http_get(f"{BINANCE_FUTURES_BASE}/fapi/v1/premiumIndex", {"symbol": "BTCUSDT"})
|
||||||
|
if not data or not isinstance(data, dict):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(data.get("lastFundingRate", 0))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_btc_long_short_ratio(period: str = "1d", limit: int = 1) -> Optional[float]:
|
||||||
|
"""获取 BTCUSDT 大户持仓多空比。>1 偏多,<1 偏空。"""
|
||||||
|
data = _http_get(
|
||||||
|
f"{BINANCE_FUTURES_DATA}/topLongShortPositionRatio",
|
||||||
|
{"symbol": "BTCUSDT", "period": period, "limit": limit},
|
||||||
|
)
|
||||||
|
if not data or not isinstance(data, list) or len(data) == 0:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(data[-1].get("longShortRatio", 1))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_btc_klines_4h(limit: int = 60) -> Optional[list]:
|
||||||
|
"""获取 BTCUSDT 4H K 线。"""
|
||||||
|
data = _http_get(
|
||||||
|
f"{BINANCE_FUTURES_BASE}/fapi/v1/klines",
|
||||||
|
{"symbol": "BTCUSDT", "interval": "4h", "limit": limit},
|
||||||
|
)
|
||||||
|
if not data or not isinstance(data, list):
|
||||||
|
return None
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _judge_trend_4h(
|
||||||
|
price_4h: float,
|
||||||
|
ema20_4h: Optional[float],
|
||||||
|
ema50_4h: Optional[float],
|
||||||
|
macd_hist: Optional[float],
|
||||||
|
) -> str:
|
||||||
|
"""根据 4H 价格、EMA、MACD 判断趋势方向。"""
|
||||||
|
if ema20_4h is None:
|
||||||
|
return "neutral"
|
||||||
|
|
||||||
|
score = 0
|
||||||
|
total = 0
|
||||||
|
|
||||||
|
if price_4h > ema20_4h:
|
||||||
|
score += 1
|
||||||
|
elif price_4h < ema20_4h:
|
||||||
|
score -= 1
|
||||||
|
total += 1
|
||||||
|
|
||||||
|
if ema50_4h is not None:
|
||||||
|
if ema20_4h > ema50_4h:
|
||||||
|
score += 1
|
||||||
|
elif ema20_4h < ema50_4h:
|
||||||
|
score -= 1
|
||||||
|
total += 1
|
||||||
|
|
||||||
|
if macd_hist is not None:
|
||||||
|
if macd_hist > 0:
|
||||||
|
score += 1
|
||||||
|
elif macd_hist < 0:
|
||||||
|
score -= 1
|
||||||
|
total += 1
|
||||||
|
|
||||||
|
if total == 0:
|
||||||
|
return "neutral"
|
||||||
|
if score >= 2:
|
||||||
|
return "up"
|
||||||
|
if score <= -2:
|
||||||
|
return "down"
|
||||||
|
return "neutral"
|
||||||
|
|
||||||
|
|
||||||
|
def compute_trend_4h_from_klines(klines: list) -> str:
|
||||||
|
"""从 4H K 线计算 trend_4h。"""
|
||||||
|
try:
|
||||||
|
from .indicators import TechnicalIndicators
|
||||||
|
except ImportError:
|
||||||
|
from indicators import TechnicalIndicators
|
||||||
|
|
||||||
|
closes = [float(k[4]) for k in klines] # 收盘价
|
||||||
|
if len(closes) < 50:
|
||||||
|
return "neutral"
|
||||||
|
|
||||||
|
ema20 = TechnicalIndicators.calculate_ema(closes, 20)
|
||||||
|
ema50 = TechnicalIndicators.calculate_ema(closes, 50)
|
||||||
|
macd = TechnicalIndicators.calculate_macd(closes, 12, 26, 9)
|
||||||
|
macd_hist = macd.get("histogram") if macd else None
|
||||||
|
|
||||||
|
price_4h = closes[-1]
|
||||||
|
return _judge_trend_4h(price_4h, ema20, ema50, macd_hist)
|
||||||
|
|
||||||
|
|
||||||
|
def detect_regime(
|
||||||
|
change_24h: Optional[float] = None,
|
||||||
|
funding_rate: Optional[float] = None,
|
||||||
|
long_short_ratio: Optional[float] = None,
|
||||||
|
trend_4h: Optional[str] = None,
|
||||||
|
*,
|
||||||
|
change_bull: float = 2.0,
|
||||||
|
change_bear: float = -2.0,
|
||||||
|
funding_bull: float = 0.00005,
|
||||||
|
funding_bear: float = -0.00005,
|
||||||
|
lsr_bull: float = 1.1,
|
||||||
|
lsr_bear: float = 0.9,
|
||||||
|
) -> Tuple[str, dict]:
|
||||||
|
"""
|
||||||
|
根据各项指标打分,综合判断市场行情。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(regime, details): regime 为 'bull'|'bear'|'normal',details 为各项得分详情。
|
||||||
|
"""
|
||||||
|
score = 0
|
||||||
|
details = {}
|
||||||
|
|
||||||
|
# 1. 24h 涨跌幅
|
||||||
|
if change_24h is not None:
|
||||||
|
details["change_24h"] = round(change_24h, 2)
|
||||||
|
if change_24h >= change_bull:
|
||||||
|
score += 1
|
||||||
|
details["change_score"] = 1
|
||||||
|
elif change_24h <= change_bear:
|
||||||
|
score -= 1
|
||||||
|
details["change_score"] = -1
|
||||||
|
else:
|
||||||
|
details["change_score"] = 0
|
||||||
|
|
||||||
|
# 2. 4H 趋势
|
||||||
|
if trend_4h:
|
||||||
|
details["trend_4h"] = trend_4h
|
||||||
|
if trend_4h == "up":
|
||||||
|
score += 1
|
||||||
|
details["trend_score"] = 1
|
||||||
|
elif trend_4h == "down":
|
||||||
|
score -= 1
|
||||||
|
details["trend_score"] = -1
|
||||||
|
else:
|
||||||
|
details["trend_score"] = 0
|
||||||
|
|
||||||
|
# 3. 资金费率
|
||||||
|
if funding_rate is not None:
|
||||||
|
details["funding_rate"] = round(funding_rate, 6)
|
||||||
|
if funding_rate >= funding_bull:
|
||||||
|
score += 1
|
||||||
|
details["funding_score"] = 1
|
||||||
|
elif funding_rate <= funding_bear:
|
||||||
|
score -= 1
|
||||||
|
details["funding_score"] = -1
|
||||||
|
else:
|
||||||
|
details["funding_score"] = 0
|
||||||
|
|
||||||
|
# 4. 大户多空比
|
||||||
|
if long_short_ratio is not None:
|
||||||
|
details["long_short_ratio"] = round(long_short_ratio, 4)
|
||||||
|
if long_short_ratio >= lsr_bull:
|
||||||
|
score += 1
|
||||||
|
details["lsr_score"] = 1
|
||||||
|
elif long_short_ratio <= lsr_bear:
|
||||||
|
score -= 1
|
||||||
|
details["lsr_score"] = -1
|
||||||
|
else:
|
||||||
|
details["lsr_score"] = 0
|
||||||
|
|
||||||
|
details["total_score"] = score
|
||||||
|
|
||||||
|
if score >= 2:
|
||||||
|
return "bull", details
|
||||||
|
if score <= -2:
|
||||||
|
return "bear", details
|
||||||
|
return "normal", details
|
||||||
|
|
||||||
|
|
||||||
|
def detect_market_regime(symbol: str = "BTCUSDT") -> Tuple[str, dict]:
|
||||||
|
"""
|
||||||
|
主入口:拉取 Binance 数据并判断市场行情。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
symbol: 基准交易对,默认 BTCUSDT
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(regime, details): regime 为 'bull'|'bear'|'normal'
|
||||||
|
"""
|
||||||
|
if symbol != "BTCUSDT":
|
||||||
|
logger.warning("当前仅支持 BTCUSDT,将使用 BTCUSDT")
|
||||||
|
symbol = "BTCUSDT"
|
||||||
|
|
||||||
|
change_24h = fetch_btc_24h_change()
|
||||||
|
funding_rate = fetch_btc_funding_rate()
|
||||||
|
long_short_ratio = fetch_btc_long_short_ratio(period="1d", limit=1)
|
||||||
|
klines = fetch_btc_klines_4h(limit=60)
|
||||||
|
trend_4h = compute_trend_4h_from_klines(klines) if klines else None
|
||||||
|
|
||||||
|
regime, details = detect_regime(
|
||||||
|
change_24h=change_24h,
|
||||||
|
funding_rate=funding_rate,
|
||||||
|
long_short_ratio=long_short_ratio,
|
||||||
|
trend_4h=trend_4h,
|
||||||
|
)
|
||||||
|
return regime, details
|
||||||
|
|
@ -198,6 +198,13 @@ class TradingStrategy:
|
||||||
|
|
||||||
# 确定交易方向(基于技术指标)
|
# 确定交易方向(基于技术指标)
|
||||||
trade_direction = trade_signal['direction']
|
trade_direction = trade_signal['direction']
|
||||||
|
# 4H 下跌禁止开多(熊市/保守方案:避免逆势抄底)
|
||||||
|
block_long_4h_down = bool(config.TRADING_CONFIG.get("BLOCK_LONG_WHEN_4H_DOWN", False))
|
||||||
|
if block_long_4h_down and trade_direction == "BUY" and trade_signal.get("trend_4h") == "down":
|
||||||
|
logger.info(
|
||||||
|
f"{symbol} 4H 趋势下跌,禁止开多(BLOCK_LONG_WHEN_4H_DOWN=true),跳过"
|
||||||
|
)
|
||||||
|
continue
|
||||||
# 开仓前资金费率过滤:避免在费率极端不利于己方时进场
|
# 开仓前资金费率过滤:避免在费率极端不利于己方时进场
|
||||||
if not await self._check_funding_rate_filter(symbol, trade_direction):
|
if not await self._check_funding_rate_filter(symbol, trade_direction):
|
||||||
logger.info(f"{symbol} 资金费率过滤未通过,跳过开仓")
|
logger.info(f"{symbol} 资金费率过滤未通过,跳过开仓")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user