This commit is contained in:
薇薇安 2026-01-19 17:37:40 +08:00
parent 0e9a0977e9
commit 5e963ecc01
7 changed files with 217 additions and 18 deletions

146
MULTI_USER_ARCHITECTURE.md Normal file
View File

@ -0,0 +1,146 @@
## 多用户(多账号)架构评估与落地方案(草案)
本文档基于你提出的多用户需求给出数据隔离边界、并发模型、Redis/DB 设计、以及 2核4G 服务器负载评估与分阶段落地路线。
---
## 目标与约束
### 目标(对应你的 6 点)
1. **每个用户使用自己的 API KEY**
2. **每个用户独立自动交易、独立持仓**
3. **每个用户独立配置**
4. **交易推荐全局共用**
5. **订单/交易记录独立**
6. **错误日志全局共用**
### 关键约束
- **币安限频是关键瓶颈**:公共行情接口通常按 IP 限频,单纯“多 key”无法线性扩容必须通过共享行情缓存降低请求。
- **隔离优先级**:下单/持仓/配置/订单记录必须强隔离;推荐/行情/错误日志可以共享。
---
## 数据归属与隔离Data Ownership
### A. 全局共享(所有用户共用)
- **推荐Recommendations**:一份全局 Redis Snapshot/List
- 建议 Key`ats:recommendations:snapshot`
- 后端/前端只读(不在请求中触发扫描/生成)
- **行情热数据Market Data**全局缓存Redis
- `ats:md:mark_price:all`(短 TTL比如 2~5s
- `ats:md:ticker_24h:all`TTL 10~30s
- `ats:md:klines:{symbol}:{interval}:{limit}`TTL 30~120s
- **错误日志Logs**:全局流
- 继续使用 `ats:logs:{error|warning|info}`
- 每条日志建议增加字段:`account_id`(便于筛选定位)
### B. 用户私有(必须隔离)
- **API KEY/SECRET敏感**:按账号独立存储(必须加密)
- **交易配置**:按账号独立
- DB`(account_id, config_key)` 唯一
- Redis`ats:cfg:{account_id}`hash
- **自动交易运行态/热数据(可选但推荐)**
- `ats:positions:{account_id}`(更快的 UI 展示/减少后端压力)
- `ats:orders:pending:{account_id}`(挂单/入场状态)
- `ats:stats:{account_id}:*`(每日下单数、成交数、撤单数等)
- **交易/订单记录**:按账号独立
- DB 表:`trades` 增加 `account_id`;统计/查询均按账号过滤
---
## 并发模型:是否需要多线程/多进程?
结论:**不推荐多线程**。更推荐以下两种模型之一:
### 方案 1推荐先落地每个账号一个 `trading_system` 进程Supervisor 管理)
- **优点**
- 隔离强:某个账号异常/限频/崩溃不影响其他账号
- 上线快:对现有单账号 trading_system 改动最少
- 运维简单Supervisor 一账号一进程
- **缺点**
- 进程数随账号增长(内存占用会上升)
- 若每进程都重复拉行情,会触发 IP 限频(必须配合“全局行情缓存”)
### 方案 2后期优化单进程 + asyncio 多账号 worker
- **优点**
- 资源更省,便于共享行情缓存/HTTP Session
- **缺点**
- 隔离弱:某个账号阻塞/异常可能影响整体
- 实现复杂:需要更严格的超时/限频/异常隔离与任务监控
---
## Redis 用法:减轻后端与交易系统压力的关键
建议将 Redis 分为四层:
1. **全局行情缓存层**(降低公共接口请求)
2. **全局推荐层**(只读 snapshot避免“刷新=触发扫描”)
3. **账号配置层**`ats:cfg:{account_id}`
4. **账号运行态层**positions/pending/stats
额外建议:
- 使用短 TTL + 分布式锁(你已有 lock 设计),避免并发刷新造成浪涌。
---
## 数据库改造(最小必要变更)
1. 新增 `accounts` 表:
- `id, name, api_key_enc, api_secret_enc, status, created_at ...`
- API Key 必须加密存储(服务端用 master key 解密)
2. `trading_config` 增加 `account_id`(或新表 `trading_config_accounts`
3. `trades` 增加 `account_id`
4. `account_snapshots` 增加 `account_id`
5. 其余按需positions、signalssignals 可全局positions 必须私有)
---
## 2核4G 负载评估(粗略)
负载主要来自两块:
1. **公共行情拉取/指标计算(全局)**
2. **每账号的下单/持仓同步/风控计算(私有)**
### 不做共享行情(不推荐)
- 每账号都拉 klines/mark price/ticker会先撞到 **币安 IP 限频**2c4g 也撑不住。
### 做共享行情 + 推荐全局(推荐)
- 公共行情只拉 1 份Redis 分发),账号 worker 只消费缓存 + 下单/持仓同步。
- 2c4g 在“波段低频”(如 15min 扫描、每账号持仓 0~5场景下通常可支撑 **10~30 个账号**(视持仓监控方式与限频策略而定)。
- 若每个账号都对每个持仓开 WebSocket 监控,上限会明显降低(更建议用全局 mark price + 定时校验替代/降频)。
---
## 分阶段落地路线(推荐)
### Phase 1最快上线强隔离、成本最低
- accounts + account_id 改造DB/API/前端)
- 每账号一个 trading_system 进程Supervisor
- 推荐/行情继续全局缓存Redis
- 日志全局流(带 account_id
### Phase 2提升承载与稳定性
- 抽全局 MarketDataService行情/klines 统一刷新一次)
- 每账号 worker 只做决策/下单,减少公共 API 调用
- 降低 per-position websocket 数量(用 mark price 缓存替代)
---
## 风险提示与建议
- **安全**API Key 必须加密存储;前端永远不返回明文 secret。
- **限频**:公共接口一定要共享缓存;多账号并发拉行情会让整体不可用。
- **隔离**:交易执行建议多进程优先;后期再做单进程多 worker 的资源优化。

View File

@ -21,10 +21,10 @@ router = APIRouter()
# 智能入场方案C配置为了“配置页可见”即使数据库尚未创建也在 GET /api/config 返回默认项 # 智能入场方案C配置为了“配置页可见”即使数据库尚未创建也在 GET /api/config 返回默认项
SMART_ENTRY_CONFIG_DEFAULTS = { SMART_ENTRY_CONFIG_DEFAULTS = {
"SMART_ENTRY_ENABLED": { "SMART_ENTRY_ENABLED": {
"value": True, "value": False,
"type": "boolean", "type": "boolean",
"category": "strategy", "category": "strategy",
"description": "智能入场开关方案C趋势时减少错过震荡时避免追价打损", "description": "智能入场开关。关闭后回归“纯限价单模式”(不追价/不市价兜底/未成交则撤单跳过),更适合低频波段",
}, },
"SMART_ENTRY_STRONG_SIGNAL": { "SMART_ENTRY_STRONG_SIGNAL": {
"value": 8, "value": 8,

View File

@ -220,6 +220,13 @@ async def get_trade_stats(
win_trades = [t for t in meaningful_trades if float(t['pnl']) > 0] win_trades = [t for t in meaningful_trades if float(t['pnl']) > 0]
loss_trades = [t for t in meaningful_trades if float(t['pnl']) < 0] loss_trades = [t for t in meaningful_trades if float(t['pnl']) < 0]
# 盈利/亏损均值(用于观察是否接近 3:1
avg_win_pnl = sum(float(t["pnl"]) for t in win_trades) / len(win_trades) if win_trades else 0.0
avg_loss_pnl_abs = (
sum(abs(float(t["pnl"])) for t in loss_trades) / len(loss_trades) if loss_trades else 0.0
)
win_loss_ratio = (avg_win_pnl / avg_loss_pnl_abs) if avg_loss_pnl_abs > 0 else None
stats = { stats = {
"total_trades": len(trades), "total_trades": len(trades),
"closed_trades": len(closed_trades), "closed_trades": len(closed_trades),
@ -231,6 +238,11 @@ async def get_trade_stats(
"win_rate": len(win_trades) / len(meaningful_trades) * 100 if meaningful_trades else 0, # 基于有意义的交易计算胜率 "win_rate": len(win_trades) / len(meaningful_trades) * 100 if meaningful_trades else 0, # 基于有意义的交易计算胜率
"total_pnl": sum(float(t['pnl']) for t in closed_trades), "total_pnl": sum(float(t['pnl']) for t in closed_trades),
"avg_pnl": sum(float(t['pnl']) for t in closed_trades) / len(closed_trades) if closed_trades else 0, "avg_pnl": sum(float(t['pnl']) for t in closed_trades) / len(closed_trades) if closed_trades else 0,
# 额外统计:盈利单均值 vs 亏损单均值(绝对值)以及比值(目标 3:1
"avg_win_pnl": avg_win_pnl,
"avg_loss_pnl_abs": avg_loss_pnl_abs,
"avg_win_loss_ratio": win_loss_ratio,
"avg_win_loss_ratio_target": 3.0,
# 总交易量(名义下单量口径):优先使用 notional_usdt新字段否则回退 entry_price * quantity # 总交易量(名义下单量口径):优先使用 notional_usdt新字段否则回退 entry_price * quantity
"total_notional_usdt": sum( "total_notional_usdt": sum(
float(t.get('notional_usdt') or (float(t.get('entry_price', 0)) * float(t.get('quantity', 0)))) float(t.get('notional_usdt') or (float(t.get('entry_price', 0)) * float(t.get('quantity', 0))))

View File

@ -25,10 +25,10 @@ const ConfigPanel = () => {
const presets = { const presets = {
swing: { swing: {
name: '波段回归(推荐)', name: '波段回归(推荐)',
desc: '回归“15分钟节奏 + 精选机会 + 小保证金占用”的波段交易。建议先跑20-30单再评估。', desc: '根治高频与追价:关闭智能入场,回归“纯限价 + 30分钟扫描 + 更高信号门槛”的低频波段。建议先跑20-30单再评估。',
configs: { configs: {
// //
SCAN_INTERVAL: 900, // 15 SCAN_INTERVAL: 1800, // 30
TOP_N_SYMBOLS: 8, TOP_N_SYMBOLS: 8,
// //
@ -37,8 +37,11 @@ const ConfigPanel = () => {
MIN_POSITION_PERCENT: 0.0, // 0% MIN_POSITION_PERCENT: 0.0, // 0%
// //
MIN_SIGNAL_STRENGTH: 7, MIN_SIGNAL_STRENGTH: 8,
USE_TRAILING_STOP: false, USE_TRAILING_STOP: false,
// /
SMART_ENTRY_ENABLED: false,
}, },
}, },
conservative: { conservative: {
@ -434,6 +437,10 @@ const ConfigPanel = () => {
// key // key
let type = 'number' let type = 'number'
let category = 'risk' let category = 'risk'
if (typeof value === 'boolean') {
type = 'boolean'
category = 'strategy'
}
if (key.includes('PERCENT') || key.includes('PCT')) { if (key.includes('PERCENT') || key.includes('PCT')) {
type = 'number' type = 'number'
if (key.includes('STOP_LOSS') || key.includes('TAKE_PROFIT')) { if (key.includes('STOP_LOSS') || key.includes('TAKE_PROFIT')) {
@ -1150,9 +1157,9 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
const getConfigDetail = (key) => { const getConfigDetail = (key) => {
const details = { const details = {
// //
'SCAN_INTERVAL': '扫描间隔(秒)。系统每隔多长时间扫描一次市场寻找交易机会。值越小扫描越频繁,能更快捕捉波动,但会增加API请求和系统负载。建议保守策略3600秒(1小时)平衡策略600秒(10分钟)激进策略300秒(5分钟)。晚间波动大时可降低到300-600秒。', 'SCAN_INTERVAL': '扫描间隔(秒)。系统每隔多长时间扫描一次市场寻找交易机会。值越小扫描越频繁,能更快捕捉波动,但更容易产生噪音与过度交易。建议低频波段1800秒(30分钟)或更长;稳健/保守可3600秒(1小时);高频不建议长期使用。',
'MIN_CHANGE_PERCENT': '最小涨跌幅阈值(%。只有24小时涨跌幅达到此值的交易对才会被考虑交易。值越小捕捉机会越多但可能包含噪音和假信号。值越大只捕捉大幅波动信号质量更高但机会更少。建议保守策略2.0-3.0%平衡策略1.5-2.0%激进策略1.0-1.5%。', 'MIN_CHANGE_PERCENT': '最小涨跌幅阈值(%。只有24小时涨跌幅达到此值的交易对才会被考虑交易。值越小捕捉机会越多但可能包含噪音和假信号。值越大只捕捉大幅波动信号质量更高但机会更少。建议保守策略2.0-3.0%平衡策略1.5-2.0%激进策略1.0-1.5%。',
'MIN_SIGNAL_STRENGTH': '最小信号强度0-10。技术指标综合评分只有达到此强度的信号才会执行交易。值越小交易机会越多但信号质量可能下降胜率降低。值越大只执行高质量信号胜率更高但机会更少。建议保守策略5-7平衡策略4-5激进策略3-4。', 'MIN_SIGNAL_STRENGTH': '最小信号强度0-10。技术指标综合评分只有达到此强度的信号才会执行交易。值越小机会越多但噪音更大值越大更偏“精选高质量”。建议低频波段建议≥8一般保守5-7平衡4-5高频3-4不推荐长期。',
'TOP_N_SYMBOLS': '每次扫描后处理的交易对数量。从符合条件的交易对中选择涨跌幅最大的前N个进行详细分析。值越大机会越多但计算量增加API请求增多。建议保守策略8-10个平衡策略12-15个激进策略15-20个。', 'TOP_N_SYMBOLS': '每次扫描后处理的交易对数量。从符合条件的交易对中选择涨跌幅最大的前N个进行详细分析。值越大机会越多但计算量增加API请求增多。建议保守策略8-10个平衡策略12-15个激进策略15-20个。',
'MAX_SCAN_SYMBOLS': '扫描的最大交易对数量0表示扫描所有。限制每次扫描时处理的交易对总数减少API请求和计算量。值越小扫描越快但可能错过一些机会。值越大覆盖更全面但API请求和计算量增加。建议保守策略100-200个平衡策略200-300个激进策略300-500个。设置为0会扫描所有交易对约500+个)。', 'MAX_SCAN_SYMBOLS': '扫描的最大交易对数量0表示扫描所有。限制每次扫描时处理的交易对总数减少API请求和计算量。值越小扫描越快但可能错过一些机会。值越大覆盖更全面但API请求和计算量增加。建议保守策略100-200个平衡策略200-300个激进策略300-500个。设置为0会扫描所有交易对约500+个)。',
'MIN_VOLATILITY': '最小波动率小数形式如0.02表示2%。过滤掉波动率低于此值的交易对确保只交易有足够波动的币种。值越小允许更多交易对但可能包含波动不足的币种。值越大只交易高波动币种但可能错过一些机会。建议0.015-0.0251.5%-2.5%)。', 'MIN_VOLATILITY': '最小波动率小数形式如0.02表示2%。过滤掉波动率低于此值的交易对确保只交易有足够波动的币种。值越小允许更多交易对但可能包含波动不足的币种。值越大只交易高波动币种但可能错过一些机会。建议0.015-0.0251.5%-2.5%)。',
@ -1176,6 +1183,7 @@ const getConfigDetail = (key) => {
// //
'LEVERAGE': '交易杠杆倍数。放大资金利用率同时放大收益和风险。杠杆越高相同仓位下需要的保证金越少但风险越大。建议保守策略5-10倍平衡策略10倍激进策略10-15倍。注意高杠杆会增加爆仓风险请谨慎使用。', 'LEVERAGE': '交易杠杆倍数。放大资金利用率同时放大收益和风险。杠杆越高相同仓位下需要的保证金越少但风险越大。建议保守策略5-10倍平衡策略10倍激进策略10-15倍。注意高杠杆会增加爆仓风险请谨慎使用。',
'USE_TRAILING_STOP': '是否启用移动止损true/false。启用后当盈利达到激活阈值时止损会自动跟踪价格保护利润。适合趋势行情可以捕捉更大的利润空间。建议平衡和激进策略启用保守策略可关闭。', 'USE_TRAILING_STOP': '是否启用移动止损true/false。启用后当盈利达到激活阈值时止损会自动跟踪价格保护利润。适合趋势行情可以捕捉更大的利润空间。建议平衡和激进策略启用保守策略可关闭。',
'SMART_ENTRY_ENABLED': '智能入场开关true/false。开启时会进行“限价回调 + 追价 +(趋势强时)市价兜底”,以减少错过;关闭时回归“纯限价单模式”:只下一次限价单,未在确认时间内成交则撤单跳过,更适合低频波段与控频。',
'TRAILING_STOP_ACTIVATION': '移动止损激活阈值百分比如0.01表示1%。当盈利达到此百分比时移动止损开始跟踪价格将止损移至成本价保本。值越小激活越早更早保护利润但可能过早退出。值越大激活越晚给价格更多波动空间。建议1-2%。', 'TRAILING_STOP_ACTIVATION': '移动止损激活阈值百分比如0.01表示1%。当盈利达到此百分比时移动止损开始跟踪价格将止损移至成本价保本。值越小激活越早更早保护利润但可能过早退出。值越大激活越晚给价格更多波动空间。建议1-2%。',
'TRAILING_STOP_PROTECT': '移动止损保护利润百分比如0.01表示1%。当价格从最高点回撤达到此百分比时触发止损平仓锁定利润。值越小保护更严格能锁定更多利润但可能过早退出。值越大允许更大回撤可能捕捉更大趋势但利润可能回吐。建议1-2%。', 'TRAILING_STOP_PROTECT': '移动止损保护利润百分比如0.01表示1%。当价格从最高点回撤达到此百分比时触发止损平仓锁定利润。值越小保护更严格能锁定更多利润但可能过早退出。值越大允许更大回撤可能捕捉更大趋势但利润可能回吐。建议1-2%。',

View File

@ -266,6 +266,23 @@ const TradeList = () => {
{stats.avg_pnl.toFixed(2)} USDT {stats.avg_pnl.toFixed(2)} USDT
</div> </div>
</div> </div>
{"avg_win_pnl" in stats && "avg_loss_pnl_abs" in stats && (
<div className="stat-card">
<div className="stat-label">平均盈利 / 平均亏损期望 3:1</div>
<div
className={`stat-value ${
typeof stats.avg_win_loss_ratio === 'number' && stats.avg_win_loss_ratio >= 3 ? 'positive' : ''
}`}
>
{typeof stats.avg_win_loss_ratio === 'number'
? `${stats.avg_win_loss_ratio.toFixed(2)} : 1`
: '—'}
</div>
<div style={{ fontSize: '12px', color: '#999', marginTop: '4px' }}>
+{Number(stats.avg_win_pnl || 0).toFixed(2)} / -{Number(stats.avg_loss_pnl_abs || 0).toFixed(2)} USDT
</div>
</div>
)}
{"total_notional_usdt" in stats && ( {"total_notional_usdt" in stats && (
<div className="stat-card"> <div className="stat-card">
<div className="stat-label">总交易量名义</div> <div className="stat-label">总交易量名义</div>

View File

@ -192,14 +192,14 @@ def _get_trading_config():
'USE_DYNAMIC_ATR_MULTIPLIER': False, # 是否根据波动率动态调整ATR倍数 'USE_DYNAMIC_ATR_MULTIPLIER': False, # 是否根据波动率动态调整ATR倍数
'ATR_MULTIPLIER_MIN': 1.5, # 动态ATR倍数最小值 'ATR_MULTIPLIER_MIN': 1.5, # 动态ATR倍数最小值
'ATR_MULTIPLIER_MAX': 2.5, # 动态ATR倍数最大值 'ATR_MULTIPLIER_MAX': 2.5, # 动态ATR倍数最大值
'SCAN_INTERVAL': 3600, 'SCAN_INTERVAL': 1800,
'KLINE_INTERVAL': '1h', 'KLINE_INTERVAL': '1h',
'PRIMARY_INTERVAL': '1h', 'PRIMARY_INTERVAL': '1h',
'CONFIRM_INTERVAL': '4h', 'CONFIRM_INTERVAL': '4h',
'ENTRY_INTERVAL': '15m', 'ENTRY_INTERVAL': '15m',
'MIN_VOLUME_24H': 5000000, # 降低到500万以获取更多推荐推荐系统可以更宽松 'MIN_VOLUME_24H': 5000000, # 降低到500万以获取更多推荐推荐系统可以更宽松
'MIN_VOLATILITY': 0.02, 'MIN_VOLATILITY': 0.02,
'MIN_SIGNAL_STRENGTH': 7, # 提高至7只交易高质量信号简化策略后 'MIN_SIGNAL_STRENGTH': 8, # 提高至8只交易高质量信号低频波段
'LEVERAGE': 10, # 基础杠杆倍数 'LEVERAGE': 10, # 基础杠杆倍数
'USE_DYNAMIC_LEVERAGE': True, # 是否启用动态杠杆(根据信号强度调整) 'USE_DYNAMIC_LEVERAGE': True, # 是否启用动态杠杆(根据信号强度调整)
'MAX_LEVERAGE': 15, # 最大杠杆倍数降低到15更保守配合更大的保证金 'MAX_LEVERAGE': 15, # 最大杠杆倍数降低到15更保守配合更大的保证金
@ -208,8 +208,9 @@ def _get_trading_config():
'TRAILING_STOP_PROTECT': 0.05, # 保护利润提高到5%保护5%利润,更合理) 'TRAILING_STOP_PROTECT': 0.05, # 保护利润提高到5%保护5%利润,更合理)
'POSITION_SYNC_INTERVAL': 60, # 持仓状态同步间隔缩短到1分钟确保状态及时同步 'POSITION_SYNC_INTERVAL': 60, # 持仓状态同步间隔缩短到1分钟确保状态及时同步
# ===== 智能入场方案C趋势少错过震荡不追价===== # ===== 智能入场方案C=====
'SMART_ENTRY_ENABLED': True, # 根治方案:默认关闭。关闭后回归“纯限价单模式”(不追价/不市价兜底/未成交撤单跳过)
'SMART_ENTRY_ENABLED': False,
'SMART_ENTRY_STRONG_SIGNAL': 8, # 强信号阈值≥8 更倾向趋势模式(允许市价兜底) 'SMART_ENTRY_STRONG_SIGNAL': 8, # 强信号阈值≥8 更倾向趋势模式(允许市价兜底)
'ENTRY_SYMBOL_COOLDOWN_SEC': 120, # 同一symbol两次入场尝试的冷却时间避免反复挂单/重入) 'ENTRY_SYMBOL_COOLDOWN_SEC': 120, # 同一symbol两次入场尝试的冷却时间避免反复挂单/重入)
'ENTRY_TIMEOUT_SEC': 180, # 智能入场总预算(秒)(限价/追价逻辑内部使用) 'ENTRY_TIMEOUT_SEC': 180, # 智能入场总预算(秒)(限价/追价逻辑内部使用)
@ -233,14 +234,14 @@ TRADING_CONFIG = _get_trading_config()
# 确保包含所有必要的默认值 # 确保包含所有必要的默认值
defaults = { defaults = {
'SCAN_INTERVAL': 3600, 'SCAN_INTERVAL': 1800,
'KLINE_INTERVAL': '1h', 'KLINE_INTERVAL': '1h',
'PRIMARY_INTERVAL': '1h', 'PRIMARY_INTERVAL': '1h',
'CONFIRM_INTERVAL': '4h', 'CONFIRM_INTERVAL': '4h',
'ENTRY_INTERVAL': '15m', 'ENTRY_INTERVAL': '15m',
'LIMIT_ORDER_OFFSET_PCT': 0.5, # 限价单偏移百分比默认0.5% 'LIMIT_ORDER_OFFSET_PCT': 0.5, # 限价单偏移百分比默认0.5%
# 智能入场默认值即使DB里没配置也能用 # 智能入场默认值即使DB里没配置也能用
'SMART_ENTRY_ENABLED': True, 'SMART_ENTRY_ENABLED': False,
'ENTRY_SYMBOL_COOLDOWN_SEC': 120, 'ENTRY_SYMBOL_COOLDOWN_SEC': 120,
'ENTRY_TIMEOUT_SEC': 180, 'ENTRY_TIMEOUT_SEC': 180,
'ENTRY_STEP_WAIT_SEC': 15, 'ENTRY_STEP_WAIT_SEC': 15,

View File

@ -298,10 +298,26 @@ class PositionManager:
order_status = None order_status = None
actual_entry_price = None actual_entry_price = None
filled_quantity = 0.0 filled_quantity = 0.0
entry_mode_used = "market" if not smart_entry_enabled else ("limit+fallback" if allow_market_fallback else "limit-chase") entry_mode_used = "limit-only" if not smart_entry_enabled else ("limit+fallback" if allow_market_fallback else "limit-chase")
if not smart_entry_enabled: if not smart_entry_enabled:
order = await self.client.place_order(symbol=symbol, side=side, quantity=quantity, order_type="MARKET") # 根治方案:关闭智能入场后,回归“纯限价单模式”
# - 不追价
# - 不市价兜底
# - 未在确认时间内成交则撤单并跳过(属于策略未触发入场,不是系统错误)
confirm_timeout = int(config.TRADING_CONFIG.get("ENTRY_CONFIRM_TIMEOUT_SEC", 30) or 30)
logger.info(
f"{symbol} [纯限价入场] side={side} | 限价={initial_limit:.6f} (offset={limit_offset_ratio*100:.2f}%) | "
f"确认超时={confirm_timeout}s未成交将撤单跳过"
)
order = await self.client.place_order(
symbol=symbol, side=side, quantity=quantity, order_type="LIMIT", price=initial_limit
)
if not order:
return None
entry_order_id = order.get("orderId")
if entry_order_id:
self._pending_entry_orders[symbol] = {"order_id": entry_order_id, "created_at_ms": int(time.time() * 1000)}
else: else:
# 1) 先挂限价单 # 1) 先挂限价单
logger.info( logger.info(
@ -418,10 +434,9 @@ class PositionManager:
filled_quantity = float(res.get("executed_qty") or 0) filled_quantity = float(res.get("executed_qty") or 0)
else: else:
# 未成交NEW/超时/CANCELED 等)属于“策略未触发入场”或“挂单没成交” # 未成交NEW/超时/CANCELED 等)属于“策略未触发入场”或“挂单没成交”
# 这不应当当作系统错误;同时需要撤单,避免留下悬挂委托造成后续混乱。 # 这不应当当作系统错误;同时需要撤单best-effort,避免留下悬挂委托造成后续混乱。
logger.warning(f"{symbol} [开仓] 未成交,状态: {order_status},跳过本次开仓并撤销挂单") logger.warning(f"{symbol} [开仓] 未成交,状态: {order_status},跳过本次开仓并撤销挂单")
try: try:
if str(order_status).upper() in {"NEW", "PARTIALLY_FILLED", "PENDING_NEW", "TIMEOUT"}:
await self.client.cancel_order(symbol, int(entry_order_id)) await self.client.cancel_order(symbol, int(entry_order_id))
except Exception: except Exception:
pass pass