Compare commits

...

483 Commits

Author SHA1 Message Date
薇薇安
d68d3ad66c feat(strategy): Implement signal filtering mechanism for trade signals
Added a unified signal filtering layer to the trading strategy, allowing for enhanced analysis of trade signals based on market conditions. The filtering process includes logging reasons for acceptance or rejection, improving transparency and decision-making in automated trading. Exception handling is also implemented to ensure robustness during signal filtering application.
2026-03-12 19:11:06 +08:00
薇薇安
199c4a95dd 1 2026-03-12 16:01:08 +08:00
薇薇安
3e6ce55663 1 2026-03-11 20:11:49 +08:00
薇薇安
1446bf852b feat(config): Introduce layered profit locking mechanism and manual trading adjustments
Added new configurations for a layered profit locking system, allowing for gradual profit protection at specified thresholds. Introduced manual trading options, including reduced and blocked symbol lists, to enhance trading strategy flexibility. Updated relevant backend and frontend components to reflect these changes, improving risk management and user control over trading activities.
2026-03-09 14:51:08 +08:00
薇薇安
218ab7e195 增加小时限制 2026-03-05 08:43:03 +08:00
薇薇安
6858ddad6b 1 2026-03-03 09:21:38 +08:00
薇薇安
4ea1c53813 1 2026-03-01 18:08:21 +08:00
薇薇安
e2c37e6d62 周日开仓限制 2026-03-01 17:40:39 +08:00
薇薇安
1e55365d43 1 2026-03-01 14:18:36 +08:00
薇薇安
7bc384a58f 1 2026-03-01 13:56:50 +08:00
薇薇安
1f796a3fef 1 2026-03-01 13:42:54 +08:00
薇薇安
74c21bea9b feat(stats): Update admin dashboard stats to include recent 30-day account snapshots and enhance trade data source options
Modified the admin dashboard statistics to retrieve account snapshots from the last 30 days instead of just 1 day, ensuring more comprehensive data. Additionally, introduced a new data source option for trades, allowing users to select between 'binance' and 'local' records, with appropriate handling for each source. Updated the frontend components to reflect these changes and improve user experience in managing trade data.
2026-03-01 12:58:49 +08:00
薇薇安
007827464a 全局仪表板调整,增加管理中心管理用户及账号 2026-03-01 11:48:59 +08:00
薇薇安
0127edbc97 feat(config): Add new risk management configurations for Sunday and night trading hours
Introduced new configuration options to manage trading activities on Sundays and during night hours. This includes limits on the number of trades on Sundays, minimum signal strength requirements for Sunday trades, and restrictions on opening new positions during specified night hours. Updated relevant backend and frontend components to reflect these changes, enhancing risk control and user awareness of trading conditions.
2026-03-01 11:25:03 +08:00
薇薇安
8bb1bf7254 feat(position_manager): Improve monitoring logic for Binance positions without SL/TP orders
Updated the position monitoring logic to handle cases where positions exist on Binance without corresponding stop-loss or take-profit orders. Enhanced logging to provide clearer insights into the status of these positions, ensuring better risk management by avoiding unprotected positions. This change allows for automatic monitoring and order creation based on the presence of SL/TP orders.
2026-02-28 19:13:58 +08:00
薇薇安
c926586f8d feat(position_manager): Implement trailing stop-loss logic for profit protection
Enhanced the stop-loss mechanism to include a trailing stop-loss feature that activates when a position is significantly profitable. This update calculates a protective stop-loss price based on current profits, ensuring better risk management. Improved logging for tracking profit levels and stop-loss adjustments during monitoring.
2026-02-28 19:09:54 +08:00
薇薇安
4a5406c7e8 feat(position_manager): Enhance stop-loss logic for short positions and implement automatic SL/TP order synchronization
Updated the stop-loss calculation for short positions to ensure it locks in profits effectively. Added logic to automatically synchronize stop-loss and take-profit orders for positions without existing SL/TP on the exchange. This improves risk management and ensures positions are adequately protected. Enhanced logging for better tracking of stop-loss updates and synchronization events.
2026-02-28 19:01:59 +08:00
薇薇安
5d5ead36ac feat(strategy): Implement stagnation early exit strategy configuration and logic
Added new configuration options for the stagnation early exit strategy, allowing for partial position closure and stop-loss adjustments when a position has not reached a new high after a specified period. Updated relevant backend logic to handle this strategy, including tracking maximum profit and timestamps for the last new high. Frontend components were also updated to reflect these new settings and their descriptions, enhancing user control over trading strategies.
2026-02-27 23:48:05 +08:00
薇薇安
849b550910 feat(config): Add staggered scanning configuration for multi-account setups
Introduced new configuration options for staggered scanning in multi-account scenarios to reduce concurrency pressure. This includes settings for enabling staggered scanning, random delay intervals, and minimum/maximum delay times. Updated frontend components to reflect these new configurations and their descriptions, enhancing user awareness and control over scanning behavior.
2026-02-27 20:18:24 +08:00
薇薇安
78007040f1 feat(blacklist): Enhance blacklist logic and UI representation
Updated the trading system to differentiate between soft and hard blacklists based on recent performance metrics. The frontend now displays the blacklist status with clear visual indicators. This change aims to improve risk management and user awareness of trading conditions.
2026-02-27 20:08:07 +08:00
薇薇安
d5ef525224 1 2026-02-27 13:58:59 +08:00
薇薇安
861f1dc548 feat(stats): 增加全局交易统计功能
在后端新增了全局交易统计功能,包括按交易对和按小时的聚合统计,支持生成软黑名单和时段过滤建议。前端组件更新以展示基于最近N天交易统计的过滤结果,旨在提升交易策略的灵活性和风险控制能力。
2026-02-27 13:29:49 +08:00
薇薇安
076b597fb4 增加仪表板统计数据的导出功能 2026-02-27 13:04:25 +08:00
薇薇安
dfe29d70dc feat(stats): 添加交易统计API和前端展示
在后端新增了交易统计API,支持获取最近N天的交易数据,包括按交易对和按小时的聚合统计。同时,前端组件StatsDashboard更新,展示交易统计信息,包括净盈亏、胜率及策略建议。此改动旨在提升交易数据分析能力,帮助用户更好地理解交易表现。
2026-02-26 20:48:04 +08:00
薇薇安
e2e7effca2 feat(database): 添加交易统计模型和聚合逻辑
在数据库模型中新增了 `TradeStats` 类,包含交易统计功能,支持按交易对和日期聚合数据。实现了从 `binance_trades` 和 `trades` 表中提取交易数据的逻辑,并创建了相应的统计表 `trade_stats_daily` 和 `trade_stats_time_bucket`。此改动旨在增强交易数据分析能力,为后续的风险控制和决策提供支持。
2026-02-26 20:08:46 +08:00
薇薇安
30c5635570 2 2026-02-26 17:49:01 +08:00
薇薇安
ed0c6754e0 1 2026-02-26 17:40:08 +08:00
薇薇安
44aa7ef273 1 2026-02-26 17:37:50 +08:00
薇薇安
a34b6ba448 1 2026-02-26 17:36:01 +08:00
薇薇安
83bb687a97 1 2026-02-26 17:32:03 +08:00
薇薇安
99281395c1 fix(config_manager, api, trading_system): 添加 Algo 条件单请求超时配置
在配置管理模块中,新增了 `ALGO_ORDER_TIMEOUT_SEC` 配置项,以控制 Algo 条件单(止损/止盈)的单次请求超时,旨在应对币安接口高负载时可能出现的超时问题。同时,更新了相关模块的日志记录,提供更清晰的错误信息,确保在网络不稳定时能够有效调整超时设置。这一改动旨在增强系统的稳定性和风险控制能力。
2026-02-26 13:26:56 +08:00
薇薇安
e609d45fcd fix(config_manager, api, trading_system): 添加市场方案下的多空限制配置
在配置管理模块中,新增了 `BLOCK_SHORT_WHEN_BULL_MARKET` 和 `BLOCK_LONG_WHEN_BEAR_MARKET` 配置项,以控制在牛市和熊市中禁止开空和开多的策略。同时,更新了相关模块以支持这些新配置,确保在不同市场条件下的交易策略能够有效执行。这一改动旨在增强风险控制能力,确保交易决策与市场趋势一致。
2026-02-26 12:28:57 +08:00
薇薇安
10e6096cc1 fix(config_manager, api, database): 添加盈利保护配置项
在配置管理模块中,新增了 `PROFIT_PROTECTION_ENABLED` 和 `LOCK_PROFIT_AT_BREAKEVEN_AFTER_PCT` 配置项,以控制保本和移动止损的执行。同时,更新了数据库初始化脚本以包含这些新配置。这一改动旨在增强风险控制能力,确保在盈利时能够有效保护利润。
2026-02-26 12:07:16 +08:00
薇薇安
432fc85a79 增加移动止损单独的日志处理 2026-02-26 11:50:52 +08:00
薇薇安
df2b8d6372 fix(config_manager, api, database, position_manager, user_data_stream): 增强配置管理和日志记录
在配置管理模块中,新增了 `ONLY_AUTO_TRADE_CREATES_RECORDS` 配置项,以控制自动开仓记录的写入行为。同时,在多个模块中优化了日志记录,确保在数据库操作和交易记录完善时提供更清晰的错误信息。这一改动旨在提升系统的稳定性和可维护性,确保交易策略的有效性与安全性。
2026-02-26 11:19:23 +08:00
薇薇安
ab100bdc23 fix(account): 优化止损和止盈价格获取逻辑
在账户模块中,改进了止损和止盈价格的获取逻辑,确保在无数据库记录时能够根据币安持仓和配置比例进行计算。同时,增强了异常处理,确保在无法确定止损止盈价时提供详细的错误信息。这一改动旨在提升风险控制能力和系统的稳定性。
2026-02-26 09:59:45 +08:00
薇薇安
d80d4559c5 fix(position_manager): 优化代码结构和日志记录
在持仓管理模块中,调整了代码缩进和结构,提升了可读性和一致性。同时,增强了日志记录,确保在保存交易记录时提供更清晰的信息。这一改动旨在提升系统的稳定性和可维护性,确保交易策略的有效性与安全性。
2026-02-26 09:49:58 +08:00
薇薇安
beafeb2707 fix(risk_manager): 修复止损和止盈价格选择逻辑
在风险管理模块中,优化了止损和止盈价格的选择逻辑,确保在做多和做空时分别选择更高和更低的止损价,以提高风险控制的有效性。同时,调整了代码缩进和结构,提升了可读性和一致性。这一改动旨在增强系统的稳定性和交易策略的安全性。
2026-02-26 09:42:59 +08:00
薇薇安
87c018594b fix(account, binance_client, position_manager): 优化代码结构和异常处理
在多个模块中,调整了代码缩进和结构,提升了可读性和一致性。同时,增强了异常处理逻辑,确保在调用交易所API时能够正确捕获并记录错误信息。这一改动旨在提升系统的稳定性和风险控制能力,确保交易策略的有效性与安全性。
2026-02-26 09:32:50 +08:00
薇薇安
ff1d985859 fix(account, binance_client, position_manager, risk_manager): 优化异常处理和代码风格
在多个模块中,增强了异常处理逻辑,确保在调用交易所API时能够正确捕获并记录错误信息。同时,调整了代码缩进和结构,提升了可读性和一致性。这一改动旨在提升系统的稳定性和风险控制能力,确保交易策略的有效性与安全性。
2026-02-26 09:17:34 +08:00
薇薇安
c53b67e294 fix(position_manager): 优化止损和止盈价格的获取与应用逻辑
在持仓管理中,增强了从交易所获取止损和止盈价格的逻辑,确保在决定使用交易所的止损或止盈时,优先考虑当前市场条件。同时,调整了止损和止盈的设置流程,确保在同步到交易所前,正确判断并应用已有的止损策略。这一改动旨在提升风险控制能力,确保交易策略的有效性与稳定性。
2026-02-26 09:09:07 +08:00
薇薇安
34e276474a 1 2026-02-25 23:19:48 +08:00
薇薇安
ac022bd62a 1 2026-02-25 23:07:14 +08:00
薇薇安
f3ce4d5d11 fix(config_manager, account, trades, position_manager, risk_manager): 清理多余空行并优化代码风格
在多个模块中,移除多余的空行以提升代码可读性,并确保遵循一致的代码风格。此外,优化了部分逻辑的缩进和结构,增强了代码的整洁性和可维护性。这一改动旨在提升代码质量,确保团队协作时的代码一致性。
2026-02-25 22:22:47 +08:00
薇薇安
41b2a21c3d fix(position_manager): 增强止损和止盈同步逻辑及日志记录
在持仓管理中,优化了止损和止盈的同步逻辑,确保在同步失败时记录详细的异常信息。同时,增加了对止损和止盈为空的警告日志,提升了系统的可用性和风险控制能力。此外,调整了移动止损的配置逻辑,确保在未设置时使用默认值。这一改动旨在提升交易策略的稳定性与用户友好性。
2026-02-25 21:48:46 +08:00
薇薇安
d7ccbe38e4 feat(position_manager): 增加从交易所读取止损和止盈价格的功能
在持仓管理中,新增 `_get_sltp_from_exchange` 方法以从币安获取当前的止损和止盈价格,确保在重启后不覆盖已有的保护。同时,优化了止损和止盈价格的设置逻辑,优先使用从交易所获取的值,提升风险控制能力和策略的灵活性。这一改动旨在增强系统的稳定性与用户友好性,确保交易策略的有效性。
2026-02-25 21:18:00 +08:00
薇薇安
1c33096917 fix(position_manager): 优化止损价格验证逻辑以增强风险控制
在持仓管理中,更新了止损价格的验证逻辑,确保多单止损价格不低于入场价,空单止损价格不高于入场价。同时,增加了对止损价格过近的警告和修正机制,以确保止损策略的有效性和安全性。这一改动旨在提升风险控制能力,确保交易策略的稳健性。
2026-02-25 20:51:42 +08:00
薇薇安
e0dfb4c31e feat(global_config): 添加市场行情JSON查看功能及策略执行概览折叠功能
在GlobalConfig组件中,新增了市场行情的JSON查看功能,用户可以复制市场数据到剪贴板。同时,优化了策略执行概览的展示逻辑,增加了折叠功能,提升了用户界面的可读性与交互性。这一改动旨在增强用户体验,使得市场信息和策略执行情况更加直观易用。
2026-02-25 15:47:20 +08:00
薇薇安
5a3888d905 fix(trade_query): 优化时间筛选逻辑以支持创建时间的回退处理
在交易查询逻辑中,调整了时间筛选条件,确保在存在 `created_at` 列时使用 `COALESCE(created_at, entry_time)`,并在无该列时回退至 `entry_time`。同时,增强了对时间筛选的支持,确保在不同筛选条件下均能正确返回结果。这一改动旨在提升查询的准确性与一致性。
2026-02-25 15:41:23 +08:00
薇薇安
5c854290eb feat(position_manager): 优化持仓获取逻辑以支持多账号
在持仓管理中,更新了 `_get_open_positions` 方法,增加了 `force_rest` 参数以支持强制从 REST 获取持仓数据,确保在多账号情况下能够正确读取对应的缓存。同时,增强了异常处理逻辑,确保在拉取持仓失败时记录调试信息。这一改动旨在提升系统的稳定性与用户友好性,确保持仓数据的准确性与一致性。
2026-02-25 14:58:13 +08:00
薇薇安
81747c4eef feat(account, position_manager): 优化持仓同步逻辑与日志记录
在持仓同步功能中,增加了对系统订单前缀的默认处理,确保在无配置时使用默认前缀 "ats_"。同时,调整了日志记录逻辑,明确区分系统单与手动单的补建条件,提升了日志的可读性与准确性。这一改动旨在增强系统的可用性与用户友好性,确保持仓与数据库记录的一致性。
2026-02-25 14:48:31 +08:00
薇薇安
6a9fcddfdc feat(config, strategy): 更新多账号错峰配置与日志记录逻辑
在配置文件中将多账号错峰开关 `SCAN_STAGGER_BY_ACCOUNT` 的默认值调整为 False,并在策略中增加了对该配置的注释说明。同时,优化了4H趋势中性信号的日志记录逻辑,确保在关闭中性信号时仅记录 DEBUG 信息,提升了日志的可读性与策略的灵活性。此改动旨在增强系统的可用性与用户友好性。
2026-02-25 14:08:36 +08:00
薇薇安
8bd7bae718 feat(config, market_scanner): 调整K线扫描限制以优化信号处理
在配置文件中将K线扫描限制从30根调整至50根,以支持4小时及日线周期的信号计算需求。同时,在市场扫描逻辑中增加了对K线限制的动态调整,确保在特定周期下信号处理的有效性。此改动旨在提升策略的灵活性与准确性,确保在市场波动时能够提供更可靠的交易建议。
2026-02-25 13:54:52 +08:00
薇薇安
09edc4f57d feat(market_scanner): 优化回退信号逻辑以提升信号处理能力
在市场扫描逻辑中调整了回退信号的计算方式,降低了信号强度为零时的触发条件,并引入24小时涨跌幅作为方向判断依据。这一改动旨在增强策略的灵活性,确保在市场波动时能够提供更有效的交易建议,同时提升用户体验。
2026-02-25 13:45:36 +08:00
薇薇安
d1c560ae16 feat(config): 更新默认交易配置以优化信号处理和风险控制
在默认交易配置中调整了信号强度和市场过滤参数,适度放宽了 `MIN_SIGNAL_STRENGTH` 和 `BETA_FILTER_THRESHOLD` 的要求,以增加交易机会。同时,允许在4H趋势中性时自动交易,旨在提升策略灵活性与用户友好性。此改动确保在市场波动中仍能有效捕捉短线机会。
2026-02-25 13:41:05 +08:00
薇薇安
0f0aa1bf5d feat(market_scanner, strategy): 引入回退信号逻辑以优化信号处理
在市场扫描和交易策略中增加了回退信号逻辑,当信号强度为零且方向未明确时,采用RSI、MACD和布林带的综合信号进行判断,避免长期无推荐。这一改动旨在提升策略的灵活性与可用性,确保在市场波动时仍能提供有效的交易建议。
2026-02-25 13:28:58 +08:00
薇薇安
5b2adb0b62 feat(strategy): 更新大盘暴跌提示信息,增加阈值说明
在交易策略中优化大盘暴跌的提示信息,新增阈值说明以指导用户调整 `BETA_FILTER_THRESHOLD` 设置。此改动旨在提升用户对市场波动的理解,增强策略的灵活性与可用性。
2026-02-25 11:29:06 +08:00
薇薇安
e99f0fc7c2 feat(market_scanner): 增加4H趋势中性允许选项以优化信号处理逻辑
在市场扫描逻辑中引入了配置选项 `AUTO_TRADE_ALLOW_4H_NEUTRAL`,允许在逆势情况下不清零信号强度,便于推荐与列表展示。此改动旨在提升策略灵活性,同时确保策略层仍然禁止逆势自动下单,增强了系统的可用性与用户友好性。
2026-02-25 11:20:17 +08:00
薇薇安
9c620e0aa0 feat(market_scanner): 增加趋势信号强度为零时的提示信息
在市场扫描逻辑中添加了对所有标的趋势信号强度为零的情况的日志记录,避免用户误解为异常。此改动旨在提升用户对市场状态的理解,并指导用户在特定情况下的交易决策。增强了系统的可用性与用户友好性。
2026-02-25 11:10:13 +08:00
薇薇安
104cb63802 feat(recommendations): 添加合约推荐提示信息以优化用户排查流程
在后端API中新增合约推荐为空时的提示信息,指导用户检查推荐服务和策略运行状态。前端组件更新以显示该提示,提升用户体验并帮助用户更有效地进行问题排查。此改动增强了系统的可用性与用户友好性。
2026-02-25 11:02:18 +08:00
薇薇安
9a720b9a19 feat(market_scanner): 增强单个交易对信息日志记录,包含信号强度与价格
更新 `_log_single_symbol` 方法,改进日志记录内容,新增信号强度和价格信息,便于用户判断未触发交易的原因。同时,添加异常处理以确保信号强度的正确转换,提升系统的健壮性与可读性。
2026-02-25 10:48:35 +08:00
薇薇安
693a2306ca fix(strategy_overview): 优化策略执行概览的错误处理与提示信息
在后端API中增强了对策略执行概览的错误处理逻辑,确保在无法加载策略执行数据时提供明确的提示信息。前端组件更新以显示相应的错误信息,提升用户体验并指导用户进行后续操作。此改动增强了系统的可用性与用户友好性。
2026-02-25 09:49:11 +08:00
薇薇安
41e53755ea feat(strategy_overview): 添加策略执行概览功能以优化策略分析
在 `market_overview.py` 中新增 `get_strategy_execution_overview` 函数,生成当前策略执行方案与配置项的易读概览,供全局配置页展示。更新后端API以支持该功能,并在前端组件中展示策略执行概览,提升用户对策略执行标准与机制的理解。此改动增强了系统的可用性与用户体验。
2026-02-25 09:39:33 +08:00
薇薇安
9086c15f2e refactor(logging): 改进账户模型中的日志记录级别
将账户模型中的日志记录级别从 info 调整为 debug,以减少日志冗余并提高调试信息的可读性。同时,优化了配置重新加载的日志记录逻辑,确保只记录一次,避免重复日志输出。此改动提升了代码的可维护性与日志管理效率。
2026-02-25 09:31:35 +08:00
薇薇安
163b8303ec feat(spot_order): 增强现货下单API的错误处理与文档说明
在现货下单API中添加了对下单金额的最小限制(5 USDT),并改进了错误处理机制,针对不同的Binance API异常提供了详细的错误信息。更新了API文档说明,确保用户能够更清晰地理解下单逻辑与要求。此改动提升了系统的健壮性与用户体验。
2026-02-25 09:26:29 +08:00
薇薇安
cbba86001a feat(spot_order): 添加现货下单API与前端支持
在后端API中新增现货下单功能,支持市价单和限价单的创建,并提供相应的错误处理机制。前端组件更新以支持现货下单的快速操作,允许用户选择现货市场并设置默认下单金额。此改动提升了用户体验,增强了交易系统的功能性与灵活性。
2026-02-25 08:53:39 +08:00
薇薇安
3389e0aafc feat(recommendations): 添加现货推荐扫描与API支持
在后端API中新增现货推荐扫描功能,定时将数据写入Redis缓存,并提供相应的API接口以获取现货推荐。前端组件更新以支持现货推荐的展示与切换,提升用户体验与决策支持。此改动为用户提供了实时的现货推荐信息,增强了系统的功能性与灵活性。
2026-02-25 08:40:52 +08:00
薇薇安
1dea3df84a feat(config): 添加详细配置项说明以优化策略分析
在 `GlobalConfig` 组件中新增配置项详细说明,提供各项参数的建议与使用说明,便于用户理解和优化交易策略。此改动提升了用户体验,帮助用户更好地进行策略配置与调整。
2026-02-24 15:53:54 +08:00
薇薇安
4dd44782c5 feat(market_overview): 添加市场行情概览API与前端展示功能
在后端API中新增 `/market-overview` 接口,拉取Binance公开市场数据,并计算策略配置与市场状态的对比。前端组件更新以支持市场行情概览的展示,提供实时市场数据与策略匹配情况,提升用户体验与决策支持。
2026-02-24 15:37:06 +08:00
薇薇安
4ccf067b24 fix(start_recommendations): 优化虚拟环境激活逻辑
更新 `start_recommendations.sh` 脚本,优先使用 `trading_system/.venv` 激活虚拟环境,确保与服务器部署一致。增强错误提示信息,提升用户体验与环境配置的准确性。
2026-02-23 19:24:33 +08:00
薇薇安
4479c4f02d fix(recommendations): 优化推荐服务进程检查逻辑
更新 `_recommendations_process_running` 函数,增强对推荐服务进程的检查能力,支持 pgrep 和 ps 方式,确保在不同环境下均能准确获取进程状态。此改动提升了系统的稳定性与兼容性,确保推荐服务的有效管理。
2026-02-23 19:18:09 +08:00
薇薇安
5f256daf27 feat(recommendations): 添加推荐服务管理API与前端控制功能
在后端API中新增推荐服务的状态检查、重启、停止和启动功能,确保能够有效管理推荐服务进程。同时,更新前端组件以支持推荐服务状态的显示与控制,提升用户体验。此改动为推荐服务的管理提供了更直观的操作界面与实时状态反馈。
2026-02-23 18:02:53 +08:00
薇薇安
24d01cba0d feat(trade_recommender): 引入4H趋势过滤逻辑以优化交易推荐
在交易推荐系统中新增 `BLOCK_LONG_WHEN_4H_DOWN` 和 `BLOCK_SHORT_WHEN_4H_UP` 配置,允许在4H趋势下跌时禁止开多和在4H趋势上涨时禁止开空。此改动增强了策略的灵活性与风险控制,确保推荐逻辑与市场趋势一致,提升交易决策的准确性。
2026-02-23 17:44:57 +08:00
薇薇安
d42cee2f1a feat(async_handling): 添加任务完成回调以处理异步任务异常
在多个流处理模块中引入 `_task_done_callback` 函数,确保在异步任务完成后能够捕获并记录异常,避免未处理的任务异常导致的潜在问题。此改动提升了系统的稳定性和错误处理能力,确保在执行异步操作时能够更好地管理任务状态。
2026-02-23 15:43:13 +08:00
薇薇安
cddcf35481 feat(config): 添加4H趋势过滤配置以优化交易策略
在配置管理中新增 `BLOCK_SHORT_WHEN_4H_UP` 参数,允许在4H上涨时禁止开空,增强策略灵活性与风险控制。同时,更新前端组件以展示该配置,提升用户体验。此改动确保在不同市场条件下,策略能够更有效地避免逆势操作。
2026-02-22 22:51:36 +08:00
薇薇安
cbf778d560 feat(config): 更新前端配置组件以支持基础策略和市场方案展示
在 `GlobalConfig` 组件中引入基础策略和市场方案的概念,优化了策略选择的用户界面。新增基础策略和市场方案的状态显示,提升用户体验。同时,更新了相关逻辑以确保策略的灵活性与可视化效果。此改动为用户提供了更清晰的策略选择与使用说明。
2026-02-22 19:30:36 +08:00
薇薇安
452e40bdf5 feat(config): 添加市场状态方案以优化交易策略
在配置管理中引入市场状态方案,允许在不同市场条件下快速切换策略(如熊市、牛市、正常、保守)。更新相关参数以自动覆盖止损、仓位和趋势过滤设置,增强策略灵活性。同时,前端组件更新以支持市场状态方案的展示与选择,提升用户体验。
2026-02-22 19:15:05 +08:00
薇薇安
3b0526f392 feat(data_management): 增强交易数据统计与推算功能
在后端 API 中新增按小时和星期的交易统计功能,优化 `_compute_binance_stats` 函数以支持更细致的统计分析。同时,新增 `_enrich_trades_with_derived` 函数,补充交易记录的推算字段,包括入场价、交易小时和星期,提升策略分析的便利性。前端 `DataManagement` 组件更新,展示按小时和星期的统计信息,增强用户对交易数据的可视化理解。
2026-02-22 11:16:33 +08:00
薇薇安
1e478c8428 feat(sync_binance_orders): 增强账号同步逻辑,过滤无API密钥账号
更新 `sync_binance_orders.py` 脚本,新增对未配置 API 密钥账号的过滤,确保仅同步有效账号。优化输出信息,明确显示同步的账号数量和跳过的账号名称,提升用户体验与系统稳定性。
2026-02-22 11:07:54 +08:00
薇薇安
fc81a8d5d6 feat(data_management): 增强数据管理功能与统计分析
在后端 API 中新增 `_compute_binance_stats` 函数,用于计算交易和订单的统计数据,并更新 `query_db_trades` 函数以支持从数据库查询已同步的币安订单和成交记录。前端 `DataManagement` 组件进行了优化,新增统计数据显示功能,确保用户能够查看交易的盈亏、手续费、胜率等关键指标,提升了数据分析的可视化效果与用户体验。
2026-02-22 11:02:12 +08:00
薇薇安
aaef73c2b3 feat(data_management): 优化数据管理功能与API接口
在后端 API 中新增 `_get_active_symbols_from_income` 函数,通过收益历史 API 获取有交易活动的交易对,减少后续请求数。更新 `fetch_binance_data` 函数以支持动态获取交易对,并优化前端 `DataManagement` 组件,确保仅显示状态为 active 的账号。调整 API 服务以支持可选参数 `activeOnly`,提升数据查询的灵活性与用户体验。
2026-02-22 10:43:37 +08:00
薇薇安
69be629369 fix(data_management): 更新数据库交易查询逻辑
在后端 API 的 `query_db_trades` 函数中,将 `reconciled_only` 参数类型从布尔值更改为可选字符串,并在查询逻辑中添加了相应的处理。同时,在前端 `DataManagement` 组件中,初始化 `dbDate` 为当前日期,并优化了参数构建逻辑,以确保在请求时正确传递日期和对账状态。这些改动提升了数据查询的灵活性与准确性。
2026-02-22 10:21:01 +08:00
薇薇安
f2b04911a2 feat(data_management): 添加数据管理功能与接口
在后端 API 中新增数据管理路由,支持从币安拉取订单和成交记录的功能。前端应用中引入数据管理组件,并在路由中添加相应的链接。更新了 API 服务,提供获取账号列表和查询 DB 交易的接口,增强了系统的数据处理能力与用户体验。
2026-02-22 10:05:18 +08:00
薇薇安
fa7208f5f3 feat(binance_order_event_logger, user_data_stream): 优化算法更新字段兼容性
在 `binance_order_event_logger.py` 和 `user_data_stream.py` 中更新了 ALGO_UPDATE 事件的字段处理逻辑,增强了对不同字段名的兼容性,确保在获取算法 ID 和客户端算法 ID 时能够正确解析多种可能的字段。这一改进提升了系统在处理算法订单时的灵活性与准确性。
2026-02-21 22:45:40 +08:00
薇薇安
32c50466f3 feat(binance_client, position_manager, user_data_stream): 增强算法更新处理与日志记录
在 `binance_client.py` 中新增 `client_algo_id` 参数以支持 ALGO_UPDATE 的精确匹配。更新 `position_manager.py` 以生成并缓存 SL_/TP_ 的 `client_algo_id`,确保在止损和止盈时能够正确关联到相应的订单。`user_data_stream.py` 中优化了 ALGO_UPDATE 处理逻辑,优先使用 `clientAlgoId` 进行订单匹配,并在 Redis 中缓存相关信息。这些改进提升了系统在处理算法订单时的准确性与效率。
2026-02-21 22:41:15 +08:00
薇薇安
e40f5c797f 1 2026-02-21 17:14:03 +08:00
薇薇安
fc6c31dd5d feat(user_data_stream): 增强订单和算法更新事件的日志记录
在 `user_data_stream.py` 中为 `ORDER_TRADE_UPDATE` 和 `ALGO_UPDATE` 事件添加了日志记录功能,确保在接收到相关推送时能够记录事件信息。这一改进提升了系统的可追踪性和调试能力。
2026-02-21 17:09:41 +08:00
薇薇安
e1759a7f4c feat(market_scanner, config): 增强K线扫描逻辑与预热机制
在 `config.py` 中新增 `SCAN_PREWARM_KLINE_ENABLED` 和 `SCAN_PREWARM_CONCURRENT` 配置,支持在扫描前预热K线数据以提高缓存命中率。更新了 `market_scanner.py` 中的扫描逻辑,添加 `_prewarm_klines_for_scan` 方法,批量预订阅WebSocket和REST预取K线,优化了数据获取效率和分析超时处理。这些改进提升了系统在高并发情况下的响应能力与稳定性。
2026-02-21 12:38:05 +08:00
薇薇安
83a09f24f8 feat(binance_client, listen_key_cache, user_data_stream): 增强 listenKey 创建逻辑与重试机制
在 `binance_client.py` 中将 `create_futures_listen_key` 方法的最大重试次数从 2 增加到 3,并调整了超时设置以提高稳定性。更新了 `listen_key_cache.py` 和 `user_data_stream.py` 中对该方法的调用,确保在创建新的 listenKey 时使用新的重试逻辑。这些改进提升了系统在高并发情况下的可靠性与响应能力。
2026-02-21 11:12:21 +08:00
薇薇安
e4e6e64608 feat(trade, binance_client, position_manager, user_data_stream): 增强待处理记录对账逻辑
在 `models.py` 中新增 `get_pending_recent` 方法,用于获取最近的待处理交易记录。`binance_client.py` 中添加 `get_order_by_client_order_id` 方法,以支持按 `client_order_id` 查询订单。`position_manager.py` 中实现 `_reconcile_pending_with_binance` 方法,增强对待处理记录的对账能力。`user_data_stream.py` 中在重连前执行待处理记录对账,确保系统在断线期间的交易状态得到及时更新。这些改进提升了系统的稳定性与交易记录的准确性。
2026-02-21 11:09:01 +08:00
薇薇安
a371e50a3e feat(config, strategy): 增强多账号错峰扫描逻辑
在 `config.py` 中新增随机延迟配置,允许在多账号环境下实现更灵活的错峰扫描策略。更新了 `strategy.py` 中的相关逻辑,支持随机延迟与固定步长延迟两种模式,提升了系统在低配服务器上的性能与稳定性。此改进有助于优化资源管理与并发处理能力。
2026-02-21 10:47:39 +08:00
薇薇安
3bfbafbab2 feat(binance_client, position_manager): 增强杠杆设置与异常处理逻辑
在 `binance_client.py` 中为杠杆设置添加了网络超时重试机制,确保在请求超时的情况下能够自动重试,提升了系统的稳定性。同时,在 `position_manager.py` 中优化了临时持仓记录的错误日志,增加了异常信息的详细记录,便于后续调试与问题追踪。这些改进增强了系统的可靠性与可维护性。
2026-02-21 10:44:55 +08:00
薇薇安
b588d5b82b feat(position_manager): 增强日志记录,添加账号信息
在 `position_manager.py` 中更新了日志记录,添加了账号 ID 信息,以便于在多账号环境中更好地追踪和管理交易记录。这一改进提升了系统的可维护性和调试能力。
2026-02-21 10:38:32 +08:00
薇薇安
8e0233dd5d feat(position_manager): 增强日志记录,添加账号信息
在 `position_manager.py` 中更新了日志记录,添加了账号 ID 信息,以便于在多账号环境中更好地追踪和管理交易记录。这一改进提升了系统的可维护性和调试能力。
2026-02-21 10:17:53 +08:00
薇薇安
1a9e5a382a 1 2026-02-21 10:12:23 +08:00
薇薇安
418eff6fb7 feat(risk_manager, user_data_stream): 增强多账号支持与缓存逻辑
在 `risk_manager.py` 中新增可用保证金检查,确保在保证金不足时拒绝开仓请求,提升风险控制能力。在 `user_data_stream.py` 中更新缓存填充逻辑,支持多账号隔离,确保 Redis 缓存键按账号区分,避免数据混淆。此更新优化了系统的稳定性与风险管理。
2026-02-21 10:09:59 +08:00
薇薇安
22901abe39 feat(book_ticker_stream, ticker_24h_stream): 引入串行化锁以优化 Redis 写入逻辑
在 `book_ticker_stream.py` 和 `ticker_24h_stream.py` 中新增了串行化锁,确保在写入 Redis 时避免并发合并导致内存膨胀。更新了合并逻辑,限制 Redis 中 USDT 交易对的数量,防止键值无限增长。此改进提升了内存管理与系统稳定性。
2026-02-21 01:09:27 +08:00
薇薇安
f1e2cabc01 feat(account, stats, trades, database): 限制交易记录查询条数以优化内存管理
在 `account.py` 和 `stats.py` 中为获取状态为 open 的交易记录添加了条数限制,避免全表加载导致内存暴增。在 `trades.py` 中也为相关查询添加了限制,确保系统在处理大量数据时的稳定性。此外,更新了 `models.py` 中的默认 limit 设置,进一步优化内存使用。此更新有助于提升系统性能与资源管理。
2026-02-21 01:03:17 +08:00
薇薇安
dbcb7012bd fix(account, frontend): 兼容处理创建时间字段
在 `account.py` 中更新了创建时间的获取逻辑,兼容 `created_at` 和 `create_at` 字段。前端组件 `StatsDashboard.jsx` 中相应调整了创建时间的展示逻辑,确保在 `created_at` 字段为空时能够正确显示。此更新提升了数据展示的准确性与用户体验。
2026-02-21 00:59:54 +08:00
薇薇安
174943722a feat(trades, database, binance_client, position_manager, risk_manager): 优化交易记录查询与内存管理
在 `trades.py` 中为获取所有有记录的交易对添加了限制条数的逻辑,避免全表加载。`models.py` 中调整了查询逻辑,未传递 limit 时使用默认上限以防内存暴增。`binance_client.py` 中为交易对信息缓存添加了最大大小限制,确保内存使用合理。`position_manager.py` 和 `risk_manager.py` 中的交易记录查询也进行了条数限制,提升了系统的稳定性与性能。此更新有助于优化内存管理与查询效率。
2026-02-21 00:53:32 +08:00
薇薇安
6f9e55aaee feat(trades, database, frontend): 增强时间筛选功能与交易记录展示
在 `trades.py` 中更新了时间筛选逻辑,新增 `created` 选项以支持按创建时间筛选交易记录。在 `models.py` 中调整了查询逻辑,确保在无 `created_at` 字段时回退为 `entry_time`。前端组件 `StatsDashboard.jsx` 和 `TradeList.jsx` 中相应更新了展示逻辑,增加了创建时间的显示,提升了用户体验与数据准确性。
2026-02-21 00:31:51 +08:00
薇薇安
3ce8493af2 feat(account, stats_dashboard, binance_client, position_manager): 增强开仓时间记录与条件单错误处理
在 `account.py` 中新增 `created_at` 字段以记录开仓时间,并在 `StatsDashboard.jsx` 中更新展示逻辑,优先显示开仓时间或创建时间。`binance_client.py` 中引入 `AlgoOrderPositionUnavailableError` 异常处理,确保在条件单被拒时记录警告信息。`position_manager.py` 中优化了止损单挂单失败的处理逻辑,提升了系统的稳定性与风险控制能力。
2026-02-21 00:24:45 +08:00
薇薇安
7569c88a67 fix(binance_client, position_manager, config): 增强止损与盈利保护逻辑
在 `binance_client.py` 中优化了错误处理,新增对特定错误信息的警告记录,确保在条件单被拒时能够清晰提示。同时,在 `position_manager.py` 中引入了保本止损逻辑,确保在盈利达到一定比例时自动将止损移至含手续费的保本价,提升了风险控制能力。此外,更新了 `config.py` 中的相关配置项,以支持移动止损与保本功能的灵活性。
2026-02-20 23:38:14 +08:00
薇薇安
13a0e7d580 delete: 移除过时的文档与代码文件
删除了多个不再使用的文档和代码文件,包括交易更新推送、条件订单推送、REST API 文档、WebSocket API 文档及相关的策略分析文档。这些文件的移除有助于清理代码库,确保项目的整洁性与可维护性。
2026-02-20 17:49:00 +08:00
薇薇安
f3089fdf7f fix(binance_client): 增强错误处理与止盈价校验逻辑
在 `binance_client.py` 中新增了对特定错误代码的处理,确保在遇到 -4509 和 -4061 错误时能够正确抛出异常。同时,优化了止盈价的合理性校验,确保在无法获取当前价格时,止盈价不低于 0.01,避免因错误数据导致的挂单被拒,提升了交易逻辑的稳定性与风险控制能力。
2026-02-20 17:14:44 +08:00
薇薇安
9b81832af2 feat(trades, database, frontend): 增强交易记录同步与展示功能
在 `trades.py` 中更新了 `include_sync` 参数的默认值为 `True`,以便于订单记录与币安一致,并添加了提示信息以指导用户如何补全缺失的订单号。在 `models.py` 中新增了 `get_trades_missing_entry_order_id` 方法,用于获取缺少 `entry_order_id` 的记录,确保在同步时能够补全数据。前端组件 `StatsDashboard.jsx` 和 `TradeList.jsx` 中相应调整了开仓时间的展示逻辑和无交易记录时的提示信息,提升了用户体验与数据准确性。
2026-02-20 12:17:01 +08:00
薇薇安
22830355c6 feat(binance_client): 优化超时设置与错误处理逻辑
在 `binance_client.py` 中调整了 API 请求的超时设置,首次请求超时为 25 秒,后续请求为 35 秒,以适应币安的响应时间。同时,增加了重试机制的等待时间,首次重试为 4 秒,后续重试为 5 秒,确保在网络波动时能更好地恢复。此外,新增了止盈价合理性校验,避免因错误数据导致的挂单被拒,提升了交易逻辑的稳定性与风险控制能力。
2026-02-20 11:23:11 +08:00
薇薇安
9299d70a31 feat(position_manager, database): 添加开仓时间记录功能以优化交易记录
在 `position_manager.py` 中引入了真实开仓时间的获取逻辑,确保在补建交易记录时使用币安的实际开仓时间,避免时间显示为“当前时间”。同时,在 `models.py` 中更新了 `Trade` 类,新增 `entry_time` 参数以存储开仓时间,提升了交易记录的准确性与分析能力。
2026-02-20 00:51:31 +08:00
薇薇安
2c5524bdcf feat(binance_client, config): 添加单向持仓模式配置以优化交易逻辑
在 `binance_client.py` 中引入了 `ONE_WAY_POSITION_ONLY` 配置,确保在单向持仓模式下不传递 `positionSide`,避免对冲模式检测。同时,更新了 `config.py`,新增该配置项以支持该功能,提升了交易策略的灵活性与风险控制能力。
2026-02-20 00:40:23 +08:00
薇薇安
bfe3d8ec75 feat(binance_client, position_manager): 优化对冲模式处理与持仓检查逻辑
在 `binance_client.py` 中增强了对冲模式的处理逻辑,添加了对对冲模式检测失败的处理,确保在尝试单向模式失败后再尝试对冲模式。同时,在 `position_manager.py` 中引入了持仓存在性检查,避免因持仓不存在或方向不匹配导致的错误,提升了系统的稳定性与风险控制能力。
2026-02-20 00:36:09 +08:00
薇薇安
d31c44a22a feat(position_manager, config): 添加早止盈功能以优化山寨币交易策略
在 `position_manager.py` 中实现了早止盈逻辑,允许在盈利达到一定比例且持仓时间超过指定小时数时,自动市价止盈,避免反转风险。同时,在 `config.py` 中新增相关配置项以支持该功能,提升了交易策略的灵活性与风险控制能力。
2026-02-20 00:33:28 +08:00
薇薇安
43daa922a4 feat(position_manager): 优化止损与止盈逻辑,确保实际止损距离与盈亏比计算一致
在 `position_manager.py` 中更新了止损和止盈计算逻辑,确保使用实际止损距离进行盈亏比的计算,避免因保证金封顶导致的止盈不合理。同时,新增止盈上限配置,防止止盈距离过大。此改动提升了交易策略的准确性与风险控制能力。
2026-02-19 18:30:23 +08:00
薇薇安
f5570f4804 1 2026-02-19 18:02:46 +08:00
薇薇安
be43ec1c33 feat(kline_stream): 优化 Redis 数据处理逻辑与内存管理
在 `kline_stream.py` 中改进了 Redis 数据处理逻辑,避免每条消息都从 Redis 获取全量数据,减少内存占用。通过复用待处理列表,提升了性能并降低了内存使用。更新了缓存管理机制,确保在有 Redis 时优先使用其进行数据存储,进一步优化了系统的内存使用效率与稳定性。
2026-02-19 09:23:53 +08:00
薇薇安
95867e90f8 feat(redis_cache, binance_client, market_scanner, position_manager, ticker_24h_stream, book_ticker_stream): 引入 Redis 缓存机制以优化数据读取与内存管理
在多个模块中实现 Redis 缓存机制,优先从 Redis 读取数据,减少进程内存占用。更新 `binance_client.py`、`market_scanner.py`、`position_manager.py`、`ticker_24h_stream.py` 和 `book_ticker_stream.py`,确保在有 Redis 时优先使用其进行数据存储,降级到内存缓存。调整缓存管理逻辑,限制进程内缓存的最大条数为 500,避免内存无限增长。此改动提升了数据访问效率,优化了内存使用,增强了系统的整体性能与稳定性。
2026-02-19 00:45:56 +08:00
薇薇安
f4feea6b87 feat(ticker_stream, book_ticker_stream): 优化内存管理与Redis写入逻辑
在 `ticker_24h_stream.py` 和 `book_ticker_stream.py` 中引入新的内存管理机制,限制进程内缓存的最大条数为 500,避免内存无限增长。更新 Redis 写入逻辑,确保在有 Redis 时优先写入 Redis,而不在进程内存中常驻数据。通过定期从 Redis 拉取数据并合并,提升了系统的内存使用效率与稳定性,同时优化了日志记录以减少高负载时的输出频率。此改动进一步增强了系统性能与资源管理能力。
2026-02-19 00:34:35 +08:00
薇薇安
a498520c51 feat(kline_stream): 优化 Redis 写入逻辑与内存管理
在 `kline_stream.py` 中增强了 Redis 写入机制,限制待处理队列大小以防止无限增长,并在 Redis 处理失败时降级到进程内存。更新了缓存管理逻辑,确保在有 Redis 时优先使用 Redis 进行数据存储,提升了系统的内存使用效率与稳定性。同时,调整了日志记录以减少高负载时的输出频率。此改动进一步优化了消息处理与系统性能。
2026-02-19 00:26:34 +08:00
薇薇安
59e25558cd feat(redis_cache, kline_stream, user_data_stream, risk_manager): 优化缓存机制与内存管理
在多个模块中引入 Redis 作为主要缓存机制,减少进程内存占用。更新 `binance_client.py`、`kline_stream.py`、`user_data_stream.py` 和 `risk_manager.py`,实现优先从 Redis 读取数据,降级到内存缓存。调整缓存 TTL 和最大条数,确保系统稳定性与性能。此改动提升了数据访问效率,优化了内存使用,增强了系统的整体性能。
2026-02-19 00:19:54 +08:00
薇薇安
80872231a5 feat(kline_stream, diagnostics): 增强 K线缓存管理与系统负载诊断功能
在 `kline_stream.py` 中新增缓存清理机制,限制缓存总大小并定期清理过期条目,防止内存无限增长。更新 `backend/诊断负载.sh` 脚本,优化系统负载检查逻辑,提供更详细的进程与日志信息,提升用户对交易服务状态的监控能力。此改动增强了系统的稳定性与性能。
2026-02-19 00:06:23 +08:00
薇薇安
e21014eb50 feat(diagnostics, documentation): 新增系统负载诊断脚本与指南
在 `backend` 目录下新增 `诊断负载.sh` 脚本,提供系统负载、CPU、内存使用情况及数据库连接数的快速诊断功能。新增文档 `负载问题排查与快速降负载指南.md`,详细说明负载诊断步骤、常见原因及解决方法,帮助用户有效管理系统负载。此改动提升了系统监控能力与用户支持。
2026-02-18 23:35:09 +08:00
薇薇安
33ac043324 feat(trades, database): 优化交易记录查询与过滤逻辑
在 `trades.py` 中更新 `get_trades` 和 `get_trade_stats` 方法,增强了交易记录的查询功能,支持更多过滤选项(如 `limit`、`reconciled_only` 和 `include_sync`)。同时,调整了日志记录级别,从 `info` 改为 `debug`,以减少高负载时的日志输出。更新 `database/models.py` 中的 `get_all` 方法,新增参数以支持更灵活的查询,提升了系统的性能与稳定性。
2026-02-18 22:22:53 +08:00
薇薇安
7139b5de76 feat(trades, database): 增强订单同步与记录完善逻辑
在 `trades.py` 中更新 `sync_trades_from_binance` 方法,确保使用当前账号的 API 密钥进行订单同步,并优化了日志记录以反映同步状态。新增自动全量同步逻辑,处理无记录情况下的补全需求。更新 `database/models.py` 中的 `update_pending_by_entry_order_id` 方法,提供兜底机制以完善 pending 记录,确保在缺失 clientOrderId 时仍能更新交易状态。此改动提升了交易记录的完整性与系统的稳定性。
2026-02-18 22:11:06 +08:00
薇薇安
44458dca90 feat(kline_stream): 优化消息处理与Redis写入逻辑
在 `kline_stream.py` 中引入并发限制,使用信号量控制同时处理的消息数量,避免任务堆积导致性能下降。优化消息处理为异步方法,减少事件循环阻塞。增加批量写入Redis的机制,降低写入频率,提升系统性能与稳定性。同时,调整日志记录频率,减少高负载时的日志输出。此改动显著提升了消息处理效率与系统的响应能力。
2026-02-18 00:52:45 +08:00
薇薇安
c9d9836df5 feat(kline_stream, market_scanner, config): 优化 K线订阅逻辑与缓存机制
在 `config.py` 中新增 `SCAN_LIMIT_KLINE_SUBSCRIBE` 配置,限制 K线订阅数量以降低负载。更新 `kline_stream.py`,引入订阅统计与数量限制,避免过多订阅导致性能问题。修改 `market_scanner.py`,优化 K线数据获取流程,优先使用已有缓存,减少不必要的订阅。此改动提升了系统的稳定性与性能。
2026-02-18 00:36:44 +08:00
薇薇安
0a7bb0de2d feat(user_data_stream, binance_client): 优化 listenKey 管理与缓存机制
在 `user_data_stream.py` 中更新 `start` 方法,优先从缓存获取 listenKey,避免重复创建。增强了错误处理和日志记录,确保在缓存不可用时能够回退到创建新 key 的逻辑。更新 `binance_client.py` 中的 `create_futures_listen_key` 方法,新增重试机制以提高稳定性。此改动提升了 listenKey 管理的灵活性和系统的性能。
2026-02-18 00:11:54 +08:00
薇薇安
a404f1fdf8 feat(binance_client, market_scanner): 优化 K线数据获取逻辑与缓存机制
在 `binance_client.py` 中更新 `get_klines` 方法,新增多账号共享 Redis 缓存机制,提升 K线数据获取效率,减少 REST API 调用。优化日志记录,确保清晰反馈缓存来源。更新 `config.py`,引入 `SCAN_PREFER_WEBSOCKET` 配置,优先使用 WebSocket 获取数据。修改 `market_scanner.py`,增强 K线数据获取流程,优先从共享缓存读取,确保数据完整性与实时性。此改动提升了系统的性能与稳定性。
2026-02-17 23:59:31 +08:00
薇薇安
a862aec4f5 feat(binance_client): 优化 listenKey 创建与延长逻辑,支持 WebSocket API
在 `binance_client.py` 中更新 `create_futures_listen_key` 和 `keepalive_futures_listen_key` 方法,新增优先使用 WebSocket API 的功能,若 WebSocket 不可用则回退到 REST API。增强了错误处理和日志记录,确保在请求失败时提供更清晰的反馈。此改动提升了 listenKey 管理的灵活性和系统的稳定性。
2026-02-17 23:28:28 +08:00
薇薇安
c7f1361d99 1 2026-02-17 23:13:49 +08:00
薇薇安
b0392f358e 1 2026-02-17 23:06:22 +08:00
薇薇安
60a7e15100 feat(trades, trade_list): 增强订单同步功能与用户界面优化
在 `trades.py` 中更新 `sync_trades_from_binance` 方法,改进日志记录以区分全量与增量同步模式,并添加对获取订单数为零的警告处理。更新 `TradeList.jsx` 组件,优化用户界面,新增订单同步选项和状态显示,提升用户体验。此改动增强了系统的灵活性和数据完整性。
2026-02-17 23:02:49 +08:00
薇薇安
1430ddc532 feat(trades, trade_list, api): 增强历史订单同步功能以支持所有交易对的补全
在 `trades.py` 中更新 `sync_trades_from_binance` 方法,新增 `sync_all_symbols` 参数,允许用户选择同步所有交易对的历史订单并创建缺失的交易记录。更新前端组件 `TradeList.jsx` 以支持该功能,添加用户确认提示和状态显示,提升用户体验和数据完整性。同时,调整 API 接口以处理新的参数,确保与后端交互的准确性。此改动增强了交易记录的完整性和系统的灵活性。
2026-02-17 22:51:31 +08:00
薇薇安
ac1336dab8 feat(trades): 增强交易同步逻辑以优化记录查询和错误处理
在 `trades.py` 中更新了 `sync_trades_from_binance` 方法,新增时间范围内记录的查询逻辑,确保能够补全缺失的历史订单号。引入了更详细的日志记录,提升了错误处理的可追溯性,确保在获取交易对列表失败时提供清晰的反馈。此改动提升了交易记录的完整性和系统的稳定性。
2026-02-17 22:46:33 +08:00
薇薇安
01b8a4932f feat(trades): 优化订单同步逻辑以补全缺失的平仓和开仓订单
在 `trades.py` 中增强了 `sync_trades_from_binance` 方法,新增对平仓订单和开仓订单的分类处理,确保能够补全缺失的订单号。引入了对已存在订单的跳过逻辑,记录无法匹配的情况,并优化了日志记录以提升可追溯性。此改动提升了交易记录的完整性和系统的稳定性。
2026-02-17 22:41:15 +08:00
薇薇安
55ae7b5b08 feat(trade_list, api): 添加订单同步功能以补全缺失的历史订单
在 `TradeList.jsx` 中新增订单同步功能,允许用户从币安同步最近的历史订单并补全缺失的订单号。引入 `syncTradesFromBinance` 方法于 `api.js`,实现与后端的交互,处理同步请求并返回结果。更新前端界面以显示同步状态和结果,提升用户体验和数据完整性。
2026-02-17 22:27:10 +08:00
薇薇安
415589e625 feat(trade, position_manager, user_data_stream): 增强交易记录管理与用户数据流处理
在 `models.py` 中新增 `update_entry_order_id` 方法,用于补全或更新开仓订单号,提升交易记录的完整性。更新 `set_exit_order_id_for_open_trade` 方法以支持按 `entry_order_id` 精确匹配,优化平仓订单的回写逻辑。在 `position_manager.py` 中添加对 `entry_order_id` 的处理,确保在保存交易记录时能够及时补全。更新 `user_data_stream.py` 中的日志记录,提供更详细的状态信息,增强系统的可追溯性与调试能力。
2026-02-17 22:11:36 +08:00
薇薇安
48c3f946cc feat(config, market_scanner, position_manager, strategy): 引入市场节奏自动识别与流动性检查功能
在 `config.py` 中新增市场节奏自动识别配置,支持低波动期参数切换。更新 `market_scanner.py` 以根据市场波动情况动态调整策略,并在扫描时计算中位数以判断市场状态。同时,在 `position_manager.py` 中实现时间止损逻辑,确保在低波动期内有效管理持仓。新增流动性检查功能于 `strategy.py`,在开仓前评估市场深度与价差,提升交易决策的准确性与风险控制能力。
2026-02-17 10:41:47 +08:00
薇薇安
42480ef886 feat(trades): 添加时间筛选功能以优化交易记录查询
在 `trades.py` 中新增 `time_filter` 参数,允许用户按平仓时间或开仓时间筛选交易记录。更新 `Trade.get_all` 方法以支持该功能,并调整查询逻辑以符合新的时间筛选需求。同时,前端组件 `TradeList.jsx` 也进行了相应更新,增加了时间筛选按钮,提升了用户体验和数据查询的灵活性。
2026-02-17 08:01:35 +08:00
薇薇安
3a2536ae96 fix(system): 优化服务状态检查的异常处理逻辑
在 `system.py` 中更新了服务状态检查的异常处理逻辑,当 supervisor 未安装或未运行时,记录为 WARNING 并返回友好的错误信息。增强了日志记录的可读性,确保在出现问题时提供清晰的反馈。同时,在 `position_manager.py` 中改进了止损止盈检查的错误日志,确保记录详细的错误信息以便于调试。
2026-02-17 07:53:54 +08:00
薇薇安
c750478af9 fix(binance_client): 优化签名计算逻辑以符合币安要求
在 `binance_client.py` 中更新了签名计算逻辑,确保参与签名的参数格式与币安REST API一致。新增 `_param_val_for_signature` 函数处理布尔值和空值,提升了签名的准确性和安全性。此改动增强了系统的稳定性和合规性。
2026-02-16 19:25:22 +08:00
薇薇安
e5bc2547aa feat(binance_client): 引入WebSocket交易客户端以优化下单逻辑
在 `binance_client.py` 中新增 WebSocket 交易客户端的延迟初始化,优先使用 WebSocket 下单以减少 REST 超时。更新 `futures_create_algo_order` 方法,尝试通过 WebSocket 创建条件单,并在失败时回退到 REST 调用。同时,调整 `ALGO_ORDER_TIMEOUT_SEC` 的默认值为 45秒,以应对高负载情况。增强了异常处理和日志记录,确保系统的稳定性和可追溯性。
2026-02-16 19:19:56 +08:00
薇薇安
857128bca9 feat(config, market_scanner, strategy): 增强多账号支持与并发控制
在 `config.py` 中新增多账号扫描配置,支持并发数和错峰扫描设置。更新 `market_scanner.py` 以根据配置动态调整并发请求数,优化资源使用。修改 `strategy.py` 以实现多账号错峰扫描,避免低配服务器的 CPU 过载,提升系统稳定性和效率。
2026-02-16 18:28:38 +08:00
薇薇安
0fb42a5f24 feat(market_cache): 引入市场数据缓存机制以优化API调用
在 `backend/database/models.py` 中新增 `MarketCache` 类,支持从数据库缓存交易对信息和资金费率,减少对币安API的调用频率。更新 `binance_client` 和 `market_scanner` 以优先从缓存读取数据,添加超时处理和重试机制,提升系统稳定性。同时,增强了资金费率和主动买卖量的过滤逻辑,确保在开仓前进行有效的风险控制。
2026-02-16 18:05:11 +08:00
薇薇安
43e993034f feat(redis_integration): 支持多进程共用市场数据流
在 `binance_client`、`kline_stream`、`book_ticker_stream` 和 `ticker_24h_stream` 中引入 Redis 缓存支持,允许 Leader 进程写入数据,其他进程从 Redis 读取,提升数据获取效率。更新了相关逻辑以确保在多进程环境下的稳定性和一致性,同时增强了异常处理和日志记录,确保系统的可追溯性。
2026-02-16 17:44:10 +08:00
薇薇安
249aec917a feat(binance_client, market_scanner, position_manager): 增强行情数据获取与处理逻辑
在 `binance_client` 中新增多个公开行情接口,包括深度信息、资金费率和未平仓合约数的获取,优化了 REST API 的调用逻辑。更新 `market_scanner` 以并行请求主周期和确认周期的 K线数据,提升了数据获取效率并引入超时处理。`position_manager` 中增加了从深度信息获取当前价格的逻辑,确保在多种情况下都能准确获取价格,增强了系统的稳定性和可追溯性。
2026-02-16 17:30:05 +08:00
薇薇安
3539180362 feat(main): 添加自定义 asyncio 异常处理器以优化日志记录
在主函数中引入自定义的 asyncio 异常处理器,确保在 WebSocket 连接关闭时的 ping 操作不会产生错误日志。此改动提升了系统的日志可读性,避免了不必要的错误信息输出,同时保持了对其他异常的标准处理方式。
2026-02-16 17:15:06 +08:00
薇薇安
30f4a22fb4 feat(binance_client, position_manager): 优化价格获取逻辑与异常处理
在 `binance_client` 中引入 K线和最优挂单的 WebSocket 流,优先从缓存中获取价格数据,减少对 REST API 的依赖。同时,更新了价格获取逻辑,确保在未能获取价格时提供详细的错误信息。增强了异常处理,确保在请求超时或失败时记录相关日志,提升系统的稳定性和可追溯性。
2026-02-16 17:11:25 +08:00
薇薇安
dfbdfee596 fix(binance_client, ticker_stream, user_data_stream): 增强异常处理和日志记录
在 `binance_client`、`ticker_24h_stream` 和 `user_data_stream` 中优化了异常处理逻辑,确保在发生错误时记录详细的错误类型和信息。更新了日志格式,以便于后续排查和监控。同时,增加了对请求超时的处理,提升了系统的稳定性和可追溯性。
2026-02-16 16:51:22 +08:00
薇薇安
ec5c76c546 feat(trades): 优化从币安同步历史订单的逻辑
更新 `sync_trades_from_binance` 接口,新增 `account_id` 参数以支持多账户同步。改进了订单同步逻辑,仅对数据库中有记录的交易对进行拉取,避免全市场请求,提升效率。同时,增强了异常处理和日志记录,确保同步过程的稳定性和可追溯性。
2026-02-16 15:51:51 +08:00
薇薇安
c6126a42c9 feat(ticker_stream): 引入24小时行情WebSocket流以优化数据获取
在交易系统中新增24小时行情WebSocket流的支持,优先从缓存中读取行情数据,减少对REST API的依赖。更新市场扫描器以使用WebSocket缓存,确保在缓存过期时回退到REST请求。同时,添加了相应的异常处理逻辑以增强系统的稳定性。
2026-02-16 15:22:51 +08:00
薇薇安
5154b4933e feat(trading_system): 优化交易记录管理与用户数据流集成
在 `position_manager` 和 `risk_manager` 中引入用户数据流缓存,优先使用 WebSocket 更新持仓和余额信息,减少对 REST API 的依赖。同时,增强了交易记录的创建和更新逻辑,支持在订单成交后完善记录,确保与币安数据一致性。新增 `update_open_fields` 和 `update_pending_to_filled` 方法,提升了交易记录的管理能力。
2026-02-16 15:16:49 +08:00
薇薇安
aa073099f2 feat(position_manager): 添加落库失败独立日志功能以便于排查
在 `position_manager` 中新增日志记录功能,当币安订单已成交但未成功写入数据库时,将相关信息记录到独立的 `trade_db_failures.log` 文件中。此功能有助于排查与对账,确保交易记录的准确性和完整性。
2026-02-16 14:23:01 +08:00
薇薇安
b9392e096c feat(trades): 添加对账校验接口以验证交易记录准确性
新增 `GET /api/trades/verify-binance` 接口,允许用户校验与币安的交易记录一致性。该接口支持指定时间范围和校验条数,返回校验结果的汇总和详细信息,确保策略执行分析所依赖的数据与交易所一致。
2026-02-16 14:02:55 +08:00
薇薇安
225cb436d1 feat(trades): 添加可对账记录筛选功能以确保与币安一致
在获取交易记录和统计时,新增 `reconciled_only` 参数,默认值为 true,确保仅返回可对账的交易记录(包含 entry_order_id 和 exit_order_id)。此改动有助于提高统计的准确性,确保系统盈亏与币安一致。
2026-02-16 12:42:58 +08:00
薇薇安
c7e39ec1a4 1 2026-02-16 12:13:44 +08:00
薇薇安
a402007b99 1 2026-02-16 12:05:11 +08:00
薇薇安
22efd377a7 fix: 修正盈亏计算以包含实盘数据
使用实盘盈亏和手续费计算有效盈亏,当实盘数据存在时优先使用。
同时保留原有的价格差盈亏计算作为参考。
2026-02-16 11:54:37 +08:00
薇薇安
0eb9b076e3 调整可能盈利的策略 2026-02-16 11:41:43 +08:00
薇薇安
a884ed13ad 订单记录与币安的一致性 2026-02-16 10:46:09 +08:00
薇薇安
b5590b760f 1 2026-02-16 10:36:03 +08:00
薇薇安
c1a9d52ae7 1 2026-02-16 10:08:44 +08:00
薇薇安
8cb9bbf42f 1 2026-02-16 09:57:26 +08:00
薇薇安
2c8c13b8d9 1 2026-02-15 22:52:44 +08:00
薇薇安
550d0b278d feat(position_manager): 添加多级止盈字段以支持部分止盈
扩展 position_info 字典,新增 takeProfit1、takeProfit2、partialProfitTaken 和 remainingQuantity 字段。
当 take_profit_2 未设置时,默认使用 take_profit_price 作为其值,确保向后兼容。
2026-02-15 22:35:45 +08:00
薇薇安
2061583482 feat: 添加持仓详细监控日志开关用于问题排查
在多个配置文件中添加 POSITION_DETAILED_LOG_ENABLED 配置项,用于控制是否记录持仓监控的详细日志。
当开启时,position_manager.py 会在每次检查时记录当前价格、止损止盈价和收益率等详细信息,
便于在排查问题时观察持仓状态,平时建议关闭以减少日志噪音。
2026-02-15 22:02:51 +08:00
薇薇安
161d42c90b chore: unify TP1/TP2 config and revert TP2 to 30% 2026-02-15 17:59:53 +08:00
薇薇安
94ba0ab5a4 1 2026-02-15 16:14:03 +08:00
薇薇安
fe3da9dfb5 11 2026-02-15 14:47:27 +08:00
薇薇安
b325084d91 1 2026-02-15 14:44:05 +08:00
薇薇安
f6f4ca11ae 1 2026-02-15 14:24:09 +08:00
薇薇安
9cd39c3655 1 2026-02-15 14:18:58 +08:00
薇薇安
ab8023139f 1 2026-02-15 14:08:51 +08:00
薇薇安
2b5906ca6d 1 2026-02-15 13:35:33 +08:00
薇薇安
977669302f 1 2026-02-15 10:21:42 +08:00
薇薇安
dda1ffc849 1 2026-02-15 10:18:56 +08:00
薇薇安
66a78759d3 1 2026-02-15 10:15:42 +08:00
薇薇安
965c1651cd 1 2026-02-15 10:09:50 +08:00
薇薇安
a1b54d658f 1 2026-02-15 10:06:25 +08:00
薇薇安
cb251a7866 1 2026-02-15 09:58:07 +08:00
薇薇安
e024bf8ebe 1 2026-02-15 09:44:56 +08:00
薇薇安
c4a23be3bf 1 2026-02-15 09:21:15 +08:00
薇薇安
7abf7db2df 1 2026-02-15 08:46:20 +08:00
薇薇安
d3ca06a8ad 1 2026-02-15 08:32:29 +08:00
薇薇安
154f1fbf1d 1 2026-02-15 08:26:22 +08:00
薇薇安
7cf6613540 1 2026-02-15 00:47:55 +08:00
薇薇安
9379a9815e 1 2026-02-15 00:42:50 +08:00
薇薇安
ba4a4b2205 1 2026-02-15 00:37:08 +08:00
薇薇安
99df066101 1 2026-02-15 00:08:12 +08:00
薇薇安
d985b94161 1 2026-02-14 23:52:22 +08:00
薇薇安
baa8277aee 1 2026-02-14 23:43:33 +08:00
薇薇安
7df054f638 1 2026-02-14 23:39:19 +08:00
薇薇安
0a9377f5ac 1 2026-02-14 19:59:57 +08:00
薇薇安
b779b7b9ec 1 2026-02-14 19:56:58 +08:00
薇薇安
11cd55ff7b 添加 client_order_id 支持,确保在交易记录中与币安自定义订单号一致 2026-02-14 19:24:27 +08:00
薇薇安
c53c5fc64a 同步币安成交的手续费与实际盈亏,确保统计一致性 2026-02-14 19:15:27 +08:00
薇薇安
78667c2604 添加全局配置项以支持同步缺失持仓和系统订单标识前缀 2026-02-14 18:56:09 +08:00
薇薇安
d7b4b82293 1 2026-02-14 18:50:20 +08:00
薇薇安
3f4e0d8971 1 2026-02-14 18:43:42 +08:00
薇薇安
3d9f58f049 使用自定义订单号确保与币安一致 2026-02-14 18:38:56 +08:00
薇薇安
a52b8c4738 1 2026-02-14 18:18:07 +08:00
薇薇安
16cf4f2157 1 2026-02-14 18:06:10 +08:00
薇薇安
19371a8e60 1 2026-02-14 17:53:22 +08:00
薇薇安
1830444ef0 1 2026-02-14 17:48:50 +08:00
薇薇安
a88e114b4c 1 2026-02-14 17:20:34 +08:00
薇薇安
777f9ff703 1 2026-02-14 17:11:46 +08:00
薇薇安
345416e32f 1 2026-02-14 14:55:40 +08:00
薇薇安
e816524972 1 2026-02-14 14:47:30 +08:00
薇薇安
ca0bbeddbf 1 2026-02-14 14:36:23 +08:00
薇薇安
ca959c1f8a 1 2026-02-14 13:57:04 +08:00
薇薇安
4a69b42392 1 2026-02-14 12:14:54 +08:00
薇薇安
d7f4f43d7f 1 2026-02-14 12:04:32 +08:00
薇薇安
6da90babe9 1 2026-02-14 11:59:29 +08:00
薇薇安
29ebb8e2c9 1 2026-02-14 11:49:51 +08:00
薇薇安
3d350ebea6 1 2026-02-14 11:36:55 +08:00
薇薇安
4b6d73a5c4 1 2026-02-14 11:34:37 +08:00
薇薇安
dab0981935 1 2026-02-14 09:31:44 +08:00
薇薇安
d97363e3d4 1 2026-02-14 01:27:47 +08:00
薇薇安
f8058083e3 1 2026-02-13 23:29:33 +08:00
薇薇安
213e31142c 1 2026-02-13 22:37:40 +08:00
薇薇安
a19c716166 1 2026-02-13 22:14:04 +08:00
薇薇安
c9f676c68a 1 2026-02-13 22:02:10 +08:00
薇薇安
60823b4056 1 2026-02-13 21:56:13 +08:00
薇薇安
42c6904604 1 2026-02-13 21:39:10 +08:00
薇薇安
43a09a57a6 1 2026-02-13 21:09:29 +08:00
薇薇安
43e44a976b 1 2026-02-13 20:25:56 +08:00
薇薇安
8b45c81906 1 2026-02-13 20:17:25 +08:00
薇薇安
41630bf580 1 2026-02-13 20:14:35 +08:00
薇薇安
5f18abbe2f 1 2026-02-13 19:09:23 +08:00
薇薇安
78dcba0c34 1 2026-02-13 19:03:17 +08:00
薇薇安
80485e7271 1 2026-02-13 19:00:04 +08:00
薇薇安
be1349c1fc 1 2026-02-13 18:47:39 +08:00
薇薇安
d4fa954682 1 2026-02-13 18:40:26 +08:00
薇薇安
cb5f513904 1 2026-02-13 18:32:06 +08:00
薇薇安
46d31fde59 1 2026-02-13 17:56:27 +08:00
薇薇安
69327a6668 1 2026-02-13 08:27:05 +08:00
薇薇安
a03bb0e8f3 1 2026-02-13 08:20:09 +08:00
薇薇安
4c7cd86fb0 1 2026-02-13 08:15:09 +08:00
薇薇安
8154508c82 trae优化交易 2026-02-13 08:11:45 +08:00
薇薇安
73f148a120 1 2026-02-13 07:40:29 +08:00
薇薇安
01c11d62f6 1 2026-02-13 07:35:23 +08:00
薇薇安
ce54164b63 1 2026-02-13 07:22:27 +08:00
薇薇安
fcbf702f71 1 2026-02-13 07:14:12 +08:00
薇薇安
7550b707f4 1 2026-02-12 21:07:11 +08:00
薇薇安
2c81d47b2b 增加放宽限制下单策略 2026-02-12 21:04:10 +08:00
薇薇安
e7443dddf3 1 2026-02-12 16:57:58 +08:00
薇薇安
7379dd1f4b 用户配置优先 2026-02-12 14:21:55 +08:00
薇薇安
42eab75e3e 调整普通用户配置项,去掉没用的 2026-02-12 14:13:27 +08:00
薇薇安
68f028f0fc 增加激进控制可放大仓位 2026-02-12 14:03:42 +08:00
薇薇安
0df841c93c 1 2026-02-12 13:57:19 +08:00
薇薇安
8c91db3f60 1 2026-02-12 10:15:44 +08:00
薇薇安
a033d1ea6d 1 2026-02-12 10:08:25 +08:00
薇薇安
71f0378c5f 1 2026-02-12 08:27:00 +08:00
薇薇安
20788e4b77 1 2026-02-11 11:38:27 +08:00
薇薇安
99c40c5752 1 2026-02-11 09:15:14 +08:00
薇薇安
972156a702 1 2026-02-10 15:40:56 +08:00
薇薇安
1dd8d5893d 1 2026-02-10 08:42:59 +08:00
薇薇安
c27bed1efd 1 2026-02-09 20:10:59 +08:00
薇薇安
78d1c3ac37 1 2026-02-09 17:39:15 +08:00
薇薇安
d16bb53e60 1 2026-02-09 17:29:11 +08:00
薇薇安
bbbac43506 1 2026-02-09 17:01:25 +08:00
薇薇安
d184eafae8 1 2026-02-09 15:51:18 +08:00
薇薇安
79adc79f98 1 2026-02-08 20:38:12 +08:00
薇薇安
ecb4b9fc2f 1 2026-02-08 20:37:40 +08:00
薇薇安
ec54716266 1 2026-02-08 20:33:37 +08:00
薇薇安
cc324eead5 11 2026-02-08 20:21:21 +08:00
薇薇安
bfae183e39 1 2026-02-08 20:06:47 +08:00
薇薇安
262ee661a5 1 2026-02-08 09:27:49 +08:00
薇薇安
3609bddace 1 2026-02-06 13:22:20 +08:00
薇薇安
c4cd3e0ffa 1 2026-02-06 11:01:31 +08:00
薇薇安
411bb1d3d3 1 2026-02-06 09:18:22 +08:00
薇薇安
b4b001833f 1 2026-02-06 08:31:10 +08:00
薇薇安
7e62247217 1 2026-02-06 08:21:29 +08:00
薇薇安
79fb20bf41 1 2026-02-05 19:55:50 +08:00
薇薇安
a38f5ff05d 1 2026-02-05 19:38:18 +08:00
薇薇安
9be1c5777d 优化推荐模块 2026-02-04 16:07:25 +08:00
薇薇安
f8eca1ed59 优化交易的止盈亏损问题 2026-02-04 15:00:12 +08:00
薇薇安
922a8f3820 1 2026-02-04 13:45:30 +08:00
薇薇安
6bee742413 1 2026-02-04 11:22:33 +08:00
薇薇安
ea4410da0f 1 2026-02-04 11:11:30 +08:00
薇薇安
2f50ecd172 1 2026-02-03 22:51:04 +08:00
薇薇安
833f8096d7 1 2026-02-03 16:21:07 +08:00
薇薇安
78c2d7f1ae 1 2026-02-03 16:09:52 +08:00
薇薇安
efc88b2083 1 2026-02-03 16:03:43 +08:00
薇薇安
9f21cc1d02 1 2026-02-03 15:55:01 +08:00
薇薇安
de3d9568e9 1 2026-02-03 15:48:18 +08:00
薇薇安
81daf10555 1 2026-02-03 15:46:45 +08:00
薇薇安
9491012938 1 2026-02-03 15:44:29 +08:00
薇薇安
9958af7c3f 1 2026-02-03 15:42:29 +08:00
薇薇安
654103177d 1 2026-02-03 15:26:28 +08:00
薇薇安
1e9b27f8b4 1 2026-02-03 15:22:25 +08:00
薇薇安
50c933a8b0 1 2026-02-03 15:00:17 +08:00
薇薇安
614b28493b Merge branch 'master' of https://gitea.xoparadise.com/miracle/auto_trade_sys 2026-02-03 14:47:29 +08:00
薇薇安
a9bed79871 1 2026-02-03 14:46:52 +08:00
薇薇安
46eb9f1187 1 2026-02-03 14:01:11 +08:00
薇薇安
d34e3cc998 1 2026-02-03 14:00:59 +08:00
薇薇安
02a1a087ab 1 2026-02-03 13:51:49 +08:00
薇薇安
8ece78a3dc 1 2026-02-03 13:46:46 +08:00
薇薇安
c713e7d27e 优化订单记录的页面 2026-02-03 13:41:45 +08:00
薇薇安
8bc6c1ecc4 a 2026-02-03 13:27:52 +08:00
薇薇安
c23db4aba0 请求账号1的问题 2026-02-03 13:12:36 +08:00
薇薇安
377ae3b966 a 2026-02-03 11:55:04 +08:00
薇薇安
d0688c57b7 a 2026-02-03 11:45:20 +08:00
薇薇安
0962df4112 a 2026-02-03 11:41:51 +08:00
薇薇安
97ecf8e605 修正用户交易状态不正常显示问题 2026-02-03 11:35:54 +08:00
薇薇安
040473d4d3 优化亏损交易策略 2026-02-03 11:26:46 +08:00
薇薇安
cc6750fa47 平仓bug处理 2026-02-03 11:21:50 +08:00
薇薇安
464b6af410 账号不切换问题 2026-02-03 10:55:11 +08:00
薇薇安
9d78c227a4 优化全局服务状态展示,仪表板的账号服务控制 2026-02-03 10:32:50 +08:00
薇薇安
48f6ab4fea a 2026-02-03 10:13:18 +08:00
薇薇安
449ad01ede a 2026-02-03 09:49:25 +08:00
薇薇安
7b8bcd758d a 2026-02-03 09:48:37 +08:00
薇薇安
76e6e5efd0 a 2026-02-02 20:05:10 +08:00
薇薇安
d3f2cce922 a 2026-02-01 22:49:07 +08:00
薇薇安
8ff8cd4ebc a 2026-02-01 22:37:27 +08:00
薇薇安
18257b7d8a a 2026-02-01 22:36:52 +08:00
薇薇安
0a4bbd3132 a 2026-02-01 22:30:53 +08:00
薇薇安
ce3a4953f5 a 2026-02-01 22:19:59 +08:00
薇薇安
4da3e0bd48 a 2026-02-01 22:15:35 +08:00
薇薇安
c01f681dec a 2026-02-01 22:04:43 +08:00
薇薇安
0a0bcd941b a 2026-02-01 20:45:18 +08:00
薇薇安
cb8b393550 a 2026-02-01 12:35:56 +08:00
薇薇安
cf86c64296 a 2026-01-31 10:35:55 +08:00
薇薇安
380ce7cda9 a 2026-01-31 10:14:57 +08:00
薇薇安
aaca165f55 a 2026-01-31 10:12:09 +08:00
薇薇安
6e23c924b2 a 2026-01-31 09:57:11 +08:00
薇薇安
4f21240116 a 2026-01-30 11:03:30 +08:00
薇薇安
9490207537 a 2026-01-29 23:34:15 +08:00
薇薇安
53396adf26 a 2026-01-29 18:45:32 +08:00
薇薇安
f1a82f53e0 a 2026-01-29 09:00:41 +08:00
薇薇安
e328272701 a 2026-01-29 08:55:09 +08:00
薇薇安
15394445b4 a 2026-01-28 21:53:41 +08:00
薇薇安
8337893b0c a 2026-01-28 20:27:34 +08:00
薇薇安
8422e93aa2 a 2026-01-28 19:05:18 +08:00
薇薇安
461aeaf359 a 2026-01-28 17:37:04 +08:00
薇薇安
8eb2476192 a 2026-01-28 17:12:45 +08:00
薇薇安
3865e25a2b a 2026-01-28 10:13:30 +08:00
薇薇安
dfd899256b a 2026-01-27 23:04:42 +08:00
薇薇安
cf678569ee a 2026-01-27 22:40:23 +08:00
薇薇安
5faf3e103d a 2026-01-27 19:04:15 +08:00
薇薇安
fb04f69965 a 2026-01-27 16:19:23 +08:00
薇薇安
1d25b3cb79 a 2026-01-27 16:06:28 +08:00
薇薇安
4c5d040746 a 2026-01-27 16:03:10 +08:00
薇薇安
8667c07134 a 2026-01-27 15:56:59 +08:00
薇薇安
8e365b3a9a a 2026-01-27 11:28:57 +08:00
薇薇安
16c4cfbdd8 a 2026-01-27 11:11:03 +08:00
薇薇安
9fe028d704 a 2026-01-27 10:36:56 +08:00
薇薇安
d4edc16f43 a 2026-01-27 08:44:35 +08:00
薇薇安
88ed3bfab4 a 2026-01-27 08:32:29 +08:00
薇薇安
9e0f180229 a 2026-01-26 22:14:35 +08:00
薇薇安
042cb02563 a 2026-01-26 21:08:06 +08:00
薇薇安
3057ce0e8b a 2026-01-26 21:00:43 +08:00
薇薇安
1eb5c618eb a 2026-01-26 20:26:21 +08:00
薇薇安
51fd3c6550 a 2026-01-26 16:22:18 +08:00
薇薇安
be6459d5dd a 2026-01-26 16:08:18 +08:00
薇薇安
9448996837 a 2026-01-26 15:50:49 +08:00
薇薇安
ed994e6e8e a 2026-01-26 15:50:06 +08:00
薇薇安
7f736c9081 a 2026-01-26 14:25:59 +08:00
薇薇安
7947101bc4 a 2026-01-25 16:55:14 +08:00
薇薇安
c3a14f0f1a a 2026-01-25 16:53:40 +08:00
薇薇安
83e628b611 a 2026-01-25 16:32:08 +08:00
薇薇安
07d3bf4398 a 2026-01-25 16:31:37 +08:00
薇薇安
00751298bb a 2026-01-25 15:59:53 +08:00
薇薇安
4c640f780b a 2026-01-25 12:51:14 +08:00
薇薇安
86b85c2609 a 2026-01-25 11:19:39 +08:00
薇薇安
096b838769 a 2026-01-25 11:04:50 +08:00
薇薇安
10fd7a7d60 a 2026-01-25 10:59:34 +08:00
薇薇安
04f222875a a 2026-01-25 09:23:01 +08:00
薇薇安
1032295052 a 2026-01-25 09:16:16 +08:00
薇薇安
d9270ad6b4 a 2026-01-25 09:12:22 +08:00
薇薇安
762e9c4b38 a 2026-01-25 08:41:01 +08:00
薇薇安
731e71aae8 a 2026-01-24 19:08:55 +08:00
薇薇安
8d2fb4b9af a 2026-01-24 18:56:01 +08:00
薇薇安
f716ea69d5 a 2026-01-24 16:09:39 +08:00
薇薇安
81c73eb9cd a 2026-01-24 15:57:18 +08:00
薇薇安
f7c68efb3e a 2026-01-24 11:00:32 +08:00
薇薇安
b01920cadf a 2026-01-24 10:56:48 +08:00
薇薇安
f2d71d3390 a 2026-01-24 10:32:41 +08:00
薇薇安
27ddbcb8c1 a 2026-01-24 10:29:40 +08:00
薇薇安
6504efbf15 a 2026-01-23 23:46:29 +08:00
薇薇安
fb3d1a0dda a 2026-01-23 21:43:36 +08:00
薇薇安
14b5acae09 a 2026-01-23 21:29:31 +08:00
薇薇安
6341bacc20 a 2026-01-23 21:24:14 +08:00
薇薇安
aca1cf26b7 a 2026-01-23 21:07:35 +08:00
薇薇安
7847d3100b a 2026-01-23 20:54:37 +08:00
薇薇安
8d3991c74c a 2026-01-23 20:47:11 +08:00
薇薇安
211ef38ee9 a 2026-01-23 20:42:05 +08:00
薇薇安
fad8a1d6fd a 2026-01-23 20:35:11 +08:00
薇薇安
150eea7a28 a 2026-01-23 20:29:59 +08:00
薇薇安
e1c6cc2681 a 2026-01-23 20:24:06 +08:00
薇薇安
0c98bfe236 a 2026-01-23 20:12:32 +08:00
薇薇安
7adf5c7126 a 2026-01-23 20:03:07 +08:00
薇薇安
7a64ff44c2 a 2026-01-23 19:46:43 +08:00
薇薇安
2ee6e7a009 a 2026-01-23 19:41:44 +08:00
薇薇安
1fcd692368 a 2026-01-23 19:31:20 +08:00
薇薇安
cb7b091280 a 2026-01-23 19:21:37 +08:00
薇薇安
95abbf50be a 2026-01-23 19:08:56 +08:00
薇薇安
fe420ad1a4 a 2026-01-23 17:36:31 +08:00
薇薇安
efbd149f1f a 2026-01-23 17:31:47 +08:00
薇薇安
9e70f90260 a 2026-01-23 17:27:41 +08:00
薇薇安
f1bc8413df a 2026-01-23 17:20:55 +08:00
薇薇安
0c489bfdee a 2026-01-23 14:59:57 +08:00
薇薇安
f798782a6d a 2026-01-23 14:36:16 +08:00
薇薇安
f9ce156e9a a 2026-01-23 14:30:50 +08:00
薇薇安
63f1ea05f0 a 2026-01-23 13:53:36 +08:00
薇薇安
6aeed50d83 a 2026-01-23 13:49:46 +08:00
薇薇安
7a72cfb30c a 2026-01-23 13:24:01 +08:00
薇薇安
cdbb660c1d a 2026-01-23 09:36:39 +08:00
薇薇安
84c4af5ff5 a 2026-01-23 09:21:14 +08:00
薇薇安
2ba8d69ee0 a 2026-01-23 09:08:35 +08:00
薇薇安
ae953d119e a 2026-01-22 23:40:53 +08:00
薇薇安
3154dd5518 a 2026-01-22 23:35:35 +08:00
薇薇安
fc128f98b4 a 2026-01-22 23:26:29 +08:00
薇薇安
7ec1ae32d7 a 2026-01-22 23:03:32 +08:00
薇薇安
ba818b480d a 2026-01-22 22:37:44 +08:00
薇薇安
972694e98f a 2026-01-22 22:36:25 +08:00
薇薇安
76bde06a4f a 2026-01-22 22:32:49 +08:00
薇薇安
a7e1e6ca0a a 2026-01-22 22:15:33 +08:00
薇薇安
651e736474 a 2026-01-22 22:13:36 +08:00
薇薇安
e7efc5e8fa a 2026-01-22 22:08:32 +08:00
薇薇安
c581174747 a 2026-01-22 22:03:25 +08:00
薇薇安
1c1580b344 a 2026-01-22 21:49:42 +08:00
薇薇安
d051be3f65 a 2026-01-22 21:30:21 +08:00
薇薇安
d6cdc2f055 a 2026-01-22 21:14:53 +08:00
薇薇安
971d4e3a39 a 2026-01-22 20:59:21 +08:00
薇薇安
c9120294c9 a 2026-01-22 20:54:00 +08:00
薇薇安
18eeac233a a 2026-01-22 20:31:10 +08:00
薇薇安
d914b64294 a 2026-01-22 20:30:02 +08:00
薇薇安
078576ddf8 a 2026-01-22 20:27:29 +08:00
薇薇安
3e9d24ebea a 2026-01-22 20:24:28 +08:00
薇薇安
43d54bad97 a 2026-01-22 20:09:53 +08:00
薇薇安
5d7166d404 a 2026-01-22 19:52:46 +08:00
薇薇安
14773e530a a 2026-01-22 19:38:12 +08:00
薇薇安
717f51b015 a 2026-01-22 19:36:02 +08:00
薇薇安
5503860a04 a 2026-01-22 19:35:46 +08:00
薇薇安
d354aec939 a 2026-01-22 19:32:57 +08:00
薇薇安
8afd282ca5 a 2026-01-22 19:31:56 +08:00
薇薇安
e5a281569c a 2026-01-22 19:30:57 +08:00
薇薇安
b1f4cbddac a 2026-01-22 18:53:32 +08:00
薇薇安
3baa00c851 a 2026-01-22 18:52:57 +08:00
薇薇安
06272b6922 a 2026-01-22 18:51:26 +08:00
薇薇安
244ef6f4ba a 2026-01-22 18:49:35 +08:00
薇薇安
3e1e3392b7 a 2026-01-22 17:36:32 +08:00
薇薇安
9ed4d4259a a 2026-01-22 17:11:21 +08:00
薇薇安
5717614f61 a 2026-01-22 13:26:01 +08:00
薇薇安
0ecdff4530 a 2026-01-22 13:05:42 +08:00
薇薇安
7d3d9c7de1 a 2026-01-22 11:50:32 +08:00
薇薇安
fac2651911 a 2026-01-22 11:44:03 +08:00
薇薇安
3ef66fb54a a 2026-01-22 11:41:32 +08:00
薇薇安
490e94f7c7 a 2026-01-22 11:39:47 +08:00
薇薇安
9a62528c93 a 2026-01-22 11:35:38 +08:00
薇薇安
66d68e319f a 2026-01-22 11:32:53 +08:00
薇薇安
3bac042273 a 2026-01-22 11:24:57 +08:00
薇薇安
28bce8f02b a 2026-01-22 09:28:58 +08:00
薇薇安
352d36e7a5 a 2026-01-22 09:11:20 +08:00
薇薇安
156acc92e0 a 2026-01-22 09:06:10 +08:00
薇薇安
dc49c2717b a 2026-01-22 08:50:42 +08:00
薇薇安
5b1370a5a2 a 2026-01-21 23:44:37 +08:00
薇薇安
87e7865cbb a 2026-01-21 22:48:01 +08:00
薇薇安
414607d566 a 2026-01-21 22:38:24 +08:00
薇薇安
2d17406799 a 2026-01-21 22:13:52 +08:00
薇薇安
5a00dc75d7 a 2026-01-21 22:02:25 +08:00
薇薇安
45a654f654 a 2026-01-21 21:45:10 +08:00
薇薇安
e80fd1059b a 2026-01-21 21:26:44 +08:00
薇薇安
d75293e037 a 2026-01-21 19:37:22 +08:00
薇薇安
fe855df566 a 2026-01-21 19:29:10 +08:00
薇薇安
18cfeaf5db a 2026-01-21 19:06:49 +08:00
薇薇安
fe60f12ee0 a 2026-01-21 17:40:25 +08:00
薇薇安
3b0ff0227e a 2026-01-21 17:15:58 +08:00
薇薇安
8d8bc409a6 a 2026-01-21 17:11:13 +08:00
薇薇安
d7813bbc80 a 2026-01-21 14:48:33 +08:00
薇薇安
ed5ea8b733 a 2026-01-21 14:43:58 +08:00
薇薇安
52e849a586 a 2026-01-21 14:36:01 +08:00
薇薇安
2d9adddb91 a 2026-01-21 14:17:21 +08:00
薇薇安
9bc73b63a3 a 2026-01-21 13:19:10 +08:00
薇薇安
c28400e51e a 2026-01-21 13:09:10 +08:00
薇薇安
11e6361235 a 2026-01-21 13:01:44 +08:00
薇薇安
4d26777845 a 2026-01-21 11:48:41 +08:00
薇薇安
6d48dc98d2 a 2026-01-21 11:04:44 +08:00
薇薇安
1fdcb9c8b7 a 2026-01-20 22:17:09 +08:00
薇薇安
ad63dbd234 a 2026-01-20 21:06:17 +08:00
薇薇安
7fb0ed39a7 a 2026-01-20 20:59:49 +08:00
薇薇安
4c20a7a488 a 2026-01-20 19:03:19 +08:00
薇薇安
8832b83ced a 2026-01-20 18:16:28 +08:00
薇薇安
746c8ac25b 增加多账号的支持体系 2026-01-20 15:55:34 +08:00
299 changed files with 58046 additions and 3042 deletions

8
. cursorignore Normal file
View File

@ -0,0 +1,8 @@
node_modules
__pycache__
*.pyc
.venv
venv
.git
logs
*.log

18
.cursorrules Normal file
View File

@ -0,0 +1,18 @@
# 交易系统开发最高准则
## 1. 风险控制(核心)
- **止损高于一切**:严禁在任何平仓逻辑前添加时间限制。任何情况下,只要触发止损条件,必须立即执行平仓。
- **严禁恢复时间锁**:绝对不允许重新启用 `MIN_HOLD_TIME_SEC` 来限制止损或止盈。
- **异常处理**:所有涉及 `binance.create_order` 的操作必须包含 try-catch 逻辑,并有重试机制或错误预警。
## 2. 币安合约逻辑
- **挂单确认**:在开仓订单成交后,必须立即调用 `_ensure_exchange_sltp_orders` 在交易所侧挂好止损单。
- **价格类型**:区分 Mark Price标记价格和 Last Price最新价格止损逻辑应优先参考标记价格以防插针。
## 3. 代码风格
- 使用 Python 异步编程 (asyncio)。
- 所有的交易日志必须记录 Symbol、价格、原因和时间戳。
## 4. 不要在本地运行交易系统,后台服务,数据库连接
- 交易系统必须在服务器上运行,严禁在本地环境测试。
- 所有配置(如 API 密钥、数据库连接等)必须在服务器上配置,本地环境不得包含任何敏感信息。

103
backend/TROUBLESHOOTING.md Normal file
View File

@ -0,0 +1,103 @@
# Backend 启动问题排查指南
## 常见问题
### 1. 语法错误
**错误信息**: `SyntaxError: expected 'except' or 'finally' block`
**解决方法**:
- 检查代码中的 `try:` 块是否都有对应的 `except``finally`
- 检查缩进是否正确
- 运行 `python3 -m py_compile api/routes/trades.py` 检查语法
### 2. 缺少依赖模块
**错误信息**: `ModuleNotFoundError: No module named 'xxx'`
**解决方法**:
```bash
# 激活虚拟环境
source ../.venv/bin/activate # 或 source .venv/bin/activate
# 安装依赖
pip install -r requirements.txt
# 或者单独安装缺失的模块
pip install python-jose[cryptography]
```
### 3. 导入错误
**错误信息**: `ModuleNotFoundError: No module named 'api'`
**解决方法**:
- 确保在 `backend` 目录下运行
- 检查 `PYTHONPATH` 是否正确设置
- 使用 `cd backend && python3 -m uvicorn api.main:app` 启动
## 排查步骤
### 步骤 1: 检查依赖
```bash
cd backend
bash check_dependencies.sh
```
### 步骤 2: 检查语法
```bash
cd backend
source ../.venv/bin/activate
python3 -m py_compile api/main.py
python3 -m py_compile api/routes/*.py
```
### 步骤 3: 测试导入
```bash
cd backend
source ../.venv/bin/activate
python3 -c "import api.main; print('✓ 导入成功')"
```
### 步骤 4: 查看日志
```bash
# 查看最新的错误日志
tail -50 backend/logs/api.log
tail -50 backend/logs/uvicorn.log
```
### 步骤 5: 手动启动测试
```bash
cd backend
source ../.venv/bin/activate
export DB_HOST=your_db_host
export DB_PORT=3306
export DB_USER=your_db_user
export DB_PASSWORD=your_db_password
export DB_NAME=auto_trade_sys
uvicorn api.main:app --host 0.0.0.0 --port 8001 --log-level info
```
## 启动脚本
### 开发模式(自动重载)
```bash
cd backend
./start_dev.sh
```
### 生产模式(后台运行)
```bash
cd backend
./start.sh
```
## 检查服务状态
```bash
# 检查进程
ps aux | grep uvicorn
# 检查端口
lsof -i :8001
# 测试健康检查
curl http://localhost:8001/api/health
```

138
backend/api/auth_deps.py Normal file
View File

@ -0,0 +1,138 @@
"""
FastAPI 依赖解析 JWT获取当前用户校验 admin校验 account_id 访问权
"""
from __future__ import annotations
from fastapi import Header, HTTPException, Depends, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from typing import Optional, Dict, Any
import os
from api.auth_utils import jwt_decode
from database.models import User, UserAccountMembership
def _auth_enabled() -> bool:
v = (os.getenv("ATS_AUTH_ENABLED") or "true").strip().lower()
return v not in {"0", "false", "no"}
_bearer_scheme = HTTPBearer(auto_error=False)
def get_current_user(credentials: Optional[HTTPAuthorizationCredentials] = Security(_bearer_scheme)) -> Dict[str, Any]:
if not _auth_enabled():
# 未启用登录:视为超级管理员(兼容开发/灰度)
return {"id": 0, "username": "dev", "role": "admin", "status": "active"}
if not credentials:
raise HTTPException(status_code=401, detail="未登录")
if (credentials.scheme or "").lower() != "bearer":
raise HTTPException(status_code=401, detail="未登录")
token = (credentials.credentials or "").strip()
if not token:
raise HTTPException(status_code=401, detail="未登录")
try:
payload = jwt_decode(token)
except Exception:
raise HTTPException(status_code=401, detail="登录已失效")
sub = payload.get("sub")
try:
uid = int(sub)
except Exception:
raise HTTPException(status_code=401, detail="登录已失效")
u = User.get_by_id(uid)
if not u:
raise HTTPException(status_code=401, detail="登录已失效")
if (u.get("status") or "active") != "active":
raise HTTPException(status_code=403, detail="用户已被禁用")
return {"id": int(u["id"]), "username": u.get("username") or "", "role": u.get("role") or "user", "status": u.get("status") or "active"}
def require_admin(user: Dict[str, Any]) -> Dict[str, Any]:
if (user.get("role") or "user") != "admin":
raise HTTPException(status_code=403, detail="需要管理员权限")
return user
def require_account_access(account_id: int, user: Dict[str, Any]) -> int:
aid = int(account_id or 1)
if (user.get("role") or "user") == "admin":
return aid
if UserAccountMembership.has_access(int(user["id"]), aid):
return aid
raise HTTPException(status_code=403, detail="无权访问该账号")
def require_account_owner(account_id: int, user: Dict[str, Any]) -> int:
"""
账号拥有者权限用于启停交易进程等高危操作
"""
aid = int(account_id or 1)
if (user.get("role") or "user") == "admin":
return aid
role = UserAccountMembership.get_role(int(user["id"]), aid)
if role == "owner":
return aid
raise HTTPException(status_code=403, detail="需要该账号 owner 权限")
def get_admin_user(user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
return require_admin(user)
def get_account_id(
x_account_id: Optional[int] = Header(None, alias="X-Account-Id"),
user: Dict[str, Any] = Depends(get_current_user),
) -> int:
import logging
logger = logging.getLogger(__name__)
# 1. 如果 header 存在,直接校验
if x_account_id is not None:
aid = int(x_account_id)
return require_account_access(aid, user)
# 2. 如果 header 不存在
# 如果是 admin默认访问 1
if (user.get("role") or "user") == "admin":
return require_account_access(1, user)
# 如果是普通用户,尝试查找他拥有的第一个账号
try:
# 查找用户关联的账号
accounts = UserAccountMembership.get_user_accounts(int(user["id"]))
if accounts and len(accounts) > 0:
first_aid = int(accounts[0]["id"])
logger.info(f"get_account_id: No header provided, auto-selected account_id={first_aid} for user {user['id']}")
return first_aid
except Exception as e:
logger.error(f"get_account_id: Failed to auto-select account for user {user['id']}: {e}")
# 兜底:仍然尝试 1然后会由 require_account_access 抛出 403
logger.warning(f"get_account_id: No header provided and no accounts found for user {user['id']}, defaulting to 1")
return require_account_access(1, user)
def require_system_admin(
x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token"),
user: Dict[str, Any] = Depends(get_admin_user),
) -> Dict[str, Any]:
"""
/api/system/* 管理员保护
- 启用登录(ATS_AUTH_ENABLED=true)要求 JWT admin
- 未启用登录兼容旧逻辑若配置了 SYSTEM_CONTROL_TOKEN则要求 X-Admin-Token
"""
if _auth_enabled():
return user
token = (os.getenv("SYSTEM_CONTROL_TOKEN") or "").strip()
if not token:
return user
if not x_admin_token or x_admin_token != token:
raise HTTPException(status_code=401, detail="Unauthorized")
return user

75
backend/api/auth_utils.py Normal file
View File

@ -0,0 +1,75 @@
"""
登录鉴权工具JWT + 密码哈希
设计目标
- 最小依赖密码哈希用 pbkdf2_hmac标准库
- JWT 使用 python-jose已加入 requirements
"""
from __future__ import annotations
import base64
import hashlib
import hmac
import os
import time
from typing import Any, Dict, Optional
from jose import jwt # type: ignore
def _jwt_secret() -> str:
s = (os.getenv("ATS_JWT_SECRET") or os.getenv("JWT_SECRET") or "").strip()
if s:
return s
# 允许开发环境兜底,但线上务必配置
return "dev-secret-change-me"
def jwt_encode(payload: Dict[str, Any], exp_sec: int = 3600) -> str:
now = int(time.time())
body = dict(payload or {})
body["iat"] = now
body["exp"] = now + int(exp_sec)
return jwt.encode(body, _jwt_secret(), algorithm="HS256")
def jwt_decode(token: str) -> Dict[str, Any]:
return jwt.decode(token, _jwt_secret(), algorithms=["HS256"])
def _b64(b: bytes) -> str:
return base64.urlsafe_b64encode(b).decode("utf-8").rstrip("=")
def _b64d(s: str) -> bytes:
s = (s or "").strip()
s = s + ("=" * (-len(s) % 4))
return base64.urlsafe_b64decode(s.encode("utf-8"))
def hash_password(password: str, iterations: int = 260_000) -> str:
"""
PBKDF2-SHA256返回格式
pbkdf2_sha256$<iterations>$<salt_b64>$<hash_b64>
"""
pw = (password or "").encode("utf-8")
salt = os.urandom(16)
dk = hashlib.pbkdf2_hmac("sha256", pw, salt, int(iterations))
return f"pbkdf2_sha256${int(iterations)}${_b64(salt)}${_b64(dk)}"
def verify_password(password: str, password_hash: str) -> bool:
try:
s = str(password_hash or "")
if not s.startswith("pbkdf2_sha256$"):
return False
_, it_s, salt_b64, dk_b64 = s.split("$", 3)
it = int(it_s)
salt = _b64d(salt_b64)
dk0 = _b64d(dk_b64)
dk1 = hashlib.pbkdf2_hmac("sha256", (password or "").encode("utf-8"), salt, it)
return hmac.compare_digest(dk0, dk1)
except Exception:
return False

View File

@ -3,8 +3,9 @@ FastAPI应用主入口
"""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from api.routes import config, trades, stats, dashboard, account, recommendations, system
from api.routes import config, trades, stats, dashboard, account, recommendations, system, accounts, auth, admin, public, data_management
import os
import sys
import logging
from pathlib import Path
from logging.handlers import RotatingFileHandler
@ -141,12 +142,12 @@ logger.info(f"日志级别: {os.getenv('LOG_LEVEL', 'INFO')}")
# 检查 redis-py 是否可用redis-py 4.2+ 同时支持同步和异步可替代aioredis
try:
import redis
import redis # type: ignore
# 检查是否是 redis-py 4.2+(支持异步)
if hasattr(redis, 'asyncio'):
logger.info(f"✓ redis-py 已安装 (版本: {redis.__version__ if hasattr(redis, '__version__') else '未知'}),支持同步和异步客户端")
logger.info(f" - redis.Redis: 同步客户端用于config_manager")
logger.info(f" - redis.asyncio.Redis: 异步客户端用于trading_system可替代aioredis")
logger.info(" - redis.Redis: 同步客户端用于config_manager")
logger.info(" - redis.asyncio.Redis: 异步客户端用于trading_system可替代aioredis")
else:
logger.warning("⚠ redis-py 版本可能过低,建议升级到 4.2+ 以获得异步支持")
except ImportError as e:
@ -154,9 +155,9 @@ except ImportError as e:
logger.warning("⚠ redis-py 未安装Redis/Valkey 缓存将不可用")
logger.warning(f" Python 路径: {sys.executable}")
logger.warning(f" 导入错误: {e}")
logger.warning(f" 提示: 请运行 'pip install redis>=4.2.0' 安装 redis-py")
logger.warning(f" 注意: redis-py 4.2+ 同时支持同步和异步,无需安装 aioredis")
logger.warning(f" 或者运行 'pip install -r backend/requirements.txt' 安装所有依赖")
logger.warning(" 提示: 请运行 'pip install redis>=4.2.0' 安装 redis-py")
logger.warning(" 注意: redis-py 4.2+ 同时支持同步和异步,无需安装 aioredis")
logger.warning(" 或者运行 'pip install -r backend/requirements.txt' 安装所有依赖")
app = FastAPI(
title="Auto Trade System API",
@ -165,9 +166,79 @@ app = FastAPI(
redirect_slashes=False # 禁用自动重定向避免307重定向问题
)
# 现货推荐定时扫描间隔(秒),默认 15 分钟;设为 0 关闭定时扫描
SPOT_SCAN_INTERVAL_SEC = int(os.getenv("SPOT_SCAN_INTERVAL_SEC", "900"))
async def _spot_scan_loop():
"""后台循环:每隔 SPOT_SCAN_INTERVAL_SEC 执行一次现货扫描并写入 Redis。"""
if SPOT_SCAN_INTERVAL_SEC <= 0:
logger.info("现货推荐定时扫描已关闭SPOT_SCAN_INTERVAL_SEC=0")
return
import asyncio
backend_dir = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(backend_dir))
try:
from spot_scanner import run_spot_scan_and_cache
except Exception as e:
logger.warning("现货扫描模块加载失败,跳过定时任务: %s", e)
return
logger.info("现货推荐定时扫描已启动,间隔 %d", SPOT_SCAN_INTERVAL_SEC)
while True:
try:
await run_spot_scan_and_cache(ttl_sec=900)
except Exception as e:
logger.warning("现货扫描执行失败: %s", e)
await asyncio.sleep(SPOT_SCAN_INTERVAL_SEC)
# 启动时:确保存在一个初始管理员(通过环境变量配置)
@app.on_event("startup")
async def _ensure_initial_admin():
try:
import os
from database.models import User, UserAccountMembership
from api.auth_utils import hash_password
username = (os.getenv("ATS_ADMIN_USERNAME") or "admin").strip()
password = (os.getenv("ATS_ADMIN_PASSWORD") or "").strip()
if not password:
# 不强制创建,避免你忘记改默认密码导致安全风险
# 你可以设置 ATS_ADMIN_PASSWORD 后重启后端自动创建
logger.warning("未设置 ATS_ADMIN_PASSWORD跳过自动创建初始管理员")
return
u = User.get_by_username(username)
if not u:
uid = User.create(username=username, password_hash=hash_password(password), role="admin", status="active")
# 默认给管理员绑定 account_id=1default
try:
UserAccountMembership.add(int(uid), 1, role="owner")
except Exception:
pass
logger.info(f"✓ 已创建初始管理员用户: {username} (id={uid})")
else:
# 若已存在但不是 admin则提升为 admin可注释掉更保守
if (u.get("role") or "user") != "admin":
try:
User.set_role(int(u["id"]), "admin")
logger.warning(f"已将用户 {username} 提升为 admin")
except Exception:
pass
except Exception as e:
logger.warning(f"初始化管理员失败(可忽略): {e}")
# 启动现货推荐定时扫描(后台任务)
try:
import asyncio
asyncio.create_task(_spot_scan_loop())
except Exception as e:
logger.warning("启动现货扫描定时任务失败(可忽略): %s", e)
# CORS配置允许React前端访问
# 默认包含:本地开发端口、主前端域名、推荐查看器域名
cors_origins_str = os.getenv('CORS_ORIGINS', 'http://localhost:3000,http://localhost:3001,http://localhost:5173,http://as.deepx1.com,http://asapi.deepx1.com,http://r.deepx1.com,https://r.deepx1.com')
cors_origins_str = os.getenv('CORS_ORIGINS', 'http://localhost:3000,http://localhost:3001,http://localhost:5173,http://as.deepx1.com,http://asapi.deepx1.com,http://r.deepx1.com,https://r.deepx1.com,http://asapi-new.deepx1.com')
cors_origins = [origin.strip() for origin in cors_origins_str.split(',') if origin.strip()]
logger.info(f"CORS允许的源: {cors_origins}")
@ -183,12 +254,17 @@ app.add_middleware(
# 注册路由
app.include_router(config.router, prefix="/api/config", tags=["配置管理"])
app.include_router(auth.router, tags=["auth"])
app.include_router(admin.router)
app.include_router(accounts.router, prefix="/api/accounts", tags=["账号管理"])
app.include_router(trades.router, prefix="/api/trades", tags=["交易记录"])
app.include_router(stats.router, prefix="/api/stats", tags=["统计分析"])
app.include_router(dashboard.router, prefix="/api/dashboard", tags=["仪表板"])
app.include_router(account.router, prefix="/api/account", tags=["账户数据"])
app.include_router(recommendations.router, tags=["交易推荐"])
app.include_router(system.router, tags=["系统控制"])
app.include_router(data_management.router)
app.include_router(public.router)
@app.get("/")

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,301 @@
"""
账号管理 API多账号
说明
- 这是多账号第一步的管理入口创建/禁用/更新密钥
- 交易/配置/统计接口通过 X-Account-Id 头来选择账号默认 1
"""
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel, Field
from typing import Optional, List, Dict, Any
import logging
from database.models import Account, UserAccountMembership
from api.auth_deps import get_current_user, get_admin_user, require_account_access, require_account_owner
from api.supervisor_account import (
ensure_account_program,
run_supervisorctl,
parse_supervisor_status,
program_name_for_account,
tail_supervisor,
tail_supervisord_log,
tail_trading_log_files,
)
logger = logging.getLogger(__name__)
router = APIRouter()
class AccountCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
api_key: Optional[str] = ""
api_secret: Optional[str] = ""
use_testnet: bool = False
status: str = Field("active", pattern="^(active|disabled)$")
class AccountUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
status: Optional[str] = Field(None, pattern="^(active|disabled)$")
use_testnet: Optional[bool] = None
class AccountCredentialsUpdate(BaseModel):
api_key: Optional[str] = None
api_secret: Optional[str] = None
use_testnet: Optional[bool] = None
@router.get("")
async def list_my_accounts(user: Dict[str, Any] = Depends(get_current_user)):
"""列出我有权访问的账号"""
try:
if user.get("role") == "admin":
accounts = Account.list_all()
else:
accounts = UserAccountMembership.get_user_accounts(user["id"])
# 补充一些运行时信息(可选),并处理敏感字段
for acc in accounts:
acc['has_api_key'] = bool(acc.get('api_key_enc'))
acc['has_api_secret'] = bool(acc.get('api_secret_enc'))
# 移除加密字段,不直接暴露给前端
acc.pop('api_key_enc', None)
acc.pop('api_secret_enc', None)
return accounts
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("")
async def create_account(
data: AccountCreate,
user: Dict[str, Any] = Depends(get_current_user)
):
"""创建新账号(仅管理员或允许的用户)"""
# 暂时只允许 admin 创建
if user.get("role") != "admin":
raise HTTPException(status_code=403, detail="Only admin can create accounts")
try:
aid = Account.create(
name=data.name,
api_key=data.api_key,
api_secret=data.api_secret,
use_testnet=data.use_testnet,
status=data.status
)
# 自动将创建者关联为 owner
UserAccountMembership.add_membership(user["id"], aid, "owner")
return {"id": aid, "message": "Account created successfully"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{account_id}")
async def get_account_detail(
account_id: int,
user: Dict[str, Any] = Depends(get_current_user)
):
"""获取账号详情"""
require_account_access(account_id, user)
try:
acc = Account.get_by_id(account_id)
if not acc:
raise HTTPException(status_code=404, detail="Account not found")
return acc
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{account_id}")
async def update_account(
account_id: int,
data: AccountUpdate,
user: Dict[str, Any] = Depends(get_current_user)
):
"""更新账号基本信息"""
require_account_owner(account_id, user)
try:
updates = {}
if data.name is not None:
updates['name'] = data.name
if data.status is not None:
updates['status'] = data.status
if data.use_testnet is not None:
updates['testnet'] = 1 if data.use_testnet else 0
if updates:
Account.update(account_id, **updates)
return {"message": "Account updated"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{account_id}/credentials")
async def update_credentials(
account_id: int,
data: AccountCredentialsUpdate,
user: Dict[str, Any] = Depends(get_current_user)
):
"""更新API密钥"""
require_account_owner(account_id, user)
try:
updates = {}
if data.api_key is not None:
updates['api_key'] = data.api_key
if data.api_secret is not None:
updates['api_secret'] = data.api_secret
if data.use_testnet is not None:
updates['testnet'] = 1 if data.use_testnet else 0
if updates:
Account.update(account_id, **updates)
return {"message": "Credentials updated"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# --- Service Management ---
@router.get("/{account_id}/trading/status")
@router.get("/{account_id}/service/status", include_in_schema=False) # 兼容旧路由
async def get_service_status(
account_id: int,
user: Dict[str, Any] = Depends(get_current_user)
):
"""获取该账号关联的交易服务状态"""
# 手动调用权限检查,因为 Depends(require_account_access) 无法直接获取路径参数 account_id
require_account_access(account_id, user)
try:
program = program_name_for_account(account_id)
# status <program>
try:
out = run_supervisorctl(["status", program])
running, pid, state = parse_supervisor_status(out)
return {
"program": program,
"running": running,
"pid": pid,
"state": state,
"raw": out
}
except RuntimeError as e:
# 可能进程不存在
return {
"program": program,
"running": False,
"pid": None,
"state": "UNKNOWN",
"raw": str(e),
"error": "Process likely not configured or supervisor error"
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{account_id}/trading/start")
@router.post("/{account_id}/service/start", include_in_schema=False)
async def start_service(
account_id: int,
user: Dict[str, Any] = Depends(get_current_user)
):
"""启动交易服务(需该账号 owner 或管理员)"""
require_account_owner(account_id, user)
try:
program = program_name_for_account(account_id)
out = run_supervisorctl(["start", program])
# Check status again
status_out = run_supervisorctl(["status", program])
running, pid, state = parse_supervisor_status(status_out)
return {
"message": "Service start command sent",
"output": out,
"status": {
"running": running,
"pid": pid,
"state": state
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{account_id}/trading/stop")
@router.post("/{account_id}/service/stop", include_in_schema=False)
async def stop_service(
account_id: int,
user: Dict[str, Any] = Depends(get_current_user)
):
"""停止交易服务(需该账号 owner 或管理员)"""
require_account_owner(account_id, user)
try:
program = program_name_for_account(account_id)
out = run_supervisorctl(["stop", program])
# Check status again
status_out = run_supervisorctl(["status", program])
running, pid, state = parse_supervisor_status(status_out)
return {
"message": "Service stop command sent",
"output": out,
"status": {
"running": running,
"pid": pid,
"state": state
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{account_id}/trading/restart")
@router.post("/{account_id}/service/restart", include_in_schema=False)
async def restart_service(
account_id: int,
user: Dict[str, Any] = Depends(get_current_user)
):
"""重启交易服务(需该账号 owner 或管理员)"""
require_account_owner(account_id, user)
try:
program = program_name_for_account(account_id)
out = run_supervisorctl(["restart", program])
# Check status again
status_out = run_supervisorctl(["status", program])
running, pid, state = parse_supervisor_status(status_out)
return {
"message": "Service restart command sent",
"output": out,
"status": {
"running": running,
"pid": pid,
"state": state
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{account_id}/trading/ensure-program")
async def ensure_trading_program(account_id: int, user: Dict[str, Any] = Depends(get_current_user)):
if int(account_id) <= 0:
raise HTTPException(status_code=400, detail="account_id 必须 >= 1")
require_account_owner(int(account_id), user)
sup = ensure_account_program(int(account_id))
if not sup.ok:
raise HTTPException(status_code=500, detail=sup.error or "生成 supervisor 配置失败")
return {
"ok": True,
"program": sup.program,
"ini_path": sup.ini_path,
"program_dir": sup.program_dir,
"supervisor_conf": sup.supervisor_conf,
"reread": sup.reread,
"update": sup.update,
}

168
backend/api/routes/admin.py Normal file
View File

@ -0,0 +1,168 @@
"""
管理员接口用户管理 / 授权管理
"""
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel, Field
from typing import Optional, List, Dict, Any
from api.auth_deps import get_admin_user
from api.auth_utils import hash_password
from database.models import User, UserAccountMembership, Account
router = APIRouter(prefix="/api/admin", tags=["admin"])
class UserCreateReq(BaseModel):
username: str = Field(..., min_length=1, max_length=64)
password: str = Field(..., min_length=1, max_length=200)
role: str = Field("user", pattern="^(admin|user)$")
status: str = Field("active", pattern="^(active|disabled)$")
@router.get("/users")
async def list_users(_admin: Dict[str, Any] = Depends(get_admin_user)):
return User.list_all()
@router.get("/users/detailed")
async def list_users_with_accounts(_admin: Dict[str, Any] = Depends(get_admin_user)):
"""获取所有用户及其关联账号列表"""
users = User.list_all()
out = []
# 获取所有授权关系
# 优化:一次性查询所有 memberships 并在内存中分组,避免 N+1 查询
# 但由于 UserAccountMembership 没有 list_all 方法,暂时循环查询或添加 list_all
# 考虑到用户量不大,循环查询尚可接受。
for u in users:
uid = u['id']
memberships = UserAccountMembership.get_user_accounts(uid)
user_accounts = []
for m in memberships or []:
user_accounts.append({
"id": m.get("id"),
"name": m.get("name"),
"status": m.get("status"),
"role": m.get("role"),
"has_api_key": bool(m.get("api_key_enc")),
"has_api_secret": bool(m.get("api_secret_enc"))
})
out.append({
"id": uid,
"username": u['username'],
"role": u['role'],
"status": u['status'],
"accounts": user_accounts
})
return out
@router.post("/users")
async def create_user(payload: UserCreateReq, _admin: Dict[str, Any] = Depends(get_admin_user)):
exists = User.get_by_username(payload.username)
if exists:
raise HTTPException(status_code=400, detail="用户名已存在")
uid = User.create(
username=payload.username,
password_hash=hash_password(payload.password),
role=payload.role,
status=payload.status,
)
return {"success": True, "id": int(uid)}
class UserPasswordReq(BaseModel):
password: str = Field(..., min_length=1, max_length=200)
@router.put("/users/{user_id}/password")
async def set_user_password(user_id: int, payload: UserPasswordReq, _admin: Dict[str, Any] = Depends(get_admin_user)):
u = User.get_by_id(int(user_id))
if not u:
raise HTTPException(status_code=404, detail="用户不存在")
User.set_password(int(user_id), hash_password(payload.password))
return {"success": True}
class UserRoleReq(BaseModel):
role: str = Field(..., pattern="^(admin|user)$")
@router.put("/users/{user_id}/role")
async def set_user_role(user_id: int, payload: UserRoleReq, _admin: Dict[str, Any] = Depends(get_admin_user)):
u = User.get_by_id(int(user_id))
if not u:
raise HTTPException(status_code=404, detail="用户不存在")
User.set_role(int(user_id), payload.role)
return {"success": True}
class UserStatusReq(BaseModel):
status: str = Field(..., pattern="^(active|disabled)$")
@router.put("/users/{user_id}/status")
async def set_user_status(user_id: int, payload: UserStatusReq, _admin: Dict[str, Any] = Depends(get_admin_user)):
u = User.get_by_id(int(user_id))
if not u:
raise HTTPException(status_code=404, detail="用户不存在")
User.set_status(int(user_id), payload.status)
return {"success": True}
@router.get("/users/{user_id}/accounts")
async def list_user_accounts(user_id: int, _admin: Dict[str, Any] = Depends(get_admin_user)):
u = User.get_by_id(int(user_id))
if not u:
raise HTTPException(status_code=404, detail="用户不存在")
memberships = UserAccountMembership.list_for_user(int(user_id))
# 追加账号名称(便于前端展示)
out = []
for m in memberships or []:
aid = int(m.get("account_id"))
a = Account.get(aid) or {}
out.append(
{
"user_id": int(m.get("user_id")),
"account_id": aid,
"role": m.get("role") or "viewer",
"account_name": a.get("name") or "",
"account_status": a.get("status") or "",
}
)
return out
class GrantReq(BaseModel):
role: str = Field("viewer", pattern="^(owner|viewer)$")
@router.put("/users/{user_id}/accounts/{account_id}")
async def grant_user_account(user_id: int, account_id: int, payload: GrantReq, _admin: Dict[str, Any] = Depends(get_admin_user)):
u = User.get_by_id(int(user_id))
if not u:
raise HTTPException(status_code=404, detail="用户不存在")
a = Account.get(int(account_id))
if not a:
raise HTTPException(status_code=404, detail="账号不存在")
try:
if payload.role == "owner":
UserAccountMembership.clear_other_owners_for_account(int(account_id), int(user_id))
UserAccountMembership.add(int(user_id), int(account_id), role=payload.role)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"关联账号失败: {str(e)}",
)
return {"success": True}
@router.delete("/users/{user_id}/accounts/{account_id}")
async def revoke_user_account(user_id: int, account_id: int, _admin: Dict[str, Any] = Depends(get_admin_user)):
UserAccountMembership.remove(int(user_id), int(account_id))
return {"success": True}

View File

@ -0,0 +1,71 @@
"""
登录鉴权 APIJWT
"""
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any
import os
from database.models import User
from api.auth_utils import verify_password, jwt_encode
from api.auth_deps import get_current_user
router = APIRouter(prefix="/api/auth", tags=["auth"])
class LoginReq(BaseModel):
username: str = Field(..., min_length=1, max_length=64)
password: str = Field(..., min_length=1, max_length=200)
class LoginResp(BaseModel):
access_token: str
token_type: str = "bearer"
user: Dict[str, Any]
def _auth_enabled() -> bool:
v = (os.getenv("ATS_AUTH_ENABLED") or "true").strip().lower()
return v not in {"0", "false", "no"}
@router.post("/login", response_model=LoginResp)
async def login(payload: LoginReq):
if not _auth_enabled():
raise HTTPException(status_code=400, detail="当前环境未启用登录ATS_AUTH_ENABLED=false")
u = User.get_by_username(payload.username)
if not u:
raise HTTPException(status_code=401, detail="用户名或密码错误")
if (u.get("status") or "active") != "active":
raise HTTPException(status_code=403, detail="用户已被禁用")
if not verify_password(payload.password, u.get("password_hash") or ""):
raise HTTPException(status_code=401, detail="用户名或密码错误")
token = jwt_encode({"sub": str(u["id"]), "role": u.get("role") or "user"}, exp_sec=24 * 3600)
return {
"access_token": token,
"token_type": "bearer",
"user": {"id": u["id"], "username": u["username"], "role": u.get("role") or "user", "status": u.get("status") or "active"},
}
class MeResp(BaseModel):
id: int
username: str
role: str
status: str
@router.get("/me", response_model=MeResp)
async def me(user: Dict[str, Any] = Depends(get_current_user)):
return {
"id": int(user["id"]),
"username": user.get("username") or "",
"role": user.get("role") or "user",
"status": user.get("status") or "active",
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,374 @@
"""
数据管理查询 DB 交易从币安拉取订单/成交供策略分析与导出
仅管理员可用
"""
import asyncio
from pathlib import Path
from fastapi import APIRouter, Query, Depends, HTTPException
from typing import Optional
from api.auth_deps import get_admin_user
from database.models import Trade, Account
from datetime import datetime, timezone, timedelta
router = APIRouter(prefix="/api/admin/data", tags=["数据管理"])
BEIJING_TZ = timezone(timedelta(hours=8))
def _get_timestamp_range(period: Optional[str], start_date: Optional[str], end_date: Optional[str]):
now = datetime.now(BEIJING_TZ)
end_ts = int(now.timestamp())
start_ts = None
if period:
if period == "today":
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
start_ts = int(today.timestamp())
elif period == "1d":
start_ts = end_ts - 24 * 3600
elif period == "7d":
start_ts = end_ts - 7 * 24 * 3600
elif period == "30d":
start_ts = end_ts - 30 * 24 * 3600
elif period == "week":
days = now.weekday()
week_start = (now - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
start_ts = int(week_start.timestamp())
elif period == "month":
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
start_ts = int(month_start.timestamp())
if start_date:
try:
s = start_date if len(start_date) > 10 else f"{start_date} 00:00:00"
dt = datetime.strptime(s, "%Y-%m-%d %H:%M:%S").replace(tzinfo=BEIJING_TZ)
start_ts = int(dt.timestamp())
except ValueError:
pass
if end_date:
try:
s = end_date if len(end_date) > 10 else f"{end_date} 23:59:59"
dt = datetime.strptime(s, "%Y-%m-%d %H:%M:%S").replace(tzinfo=BEIJING_TZ)
end_ts = int(dt.timestamp())
except ValueError:
pass
if start_ts is None:
start_ts = end_ts - 7 * 24 * 3600 # 默认 7 天
return start_ts, end_ts
def _compute_binance_stats(data: list, data_type: str) -> dict:
"""计算用于策略分析的统计数据(成交/订单原始字段均已保留,导出 JSON 含全部)"""
stats = {"count": len(data)}
valid = [r for r in data if isinstance(r, dict) and "_error" not in r]
if not valid:
return stats
if data_type == "trades":
pnls = []
commissions = []
quote_qtys = []
by_symbol = {}
wins, losses = 0, 0
maker_count, taker_count = 0, 0
for r in valid:
sym = r.get("_symbol") or r.get("symbol") or "-"
p = float(r.get("realizedPnl") or 0)
c = float(r.get("commission") or 0)
qq = float(r.get("quoteQty") or 0)
pnls.append(p)
commissions.append(c)
if qq:
quote_qtys.append(qq)
if p > 0:
wins += 1
elif p < 0:
losses += 1
if r.get("maker"):
maker_count += 1
else:
taker_count += 1
by_symbol[sym] = by_symbol.get(sym, {"count": 0, "pnl": 0.0, "commission": 0.0, "quoteQty": 0.0})
by_symbol[sym]["count"] += 1
by_symbol[sym]["pnl"] += p
by_symbol[sym]["commission"] += c
by_symbol[sym]["quoteQty"] += qq
stats["total_realized_pnl"] = round(sum(pnls), 4)
stats["total_commission"] = round(sum(commissions), 4)
stats["net_pnl"] = round(stats["total_realized_pnl"] - stats["total_commission"], 4)
stats["win_count"] = wins
stats["loss_count"] = losses
stats["win_rate"] = round(100 * wins / (wins + losses), 1) if (wins + losses) > 0 else 0
stats["avg_pnl_per_trade"] = round(sum(pnls) / len(pnls), 4) if pnls else 0
stats["total_quote_qty"] = round(sum(quote_qtys), 2)
stats["maker_count"] = maker_count
stats["taker_count"] = taker_count
stats["by_symbol"] = {
k: {
"count": v["count"],
"pnl": round(v["pnl"], 4),
"commission": round(v["commission"], 4),
"quoteQty": round(v["quoteQty"], 2),
}
for k, v in sorted(by_symbol.items())
}
by_hour = {}
by_weekday = {}
weekday_names = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
for r in valid:
t = r.get("time") or r.get("trade_time") or 0
if t:
dt = datetime.fromtimestamp(t / 1000, tz=BEIJING_TZ)
h = dt.hour
wd = dt.weekday()
by_hour[h] = by_hour.get(h, {"count": 0, "pnl": 0.0})
by_hour[h]["count"] += 1
by_hour[h]["pnl"] += float(r.get("realizedPnl") or 0)
by_weekday[wd] = by_weekday.get(wd, {"count": 0, "pnl": 0.0})
by_weekday[wd]["count"] += 1
by_weekday[wd]["pnl"] += float(r.get("realizedPnl") or 0)
stats["by_hour"] = {str(k): {"count": v["count"], "pnl": round(v["pnl"], 4)} for k, v in sorted(by_hour.items())}
stats["by_weekday"] = {weekday_names[k]: {"count": v["count"], "pnl": round(v["pnl"], 4)} for k, v in sorted(by_weekday.items())}
else:
by_status = {}
by_type = {}
by_symbol = {}
filled_count = 0
for r in valid:
status = r.get("status") or "UNKNOWN"
typ = r.get("type") or r.get("origType") or "UNKNOWN"
sym = r.get("_symbol") or r.get("symbol") or "-"
by_status[status] = by_status.get(status, 0) + 1
by_type[typ] = by_type.get(typ, 0) + 1
by_symbol[sym] = by_symbol.get(sym, 0) + 1
if status == "FILLED":
filled_count += 1
stats["by_status"] = by_status
stats["by_type"] = by_type
stats["by_symbol"] = dict(sorted(by_symbol.items()))
stats["filled_count"] = filled_count
return stats
async def _get_active_symbols_from_income(binance_client, start_ms: int, end_ms: int) -> list:
"""
通过收益历史 API 获取该时间段内有交易活动的交易对避免全量遍历 250+ 交易对
一次 API 调用weight 100即可拿到有成交/盈亏的 symbol 列表大幅减少后续 trades/orders 的请求数
"""
try:
symbols = set()
current_end = end_ms
for _ in range(10): # 最多分页 10 次(单次最多 1000 条)
rows = await binance_client.futures_income_history(
startTime=start_ms,
endTime=current_end,
limit=1000,
recvWindow=20000,
)
if not rows:
break
for r in rows:
sym = (r.get("symbol") or "").strip()
if sym and sym.endswith("USDT"):
symbols.add(sym)
if len(rows) < 1000:
break
oldest = min(r.get("time", current_end) for r in rows)
current_end = oldest - 1
if current_end < start_ms:
break
await asyncio.sleep(0.15)
return sorted(symbols)
except Exception:
return []
@router.get("/accounts")
async def list_accounts(_admin=Depends(get_admin_user), active_only: bool = Query(False)):
"""获取账号列表供数据管理选择。active_only=true 时仅返回 status=active 的账号"""
rows = Account.list_all()
accounts = [{"id": r["id"], "name": r.get("name") or f"Account {r['id']}", "status": r.get("status") or "active"} for r in (rows or [])]
if active_only:
accounts = [a for a in accounts if (a.get("status") or "").lower() == "active"]
return {"accounts": accounts}
@router.get("/trades")
async def query_db_trades(
_admin=Depends(get_admin_user),
account_id: int = Query(..., ge=1, description="账号 ID"),
period: Optional[str] = Query(None, description="today/1d/7d/30d/week/month"),
date: Optional[str] = Query(None, description="YYYY-MM-DD指定日期等同于 start_date=end_date"),
start_date: Optional[str] = Query(None),
end_date: Optional[str] = Query(None),
symbol: Optional[str] = Query(None),
time_filter: str = Query("created", description="created/entry/exit"),
reconciled_only: Optional[str] = Query(None),
limit: int = Query(500, ge=1, le=2000),
):
"""
查询 DB 交易记录管理员可指定任意账号
"""
sd, ed = start_date, end_date
if date:
sd, ed = date, date
_reconciled = str(reconciled_only or "").lower() in ("true", "1", "yes")
start_ts, end_ts = _get_timestamp_range(period or "today", sd, ed)
trades = Trade.get_all(
start_timestamp=start_ts,
end_timestamp=end_ts,
symbol=symbol,
status=None,
account_id=account_id,
time_filter=time_filter,
limit=limit,
reconciled_only=_reconciled,
include_sync=True,
)
out = []
for t in trades:
row = dict(t)
for k, v in row.items():
if hasattr(v, "isoformat"):
row[k] = v.isoformat()
out.append(row)
return {"total": len(out), "trades": out}
def _enrich_trades_with_derived(trades: list) -> list:
"""补充推算字段:入场价、交易小时、星期,便于策略分析"""
result = []
for r in trades:
out = dict(r)
t = r.get("time") or 0
if t:
dt = datetime.fromtimestamp(t / 1000, tz=BEIJING_TZ)
out["_trade_hour"] = dt.hour
out["_trade_weekday"] = dt.weekday()
out["_trade_date"] = dt.strftime("%Y-%m-%d")
pnl = float(r.get("realizedPnl") or 0)
qty = float(r.get("qty") or 0)
price = float(r.get("price") or 0)
side = (r.get("side") or "").upper()
if qty and pnl != 0 and side:
if side == "SELL":
out["_approx_entry_price"] = round(price - pnl / qty, 8)
else:
out["_approx_entry_price"] = round(price + pnl / qty, 8)
else:
out["_approx_entry_price"] = None
result.append(out)
return result
def _binance_row_to_api_format(row: dict, data_type: str) -> dict:
"""将 DB 行转换为前端/导出期望的币安 API 格式"""
if data_type == "trades":
return {
"id": row.get("trade_id"),
"orderId": row.get("order_id"),
"symbol": row.get("symbol"),
"_symbol": row.get("symbol"),
"side": row.get("side"),
"positionSide": row.get("position_side"),
"price": str(row.get("price") or ""),
"qty": str(row.get("qty") or ""),
"quoteQty": str(row.get("quote_qty") or ""),
"realizedPnl": str(row.get("realized_pnl") or ""),
"commission": str(row.get("commission") or ""),
"commissionAsset": row.get("commission_asset"),
"buyer": bool(row.get("buyer")),
"maker": bool(row.get("maker")),
"time": row.get("trade_time"),
}
else:
return {
"orderId": row.get("order_id"),
"clientOrderId": row.get("client_order_id"),
"symbol": row.get("symbol"),
"_symbol": row.get("symbol"),
"side": row.get("side"),
"type": row.get("type"),
"origType": row.get("orig_type"),
"status": row.get("status"),
"price": str(row.get("price") or ""),
"avgPrice": str(row.get("avg_price") or ""),
"origQty": str(row.get("orig_qty") or ""),
"executedQty": str(row.get("executed_qty") or ""),
"cumQty": str(row.get("cum_qty") or ""),
"cumQuote": str(row.get("cum_quote") or ""),
"stopPrice": str(row.get("stop_price") or "") if row.get("stop_price") else "",
"reduceOnly": bool(row.get("reduce_only")),
"positionSide": row.get("position_side"),
"time": row.get("order_time"),
"updateTime": row.get("update_time"),
}
@router.post("/binance-fetch")
async def query_binance_data_from_db(
_admin=Depends(get_admin_user),
account_id: int = Query(..., ge=1),
symbols: Optional[str] = Query(None, description="交易对,逗号分隔;留空则全部"),
data_type: str = Query("trades", description="orders 或 trades"),
days: int = Query(7, ge=0, le=7),
):
"""
DB 查询已同步的币安订单/成交由定时任务 scripts/sync_binance_orders.py 拉取入库
"""
from database.connection import db
now = datetime.now(BEIJING_TZ)
end_ts = int(now.timestamp())
if days == 0:
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
start_ts = int(today_start.timestamp())
else:
start_ts = end_ts - days * 24 * 3600
start_ms = start_ts * 1000
end_ms = end_ts * 1000
symbol_list = [s.strip().upper() for s in (symbols or "").split(",") if s.strip()]
try:
if data_type == "trades":
q = """SELECT * FROM binance_trades
WHERE account_id = %s AND trade_time >= %s AND trade_time <= %s"""
params = [account_id, start_ms, end_ms]
if symbol_list:
q += " AND symbol IN (" + ",".join(["%s"] * len(symbol_list)) + ")"
params.extend(symbol_list)
q += " ORDER BY trade_time DESC LIMIT 5000"
else:
q = """SELECT * FROM binance_orders
WHERE account_id = %s AND order_time >= %s AND order_time <= %s"""
params = [account_id, start_ms, end_ms]
if symbol_list:
q += " AND symbol IN (" + ",".join(["%s"] * len(symbol_list)) + ")"
params.extend(symbol_list)
q += " ORDER BY order_time DESC LIMIT 5000"
rows = db.execute_query(q, params)
except Exception as e:
raise HTTPException(status_code=500, detail=f"查询失败(请确认已执行 add_binance_sync_tables.sql 并运行过同步脚本): {e}")
all_data = [_binance_row_to_api_format(dict(r), data_type) for r in (rows or [])]
if data_type == "trades":
all_data = _enrich_trades_with_derived(all_data)
symbols_queried = len(symbol_list) if symbol_list else len({(r or {}).get("symbol") for r in (rows or []) if (r or {}).get("symbol")})
stats = _compute_binance_stats(all_data, data_type)
return {
"total": len(all_data),
"data_type": data_type,
"symbols_queried": symbols_queried,
"stats": stats,
"data": all_data,
"source": "db",
}

View File

@ -0,0 +1,183 @@
"""
公开只读状态接口非管理员也可访问
用途
- 普通用户能看到后端是否在线启动时间推荐是否在更新snapshot 时间
- recommendations-viewer 也可复用该接口展示服务状态
安全原则
- 不返回任何敏感信息不返回密钥密码完整 Redis URL
"""
from __future__ import annotations
import json
import os
import time
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, Optional, Tuple
from fastapi import APIRouter
try:
import redis.asyncio as redis_async
except Exception: # pragma: no cover
redis_async = None
router = APIRouter(prefix="/api/public", tags=["public"])
_STARTED_AT_MS = int(time.time() * 1000)
REDIS_KEY_RECOMMENDATIONS_SNAPSHOT = "recommendations:snapshot"
def _beijing_time_str(ts_ms: Optional[int] = None) -> str:
beijing_tz = timezone(timedelta(hours=8))
if ts_ms is None:
return datetime.now(tz=beijing_tz).strftime("%Y-%m-%d %H:%M:%S")
return datetime.fromtimestamp(ts_ms / 1000, tz=beijing_tz).strftime("%Y-%m-%d %H:%M:%S")
def _mask_redis_url(redis_url: str) -> str:
s = (redis_url or "").strip()
if not s:
return ""
# 简单脱敏:去掉 username/password如果有
# rediss://user:pass@host:6379/0 -> rediss://***@host:6379/0
if "://" in s and "@" in s:
scheme, rest = s.split("://", 1)
creds_and_host = rest
# 仅替换 @ 前面的内容
idx = creds_and_host.rfind("@")
if idx > 0:
return f"{scheme}://***@{creds_and_host[idx+1:]}"
return s
def _redis_connection_kwargs() -> Tuple[str, Dict[str, Any]]:
redis_url = (os.getenv("REDIS_URL", "") or "").strip() or "redis://localhost:6379"
username = os.getenv("REDIS_USERNAME", None)
password = os.getenv("REDIS_PASSWORD", None)
ssl_cert_reqs = (os.getenv("REDIS_SSL_CERT_REQS", "required") or "required").strip()
ssl_ca_certs = os.getenv("REDIS_SSL_CA_CERTS", None)
select = os.getenv("REDIS_SELECT", None)
try:
select_i = int(select) if select is not None else 0
except Exception:
select_i = 0
kwargs: Dict[str, Any] = {"decode_responses": True}
if username:
kwargs["username"] = username
if password:
kwargs["password"] = password
kwargs["db"] = select_i
use_tls = redis_url.startswith("rediss://") or (os.getenv("REDIS_USE_TLS", "False").lower() == "true")
if use_tls and not redis_url.startswith("rediss://"):
if redis_url.startswith("redis://"):
redis_url = redis_url.replace("redis://", "rediss://", 1)
else:
redis_url = f"rediss://{redis_url}"
if use_tls or redis_url.startswith("rediss://"):
kwargs["ssl_cert_reqs"] = ssl_cert_reqs
if ssl_ca_certs:
kwargs["ssl_ca_certs"] = ssl_ca_certs
kwargs["ssl_check_hostname"] = (ssl_cert_reqs == "required")
return redis_url, kwargs
async def _get_redis():
if redis_async is None:
return None
redis_url, kwargs = _redis_connection_kwargs()
try:
client = redis_async.from_url(redis_url, **kwargs)
await client.ping()
return client
except Exception:
return None
async def _get_cached_json(client, key: str) -> Optional[Any]:
try:
raw = await client.get(key)
if not raw:
return None
return json.loads(raw)
except Exception:
return None
@router.get("/status")
async def public_status():
"""
公共状态
- backend在线/启动时间
- redis可用性不暴露密码
- recommendationssnapshot 最新生成时间若推荐进程在跑会持续更新
"""
now_ms = int(time.time() * 1000)
# Redis + 推荐快照
redis_ok = False
reco: Dict[str, Any] = {"snapshot_ok": False}
redis_meta: Dict[str, Any] = {"ok": False, "db": int(os.getenv("REDIS_SELECT", "0") or 0), "url": _mask_redis_url(os.getenv("REDIS_URL", ""))}
rds = await _get_redis()
if rds is not None:
redis_ok = True
redis_meta["ok"] = True
try:
snap = await _get_cached_json(rds, REDIS_KEY_RECOMMENDATIONS_SNAPSHOT)
except Exception:
snap = None
if isinstance(snap, dict):
gen_ms = snap.get("generated_at_ms")
try:
gen_ms = int(gen_ms) if gen_ms is not None else None
except Exception:
gen_ms = None
count = snap.get("count")
try:
count = int(count) if count is not None else None
except Exception:
count = None
age_sec = None
if gen_ms:
age_sec = max(0, int((now_ms - gen_ms) / 1000))
reco = {
"snapshot_ok": True,
"generated_at_ms": gen_ms,
"generated_at": snap.get("generated_at"),
"generated_at_beijing": _beijing_time_str(gen_ms) if gen_ms else None,
"age_sec": age_sec,
"count": count,
"ttl_sec": snap.get("ttl_sec"),
}
try:
await rds.close()
except Exception:
pass
return {
"backend": {
"running": True,
"started_at_ms": _STARTED_AT_MS,
"started_at": _beijing_time_str(_STARTED_AT_MS),
"now_ms": now_ms,
"now": _beijing_time_str(now_ms),
},
"redis": redis_meta,
"recommendations": reco,
"auth": {
"enabled": (os.getenv("ATS_AUTH_ENABLED") or "true").strip().lower() not in {"0", "false", "no"},
},
}

View File

@ -366,7 +366,15 @@ async def get_recommendations(
# 限制返回数量
recommendations = recommendations[:limit]
# 合约推荐为空时给出排查提示(与现货独立:现货来自定时扫描,合约来自策略/推荐服务)
hint = None
if len(recommendations) == 0:
hint = (
"合约推荐来自策略扫描需信号强度≥5且方向明确才会写入。"
"若长期为空请检查1) 推荐服务(recommendations_main)或主策略(main)是否在运行;"
"2) 扫描日志中是否有「信号:N」≥5 的标的3) 是否有推荐被时间/价格偏离过滤掉(见 meta.dropped"
)
return {
"success": True,
"count": len(recommendations),
@ -384,6 +392,7 @@ async def get_recommendations(
"price_drift": dropped_drift,
"invalid": dropped_invalid,
},
"hint": hint,
},
"data": recommendations
}
@ -495,6 +504,72 @@ async def get_recommendations(
raise HTTPException(status_code=500, detail=f"获取推荐列表失败: {str(e)}")
REDIS_KEY_SPOT_SNAPSHOT = "recommendations:spot:snapshot"
@router.get("/spot")
async def get_spot_recommendations(
limit: int = Query(50, ge=1, le=200, description="返回数量限制"),
):
"""
获取现货推荐只做多数据来自定时任务扫描并写入的 Redis 缓存
"""
try:
rds = await _get_redis()
if rds is None:
raise HTTPException(status_code=503, detail="Redis 不可用,无法读取现货推荐缓存")
snapshot = await _get_cached_json(rds, REDIS_KEY_SPOT_SNAPSHOT)
if not isinstance(snapshot, dict):
return {
"success": True,
"count": 0,
"type": "spot",
"from_cache": False,
"meta": {"generated_at": None, "message": "暂无现货推荐数据,请等待定时扫描更新"},
"data": [],
}
items = snapshot.get("items") or []
if not isinstance(items, list):
items = []
items = items[:limit]
return {
"success": True,
"count": len(items),
"type": "spot",
"from_cache": True,
"meta": {
"generated_at": snapshot.get("generated_at"),
"generated_at_ms": snapshot.get("generated_at_ms"),
"ttl_sec": snapshot.get("ttl_sec"),
},
"data": items,
}
except HTTPException:
raise
except Exception as e:
logger.error(f"获取现货推荐失败: {e}")
raise HTTPException(status_code=500, detail=f"获取现货推荐失败: {str(e)}")
@router.post("/spot/scan")
async def trigger_spot_scan():
"""
手动触发一次现货扫描并更新 Redis 缓存供定时任务或管理员调用
"""
try:
import sys
from pathlib import Path
backend_dir = Path(__file__).resolve().parent.parent.parent
if str(backend_dir) not in sys.path:
sys.path.insert(0, str(backend_dir))
from spot_scanner import run_spot_scan_and_cache
count = await run_spot_scan_and_cache(ttl_sec=900)
return {"success": True, "message": f"已扫描并缓存 {count} 条现货推荐", "count": count}
except Exception as e:
logger.error(f"现货扫描失败: {e}")
raise HTTPException(status_code=500, detail=f"现货扫描失败: {str(e)}")
@router.get("/active")
async def get_active_recommendations():
"""

View File

@ -1,7 +1,7 @@
"""
统计分析API
"""
from fastapi import APIRouter, Query
from fastapi import APIRouter, Query, Header, Depends
import sys
from pathlib import Path
from datetime import datetime, timedelta
@ -11,23 +11,251 @@ project_root = Path(__file__).parent.parent.parent.parent
sys.path.insert(0, str(project_root))
sys.path.insert(0, str(project_root / 'backend'))
from database.models import AccountSnapshot, Trade, MarketScan, TradingSignal
from database.models import AccountSnapshot, Trade, MarketScan, TradingSignal, Account, TradeStats
from fastapi import HTTPException
from api.auth_deps import get_account_id, get_admin_user
from typing import Dict, Any
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/admin/dashboard")
async def get_admin_dashboard_stats(user: Dict[str, Any] = Depends(get_admin_user)):
"""获取管理员仪表板数据总资产来自各账号快照汇总不调币安总盈亏为最近7天聚合已实现盈亏。"""
try:
accounts = Account.list_all()
stats = []
total_assets = 0.0
active_accounts = 0
for acc in accounts:
aid = acc["id"]
# 取最近 30 天内的快照,再取最新一条,避免“仅 1 天”导致无数据
snapshots = AccountSnapshot.get_recent(30, account_id=aid)
acc_stat = {
"id": aid,
"name": acc["name"],
"status": acc["status"],
"total_balance": 0,
"total_pnl": 0,
"open_positions": 0,
}
if snapshots:
snap = snapshots[0]
acc_stat["total_balance"] = snap.get("total_balance", 0)
acc_stat["total_pnl"] = snap.get("total_pnl", 0)
acc_stat["open_positions"] = snap.get("open_positions", 0)
total_assets += float(acc_stat["total_balance"])
if acc["status"] == "active":
active_accounts += 1
stats.append(acc_stat)
total_pnl_7d = 0.0
try:
global_symbols = TradeStats.get_global_symbol_stats(days=7)
for row in global_symbols:
total_pnl_7d += float(row.get("net_pnl") or 0)
except Exception as e:
logger.debug(f"获取全局7天净盈亏失败: {e}")
return {
"summary": {
"total_accounts": len(accounts),
"active_accounts": active_accounts,
"total_assets_usdt": round(total_assets, 2),
"total_pnl_usdt": round(total_pnl_7d, 2),
},
"accounts": stats,
}
except Exception as e:
logger.error(f"获取管理员仪表板数据失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@router.get("/admin/overall-trade-stats")
async def get_admin_overall_trade_stats(
days: int = Query(7, ge=1, le=90),
user: Dict[str, Any] = Depends(get_admin_user),
):
"""管理员:全账号最近 N 天整体订单统计。"""
try:
by_symbol_raw = TradeStats.get_global_symbol_stats(days=days)
by_hour_raw = TradeStats.get_global_hourly_stats(days=days)
by_symbol = []
for row in by_symbol_raw:
tc = int(row.get("trade_count") or 0)
win_count = int(row.get("win_count") or 0)
loss_count = int(row.get("loss_count") or 0)
net_pnl = float(row.get("net_pnl") or 0)
win_rate = (100.0 * win_count / tc) if tc > 0 else 0.0
by_symbol.append({
"symbol": (row.get("symbol") or "").strip(),
"trade_count": tc,
"win_count": win_count,
"loss_count": loss_count,
"net_pnl": round(net_pnl, 4),
"win_rate_pct": round(win_rate, 1),
})
by_symbol = [x for x in by_symbol if x["symbol"]]
by_symbol.sort(key=lambda x: (-x["net_pnl"], -x["trade_count"]))
hourly_agg = [{"hour": h, "trade_count": 0, "net_pnl": 0.0} for h in range(24)]
for row in by_hour_raw:
h = row.get("hour")
if h is not None and 0 <= int(h) <= 23:
hi = int(h)
hourly_agg[hi]["trade_count"] = int(row.get("trade_count") or 0)
hourly_agg[hi]["net_pnl"] = round(float(row.get("net_pnl") or 0), 4)
total_trade_count = sum(x["trade_count"] for x in by_symbol)
total_win = sum(x["win_count"] for x in by_symbol)
total_loss = sum(x["loss_count"] for x in by_symbol)
total_net_pnl = sum(x["net_pnl"] for x in by_symbol)
suggestions = _build_suggestions(by_symbol)
return {
"days": days,
"summary": {
"trade_count": total_trade_count,
"win_count": total_win,
"loss_count": total_loss,
"net_pnl": round(total_net_pnl, 4),
},
"by_symbol": by_symbol,
"hourly_agg": hourly_agg,
"suggestions": suggestions,
}
except Exception as e:
logger.exception("get_admin_overall_trade_stats 失败")
raise HTTPException(status_code=500, detail=str(e))
def _aggregate_daily_by_symbol(daily: list) -> list:
"""将 daily按 date+symbol聚合成按 symbol 的汇总。"""
from collections import defaultdict
agg = defaultdict(lambda: {"trade_count": 0, "win_count": 0, "loss_count": 0, "net_pnl": 0.0})
for row in daily:
sym = (row.get("symbol") or "").strip()
if not sym:
continue
agg[sym]["trade_count"] += int(row.get("trade_count") or 0)
agg[sym]["win_count"] += int(row.get("win_count") or 0)
agg[sym]["loss_count"] += int(row.get("loss_count") or 0)
try:
agg[sym]["net_pnl"] += float(row.get("net_pnl") or 0)
except (TypeError, ValueError):
pass
out = []
for symbol, v in agg.items():
tc = v["trade_count"]
win_rate = (100.0 * v["win_count"] / tc) if tc > 0 else 0.0
out.append({
"symbol": symbol,
"trade_count": tc,
"win_count": v["win_count"],
"loss_count": v["loss_count"],
"net_pnl": round(v["net_pnl"], 4),
"win_rate_pct": round(win_rate, 1),
})
return sorted(out, key=lambda x: (-x["net_pnl"], -x["trade_count"]))
def _aggregate_hourly(by_hour: list) -> list:
"""将 by_hour按 date+hour聚合成按 hour 0-23 的汇总。"""
from collections import defaultdict
agg = defaultdict(lambda: {"trade_count": 0, "net_pnl": 0.0})
for row in by_hour:
h = row.get("hour")
if h is None:
continue
try:
h = int(h)
except (TypeError, ValueError):
continue
if 0 <= h <= 23:
agg[h]["trade_count"] += int(row.get("trade_count") or 0)
try:
agg[h]["net_pnl"] += float(row.get("net_pnl") or 0)
except (TypeError, ValueError):
pass
return [{"hour": h, "trade_count": agg[h]["trade_count"], "net_pnl": round(agg[h]["net_pnl"], 4)} for h in range(24)]
def _build_suggestions(by_symbol: list) -> dict:
"""
根据按交易对汇总生成白名单/黑名单建议仅展示不自动改策略
- 黑名单净亏且笔数多 建议降权或观察
- 白名单净盈且胜率较高笔数足够 可优先考虑
"""
blacklist = []
whitelist = []
for row in by_symbol:
sym = row.get("symbol", "")
tc = int(row.get("trade_count") or 0)
net_pnl = float(row.get("net_pnl") or 0)
win_rate = float(row.get("win_rate_pct") or 0)
if tc < 2:
continue
if net_pnl < 0:
blacklist.append({
"symbol": sym,
"trade_count": tc,
"net_pnl": round(net_pnl, 2),
"win_rate_pct": round(win_rate, 1),
"suggestion": "近期净亏且笔数较多,建议降权或观察后再开仓",
})
elif net_pnl > 0 and win_rate >= 50:
whitelist.append({
"symbol": sym,
"trade_count": tc,
"net_pnl": round(net_pnl, 2),
"win_rate_pct": round(win_rate, 1),
"suggestion": "近期净盈且胜率尚可,可优先考虑",
})
return {"blacklist": blacklist, "whitelist": whitelist}
@router.get("/trade-stats")
async def get_trade_stats(
days: int = Query(7, ge=1, le=90),
account_id: int = Depends(get_account_id),
):
"""获取交易统计:最近 N 天按交易对、按小时聚合(来自 trade_stats_daily / trade_stats_time_bucket
返回原始 daily/by_hour按交易对汇总 by_symbol按小时汇总 hourly_agg以及白名单/黑名单建议"""
try:
daily = TradeStats.get_daily_stats(account_id=account_id, days=days)
by_hour = TradeStats.get_hourly_stats(account_id=account_id, days=days)
by_symbol = _aggregate_daily_by_symbol(daily)
hourly_agg = _aggregate_hourly(by_hour)
suggestions = _build_suggestions(by_symbol)
return {
"days": days,
"daily": daily,
"by_hour": by_hour,
"by_symbol": by_symbol,
"hourly_agg": hourly_agg,
"suggestions": suggestions,
}
except Exception as e:
logger.exception("get_trade_stats 失败")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/performance")
async def get_performance_stats(days: int = Query(7, ge=1, le=365)):
async def get_performance_stats(
days: int = Query(7, ge=1, le=365),
account_id: int = Depends(get_account_id),
):
"""获取性能统计"""
try:
# 账户快照
snapshots = AccountSnapshot.get_recent(days)
snapshots = AccountSnapshot.get_recent(days, account_id=account_id)
# 交易统计
start_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
trades = Trade.get_all(start_date=start_date)
# 交易统计(时间范围 + limit 防内存暴增)
start_ts = int((datetime.now() - timedelta(days=days)).timestamp())
end_ts = int(datetime.now().timestamp())
trades = Trade.get_all(
start_timestamp=start_ts,
end_timestamp=end_ts,
account_id=account_id,
time_filter="exit",
limit=10000,
)
return {
"snapshots": snapshots,
@ -39,24 +267,32 @@ async def get_performance_stats(days: int = Query(7, ge=1, le=365)):
@router.get("/dashboard")
async def get_dashboard_data():
async def get_dashboard_data(account_id: int = Depends(get_account_id)):
"""获取仪表板数据"""
logger.info("=" * 60)
logger.info(f"获取仪表板数据 - account_id={account_id}")
logger.info("=" * 60)
try:
account_data = None
account_error = None
# 优先尝试获取实时账户数据
# 优先请求币安实时余额;失败时(如 -1003 IP 封禁)再回退到数据库快照
try:
from api.routes.account import get_realtime_account_data
account_data = await get_realtime_account_data()
logger.info("成功获取实时账户数据")
except HTTPException as e:
# HTTPException 需要特殊处理,提取错误信息
account_error = e.detail
logger.warning(f"获取实时账户数据失败 (HTTP {e.status_code}): {account_error}")
# 回退到数据库快照
account_data = await get_realtime_account_data(account_id=account_id)
if account_data and account_data.get('total_balance') is not None:
logger.info("使用币安实时账户数据")
else:
account_data = None
account_error = "实时余额返回为空"
except Exception as live_err:
account_error = str(live_err)
logger.warning(f"获取实时账户数据失败 (account_id={account_id}),回退到数据库快照: {live_err}")
# 实时请求失败或无数据时,使用数据库快照
if not account_data or account_data.get('total_balance') is None:
try:
snapshots = AccountSnapshot.get_recent(1)
snapshots = AccountSnapshot.get_recent(1, account_id=account_id)
if snapshots:
account_data = {
"total_balance": snapshots[0].get('total_balance', 0),
@ -67,94 +303,68 @@ async def get_dashboard_data():
}
logger.info("使用数据库快照作为账户数据")
else:
logger.warning("数据库中没有账户快照数据")
if not account_data:
account_data = {}
account_data.setdefault("total_balance", 0)
account_data.setdefault("available_balance", 0)
account_data.setdefault("total_position_value", 0)
account_data.setdefault("total_pnl", 0)
account_data.setdefault("open_positions", 0)
logger.warning("数据库中没有账户快照数据,仪表板显示 0交易进程会定期写入快照")
except Exception as db_error:
logger.error(f"从数据库获取账户快照失败: {db_error}")
except Exception as e:
account_error = str(e)
logger.warning(f"获取实时账户数据失败: {account_error}", exc_info=True)
# 回退到数据库快照
try:
snapshots = AccountSnapshot.get_recent(1)
if snapshots:
if not account_data:
account_data = {
"total_balance": snapshots[0].get('total_balance', 0),
"available_balance": snapshots[0].get('available_balance', 0),
"total_position_value": snapshots[0].get('total_position_value', 0),
"total_pnl": snapshots[0].get('total_pnl', 0),
"open_positions": snapshots[0].get('open_positions', 0)
"total_balance": 0,
"available_balance": 0,
"total_position_value": 0,
"total_pnl": 0,
"open_positions": 0
}
logger.info("使用数据库快照作为账户数据")
except Exception as db_error:
logger.error(f"从数据库获取账户快照失败: {db_error}")
# 获取持仓数据(优先实时,回退到数据库)
# 获取持仓数据:优先「币安实时持仓」(含本系统下的挂单),失败时回退到数据库列表
open_trades = []
positions_error = None
try:
from api.routes.account import get_realtime_positions
positions = await get_realtime_positions()
# 转换为前端需要的格式
open_trades = positions
logger.info(f"成功获取实时持仓数据: {len(open_trades)} 个持仓")
except HTTPException as e:
positions_error = e.detail
logger.warning(f"获取实时持仓失败 (HTTP {e.status_code}): {positions_error}")
# 回退到数据库记录
try:
db_trades = Trade.get_all(status='open')[:10]
# 格式化数据库记录,添加 entry_value_usdt 字段
from api.routes.account import fetch_realtime_positions
open_trades = await fetch_realtime_positions(account_id)
except Exception as fetch_err:
logger.warning(f"获取币安实时持仓失败,回退到数据库列表: {fetch_err}")
open_trades = []
if not open_trades:
db_trades = Trade.get_all(status='open', account_id=account_id, limit=500)
for trade in db_trades:
entry_value_usdt = float(trade.get('quantity', 0)) * float(trade.get('entry_price', 0))
leverage = float(trade.get('leverage', 1))
pnl = float(trade.get('pnl', 0))
# 数据库中的pnl_percent是价格涨跌幅需要转换为收益率
# 收益率 = 盈亏 / 保证金
margin = entry_value_usdt / leverage if leverage > 0 else entry_value_usdt
pnl_percent = (pnl / margin * 100) if margin > 0 else 0
formatted_trade = {
open_trades.append({
**trade,
'entry_value_usdt': entry_value_usdt,
'mark_price': trade.get('entry_price', 0), # 数据库中没有标记价,使用入场价
'mark_price': trade.get('entry_price', 0),
'pnl': pnl,
'pnl_percent': pnl_percent # 使用重新计算的收益率
}
open_trades.append(formatted_trade)
'pnl_percent': pnl_percent
})
try:
from api.routes.account import fetch_live_positions_pnl
live_list = await fetch_live_positions_pnl(account_id)
by_symbol = {p["symbol"]: p for p in live_list}
for t in open_trades:
sym = t.get("symbol")
if sym and sym in by_symbol:
lp = by_symbol[sym]
t["mark_price"] = lp.get("mark_price", t.get("entry_price"))
t["pnl"] = lp.get("pnl", 0)
t["pnl_percent"] = lp.get("pnl_percent", 0)
except Exception as merge_err:
logger.debug(f"合并实时持仓盈亏失败: {merge_err}")
logger.info(f"使用数据库记录作为持仓数据: {len(open_trades)} 个持仓")
except Exception as db_error:
logger.error(f"从数据库获取持仓记录失败: {db_error}")
except Exception as e:
positions_error = str(e)
logger.warning(f"获取实时持仓失败: {positions_error}", exc_info=True)
# 回退到数据库记录
try:
db_trades = Trade.get_all(status='open')[:10]
# 格式化数据库记录,添加 entry_value_usdt 字段
open_trades = []
for trade in db_trades:
entry_value_usdt = float(trade.get('quantity', 0)) * float(trade.get('entry_price', 0))
leverage = float(trade.get('leverage', 1))
pnl = float(trade.get('pnl', 0))
# 数据库中的pnl_percent是价格涨跌幅需要转换为收益率
# 收益率 = 盈亏 / 保证金
margin = entry_value_usdt / leverage if leverage > 0 else entry_value_usdt
pnl_percent = (pnl / margin * 100) if margin > 0 else 0
formatted_trade = {
**trade,
'entry_value_usdt': entry_value_usdt,
'mark_price': trade.get('entry_price', 0), # 数据库中没有标记价,使用入场价
'pnl': pnl,
'pnl_percent': pnl_percent # 使用重新计算的收益率
}
open_trades.append(formatted_trade)
logger.info(f"使用数据库记录作为持仓数据: {len(open_trades)} 个持仓")
except Exception as db_error:
logger.error(f"从数据库获取持仓记录失败: {db_error}")
else:
logger.info(f"使用币安实时持仓作为列表: {len(open_trades)} 个持仓")
except Exception as db_error:
logger.error(f"从数据库获取持仓记录失败: {db_error}")
# 最近的扫描记录
recent_scans = []
@ -176,7 +386,7 @@ async def get_dashboard_data():
try:
from database.models import TradingConfig
total_balance = float(account_data.get('total_balance', 0))
max_total_position_percent = float(TradingConfig.get_value('MAX_TOTAL_POSITION_PERCENT', 0.30))
max_total_position_percent = float(TradingConfig.get_value('MAX_TOTAL_POSITION_PERCENT', 0.30, account_id=account_id))
# 名义仓位notional与保证金占用margin是两个口径
# - 名义仓位可以 > 100%(高杠杆下非常正常)
@ -237,7 +447,7 @@ async def get_dashboard_data():
from database.models import TradingConfig
config_keys = ['STOP_LOSS_PERCENT', 'TAKE_PROFIT_PERCENT', 'LEVERAGE', 'MAX_POSITION_PERCENT']
for key in config_keys:
config = TradingConfig.get(key)
config = TradingConfig.get(key, account_id=account_id)
if config:
trading_config[key] = {
'value': TradingConfig._convert_value(config['config_value'], config['config_type']),
@ -246,13 +456,21 @@ async def get_dashboard_data():
except Exception as e:
logger.debug(f"获取交易配置失败: {e}")
# 本系统持仓数 = 数据库 status=open 条数,与下方「当前持仓」列表一致;币安持仓数 = 接口/快照中的 open_positions可能与币安页面一致
open_trades_count = len(open_trades)
result = {
"account": account_data,
"open_trades": open_trades,
"open_trades_count": open_trades_count, # 本系统持仓数,与列表条数一致
"recent_scans": recent_scans,
"recent_signals": recent_signals,
"position_stats": position_stats,
"trading_config": trading_config # 添加交易配置
"trading_config": trading_config, # 添加交易配置
"_debug": { # 添加调试信息
"account_id": account_id,
"account_data_total_balance": account_data.get('total_balance', 'N/A') if account_data else 'N/A',
"open_trades_count": open_trades_count,
}
}
# 如果有错误,在响应中包含错误信息(但不影响返回)
@ -263,6 +481,14 @@ async def get_dashboard_data():
if positions_error:
result["warnings"]["positions"] = positions_error
logger.info(f"返回仪表板数据:")
logger.info(f" - account_id: {account_id}")
logger.info(f" - total_balance: {account_data.get('total_balance', 'N/A') if account_data else 'N/A'}")
logger.info(f" - available_balance: {account_data.get('available_balance', 'N/A') if account_data else 'N/A'}")
logger.info(f" - open_trades count: {len(open_trades)}")
if open_trades and len(open_trades) > 0:
logger.info(f" - 第一个持仓: {open_trades[0].get('symbol', 'N/A')}")
logger.info("=" * 60)
return result
except Exception as e:
logger.error(f"获取仪表板数据失败: {e}", exc_info=True)

View File

@ -6,7 +6,7 @@ import time
from pathlib import Path
from typing import Any, Dict, Optional, Tuple
from fastapi import APIRouter, HTTPException, Header
from fastapi import APIRouter, HTTPException, Header, Depends, BackgroundTasks
from pydantic import BaseModel
import logging
@ -15,6 +15,10 @@ logger = logging.getLogger(__name__)
# 路由统一挂在 /api/system 下,前端直接调用 /api/system/...
router = APIRouter(prefix="/api/system")
# 管理员鉴权JWT未启用登录时兼容 X-Admin-Token
from api.auth_deps import require_system_admin # noqa: E402
from database.models import Account # noqa: E402
LOG_GROUPS = ("error", "warning", "info")
# 后端服务启动时间(用于前端展示“运行多久/是否已重启”)
@ -175,13 +179,11 @@ def _beijing_time_str() -> str:
@router.post("/logs/test-write")
async def logs_test_write(
x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token"),
_admin: Dict[str, Any] = Depends(require_system_admin),
) -> Dict[str, Any]:
"""
写入 3 条测试日志到 Rediserror/warning/info用于验证是否写入到同一台 Redis同一组 key
"""
_require_admin(os.getenv("SYSTEM_CONTROL_TOKEN", "").strip(), x_admin_token)
client = _get_redis_client_for_logs()
if client is None:
raise HTTPException(status_code=503, detail="Redis 不可用,无法写入测试日志")
@ -238,6 +240,35 @@ async def logs_test_write(
}
@router.post("/trading/trigger-scan")
async def trigger_scan(
_admin: Dict[str, Any] = Depends(require_system_admin),
) -> Dict[str, Any]:
"""
触发手动扫描
通过设置 Redis 信号ats:trigger-scan通知所有运行中的 strategy 进程立即执行扫描
"""
client = _get_redis_client_for_logs()
if client is None:
raise HTTPException(status_code=503, detail="Redis 不可用,无法触发扫描")
try:
# 设置触发信号(当前时间戳),让 strategy 检测到变化
import time
ts = int(time.time())
# 使用 setex 设置 600秒过期防止永久残留虽然 strategy 只关心变化,但过期是个好习惯)
# 注意strategy 端比较的是时间戳大小,所以只要比上一次大即可
client.setex("ats:trigger-scan", 600, str(ts))
return {
"message": "已发送扫描触发信号",
"timestamp": ts
}
except Exception as e:
logger.error(f"触发扫描失败: {e}")
raise HTTPException(status_code=500, detail=f"触发扫描失败: {e}")
def _get_redis_client_for_logs():
"""
获取 Redis 客户端优先复用 config_manager 的连接失败则自行创建
@ -311,7 +342,7 @@ async def get_logs(
start: int = 0,
service: Optional[str] = None,
level: Optional[str] = None,
x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token"),
_admin: Dict[str, Any] = Depends(require_system_admin),
) -> Dict[str, Any]:
"""
Redis List 读取最新日志默认 group=error -> ats:logs:error
@ -322,8 +353,6 @@ async def get_logs(
- service: 过滤backend / trading_system
- level: 过滤ERROR / CRITICAL ...
"""
_require_admin(os.getenv("SYSTEM_CONTROL_TOKEN", "").strip(), x_admin_token)
if limit <= 0:
limit = 200
if limit > 20000:
@ -332,6 +361,12 @@ async def get_logs(
if start < 0:
start = 0
# 定义管理员不需要关注的日志模式(噪声过滤)
IGNORED_PATTERNS = [
"API密钥未配置",
"请在配置界面设置该账号的BINANCE_API_KEY",
]
group = (group or "error").strip().lower()
if group not in LOG_GROUPS:
raise HTTPException(status_code=400, detail=f"非法 group{group}(可选:{', '.join(LOG_GROUPS)}")
@ -388,6 +423,12 @@ async def get_logs(
continue
if level and str(parsed.get("level")) != level:
continue
# 噪声过滤
msg = str(parsed.get("message", ""))
if any(p in msg for p in IGNORED_PATTERNS):
continue
items.append(parsed)
if len(items) >= limit:
break
@ -414,8 +455,7 @@ async def get_logs(
@router.get("/logs/overview")
async def logs_overview(x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token")) -> Dict[str, Any]:
_require_admin(os.getenv("SYSTEM_CONTROL_TOKEN", "").strip(), x_admin_token)
async def logs_overview(_admin: Dict[str, Any] = Depends(require_system_admin)) -> Dict[str, Any]:
client = _get_redis_client_for_logs()
if client is None:
@ -472,10 +512,8 @@ async def logs_overview(x_admin_token: Optional[str] = Header(default=None, alia
@router.put("/logs/config")
async def update_logs_config(
payload: LogsConfigUpdate,
x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token"),
_admin: Dict[str, Any] = Depends(require_system_admin),
) -> Dict[str, Any]:
_require_admin(os.getenv("SYSTEM_CONTROL_TOKEN", "").strip(), x_admin_token)
client = _get_redis_client_for_logs()
if client is None:
raise HTTPException(status_code=503, detail="Redis 不可用,无法更新日志配置")
@ -525,6 +563,10 @@ def _require_admin(token: Optional[str], provided: Optional[str]) -> None:
raise HTTPException(status_code=401, detail="Unauthorized")
#
# 注意require_system_admin 已迁移到 api.auth_deps避免导入不一致导致 uvicorn 启动失败
def _build_supervisorctl_cmd(args: list[str]) -> list[str]:
supervisorctl_path = os.getenv("SUPERVISORCTL_PATH", "supervisorctl")
supervisor_conf = os.getenv("SUPERVISOR_CONF", "").strip()
@ -567,7 +609,12 @@ def _run_supervisorctl(args: list[str]) -> str:
out = (res.stdout or "").strip()
err = (res.stderr or "").strip()
combined = "\n".join([s for s in [out, err] if s]).strip()
if res.returncode != 0:
# supervisorctl 约定:
# - status 在存在 STOPPED/FATAL 等进程时可能返回 exit=3但输出仍然有效
ok_rc = {0}
if args and args[0] == "status":
ok_rc.add(3)
if res.returncode not in ok_rc:
raise RuntimeError(combined or f"supervisorctl failed (exit={res.returncode})")
return combined or out
@ -587,6 +634,20 @@ def _parse_supervisor_status(raw: str) -> Tuple[bool, Optional[int], str]:
return False, None, state
return False, None, "UNKNOWN"
def _list_supervisor_process_names(status_all_raw: str) -> list[str]:
names: list[str] = []
if not status_all_raw:
return names
for ln in status_all_raw.splitlines():
s = (ln or "").strip()
if not s:
continue
# 每行格式:<name> <STATE> ...
name = s.split(None, 1)[0].strip()
if name:
names.append(name)
return names
def _get_program_name() -> str:
# 你给的宝塔配置是 [program:auto_sys]
@ -676,17 +737,68 @@ def _action_with_fallback(action: str, program: str) -> Tuple[str, Optional[str]
return out, resolved, status_all
def _run_fix_script():
"""Run the fix_trade_records.py script in a subprocess"""
try:
script_path = Path(__file__).parent.parent.parent.parent / "scripts" / "fix_trade_records.py"
if not script_path.exists():
logger.error(f"Fix script not found at {script_path}")
return
logger.info(f"Starting trade record fix script: {script_path}")
# Ensure project root is in PYTHONPATH
env = os.environ.copy()
project_root = Path(__file__).parent.parent.parent.parent
env["PYTHONPATH"] = f"{env.get('PYTHONPATH', '')}:{project_root}"
process = subprocess.Popen(
["python3", str(script_path)],
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
stdout, stderr = process.communicate()
if process.returncode == 0:
logger.info(f"Trade record fix completed successfully:\n{stdout}")
else:
logger.error(f"Trade record fix failed (exit code {process.returncode}):\n{stderr}")
except Exception as e:
logger.error(f"Error running trade record fix script: {e}")
@router.post("/fix-trade-records")
async def fix_trade_records(
background_tasks: BackgroundTasks,
_admin: Dict[str, Any] = Depends(require_system_admin)
):
"""
Trigger the trade record fix script (time inversion & commission backfill).
Runs in background.
"""
background_tasks.add_task(_run_fix_script)
return {"message": "Trade fix task started in background"}
@router.post("/clear-cache")
async def clear_cache(x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token")) -> Dict[str, Any]:
async def clear_cache(
_admin: Dict[str, Any] = Depends(require_system_admin),
x_account_id: Optional[int] = Header(default=None, alias="X-Account-Id"),
) -> Dict[str, Any]:
"""
清理配置缓存Redis Hash: trading_config并从数据库回灌到 Redis
"""
_require_admin(os.getenv("SYSTEM_CONTROL_TOKEN", "").strip(), x_admin_token)
try:
import config_manager
cm = getattr(config_manager, "config_manager", None)
account_id = int(x_account_id or 1)
cm = None
if hasattr(config_manager, "ConfigManager") and hasattr(config_manager.ConfigManager, "for_account"):
cm = config_manager.ConfigManager.for_account(account_id)
else:
cm = getattr(config_manager, "config_manager", None)
if cm is None:
raise HTTPException(status_code=500, detail="config_manager 未初始化")
@ -710,10 +822,16 @@ async def clear_cache(x_admin_token: Optional[str] = Header(default=None, alias=
if redis_client is not None and redis_connected:
try:
redis_client.delete("trading_config")
deleted_keys.append("trading_config")
key = getattr(cm, "_redis_hash_key", "trading_config")
redis_client.delete(key)
deleted_keys.append(str(key))
# 兼容:老 key仅 default 账号)
legacy = getattr(cm, "_legacy_hash_key", None)
if legacy and legacy != key:
redis_client.delete(legacy)
deleted_keys.append(str(legacy))
except Exception as e:
logger.warning(f"删除 Redis key trading_config 失败: {e}")
logger.warning(f"删除 Redis key 失败: {e}")
# 可选:实时推荐缓存(如果存在)
try:
@ -742,9 +860,90 @@ async def clear_cache(x_admin_token: Optional[str] = Header(default=None, alias=
raise HTTPException(status_code=500, detail=str(e))
@router.get("/trading/services")
async def list_trading_services(_admin: Dict[str, Any] = Depends(require_system_admin)):
"""获取所有交易服务状态(包括所有账号)"""
try:
# 获取所有 supervisor 进程状态
status_all = _run_supervisorctl(["status"])
services = []
summary = {"total": 0, "running": 0, "stopped": 0, "unknown": 0}
# 解析每一行
# 格式通常是: name state description
for line in status_all.splitlines():
line = line.strip()
if not line:
continue
parts = line.split(None, 2)
if len(parts) < 2:
continue
name = parts[0]
state = parts[1]
desc = parts[2] if len(parts) > 2 else ""
# 只关注 auto_sys 开头的服务
if name.startswith("auto_sys"):
is_running = state == "RUNNING"
pid = None
if is_running:
# Parse PID from desc: "pid 1234, uptime ..."
m = re.search(r"pid\s+(\d+)", desc)
if m:
pid = int(m.group(1))
services.append({
"program": name,
"state": state,
"running": is_running,
"pid": pid,
"description": desc
})
summary["total"] += 1
if is_running:
summary["running"] += 1
elif state in ["STOPPED", "EXITED", "FATAL"]:
summary["stopped"] += 1
else:
summary["unknown"] += 1
return {
"summary": summary,
"services": services,
"raw": status_all
}
except Exception as e:
# supervisor 未安装/未运行时(如 unix socket 不存在)避免刷 ERROR改为 WARNING 并返回友好说明
err_msg = str(e).strip()
if not err_msg:
err_msg = repr(e)
is_supervisor_unavailable = (
"no such file" in err_msg.lower()
or "connection refused" in err_msg.lower()
or "sock" in err_msg.lower()
or "unix://" in err_msg.lower()
)
if is_supervisor_unavailable:
logger.warning(f"列出服务失败supervisor 未运行或不可用): {err_msg}")
return {
"summary": {"total": 0, "running": 0, "stopped": 0, "unknown": 0},
"services": [],
"error": "supervisor 未安装或未运行,请检查 supervisord 或配置 SUPERVISOR_CONF"
}
logger.error(f"列出服务失败: {e}")
return {
"summary": {"total": 0, "running": 0, "stopped": 0, "unknown": 0},
"services": [],
"error": err_msg
}
@router.get("/trading/status")
async def trading_status(x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token")) -> Dict[str, Any]:
_require_admin(os.getenv("SYSTEM_CONTROL_TOKEN", "").strip(), x_admin_token)
async def trading_status(_admin: Dict[str, Any] = Depends(require_system_admin)) -> Dict[str, Any]:
program = _get_program_name()
try:
@ -770,8 +969,7 @@ async def trading_status(x_admin_token: Optional[str] = Header(default=None, ali
@router.post("/trading/start")
async def trading_start(x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token")) -> Dict[str, Any]:
_require_admin(os.getenv("SYSTEM_CONTROL_TOKEN", "").strip(), x_admin_token)
async def trading_start(_admin: Dict[str, Any] = Depends(require_system_admin)) -> Dict[str, Any]:
program = _get_program_name()
try:
@ -797,8 +995,7 @@ async def trading_start(x_admin_token: Optional[str] = Header(default=None, alia
@router.post("/trading/stop")
async def trading_stop(x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token")) -> Dict[str, Any]:
_require_admin(os.getenv("SYSTEM_CONTROL_TOKEN", "").strip(), x_admin_token)
async def trading_stop(_admin: Dict[str, Any] = Depends(require_system_admin)) -> Dict[str, Any]:
program = _get_program_name()
try:
@ -824,8 +1021,7 @@ async def trading_stop(x_admin_token: Optional[str] = Header(default=None, alias
@router.post("/trading/restart")
async def trading_restart(x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token")) -> Dict[str, Any]:
_require_admin(os.getenv("SYSTEM_CONTROL_TOKEN", "").strip(), x_admin_token)
async def trading_restart(_admin: Dict[str, Any] = Depends(require_system_admin)) -> Dict[str, Any]:
program = _get_program_name()
try:
@ -867,8 +1063,274 @@ async def trading_restart(x_admin_token: Optional[str] = Header(default=None, al
raise HTTPException(status_code=500, detail=f"supervisorctl restart 失败: {e}")
@router.post("/trading/stop-all")
async def trading_stop_all(
_admin: Dict[str, Any] = Depends(require_system_admin),
prefix: str = "auto_sys_acc",
include_default: bool = False,
) -> Dict[str, Any]:
"""
一键停止所有账号交易进程supervisor
"""
try:
prefix = (prefix or "auto_sys_acc").strip()
if not prefix:
prefix = "auto_sys_acc"
# 先读取全量 status拿到有哪些进程
status_all = _run_supervisorctl(["status"])
names = _list_supervisor_process_names(status_all)
targets: list[str] = []
for n in names:
if n.startswith(prefix):
targets.append(n)
if include_default:
default_prog = _get_program_name()
if default_prog and default_prog not in targets and default_prog in names:
targets.append(default_prog)
if not targets:
return {
"message": "未找到可停止的交易进程",
"prefix": prefix,
"include_default": include_default,
"count": 0,
"targets": [],
"status_all": status_all,
}
results: list[Dict[str, Any]] = []
ok = 0
failed = 0
for prog in targets:
try:
out = _run_supervisorctl(["stop", prog])
raw = _run_supervisorctl(["status", prog])
running, pid, state = _parse_supervisor_status(raw)
results.append(
{
"program": prog,
"ok": True,
"output": out,
"status": {"running": running, "pid": pid, "state": state, "raw": raw},
}
)
ok += 1
except Exception as e:
failed += 1
results.append({"program": prog, "ok": False, "error": str(e)})
return {
"message": "已发起批量停止",
"prefix": prefix,
"include_default": include_default,
"count": len(targets),
"ok": ok,
"failed": failed,
"targets": targets,
"results": results,
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"批量停止失败: {e}")
@router.post("/trading/restart-all")
async def trading_restart_all(
_admin: Dict[str, Any] = Depends(require_system_admin),
prefix: str = "auto_sys_acc",
include_default: bool = False,
do_update: bool = True,
) -> Dict[str, Any]:
"""
一键重启所有账号交易进程supervisor
- 默认重启所有以 auto_sys_acc 开头的 program例如 auto_sys_acc1/2/3...
- 可选 include_default=true同时包含 SUPERVISOR_TRADING_PROGRAM默认 auto_sys
- 可选 do_update=true先执行 supervisorctl reread/update 再重启确保新 ini 生效
"""
try:
prefix = (prefix or "auto_sys_acc").strip()
if not prefix:
prefix = "auto_sys_acc"
# 先读取全量 status拿到有哪些进程
status_all = _run_supervisorctl(["status"])
names = _list_supervisor_process_names(status_all)
targets: list[str] = []
skipped_disabled: list[Dict[str, Any]] = []
for n in names:
if n.startswith(prefix):
# 若能解析出 account_id则跳过 disabled 的账号
try:
m = re.match(rf"^{re.escape(prefix)}(\d+)$", n)
if m:
aid = int(m.group(1))
row = Account.get(aid)
st = (row.get("status") if isinstance(row, dict) else None) or "active"
if str(st).strip().lower() != "active":
skipped_disabled.append({"program": n, "account_id": aid, "status": st})
continue
except Exception:
# 解析失败/查库失败:不影响批量重启流程
pass
targets.append(n)
if include_default:
default_prog = _get_program_name()
if default_prog and default_prog not in targets and default_prog in names:
targets.append(default_prog)
if not targets:
return {
"message": "未找到可重启的交易进程",
"prefix": prefix,
"include_default": include_default,
"count": 0,
"targets": [],
"status_all": status_all,
"skipped_disabled": skipped_disabled,
}
reread_out = ""
update_out = ""
if do_update:
try:
reread_out = _run_supervisorctl(["reread"])
except Exception as e:
reread_out = f"failed: {e}"
try:
update_out = _run_supervisorctl(["update"])
except Exception as e:
update_out = f"failed: {e}"
results: list[Dict[str, Any]] = []
ok = 0
failed = 0
for prog in targets:
try:
out = _run_supervisorctl(["restart", prog])
raw = _run_supervisorctl(["status", prog])
running, pid, state = _parse_supervisor_status(raw)
results.append(
{
"program": prog,
"ok": True,
"output": out,
"status": {"running": running, "pid": pid, "state": state, "raw": raw},
}
)
ok += 1
except Exception as e:
failed += 1
results.append({"program": prog, "ok": False, "error": str(e)})
return {
"message": "已发起批量重启",
"prefix": prefix,
"include_default": include_default,
"do_update": do_update,
"count": len(targets),
"ok": ok,
"failed": failed,
"reread": reread_out,
"update": update_out,
"targets": targets,
"results": results,
"skipped_disabled": skipped_disabled,
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"批量重启失败: {e}")
@router.get("/market-overview")
async def market_overview(_admin: Dict[str, Any] = Depends(require_system_admin)) -> Dict[str, Any]:
"""
市场行情概览拉取 Binance 公开接口展示与策略过滤对应的数据
供全局配置页展示帮助用户确认当前策略方案是否匹配市场
"""
try:
from market_overview import get_market_overview
except ImportError:
try:
from backend.market_overview import get_market_overview
except ImportError:
import sys
backend_dir = Path(__file__).parent.parent.parent
if str(backend_dir) not in sys.path:
sys.path.insert(0, str(backend_dir))
from market_overview import get_market_overview
data = get_market_overview()
# 获取当前策略配置,用于对比
beta_enabled = False
beta_threshold = -0.005
market_scheme = "normal"
try:
from config_manager import GlobalStrategyConfigManager
mgr = GlobalStrategyConfigManager()
beta_enabled = str(mgr.get("BETA_FILTER_ENABLED", "true")).lower() in ("true", "1", "yes")
try:
beta_threshold = float(mgr.get("BETA_FILTER_THRESHOLD", -0.005))
except (TypeError, ValueError):
pass
market_scheme = str(mgr.get("MARKET_SCHEME", "normal")).strip().lower() or "normal"
except Exception:
pass
# 计算大盘共振是否触发(与 strategy._check_beta_filter 一致)
threshold_pct = beta_threshold * 100
triggered = False
if beta_enabled:
for key in ["btc_15m_change_pct", "btc_1h_change_pct", "eth_15m_change_pct", "eth_1h_change_pct"]:
val = data.get(key)
if val is not None and val < threshold_pct:
triggered = True
break
data["config"] = {
"BETA_FILTER_ENABLED": beta_enabled,
"BETA_FILTER_THRESHOLD": beta_threshold,
"BETA_FILTER_THRESHOLD_PCT": round(threshold_pct, 2),
"MARKET_SCHEME": market_scheme,
}
data["beta_filter_triggered"] = triggered
# 策略执行概览:当前执行方案与配置项执行情况(易读文字)
get_strategy_execution_overview = None
try:
from market_overview import get_strategy_execution_overview
except ImportError:
try:
from backend.market_overview import get_strategy_execution_overview
except ImportError:
pass
if get_strategy_execution_overview is None:
try:
import sys
backend_dir = Path(__file__).resolve().parent.parent.parent
if str(backend_dir) not in sys.path:
sys.path.insert(0, str(backend_dir))
from market_overview import get_strategy_execution_overview
except ImportError:
pass
if get_strategy_execution_overview is not None:
try:
data["strategy_execution_overview"] = get_strategy_execution_overview()
except Exception as e:
data["strategy_execution_overview"] = {"sections": [{"title": "加载失败", "content": str(e)}]}
else:
data["strategy_execution_overview"] = {
"sections": [{"title": "策略执行概览暂不可用", "content": "请确认后端已重启并已部署最新代码;若已重启仍无数据,请检查 backend/market_overview.py 与 config_manager 是否可正常导入。"}]
}
return data
@router.get("/backend/status")
async def backend_status(x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token")) -> Dict[str, Any]:
async def backend_status(_admin: Dict[str, Any] = Depends(require_system_admin)) -> Dict[str, Any]:
"""
查看后端服务状态当前 uvicorn 进程
@ -876,7 +1338,6 @@ async def backend_status(x_admin_token: Optional[str] = Header(default=None, ali
- pid 使用 os.getpid()当前 FastAPI 进程
- last_restart Redis 读取若可用
"""
_require_admin(os.getenv("SYSTEM_CONTROL_TOKEN", "").strip(), x_admin_token)
meta = _system_meta_read("backend:last_restart") or {}
return {
"running": True,
@ -888,7 +1349,7 @@ async def backend_status(x_admin_token: Optional[str] = Header(default=None, ali
@router.post("/backend/restart")
async def backend_restart(x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token")) -> Dict[str, Any]:
async def backend_restart(_admin: Dict[str, Any] = Depends(require_system_admin)) -> Dict[str, Any]:
"""
重启后端服务uvicorn
@ -901,8 +1362,6 @@ async def backend_restart(x_admin_token: Optional[str] = Header(default=None, al
注意
- 为了让接口能先返回这里会延迟 1s 再执行 restart.sh
"""
_require_admin(os.getenv("SYSTEM_CONTROL_TOKEN", "").strip(), x_admin_token)
backend_dir = Path(__file__).parent.parent.parent # backend/
restart_script = backend_dir / "restart.sh"
if not restart_script.exists():
@ -944,3 +1403,159 @@ async def backend_restart(x_admin_token: Optional[str] = Header(default=None, al
"note": "重启期间接口可能短暂不可用,页面可等待 3-5 秒后刷新状态。",
}
@router.post("/backend/stop")
async def backend_stop(_admin: Dict[str, Any] = Depends(require_system_admin)) -> Dict[str, Any]:
"""
停止后端服务uvicorn
警告停止后 API 将不可用必须手动登录服务器启动
"""
backend_dir = Path(__file__).parent.parent.parent # backend/
stop_script = backend_dir / "stop.sh"
if not stop_script.exists():
raise HTTPException(status_code=500, detail=f"找不到停止脚本: {stop_script}")
cur_pid = os.getpid()
# 后台执行sleep 1 后再停止,保证当前请求可以返回
cmd = ["bash", "-lc", f"sleep 1; '{stop_script}'"]
try:
subprocess.Popen(
cmd,
cwd=str(backend_dir),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"启动停止脚本失败: {e}")
return {
"message": "已发起后端停止1s 后执行)",
"pid_before": cur_pid,
"script": str(stop_script),
"warning": "后端服务停止后Web 界面将无法访问,请手动在服务器启动!",
}
def _recommendations_process_running() -> Tuple[bool, Optional[int]]:
"""检查推荐服务进程是否运行,返回 (running, pid)。兼容 pgrep/ps 及 supervisor 等启动方式。"""
# 1. 优先 pgrepLinux/macOS 常见)
for pattern in ["trading_system.recommendations_main", "recommendations_main1", "recommendations_main"]:
try:
result = subprocess.run(
["pgrep", "-f", pattern],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode == 0 and result.stdout.strip():
pids = [x for x in result.stdout.strip().split() if x.isdigit()]
if pids:
return True, int(pids[0])
except (FileNotFoundError, subprocess.TimeoutExpired, ValueError):
break
# 2. 回退ps + greppgrep 不可用或匹配失败时)
try:
result = subprocess.run(
["sh", "-c", "ps aux 2>/dev/null | grep -E 'recommendations_main1|recommendations_main|trading_system.recommendations' | grep -v grep | head -1"],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode == 0 and result.stdout.strip():
parts = result.stdout.strip().split()
if len(parts) >= 2 and parts[1].isdigit():
return True, int(parts[1])
except (FileNotFoundError, subprocess.TimeoutExpired, ValueError):
pass
return False, None
@router.get("/recommendations/status")
async def recommendations_status(_admin: Dict[str, Any] = Depends(require_system_admin)) -> Dict[str, Any]:
"""查看推荐服务状态recommendations_main 进程)"""
running, pid = _recommendations_process_running()
return {
"running": running,
"pid": pid,
}
@router.post("/recommendations/restart")
async def recommendations_restart(_admin: Dict[str, Any] = Depends(require_system_admin)) -> Dict[str, Any]:
"""重启推荐服务recommendations_main"""
backend_dir = Path(__file__).parent.parent.parent
restart_script = backend_dir / "restart_recommendations.sh"
if not restart_script.exists():
raise HTTPException(status_code=500, detail=f"找不到重启脚本: {restart_script}")
running_before, pid_before = _recommendations_process_running()
cmd = ["bash", "-lc", f"sleep 1; '{restart_script}'"]
try:
subprocess.Popen(
cmd,
cwd=str(backend_dir),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"启动重启脚本失败: {e}")
return {
"message": "已发起推荐服务重启1s 后执行)",
"pid_before": pid_before,
"running_before": running_before,
"script": str(restart_script),
}
@router.post("/recommendations/stop")
async def recommendations_stop(_admin: Dict[str, Any] = Depends(require_system_admin)) -> Dict[str, Any]:
"""停止推荐服务"""
backend_dir = Path(__file__).parent.parent.parent
stop_script = backend_dir / "stop_recommendations.sh"
if not stop_script.exists():
raise HTTPException(status_code=500, detail=f"找不到停止脚本: {stop_script}")
running_before, pid_before = _recommendations_process_running()
cmd = ["bash", "-lc", f"sleep 1; '{stop_script}'"]
try:
subprocess.Popen(
cmd,
cwd=str(backend_dir),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"启动停止脚本失败: {e}")
return {
"message": "已发起推荐服务停止",
"pid_before": pid_before,
"running_before": running_before,
}
@router.post("/recommendations/start")
async def recommendations_start(_admin: Dict[str, Any] = Depends(require_system_admin)) -> Dict[str, Any]:
"""启动推荐服务(若已运行则跳过)"""
running, pid = _recommendations_process_running()
if running:
return {"message": "推荐服务已在运行中", "pid": pid, "skipped": True}
backend_dir = Path(__file__).parent.parent.parent
start_script = backend_dir / "start_recommendations.sh"
if not start_script.exists():
raise HTTPException(status_code=500, detail=f"找不到启动脚本: {start_script}")
cmd = ["bash", "-lc", f"sleep 1; '{start_script}'"]
try:
subprocess.Popen(
cmd,
cwd=str(backend_dir),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"启动脚本执行失败: {e}")
return {"message": "已发起推荐服务启动1s 后执行)", "script": str(start_script)}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,568 @@
"""
Supervisor 多账号托管宝塔插件兼容
目标
- 根据 account_id 自动生成一个 supervisor program 配置文件.ini
- 自动定位 supervisord.conf include 目录尽量不要求你手填路径
- 提供 supervisorctl 的常用调用封装reread/update/status/start/stop/restart
重要说明
- 本模块只写入程序配置文件不包含任何 API Key/Secret
- trading_system 进程通过 ATS_ACCOUNT_ID 选择自己的账号配置
"""
from __future__ import annotations
import os
import re
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Tuple, List, Dict, Any
DEFAULT_CANDIDATE_CONFS = [
"/www/server/panel/plugin/supervisor/supervisord.conf",
"/www/server/panel/plugin/supervisor/supervisor.conf",
"/etc/supervisor/supervisord.conf",
"/etc/supervisord.conf",
]
# 常见 supervisorctl 路径候选
DEFAULT_SUPERVISORCTL_CANDIDATES = [
"/www/server/panel/pyenv/bin/supervisorctl",
"/usr/bin/supervisorctl",
"/usr/local/bin/supervisorctl",
"/usr/local/python/bin/supervisorctl",
]
def _detect_supervisorctl_path() -> str:
"""
探测 supervisorctl 可执行文件路径
"""
env_path = (os.getenv("SUPERVISORCTL_PATH") or "").strip()
if env_path:
return env_path
# 优先检查 PATH 中的 supervisorctl
import shutil
if shutil.which("supervisorctl"):
return "supervisorctl"
# 检查常见绝对路径
for p in DEFAULT_SUPERVISORCTL_CANDIDATES:
try:
if os.path.exists(p) and os.access(p, os.X_OK):
return p
except Exception:
continue
return "supervisorctl" # 兜底
# 常见 supervisord 主日志路径候选(不同发行版/面板插件差异很大)
DEFAULT_SUPERVISORD_LOG_CANDIDATES = [
# aaPanel / 宝塔 supervisor 插件常见
"/www/server/panel/plugin/supervisor/log/supervisord.log",
"/www/server/panel/plugin/supervisor/log/supervisor.log",
"/www/server/panel/plugin/supervisor/supervisord.log",
"/www/server/panel/plugin/supervisor/supervisor.log",
# 系统 supervisor 常见
"/var/log/supervisor/supervisord.log",
"/var/log/supervisor/supervisor.log",
"/var/log/supervisord.log",
"/var/log/supervisord/supervisord.log",
"/tmp/supervisord.log",
]
def _get_project_root() -> Path:
# backend/api/supervisor_account.py -> api -> backend -> project_root
# 期望得到:<project_root>(例如 /www/wwwroot/autosys_new
return Path(__file__).resolve().parents[2]
def _detect_supervisor_conf_path() -> Optional[Path]:
p = (os.getenv("SUPERVISOR_CONF") or "").strip()
if p:
pp = Path(p)
return pp if pp.exists() else pp # 允许不存在时也返回,便于报错信息
for cand in DEFAULT_CANDIDATE_CONFS:
try:
cp = Path(cand)
if cp.exists():
return cp
except Exception:
continue
return None
def _parse_include_dir_from_conf(conf_path: Path) -> Optional[Path]:
"""
尝试解析 supervisord.conf [include] files=... 目录
常见格式
[include]
files = /path/to/conf.d/*.ini
"""
try:
text = conf_path.read_text(encoding="utf-8", errors="ignore")
except Exception:
return None
in_include = False
for raw in text.splitlines():
line = raw.strip()
if not line or line.startswith(";") or line.startswith("#"):
continue
if re.match(r"^\[include\]\s*$", line, flags=re.I):
in_include = True
continue
if in_include and line.startswith("[") and line.endswith("]"):
break
if not in_include:
continue
m = re.match(r"^files\s*=\s*(.+)$", line, flags=re.I)
if not m:
continue
val = (m.group(1) or "").strip().strip('"').strip("'")
if not val:
continue
# 只取第一个 pattern即使写了多个用空格分隔
first = val.split()[0]
p = Path(first)
if not p.is_absolute():
p = (conf_path.parent / p).resolve()
return p.parent
return None
def get_supervisor_program_dir() -> Path:
"""
获取 supervisor program 配置目录优先级
1) SUPERVISOR_PROGRAM_DIR
2) supervisord.conf [include] files= 解析
3) 兜底/www/server/panel/plugin/supervisor你当前看到的目录
"""
env_dir = (os.getenv("SUPERVISOR_PROGRAM_DIR") or "").strip()
if env_dir:
return Path(env_dir)
conf = _detect_supervisor_conf_path()
if conf and conf.exists():
inc = _parse_include_dir_from_conf(conf)
if inc:
return inc
return Path("/www/server/panel/plugin/supervisor")
def program_name_for_account(account_id: int) -> str:
tmpl = (os.getenv("SUPERVISOR_TRADING_PROGRAM_TEMPLATE") or "auto_sys_acc{account_id}").strip()
try:
return tmpl.format(account_id=int(account_id))
except Exception:
return f"auto_sys_acc{int(account_id)}"
def ini_filename_for_program(program_name: str) -> str:
safe = re.sub(r"[^a-zA-Z0-9_\-:.]+", "_", program_name).strip("_") or "auto_sys"
return f"{safe}.ini"
def render_program_ini(account_id: int, program_name: str) -> str:
project_root = _get_project_root()
# Python 可执行文件:
# - 优先使用 TRADING_PYTHON_BIN线上可显式指定 trading_system 的 venv
# - 否则尝试多种候选路径(避免 backend venv 未安装交易依赖导致启动失败)
python_bin_env = (os.getenv("TRADING_PYTHON_BIN") or "").strip()
candidates = []
if python_bin_env:
candidates.append(python_bin_env)
# 当前进程 pythonbackend venv
candidates.append(sys.executable)
# 常见 venv 位置
candidates += [
str(project_root / "backend" / ".venv" / "bin" / "python"),
str(project_root / ".venv" / "bin" / "python"),
str(project_root / "trading_system" / ".venv" / "bin" / "python"),
"/usr/bin/python3",
"/usr/local/bin/python3",
]
python_bin = None
for c in candidates:
try:
p = Path(c)
if p.exists() and os.access(str(p), os.X_OK):
python_bin = str(p)
break
except Exception:
continue
if not python_bin:
# 最后兜底:写 sys.executable让错误能在日志里体现
python_bin = sys.executable
# 日志目录可通过环境变量覆盖
log_dir, out_log, err_log = expected_trading_log_paths(project_root, int(account_id))
# supervisor 在 reread/update 时会校验 logfile 目录是否存在;这里提前创建避免 CANT_REREAD
try:
log_dir.mkdir(parents=True, exist_ok=True)
except Exception:
# 最后兜底到 /tmp确保一定存在
log_dir = Path("/tmp") / "autosys_logs"
log_dir.mkdir(parents=True, exist_ok=True)
out_log = log_dir / f"trading_{int(account_id)}.out.log"
err_log = log_dir / f"trading_{int(account_id)}.err.log"
# 默认不自动启动,避免“创建账号=立刻下单”
autostart = (os.getenv("TRADING_AUTOSTART_DEFAULT", "false") or "false").lower() == "true"
run_user = (os.getenv("SUPERVISOR_RUN_USER") or "").strip()
return "\n".join(
[
f"[program:{program_name}]",
f"directory={project_root}",
f"command={python_bin} -m trading_system.main",
"autostart=" + ("true" if autostart else "false"),
# 更合理仅在“非0退出”时重启0 退出视为“正常结束”,不进入 FATAL 反复拉起
"autorestart=unexpected",
# 兼容0/2 都视为“预期退出”(例如配置不完整/前置检查失败时主动退出)
"exitcodes=0,2",
"startsecs=0",
"stopasgroup=true",
"killasgroup=true",
"stopsignal=TERM",
"",
# 关键PYTHONPATH 指向项目根,确保 -m trading_system.main 可导入
f'environment=ATS_ACCOUNT_ID="{int(account_id)}",PYTHONUNBUFFERED="1",PYTHONPATH="{project_root}"',
(f"user={run_user}" if run_user else "").rstrip(),
"",
f"stdout_logfile={out_log}",
f"stderr_logfile={err_log}",
"stdout_logfile_maxbytes=20MB",
"stdout_logfile_backups=5",
"stderr_logfile_maxbytes=20MB",
"stderr_logfile_backups=5",
"",
]
)
def write_program_ini(program_dir: Path, filename: str, content: str) -> Path:
program_dir.mkdir(parents=True, exist_ok=True)
target = program_dir / filename
tmp = program_dir / (filename + ".tmp")
tmp.write_text(content, encoding="utf-8")
os.replace(str(tmp), str(target))
return target
def _build_supervisorctl_cmd(args: list[str]) -> list[str]:
supervisorctl_path = _detect_supervisorctl_path()
supervisor_conf = (os.getenv("SUPERVISOR_CONF") or "").strip()
use_sudo = (os.getenv("SUPERVISOR_USE_SUDO", "false") or "false").lower() == "true"
if not supervisor_conf:
conf = _detect_supervisor_conf_path()
supervisor_conf = str(conf) if conf else ""
cmd: list[str] = []
if use_sudo:
cmd += ["sudo", "-n"]
cmd += [supervisorctl_path]
if supervisor_conf:
cmd += ["-c", supervisor_conf]
cmd += args
return cmd
def run_supervisorctl(args: list[str], timeout_sec: int = 10) -> str:
cmd = _build_supervisorctl_cmd(args)
try:
res = subprocess.run(cmd, capture_output=True, text=True, timeout=int(timeout_sec))
except subprocess.TimeoutExpired:
raise RuntimeError("supervisorctl 超时")
except FileNotFoundError:
# 明确提示找不到命令,帮助排查路径问题
cmd_str = " ".join(cmd)
raise RuntimeError(f"Command not found: {cmd[0]} (Full cmd: {cmd_str})")
except Exception as e:
raise RuntimeError(f"supervisorctl execution failed: {str(e)}")
out = (res.stdout or "").strip()
err = (res.stderr or "").strip()
combined = "\n".join([s for s in [out, err] if s]).strip()
# supervisorctl: status 在存在 STOPPED 等进程时可能返回 exit=3但输出仍然有效
ok_rc = {0}
if args and args[0] == "status":
ok_rc.add(3)
if res.returncode not in ok_rc:
raise RuntimeError(combined or f"supervisorctl failed (exit={res.returncode})")
return combined or out
def parse_supervisor_status(raw: str) -> Tuple[bool, Optional[int], str]:
if "RUNNING" in raw:
m = re.search(r"\bpid\s+(\d+)\b", raw)
pid = int(m.group(1)) if m else None
return True, pid, "RUNNING"
for state in ["STOPPED", "FATAL", "EXITED", "BACKOFF", "STARTING", "UNKNOWN"]:
if state in raw:
return False, None, state
return False, None, "UNKNOWN"
def tail_supervisor(program: str, stream: str = "stderr", lines: int = 120) -> str:
"""
读取 supervisor 进程最近日志stdout/stderr
修复优先直接读取日志文件避免 XML-RPC 编码错误
如果 supervisorctl tail 失败编码错误回退到直接读取文件
"""
s = (stream or "stderr").strip().lower()
if s not in {"stdout", "stderr"}:
s = "stderr"
n = int(lines or 120)
if n < 20:
n = 20
if n > 500:
n = 500
# 优先尝试通过 supervisorctl tail正常情况
try:
return run_supervisorctl(["tail", f"-{n}", str(program), s])
except Exception as e:
# 如果 supervisorctl tail 失败(可能是编码错误),尝试直接读取日志文件
error_msg = str(e)
if "UnicodeDecodeError" in error_msg or "utf-8" in error_msg.lower() or "codec" in error_msg.lower():
# 尝试从程序名解析 account_id例如 auto_sys_acc4 -> 4
try:
m = re.match(r"^auto_sys_acc(\d+)$", program)
if m:
account_id = int(m.group(1))
project_root = _get_project_root()
log_dir, out_log, err_log = expected_trading_log_paths(project_root, account_id)
# 根据 stream 选择对应的日志文件
log_file = out_log if s == "stdout" else err_log
if log_file.exists():
# 直接读取文件,使用宽松的编码处理
return _tail_text_file(log_file, lines=n)
except Exception:
pass
# 如果所有尝试都失败,返回错误信息(但不要抛出异常,避免影响主流程)
return f"[读取日志失败: {error_msg}]"
def _tail_text_file(path: Path, lines: int = 200, max_bytes: int = 64 * 1024) -> str:
"""
读取文本文件末尾用于 supervisor spawn error 等场景program stderr 可能为空
尽量只读最后 max_bytes避免大文件占用内存
修复使用更宽松的编码处理支持中文等多字节字符
"""
try:
p = Path(path)
if not p.exists():
return ""
size = p.stat().st_size
read_size = min(int(max_bytes), int(size))
with p.open("rb") as f:
if size > read_size:
f.seek(-read_size, os.SEEK_END)
data = f.read()
# ⚠️ 修复:尝试多种编码,优先 UTF-8失败则尝试常见中文编码
text = None
encodings = ["utf-8", "gbk", "gb2312", "gb18030", "latin1"]
for enc in encodings:
try:
text = data.decode(enc, errors="strict")
break
except (UnicodeDecodeError, LookupError):
continue
# 如果所有编码都失败,使用 errors="ignore" 强制解码(会丢失部分字符但不会报错)
if text is None:
text = data.decode("utf-8", errors="ignore")
# 仅保留最后 N 行
parts = text.splitlines()
if not parts:
return ""
n = int(lines or 200)
if n < 20:
n = 20
if n > 500:
n = 500
return "\n".join(parts[-n:]).strip()
except Exception:
# 兜底:若启用 sudo通常 backend 自己无权读 root 日志),尝试 sudo tail
try:
use_sudo = (os.getenv("SUPERVISOR_USE_SUDO", "false") or "false").lower() == "true"
if not use_sudo:
return ""
n = int(lines or 200)
if n < 20:
n = 20
if n > 500:
n = 500
res = subprocess.run(
["sudo", "-n", "tail", "-n", str(n), str(path)],
capture_output=True,
text=True,
timeout=5,
)
out = (res.stdout or "").strip()
err = (res.stderr or "").strip()
# 不强行报错:宁可空,也不要影响主流程
return (out or err or "").strip()
except Exception:
return ""
def _parse_supervisord_logfile_from_conf(conf_path: Path) -> Optional[Path]:
"""
解析 supervisord.conf [supervisord] logfile= 路径
"""
try:
text = conf_path.read_text(encoding="utf-8", errors="ignore")
except Exception:
return None
in_section = False
for raw in text.splitlines():
line = raw.strip()
if not line or line.startswith(";") or line.startswith("#"):
continue
if re.match(r"^\[supervisord\]\s*$", line, flags=re.I):
in_section = True
continue
if in_section and line.startswith("[") and line.endswith("]"):
break
if not in_section:
continue
m = re.match(r"^logfile\s*=\s*(.+)$", line, flags=re.I)
if not m:
continue
val = (m.group(1) or "").strip().strip('"').strip("'")
if not val:
continue
p = Path(val)
if not p.is_absolute():
p = (conf_path.parent / p).resolve()
return p
return None
def tail_supervisord_log(lines: int = 200) -> str:
"""
读取 supervisord 主日志尾部spawn error 的根因经常在这里
可通过环境变量 SUPERVISOR_LOGFILE 指定
"""
env_p = (os.getenv("SUPERVISOR_LOGFILE") or "").strip()
if env_p:
return _tail_text_file(Path(env_p), lines=lines)
conf = _detect_supervisor_conf_path()
if conf and conf.exists():
lp = _parse_supervisord_logfile_from_conf(conf)
if lp:
return _tail_text_file(lp, lines=lines)
# 最后兜底:尝试常见路径
for cand in DEFAULT_SUPERVISORD_LOG_CANDIDATES:
try:
p = Path(cand)
if p.exists():
text = _tail_text_file(p, lines=lines)
if text:
return text
except Exception:
continue
return ""
def expected_trading_log_paths(project_root: Path, account_id: int) -> Tuple[Path, Path, Path]:
"""
计算 trading program stdout/stderr logfile 路径需与 render_program_ini 保持一致
返回 (log_dir, out_log, err_log)
"""
log_dir = Path(os.getenv("TRADING_LOG_DIR", str(project_root / "logs"))).expanduser()
out_log = log_dir / f"trading_{int(account_id)}.out.log"
err_log = log_dir / f"trading_{int(account_id)}.err.log"
return log_dir, out_log, err_log
def tail_trading_log_files(account_id: int, lines: int = 200) -> Dict[str, Any]:
"""
直接读取该账号 trading 进程的 stdout/stderr logfile 尾部不依赖 supervisorctl tail
返回 {out_log, err_log, stdout_tail, stderr_tail}
"""
project_root = _get_project_root()
log_dir, out_log, err_log = expected_trading_log_paths(project_root, int(account_id))
return {
"log_dir": str(log_dir),
"out_log": str(out_log),
"err_log": str(err_log),
"stdout_tail_file": _tail_text_file(out_log, lines=lines),
"stderr_tail_file": _tail_text_file(err_log, lines=lines),
}
@dataclass
class EnsureProgramResult:
ok: bool
program: str
ini_path: str
program_dir: str
supervisor_conf: str
reread: str = ""
update: str = ""
error: str = ""
def ensure_account_program(account_id: int) -> EnsureProgramResult:
aid = int(account_id)
program = program_name_for_account(aid)
program_dir = get_supervisor_program_dir()
ini_name = ini_filename_for_program(program)
ini_text = render_program_ini(aid, program)
conf = _detect_supervisor_conf_path()
conf_s = str(conf) if conf else (os.getenv("SUPERVISOR_CONF") or "")
try:
path = write_program_ini(program_dir, ini_name, ini_text)
reread_out = ""
update_out = ""
try:
reread_out = run_supervisorctl(["reread"])
update_out = run_supervisorctl(["update"])
except Exception as e:
# 写文件成功但 supervisorctl 失败也要给出可诊断信息
return EnsureProgramResult(
ok=False,
program=program,
ini_path=str(path),
program_dir=str(program_dir),
supervisor_conf=conf_s,
reread=reread_out,
update=update_out,
error=f"写入配置成功,但执行 supervisorctl reread/update 失败: {e}",
)
return EnsureProgramResult(
ok=True,
program=program,
ini_path=str(path),
program_dir=str(program_dir),
supervisor_conf=conf_s,
reread=reread_out,
update=update_out,
)
except Exception as e:
return EnsureProgramResult(
ok=False,
program=program,
ini_path="",
program_dir=str(program_dir),
supervisor_conf=conf_s,
error=str(e),
)

View File

@ -0,0 +1,68 @@
import sys
import os
from pathlib import Path
import re
# Add backend directory to sys.path
backend_path = Path(__file__).parent
sys.path.insert(0, str(backend_path))
try:
from database.connection import db
print("Database connection imported successfully.")
except ImportError as e:
print(f"Error importing database connection: {e}")
sys.exit(1)
def is_ascii(s):
return all(ord(c) < 128 for c in s)
def check_table(table_name, column_name):
print(f"Checking table '{table_name}' column '{column_name}'...")
try:
query = f"SELECT DISTINCT {column_name} FROM {table_name}"
rows = db.execute_query(query)
found_invalid = False
for row in rows:
symbol = row.get(column_name)
if symbol and not is_ascii(symbol):
print(f"!!! FOUND INVALID SYMBOL in {table_name}: '{symbol}'")
found_invalid = True
if symbol and "币安" in symbol:
print(f"!!! FOUND '币安' in {table_name}: '{symbol}'")
found_invalid = True
if not found_invalid:
print(f"No invalid symbols found in {table_name}.")
except Exception as e:
print(f"Error querying {table_name}: {e}")
def check_config(table_name):
print(f"Checking table '{table_name}' for '币安'...")
try:
query = f"SELECT config_key, config_value FROM {table_name}"
rows = db.execute_query(query)
for row in rows:
key = row.get('config_key')
val = row.get('config_value')
if val and "币安" in str(val):
# Ignore expected descriptions/comments if any (usually description is separate column)
# But here we check config_value
print(f"Found '币安' in {table_name} KEY='{key}': VALUE='{val}'")
if key and "币安" in str(key):
print(f"Found '币安' in {table_name} KEY='{key}'")
except Exception as e:
print(f"Error querying {table_name}: {e}")
if __name__ == "__main__":
check_table("trades", "symbol")
check_table("trade_recommendations", "symbol")
check_config("trading_config")
check_config("global_strategy_config")

46
backend/check_dependencies.sh Executable file
View File

@ -0,0 +1,46 @@
#!/bin/bash
# 检查 backend 依赖是否完整安装
cd "$(dirname "$0")"
echo "=== 检查 Backend 依赖 ==="
echo ""
# 检查虚拟环境
if [ -d "../.venv" ]; then
echo "✓ 找到虚拟环境: ../.venv"
source ../.venv/bin/activate
elif [ -d ".venv" ]; then
echo "✓ 找到虚拟环境: .venv"
source .venv/bin/activate
else
echo "⚠ 未找到虚拟环境,使用系统 Python"
fi
echo ""
echo "Python 路径: $(which python3)"
echo "Python 版本: $(python3 --version)"
echo ""
# 检查关键依赖
echo "检查关键依赖..."
python3 -c "import fastapi; print('✓ fastapi:', fastapi.__version__)" 2>&1 || echo "✗ fastapi 未安装"
python3 -c "import uvicorn; print('✓ uvicorn:', uvicorn.__version__)" 2>&1 || echo "✗ uvicorn 未安装"
python3 -c "from jose import jwt; print('✓ python-jose: 已安装')" 2>&1 || echo "✗ python-jose 未安装"
python3 -c "import pymysql; print('✓ pymysql:', pymysql.__version__)" 2>&1 || echo "✗ pymysql 未安装"
python3 -c "import redis; print('✓ redis:', redis.__version__)" 2>&1 || echo "✗ redis 未安装"
python3 -c "from cryptography.fernet import Fernet; print('✓ cryptography: 已安装')" 2>&1 || echo "✗ cryptography 未安装"
echo ""
echo "=== 尝试导入 api.main ==="
python3 -c "import api.main; print('✓ api.main 导入成功')" 2>&1 || echo "✗ api.main 导入失败"
echo ""
echo "=== 检查完成 ==="
echo ""
echo "如果缺少依赖,请运行:"
echo " pip install -r backend/requirements.txt"
echo ""
echo "或者激活虚拟环境后运行:"
echo " source .venv/bin/activate"
echo " pip install -r backend/requirements.txt"

View File

@ -35,9 +35,10 @@ sys.path.insert(0, str(project_root))
# 延迟导入避免在trading_system中导入时因为缺少依赖而失败
try:
from database.models import TradingConfig
from database.models import TradingConfig, Account
except ImportError as e:
TradingConfig = None
Account = None
import logging
logger = logging.getLogger(__name__)
logger.warning(f"无法导入TradingConfig: {e},配置管理器将无法使用数据库")
@ -46,6 +47,39 @@ import logging
logger = logging.getLogger(__name__)
# 平台兜底策略核心使用全局配置表global_strategy_config普通用户账号只允许调整“风险旋钮”
# 执行策略合并顺序:普通用户(账号)配置优先,未设置时使用全局配置,允许用户简单控制自己的交易并只影响本人执行
# - 风险旋钮:每个账号独立(仓位/频次等),账号有则用账号,无则用全局
# - 其它策略参数:账号有则用账号,无则用全局(管理员可在全局配置设默认,用户可覆盖)
# 注意不再依赖account_id=1全局配置存储在独立的global_strategy_config表中
_MISSING = object() # 用于区分“账号未设置”与“值为 None/0/False”
RISK_KNOBS_KEYS = {
"MIN_MARGIN_USDT",
"MIN_POSITION_PERCENT",
"MAX_POSITION_PERCENT",
"MAX_TOTAL_POSITION_PERCENT",
"AUTO_TRADE_ENABLED",
"MAX_OPEN_POSITIONS",
"MAX_DAILY_ENTRIES",
"SUNDAY_MAX_OPENS",
"SUNDAY_MIN_SIGNAL_STRENGTH",
"NIGHT_HOURS_NO_OPEN_ENABLED",
"NIGHT_HOURS_START",
"NIGHT_HOURS_END",
# 2026-02-06 Added for Altcoin Strategy presets
"TOP_N_SYMBOLS",
"MIN_SIGNAL_STRENGTH",
"MIN_VOLUME_24H",
"MIN_VOLATILITY",
"SCAN_EXTRA_SYMBOLS_FOR_SUPPLEMENT",
"EXCLUDE_MAJOR_COINS",
# 2026-02-06 Added for User Customization
"MAX_SCAN_SYMBOLS",
"SCAN_INTERVAL",
}
# 尝试导入同步Redis客户端用于配置缓存
try:
import redis
@ -55,16 +89,242 @@ except ImportError:
redis = None
class ConfigManager:
"""配置管理器 - 优先从Redis缓存读取其次从数据库读取回退到环境变量和默认值"""
class GlobalStrategyConfigManager:
"""全局策略配置管理器(独立于账户,管理员专用)"""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if hasattr(self, '_initialized'):
return
self._initialized = True
self._cache = {}
self._redis_client: Optional[redis.Redis] = None
self._redis_connected = False
self._redis_hash_key = "global_strategy_config_v5" # 独立的Redis键 (v5: 强制刷新缓存 - 2025-02-14)
self._init_redis()
self._load_from_db()
def _init_redis(self):
"""初始化Redis客户端同步"""
if not REDIS_SYNC_AVAILABLE:
logger.debug("redis-py未安装全局配置缓存将不使用Redis")
return
try:
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 not redis_url or not isinstance(redis_url, str):
redis_url = 'redis://localhost:6379'
if redis_use_tls and not redis_url.startswith('rediss://'):
if redis_url.startswith('redis://'):
redis_url = redis_url.replace('redis://', 'rediss://', 1)
connection_kwargs = {
'username': redis_username,
'password': redis_password,
'decode_responses': True
}
if redis_url.startswith('rediss://') or redis_use_tls:
ssl_cert_reqs = os.getenv('REDIS_SSL_CERT_REQS', 'required')
ssl_ca_certs = os.getenv('REDIS_SSL_CA_CERTS', None)
connection_kwargs['select'] = int(os.getenv('REDIS_SELECT', 0))
connection_kwargs['ssl_cert_reqs'] = ssl_cert_reqs
if ssl_ca_certs:
connection_kwargs['ssl_ca_certs'] = ssl_ca_certs
if ssl_cert_reqs == 'none':
connection_kwargs['ssl_check_hostname'] = False
elif ssl_cert_reqs == 'required':
connection_kwargs['ssl_check_hostname'] = True
else:
connection_kwargs['ssl_check_hostname'] = False
self._redis_client = redis.from_url(redis_url, **connection_kwargs)
self._redis_client.ping()
self._redis_connected = True
logger.info("✓ 全局策略配置Redis缓存连接成功")
except Exception as e:
logger.debug(f"全局策略配置Redis缓存连接失败: {e},将使用数据库缓存")
self._redis_client = None
self._redis_connected = False
def _get_from_redis(self, key: str) -> Optional[Any]:
"""从Redis获取全局配置值"""
if not self._redis_connected or not self._redis_client:
return None
try:
value = self._redis_client.hget(self._redis_hash_key, key)
if value is not None and value != '':
return ConfigManager._coerce_redis_value(value)
except Exception as e:
logger.debug(f"从Redis获取全局配置失败 {key}: {e}")
try:
self._redis_client.ping()
self._redis_connected = True
except:
self._redis_connected = False
return None
def _set_to_redis(self, key: str, value: Any):
"""设置全局配置到Redis"""
if not self._redis_connected or not self._redis_client:
return False
try:
if isinstance(value, (dict, list, bool, int, float)):
value_str = json.dumps(value, ensure_ascii=False)
else:
value_str = str(value)
self._redis_client.hset(self._redis_hash_key, key, value_str)
self._redis_client.expire(self._redis_hash_key, 3600)
return True
except Exception as e:
logger.debug(f"设置全局配置到Redis失败 {key}: {e}")
try:
self._redis_client.ping()
self._redis_connected = True
except:
self._redis_connected = False
return False
def _load_from_db(self):
"""从数据库加载全局配置"""
try:
from database.models import GlobalStrategyConfig
except ImportError:
logger.warning("GlobalStrategyConfig未导入无法从数据库加载全局配置")
self._cache = {}
return
try:
# 先尝试从Redis加载
if self._redis_connected and self._redis_client:
try:
self._redis_client.ping()
redis_configs = self._redis_client.hgetall(self._redis_hash_key)
if redis_configs and len(redis_configs) > 0:
for key, value_str in redis_configs.items():
self._cache[key] = ConfigManager._coerce_redis_value(value_str)
logger.info(f"从Redis加载了 {len(self._cache)} 个全局配置项")
return
except Exception as e:
logger.debug(f"从Redis加载全局配置失败: {e},回退到数据库")
try:
self._redis_client.ping()
except:
self._redis_connected = False
# 从数据库加载
configs = GlobalStrategyConfig.get_all()
for config in configs:
key = config['config_key']
# 使用TradingConfig的转换方法GlobalStrategyConfig复用
from database.models import TradingConfig
value = TradingConfig._convert_value(
config['config_value'],
config['config_type']
)
self._cache[key] = value
self._set_to_redis(key, value)
logger.info(f"从数据库加载了 {len(self._cache)} 个全局配置项已同步到Redis")
except Exception as e:
logger.warning(f"从数据库加载全局配置失败,使用默认配置: {e}")
self._cache = {}
def get(self, key: str, default: Any = None) -> Any:
"""获取全局配置值"""
# 1. 优先从Redis缓存读取
if self._redis_connected and self._redis_client:
redis_value = self._get_from_redis(key)
if redis_value is not None:
self._cache[key] = redis_value
return redis_value
# 2. 从本地缓存读取
if key in self._cache:
return self._cache[key]
# 3. 从数据库读取
try:
from database.models import GlobalStrategyConfig
db_value = GlobalStrategyConfig.get_value(key)
if db_value is not None:
self._cache[key] = db_value
self._set_to_redis(key, db_value)
return db_value
except Exception:
pass
# 4. 从环境变量读取
env_value = os.getenv(key)
if env_value is not None:
return env_value
# 5. 返回默认值
return default
def reload_from_redis(self):
"""强制从Redis重新加载全局配置"""
if not self._redis_connected or not self._redis_client:
return
try:
self._redis_client.ping()
except Exception as e:
logger.debug(f"Redis连接不可用: {e}跳过从Redis重新加载")
self._redis_connected = False
return
try:
redis_configs = self._redis_client.hgetall(self._redis_hash_key)
if redis_configs and len(redis_configs) > 0:
self._cache = {}
for key, value_str in redis_configs.items():
self._cache[key] = ConfigManager._coerce_redis_value(value_str)
logger.debug(f"从Redis重新加载了 {len(self._cache)} 个全局配置项")
except Exception as e:
logger.debug(f"从Redis重新加载全局配置失败: {e},保持现有缓存")
class ConfigManager:
"""配置管理器 - 优先从Redis缓存读取其次从数据库读取回退到环境变量和默认值"""
_instances = {}
def __init__(self, account_id: int = 1):
self.account_id = int(account_id or 1)
self._cache = {}
self._redis_client: Optional[redis.Redis] = None
self._redis_connected = False
self._redis_hash_key = f"trading_config:{self.account_id}"
self._legacy_hash_key = "trading_config" if self.account_id == 1 else None
self._init_redis()
self._load_from_db()
@classmethod
def for_account(cls, account_id: int):
aid = int(account_id or 1)
inst = cls._instances.get(aid)
if inst:
return inst
inst = cls(account_id=aid)
cls._instances[aid] = inst
return inst
def _init_redis(self):
"""初始化Redis客户端同步"""
if not REDIS_SYNC_AVAILABLE:
@ -115,6 +375,12 @@ class ConfigManager:
ssl_cert_reqs = os.getenv('REDIS_SSL_CERT_REQS', 'required')
ssl_ca_certs = os.getenv('REDIS_SSL_CA_CERTS', None)
connection_kwargs['select'] = os.getenv('REDIS_SELECT', 0)
if connection_kwargs['select'] is not None:
connection_kwargs['select'] = int(connection_kwargs['select'])
else:
connection_kwargs['select'] = 0
logger.info(f"使用 Redis 数据库: {connection_kwargs['select']}")
# 设置SSL参数
connection_kwargs['ssl_cert_reqs'] = ssl_cert_reqs
if ssl_ca_certs:
@ -151,8 +417,10 @@ class ConfigManager:
return None
try:
# 使用Hash存储所有配置键为 trading_config:{key}
value = self._redis_client.hget('trading_config', key)
# 使用账号维度 Hash 存储所有配置
value = self._redis_client.hget(self._redis_hash_key, key)
if (value is None or value == '') and self._legacy_hash_key:
value = self._redis_client.hget(self._legacy_hash_key, key)
if value is not None and value != '':
return self._coerce_redis_value(value)
except Exception as e:
@ -217,21 +485,22 @@ class ConfigManager:
return s
def _set_to_redis(self, key: str, value: Any):
"""设置配置到Redis"""
"""设置配置到Redis(账号维度 + legacy兼容"""
if not self._redis_connected or not self._redis_client:
return False
try:
# 使用Hash存储所有配置键为 trading_config:{key}
# 将值序列化:复杂类型/基础类型使用 JSON避免 bool 被写成 "False" 字符串后逻辑误判
if isinstance(value, (dict, list, bool, int, float)):
value_str = json.dumps(value, ensure_ascii=False)
else:
value_str = str(value)
self._redis_client.hset('trading_config', key, value_str)
# 设置整个Hash的过期时间为7天配置不会频繁变化但需要定期刷新
self._redis_client.expire('trading_config', 7 * 24 * 3600)
self._redis_client.hset(self._redis_hash_key, key, value_str)
self._redis_client.expire(self._redis_hash_key, 3600)
if self._legacy_hash_key:
self._redis_client.hset(self._legacy_hash_key, key, value_str)
self._redis_client.expire(self._legacy_hash_key, 3600)
return True
except Exception as e:
logger.debug(f"设置配置到Redis失败 {key}: {e}")
@ -244,8 +513,11 @@ class ConfigManager:
value_str = json.dumps(value, ensure_ascii=False)
else:
value_str = str(value)
self._redis_client.hset('trading_config', key, value_str)
self._redis_client.expire('trading_config', 7 * 24 * 3600)
self._redis_client.hset(self._redis_hash_key, key, value_str)
self._redis_client.expire(self._redis_hash_key, 3600)
if self._legacy_hash_key:
self._redis_client.hset(self._legacy_hash_key, key, value_str)
self._redis_client.expire(self._legacy_hash_key, 3600)
return True
except:
self._redis_connected = False
@ -257,15 +529,23 @@ class ConfigManager:
return
try:
# 批量设置所有配置到Redis
# 批量设置所有配置到Redis(账号维度)
pipe = self._redis_client.pipeline()
for key, value in self._cache.items():
if isinstance(value, (dict, list, bool, int, float)):
value_str = json.dumps(value, ensure_ascii=False)
else:
value_str = str(value)
pipe.hset('trading_config', key, value_str)
pipe.expire('trading_config', 7 * 24 * 3600)
pipe.hset(self._redis_hash_key, key, value_str)
pipe.expire(self._redis_hash_key, 3600)
if self._legacy_hash_key:
for key, value in self._cache.items():
if isinstance(value, (dict, list, bool, int, float)):
value_str = json.dumps(value, ensure_ascii=False)
else:
value_str = str(value)
pipe.hset(self._legacy_hash_key, key, value_str)
pipe.expire(self._legacy_hash_key, 3600)
pipe.execute()
logger.debug(f"已将 {len(self._cache)} 个配置项同步到Redis")
except Exception as e:
@ -284,7 +564,9 @@ class ConfigManager:
try:
# 测试连接是否真正可用
self._redis_client.ping()
redis_configs = self._redis_client.hgetall('trading_config')
redis_configs = self._redis_client.hgetall(self._redis_hash_key)
if (not redis_configs) and self._legacy_hash_key:
redis_configs = self._redis_client.hgetall(self._legacy_hash_key)
if redis_configs and len(redis_configs) > 0:
# 解析Redis中的配置
for key, value_str in redis_configs.items():
@ -303,7 +585,7 @@ class ConfigManager:
self._redis_connected = False
# 从数据库加载配置仅在Redis不可用或Redis中没有数据时
configs = TradingConfig.get_all()
configs = TradingConfig.get_all(account_id=self.account_id)
for config in configs:
key = config['config_key']
value = TradingConfig._convert_value(
@ -321,6 +603,29 @@ class ConfigManager:
def get(self, key, default=None):
"""获取配置值"""
# 账号私有API Key/Secret/Testnet 从 accounts 表读取(不走 trading_config
if key in ("BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET") and Account is not None:
try:
api_key, api_secret, use_testnet, status = Account.get_credentials(self.account_id)
logger.debug(f"ConfigManager.get({key}, account_id={self.account_id}): api_key存在={bool(api_key)}, api_secret存在={bool(api_secret)}, status={status}")
if key == "BINANCE_API_KEY":
# 如果 api_key 为空字符串,返回 None 而不是 default避免返回 'your_api_key_here'
if not api_key or api_key.strip() == "":
logger.warning(f"ConfigManager.get(BINANCE_API_KEY, account_id={self.account_id}): API密钥为空字符串")
return None # 返回 None让调用方知道密钥未配置
return api_key
if key == "BINANCE_API_SECRET":
# 如果 api_secret 为空字符串,返回 None 而不是 default避免返回 'your_api_secret_here'
if not api_secret or api_secret.strip() == "":
logger.warning(f"ConfigManager.get(BINANCE_API_SECRET, account_id={self.account_id}): API密钥Secret为空字符串")
return None # 返回 None让调用方知道密钥未配置
return api_secret
return bool(use_testnet)
except Exception as e:
# 回退到后续逻辑(旧数据/无表)
logger.warning(f"ConfigManager.get({key}, account_id={self.account_id}): Account.get_credentials 失败: {e}")
pass
# 1. 优先从Redis缓存读取最新
# 注意只在Redis连接正常时尝试读取避免频繁连接失败
if self._redis_connected and self._redis_client:
@ -334,7 +639,18 @@ class ConfigManager:
if key in self._cache:
return self._cache[key]
# 3. 从环境变量读取
# 3. 从全局策略配置读取(如果账号未设置)
# API密钥等敏感信息不走全局配置
if key not in ("BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"):
try:
# GlobalStrategyConfigManager是单例开销很小
global_val = GlobalStrategyConfigManager().get(key)
if global_val is not None:
return global_val
except Exception:
pass
# 4. 从环境变量读取
env_value = os.getenv(key)
if env_value is not None:
return env_value
@ -344,6 +660,21 @@ class ConfigManager:
def set(self, key, value, config_type='string', category='general', description=None):
"""设置配置同时更新数据库、Redis缓存和本地缓存"""
# 账号私有API Key/Secret/Testnet 写入 accounts 表
if key in ("BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET") and Account is not None:
try:
if key == "BINANCE_API_KEY":
Account.update_credentials(self.account_id, api_key=str(value or ""))
elif key == "BINANCE_API_SECRET":
Account.update_credentials(self.account_id, api_secret=str(value or ""))
else:
Account.update_credentials(self.account_id, use_testnet=bool(value))
self._cache[key] = value
return
except Exception as e:
logger.error(f"更新账号API配置失败: {e}")
raise
if TradingConfig is None:
logger.warning("TradingConfig未导入无法更新数据库配置")
self._cache[key] = value
@ -353,7 +684,7 @@ class ConfigManager:
try:
# 1. 更新数据库
TradingConfig.set(key, value, config_type, category, description)
TradingConfig.set(key, value, config_type, category, description, account_id=self.account_id)
# 2. 更新本地缓存
self._cache[key] = value
@ -387,7 +718,9 @@ class ConfigManager:
return
try:
redis_configs = self._redis_client.hgetall('trading_config')
redis_configs = self._redis_client.hgetall(self._redis_hash_key)
if (not redis_configs) and self._legacy_hash_key:
redis_configs = self._redis_client.hgetall(self._legacy_hash_key)
if redis_configs and len(redis_configs) > 0:
self._cache = {} # 清空缓存
for key, value_str in redis_configs.items():
@ -406,77 +739,312 @@ class ConfigManager:
def get_trading_config(self):
"""获取交易配置字典兼容原有config.py的TRADING_CONFIG"""
return {
# 全局策略配置管理器从独立的global_strategy_config表读取
global_config_mgr = GlobalStrategyConfigManager()
try:
global_config_mgr.reload_from_redis()
except Exception:
pass
def eff_get(key: str, default: Any):
"""
执行策略合并账号用户配置优先未设置时使用全局配置只影响该账号的交易执行
- API key/secret/testnet 仅账号无全局兜底
- 其余项先读账号有则用无则用全局再无则用 default
"""
value_from_account = False
if key in ("BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"):
value = self.get(key, default)
value_from_account = True
else:
account_val = self.get(key, _MISSING)
if account_val is not _MISSING:
value = account_val
value_from_account = True
else:
try:
value = global_config_mgr.get(key, default)
except Exception:
value = default
# ⚠️ 临时兼容性处理:百分比配置值格式转换
# 如果配置值是百分比形式(>1转换为比例形式除以100
# 兼容数据库中可能存在的旧数据百分比形式如30表示30%
# 数据迁移完成后,可以移除此逻辑
# 统一格式数据库、前端、后端都使用比例形式0.30表示30%
if isinstance(value, (int, float)) and value is not None:
# 需要转换的百分比配置项
percent_keys = [
'TRAILING_STOP_ACTIVATION',
'TRAILING_STOP_PROTECT',
'LOCK_PROFIT_STAGE1_TRIGGER_PCT',
'LOCK_PROFIT_STAGE1_PCT',
'LOCK_PROFIT_STAGE2_TRIGGER_PCT',
'LOCK_PROFIT_STAGE2_PCT',
'MIN_VOLATILITY',
'TAKE_PROFIT_PERCENT',
'TAKE_PROFIT_1_PERCENT', # 分步止盈第一目标默认15%
'STOP_LOSS_PERCENT',
'MIN_STOP_LOSS_PRICE_PCT',
'MIN_TAKE_PROFIT_PRICE_PCT',
'FIXED_RISK_PERCENT',
'MAX_POSITION_PERCENT',
'MAX_TOTAL_POSITION_PERCENT',
'MIN_POSITION_PERCENT',
]
if key in percent_keys:
# 如果值>1认为是百分比形式旧数据转换为比例形式
# 静默转换,不输出警告(用户已确认数据库应存储小数形式)
if value > 1:
value = value / 100.0
# 静默更新缓存:值来自账号则写回账号,否则写回全局
try:
if value_from_account:
self._set_to_redis(key, value)
self._cache[key] = value
else:
global_config_mgr._set_to_redis(key, value)
global_config_mgr._cache[key] = value
except Exception as e:
logger.debug(f"更新Redis缓存失败不影响使用: {key} = {e}")
return value
# 交易预设:控制一组参数的“默认性格”
profile = str(eff_get('TRADING_PROFILE', 'conservative') or 'conservative').lower()
is_fast = profile in ('fast', 'fast_test', 'aggressive')
max_daily_default = 30 if is_fast else 8
scan_interval_default = 900 if is_fast else 1800
min_signal_default = 7 if is_fast else 8 # 2026-01-29优化稳健模式从9降到8平衡胜率和交易频率
cooldown_default = 900 if is_fast else 1800
allow_neutral_default = True if is_fast else False
short_filter_default = False if is_fast else True
max_trend_move_default = 0.08 if is_fast else 0.05
result = {
# 仓位控制
'MAX_POSITION_PERCENT': self.get('MAX_POSITION_PERCENT', 0.08), # 提高单笔仓位到8%
'MAX_TOTAL_POSITION_PERCENT': self.get('MAX_TOTAL_POSITION_PERCENT', 0.40), # 提高总仓位到40%
'MIN_POSITION_PERCENT': self.get('MIN_POSITION_PERCENT', 0.02), # 提高最小仓位到2%
'MIN_MARGIN_USDT': self.get('MIN_MARGIN_USDT', 5.0), # 提高最小保证金到5美元
'MAX_POSITION_PERCENT': eff_get('MAX_POSITION_PERCENT', 0.12), # 单笔最大保证金占比12%,加大单笔盈利空间)
'MAX_TOTAL_POSITION_PERCENT': eff_get('MAX_TOTAL_POSITION_PERCENT', 0.40), # 总保证金占比上限
'MIN_POSITION_PERCENT': eff_get('MIN_POSITION_PERCENT', 0.02), # 最小保证金占比
'MIN_MARGIN_USDT': eff_get('MIN_MARGIN_USDT', 2.0), # 最小保证金USDT
# 用户风险旋钮:自动交易开关/频次控制
'AUTO_TRADE_ENABLED': eff_get('AUTO_TRADE_ENABLED', True),
'MAX_OPEN_POSITIONS': eff_get('MAX_OPEN_POSITIONS', 3),
'MAX_DAILY_ENTRIES': eff_get('MAX_DAILY_ENTRIES', max_daily_default),
'SUNDAY_MAX_OPENS': eff_get('SUNDAY_MAX_OPENS', 3), # 周日开仓上限0=不限制
'SUNDAY_MIN_SIGNAL_STRENGTH': eff_get('SUNDAY_MIN_SIGNAL_STRENGTH', 8), # 周日最低信号强度0=不提高
'NIGHT_HOURS_NO_OPEN_ENABLED': eff_get('NIGHT_HOURS_NO_OPEN_ENABLED', True),
'NIGHT_HOURS_START': eff_get('NIGHT_HOURS_START', 21),
'NIGHT_HOURS_END': eff_get('NIGHT_HOURS_END', 6),
'NIGHT_HOURS_ONLY_SUNDAY': eff_get('NIGHT_HOURS_ONLY_SUNDAY', True),
'NO_OPEN_HOURS_BJ': (eff_get('NO_OPEN_HOURS_BJ', '') or '').strip(), # 禁止开仓小时(北京),逗号分隔如 "17,19,22",空则不限制
# 同步/系统单标识(全局配置,账号可覆盖)
'ONLY_AUTO_TRADE_CREATES_RECORDS': eff_get('ONLY_AUTO_TRADE_CREATES_RECORDS', True), # True=不补建「仅币安有仓」False 时配合 SYNC_RECOVER 可补建
'SYNC_RECOVER_MISSING_POSITIONS': eff_get('SYNC_RECOVER_MISSING_POSITIONS', True),
'SYNC_RECOVER_ONLY_WHEN_HAS_SLTP': eff_get('SYNC_RECOVER_ONLY_WHEN_HAS_SLTP', True),
'SYSTEM_ORDER_ID_PREFIX': eff_get('SYSTEM_ORDER_ID_PREFIX', 'SYS') or '',
# 涨跌幅阈值
'MIN_CHANGE_PERCENT': self.get('MIN_CHANGE_PERCENT', 2.0),
'TOP_N_SYMBOLS': self.get('TOP_N_SYMBOLS', 10),
'MIN_CHANGE_PERCENT': eff_get('MIN_CHANGE_PERCENT', 2.0),
# 风险控制
'STOP_LOSS_PERCENT': self.get('STOP_LOSS_PERCENT', 0.10), # 默认10%
'TAKE_PROFIT_PERCENT': self.get('TAKE_PROFIT_PERCENT', 0.30), # 默认30%盈亏比3:1
'MIN_STOP_LOSS_PRICE_PCT': self.get('MIN_STOP_LOSS_PRICE_PCT', 0.02), # 默认2%
'MIN_TAKE_PROFIT_PRICE_PCT': self.get('MIN_TAKE_PROFIT_PRICE_PCT', 0.03), # 默认3%
'USE_ATR_STOP_LOSS': self.get('USE_ATR_STOP_LOSS', True), # 是否使用ATR动态止损
'ATR_STOP_LOSS_MULTIPLIER': self.get('ATR_STOP_LOSS_MULTIPLIER', 1.8), # ATR止损倍数1.5-2倍
'ATR_TAKE_PROFIT_MULTIPLIER': self.get('ATR_TAKE_PROFIT_MULTIPLIER', 3.0), # ATR止盈倍数3倍ATR
'RISK_REWARD_RATIO': self.get('RISK_REWARD_RATIO', 3.0), # 盈亏比(止损距离的倍数)
'ATR_PERIOD': self.get('ATR_PERIOD', 14), # ATR计算周期
'USE_DYNAMIC_ATR_MULTIPLIER': self.get('USE_DYNAMIC_ATR_MULTIPLIER', False), # 是否根据波动率动态调整ATR倍数
'ATR_MULTIPLIER_MIN': self.get('ATR_MULTIPLIER_MIN', 1.5), # 动态ATR倍数最小值
'ATR_MULTIPLIER_MAX': self.get('ATR_MULTIPLIER_MAX', 2.5), # 动态ATR倍数最大值
# ⚠️ 2026-01-29优化放宽止损减少被正常波动扫出
# - 提高ATR倍数从1.5到2.0),给市场波动更多空间
# - 提高最小价格变动百分比从2%到2.5%),避免止损过紧
'STOP_LOSS_PERCENT': eff_get('STOP_LOSS_PERCENT', 0.12), # 默认12%(保证金百分比)
'TAKE_PROFIT_PERCENT': eff_get('TAKE_PROFIT_PERCENT', 0.30), # 默认30%(第二目标/单目标止盈)
'TAKE_PROFIT_1_PERCENT': eff_get('TAKE_PROFIT_1_PERCENT', 0.20), # 默认20%2026-02-12优化拉高第一目标改善盈亏比
'MIN_STOP_LOSS_PRICE_PCT': eff_get('MIN_STOP_LOSS_PRICE_PCT', 0.025), # 默认2.5%2026-01-29优化从2%提高到2.5%,给波动更多空间)
'MIN_TAKE_PROFIT_PRICE_PCT': eff_get('MIN_TAKE_PROFIT_PRICE_PCT', 0.02), # 默认2%防止ATR过小时计算出不切实际的微小止盈距离
'USE_ATR_STOP_LOSS': eff_get('USE_ATR_STOP_LOSS', True), # 是否使用ATR动态止损
'ATR_STOP_LOSS_MULTIPLIER': eff_get('ATR_STOP_LOSS_MULTIPLIER', 3.0), # ATR止损倍数3.02026-02-12优化减少噪音止损配合止盈拉远
'ATR_TAKE_PROFIT_MULTIPLIER': eff_get('ATR_TAKE_PROFIT_MULTIPLIER', 2.0), # ATR止盈倍数2.02026-01-27优化降低止盈目标更容易触发
'RISK_REWARD_RATIO': eff_get('RISK_REWARD_RATIO', 3.0), # 盈亏比3:12026-01-27优化降低更容易触发保证胜率
'ATR_PERIOD': eff_get('ATR_PERIOD', 14), # ATR计算周期
'USE_DYNAMIC_ATR_MULTIPLIER': eff_get('USE_DYNAMIC_ATR_MULTIPLIER', False), # 是否根据波动率动态调整ATR倍数
'ATR_MULTIPLIER_MIN': eff_get('ATR_MULTIPLIER_MIN', 1.5), # 动态ATR倍数最小值
'ATR_MULTIPLIER_MAX': eff_get('ATR_MULTIPLIER_MAX', 2.5), # 动态ATR倍数最大值
# 市场扫描1小时主周期
'SCAN_INTERVAL': self.get('SCAN_INTERVAL', 3600), # 1小时
'TOP_N_SYMBOLS': self.get('TOP_N_SYMBOLS', 10), # 每次扫描后处理的交易对数量
'MAX_SCAN_SYMBOLS': self.get('MAX_SCAN_SYMBOLS', 500), # 扫描的最大交易对数量0表示扫描所有
'KLINE_INTERVAL': self.get('KLINE_INTERVAL', '1h'),
'PRIMARY_INTERVAL': self.get('PRIMARY_INTERVAL', '1h'),
'CONFIRM_INTERVAL': self.get('CONFIRM_INTERVAL', '4h'),
'ENTRY_INTERVAL': self.get('ENTRY_INTERVAL', '15m'),
# 固定风险百分比仓位计算(凯利公式)
'USE_FIXED_RISK_SIZING': eff_get('USE_FIXED_RISK_SIZING', True), # 使用固定风险百分比计算仓位
'FIXED_RISK_PERCENT': eff_get('FIXED_RISK_PERCENT', 0.02), # 每笔单子承受的风险2%
# 仓位放大系数1.0=正常1.2=+20% 仓位,上限 2.0,仍受 MAX_POSITION_PERCENT 约束(盈利时适度放大用)
'POSITION_SCALE_FACTOR': eff_get('POSITION_SCALE_FACTOR', 1.0),
# 市场扫描30分钟主周期
'SCAN_INTERVAL': eff_get('SCAN_INTERVAL', scan_interval_default), # 30分钟增加交易机会
'TOP_N_SYMBOLS': eff_get('TOP_N_SYMBOLS', 20), # 每次扫描后优先处理的交易对数量
'SCAN_EXTRA_SYMBOLS_FOR_SUPPLEMENT': eff_get('SCAN_EXTRA_SYMBOLS_FOR_SUPPLEMENT', 15), # 智能补单:多返回的候选数量,冷却时仍可尝试后续交易对
'MAX_SCAN_SYMBOLS': eff_get('MAX_SCAN_SYMBOLS', 500), # 扫描的最大交易对数量增加到500
'EXCLUDE_MAJOR_COINS': eff_get('EXCLUDE_MAJOR_COINS', True), # 是否排除主流币BTC、ETH、BNB等专注于山寨币
'KLINE_INTERVAL': eff_get('KLINE_INTERVAL', '1h'),
'PRIMARY_INTERVAL': eff_get('PRIMARY_INTERVAL', '1h'),
'CONFIRM_INTERVAL': eff_get('CONFIRM_INTERVAL', '4h'),
'ENTRY_INTERVAL': eff_get('ENTRY_INTERVAL', '15m'),
# 过滤条件
'MIN_VOLUME_24H': self.get('MIN_VOLUME_24H', 10000000),
'MIN_VOLATILITY': self.get('MIN_VOLATILITY', 0.02),
'MIN_VOLUME_24H': eff_get('MIN_VOLUME_24H', 10000000),
'MIN_VOLATILITY': eff_get('MIN_VOLATILITY', 0.02),
# 高胜率策略参数
'MIN_SIGNAL_STRENGTH': self.get('MIN_SIGNAL_STRENGTH', 5),
'LEVERAGE': self.get('LEVERAGE', 10),
'USE_DYNAMIC_LEVERAGE': self.get('USE_DYNAMIC_LEVERAGE', True),
'MAX_LEVERAGE': self.get('MAX_LEVERAGE', 15), # 降低到15更保守配合更大的保证金
'USE_TRAILING_STOP': self.get('USE_TRAILING_STOP', True),
'TRAILING_STOP_ACTIVATION': self.get('TRAILING_STOP_ACTIVATION', 0.10), # 默认10%(给趋势更多空间)
'TRAILING_STOP_PROTECT': self.get('TRAILING_STOP_PROTECT', 0.05), # 默认5%(保护更多利润)
# ⚠️ 2026-01-29优化提高信号强度门槛稳健模式从9到8减少低质量信号提升胜率
'MIN_SIGNAL_STRENGTH': eff_get('MIN_SIGNAL_STRENGTH', min_signal_default), # 默认值随 profile 调整快速模式7稳健模式8
'LEVERAGE': eff_get('LEVERAGE', 10),
'USE_DYNAMIC_LEVERAGE': eff_get('USE_DYNAMIC_LEVERAGE', True),
'MAX_LEVERAGE': eff_get('MAX_LEVERAGE', 20), # 动态杠杆上限 20配合单笔仓位提高收益
'MIN_LEVERAGE': eff_get('MIN_LEVERAGE', 8), # 动态杠杆下限,不低于此值(之前盈利阶段多为 8x避免被压到 24x 导致单笔盈利过少)
'MAX_LEVERAGE_SMALL_CAP': eff_get('MAX_LEVERAGE_SMALL_CAP', 8), # 高波动/小众币最大杠杆,默认 8 与之前盈利阶段一致
# 盈利保护总开关与保本:关闭后不执行保本、不执行移动止损
'PROFIT_PROTECTION_ENABLED': eff_get('PROFIT_PROTECTION_ENABLED', True), # True=启用保本+移动止损False=全部关闭
'LOCK_PROFIT_AT_BREAKEVEN_AFTER_PCT': eff_get('LOCK_PROFIT_AT_BREAKEVEN_AFTER_PCT', 0.03), # 盈利达保证金比例时移至保本0.03=3%0=关闭)
'LOCK_PROFIT_STAGE1_TRIGGER_PCT': eff_get('LOCK_PROFIT_STAGE1_TRIGGER_PCT', 0.08), # 盈利达该比例后进入第一层锁盈
'LOCK_PROFIT_STAGE1_PCT': eff_get('LOCK_PROFIT_STAGE1_PCT', 0.02), # 第一层锁住的利润比例
'LOCK_PROFIT_STAGE2_TRIGGER_PCT': eff_get('LOCK_PROFIT_STAGE2_TRIGGER_PCT', 0.15), # 盈利达该比例后进入第二层锁盈
'LOCK_PROFIT_STAGE2_PCT': eff_get('LOCK_PROFIT_STAGE2_PCT', 0.05), # 第二层锁住的利润比例
# 移动止损
'USE_TRAILING_STOP': eff_get('USE_TRAILING_STOP', True), # 默认启用2026-01-27优化启用移动止损保护利润
'TRAILING_STOP_ACTIVATION': eff_get('TRAILING_STOP_ACTIVATION', 0.05), # 默认5%2026-01-27优化更早保护利润避免回吐
'TRAILING_STOP_PROTECT': eff_get('TRAILING_STOP_PROTECT', 0.025), # 默认2.5%2026-01-27优化给回撤足够空间避免被震荡扫出
# 最小持仓时间锁(强制波段持仓纪律,避免分钟级平仓)
'MIN_HOLD_TIME_SEC': eff_get('MIN_HOLD_TIME_SEC', 1800), # 默认30分钟1800秒
# 自动交易过滤(用于提升胜率/控频)
# 说明:这两个 key 需要出现在 TRADING_CONFIG 中,否则 trading_system 在每次 reload_from_redis 后会丢失它们,
# 导致始终按默认值拦截自动交易(用户在配置页怎么开都没用)。
'AUTO_TRADE_ONLY_TRENDING': self.get('AUTO_TRADE_ONLY_TRENDING', True),
'AUTO_TRADE_ALLOW_4H_NEUTRAL': self.get('AUTO_TRADE_ALLOW_4H_NEUTRAL', False),
'AUTO_TRADE_ONLY_TRENDING': eff_get('AUTO_TRADE_ONLY_TRENDING', True),
'AUTO_TRADE_ALLOW_RANGING': eff_get('AUTO_TRADE_ALLOW_RANGING', False),
'AUTO_TRADE_ALLOW_UNKNOWN': eff_get('AUTO_TRADE_ALLOW_UNKNOWN', False),
'AUTO_TRADE_ALLOW_4H_NEUTRAL': eff_get('AUTO_TRADE_ALLOW_4H_NEUTRAL', allow_neutral_default),
'RANGING_MARKET_SIGNAL_BOOST': eff_get('RANGING_MARKET_SIGNAL_BOOST', 2),
# 智能入场/限价偏移(部分逻辑会直接读取 TRADING_CONFIG
'LIMIT_ORDER_OFFSET_PCT': self.get('LIMIT_ORDER_OFFSET_PCT', 0.5),
'SMART_ENTRY_ENABLED': self.get('SMART_ENTRY_ENABLED', False),
'SMART_ENTRY_STRONG_SIGNAL': self.get('SMART_ENTRY_STRONG_SIGNAL', 8),
'ENTRY_SYMBOL_COOLDOWN_SEC': self.get('ENTRY_SYMBOL_COOLDOWN_SEC', 120),
'ENTRY_TIMEOUT_SEC': self.get('ENTRY_TIMEOUT_SEC', 180),
'ENTRY_STEP_WAIT_SEC': self.get('ENTRY_STEP_WAIT_SEC', 15),
'ENTRY_CHASE_MAX_STEPS': self.get('ENTRY_CHASE_MAX_STEPS', 4),
'ENTRY_MARKET_FALLBACK_AFTER_SEC': self.get('ENTRY_MARKET_FALLBACK_AFTER_SEC', 45),
'ENTRY_CONFIRM_TIMEOUT_SEC': self.get('ENTRY_CONFIRM_TIMEOUT_SEC', 30),
'ENTRY_MAX_DRIFT_PCT_TRENDING': self.get('ENTRY_MAX_DRIFT_PCT_TRENDING', 0.6),
'ENTRY_MAX_DRIFT_PCT_RANGING': self.get('ENTRY_MAX_DRIFT_PCT_RANGING', 0.3),
'LIMIT_ORDER_OFFSET_PCT': eff_get('LIMIT_ORDER_OFFSET_PCT', 0.5),
'SMART_ENTRY_ENABLED': eff_get('SMART_ENTRY_ENABLED', False),
'SMART_ENTRY_STRONG_SIGNAL': eff_get('SMART_ENTRY_STRONG_SIGNAL', min_signal_default),
'ENTRY_SYMBOL_COOLDOWN_SEC': eff_get('ENTRY_SYMBOL_COOLDOWN_SEC', cooldown_default),
'ENTRY_TIMEOUT_SEC': eff_get('ENTRY_TIMEOUT_SEC', 180),
'ENTRY_STEP_WAIT_SEC': eff_get('ENTRY_STEP_WAIT_SEC', 15),
'ENTRY_CHASE_MAX_STEPS': eff_get('ENTRY_CHASE_MAX_STEPS', 4),
'ENTRY_MARKET_FALLBACK_AFTER_SEC': eff_get('ENTRY_MARKET_FALLBACK_AFTER_SEC', 45),
'ENTRY_CONFIRM_TIMEOUT_SEC': eff_get('ENTRY_CONFIRM_TIMEOUT_SEC', 30),
'ENTRY_MAX_DRIFT_PCT_TRENDING': eff_get('ENTRY_MAX_DRIFT_PCT_TRENDING', 0.006),
'ENTRY_MAX_DRIFT_PCT_RANGING': eff_get('ENTRY_MAX_DRIFT_PCT_RANGING', 0.3),
# Algo 条件单(止损/止盈)单次请求超时(秒),币安接口高负载时易超时,网络不稳可调大至 60
'ALGO_ORDER_TIMEOUT_SEC': eff_get('ALGO_ORDER_TIMEOUT_SEC', 45),
# 持仓详细监控日志开关(用于排查问题时观察每次检查的当前价/目标价/ROE 等)
'POSITION_DETAILED_LOG_ENABLED': eff_get('POSITION_DETAILED_LOG_ENABLED', False),
# 动态过滤优化
'BETA_FILTER_ENABLED': eff_get('BETA_FILTER_ENABLED', True), # 大盘共振过滤BTC/ETH下跌时屏蔽多单
'BETA_FILTER_THRESHOLD': eff_get('BETA_FILTER_THRESHOLD', -0.005), # -0.5%2026-01-27优化更敏感地过滤大盘风险15分钟内跌幅超过0.5%即屏蔽多单)
# RSI / 24h 涨跌幅过滤(避免追高杀跌)
'MAX_RSI_FOR_LONG': eff_get('MAX_RSI_FOR_LONG', 65), # 做多时 RSI 超过此值则不开多2026-02-1265 避免追高)
'MAX_CHANGE_PERCENT_FOR_LONG': eff_get('MAX_CHANGE_PERCENT_FOR_LONG', 25), # 做多时 24h 涨跌幅超过此值则不开多
'MIN_RSI_FOR_SHORT': eff_get('MIN_RSI_FOR_SHORT', 30), # 做空时 RSI 低于此值则不做空
'MAX_CHANGE_PERCENT_FOR_SHORT': eff_get('MAX_CHANGE_PERCENT_FOR_SHORT', 10), # 做空时 24h 涨跌幅超过此值则不做空
# RSI 极限反转(与盈利期对齐:关闭可避免趋势里逆势止损)
'RSI_EXTREME_REVERSE_ENABLED': eff_get('RSI_EXTREME_REVERSE_ENABLED', False),
'RSI_EXTREME_REVERSE_ONLY_NEUTRAL_4H': eff_get('RSI_EXTREME_REVERSE_ONLY_NEUTRAL_4H', True),
# 止盈/止损按保证金封顶(避免 TP 过远、SL 过宽扛单)
'USE_MARGIN_CAP_FOR_TP': eff_get('USE_MARGIN_CAP_FOR_TP', True),
'USE_MARGIN_CAP_FOR_SL': eff_get('USE_MARGIN_CAP_FOR_SL', True),
# 趋势尾部入场过滤 & 15m 短周期方向过滤开关(由 profile 控制默认值)
'ENTRY_SHORT_INTERVAL': eff_get('ENTRY_SHORT_INTERVAL', '15m'),
'ENTRY_SHORT_TREND_FILTER_ENABLED': eff_get('ENTRY_SHORT_TREND_FILTER_ENABLED', short_filter_default),
'ENTRY_SHORT_TREND_MIN_PCT': eff_get('ENTRY_SHORT_TREND_MIN_PCT', 0.003),
'ENTRY_SHORT_CONFIRM_CANDLES': eff_get('ENTRY_SHORT_CONFIRM_CANDLES', 3),
'USE_TREND_ENTRY_FILTER': eff_get('USE_TREND_ENTRY_FILTER', True),
# ⚠️ 2026-01-29优化收紧趋势尾部过滤稳健模式从0.05到0.04),更严格避免追高杀跌
'MAX_TREND_MOVE_BEFORE_ENTRY': eff_get('MAX_TREND_MOVE_BEFORE_ENTRY', max_trend_move_default), # 快速模式0.08稳健模式0.04
'TREND_STATE_TTL_SEC': eff_get('TREND_STATE_TTL_SEC', 3600),
'RECO_USE_TREND_ENTRY_FILTER': eff_get('RECO_USE_TREND_ENTRY_FILTER', True),
'RECO_MAX_TREND_MOVE_BEFORE_ENTRY': eff_get('RECO_MAX_TREND_MOVE_BEFORE_ENTRY', 0.04),
# 当前交易预设(让 trading_system 能知道是哪种模式)
'TRADING_PROFILE': profile,
# ⚠️ 2026-01-29新增同一交易对连续亏损过滤避免连续亏损后继续交易
'SYMBOL_LOSS_COOLDOWN_ENABLED': eff_get('SYMBOL_LOSS_COOLDOWN_ENABLED', True),
'SYMBOL_MAX_CONSECUTIVE_LOSSES': eff_get('SYMBOL_MAX_CONSECUTIVE_LOSSES', 2),
'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 倍止损距离,改善盈亏比
# 市场状态方案(便于在不同行情间切换)
'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 下跌时禁止开多(熊市/保守用)
'BLOCK_SHORT_WHEN_4H_UP': eff_get('BLOCK_SHORT_WHEN_4H_UP', True), # 4H 上涨时禁止开空(默认 True避免逆势做空
# 全局市场方案下禁空/禁多:牛市不推空单、熊市不推多单
'BLOCK_SHORT_WHEN_BULL_MARKET': eff_get('BLOCK_SHORT_WHEN_BULL_MARKET', True), # 市场方案=牛市时禁止开空
'BLOCK_LONG_WHEN_BEAR_MARKET': eff_get('BLOCK_LONG_WHEN_BEAR_MARKET', True), # 市场方案=熊市时禁止开多
}
# 根据市场方案覆盖关键参数(便于快速切换熊市/牛市/保守等预设)
_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,
'BLOCK_SHORT_WHEN_4H_UP': True, # 4H 上涨不开空
},
'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 下跌不开多
'BLOCK_SHORT_WHEN_4H_UP': 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,
'BLOCK_SHORT_WHEN_4H_UP': True, # 4H 上涨不开空(牛市尤需)
},
'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,
'BLOCK_SHORT_WHEN_4H_UP': 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
# 全局配置管理器实例
config_manager = ConfigManager()
def _sync_to_redis(self):
"""将配置同步到Redis缓存账号维度"""
if not self._redis_connected or not self._redis_client:
return
try:
payload = {k: json.dumps(v) for k, v in self._cache.items()}
self._redis_client.hset(self._redis_hash_key, mapping=payload)
self._redis_client.expire(self._redis_hash_key, 3600)
if self._legacy_hash_key:
self._redis_client.hset(self._legacy_hash_key, mapping=payload)
self._redis_client.expire(self._legacy_hash_key, 3600)
except Exception as e:
logger.debug(f"同步配置到Redis失败: {e}")
# 全局配置管理器实例默认账号trading_system 进程可通过 ATS_ACCOUNT_ID 指定)
try:
_default_account_id = int(os.getenv("ATS_ACCOUNT_ID") or os.getenv("ACCOUNT_ID") or 1)
except Exception:
_default_account_id = 1
config_manager = ConfigManager.for_account(_default_account_id)
# 兼容原有config.py的接口
def get_config(key, default=None):

View File

@ -0,0 +1,31 @@
-- 登录与权限系统迁移脚本(在已有库上执行一次)
-- 目标:
-- 1) 新增 users 表(管理员/普通用户)
-- 2) 新增 user_account_memberships 表(用户可访问哪些交易账号)
--
-- 执行前建议备份数据库。
USE `auto_trade_sys`;
CREATE TABLE IF NOT EXISTS `users` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(64) NOT NULL,
`password_hash` VARCHAR(255) NOT NULL,
`role` VARCHAR(20) NOT NULL DEFAULT 'user' COMMENT 'admin, user',
`status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'active, disabled',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='登录用户';
CREATE TABLE IF NOT EXISTS `user_account_memberships` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`user_id` INT NOT NULL,
`account_id` INT NOT NULL,
`role` VARCHAR(20) NOT NULL DEFAULT 'viewer' COMMENT 'owner, viewer',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `uk_user_account` (`user_id`, `account_id`),
INDEX `idx_user_id` (`user_id`),
INDEX `idx_account_id` (`account_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户-交易账号授权';

View File

@ -0,0 +1,56 @@
-- 币安订单/成交同步表,供定时任务拉取后存储,数据管理从 DB 查询分析
-- 执行: mysql -u user -p db_name < add_binance_sync_tables.sql
USE `auto_trade_sys`;
-- 币安成交记录userTrades
CREATE TABLE IF NOT EXISTS `binance_trades` (
`id` BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
`account_id` INT UNSIGNED NOT NULL,
`symbol` VARCHAR(32) NOT NULL,
`trade_id` BIGINT UNSIGNED NOT NULL COMMENT '币安 trade id',
`order_id` BIGINT UNSIGNED NOT NULL,
`side` VARCHAR(10) NOT NULL,
`position_side` VARCHAR(10) DEFAULT NULL,
`price` DECIMAL(24, 8) NOT NULL,
`qty` DECIMAL(24, 8) NOT NULL,
`quote_qty` DECIMAL(24, 8) DEFAULT NULL,
`realized_pnl` DECIMAL(24, 8) DEFAULT NULL,
`commission` DECIMAL(24, 8) DEFAULT NULL,
`commission_asset` VARCHAR(20) DEFAULT NULL,
`buyer` TINYINT(1) DEFAULT NULL,
`maker` TINYINT(1) DEFAULT NULL,
`trade_time` BIGINT UNSIGNED NOT NULL COMMENT '成交时间戳毫秒',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `uk_account_trade` (`account_id`, `trade_id`),
INDEX `idx_account_time` (`account_id`, `trade_time`),
INDEX `idx_symbol_time` (`account_id`, `symbol`, `trade_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='币安成交记录(定时同步)';
-- 币安订单记录allOrders
CREATE TABLE IF NOT EXISTS `binance_orders` (
`id` BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
`account_id` INT UNSIGNED NOT NULL,
`symbol` VARCHAR(32) NOT NULL,
`order_id` BIGINT UNSIGNED NOT NULL,
`client_order_id` VARCHAR(64) DEFAULT NULL,
`side` VARCHAR(10) NOT NULL,
`type` VARCHAR(32) DEFAULT NULL,
`orig_type` VARCHAR(32) DEFAULT NULL,
`status` VARCHAR(32) NOT NULL,
`price` DECIMAL(24, 8) DEFAULT NULL,
`avg_price` DECIMAL(24, 8) DEFAULT NULL,
`orig_qty` DECIMAL(24, 8) DEFAULT NULL,
`executed_qty` DECIMAL(24, 8) DEFAULT NULL,
`cum_qty` DECIMAL(24, 8) DEFAULT NULL,
`cum_quote` DECIMAL(24, 8) DEFAULT NULL,
`stop_price` DECIMAL(24, 8) DEFAULT NULL,
`reduce_only` TINYINT(1) DEFAULT NULL,
`position_side` VARCHAR(10) DEFAULT NULL,
`order_time` BIGINT UNSIGNED NOT NULL COMMENT '下单时间戳毫秒',
`update_time` BIGINT UNSIGNED DEFAULT NULL COMMENT '更新时间戳毫秒',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `uk_account_order` (`account_id`, `order_id`),
INDEX `idx_account_time` (`account_id`, `order_time`),
INDEX `idx_symbol_time` (`account_id`, `symbol`, `order_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='币安订单记录(定时同步)';

View File

@ -0,0 +1,5 @@
-- 为 trades 表增加「自定义订单号」字段,用于存储币安 clientOrderId便于在订单记录中核对系统单
-- 若已存在该列可跳过本句
ALTER TABLE trades ADD COLUMN client_order_id VARCHAR(64) NULL COMMENT '币安自定义订单号 clientOrderId系统单格式: 前缀_时间戳_随机' AFTER entry_order_id;
-- 可选:为按自定义订单号查询建索引(若已存在可跳过)
-- CREATE INDEX idx_client_order_id ON trades (client_order_id);

View File

@ -0,0 +1,20 @@
-- 为 trades 表增加 created_at创建时间字段仅当不存在时
-- 用于持仓/订单展示「开仓时间」时至少有创建时间可显示;与 init.sql 中定义一致。
-- MySQL 5.7+:通过 procedure 判断后添加,避免重复执行报错
DELIMITER //
DROP PROCEDURE IF EXISTS add_created_at_to_trades_if_missing//
CREATE PROCEDURE add_created_at_to_trades_if_missing()
BEGIN
IF (SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'trades' AND COLUMN_NAME = 'created_at') = 0 THEN
ALTER TABLE trades
ADD COLUMN created_at INT UNSIGNED NULL COMMENT '创建时间Unix时间戳秒数' AFTER status;
UPDATE trades SET created_at = COALESCE(entry_time, UNIX_TIMESTAMP()) WHERE created_at IS NULL;
ALTER TABLE trades
MODIFY COLUMN created_at INT UNSIGNED NOT NULL DEFAULT (UNIX_TIMESTAMP()) COMMENT '创建时间Unix时间戳秒数';
END IF;
END//
DELIMITER ;
CALL add_created_at_to_trades_if_missing();
DROP PROCEDURE IF EXISTS add_created_at_to_trades_if_missing;

View File

@ -0,0 +1,21 @@
-- 为 trades 表添加「入场思路/过程」字段,便于事后分析策略执行效果
-- 存储 JSONsignal_strength, market_regime, trend_4h, change_percent, rsi, reason, volume_confirmed 等
-- 使用动态 SQL 检查列是否存在(兼容已有库)
SET @column_exists = (
SELECT COUNT(*)
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'trades'
AND column_name = 'entry_context'
);
SET @sql = IF(@column_exists = 0,
'ALTER TABLE `trades` ADD COLUMN `entry_context` JSON NULL COMMENT ''入场时的思路与过程(信号强度、市场状态、趋势、过滤通过情况等),便于综合分析策略执行效果'' AFTER `entry_reason`',
'SELECT "entry_context 列已存在,跳过添加" AS message'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SELECT 'Migration completed: entry_context added to trades (if not exists).' AS result;

View File

@ -0,0 +1,45 @@
-- 创建全局策略配置表(独立于账户)
-- 全局配置不依赖任何account_id由管理员统一管理
CREATE TABLE IF NOT EXISTS `global_strategy_config` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`config_key` VARCHAR(100) NOT NULL,
`config_value` TEXT NOT NULL,
`config_type` VARCHAR(50) NOT NULL COMMENT 'string, number, boolean, json',
`category` VARCHAR(50) NOT NULL COMMENT 'strategy, risk, scan',
`description` TEXT,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`updated_by` VARCHAR(50) COMMENT '更新人(用户名)',
INDEX `idx_category` (`category`),
UNIQUE KEY `uk_config_key` (`config_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='全局策略配置表(管理员专用)';
-- 迁移现有account_id=1的核心策略配置到全局配置表
-- 注意:只迁移非风险旋钮的配置
INSERT INTO `global_strategy_config` (`config_key`, `config_value`, `config_type`, `category`, `description`)
SELECT
`config_key`,
`config_value`,
`config_type`,
`category`,
`description`
FROM `trading_config`
WHERE `account_id` = 1
AND `config_key` NOT IN (
'MIN_MARGIN_USDT',
'MIN_POSITION_PERCENT',
'MAX_POSITION_PERCENT',
'MAX_TOTAL_POSITION_PERCENT',
'AUTO_TRADE_ENABLED',
'MAX_OPEN_POSITIONS',
'MAX_DAILY_ENTRIES',
'BINANCE_API_KEY',
'BINANCE_API_SECRET',
'USE_TESTNET'
)
ON DUPLICATE KEY UPDATE
`config_value` = VALUES(`config_value`),
`config_type` = VALUES(`config_type`),
`category` = VALUES(`category`),
`description` = VALUES(`description`),
`updated_at` = CURRENT_TIMESTAMP;

View File

@ -0,0 +1,13 @@
-- 市场缓存表:存放较固定的交易所数据(交易对信息、资金费率规则等),减少 API 调用
-- 执行: mysql -u root -p auto_trade_sys < add_market_cache.sql
USE `auto_trade_sys`;
CREATE TABLE IF NOT EXISTS `market_cache` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`cache_key` VARCHAR(128) NOT NULL COMMENT '如 exchange_info, funding_info',
`cache_value` LONGTEXT NOT NULL COMMENT 'JSON 内容',
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `uk_cache_key` (`cache_key`),
INDEX `idx_updated_at` (`updated_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='市场数据缓存(交易对/费率规则等)';

View File

@ -0,0 +1,91 @@
-- 多账号迁移脚本(在已有库上执行一次)
-- 目标:
-- 1) 新增 accounts 表(存加密后的 API KEY/SECRET
-- 2) trading_config/trades/account_snapshots 增加 account_id默认=1
-- 3) trading_config 的唯一约束从 config_key 改为 (account_id, config_key)
--
-- ⚠️ 注意:
-- - 不同 MySQL 版本对 "ADD COLUMN IF NOT EXISTS" 支持不一致,因此这里用 INFORMATION_SCHEMA + 动态SQL。
-- - 执行前建议先备份数据库。
USE `auto_trade_sys`;
-- 1) accounts 表
CREATE TABLE IF NOT EXISTS `accounts` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
`status` VARCHAR(20) DEFAULT 'active' COMMENT 'active, disabled',
`api_key_enc` TEXT NULL COMMENT '加密后的 API KEYenc:v1:...',
`api_secret_enc` TEXT NULL COMMENT '加密后的 API SECRETenc:v1:...',
`use_testnet` BOOLEAN DEFAULT FALSE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账号表(多账号)';
INSERT INTO `accounts` (`id`, `name`, `status`, `use_testnet`)
VALUES (1, 'default', 'active', false)
ON DUPLICATE KEY UPDATE `name`=VALUES(`name`);
-- 2) trading_config.account_id
SET @has_col := (
SELECT COUNT(1)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'trading_config'
AND COLUMN_NAME = 'account_id'
);
SET @sql := IF(@has_col = 0, 'ALTER TABLE trading_config ADD COLUMN account_id INT NOT NULL DEFAULT 1 AFTER id', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 3) trades.account_id
SET @has_col := (
SELECT COUNT(1)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'trades'
AND COLUMN_NAME = 'account_id'
);
SET @sql := IF(@has_col = 0, 'ALTER TABLE trades ADD COLUMN account_id INT NOT NULL DEFAULT 1 AFTER id', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 4) account_snapshots.account_id
SET @has_col := (
SELECT COUNT(1)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'account_snapshots'
AND COLUMN_NAME = 'account_id'
);
SET @sql := IF(@has_col = 0, 'ALTER TABLE account_snapshots ADD COLUMN account_id INT NOT NULL DEFAULT 1 AFTER id', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 5) trading_config 唯一键:改为 (account_id, config_key)
-- 尝试删除旧 UNIQUE(config_key)(名字可能是 config_key 或其他)
SET @idx_name := (
SELECT INDEX_NAME
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'trading_config'
AND NON_UNIQUE = 0
AND COLUMN_NAME = 'config_key'
LIMIT 1
);
SET @sql := IF(@idx_name IS NOT NULL, CONCAT('ALTER TABLE trading_config DROP INDEX ', @idx_name), 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 添加新唯一键(如果不存在)
SET @has_uk := (
SELECT COUNT(1)
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'trading_config'
AND INDEX_NAME = 'uk_account_config_key'
);
SET @sql := IF(@has_uk = 0, 'ALTER TABLE trading_config ADD UNIQUE KEY uk_account_config_key (account_id, config_key)', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6) 索引(可选,老版本 MySQL 不支持 IF NOT EXISTS可忽略报错后手动检查
-- 如果你看到 “Duplicate key name” 可直接忽略。
CREATE INDEX idx_trades_account_id ON trades(account_id);
CREATE INDEX idx_account_snapshots_account_id ON account_snapshots(account_id);

View File

@ -0,0 +1,11 @@
-- 可选:订单类型字段,便于统计与策略分析(开仓/平仓方式)
-- 执行前请确认表已存在;若列已存在可跳过
-- 开仓订单类型LIMIT / MARKET 等(来自币安订单 type
ALTER TABLE trades ADD COLUMN IF NOT EXISTS entry_order_type VARCHAR(32) NULL COMMENT '开仓订单类型 LIMIT/MARKET' AFTER client_order_id;
-- 平仓订单类型MARKET / STOP_MARKET / TAKE_PROFIT_MARKET 等(便于区分市价平、止损、止盈)
ALTER TABLE trades ADD COLUMN IF NOT EXISTS exit_order_type VARCHAR(32) NULL COMMENT '平仓订单类型' AFTER exit_order_id;
-- 来源口径:仅自动下单入 DB 时可固定为 auto_trade预留便于扩展
-- ALTER TABLE trades ADD COLUMN IF NOT EXISTS source VARCHAR(32) NULL DEFAULT 'auto_trade' COMMENT '记录来源 auto_trade' AFTER entry_reason;

View File

@ -0,0 +1,42 @@
-- 分步止盈状态细分添加新的exit_reason值支持
-- 执行时间2026-01-27
-- 1. 更新exit_reason字段注释说明新的状态值
ALTER TABLE `trades` MODIFY COLUMN `exit_reason` VARCHAR(50)
COMMENT '平仓原因: manual(手动), stop_loss(止损), take_profit(单次止盈), trailing_stop(移动止损), sync(同步), take_profit_partial_then_take_profit(第一目标止盈后第二目标止盈), take_profit_partial_then_stop(第一目标止盈后剩余仓位止损), take_profit_partial_then_trailing_stop(第一目标止盈后剩余仓位移动止损)';
-- 2. 验证字段长度是否足够VARCHAR(50)应该足够)
SELECT
COLUMN_NAME,
COLUMN_TYPE,
COLUMN_COMMENT
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'trades'
AND COLUMN_NAME = 'exit_reason';
-- 3. 查看当前exit_reason的分布情况用于验证
SELECT
exit_reason,
COUNT(*) as count,
ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM trades WHERE status = 'closed'), 2) as percentage
FROM
trades
WHERE
status = 'closed'
GROUP BY
exit_reason
ORDER BY
count DESC;
-- 说明:
-- 新的状态值:
-- - take_profit_partial_then_take_profit: 第一目标止盈50%仓位)后,剩余仓位第二目标止盈
-- - take_profit_partial_then_stop: 第一目标止盈50%仓位)后,剩余仓位止损(保本)
-- - take_profit_partial_then_trailing_stop: 第一目标止盈50%仓位)后,剩余仓位移动止损
--
-- 这些状态用于更准确地统计胜率和盈亏比:
-- - 第一目标止盈后剩余仓位止损,应该算作"部分成功"(第一目标已达成)
-- - 第一目标止盈后剩余仓位第二目标止盈,应该算作"完整成功"

View File

@ -0,0 +1,4 @@
ALTER TABLE trades ADD COLUMN IF NOT EXISTS realized_pnl DECIMAL(20, 8) DEFAULT NULL COMMENT '币安实际结算盈亏(包含资金费率等)';
ALTER TABLE trades ADD COLUMN IF NOT EXISTS commission DECIMAL(20, 8) DEFAULT NULL COMMENT '交易手续费USDT计价';
ALTER TABLE trades ADD COLUMN IF NOT EXISTS commission_asset VARCHAR(10) DEFAULT NULL COMMENT '手续费币种BNB/USDT';

View File

@ -0,0 +1,23 @@
-- 清理「非交易系统下单」的交易记录(无开仓订单号的记录)
-- 本系统开仓会在成交后保存 entry_order_id无该字段或为 0 的为同步补录/其它来源,可安全删除。
-- 执行前请先备份数据库或至少备份 trades 表。
-- 若表结构较旧、没有 entry_order_id 列,请先执行 add_order_ids.sql 或跳过本脚本。
-- 1) 查看将要删除的记录数(按账号)
SELECT account_id, status, COUNT(*) AS cnt
FROM trades
WHERE entry_order_id IS NULL OR entry_order_id = 0
GROUP BY account_id, status
ORDER BY account_id, status;
-- 2) 查看将要删除的总数
SELECT COUNT(*) AS will_delete FROM trades
WHERE entry_order_id IS NULL OR entry_order_id = 0;
-- 3) 确认无误后执行删除建议先备份mysqldump -u user -p db_name trades > trades_backup.sql
-- DELETE FROM trades
-- WHERE entry_order_id IS NULL OR entry_order_id = 0;
-- 若只清理指定账号,可加上条件,例如:
-- DELETE FROM trades
-- WHERE (entry_order_id IS NULL OR entry_order_id = 0) AND account_id = 1;

View File

@ -6,6 +6,7 @@ from contextlib import contextmanager
import os
import logging
from pathlib import Path
from sqlalchemy import create_engine, pool
logger = logging.getLogger(__name__)
@ -41,46 +42,103 @@ except Exception as e:
class Database:
"""数据库连接类"""
"""数据库连接类使用SQLAlchemy连接池"""
_engine = None
def __init__(self):
self.host = os.getenv('DB_HOST', 'localhost')
self.port = int(os.getenv('DB_PORT', 3306))
self.user = os.getenv('DB_USER', 'root')
self.password = os.getenv('DB_PASSWORD', '')
self.database = os.getenv('DB_NAME', 'auto_trade_sys')
self.database = os.getenv('DB_NAME', 'auto_trade_sys_new')
# 记录配置信息(不显示密码)
logger.debug(f"数据库配置: host={self.host}, port={self.port}, user={self.user}, database={self.database}")
# 初始化连接池
self._init_engine()
def _init_engine(self):
"""初始化SQLAlchemy引擎和连接池"""
if Database._engine is None:
# 构建数据库URL
# 注意:密码中如果有特殊字符需要转义,这里简单处理
from urllib.parse import quote_plus
encoded_password = quote_plus(self.password)
db_url = f"mysql+pymysql://{self.user}:{encoded_password}@{self.host}:{self.port}/{self.database}?charset=utf8mb4"
try:
Database._engine = create_engine(
db_url,
pool_size=20, # 基础连接池大小
max_overflow=30, # 最大溢出连接数
pool_recycle=3600, # 连接回收时间(秒)
pool_timeout=30, # 获取连接超时时间(秒)
pool_pre_ping=True, # 预检测连接是否可用
connect_args={
# 'cursorclass': pymysql.cursors.DictCursor, # Removed to prevent KeyError: 0 in SQLAlchemy init
'autocommit': False
}
)
logger.info("数据库连接池初始化成功")
except Exception as e:
logger.error(f"数据库连接池初始化失败: {e}")
raise
@contextmanager
def get_connection(self):
"""获取数据库连接(上下文管理器)"""
"""获取数据库连接(从连接池"""
conn = None
try:
conn = pymysql.connect(
host=self.host,
port=self.port,
user=self.user,
password=self.password,
database=self.database,
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor,
autocommit=False
)
# 获取原始pymysql连接
conn = Database._engine.raw_connection()
# Explicitly set cursor class to DictCursor since we removed it from create_engine
# We need to set it on the underlying DBAPI connection
try:
if hasattr(conn, 'driver_connection'):
# SQLAlchemy 2.0+
conn.driver_connection.cursorclass = pymysql.cursors.DictCursor
elif hasattr(conn, 'connection'):
# Older SQLAlchemy
conn.connection.cursorclass = pymysql.cursors.DictCursor
else:
# Fallback
conn.cursorclass = pymysql.cursors.DictCursor
except Exception as e:
logger.warning(f"设置DictCursor失败: {e}")
# 设置时区为北京时间UTC+8
# 注意raw_connection可能不自动应用connect_args中的autocommit需确认
# SQLAlchemy的raw_connection通常返回DBAPI连接autocommit行为取决于驱动
# 这里显式关闭autocommit以保持兼容性
try:
conn.autocommit(False)
except AttributeError:
# 某些旧版本pymysql或wrapper可能不支持方法调用尝试属性赋值
pass
with conn.cursor() as cursor:
cursor.execute("SET time_zone = '+08:00'")
conn.commit()
# 注意不在这里commit除非是只读操作。调用者负责commit/rollback
# 但原代码在yield前commit了时区设置?
# 原代码cursor.execute(...); conn.commit(); yield conn
# SET time_zone 不需要 commit但为了保险起见保留原行为
conn.commit()
yield conn
except Exception as e:
if conn:
conn.rollback()
try:
conn.rollback()
except:
pass
logger.error(f"数据库连接错误: {e}")
raise
finally:
if conn:
conn.close()
conn.close() # 归还给连接池
def execute_query(self, query, params=None):
"""执行查询,返回所有结果"""

View File

@ -0,0 +1,57 @@
-- 按 entry_order_id + symbol 去重:同一开仓订单只保留一条(保留 id 最小的,即最早创建的)
-- 使用前请先备份 trades 表建议先执行「1. 查看重复」确认后再执行「2. 删除重复」
-- 说明:仅处理 entry_order_id 非空的重复;无开仓订单号的重复记录(如 sync_recovered 脏数据)需人工按 symbol/时间判断后删除。
-- ========== 1. 查看重复(只读,不写库)==========
-- 列出所有 (entry_order_id, symbol) 出现多于一次的组,以及每组中的记录
SELECT
t.entry_order_id,
t.symbol,
COUNT(*) AS cnt,
GROUP_CONCAT(t.id ORDER BY t.id) AS ids,
GROUP_CONCAT(CONCAT(t.id, '(', t.status, ',entry=', FROM_UNIXTIME(t.entry_time), ',exit=', IFNULL(FROM_UNIXTIME(t.exit_time), 'NULL'), ')') ORDER BY t.id SEPARATOR ' | ') AS detail
FROM trades t
WHERE t.entry_order_id IS NOT NULL
GROUP BY t.entry_order_id, t.symbol
HAVING COUNT(*) > 1;
-- 若有多账号,按 account_id 也分组查看(可选):
-- SELECT account_id, entry_order_id, symbol, COUNT(*) AS cnt, GROUP_CONCAT(id ORDER BY id) AS ids
-- FROM trades WHERE entry_order_id IS NOT NULL
-- GROUP BY account_id, entry_order_id, symbol HAVING COUNT(*) > 1;
-- ========== 2. 删除重复(保留每组 id 最小的那条,删除同组其余行)==========
-- 执行前请确认上面查询结果符合预期;建议先备份: CREATE TABLE trades_backup_YYYYMMDD AS SELECT * FROM trades;
DELETE t
FROM trades t
INNER JOIN (
SELECT entry_order_id, symbol, MIN(id) AS keep_id
FROM trades
WHERE entry_order_id IS NOT NULL
GROUP BY entry_order_id, symbol
HAVING COUNT(*) > 1
) g ON t.entry_order_id = g.entry_order_id AND t.symbol = g.symbol AND t.id <> g.keep_id;
-- 若有多账号,按 account_id 去重(取消下面注释并注释掉上面的 DELETE
/*
DELETE t
FROM trades t
INNER JOIN (
SELECT account_id, entry_order_id, symbol, MIN(id) AS keep_id
FROM trades
WHERE entry_order_id IS NOT NULL
GROUP BY account_id, entry_order_id, symbol
HAVING COUNT(*) > 1
) g ON t.account_id = g.account_id AND t.entry_order_id = g.entry_order_id AND t.symbol = g.symbol AND t.id <> g.keep_id;
*/
-- ========== 3. 再次检查(应无重复)==========
SELECT entry_order_id, symbol, COUNT(*) AS cnt
FROM trades
WHERE entry_order_id IS NOT NULL
GROUP BY entry_order_id, symbol
HAVING COUNT(*) > 1;
-- 期望结果:空

View File

@ -4,22 +4,69 @@ CREATE DATABASE IF NOT EXISTS `auto_trade_sys` DEFAULT CHARACTER SET utf8mb4 COL
USE `auto_trade_sys`;
-- 用户表(登录用户:管理员/普通用户)
CREATE TABLE IF NOT EXISTS `users` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(64) NOT NULL,
`password_hash` VARCHAR(255) NOT NULL,
`role` VARCHAR(20) NOT NULL DEFAULT 'user' COMMENT 'admin, user',
`status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'active, disabled',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='登录用户';
-- 用户-交易账号授权关系
CREATE TABLE IF NOT EXISTS `user_account_memberships` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`user_id` INT NOT NULL,
`account_id` INT NOT NULL,
`role` VARCHAR(20) NOT NULL DEFAULT 'viewer' COMMENT 'owner, viewer',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `uk_user_account` (`user_id`, `account_id`),
INDEX `idx_user_id` (`user_id`),
INDEX `idx_account_id` (`account_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户-交易账号授权';
-- 账号表(多账号)
CREATE TABLE IF NOT EXISTS `accounts` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
`status` VARCHAR(20) DEFAULT 'active' COMMENT 'active, disabled',
`api_key_enc` TEXT NULL COMMENT '加密后的 API KEYenc:v1:...',
`api_secret_enc` TEXT NULL COMMENT '加密后的 API SECRETenc:v1:...',
`use_testnet` BOOLEAN DEFAULT FALSE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账号表(多账号)';
-- 默认账号(兼容单账号)
INSERT INTO `accounts` (`id`, `name`, `status`, `use_testnet`)
VALUES (1, 'default', 'active', false)
ON DUPLICATE KEY UPDATE `name`=VALUES(`name`);
-- 配置表
CREATE TABLE IF NOT EXISTS `trading_config` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`config_key` VARCHAR(100) UNIQUE NOT NULL,
`account_id` INT NOT NULL DEFAULT 1,
`config_key` VARCHAR(100) NOT NULL,
`config_value` TEXT NOT NULL,
`config_type` VARCHAR(50) NOT NULL COMMENT 'string, number, boolean, json',
`category` VARCHAR(50) NOT NULL COMMENT 'position, risk, scan, strategy, api',
`description` TEXT,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`updated_by` VARCHAR(50),
INDEX `idx_category` (`category`)
INDEX `idx_category` (`category`),
INDEX `idx_account_id` (`account_id`),
UNIQUE KEY `uk_account_config_key` (`account_id`, `config_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易配置表';
-- 注意:多账号需要 (account_id, config_key) 唯一。旧库升级请跑迁移脚本(见 add_multi_account.sql
-- 交易记录表
CREATE TABLE IF NOT EXISTS `trades` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`account_id` INT NOT NULL DEFAULT 1,
`symbol` VARCHAR(20) NOT NULL,
`side` VARCHAR(10) NOT NULL COMMENT 'BUY, SELL',
`quantity` DECIMAL(20, 8) NOT NULL,
@ -45,6 +92,7 @@ CREATE TABLE IF NOT EXISTS `trades` (
`take_profit_2` DECIMAL(20, 8) NULL COMMENT '第二目标止盈价(用于展示与分步止盈)',
`status` VARCHAR(20) DEFAULT 'open' COMMENT 'open, closed, cancelled',
`created_at` INT UNSIGNED NOT NULL DEFAULT (UNIX_TIMESTAMP()) COMMENT '创建时间Unix时间戳秒数',
INDEX `idx_account_id` (`account_id`),
INDEX `idx_symbol` (`symbol`),
INDEX `idx_entry_time` (`entry_time`),
INDEX `idx_status` (`status`),
@ -57,12 +105,14 @@ CREATE TABLE IF NOT EXISTS `trades` (
-- 账户快照表
CREATE TABLE IF NOT EXISTS `account_snapshots` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`account_id` INT NOT NULL DEFAULT 1,
`total_balance` DECIMAL(20, 8) NOT NULL,
`available_balance` DECIMAL(20, 8) NOT NULL,
`total_position_value` DECIMAL(20, 8) DEFAULT 0,
`total_pnl` DECIMAL(20, 8) DEFAULT 0,
`open_positions` INT DEFAULT 0,
`snapshot_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_account_id` (`account_id`),
INDEX `idx_snapshot_time` (`snapshot_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账户快照表';
@ -158,11 +208,11 @@ INSERT INTO `trading_config` (`config_key`, `config_value`, `config_type`, `cate
('STOP_LOSS_PERCENT', '0.10', 'number', 'risk', '止损10%(相对于保证金)'),
('TAKE_PROFIT_PERCENT', '0.30', 'number', 'risk', '止盈30%相对于保证金盈亏比3:1'),
('MIN_STOP_LOSS_PRICE_PCT', '0.02', 'number', 'risk', '最小止损价格变动2%(防止止损过紧)'),
('MIN_TAKE_PROFIT_PRICE_PCT', '0.03', 'number', 'risk', '最小止盈价格变动:3%(防止止盈过紧'),
('MIN_TAKE_PROFIT_PRICE_PCT', '0.02', 'number', 'risk', '最小止盈价格变动:2%防止ATR过小时计算出不切实际的微小止盈距离'),
('USE_ATR_STOP_LOSS', 'true', 'boolean', 'risk', '是否使用ATR动态止损优先于固定百分比'),
('ATR_STOP_LOSS_MULTIPLIER', '1.8', 'number', 'risk', 'ATR止损倍数1.5-2倍ATR默认1.8'),
('ATR_TAKE_PROFIT_MULTIPLIER', '3.0', 'number', 'risk', 'ATR止盈倍数3倍ATR对应3:1盈亏比'),
('RISK_REWARD_RATIO', '3.0', 'number', 'risk', '盈亏比(止损距离的倍数,用于计算止盈'),
('ATR_TAKE_PROFIT_MULTIPLIER', '1.5', 'number', 'risk', 'ATR止盈倍数从4.5降至1.5将盈亏比从3:1降至更现实、可达成的1.5:1提升止盈触发率'),
('RISK_REWARD_RATIO', '1.5', 'number', 'risk', '盈亏比(止损距离的倍数,用于计算止盈从3.0降至1.5,更容易达成'),
('ATR_PERIOD', '14', 'number', 'risk', 'ATR计算周期默认14'),
('USE_DYNAMIC_ATR_MULTIPLIER', 'false', 'boolean', 'risk', '是否根据波动率动态调整ATR倍数'),
('ATR_MULTIPLIER_MIN', '1.5', 'number', 'risk', '动态ATR倍数最小值'),
@ -184,6 +234,8 @@ INSERT INTO `trading_config` (`config_key`, `config_value`, `config_type`, `cate
('LEVERAGE', '10', 'number', 'strategy', '基础杠杆倍数'),
('USE_DYNAMIC_LEVERAGE', 'true', 'boolean', 'strategy', '是否启用动态杠杆(根据信号强度调整杠杆倍数)'),
('MAX_LEVERAGE', '15', 'number', 'strategy', '最大杠杆倍数动态杠杆上限降低到15更保守'),
('PROFIT_PROTECTION_ENABLED', 'true', 'boolean', 'strategy', '盈利保护总开关:启用保本+移动止损'),
('LOCK_PROFIT_AT_BREAKEVEN_AFTER_PCT', '0.03', 'number', 'strategy', '盈利达保证金比例时移至保本0.03=3%0=关闭)'),
('USE_TRAILING_STOP', 'true', 'boolean', 'strategy', '是否使用移动止损'),
('TRAILING_STOP_ACTIVATION', '0.10', 'number', 'strategy', '移动止损激活阈值盈利10%后激活,给趋势更多空间)'),
('TRAILING_STOP_PROTECT', '0.05', 'number', 'strategy', '移动止损保护利润保护5%利润,更合理)'),
@ -195,6 +247,12 @@ INSERT INTO `trading_config` (`config_key`, `config_value`, `config_type`, `cate
-- API配置
('BINANCE_API_KEY', '', 'string', 'api', '币安API密钥'),
('BINANCE_API_SECRET', '', 'string', 'api', '币安API密钥'),
('USE_TESTNET', 'false', 'boolean', 'api', '是否使用测试网')
('USE_TESTNET', 'false', 'boolean', 'api', '是否使用测试网'),
-- 与盈利期对齐2026-02-15
('RSI_EXTREME_REVERSE_ENABLED', 'false', 'boolean', 'strategy', '关闭RSI极限反转与盈利期一致'),
('RSI_EXTREME_REVERSE_ONLY_NEUTRAL_4H', 'true', 'boolean', 'strategy', '若开启反向仅允许4H中性'),
('USE_MARGIN_CAP_FOR_TP', 'true', 'boolean', 'risk', '止盈按保证金封顶,避免过远'),
('USE_MARGIN_CAP_FOR_SL', 'true', 'boolean', 'risk', '止损按保证金封顶,避免扛单')
ON DUPLICATE KEY UPDATE `config_value` = VALUES(`config_value`);

View File

@ -0,0 +1,130 @@
-- ============================================================
-- 配置值格式统一迁移脚本
-- 将百分比形式(>1转换为比例形式除以100
-- 执行时间2026-01-26
-- ============================================================
-- 说明:
-- 此脚本将数据库中的百分比配置项从百分比形式如30表示30%
-- 转换为比例形式如0.30表示30%),以统一数据格式。
-- ⚠️ 重要:执行前请备份数据库!
-- ============================================================
-- 1. 备份表(强烈推荐)
-- ============================================================
CREATE TABLE IF NOT EXISTS trading_config_backup_20260126 AS
SELECT * FROM trading_config;
CREATE TABLE IF NOT EXISTS global_strategy_config_backup_20260126 AS
SELECT * FROM global_strategy_config;
-- ============================================================
-- 2. 迁移 trading_config 表
-- ============================================================
UPDATE trading_config
SET config_value = CAST(config_value AS DECIMAL(10, 4)) / 100.0
WHERE config_key IN (
'TRAILING_STOP_ACTIVATION',
'TRAILING_STOP_PROTECT',
'MIN_VOLATILITY',
'TAKE_PROFIT_PERCENT',
'STOP_LOSS_PERCENT',
'MIN_STOP_LOSS_PRICE_PCT',
'MIN_TAKE_PROFIT_PRICE_PCT',
'FIXED_RISK_PERCENT',
'MAX_POSITION_PERCENT',
'MAX_TOTAL_POSITION_PERCENT',
'MIN_POSITION_PERCENT'
)
AND config_type = 'number'
AND CAST(config_value AS DECIMAL(10, 4)) > 1;
-- ============================================================
-- 3. 迁移 global_strategy_config 表
-- ============================================================
UPDATE global_strategy_config
SET config_value = CAST(config_value AS DECIMAL(10, 4)) / 100.0
WHERE config_key IN (
'TRAILING_STOP_ACTIVATION',
'TRAILING_STOP_PROTECT',
'MIN_VOLATILITY',
'TAKE_PROFIT_PERCENT',
'STOP_LOSS_PERCENT',
'MIN_STOP_LOSS_PRICE_PCT',
'MIN_TAKE_PROFIT_PRICE_PCT',
'FIXED_RISK_PERCENT',
'MAX_POSITION_PERCENT',
'MAX_TOTAL_POSITION_PERCENT',
'MIN_POSITION_PERCENT'
)
AND config_type = 'number'
AND CAST(config_value AS DECIMAL(10, 4)) > 1;
-- ============================================================
-- 4. 验证迁移结果
-- ============================================================
-- 检查是否还有>1的百分比配置项应该返回0行
SELECT 'trading_config' as table_name, config_key, config_value, account_id
FROM trading_config
WHERE config_key IN (
'TRAILING_STOP_ACTIVATION',
'TRAILING_STOP_PROTECT',
'MIN_VOLATILITY',
'TAKE_PROFIT_PERCENT',
'STOP_LOSS_PERCENT',
'MIN_STOP_LOSS_PRICE_PCT',
'MIN_TAKE_PROFIT_PRICE_PCT',
'FIXED_RISK_PERCENT',
'MAX_POSITION_PERCENT',
'MAX_TOTAL_POSITION_PERCENT',
'MIN_POSITION_PERCENT'
)
AND config_type = 'number'
AND CAST(config_value AS DECIMAL(10, 4)) > 1
UNION ALL
SELECT 'global_strategy_config' as table_name, config_key, config_value, NULL as account_id
FROM global_strategy_config
WHERE config_key IN (
'TRAILING_STOP_ACTIVATION',
'TRAILING_STOP_PROTECT',
'MIN_VOLATILITY',
'TAKE_PROFIT_PERCENT',
'STOP_LOSS_PERCENT',
'MIN_STOP_LOSS_PRICE_PCT',
'MIN_TAKE_PROFIT_PRICE_PCT',
'FIXED_RISK_PERCENT',
'MAX_POSITION_PERCENT',
'MAX_TOTAL_POSITION_PERCENT',
'MIN_POSITION_PERCENT'
)
AND config_type = 'number'
AND CAST(config_value AS DECIMAL(10, 4)) > 1;
-- ============================================================
-- 5. 查看迁移结果(可选)
-- ============================================================
-- 查看迁移后的配置值
SELECT config_key, config_value, account_id
FROM trading_config
WHERE config_key IN (
'TRAILING_STOP_ACTIVATION',
'TRAILING_STOP_PROTECT',
'MIN_VOLATILITY',
'TAKE_PROFIT_PERCENT',
'STOP_LOSS_PERCENT'
)
AND config_type = 'number'
ORDER BY config_key, account_id;
SELECT config_key, config_value
FROM global_strategy_config
WHERE config_key IN (
'TRAILING_STOP_ACTIVATION',
'TRAILING_STOP_PROTECT',
'MIN_VOLATILITY',
'TAKE_PROFIT_PERCENT',
'STOP_LOSS_PERCENT'
)
AND config_type = 'number'
ORDER BY config_key;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
-- 与盈利期对齐RSI 关闭反向 + 止盈/止损封顶2026-02-15
-- 【重要】只更新【全局配置】表 global_strategy_config无 account_id策略只读此表
-- 不修改 trading_config个人/账号配置);个人用不到,请用本脚本或前端「全局配置」页,不要改个人配置。
INSERT INTO `global_strategy_config` (`config_key`, `config_value`, `config_type`, `category`, `description`) VALUES
('RSI_EXTREME_REVERSE_ENABLED', 'false', 'boolean', 'strategy', '关闭RSI极限反转与盈利期一致'),
('RSI_EXTREME_REVERSE_ONLY_NEUTRAL_4H', 'true', 'boolean', 'strategy', '若开启反向仅允许4H中性'),
('USE_MARGIN_CAP_FOR_TP', 'true', 'boolean', 'risk', '止盈按保证金封顶,避免过远'),
('USE_MARGIN_CAP_FOR_SL', 'true', 'boolean', 'risk', '止损按保证金封顶,避免扛单')
ON DUPLICATE KEY UPDATE
`config_value` = VALUES(`config_value`),
`config_type` = VALUES(`config_type`),
`category` = VALUES(`category`),
`description` = VALUES(`description`),
`updated_at` = CURRENT_TIMESTAMP;

341
backend/market_overview.py Normal file
View File

@ -0,0 +1,341 @@
"""
市场行情概览 - 用于全局配置页展示
拉取 Binance 公开接口无需 API Key与策略过滤逻辑对应的数据
"""
import json
import logging
import ssl
import urllib.request
from typing import Any, Dict, Optional
logger = logging.getLogger(__name__)
BINANCE_FUTURES_BASE = "https://fapi.binance.com"
BINANCE_FUTURES_DATA = "https://fapi.binance.com/futures/data"
REQUEST_TIMEOUT = 10
def _http_get(url: str, params: Optional[dict] = None) -> Optional[Any]:
"""发起 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:
return json.loads(resp.read().decode("utf-8"))
except Exception as e:
logger.debug("market_overview HTTP GET 失败 %s: %s", url[:80], e)
return None
def _fetch_klines(symbol: str, interval: str, limit: int) -> Optional[list]:
"""获取 K 线数据。"""
data = _http_get(
f"{BINANCE_FUTURES_BASE}/fapi/v1/klines",
{"symbol": symbol, "interval": interval, "limit": limit},
)
return data if isinstance(data, list) else None
def _compute_change_from_klines(klines: list, periods: int) -> Optional[float]:
"""根据 K 线计算最近 N 根的总涨跌幅(比例,如 -0.0167 表示 -1.67%)。"""
if not klines or len(klines) < periods + 1:
return None
first_close = float(klines[0][4])
last_close = float(klines[-1][4])
return (last_close - first_close) / first_close if first_close else None
def fetch_symbol_change_period(symbol: str, interval: str, periods: int) -> Optional[float]:
"""获取指定交易对在指定周期内的涨跌幅(比例)。"""
klines = _fetch_klines(symbol, interval, periods + 1)
return _compute_change_from_klines(klines, periods) if klines else None
def fetch_ticker_24h(symbol: str) -> Optional[Dict]:
"""获取 24h ticker。"""
data = _http_get(f"{BINANCE_FUTURES_BASE}/fapi/v1/ticker/24hr", {"symbol": symbol})
return data if isinstance(data, dict) else None
def fetch_premium_index(symbol: str) -> Optional[Dict]:
"""获取资金费率等。"""
data = _http_get(f"{BINANCE_FUTURES_BASE}/fapi/v1/premiumIndex", {"symbol": symbol})
return data if isinstance(data, dict) else None
def fetch_long_short_ratio(symbol: str = "BTCUSDT", period: str = "1d", limit: int = 1) -> Optional[float]:
"""获取大户多空比。"""
data = _http_get(
f"{BINANCE_FUTURES_DATA}/topLongShortPositionRatio",
{"symbol": symbol, "period": period, "limit": limit},
)
if not isinstance(data, list) or len(data) == 0:
return None
try:
return float(data[-1].get("longShortRatio", 1))
except (TypeError, ValueError):
return None
def get_market_overview() -> Dict[str, Any]:
"""
获取市场行情概览与策略过滤逻辑对应的数据
供全局配置页展示帮助用户确认当前策略方案是否匹配市场
"""
result = {
"btc_24h_change_pct": None,
"eth_24h_change_pct": None,
"btc_15m_change_pct": None,
"btc_1h_change_pct": None,
"eth_15m_change_pct": None,
"eth_1h_change_pct": None,
"btc_funding_rate": None,
"eth_funding_rate": None,
"btc_long_short_ratio": None,
"btc_trend_4h": None,
"market_regime": None,
"beta_filter_triggered": None,
}
# 24h 涨跌幅
btc_ticker = fetch_ticker_24h("BTCUSDT")
if btc_ticker is not None:
try:
result["btc_24h_change_pct"] = round(float(btc_ticker.get("priceChangePercent", 0)), 2)
except (TypeError, ValueError):
pass
eth_ticker = fetch_ticker_24h("ETHUSDT")
if eth_ticker is not None:
try:
result["eth_24h_change_pct"] = round(float(eth_ticker.get("priceChangePercent", 0)), 2)
except (TypeError, ValueError):
pass
# 15m / 1h 涨跌幅(大盘共振过滤用)
btc_15m = fetch_symbol_change_period("BTCUSDT", "15m", 5)
btc_1h = fetch_symbol_change_period("BTCUSDT", "1h", 3)
eth_15m = fetch_symbol_change_period("ETHUSDT", "15m", 5)
eth_1h = fetch_symbol_change_period("ETHUSDT", "1h", 3)
if btc_15m is not None:
result["btc_15m_change_pct"] = round(btc_15m * 100, 2)
if btc_1h is not None:
result["btc_1h_change_pct"] = round(btc_1h * 100, 2)
if eth_15m is not None:
result["eth_15m_change_pct"] = round(eth_15m * 100, 2)
if eth_1h is not None:
result["eth_1h_change_pct"] = round(eth_1h * 100, 2)
# 资金费率
btc_prem = fetch_premium_index("BTCUSDT")
if btc_prem is not None:
try:
result["btc_funding_rate"] = round(float(btc_prem.get("lastFundingRate", 0)), 6)
except (TypeError, ValueError):
pass
eth_prem = fetch_premium_index("ETHUSDT")
if eth_prem is not None:
try:
result["eth_funding_rate"] = round(float(eth_prem.get("lastFundingRate", 0)), 6)
except (TypeError, ValueError):
pass
# 大户多空比
lsr = fetch_long_short_ratio("BTCUSDT", "1d", 1)
if lsr is not None:
result["btc_long_short_ratio"] = round(lsr, 4)
# 4H 趋势
klines_4h = _fetch_klines("BTCUSDT", "4h", 60)
if klines_4h and len(klines_4h) >= 21:
try:
from trading_system.market_regime_detector import compute_trend_4h_from_klines
result["btc_trend_4h"] = compute_trend_4h_from_klines(klines_4h)
except Exception:
result["btc_trend_4h"] = _simple_trend_4h(klines_4h)
# 市场状态bull/bear/normal
try:
from trading_system.market_regime_detector import detect_market_regime
regime, details = detect_market_regime()
result["market_regime"] = regime
result["market_regime_details"] = details
except Exception as e:
logger.debug("market_overview 获取市场状态失败: %s", e)
return result
def _simple_trend_4h(klines: list) -> str:
"""简化 4H 趋势:价格 vs 最近一根 K 线前 20 根均价。"""
if len(klines) < 21:
return "neutral"
closes = [float(k[4]) for k in klines]
price = closes[-1]
avg20 = sum(closes[-21:-1]) / 20
if price > avg20 * 1.002:
return "up"
if price < avg20 * 0.998:
return "down"
return "neutral"
def _g(key: str, default: Any, cfg: dict) -> Any:
"""从配置字典取键,支持 bool/数字/字符串。"""
v = cfg.get(key, default)
if v is None:
return default
if isinstance(default, bool):
return str(v).lower() in ("true", "1", "yes")
return v
def get_strategy_execution_overview() -> Dict[str, Any]:
"""
生成策略执行概览当前执行方案配置项执行情况用易读文字描述整体策略执行标准与机制
供全局配置页策略执行概览展示
返回格式{ "sections": [ { "title": "小节标题", "content": "正文" } ] }
"""
sections = []
cfg = {}
try:
from config_manager import GlobalStrategyConfigManager
mgr = GlobalStrategyConfigManager()
mgr.reload_from_redis()
for key in (
"AUTO_TRADE_ENABLED", "AUTO_TRADE_ONLY_TRENDING", "AUTO_TRADE_ALLOW_4H_NEUTRAL",
"MIN_SIGNAL_STRENGTH", "LOW_VOLATILITY_MIN_SIGNAL_STRENGTH", "MARKET_REGIME_AUTO",
"TOP_N_SYMBOLS", "SCAN_INTERVAL", "PRIMARY_INTERVAL", "CONFIRM_INTERVAL",
"MAX_OPEN_POSITIONS", "MAX_DAILY_ENTRIES", "FIXED_RISK_PERCENT", "USE_FIXED_RISK_SIZING",
"BETA_FILTER_ENABLED", "BETA_FILTER_THRESHOLD", "MARKET_SCHEME",
"USE_ATR_STOP_LOSS", "ATR_STOP_LOSS_MULTIPLIER", "STOP_LOSS_PERCENT",
"TAKE_PROFIT_1_PERCENT", "TAKE_PROFIT_PERCENT", "USE_TRAILING_STOP",
"TRAILING_STOP_ACTIVATION", "TRAILING_STOP_PROTECT", "PROFIT_PROTECTION_ENABLED",
"SMART_ENTRY_ENABLED", "USE_TREND_ENTRY_FILTER", "MAX_TREND_MOVE_BEFORE_ENTRY",
"MAX_RSI_FOR_LONG", "MIN_RSI_FOR_SHORT", "MAX_CHANGE_PERCENT_FOR_LONG", "MAX_CHANGE_PERCENT_FOR_SHORT",
"MIN_VOLUME_24H", "MIN_VOLATILITY", "MIN_HOLD_TIME_SEC",
):
cfg[key] = mgr.get(key)
except Exception as e:
logger.debug("get_strategy_execution_overview 加载配置失败: %s", e)
def pct(x):
if x is None:
return ""
try:
f = float(x)
if abs(f) < 1 and abs(f) > 0:
return f"{f * 100:.2f}%"
return f"{f}%"
except (TypeError, ValueError):
return str(x)
# ---------- 1. 总开关与自动交易条件 ----------
auto_on = _g("AUTO_TRADE_ENABLED", True, cfg)
only_trending = _g("AUTO_TRADE_ONLY_TRENDING", True, cfg)
allow_4h_neutral = _g("AUTO_TRADE_ALLOW_4H_NEUTRAL", False, cfg)
min_strength = _g("MIN_SIGNAL_STRENGTH", 8, cfg)
low_vol_strength = _g("LOW_VOLATILITY_MIN_SIGNAL_STRENGTH", 9, cfg)
regime_auto = _g("MARKET_REGIME_AUTO", True, cfg)
c1 = []
c1.append("自动交易总开关:" + ("开启" if auto_on else "关闭"))
if not auto_on:
c1.append("关闭时仅生成推荐,不会自动下单。")
else:
c1.append("自动下单条件(需同时满足):")
c1.append("• 信号强度 ≥ " + str(min_strength) + "(技术指标综合评分);低波动期自动提高至 " + str(low_vol_strength) + "" + ("已开启" if regime_auto else "未开启") + "市场节奏识别)。")
c1.append("• 市场状态:仅当「仅做趋势市」开启时,要求市场状态为 trending 才下单ranging/unknown 只生成推荐、不自动下单。当前「仅做趋势市」=" + ("" if only_trending else "") + "")
c1.append("• 4H 趋势:允许 4H 中性时自动交易 = " + ("" if allow_4h_neutral else "") + ";为否时 4H 为中性会跳过自动下单。")
sections.append({
"title": "一、总开关与自动交易条件",
"content": "\n".join(c1),
})
# ---------- 2. 扫描与候选池 ----------
top_n = _g("TOP_N_SYMBOLS", 30, cfg)
scan_interval = _g("SCAN_INTERVAL", 900, cfg)
primary = _g("PRIMARY_INTERVAL", "4h", cfg)
confirm = _g("CONFIRM_INTERVAL", "1d", cfg)
min_vol = _g("MIN_VOLUME_24H", 30000000, cfg)
min_vol_str = f"{min_vol / 1e6:.0f} 万 USDT" if isinstance(min_vol, (int, float)) and min_vol >= 1e6 else str(min_vol)
vol_pct = _g("MIN_VOLATILITY", 0.03, cfg)
vol_pct_str = f"{float(vol_pct) * 100:.1f}%" if isinstance(vol_pct, (int, float)) else str(vol_pct)
c2 = []
c2.append("每次扫描取涨跌幅最大的前 " + str(top_n) + " 个交易对进行详细分析;扫描间隔 " + str(scan_interval) + " 秒。")
c2.append("主周期 " + str(primary) + ",确认周期 " + str(confirm) + "24h 成交额 ≥ " + min_vol_str + ",最小波动率 " + vol_pct_str + "")
sections.append({
"title": "二、扫描与候选池",
"content": "\n".join(c2),
})
# ---------- 3. 仓位与风控 ----------
max_pos = _g("MAX_OPEN_POSITIONS", 4, cfg)
max_daily = _g("MAX_DAILY_ENTRIES", 15, cfg)
fixed_risk = _g("USE_FIXED_RISK_SIZING", True, cfg)
risk_pct = _g("FIXED_RISK_PERCENT", 0.01, cfg)
risk_pct_str = pct(risk_pct) if isinstance(risk_pct, (int, float)) and risk_pct <= 1 else f"{float(risk_pct)}%"
c3 = []
c3.append("同时持仓上限 " + str(max_pos) + " 个,每日最多开仓 " + str(max_daily) + " 笔。")
c3.append("固定风险 sizing" + ("开启" if fixed_risk else "关闭") + ";每笔最大亏损 " + risk_pct_str + " 账户资金。")
sections.append({
"title": "三、仓位与风控",
"content": "\n".join(c3),
})
# ---------- 4. 大盘与市场方案 ----------
beta_on = _g("BETA_FILTER_ENABLED", True, cfg)
beta_th = _g("BETA_FILTER_THRESHOLD", -0.005, cfg)
scheme = str(_g("MARKET_SCHEME", "normal", cfg) or "normal")
c4 = []
c4.append("大盘共振过滤:" + ("开启" if beta_on else "关闭") + "BTC/ETH 短周期跌逾 " + pct(beta_th) + " 时屏蔽多单。")
c4.append("当前市场方案:" + scheme + "(用于参数预设)。")
sections.append({
"title": "四、大盘与市场方案",
"content": "\n".join(c4),
})
# ---------- 5. 止损止盈与保护 ----------
use_atr = _g("USE_ATR_STOP_LOSS", True, cfg)
atr_mult = _g("ATR_STOP_LOSS_MULTIPLIER", 2.0, cfg)
sl_pct = _g("STOP_LOSS_PERCENT", 0.05, cfg)
tp1 = _g("TAKE_PROFIT_1_PERCENT", 0.12, cfg)
tp2 = _g("TAKE_PROFIT_PERCENT", 0.25, cfg)
trail = _g("USE_TRAILING_STOP", True, cfg)
trail_act = _g("TRAILING_STOP_ACTIVATION", 0.10, cfg)
trail_prot = _g("TRAILING_STOP_PROTECT", 0.02, cfg)
profit_prot = _g("PROFIT_PROTECTION_ENABLED", True, cfg)
c5 = []
c5.append("止损ATR 动态止损 " + ("开启" if use_atr else "关闭") + (",倍数 " + str(atr_mult) if use_atr else "") + ";固定止损 " + pct(sl_pct) + "")
c5.append("止盈:第一目标 " + pct(tp1) + ",第二目标 " + pct(tp2) + "")
c5.append("盈利保护总开关:" + ("开启" if profit_prot else "关闭") + ";移动止损 " + ("开启" if trail else "关闭") + (",盈利 " + pct(trail_act) + " 激活、保护 " + pct(trail_prot) + " 利润" if trail else "") + "")
sections.append({
"title": "五、止损止盈与保护",
"content": "\n".join(c5),
})
# ---------- 6. 入场与过滤 ----------
smart = _g("SMART_ENTRY_ENABLED", True, cfg)
trend_filter = _g("USE_TREND_ENTRY_FILTER", True, cfg)
max_trend = _g("MAX_TREND_MOVE_BEFORE_ENTRY", 0.04, cfg)
max_rsi_long = _g("MAX_RSI_FOR_LONG", 65, cfg)
min_rsi_short = _g("MIN_RSI_FOR_SHORT", 30, cfg)
max_ch_long = _g("MAX_CHANGE_PERCENT_FOR_LONG", 25, cfg)
max_ch_short = _g("MAX_CHANGE_PERCENT_FOR_SHORT", 10, cfg)
c6 = []
c6.append("智能入场(限价+追价+市价兜底):" + ("开启" if smart else "关闭") + "")
c6.append("趋势入场过滤:" + ("开启" if trend_filter else "关闭") + ";信号方向已走超 " + pct(max_trend) + " 则不再入场。")
c6.append("做多RSI ≤ " + str(max_rsi_long) + "24h 涨跌幅 ≤ " + str(max_ch_long) + "%。做空RSI ≥ " + str(min_rsi_short) + "24h 涨跌幅 ≤ " + str(max_ch_short) + "%")
sections.append({
"title": "六、入场与过滤",
"content": "\n".join(c6),
})
return {"sections": sections}

View File

@ -24,3 +24,9 @@ aiohttp==3.9.1
redis>=4.2.0
# 保留aioredis作为备选如果某些代码仍在使用aioredis接口
aioredis==2.0.1
# 安全加密存储敏感字段API KEY/SECRET
cryptography>=42.0.0
# 登录鉴权JWT
python-jose[cryptography]>=3.3.0

View File

@ -3,8 +3,13 @@
cd "$(dirname "$0")"
# 查找运行中的uvicorn进程
PID=$(ps aux | grep "uvicorn api.main:app" | grep -v grep | awk '{print $2}')
# 查找运行中的uvicorn进程 (优先使用 lsof 查找端口占用)
PID=$(lsof -t -i:8001)
if [ -z "$PID" ]; then
# 回退到 ps 查找 (如果 lsof 没找到或不可用)
PID=$(ps aux | grep "uvicorn api.main:app" | grep -v grep | awk '{print $2}')
fi
if [ -z "$PID" ]; then
echo "未找到运行中的后端服务"
@ -16,8 +21,8 @@ else
kill $PID
sleep 2
# 检查是否成功停止
if ps -p $PID > /dev/null 2>&1; then
# 检查是否成功停止 (使用 kill -0 检查进程是否存在,替代 ps -p)
if kill -0 $PID > /dev/null 2>&1; then
echo "强制停止服务..."
kill -9 $PID
sleep 1

View File

@ -0,0 +1,22 @@
#!/bin/bash
# 重启推荐服务
cd "$(dirname "$0")"
# 查找 recommendations_main 进程
PID=$(ps aux | grep "trading_system.recommendations_main" | grep -v grep | awk '{print $2}')
if [ -z "$PID" ]; then
echo "未找到运行中的推荐服务,直接启动..."
./start_recommendations.sh
else
echo "找到推荐服务PID: $PID,正在重启..."
kill $PID 2>/dev/null || true
sleep 2
if ps -p $PID > /dev/null 2>&1; then
kill -9 $PID 2>/dev/null || true
sleep 1
fi
echo "正在启动新服务..."
./start_recommendations.sh
fi

View File

@ -0,0 +1,4 @@
"""
安全相关工具加密/解密等
"""

119
backend/security/crypto.py Normal file
View File

@ -0,0 +1,119 @@
"""
对称加密工具用于存储 API Key/Secret 等敏感字段
说明
- 使用 AES-GCM需要 cryptography 依赖
- master key 来自环境变量
- ATS_MASTER_KEY推荐32字节 key base64(urlsafe) hex
- AUTO_TRADE_SYS_MASTER_KEY兼容
"""
from __future__ import annotations
import base64
import binascii
import os
from typing import Optional
def _load_master_key_bytes() -> Optional[bytes]:
raw = (
os.getenv("ATS_MASTER_KEY")
or os.getenv("AUTO_TRADE_SYS_MASTER_KEY")
or os.getenv("MASTER_KEY")
or ""
).strip()
if not raw:
return None
# 1) hex
try:
b = bytes.fromhex(raw)
if len(b) == 32:
return b
except Exception:
pass
# 2) urlsafe base64
try:
padded = raw + ("=" * (-len(raw) % 4))
b = base64.urlsafe_b64decode(padded.encode("utf-8"))
if len(b) == 32:
return b
except binascii.Error:
pass
except Exception:
pass
return None
def _aesgcm():
try:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM # type: ignore
return AESGCM
except Exception as e: # pragma: no cover
raise RuntimeError(
"缺少加密依赖 cryptography无法安全存储敏感字段。请安装 cryptography 并设置 ATS_MASTER_KEY。"
) from e
def encrypt_str(plaintext: str) -> str:
"""
加密字符串返回带版本前缀的密文
enc:v1:<b64(nonce)>:<b64(ciphertext)>
"""
if plaintext is None:
plaintext = ""
s = str(plaintext)
if s == "":
return ""
key = _load_master_key_bytes()
if not key:
# 允许降级不加密直接存避免线上因缺KEY彻底不可用但强烈建议尽快配置 master key
return s
import os as _os
AESGCM = _aesgcm()
nonce = _os.urandom(12)
aes = AESGCM(key)
ct = aes.encrypt(nonce, s.encode("utf-8"), None)
return "enc:v1:{}:{}".format(
base64.urlsafe_b64encode(nonce).decode("utf-8").rstrip("="),
base64.urlsafe_b64encode(ct).decode("utf-8").rstrip("="),
)
def decrypt_str(ciphertext: str) -> str:
"""
解密 encrypt_str 的输出若不是 enc:v1 前缀则视为明文原样返回兼容旧数据
"""
if ciphertext is None:
return ""
s = str(ciphertext)
if s == "":
return ""
if not s.startswith("enc:v1:"):
return s
key = _load_master_key_bytes()
if not key:
raise RuntimeError("密文存在但未配置 ATS_MASTER_KEY无法解密敏感字段。")
parts = s.split(":")
if len(parts) != 4:
raise ValueError("密文格式不正确")
b64_nonce = parts[2] + ("=" * (-len(parts[2]) % 4))
b64_ct = parts[3] + ("=" * (-len(parts[3]) % 4))
nonce = base64.urlsafe_b64decode(b64_nonce.encode("utf-8"))
ct = base64.urlsafe_b64decode(b64_ct.encode("utf-8"))
AESGCM = _aesgcm()
aes = AESGCM(key)
pt = aes.decrypt(nonce, ct, None)
return pt.decode("utf-8")

280
backend/spot_scanner.py Normal file
View File

@ -0,0 +1,280 @@
"""
现货推荐扫描拉取币安现货行情仅做多信号写入 Redis /api/recommendations/spot 使用
使用公开 API无需 API Key定时任务调用 run_spot_scan_and_cache()
"""
import asyncio
import json
import logging
import os
import sys
import time
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
import aiohttp
# 可选的 Redis 写入(与 recommendations 路由共用连接方式)
try:
import redis.asyncio as redis_async
except Exception:
redis_async = None
logger = logging.getLogger(__name__)
BINANCE_SPOT_BASE = "https://api.binance.com"
SPOT_KLINES_LIMIT = 60
SPOT_TOP_N = 80
SPOT_MIN_STRENGTH = 4
SPOT_MAX_RECS = 30
def _beijing_now_iso() -> str:
from datetime import timedelta
return datetime.now(tz=timezone(timedelta(hours=8))).isoformat()
async def _http_get(session: aiohttp.ClientSession, url: str, params: Optional[Dict] = None) -> Optional[Any]:
try:
async with session.get(url, params=params or {}, timeout=aiohttp.ClientTimeout(total=15)) as resp:
if resp.status != 200:
return None
return await resp.json()
except Exception as e:
logger.warning("spot_scanner _http_get %s: %s", url[:60], e)
return None
def _technical_indicators():
"""延迟导入 trading_system.indicators避免 backend 强依赖 trading_system 路径。"""
project_root = __import__("pathlib").Path(__file__).resolve().parent.parent
trading_system = project_root / "trading_system"
if str(trading_system) not in sys.path:
sys.path.insert(0, str(trading_system))
try:
from indicators import TechnicalIndicators
return TechnicalIndicators
except ImportError:
from trading_system.indicators import TechnicalIndicators
return TechnicalIndicators
async def _fetch_spot_symbols(session: aiohttp.ClientSession) -> List[str]:
"""获取所有 USDT 现货交易对status=TRADING"""
data = await _http_get(session, f"{BINANCE_SPOT_BASE}/api/v3/exchangeInfo")
if not data or "symbols" not in data:
return []
symbols = []
for s in data["symbols"]:
if s.get("status") != "TRADING":
continue
if s.get("quoteAsset") != "USDT":
continue
sym = s.get("symbol")
if sym:
symbols.append(sym)
return symbols
async def _fetch_spot_ticker_24h(session: aiohttp.ClientSession) -> List[Dict]:
"""获取 24h ticker返回 list of dict (symbol, lastPrice, priceChangePercent, volume, ...)。"""
data = await _http_get(session, f"{BINANCE_SPOT_BASE}/api/v3/ticker/24hr")
if not isinstance(data, list):
return []
return data
async def _fetch_spot_klines(session: aiohttp.ClientSession, symbol: str, interval: str = "15m", limit: int = 60) -> Optional[List[List]]:
"""现货 K 线,格式与合约一致 [open_time, o, h, l, c, volume, ...]。"""
data = await _http_get(
session,
f"{BINANCE_SPOT_BASE}/api/v3/klines",
{"symbol": symbol, "interval": interval, "limit": limit},
)
return data if isinstance(data, list) else None
def _compute_spot_signal(klines: List[List], ticker: Dict, TechnicalIndicators) -> Optional[Dict]:
"""
基于 K 线计算只做多信号返回 None { direction: 'BUY', strength: int, ... }
"""
if not klines or len(klines) < 50:
return None
closes = [float(k[4]) for k in klines]
highs = [float(k[2]) for k in klines]
lows = [float(k[3]) for k in klines]
current_price = closes[-1]
rsi = TechnicalIndicators.calculate_rsi(closes, period=14)
macd = TechnicalIndicators.calculate_macd(closes)
bollinger = TechnicalIndicators.calculate_bollinger_bands(closes, period=20)
ema20 = TechnicalIndicators.calculate_ema(closes, period=20)
ema50 = TechnicalIndicators.calculate_ema(closes, period=50)
strength = 0
# 只做多RSI 超卖、价格在下轨附近、MACD 金叉、价格在均线上方等
if rsi is not None and rsi < 35:
strength += 3
elif rsi is not None and rsi < 50:
strength += 1
if bollinger and current_price <= bollinger["lower"] * 1.002:
strength += 3
elif bollinger and current_price < bollinger["middle"]:
strength += 1
if macd and macd["histogram"] > 0 and macd["macd"] > macd["signal"]:
strength += 2
if ema20 and ema50 and current_price > ema20 > ema50:
strength += 2
elif ema20 and current_price > ema20:
strength += 1
strength = max(0, min(strength, 10))
if strength < SPOT_MIN_STRENGTH:
return None
return {
"direction": "BUY",
"strength": strength,
"rsi": rsi,
"current_price": current_price,
}
def _build_spot_recommendation(
symbol: str,
ticker: Dict,
signal: Dict,
) -> Dict[str, Any]:
"""构造单条现货推荐(与合约推荐结构兼容,便于前端复用)。"""
current_price = float(ticker.get("lastPrice") or signal.get("current_price") or 0)
change_percent = float(ticker.get("priceChangePercent") or 0)
ts = time.time()
entry = current_price * 0.995
stop_pct = 0.05
tp1_pct = 0.08
tp2_pct = 0.15
if current_price <= 0:
return None
stop_loss = entry * (1 - stop_pct)
tp1 = entry * (1 + tp1_pct)
tp2 = entry * (1 + tp2_pct)
return {
"symbol": symbol,
"direction": "BUY",
"market": "spot",
"current_price": current_price,
"signal_strength": signal.get("strength", 0),
"change_percent": change_percent,
"suggested_limit_price": entry,
"planned_entry_price": entry,
"suggested_stop_loss": stop_loss,
"suggested_take_profit_1": tp1,
"suggested_take_profit_2": tp2,
"suggested_position_percent": 0.05,
"recommendation_time": _beijing_now_iso(),
"timestamp": ts,
"recommendation_reason": "现货做多信号RSI/布林带/MACD/均线)",
"user_guide": f"现货建议在 {entry:.4f} USDT 附近买入,止损 {stop_loss:.4f}目标1 {tp1:.4f}目标2 {tp2:.4f}。仅供参考,请自行判断。",
}
async def run_spot_scan() -> List[Dict[str, Any]]:
"""执行一次现货扫描,返回推荐列表(不写 Redis"""
TechnicalIndicators = _technical_indicators()
recommendations = []
async with aiohttp.ClientSession() as session:
symbols = await _fetch_spot_symbols(session)
if not symbols:
logger.warning("spot_scanner: 未获取到现货交易对")
return []
tickers = await _fetch_spot_ticker_24h(session)
ticker_map = {t["symbol"]: t for t in tickers if isinstance(t.get("symbol"), str)}
# 按 24h 成交量排序,取前 SPOT_TOP_N 再按涨跌幅取部分
def volume_key(t):
try:
return float(t.get("volume") or 0) * float(t.get("lastPrice") or 0)
except Exception:
return 0
sorted_tickers = sorted(
[t for t in tickers if t.get("symbol") in symbols],
key=volume_key,
reverse=True,
)[: SPOT_TOP_N * 2]
# 按涨跌幅取前 N 个(偏强势或超跌反弹)
with_change = [(t, float(t.get("priceChangePercent") or 0)) for t in sorted_tickers]
with_change.sort(key=lambda x: -abs(x[1]))
to_scan = [t[0]["symbol"] for t in with_change[: SPOT_TOP_N]]
for symbol in to_scan:
try:
klines = await _fetch_spot_klines(session, symbol, "15m", SPOT_KLINES_LIMIT)
ticker = ticker_map.get(symbol, {})
if not klines or not ticker:
continue
signal = _compute_spot_signal(klines, ticker, TechnicalIndicators)
if not signal:
continue
rec = _build_spot_recommendation(symbol, ticker, signal)
if rec:
recommendations.append(rec)
if len(recommendations) >= SPOT_MAX_RECS:
break
except Exception as e:
logger.debug("spot_scanner %s: %s", symbol, e)
await asyncio.sleep(0.05)
recommendations.sort(key=lambda x: x.get("signal_strength", 0), reverse=True)
return recommendations[: SPOT_MAX_RECS]
def _redis_connection_kwargs():
redis_url = (os.getenv("REDIS_URL", "") or "").strip() or "redis://localhost:6379"
kwargs = {"decode_responses": True}
if os.getenv("REDIS_USERNAME"):
kwargs["username"] = os.getenv("REDIS_USERNAME")
if os.getenv("REDIS_PASSWORD"):
kwargs["password"] = os.getenv("REDIS_PASSWORD")
if redis_url.startswith("rediss://") or os.getenv("REDIS_USE_TLS", "").lower() == "true":
if redis_url.startswith("redis://"):
redis_url = redis_url.replace("redis://", "rediss://", 1)
kwargs.setdefault("ssl_cert_reqs", os.getenv("REDIS_SSL_CERT_REQS", "required"))
if os.getenv("REDIS_SSL_CA_CERTS"):
kwargs["ssl_ca_certs"] = os.getenv("REDIS_SSL_CA_CERTS")
return redis_url, kwargs
async def run_spot_scan_and_cache(ttl_sec: int = 900) -> int:
"""
执行现货扫描并写入 Redis返回写入的推荐数量
Redis key: recommendations:spot:snapshot
"""
items = await run_spot_scan()
now_ms = int(time.time() * 1000)
payload = {
"items": items,
"generated_at": _beijing_now_iso(),
"generated_at_ms": now_ms,
"ttl_sec": ttl_sec,
"count": len(items),
}
if redis_async is None:
logger.warning("spot_scanner: redis 不可用,跳过写入")
return len(items)
redis_url, kwargs = _redis_connection_kwargs()
try:
client = redis_async.from_url(redis_url, **kwargs)
await client.ping()
key = "recommendations:spot:snapshot"
await client.setex(key, ttl_sec, json.dumps(payload, ensure_ascii=False))
logger.info("spot_scanner: 已写入 %d 条现货推荐到 %s", len(items), key)
await client.aclose()
return len(items)
except Exception as e:
logger.warning("spot_scanner: Redis 写入失败 %s", e)
return len(items)

View File

@ -0,0 +1,39 @@
#!/bin/bash
# 启动推荐服务recommendations_main
# 优先使用 trading_system/.venv与服务器实际部署一致其次 backend/.venv
cd "$(dirname "$0")"
BACKEND_DIR="$(pwd)"
PROJECT_ROOT="$(cd .. && pwd)"
TRADING_VENV="${PROJECT_ROOT}/trading_system/.venv"
# 激活虚拟环境:优先 trading_system/.venv其次 backend 下
if [ -d "${TRADING_VENV}" ]; then
source "${TRADING_VENV}/bin/activate"
elif [ -d ".venv" ]; then
source .venv/bin/activate
elif [ -d "../.venv" ]; then
source ../.venv/bin/activate
else
echo "错误: 找不到虚拟环境trading_system/.venv、.venv 或 ../.venv"
exit 1
fi
# 设置环境变量
export PYTHONPATH="${PROJECT_ROOT}"
export DB_HOST=${DB_HOST:-localhost}
export DB_PORT=${DB_PORT:-3306}
export DB_USER=${DB_USER:-autosys}
export DB_PASSWORD=${DB_PASSWORD:-}
export DB_NAME=${DB_NAME:-auto_trade_sys}
export LOG_LEVEL=${LOG_LEVEL:-INFO}
# 创建日志目录
mkdir -p "${PROJECT_ROOT}/logs"
# 启动推荐服务(后台运行)
cd "${PROJECT_ROOT}"
nohup python -m trading_system.recommendations_main > logs/recommendations.log 2>&1 &
PID=$!
echo "推荐服务已启动PID: $PID"
echo "日志: tail -f ${PROJECT_ROOT}/logs/recommendations.log"

23
backend/stop.sh Normal file
View File

@ -0,0 +1,23 @@
#!/bin/bash
# 停止后端服务脚本
cd "$(dirname "$0")"
# 查找运行中的uvicorn进程
PID=$(ps aux | grep "uvicorn api.main:app" | grep -v grep | awk '{print $2}')
if [ -z "$PID" ]; then
echo "未找到运行中的后端服务"
else
echo "找到运行中的后端服务PID: $PID"
echo "正在停止服务..."
kill $PID
sleep 1
# 检查是否成功停止
if ps -p $PID > /dev/null 2>&1; then
echo "停止失败,尝试强制停止..."
kill -9 $PID
fi
echo "后端服务已停止"
fi

21
backend/stop_recommendations.sh Executable file
View File

@ -0,0 +1,21 @@
#!/bin/bash
# 停止推荐服务recommendations_main
cd "$(dirname "$0")"
# 查找 recommendations_main 进程
PID=$(ps aux | grep "trading_system.recommendations_main" | grep -v grep | awk '{print $2}')
if [ -z "$PID" ]; then
echo "未找到运行中的推荐服务"
else
echo "找到推荐服务PID: $PID"
echo "正在停止..."
kill $PID 2>/dev/null || true
sleep 2
if ps -p $PID > /dev/null 2>&1; then
echo "尝试强制停止..."
kill -9 $PID 2>/dev/null || true
fi
echo "推荐服务已停止"
fi

View File

@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""
缺省全局配置项同步到数据库 global_strategy_config
- 已在 UI 保存过的项不会覆盖只插入缺失的 key
- 用于新上线配置项 MAX_RSI_FOR_LONGMIN_RSI_FOR_SHORT 一次性写入默认值
便于在数据库中可见可备份且不依赖先在页面改一次再保存
使用方式在项目根目录
cd backend && python sync_global_config_defaults.py
python backend/sync_global_config_defaults.py
"""
import os
import sys
from pathlib import Path
# 确保 backend 在路径中
backend_dir = Path(__file__).resolve().parent
if str(backend_dir) not in sys.path:
sys.path.insert(0, str(backend_dir))
# 需要同步的缺省项(仅插入数据库中不存在的 key
DEFAULTS_TO_SYNC = [
{"config_key": "MAX_RSI_FOR_LONG", "config_value": "65", "config_type": "number", "category": "strategy",
"description": "做多时 RSI 超过此值则不开多2026-02-1265 避免追高)。"},
{"config_key": "MAX_CHANGE_PERCENT_FOR_LONG", "config_value": "25", "config_type": "number", "category": "strategy",
"description": "做多时 24h 涨跌幅超过此值则不开多(避免追大涨)。单位:百分比数值,如 25 表示 25%。2026-01-31新增。"},
{"config_key": "MIN_RSI_FOR_SHORT", "config_value": "30", "config_type": "number", "category": "strategy",
"description": "做空时 RSI 低于此值则不做空避免深超卖反弹。2026-01-31新增。"},
{"config_key": "MAX_CHANGE_PERCENT_FOR_SHORT", "config_value": "10", "config_type": "number", "category": "strategy",
"description": "做空时 24h 涨跌幅超过此值则不做空24h 仍大涨时不做空。单位百分比数值。2026-01-31新增。"},
{"config_key": "TAKE_PROFIT_1_PERCENT", "config_value": "0.3", "config_type": "number", "category": "strategy",
"description": "分步止盈第一目标(保证金百分比,如 0.2=20%。2026-02-12 提高以改善盈亏比。"},
{"config_key": "MIN_RR_FOR_TP1", "config_value": "1.5", "config_type": "number", "category": "strategy",
"description": "第一目标止盈相对止损的最小盈亏比TP1 至少为止损距离的 1.5 倍。2026-02-12 新增。"},
{"config_key": "SCAN_EXTRA_SYMBOLS_FOR_SUPPLEMENT", "config_value": "8", "config_type": "number", "category": "scan",
"description": "智能补单:多返回的候选数量。当前 TOP_N 中部分因冷却等被跳过时,仍会尝试这批额外候选,避免无单可下。"},
{"config_key": "BETA_FILTER_ENABLED", "config_value": "true", "config_type": "boolean", "category": "strategy",
"description": "大盘共振过滤BTC/ETH 下跌时屏蔽多单。"},
{"config_key": "BETA_FILTER_THRESHOLD", "config_value": "-0.005", "config_type": "number", "category": "strategy",
"description": "大盘共振阈值(比例,如 -0.005 表示 -0.5%)。"},
{"config_key": "POSITION_SCALE_FACTOR", "config_value": "1.0", "config_type": "number", "category": "risk",
"description": "仓位放大系数1.0=正常1.2=+20%上限2.0。盈利时适度调高可扩大收益。"},
{"config_key": "USE_FIXED_RISK_SIZING", "config_value": "true", "config_type": "boolean", "category": "risk",
"description": "是否启用固定风险仓位计算(推荐)。若启用,则忽略 MAX_POSITION_PERCENT改用 FIXED_RISK_PERCENT 计算仓位。"},
{"config_key": "FIXED_RISK_PERCENT", "config_value": "0.03", "config_type": "number", "category": "risk",
"description": "每笔交易风险占总账户的百分比(如 0.025=2.5%)。配合止损距离计算仓位,风险可控。"},
{"config_key": "MIN_MARGIN_USDT", "config_value": "10.0", "config_type": "number", "category": "risk",
"description": "最小保证金USDT。2026-02-13 提高到 10.0 USDT 以避免无效小单。"},
# 盈利期对齐2026-02-15仅当 key 不存在时插入,不覆盖已有值
{"config_key": "RSI_EXTREME_REVERSE_ENABLED", "config_value": "false", "config_type": "boolean", "category": "strategy",
"description": "关闭RSI极限反转与盈利期一致"},
{"config_key": "RSI_EXTREME_REVERSE_ONLY_NEUTRAL_4H", "config_value": "true", "config_type": "boolean", "category": "strategy",
"description": "若开启反向仅允许4H中性"},
{"config_key": "USE_MARGIN_CAP_FOR_TP", "config_value": "true", "config_type": "boolean", "category": "risk",
"description": "止盈按保证金封顶,避免过远"},
{"config_key": "USE_MARGIN_CAP_FOR_SL", "config_value": "true", "config_type": "boolean", "category": "risk",
"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": "BLOCK_SHORT_WHEN_4H_UP", "config_value": "true", "config_type": "boolean", "category": "strategy",
"description": "4H 趋势上涨时禁止开空。默认 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 行情识别牛/熊/正常)。"},
]
def main():
try:
from database.models import GlobalStrategyConfig
from database.connection import db
except ImportError as e:
print(f"无法导入数据库模块,请确保在 backend 目录或设置 PYTHONPATH: {e}")
sys.exit(1)
def _table_has_column(table: str, col: str) -> bool:
try:
db.execute_one(f"SELECT {col} FROM {table} LIMIT 1")
return True
except Exception:
return False
if not _table_has_column("global_strategy_config", "config_key"):
print("表 global_strategy_config 不存在或结构异常,请先执行 backend/database/add_global_strategy_config.sql")
sys.exit(1)
inserted = 0
skipped = 0
for row in DEFAULTS_TO_SYNC:
key = row["config_key"]
existing = GlobalStrategyConfig.get(key)
if existing:
skipped += 1
print(f" 已有: {key}")
continue
GlobalStrategyConfig.set(
key,
row["config_value"],
row["config_type"],
row["category"],
row.get("description"),
updated_by="sync_global_config_defaults",
)
inserted += 1
print(f" 插入: {key} = {row['config_value']}")
print(f"\n同步完成: 新增 {inserted} 项,已存在跳过 {skipped} 项。")
if __name__ == "__main__":
main()

39
backend/查看同步日志.sh Executable file
View File

@ -0,0 +1,39 @@
#!/bin/bash
# 查看同步订单日志的便捷脚本
cd "$(dirname "$0")"
echo "=== 同步订单日志查看工具 ==="
echo ""
# 检查日志文件是否存在
if [ ! -f "logs/api.log" ]; then
echo "⚠️ 日志文件不存在: logs/api.log"
echo " 请先启动 backend 服务"
exit 1
fi
echo "日志文件位置:"
echo " - Python 应用日志: backend/logs/api.log"
echo " - Uvicorn 服务器日志: backend/logs/uvicorn.log"
echo ""
# 显示最近的同步日志
echo "=== 最近的同步订单日志(最后 50 行)==="
echo ""
tail -50 logs/api.log | grep -i "同步\|sync\|订单\|order" --color=always || echo "未找到同步相关日志"
echo ""
echo "=== 使用说明 ==="
echo ""
echo "实时查看同步日志:"
echo " tail -f logs/api.log | grep -i '同步\|sync'"
echo ""
echo "查看最近的同步日志:"
echo " tail -100 logs/api.log | grep -i '同步\|sync'"
echo ""
echo "查看特定时间的同步日志:"
echo " grep '2026-02-17 23:' logs/api.log | grep -i '同步\|sync'"
echo ""
echo "查看所有同步相关日志(包括详细信息):"
echo " grep -i '同步\|sync\|订单\|order' logs/api.log | tail -100"

93
backend/检查内存问题.sh Executable file
View File

@ -0,0 +1,93 @@
#!/bin/bash
# 检查交易服务内存问题
echo "=== 交易服务内存问题诊断 ==="
echo ""
# 1. 查看交易服务进程的详细内存信息
echo "📊 交易服务进程内存详情:"
TRADING_PID=$(ps aux | grep "trading_system.main" | grep -v grep | awk '{print $2}')
if [ -z "$TRADING_PID" ]; then
echo " ⚠️ 未找到交易服务进程"
exit 1
fi
echo "进程 PID: $TRADING_PID"
ps -p $TRADING_PID -o pid,vsz,rss,%mem,cmd
echo ""
# 2. 查看进程的内存映射(找出占用大的区域)
echo "📈 进程内存映射(前 20 行,按大小排序):"
if [ -f "/proc/$TRADING_PID/smaps" ]; then
cat /proc/$TRADING_PID/smaps 2>/dev/null | awk '/^Size:/ {size=$2} /^Rss:/ {rss=$2} /^Pss:/ {pss=$2} /^Name:/ {if (rss > 1024) print size" KB (RSS: "rss" KB) - " $2}' | sort -rn | head -20 || echo " 无法读取内存映射(需要 root 权限)"
else
echo " 无法访问 /proc/$TRADING_PID/smaps"
fi
echo ""
# 3. 查看交易服务日志中的内存相关错误
echo "🔍 检查交易服务日志:"
LOG_DIRS=(
"../trading_system/logs"
"logs"
"/www/wwwroot/autosys_new/trading_system/logs"
)
for LOG_DIR in "${LOG_DIRS[@]}"; do
if [ -d "$LOG_DIR" ]; then
echo "检查目录: $LOG_DIR"
# 查找内存相关错误
find "$LOG_DIR" -name "*.log" -type f -mtime -1 2>/dev/null | while read logfile; do
echo " 文件: $logfile"
# 查找内存错误
grep -i "memory\|oom\|out of memory\|memoryerror\|memory leak" "$logfile" 2>/dev/null | tail -5 || echo " 未找到内存相关错误"
# 查找最近的错误
tail -50 "$logfile" 2>/dev/null | grep -i "error\|exception\|failed" | tail -5 || echo " 未找到错误"
done
break
fi
done
echo ""
# 4. 查看系统内存压力
echo "💾 系统内存压力:"
free -h
echo ""
echo "内存使用率:"
free | awk 'NR==2{printf "已用: %.1f%%\n", $3*100/$2}'
echo ""
# 5. 检查是否有 swap 使用(如果有说明内存不足)
echo "🔄 Swap 使用情况:"
free | awk 'NR==3{if ($3 > 0) print "⚠️ Swap 正在使用: " $3 " KB (内存不足)"; else print "✓ Swap 未使用"}'
echo ""
# 6. 查看最近的交易服务输出
echo "📝 最近的交易服务输出(最后 30 行):"
for LOG_DIR in "${LOG_DIRS[@]}"; do
if [ -d "$LOG_DIR" ]; then
find "$LOG_DIR" -name "trading_*.log" -o -name "*.out.log" -type f 2>/dev/null | head -1 | while read logfile; do
if [ -f "$logfile" ]; then
tail -30 "$logfile" 2>/dev/null
break
fi
done
break
fi
done
echo ""
echo "=== 诊断完成 ==="
echo ""
echo "💡 可能的原因:"
echo " 1. K线数据缓存过大market_scanner 加载了太多历史K线"
echo " 2. 持仓数据或订单数据在内存中累积"
echo " 3. WebSocket 连接或消息队列占用过多内存"
echo " 4. 数据库查询结果集太大(未使用 LIMIT"
echo " 5. 内存泄漏(某个数据结构不断增长)"
echo ""
echo "💡 临时解决方案:"
echo " 1. 重启交易服务(释放内存)"
echo " 2. 检查配置中的缓存大小限制"
echo " 3. 减少扫描的交易对数量"
echo " 4. 检查是否有大量未关闭的数据库连接"

109
backend/诊断负载.sh Executable file
View File

@ -0,0 +1,109 @@
#!/bin/bash
# 快速诊断系统负载问题
echo "=== 系统负载诊断工具 ==="
echo ""
# 1. 当前负载
echo "📊 当前负载情况:"
uptime
echo ""
# 2. CPU 和内存使用
echo "💻 CPU 和内存使用:"
top -bn1 | head -5
echo ""
# 3. 查看占用 CPU 最高的进程
echo "🔥 CPU 占用最高的进程(前 10"
ps aux --sort=-%cpu | head -11 | awk '{printf "%-8s %-6s %-6s %-6s %s\n", $1, $2, $3"%", $4"%", $11}'
echo ""
# 4. 查看 Python 进程(交易服务)
echo "🐍 Python 进程(交易服务):"
PYTHON_PROCS=$(ps aux | grep -E "python.*trading|python.*main|uvicorn" | grep -v grep)
if [ -z "$PYTHON_PROCS" ]; then
echo " ⚠️ 未发现交易服务进程(服务可能未运行)"
else
echo "$PYTHON_PROCS" | awk '{printf "PID: %-6s CPU: %-5s MEM: %-5s CMD: %s\n", $2, $3"%", $4"%", $11" "$12" "$13" "$14}'
fi
echo ""
# 5. 检查是否有同步操作在运行
echo "🔄 检查同步操作:"
if [ -f "logs/api.log" ]; then
SYNC_LOGS=$(tail -100 logs/api.log | grep -i "同步\|sync.*binance\|sync_trades" | tail -10)
if [ -z "$SYNC_LOGS" ]; then
echo " 未找到同步日志(可能未执行同步操作)"
else
echo "最近的同步日志(最后 10 行):"
echo "$SYNC_LOGS"
fi
else
echo " ⚠️ 日志文件不存在backend 服务可能未运行)"
fi
echo ""
# 6. 检查数据库连接数
echo "🗄️ 数据库连接数:"
if command -v mysql >/dev/null 2>&1; then
DB_HOST="${DB_HOST:-localhost}"
DB_USER="${DB_USER:-root}"
DB_PASS="${DB_PASS:-}"
DB_NAME="${DB_NAME:-auto_trade_sys}"
if [ -n "$DB_PASS" ]; then
mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" -e "SHOW PROCESSLIST;" 2>/dev/null | head -20 || echo " 无法连接数据库"
else
mysql -h"$DB_HOST" -u"$DB_USER" -e "SHOW PROCESSLIST;" 2>/dev/null | head -20 || echo " 无法连接数据库(需要配置 DB_PASS"
fi
else
echo " mysql 客户端未安装"
fi
echo ""
# 7. 检查内存使用详情
echo "💾 内存使用详情:"
free -h
echo ""
# 8. 检查是否有大量 I/O 等待
echo "📈 I/O 和系统状态5秒采样"
vmstat 1 5
echo ""
# 9. 检查交易服务日志中的错误
echo "⚠️ 最近的错误日志(最后 5 条):"
if [ -f "logs/api.log" ]; then
tail -200 logs/api.log | grep -i "error\|exception\|failed\|timeout" | tail -5 || echo " 未找到错误日志"
fi
if [ -f "../trading_system/logs/trading_*.log" ] 2>/dev/null; then
tail -200 ../trading_system/logs/trading_*.log 2>/dev/null | grep -i "error\|exception\|failed" | tail -5 || echo ""
fi
echo ""
echo "=== 诊断完成 ==="
echo ""
echo "💡 说明:"
echo " - 此脚本可以在交易服务未运行时使用,用于检查系统整体负载"
echo " - 如果交易服务正在运行,会显示更详细的进程和日志信息"
echo ""
echo "💡 如果负载高,可能原因:"
echo " 1. Python 进程(交易服务)占用高:"
echo " - 市场扫描正在运行(计算技术指标)"
echo " - 订单同步正在运行(从币安拉取大量订单)"
echo " - 数据库查询慢(检查慢查询日志)"
echo ""
echo " 2. 其他进程占用高:"
echo " - 检查 top/htop 查看具体是哪个进程"
echo " - 可能是系统更新、备份等后台任务"
echo ""
echo " 3. 内存占用高:检查是否有内存泄漏"
echo ""
echo " 4. I/O 等待高:可能是数据库查询慢或磁盘慢"
echo ""
echo "💡 临时降负载方法:"
echo " - 暂停市场扫描(在配置中设置 SCAN_ENABLED=False"
echo " - 等待同步操作完成(不要手动取消)"
echo " - 重启交易服务(如果进程异常)"
echo " - 降低扫描并发(设置 SCAN_CONCURRENT_SYMBOLS=1"

View File

@ -0,0 +1,127 @@
# DB 与币安订单对账说明
## 一、查询系统今日落入 DB 的单子
### 1. 命令行脚本(推荐)
```bash
# 今日、默认账号、按创建时间(落库时间)
python scripts/query_trades_today.py
# 指定账号
python scripts/query_trades_today.py --account 2
# 指定日期
python scripts/query_trades_today.py --date 2026-02-21
# 按入场时间筛选
python scripts/query_trades_today.py --time-filter entry
# 仅可对账记录(有开仓/平仓订单号)
python scripts/query_trades_today.py --reconciled-only
# 导出到 JSON 文件
python scripts/query_trades_today.py -o today_trades.json
```
### 2. API 接口
```
GET /api/trades?period=today&time_filter=created&reconciled_only=false
```
- `period=today`:今天
- `time_filter=created`:按创建时间(落库时间),便于对照「何时写入 DB」
- `time_filter=entry`:按入场时间
- `time_filter=exit`:按平仓时间
- `reconciled_only=false`:包含所有记录(含取消、无订单号)
### 3. 前端导出
交易记录页面 → 选择「今天」→ 导出 JSON / Excel。
---
## 二、币安订单推送日志
系统会将收到的 **ORDER_TRADE_UPDATE**、**ALGO_UPDATE** 写入日志,便于与 DB 对照。
### 日志路径
```
{项目根}/logs/binance_order_events.log
```
### 格式
每行一条 JSON例如
```json
{"ts":1737500000000,"event_type":"ORDER_TRADE_UPDATE","account_id":1,"E":1737500000123,"symbol":"BTCUSDT","orderId":123456,"clientOrderId":"SYS_1737500000_abcd","event":"TRADE","status":"FILLED","reduceOnly":false,"avgPrice":"95000","executedQty":"0.01","realizedPnl":"0"}
```
### 字段说明
| 字段 | 说明 |
|------|------|
| ts | 本机接收时间戳 |
| event_type | ORDER_TRADE_UPDATE / ALGO_UPDATE |
| account_id | 账号 ID |
| E | 币安事件时间(毫秒) |
| symbol | 交易对 |
| orderId | 币安订单号 |
| clientOrderId | 自定义订单号(系统前缀) |
| event | NEW/TRADE/CANCELED |
| status | NEW/FILLED/CANCELED 等 |
| reduceOnly | 是否只减仓(平仓单) |
| avgPrice/executedQty | 成交价/成交量FILLED 时) |
| realizedPnl | 实现盈亏(平仓时) |
| algoId/triggeredOrderId | ALGO_UPDATE 专用 |
### 对账用法
```bash
# 查看今天收到的所有订单推送
grep "ORDER_TRADE_UPDATE" logs/binance_order_events.log
# 查看 FILLED 成交
grep '"status":"FILLED"' logs/binance_order_events.log
# 按 clientOrderId 对照
grep "SYS_1737500000_abcd" logs/binance_order_events.log
```
---
## 三、从币安拉取订单/成交DB 缺失时)
当 DB 记录查不到或需直接从币安做策略分析时,可用脚本拉取:
```bash
# 拉取最近 7 天成交记录(默认,适合策略分析)
python scripts/fetch_binance_orders.py --account 2 --symbol BTCUSDT
# 多个交易对
python scripts/fetch_binance_orders.py --account 2 --symbols ASTERUSDT,FILUSDT,PENGUUSDT
# 拉取订单列表
python scripts/fetch_binance_orders.py --account 2 --symbol BTCUSDT --type orders
# 指定天数、导出
python scripts/fetch_binance_orders.py --account 2 --symbol BTCUSDT --days 7 -o binance_trades.json
```
- `--type trades`:成交记录(含价格、数量、盈亏,策略分析推荐)
- `--type orders`:订单列表(含 FILLED/CANCELED
- 币安单次时间范围最多 7 天
---
## 四、对账流程建议
1. **查 DB 今日记录**`python scripts/query_trades_today.py -o db_today.json`
2. **查币安推送日志**`tail -f logs/binance_order_events.log` 或 `grep "ORDER_TRADE_UPDATE" logs/binance_order_events.log`
3. **对照**:用 `clientOrderId``orderId` 关联 DB 记录与推送日志,确认:
- DB 有 pending 且收到 FILLED 推送 → 应更新为 open
- DB 有 open 且收到 reduceOnly FILLED → 应更新 exit_order_id
- 收到推送但 DB 无对应记录 → 可能漏建或为手动单

View File

@ -0,0 +1,255 @@
# 山寨币专属策略配置更新总结
> 更新时间2026-01-24
> 核心理念:**高盈亏比 + 宽止损 + 快速止盈 + 精选时机**
## 📋 更新概述
基于交易记录分析和山寨币市场特性,从"波段趋势策略"转变为"山寨币高盈亏比狙击策略"。
## 🔧 核心配置变更
### 1. 风险控制参数(最关键)
| 参数 | 原值 | 新值 | 原因 |
|------|------|------|------|
| `ATR_STOP_LOSS_MULTIPLIER` | 2.5 | **2.0** | 山寨币波动大,止损要宽但不过宽 |
| `MIN_HOLD_TIME_SEC` | 1800 | **0** | **立即取消!**山寨币30分钟可能暴涨暴跌50% |
| `STOP_LOSS_PERCENT` | 0.10 | **0.15** | 固定止损15%(相对保证金) |
| `RISK_REWARD_RATIO` | 1.5 | **4.0** | 盈亏比必须≥4用大赢家覆盖亏损 |
| `USE_FIXED_RISK_SIZING` | True | **True** | 保持固定风险,避免亏损扩大 |
| `FIXED_RISK_PERCENT` | 0.02 | **0.01** | 每笔最多亏1%(山寨币风险高) |
| `ATR_TAKE_PROFIT_MULTIPLIER` | 1.5 | **8.0** | 止盈倍数提高到8盈亏比4:1 |
| `TAKE_PROFIT_PERCENT` | 0.25 | **0.60** | 固定止盈60%4:1盈亏比 |
### 2. 入场与出场优化
| 参数 | 原值 | 新值 | 原因 |
|------|------|------|------|
| `MIN_SIGNAL_STRENGTH` | 8 | **7** | 保持较高门槛但比8合理 |
| `AUTO_TRADE_ONLY_TRENDING` | True | **True** | 山寨币只做趋势明确的 |
| `SMART_ENTRY_ENABLED` | False | **True** | 开启智能入场,提高成交率 |
| `USE_TRAILING_STOP` | False | **True** | **必须开启!**山寨币利润要保护 |
| `TRAILING_STOP_ACTIVATION` | 0.10 | **0.30** | 盈利30%后激活(山寨币波动大) |
| `TRAILING_STOP_PROTECT` | 0.05 | **0.15** | 保护15%利润(给回撤足够空间) |
| `ENTRY_MAX_DRIFT_PCT_TRENDING` | 0.6 | **0.8** | 追价偏离放宽到0.8%(山寨币跳空大) |
| `ENTRY_SYMBOL_COOLDOWN_SEC` | 120 | **1800** | 同一币种冷却30分钟 |
### 3. 交易品种筛选
| 参数 | 原值 | 新值 | 原因 |
|------|------|------|------|
| `MIN_VOLUME_24H` | 5000000 | **30000000** | 24H成交额≥3000万美元过滤垃圾币 |
| `MIN_VOLUME_24H_STRICT` | 10000000 | **50000000** | 严格过滤≥5000万美元 |
| `MAX_SCAN_SYMBOLS` | 500 | **150** | 扫描前150个覆盖主流山寨 |
| `TOP_N_SYMBOLS` | 50 | **5** | 只做信号最强的5个专注优质机会 |
| `MIN_VOLATILITY` | 0.02 | **0.03** | 最小波动率3%,过滤死币 |
### 4. 仓位与频率控制
| 参数 | 原值 | 新值 | 原因 |
|------|------|------|------|
| `MAX_POSITION_PERCENT` | 0.08 | **0.015** | 单笔仓位1.5%,山寨币不加仓 |
| `MAX_TOTAL_POSITION_PERCENT` | 0.40 | **0.12** | 总仓位12%,保守控制总风险 |
| `MAX_DAILY_ENTRIES` | 8 | **5** | 每日最多5笔山寨币少做多看 |
| `MAX_OPEN_POSITIONS` | 3 | **4** | 同时持仓不超过4个 |
| `LEVERAGE` | 10 | **8** | 基础杠杆降到8倍山寨币波动大 |
| `MAX_LEVERAGE` | 15 | **12** | 最大杠杆12倍不要超过 |
| `USE_DYNAMIC_LEVERAGE` | True | **False** | 不使用动态杠杆(保持简单) |
### 5. 时间框架调整
| 参数 | 原值 | 新值 | 原因 |
|------|------|------|------|
| `PRIMARY_INTERVAL` | 1h | **4h** | 主周期用4小时过滤噪音 |
| `ENTRY_INTERVAL` | 15m | **1h** | 入场周期1小时避免太小的时间框架 |
| `CONFIRM_INTERVAL` | 4h | **1d** | 确认周期用日线,看大趋势 |
| `SCAN_INTERVAL` | 1800 | **3600** | 扫描间隔1小时3600秒 |
## 📈 山寨币专用策略逻辑
### 1. 止损策略:宽但坚决
```
ATR倍数2.0 + 固定止损15%(哪个先触发用哪个)
不设持仓锁:触及止损立即离场
逻辑山寨币正常波动10-20%很常见,止损要容忍正常波动,但不能容忍趋势反转
```
### 2. 止盈策略:分批 + 移动止损
```
第一目标盈亏比1:1快速锁定30-50%利润)
第二目标盈亏比4:1剩余仓位追求大赢家
移动止损盈利30%后激活保护15%利润
逻辑山寨币可能暴涨100%+,也可能瞬间反转,要快速锁定部分利润
```
### 3. 品种选择:流动性为王
```
合格山寨币标准:
1. 24小时成交额 > 3000万美元
2. 市值排名前150
3. 有明确趋势4小时+日线)
4. 波动率 ≥ 3%
5. 不在异常暴涨暴跌期间
```
### 4. 时机选择:跟随大盘
```
只在BTC处于明确趋势时交易山寨币
AUTO_TRADE_ONLY_TRENDING = True
AUTO_TRADE_ALLOW_4H_NEUTRAL = False
```
## 💰 数学期望计算
### 优化后目标
```
胜率35%(山寨币难有高胜率)
盈亏比4.0
固定风险每笔1%
期望值 = (胜率 × 盈亏比) - (1 - 胜率)
= (0.35 × 4.0) - 0.65
= 1.4 - 0.65
= 0.75
每笔交易平均盈利0.75个风险单位即总资金的0.75%
```
### 与现状对比
```
现状:
- 胜率30%
- 盈亏比0.91:1
- 期望值:(0.30 × 0.91) - 0.70 = -0.427(严重亏损)
优化后:
- 胜率35%(目标)
- 盈亏比4.0:1
- 期望值:+0.75(盈利)
改善:从-42.7%变为+75%期望值提升117.7%
```
## ⚠️ 山寨币交易铁律
1. **绝不扛单**亏损15%无条件离场
2. **绝不加仓**:山寨币没有"摊平成本",只有越亏越多
3. **绝不做空低流通币**:容易被轧空
4. **绝不信消息**:只信价格和成交量
5. **仓位永远小于主流币**单笔不超过1.5%
## 🎯 执行计划
### 第一阶段:配置更新(今天)
1. ✅ 更新 `trading_system/config.py` 中的所有配置默认值
2. ✅ 更新 `trade_recommender.py` 中的分批止盈逻辑
3. ⏳ 重启所有trading_system进程使新配置生效
4. ⏳ 在Redis中清除旧配置缓存或等待自动过期
### 第二阶段回测验证1-2天
1. 用极小实盘单笔0.5%)测试新策略
2. 记录每笔交易的:
- 入场信号强度
- 最大浮盈
- 是否触及止损/止盈
- 持仓时间
- 退出原因
3. 目标胜率35-40%盈亏比3.5-4.5
### 第三阶段正式运行3天后
1. 单笔风险1%总仓位不超过10%
2. 每日最多交易3-5笔
3. 每周复盘,调整过滤条件
4. 持续监控盈亏比和期望值
## 📊 关键指标监控
### 必须监控的指标
1. **实际盈亏比**:必须 > 3.5目标4.0
2. **盈利因子**:总盈利 / 总亏损,必须 > 1.1
3. **平均持仓时间**应该在1-4小时之间
4. **最大回撤**单日不超过总资金的5%
5. **胜率**目标35-40%
### 预警阈值
- 盈亏比 < 3.0立即暂停交易检查策略
- 胜率 < 25%信号质量有问题提高MIN_SIGNAL_STRENGTH
- 单日亏损 > 3%:暂停交易,检查市场环境
- 连续亏损 > 5笔暂停交易等待市场转好
## 🔄 后续优化方向
### 短期1周内
1. 监控并微调 `MIN_SIGNAL_STRENGTH`7-8之间
2. 根据实际情况微调 `ATR_STOP_LOSS_MULTIPLIER`1.8-2.2之间)
3. 观察并记录哪些币种表现最好
### 中期1月内
1. 实现按市值分级的动态参数见summary中的伪代码
2. 添加BTC趋势过滤BTC下跌时不做山寨币多单
3. 优化移动止损的激活和保护参数
### 长期3月内
1. 建立山寨币白名单/黑名单机制
2. 实现资金管理优化(凯利公式动态调整)
3. 开发山寨币专用的技术指标组合
## 📝 配置文件清单
已更新的文件:
- ✅ `trading_system/config.py` - 核心配置默认值
- ✅ `trading_system/trade_recommender.py` - 推荐生成逻辑
- ⏳ `backend/config_manager.py` - 配置管理器默认值(待更新)
- ⏳ `backend/api/routes/config.py` - API配置元数据待更新
## ⚡ 立即执行的操作
```bash
# 1. 重启所有trading_system进程使新配置生效
supervisorctl restart auto_sys:*
# 2. 重启推荐服务
supervisorctl restart auto_recommend:*
# 3. 查看日志确认新配置已生效
tail -f /www/wwwroot/autosys_new/logs/trading_*.log
# 4. 检查配置是否正确加载
# 在日志中查找以下关键配置:
# - ATR_STOP_LOSS_MULTIPLIER: 2.0
# - RISK_REWARD_RATIO: 4.0
# - MIN_HOLD_TIME_SEC: 0
# - USE_TRAILING_STOP: True
```
## ✅ 验证清单
- [ ] ATR止损倍数 = 2.0
- [ ] 盈亏比 = 4.0
- [ ] 最小持仓时间 = 0已取消
- [ ] 移动止损已启用激活30%保护15%
- [ ] 智能入场已启用
- [ ] 单笔仓位 ≤ 1.5%
- [ ] 总仓位 ≤ 12%
- [ ] 每日最多5笔
- [ ] 基础杠杆 = 8倍
- [ ] 24H成交量 ≥ 3000万美元
---
**重要提醒**配置更新后务必密切监控前3-5笔交易确保新策略按预期运行。如有异常立即暂停并检查日志。

View File

@ -0,0 +1,259 @@
# ATR使用合理性分析与优化建议2026-01-27
## 📊 交易数据统计
### 基本统计基于交易记录_2026-01-27T02-26-05.json
**总交易数**20单
- **持仓中**6单30%
- **已平仓**14单70%
**已平仓交易分析**
- **止盈单**2单14.3%
- CHZUSDT BUY: +24.51%
- ZROUSDT SELL: +30.18%
- **止损单**10单71.4%
- 盈利单3单AXSUSDT +4.93%, AXLUSDT +7.78%, AXSUSDT +12.04%
- 亏损单7单-0.95%, -0.61%, -12.33%, -13.88%, -11.88%, -31.56%, -12.03%
- **同步平仓**2单14.3%
- AUCTIONUSDT BUY: -12.22%
- ZETAUSDT BUY: -35.54%
- AXSUSDT SELL: -16.37%
**胜率分析**
- 已平仓14单
- 盈利单5单35.7%
- 亏损单9单64.3%
- **胜率35.7%**(严重偏低)
**严重问题单**
- AXSUSDT SELL: -65.84%巨额亏损SELL单止损错误
- ZETAUSDT BUY: -35.54%(巨额亏损)
- JTOUSDT BUY: -31.56%(巨额亏损)
---
## 🔍 ATR使用合理性分析
### 当前ATR配置
- `USE_ATR_STOP_LOSS`: True
- `ATR_STOP_LOSS_MULTIPLIER`: 2.0
- `ATR_TAKE_PROFIT_MULTIPLIER`: 3.0
- `STOP_LOSS_PERCENT`: 0.1212%
- `TAKE_PROFIT_PERCENT`: 0.2020%
---
### ATR止损计算逻辑
**计算步骤**`risk_manager.py:602-760`
1. **ATR止损价**`entry_price × (1 ± ATR% × 2.0)`
2. **保证金止损价**:基于`STOP_LOSS_PERCENT`12%
3. **价格百分比止损价**:基于`MIN_STOP_LOSS_PRICE_PCT`2%
4. **选择最终的止损价**:取"更紧"的(更接近入场价)✅ 已修复
**问题分析**
- ✅ SELL单止损选择逻辑已修复选择更紧的止损
- ⚠️ 但ATR止损倍数2.0可能仍然过宽
- ⚠️ 如果ATR很大比如5%2.0倍就是10%的止损距离
- ⚠️ 对于山寨币10%的止损距离可能过大,导致巨额亏损
---
### ATR止盈计算逻辑
**计算步骤**`risk_manager.py:772-844`
1. **ATR止盈价**:基于`ATR_TAKE_PROFIT_MULTIPLIER`3.0
2. **保证金止盈价**:基于`TAKE_PROFIT_PERCENT`20%
3. **价格百分比止盈价**:基于`MIN_TAKE_PROFIT_PRICE_PCT`3%
4. **选择最终的止盈价**:取"更宽松"的(更远离入场价)❌ 问题
**问题分析**
- ❌ 选择"更宽松"的止盈,导致止盈目标过高
- ❌ 如果ATR很大比如5%3.0倍就是15%的止盈距离
- ❌ 对于山寨币15%的止盈距离可能过高导致止盈单比例过低14.3%
---
## 🚨 核心问题
### 问题1ATR止损倍数可能过宽
**当前配置**
- `ATR_STOP_LOSS_MULTIPLIER`: 2.0
**问题**
- 如果ATR = 5%,止损距离 = 5% × 2.0 = 10%
- 对于8倍杠杆10%的价格变动 = 80%的保证金变动
- 这可能导致巨额亏损(如-65.84%
**建议**
- 收紧ATR止损倍数2.0 → **1.5**
- 既能容忍波动,又能控制风险
---
### 问题2ATR止盈倍数可能过高
**当前配置**
- `ATR_TAKE_PROFIT_MULTIPLIER`: 3.0
**问题**
- 如果ATR = 5%,止盈距离 = 5% × 3.0 = 15%
- 对于8倍杠杆15%的价格变动 = 120%的保证金变动
- 这可能导致止盈目标过高,难以触发
- 止盈单比例过低14.3%
**建议**
- 降低ATR止盈倍数3.0 → **2.0**
- 更容易触发止盈,提升止盈单比例
---
### 问题3止盈选择逻辑问题
**当前逻辑**
- 选择"更宽松"的止盈(更远离入场价)
**问题**
- 导致止盈目标过高,难以触发
- 止盈单比例过低14.3%
**建议**
- 选择"更紧"的止盈(更接近入场价),更容易触发
- 或者优先使用固定百分比止盈20%而不是ATR止盈
---
## ✅ 优化建议
### 建议1收紧ATR止损倍数紧急
**当前配置**
- `ATR_STOP_LOSS_MULTIPLIER`: 2.0
**建议配置**
- `ATR_STOP_LOSS_MULTIPLIER`: **1.5**
**理由**
- 2.0倍对于山寨币来说可能过宽
- 收紧到1.5倍,既能容忍波动,又能控制风险
- 配合12%的固定止损,应该能更好地控制风险
**预期效果**
- 减少巨额亏损单(-65.84%, -35.54%, -31.56%
- 减少单笔亏损幅度
---
### 建议2降低ATR止盈倍数重要
**当前配置**
- `ATR_TAKE_PROFIT_MULTIPLIER`: 3.0
**建议配置**
- `ATR_TAKE_PROFIT_MULTIPLIER`: **2.0**
**理由**
- 3.0倍对于山寨币来说可能过高
- 降低到2.0倍,更容易触发止盈
- 配合20%的固定止盈,应该能提升止盈单比例
**预期效果**
- 提升止盈单比例从14.3%提升到30%+
- 更容易触发止盈,锁定利润
---
### 建议3优化止盈选择逻辑建议
**当前逻辑**
- 选择"更宽松"的止盈(更远离入场价)
**建议逻辑**
- 选择"更紧"的止盈(更接近入场价),更容易触发
- 或者优先使用固定百分比止盈20%而不是ATR止盈
**理由**
- 固定百分比止盈20%)更容易触发
- ATR止盈可能过高导致止盈单比例过低
---
## 📊 配置调整建议
### 当前配置(问题)
- `ATR_STOP_LOSS_MULTIPLIER`: 2.0(可能过宽)
- `ATR_TAKE_PROFIT_MULTIPLIER`: 3.0(可能过高)
- `STOP_LOSS_PERCENT`: 0.1212%
- `TAKE_PROFIT_PERCENT`: 0.2020%
### 建议配置(优化)
- `ATR_STOP_LOSS_MULTIPLIER`: **1.5**(收紧止损)
- `ATR_TAKE_PROFIT_MULTIPLIER`: **2.0**(降低止盈目标)
- `STOP_LOSS_PERCENT`: **0.12**12%,保持)
- `TAKE_PROFIT_PERCENT`: **0.20**20%,保持)
---
## 🎯 预期效果
### 优化后预期
**止损单比例**
- 当前71.4%
- 预期50% - 60%
**止盈单比例**
- 当前14.3%
- 预期30% - 40%
**胜率**
- 当前35.7%
- 预期45% - 55%
**盈亏比**
- 当前:需要计算
- 预期1.5:1 - 2.0:1
**巨额亏损单**
- 当前:-65.84%, -35.54%, -31.56%
- 预期:减少或消除巨额亏损单
---
## ⚠️ 注意事项
1. **ATR倍数调整**
- 收紧ATR止损倍数减少单笔亏损
- 降低ATR止盈倍数提升止盈单比例
2. **止损选择逻辑**
- 已修复SELL单的止损选择逻辑
- 应该能减少巨额亏损单
3. **止盈选择逻辑**
- 建议优化止盈选择逻辑,优先使用固定百分比止盈
---
## ✅ 总结
**ATR使用合理性**
- ⚠️ ATR止损倍数2.0可能过宽建议收紧到1.5
- ⚠️ ATR止盈倍数3.0可能过高建议降低到2.0
- ⚠️ 止盈选择逻辑建议优化,优先使用固定百分比止盈
**优化建议**
- ✅ 收紧ATR止损倍数2.0 → 1.5
- ✅ 降低ATR止盈倍数3.0 → 2.0
- ✅ 保持固定止损止盈12% / 20%
**预期效果**
- ✅ 减少巨额亏损单
- ✅ 提升止盈单比例
- ✅ 提升胜率
- ✅ 改善盈亏比

View File

@ -0,0 +1,133 @@
# ATR配置优化完成总结2026-01-27
## 🎯 优化目标
**结合ATR的使用优化配置减少巨额亏损单提升止盈单比例提升胜率**
---
## ✅ 已完成的优化
### 1. 收紧ATR止损倍数
**修改位置**
- `trading_system/config.py`
- `backend/config_manager.py`
- `frontend/src/components/GlobalConfig.jsx`
- `frontend/src/components/ConfigPanel.jsx`
**优化内容**
- `ATR_STOP_LOSS_MULTIPLIER`: 2.0 → **1.5**
**理由**
- 2.0倍对于山寨币来说可能过宽
- 如果ATR = 5%,止损距离 = 5% × 2.0 = 10%
- 对于8倍杠杆10%的价格变动 = 80%的保证金变动
- 收紧到1.5倍,既能容忍波动,又能控制风险
---
### 2. 降低ATR止盈倍数
**修改位置**
- `trading_system/config.py`
- `backend/config_manager.py`
- `frontend/src/components/GlobalConfig.jsx`
- `frontend/src/components/ConfigPanel.jsx`
**优化内容**
- `ATR_TAKE_PROFIT_MULTIPLIER`: 3.0 → **2.0**
**理由**
- 3.0倍对于山寨币来说可能过高
- 如果ATR = 5%,止盈距离 = 5% × 3.0 = 15%
- 对于8倍杠杆15%的价格变动 = 120%的保证金变动
- 降低到2.0倍,更容易触发止盈
---
### 3. 优化止盈选择逻辑
**修改位置**`trading_system/risk_manager.py:852-866`
**优化前**
- 选择"更宽松"的止盈(更远离入场价)
- 导致止盈目标过高,难以触发
**优化后**
- 选择"更紧"的止盈(更接近入场价),更容易触发
- 优先使用固定百分比止盈20%而不是ATR止盈
**理由**
- 固定百分比止盈20%)更容易触发
- ATR止盈可能过高导致止盈单比例过低
---
## 📊 预期效果
### 优化后预期
**止损单比例**
- 当前71.4%
- 预期50% - 60%
**止盈单比例**
- 当前14.3%
- 预期30% - 40%
**胜率**
- 当前35.7%
- 预期45% - 55%
**巨额亏损单**
- 当前:-65.84%, -35.54%, -31.56%
- 预期:减少或消除巨额亏损单
---
## 🔧 配置调整清单
### 已调整的配置项
| 配置项 | 原值 | 优化值 | 变化 | 理由 |
|--------|------|--------|------|------|
| `ATR_STOP_LOSS_MULTIPLIER` | 2.0 | **1.5** | ↓ | 收紧止损,减少单笔亏损 |
| `ATR_TAKE_PROFIT_MULTIPLIER` | 3.0 | **2.0** | ↓ | 降低止盈目标,更容易触发 |
| 止盈选择逻辑 | 更宽松 | **更紧** | ↑ | 更容易触发止盈 |
---
## ⚠️ 注意事项
1. **ATR倍数调整**
- 收紧ATR止损倍数减少单笔亏损
- 降低ATR止盈倍数提升止盈单比例
2. **止盈选择逻辑**
- 已优化:选择"更紧"的止盈,更容易触发
- 优先使用固定百分比止盈20%而不是ATR止盈
3. **止损选择逻辑**
- 已修复SELL单选择"更紧"的止损
- 应该能减少巨额亏损单
---
## ✅ 总结
**ATR使用合理性**
- ⚠️ ATR止损倍数2.0过宽 → 已优化为1.5
- ⚠️ ATR止盈倍数3.0过高 → 已优化为2.0
- ⚠️ 止盈选择逻辑问题 → 已优化为选择"更紧"的止盈
**优化效果**
- ✅ 减少巨额亏损单
- ✅ 提升止盈单比例
- ✅ 提升胜率
- ✅ 改善盈亏比
**下一步**
- 清除Redis缓存
- 重启交易进程
- 监控效果

View File

@ -0,0 +1,282 @@
# 配置架构验证文档
## 📋 验证目标
确认:
1. ✅ **所有用户下的账户都使用全局策略配置**
2. ✅ **普通用户无法通过自己的配置直接影响核心策略参数**
---
## 🏗️ 配置架构设计
### 1. 配置层级
```
┌─────────────────────────────────────────────────────────┐
│ 全局策略账号 (account_id=1, 默认) │
│ - 存储所有核心策略参数 │
│ - 例如ATR_STOP_LOSS_MULTIPLIER, ATR_TAKE_PROFIT_... │
└─────────────────────────────────────────────────────────┘
↓ 读取
┌─────────────────────────────────────────────────────────┐
│ 用户账户 (account_id=2, 3, 4...) │
│ - 存储风险旋钮(每个账户独立) │
│ - 例如MAX_POSITION_PERCENT, AUTO_TRADE_ENABLED... │
└─────────────────────────────────────────────────────────┘
```
### 2. 配置读取逻辑
**位置**`backend/config_manager.py` 的 `get_trading_config()` 方法
```python
def eff_get(key: str, default: Any):
"""
策略核心默认从全局账号读取GLOBAL_STRATEGY_ACCOUNT_ID
风险旋钮:从当前账号读取。
"""
# API key/secret/testnet 永远按账号读取
if key in RISK_KNOBS_KEYS or global_mgr is None:
return self.get(key, default) # 从当前账号读取
try:
# 从全局账号读取
return global_mgr.get(key, default)
except Exception:
return self.get(key, default)
```
**风险旋钮列表**`RISK_KNOBS_KEYS`
- `MIN_MARGIN_USDT`
- `MIN_POSITION_PERCENT`
- `MAX_POSITION_PERCENT`
- `MAX_TOTAL_POSITION_PERCENT`
- `AUTO_TRADE_ENABLED`
- `MAX_OPEN_POSITIONS`
- `MAX_DAILY_ENTRIES`
**核心策略参数**(从全局账号读取):
- `ATR_STOP_LOSS_MULTIPLIER`
- `ATR_TAKE_PROFIT_MULTIPLIER`
- `RISK_REWARD_RATIO`
- `USE_FIXED_RISK_SIZING`
- `FIXED_RISK_PERCENT`
- `USE_DYNAMIC_ATR_MULTIPLIER`
- `MIN_SIGNAL_STRENGTH`
- `SCAN_INTERVAL`
- `TOP_N_SYMBOLS`
- ... 等等所有非风险旋钮的配置
---
## 🔒 权限控制
### 1. 后端API权限控制
**位置**`backend/api/routes/config.py`
#### GET `/api/config` - 获取配置列表
```python
# 普通用户:只展示风险旋钮 + 账号密钥
# 管理员:若当前不是"全局策略账号",同样只展示风险旋钮
is_admin = (user.get("role") or "user") == "admin"
gid = _global_strategy_account_id()
if (not is_admin) or (is_admin and int(account_id) != int(gid)):
allowed = set(USER_RISK_KNOBS) | {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}
result = {k: v for k, v in result.items() if k in allowed}
```
**验证**
- ✅ 普通用户只能看到 `USER_RISK_KNOBS` + API密钥
- ✅ 管理员在非全局策略账号时,也只能看到风险旋钮
- ✅ 只有管理员在全局策略账号时,才能看到所有配置
#### PUT `/api/config/{key}` - 更新单个配置
```python
# 管理员:若不是全局策略账号,则禁止修改策略核心
if (user.get("role") or "user") == "admin":
gid = _global_strategy_account_id()
if int(account_id) != int(gid):
if key not in (USER_RISK_KNOBS | {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}):
raise HTTPException(status_code=403, detail=f"该配置由全局策略账号 #{gid} 统一管理")
# 产品模式:普通用户只能改"风险旋钮"与账号私有密钥/测试网
if (user.get("role") or "user") != "admin":
if key not in (USER_RISK_KNOBS | {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}):
raise HTTPException(status_code=403, detail="该配置由平台统一管理(仅管理员可修改)")
```
**验证**
- ✅ 普通用户尝试修改核心策略参数会返回 403 错误
- ✅ 管理员在非全局策略账号时,也无法修改核心策略参数
- ✅ 只有管理员在全局策略账号时,才能修改核心策略参数
#### POST `/api/config/batch` - 批量更新配置
```python
for item in configs:
# 管理员:若不是全局策略账号,则批量只允许风险旋钮/密钥
if (user.get("role") or "user") == "admin":
gid = _global_strategy_account_id()
if int(account_id) != int(gid):
if item.key not in (USER_RISK_KNOBS | {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}):
errors.append(f"{item.key}: 该配置由全局策略账号 #{gid} 统一管理,请切换账号修改")
continue
# 产品模式:普通用户只能改"风险旋钮"与账号私有密钥/测试网
if (user.get("role") or "user") != "admin":
if item.key not in (USER_RISK_KNOBS | {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}):
errors.append(f"{item.key}: 该配置由平台统一管理(仅管理员可修改)")
continue
```
**验证**
- ✅ 普通用户批量更新时,核心策略参数会被过滤并返回错误
- ✅ 管理员在非全局策略账号时,核心策略参数也会被过滤
---
## ✅ 验证结果
### 1. 所有账户使用全局策略配置 ✅
**验证点**
- `config_manager.py``get_trading_config()` 方法中,所有非风险旋钮的配置都通过 `eff_get()` 从全局账号读取
- 即使普通用户在自己的账户中设置了核心策略参数,也不会生效(因为读取时从全局账号读取)
**代码位置**
- `backend/config_manager.py:509-522`
**结论**:✅ **所有账户都使用全局策略配置**
---
### 2. 普通用户无法修改核心策略参数 ✅
**验证点**
- **前端限制**普通用户在配置页面只能看到风险旋钮通过API过滤
- **后端限制**
- GET `/api/config`:只返回风险旋钮
- PUT `/api/config/{key}`:尝试修改核心参数返回 403
- POST `/api/config/batch`:核心参数被过滤并返回错误
**代码位置**
- `backend/api/routes/config.py:273-280` (GET)
- `backend/api/routes/config.py:645-655` (PUT)
- `backend/api/routes/config.py:765-776` (POST)
**结论**:✅ **普通用户无法通过自己的配置直接影响核心策略参数**
---
## 📊 配置分类总结
### 风险旋钮(每个账户独立)
- `MIN_MARGIN_USDT` - 最小保证金USDT
- `MIN_POSITION_PERCENT` - 最小仓位占比
- `MAX_POSITION_PERCENT` - 最大仓位占比
- `MAX_TOTAL_POSITION_PERCENT` - 总仓位占比上限
- `AUTO_TRADE_ENABLED` - 自动交易开关
- `MAX_OPEN_POSITIONS` - 同时持仓数量上限
- `MAX_DAILY_ENTRIES` - 每日最多开仓次数
### 核心策略参数(全局统一)
- `ATR_STOP_LOSS_MULTIPLIER` - ATR止损倍数
- `ATR_TAKE_PROFIT_MULTIPLIER` - ATR止盈倍数
- `RISK_REWARD_RATIO` - 盈亏比
- `USE_FIXED_RISK_SIZING` - 使用固定风险百分比
- `FIXED_RISK_PERCENT` - 固定风险百分比
- `USE_DYNAMIC_ATR_MULTIPLIER` - 动态ATR倍数
- `MIN_SIGNAL_STRENGTH` - 最小信号强度
- `SCAN_INTERVAL` - 扫描间隔
- `TOP_N_SYMBOLS` - 每次扫描处理的交易对数量
- ... 等等所有非风险旋钮的配置
### 账号私有配置(每个账户独立)
- `BINANCE_API_KEY` - 币安API密钥
- `BINANCE_API_SECRET` - 币安API密钥
- `USE_TESTNET` - 是否使用测试网
---
## 🎯 实际运行验证
### 测试场景1普通用户查看配置
1. 普通用户登录
2. 进入配置页面
3. **预期**:只能看到风险旋钮 + API密钥配置
4. **验证**:前端只显示允许的配置项
### 测试场景2普通用户尝试修改核心策略参数
1. 普通用户登录
2. 尝试通过API修改 `ATR_STOP_LOSS_MULTIPLIER`
3. **预期**:返回 403 错误:"该配置由平台统一管理(仅管理员可修改)"
4. **验证**:后端拒绝修改请求
### 测试场景3管理员在非全局策略账号修改核心策略参数
1. 管理员登录
2. 切换到非全局策略账号(如 account_id=2
3. 尝试修改 `ATR_STOP_LOSS_MULTIPLIER`
4. **预期**:返回 403 错误:"该配置由全局策略账号 #1 统一管理,请切换到该账号修改"
5. **验证**:后端拒绝修改请求
### 测试场景4管理员在全局策略账号修改核心策略参数
1. 管理员登录
2. 切换到全局策略账号account_id=1
3. 修改 `ATR_STOP_LOSS_MULTIPLIER = 2.5`
4. **预期**:修改成功
5. **验证**:所有账户的交易系统都会使用新的值(通过 `config_manager.get_trading_config()` 读取)
---
## 🔍 代码检查清单
- [x] `backend/config_manager.py` - 配置读取逻辑使用全局账号
- [x] `backend/api/routes/config.py` - API权限控制
- [x] `frontend/src/components/ConfigPanel.jsx` - 前端配置页面(依赖后端过滤)
- [x] `frontend/src/components/GlobalConfig.jsx` - 管理员全局配置页面
---
## ✅ 最终结论
1. ✅ **所有用户下的账户都使用全局策略配置**
- 通过 `config_manager.get_trading_config()``eff_get()` 函数实现
- 核心策略参数从全局账号account_id=1读取
- 风险旋钮从当前账号读取
2. ✅ **普通用户无法通过自己的配置直接影响核心策略参数**
- 前端:只能看到风险旋钮
- 后端:尝试修改核心参数会返回 403 错误
- 即使数据库中有值,读取时也会从全局账号读取
3. ✅ **管理员权限控制**
- 管理员在非全局策略账号时,也只能修改风险旋钮
- 只有管理员在全局策略账号时,才能修改核心策略参数
---
## 📝 注意事项
1. **全局策略账号ID**:默认是 `account_id=1`,可通过环境变量 `ATS_GLOBAL_STRATEGY_ACCOUNT_ID` 修改
2. **配置缓存**:配置存储在 Redis 中,修改后需要确保 Redis 缓存已更新
3. **配置生效**:修改全局策略配置后,所有账户的交易系统会在下次 `reload_from_redis()` 时读取新值
4. **风险旋钮的作用**:虽然核心策略参数是全局的,但每个账户可以通过风险旋钮控制:
- 仓位大小MAX_POSITION_PERCENT
- 交易频率MAX_DAILY_ENTRIES
- 同时持仓数量MAX_OPEN_POSITIONS
- 是否启用自动交易AUTO_TRADE_ENABLED
---
## 🎯 建议
1. **定期检查**:定期验证全局策略账号的配置是否正确
2. **配置快照**:在修改全局策略配置前,先导出配置快照作为备份
3. **测试环境**:在测试环境验证配置修改的效果,再应用到生产环境
4. **文档更新**:修改配置后,及时更新相关文档

View File

@ -0,0 +1,266 @@
# 全局配置独立化迁移说明
## 📋 概述
将全局策略配置从依赖 `account_id=1` 改为独立的配置系统,使用独立的 `global_strategy_config` 表和 Redis 缓存。
## 🎯 目标
1. ✅ 全局配置不再依赖任何账户account_id
2. ✅ 独立的数据库表 `global_strategy_config`
3. ✅ 独立的 Redis 缓存键 `global_strategy_config`
4. ✅ 只有管理员可以查看和修改全局配置
5. ✅ 所有账户自动使用全局配置(通过 `config_manager.py` 读取)
---
## 📦 数据库变更
### 1. 创建新表
执行迁移脚本:
```bash
mysql -u your_user -p auto_trade_sys < backend/database/add_global_strategy_config.sql
```
**新表结构**
```sql
CREATE TABLE `global_strategy_config` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`config_key` VARCHAR(100) NOT NULL,
`config_value` TEXT NOT NULL,
`config_type` VARCHAR(50) NOT NULL,
`category` VARCHAR(50) NOT NULL,
`description` TEXT,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`updated_by` VARCHAR(50),
UNIQUE KEY `uk_config_key` (`config_key`)
)
```
### 2. 数据迁移
迁移脚本会自动将 `account_id=1` 的核心策略配置迁移到 `global_strategy_config` 表。
**迁移规则**
- 只迁移非风险旋钮的配置
- 风险旋钮(`MIN_MARGIN_USDT`, `MAX_POSITION_PERCENT` 等)不迁移
- API密钥`BINANCE_API_KEY`, `BINANCE_API_SECRET`)不迁移
---
## 🔧 代码变更
### 1. 数据库模型 (`backend/database/models.py`)
**新增**`GlobalStrategyConfig` 类
- `get_all()` - 获取所有全局配置
- `get(key)` - 获取单个配置
- `set(key, value, ...)` - 设置配置
- `get_value(key, default)` - 获取配置值(自动转换类型)
- `delete(key)` - 删除配置
### 2. 配置管理器 (`backend/config_manager.py`)
**新增**`GlobalStrategyConfigManager` 类
- 独立的 Redis 缓存键:`global_strategy_config`
- 独立的数据库表:`global_strategy_config`
- 单例模式,确保全局唯一
**修改**`ConfigManager.get_trading_config()`
- 从 `GlobalStrategyConfigManager` 读取全局配置
- 不再依赖 `account_id=1`
- 风险旋钮仍从当前账户读取
### 3. API 路由 (`backend/api/routes/config.py`)
**新增端点**
- `GET /api/config/global` - 获取全局配置(仅管理员)
- `PUT /api/config/global/{key}` - 更新单个全局配置(仅管理员)
- `POST /api/config/global/batch` - 批量更新全局配置(仅管理员)
**修改端点**
- `GET /api/config/meta` - 移除 `global_strategy_account_id` 字段
**权限控制**
- 所有全局配置端点都检查管理员权限
- 非管理员访问返回 403 错误
### 4. 前端 API 服务 (`frontend/src/services/api.js`)
**修改**
- `getGlobalConfigs()` - 不再需要 `globalAccountId` 参数
- `updateGlobalConfigsBatch()` - 不再需要 `globalAccountId` 参数
### 5. 前端组件 (`frontend/src/components/GlobalConfig.jsx`)
**移除**
- 所有对 `configMeta.global_strategy_account_id` 的引用
- 所有对 `globalAccountId` 的计算和使用
**简化**
- `loadConfigs()` - 直接调用 `api.getGlobalConfigs()`,无需 account_id
- `handleApplyPreset()` - 直接调用 `api.updateGlobalConfigsBatch()`,无需 account_id
- `buildConfigSnapshot()` - 直接调用 `api.getGlobalConfigs()`,无需 account_id
---
## 🔄 迁移步骤
### 步骤1执行数据库迁移
```bash
cd /path/to/auto_trade_sys
mysql -u your_user -p auto_trade_sys < backend/database/add_global_strategy_config.sql
```
### 步骤2重启后端服务
```bash
# 重启 FastAPI 后端
systemctl restart your-backend-service
# 或
supervisorctl restart backend
```
### 步骤3重启交易系统
```bash
# 重启所有交易进程,使新配置生效
supervisorctl restart all
```
### 步骤4验证
1. **管理员登录**,进入"全局配置"页面
2. **检查配置项**:应该能看到所有核心策略配置
3. **修改配置**:尝试修改一个配置项,确认保存成功
4. **检查数据库**:确认 `global_strategy_config` 表中有数据
5. **检查 Redis**:确认 `global_strategy_config` 键中有缓存
---
## ⚠️ 注意事项
### 1. 向后兼容
- 如果 `global_strategy_config` 表不存在,系统会回退到从 `account_id=1` 读取(兼容旧系统)
- 迁移脚本会自动迁移现有配置,不会丢失数据
### 2. Redis 缓存
- 全局配置使用独立的 Redis 键:`global_strategy_config`
- 账户配置仍使用:`trading_config:{account_id}`
- 修改全局配置后,会自动更新 Redis 缓存
### 3. 权限控制
- **管理员**:可以查看和修改全局配置
- **普通用户**:无法访问全局配置 API返回 403
- 普通用户只能修改自己账户的风险旋钮
### 4. 配置读取优先级
1. **风险旋钮**:从当前账户的 `trading_config` 表读取
2. **核心策略参数**:从 `global_strategy_config` 表读取
3. **API密钥**:从 `accounts` 表读取(每个账户独立)
---
## 📊 配置分类
### 全局配置(管理员专用)
- `ATR_STOP_LOSS_MULTIPLIER`
- `ATR_TAKE_PROFIT_MULTIPLIER`
- `RISK_REWARD_RATIO`
- `USE_FIXED_RISK_SIZING`
- `FIXED_RISK_PERCENT`
- `USE_DYNAMIC_ATR_MULTIPLIER`
- `MIN_SIGNAL_STRENGTH`
- `SCAN_INTERVAL`
- `TOP_N_SYMBOLS`
- ... 等等所有非风险旋钮的配置
### 账户配置(每个账户独立)
- `MIN_MARGIN_USDT`
- `MIN_POSITION_PERCENT`
- `MAX_POSITION_PERCENT`
- `MAX_TOTAL_POSITION_PERCENT`
- `AUTO_TRADE_ENABLED`
- `MAX_OPEN_POSITIONS`
- `MAX_DAILY_ENTRIES`
### 账号私有配置(每个账户独立)
- `BINANCE_API_KEY`
- `BINANCE_API_SECRET`
- `USE_TESTNET`
---
## 🐛 故障排查
### 问题1全局配置无法加载
**检查**
1. 确认 `global_strategy_config` 表已创建
2. 确认表中有数据(执行迁移脚本)
3. 检查后端日志,查看是否有错误
**解决**
```sql
-- 检查表是否存在
SHOW TABLES LIKE 'global_strategy_config';
-- 检查表中是否有数据
SELECT COUNT(*) FROM global_strategy_config;
```
### 问题2管理员无法修改全局配置
**检查**
1. 确认用户角色是 `admin`
2. 检查 API 返回的错误信息
3. 检查后端日志
**解决**
```sql
-- 检查用户角色
SELECT id, username, role FROM users WHERE username = 'your_username';
```
### 问题3交易系统仍使用旧配置
**检查**
1. 确认 Redis 缓存已更新
2. 确认交易系统已重启
3. 检查 `config_manager.py` 是否正确读取全局配置
**解决**
```bash
# 清除 Redis 缓存(可选)
redis-cli DEL global_strategy_config
# 重启交易系统
supervisorctl restart all
```
---
## ✅ 验证清单
- [ ] 数据库迁移脚本已执行
- [ ] `global_strategy_config` 表已创建
- [ ] 配置数据已迁移
- [ ] 后端服务已重启
- [ ] 交易系统已重启
- [ ] 管理员可以查看全局配置
- [ ] 管理员可以修改全局配置
- [ ] 普通用户无法访问全局配置
- [ ] 所有账户使用相同的全局配置
- [ ] Redis 缓存正常工作
---
## 📝 总结
全局配置已完全独立化,不再依赖任何账户。所有核心策略参数由管理员统一管理,存储在独立的 `global_strategy_config` 表中,使用独立的 Redis 缓存键。普通用户只能修改自己账户的风险旋钮,无法影响全局策略。

View File

@ -0,0 +1,234 @@
# 交易策略优化实施总结
## ✅ 已完成的优化(高优先级)
### 实施总结
已完成5项高优先级优化显著提升系统风险控制和信号质量
1. ✅ **大盘共振Beta Filter** - 减少大盘暴跌时的多单损失
2. ✅ **成交量验证** - 避免流动性差的币种,减少滑点损失
3. ✅ **固定风险百分比仓位计算** - 每笔单子风险恒定2%避免30%大额亏损
4. ✅ **信号强度分级** - 高质量信号9-10分获得更大收益低质量信号8分降低风险
5. ✅ **阶梯杠杆** - 小众币风险降低最大杠杆5倍
---
### 1. ✅ 动态过滤大盘共振Beta Filter
**实现位置**
- `trading_system/strategy.py` - `_check_beta_filter()`, `_get_symbol_change_period()`
- `trading_system/strategy.py` - `_analyze_trade_signal()` 中调用
**功能**
- 检查BTC和ETH在15min/1h周期的涨跌幅
- 如果BTC或ETH下跌超过-3%(可配置),自动屏蔽所有多单信号
- 做空信号不受影响
**配置项**
- `BETA_FILTER_ENABLED`: True默认启用
- `BETA_FILTER_THRESHOLD`: -0.03-3%
### 2. ✅ 成交量验证(严格过滤)
**实现位置**
- `trading_system/market_scanner.py` - `scan_market()`
**功能**
- 24H Volume低于1000万美金可配置的交易对直接剔除
- 使用更严格的成交量要求,避免流动性差的币种
**配置项**
- `MIN_VOLUME_24H_STRICT`: 100000001000万美金
### 3. ✅ 固定风险百分比仓位计算(凯利公式)
**实现位置**
- `trading_system/risk_manager.py` - `calculate_position_size()`
- `trading_system/position_manager.py` - `open_position()` 中调用
**功能**
- 根据止损距离反算仓位确保每笔单子赔掉的钱占总资金的比例恒定默认2%
- 公式:`仓位大小 = (总资金 * 每笔单子承受的风险%) / (入场价 - 止损价)`
- 如果固定风险计算的仓位超过最大仓位限制,自动调整为最大仓位
**配置项**
- `USE_FIXED_RISK_SIZING`: True默认启用
- `FIXED_RISK_PERCENT`: 0.022%
### 4. ✅ 信号强度分级
**实现位置**
- `trading_system/risk_manager.py` - `calculate_position_size()`
- `trading_system/position_manager.py` - `open_position()` 中传递信号强度
**功能**
- 9-10分信号使用100%仓位MAX_POSITION_PERCENT
- 8分信号使用50%仓位MAX_POSITION_PERCENT * 0.5
- 提高高质量信号的收益,降低低质量信号的风险
**配置项**
- `SIGNAL_STRENGTH_POSITION_MULTIPLIER`: {8: 0.5, 9: 1.0, 10: 1.0}
### 5. ✅ 阶梯杠杆(小众币限制)
**实现位置**
- `trading_system/risk_manager.py` - `calculate_dynamic_leverage()`
- `trading_system/strategy.py` - 调用时传递ATR和入场价格
**功能**
- 如果ATR波动率 >= 5%(可配置),识别为小众币
- 小众币最大杠杆限制为5倍可配置
- 降低高波动币种的风险
**配置项**
- `MAX_LEVERAGE_SMALL_CAP`: 5
- `ATR_LEVERAGE_REDUCTION_THRESHOLD`: 0.055%
---
## ⏳ 待实现的优化(中低优先级)
### 6. ⏳ 波动率阈值
**目标**避开ATR异常激增的时刻
**实现方案**
- 在 `market_scanner.py` 中计算平均ATR
- 如果当前ATR / 平均ATR > 2.0,过滤掉该交易对
**配置项**
- `ATR_SPIKE_THRESHOLD`: 2.0
### 7. ⏳ 追踪止损Trailing Stop
**目标**当价格达到1:1目标后利用币安Trailing Stop Order或代码层面实现
**实现方案**
- 检查币安是否支持 `TRAILING_STOP_MARKET` 订单类型
- 在分步止盈后挂币安Trailing Stop Order或代码层面实现
**配置项**
- `USE_TRAILING_STOP_AFTER_PARTIAL_PROFIT`: True
- `TRAILING_STOP_ATR_MULTIPLIER`: 1.5
### 8. ⏳ ADX趋势强度判断
**目标**如果ADX > 25且处于上升趋势延迟第一止盈位触发或取消50%减仓
**实现方案**
- 在 `indicators.py` 中计算ADX
- 在 `position_manager.py` 的止盈检查中如果ADX > 25且趋势向上跳过第一止盈
**配置项**
- `ADX_STRONG_TREND_THRESHOLD`: 25
- `ADX_SKIP_PARTIAL_PROFIT`: True
### 9. ⏳ 心跳检测与兜底巡检
**目标**WebSocket断线重连机制 + 每1-2分钟兜底巡检
**实现方案**
- 在 `position_manager.py` 的WebSocket监控中增加心跳检测
- 增加独立的定时巡检任务每1-2分钟作为兜底
**配置项**
- `WEBSOCKET_HEARTBEAT_INTERVAL`: 3030秒
- `FALLBACK_CHECK_INTERVAL`: 1202分钟
### 10. ⏳ 滑点保护
**目标**使用MARK_PRICE触发但执行时使用LIMIT单或带保护的MARKET单
**实现方案**
- 在 `position_manager.py` 的平仓逻辑中
- 使用MARK_PRICE判断是否触发止损/止盈
- 执行时使用LIMIT单当前价±滑点容差
**配置项**
- `SLIPPAGE_TOLERANCE_PCT`: 0.0020.2%
- `USE_LIMIT_ON_CLOSE`: True
### 11. ⏳ 资金费率避险
**目标**在费率结算前8:00, 16:00, 24:00如果费率过高>0.1%),提前止盈或暂缓入场
**实现方案**
- 在 `binance_client.py` 中获取资金费率
- 在 `strategy.py` 中检查是否接近结算时间
- 如果费率 > 0.1%,提前止盈或暂缓入场
**配置项**
- `FUNDING_RATE_THRESHOLD`: 0.0010.1%
- `FUNDING_RATE_EARLY_EXIT_HOURS`: 1结算前1小时
---
## 📊 配置项汇总
所有新增配置项已添加到 `trading_system/config.py``_get_trading_config()` 函数中:
```python
# 动态过滤
'BETA_FILTER_ENABLED': True,
'BETA_FILTER_THRESHOLD': -0.03, # -3%
'MIN_VOLUME_24H_STRICT': 10000000, # 1000万美金
'SIGNAL_STRENGTH_POSITION_MULTIPLIER': {8: 0.5, 9: 1.0, 10: 1.0},
# 仓位管理
'USE_FIXED_RISK_SIZING': True,
'FIXED_RISK_PERCENT': 0.02, # 2%
'MAX_LEVERAGE_SMALL_CAP': 5,
'ATR_LEVERAGE_REDUCTION_THRESHOLD': 0.05, # 5%
```
---
## 🎯 预期效果
### 已实现优化的预期效果:
1. **大盘共振过滤**
- ✅ 减少在大盘暴跌时的多单损失
- ✅ 提高整体胜率
2. **成交量验证**
- ✅ 避免流动性差的币种
- ✅ 减少滑点损失2-3%
3. **固定风险百分比**
- ✅ 每笔单子风险恒定2%避免30%的大额亏损
- ✅ 根据止损距离自动调整仓位,更科学
4. **信号强度分级**
- ✅ 高质量信号9-10分获得更大收益
- ✅ 低质量信号8分降低风险
5. **阶梯杠杆**
- ✅ 小众币风险降低最大杠杆5倍
- ✅ 减少因高杠杆导致的强平风险
---
## 📝 使用说明
### 管理员配置
所有优化配置项都可以在 `GlobalConfig` 页面中配置:
- 大盘共振过滤:`BETA_FILTER_ENABLED`, `BETA_FILTER_THRESHOLD`
- 成交量验证:`MIN_VOLUME_24H_STRICT`
- 固定风险百分比:`USE_FIXED_RISK_SIZING`, `FIXED_RISK_PERCENT`
- 信号强度分级:`SIGNAL_STRENGTH_POSITION_MULTIPLIER`
- 阶梯杠杆:`MAX_LEVERAGE_SMALL_CAP`, `ATR_LEVERAGE_REDUCTION_THRESHOLD`
### 默认值
所有优化默认启用,使用推荐的参数值。管理员可以根据实际情况调整。
---
## 🔄 后续优化建议
1. **监控效果**:观察优化后的实际效果,根据数据调整参数
2. **逐步实现**:剩余优化可以根据实际需求逐步实现
3. **测试验证**:建议在测试环境或小资金账户先测试

View File

@ -0,0 +1,250 @@
# 山寨币策略快速应用指南
> 5分钟内完成配置更新和验证
## 🚀 快速应用步骤
### 步骤1确认代码已更新✅ 已完成)
已更新的文件:
- ✅ `trading_system/config.py` - 核心配置
- ✅ `trading_system/trade_recommender.py` - 推荐生成
- ✅ `trading_system/position_manager.py` - 持仓管理
### 步骤2重启所有进程⚡ 立即执行)
```bash
# 1. 重启所有交易进程
supervisorctl restart auto_sys:*
# 2. 重启推荐服务
supervisorctl restart auto_recommend:*
# 3. 确认进程状态
supervisorctl status
```
### 步骤3验证配置生效🔍 关键检查)
查看日志,确认以下关键参数:
```bash
# 查看最新日志
tail -n 100 /www/wwwroot/autosys_new/logs/trading_*.log | grep -E "ATR_STOP_LOSS_MULTIPLIER|RISK_REWARD_RATIO|MIN_HOLD_TIME_SEC|USE_TRAILING_STOP|MAX_POSITION_PERCENT"
```
应该看到:
- `ATR_STOP_LOSS_MULTIPLIER: 2.0`
- `RISK_REWARD_RATIO: 4.0`
- `MIN_HOLD_TIME_SEC: 0`
- `USE_TRAILING_STOP: True`
- `MAX_POSITION_PERCENT: 0.015`
### 步骤4清理旧配置缓存可选
如果配置没有生效可能需要清理Redis缓存
```bash
# 方法1通过backend API清理推荐
curl -X POST "http://your-api-domain/api/config/clear-cache"
# 方法2直接重启Redis谨慎
# supervisorctl restart redis
```
## ✅ 验证清单
使用这个清单逐项验证:
### 风险控制
- [ ] ATR止损倍数 = 2.0(日志确认)
- [ ] 固定止损 = 15%(日志确认)
- [ ] 盈亏比 = 4.0(日志确认)
- [ ] 最小持仓时间 = 0秒已取消
- [ ] 每笔风险 = 1%
### 止盈策略
- [ ] 移动止损已启用
- [ ] 移动止损激活 = 30%
- [ ] 移动止损保护 = 15%
- [ ] 第一目标盈亏比 = 1:1
- [ ] 第二目标盈亏比 = 4:1
### 仓位管理
- [ ] 单笔仓位 ≤ 1.5%
- [ ] 总仓位 ≤ 12%
- [ ] 最大同时持仓 = 4个
- [ ] 基础杠杆 = 8倍
- [ ] 最大杠杆 = 12倍
### 交易控制
- [ ] 每日最多5笔
- [ ] 智能入场已启用
- [ ] 币种冷却 = 30分钟
- [ ] 只做趋势市AUTO_TRADE_ONLY_TRENDING = True
### 品种筛选
- [ ] 24H成交量 ≥ 3000万美元
- [ ] 最小波动率 ≥ 3%
- [ ] 最多扫描150个
- [ ] 只做前5个最强信号
### 时间框架
- [ ] 主周期 = 4小时
- [ ] 入场周期 = 1小时
- [ ] 确认周期 = 日线
- [ ] 扫描间隔 = 1小时
## 🔧 如果配置未生效
### 情况1进程重启失败
```bash
# 查看错误日志
tail -n 50 /www/wwwroot/autosys_new/logs/trading_*.err.log
# 常见问题:
# - 代码语法错误:检查最近修改的代码
# - 数据库连接失败:检查数据库状态
# - Redis连接失败检查Redis状态
```
### 情况2配置值仍是旧值
```bash
# 强制重新加载配置
# 在Python代码中调用
# config._config_manager.reload_from_redis()
# 或重启backend服务
supervisorctl restart backend
```
### 情况3部分配置生效部分未生效
```bash
# 检查数据库中的配置(可能有冲突)
# 使用backend管理界面或直接查询数据库
# SELECT * FROM trading_config WHERE config_key LIKE '%ATR%' OR config_key LIKE '%RISK%';
```
## 📊 监控前3笔交易
策略更新后密切监控前3笔交易的关键数据
```
第1笔交易
- 开仓价格_______
- 止损价格_______应该是开仓价的±15%左右)
- 止盈价格_______应该是止损距离的4倍
- 实际杠杆_______应该是8倍左右
- 保证金占比_______应该≤1.5%
第2笔交易
- 开仓价格_______
- 止损价格_______
- 止盈价格_______
- 实际杠杆_______
- 保证金占比_______
第3笔交易
- 开仓价格_______
- 止损价格_______
- 止盈价格_______
- 实际杠杆_______
- 保证金占比_______
```
### 异常判断标准
如果出现以下情况,立即暂停并检查:
- ❌ 止损距离 < 10% > 20%
- ❌ 盈亏比 < 3:1
- ❌ 单笔保证金 > 2%
- ❌ 杠杆 > 12倍
- ❌ 同时持仓 > 4个
- ❌ 触发止损但仍在持仓(说明止损未生效)
## 🎯 预期效果3-5天后
如果策略正确执行,应该看到:
### 短期指标1-2天
- 胜率30-40%(初期可能偏低,正常)
- 单笔盈亏:盈利单平均+4%,亏损单平均-1%
- 交易频率每日2-5笔
- 持仓时间1-4小时
### 中期指标3-5天
- 胜率35-45%
- 盈亏比3.5:1 - 4.5:1
- 期望值:+0.5% - +1.0%(每笔)
- 最大回撤:单日 < 3%
### 预警信号
如果出现以下情况,说明需要调整:
- ⚠️ 胜率 < 25%提高MIN_SIGNAL_STRENGTH到8
- ⚠️ 盈亏比 < 3:1检查止盈设置
- ⚠️ 单日亏损 > 5%:暂停交易,检查市场环境
- ⚠️ 连续亏损 > 5笔暂停交易等待市场转好
## 📞 问题排查
### 问题1配置更新后没有新交易
**可能原因:**
- 信号强度要求提高MIN_SIGNAL_STRENGTH=7
- 成交量要求提高MIN_VOLUME_24H=3000万
- 市场不满足AUTO_TRADE_ONLY_TRENDING条件
**解决方案:**
- 查看推荐服务日志,确认是否有新推荐生成
- 检查当前市场是否处于趋势中
- 如果长期没有交易可以临时降低MIN_SIGNAL_STRENGTH到6
### 问题2止损触发太频繁
**可能原因:**
- ATR_STOP_LOSS_MULTIPLIER太小
- 选择的币种波动过大
**解决方案:**
- 提高ATR_STOP_LOSS_MULTIPLIER到2.2或2.5
- 提高MIN_VOLATILITY筛选标准
- 检查是否在异常波动期间交易
### 问题3盈利单无法达到TP2
**可能原因:**
- 盈亏比4:1对当前市场环境过高
- 移动止损激活过早
**解决方案:**
- 降低RISK_REWARD_RATIO到3.0或3.5
- 提高TRAILING_STOP_ACTIVATION到40%
- 观察是否有盈利单达到30%但未触发移动止损
## 🔄 后续优化
根据实际运行情况,可能需要微调:
### 1周后可能的调整
- MIN_SIGNAL_STRENGTH6.5 - 8
- ATR_STOP_LOSS_MULTIPLIER1.8 - 2.2
- RISK_REWARD_RATIO3.5 - 4.5
- TRAILING_STOP_ACTIVATION25% - 35%
### 1个月后可能的调整
- 建立币种白名单/黑名单
- 按市值分级设置不同参数
- 添加BTC趋势过滤
---
**最后提醒**
1. 🚨 配置更新后前3笔交易必须人工监控
2. 📊 每日检查盈亏比和期望值是否符合预期
3. ⚡ 如有异常立即暂停交易并检查日志
4. 📈 坚持记录每笔交易数据,持续优化
**祝交易顺利!**

View File

@ -0,0 +1,198 @@
# 快速方案选择建议
## 📊 当前可用的快速方案
根据你的系统已完成的优化(大盘共振、固定风险百分比、信号强度分级、阶梯杠杆),以下是各方案的适用场景:
---
## 🎯 推荐方案(按优先级)
### ⭐⭐⭐ **首选波段回归swing**
**适用场景**
- ✅ 刚完成优化,想验证效果
- ✅ 追求稳定盈利,不追求高频
- ✅ 能接受"可能撤单"的情况
**核心特点**
- `SMART_ENTRY_ENABLED: false` - 纯限价单,不追价
- `MIN_SIGNAL_STRENGTH: 8` - 高质量信号配合信号强度分级8分用50%仓位)
- `SCAN_INTERVAL: 1800` - 30分钟扫描低频波段
- `ATR_TAKE_PROFIT_MULTIPLIER: 1.5` - 已优化为1.5:1盈亏比
- `MAX_POSITION_PERCENT: 2.0%` - 配合固定风险百分比,每笔风险恒定
**优势**
- ✅ 与最新优化最匹配(固定风险百分比、信号强度分级)
- ✅ 避免高频追价导致的损失
- ✅ 高质量信号,胜率更高
- ✅ 配合大盘共振过滤,减少大盘暴跌时的损失
**注意事项**
- ⚠️ 可能因为限价单未成交而撤单(这是正常的,避免追价损失)
- ⚠️ 交易频率较低,需要耐心
---
### ⭐⭐ **次选精选低频strict**
**适用场景**
- ✅ 追求最高胜率
- ✅ 只做趋势行情
- ✅ 能接受更少的交易次数
**核心特点**
- `AUTO_TRADE_ONLY_TRENDING: true` - 仅趋势行情自动交易
- `AUTO_TRADE_ALLOW_4H_NEUTRAL: false` - 4H中性不自动下单
- `MIN_SIGNAL_STRENGTH: 8` - 高质量信号
- `SMART_ENTRY_ENABLED: false` - 纯限价单
- `LIMIT_ORDER_OFFSET_PCT: 0.1` - 限价偏移较小,更容易成交
**优势**
- ✅ 胜率最高(只做趋势行情)
- ✅ 避免震荡市亏损
- ✅ 配合大盘共振过滤,效果更好
**注意事项**
- ⚠️ 交易次数最少
- ⚠️ 如果市场长期震荡,可能很久不出单
---
### ⭐ **备选成交优先fill**
**适用场景**
- ✅ 发现"波段回归"方案撤单太多
- ✅ 想要更多成交,但不想回到高频追价
- ✅ 能接受有限的追价(有上限保护)
**核心特点**
- `SMART_ENTRY_ENABLED: true` - 智能入场,有限追价
- `ENTRY_CHASE_MAX_STEPS: 2` - 最多追价2步严格限制
- `ENTRY_MAX_DRIFT_PCT_TRENDING: 0.3` - 追价上限30%(有保护)
- `MIN_SIGNAL_STRENGTH: 7` - 信号门槛略低
- `AUTO_TRADE_ONLY_TRENDING: false` - 解锁自动交易过滤
**优势**
- ✅ 成交率更高,减少撤单
- ✅ 追价有严格限制,不会回到高频追价
- ✅ 配合固定风险百分比,风险可控
**注意事项**
- ⚠️ 追价可能增加成本(但有限制)
- ⚠️ 信号门槛略低,需要配合信号强度分级使用
---
## ⚠️ 不推荐(除非特殊需求)
### 稳定出单steady
- **问题**`MIN_SIGNAL_STRENGTH: 6` 太低,信号质量差
- **问题**`SCAN_INTERVAL: 900` 15分钟扫描频率较高
- **建议**:除非你发现其他方案完全不出单,否则不推荐
### 传统方案conservative/balanced/aggressive
- **问题**:这些方案没有应用最新的优化(固定风险百分比、信号强度分级等)
- **问题**`ATR_TAKE_PROFIT_MULTIPLIER` 可能还是旧值
- **建议**:仅用于对比测试,不建议长期使用
---
## 🎯 选择决策树
```
开始
├─ 是否刚完成优化,想验证效果?
│ └─ 是 → 选择「波段回归swing
├─ 是否追求最高胜率,能接受很少交易?
│ └─ 是 → 选择「精选低频strict
├─ 是否发现「波段回归」撤单太多?
│ └─ 是 → 选择「成交优先fill
└─ 其他情况 → 选择「波段回归swing
```
---
## 📋 方案对比表
| 方案 | 信号门槛 | 入场机制 | 交易频率 | 胜率倾向 | 推荐度 |
|------|---------|---------|---------|---------|--------|
| **波段回归swing** | 8分 | 纯限价 | 低频 | 高 | ⭐⭐⭐ |
| **精选低频strict** | 8分 | 纯限价 | 最低 | 最高 | ⭐⭐ |
| **成交优先fill** | 7分 | 智能入场(有限) | 中频 | 中高 | ⭐ |
| **稳定出单steady** | 6分 | 智能入场 | 中高频 | 中 | ⚠️ |
| **传统方案** | 3-5分 | 混合 | 高频 | 低 | ❌ |
---
## 💡 最终建议
### 第一步先用「波段回归swing
**理由**
1. ✅ 与最新优化最匹配(固定风险百分比、信号强度分级、大盘共振)
2. ✅ 高质量信号8分配合信号强度分级8分用50%仓位
3. ✅ 纯限价单,避免追价损失
4. ✅ 已优化为1.5:1盈亏比更容易止盈
**观察期**运行20-30单观察
- 胜率是否提升
- 是否出现30%以上的大额亏损(应该不会,因为有固定风险百分比)
- 撤单率是否过高
### 第二步:根据观察结果调整
**如果撤单太多**
- 切换到「成交优先fill
- 或手动调整 `LIMIT_ORDER_OFFSET_PCT` 从 0.5% 降到 0.1%
**如果交易太少**
- 切换到「精选低频strict
- 或手动调整 `AUTO_TRADE_ONLY_TRENDING: false`
**如果胜率不够**
- 保持「波段回归swing
- 或切换到「精选低频strict
---
## 🔧 配合最新优化的配置
无论选择哪个方案,以下配置已自动应用(在 `config.py` 中):
- ✅ `BETA_FILTER_ENABLED: True` - 大盘共振过滤
- ✅ `MIN_VOLUME_24H_STRICT: 10000000` - 成交量验证1000万美金
- ✅ `USE_FIXED_RISK_SIZING: True` - 固定风险百分比2%
- ✅ `SIGNAL_STRENGTH_POSITION_MULTIPLIER: {8: 0.5, 9: 1.0, 10: 1.0}` - 信号强度分级
- ✅ `MAX_LEVERAGE_SMALL_CAP: 5` - 小众币杠杆限制
这些优化会在所有方案中生效,进一步提升系统表现。
---
## 📊 预期效果
使用「波段回归swing」+ 最新优化,预期:
- ✅ **胜率**:提升(高质量信号 + 大盘共振过滤)
- ✅ **单笔亏损**控制在2%(固定风险百分比)
- ✅ **大额亏损**避免30%以上的亏损(固定风险百分比 + 阶梯杠杆)
- ✅ **滑点损失**减少2-3%(成交量验证)
- ✅ **大盘暴跌损失**:减少(大盘共振过滤)
---
## 🎯 总结
**推荐顺序**
1. **首选**波段回归swing
2. **次选**精选低频strict
3. **备选**成交优先fill
**不推荐**:稳定出单、传统方案
**建议**先用「波段回归swing」跑20-30单根据实际效果再调整。

20
docs/archive/README.md Normal file
View File

@ -0,0 +1,20 @@
# 归档文档说明
本目录为 **历史/一次性** 文档,已从 `docs/` 根目录移入,避免日常阅读与 AI 整理时干扰。
## 归档内容概览
- **按日期的一次性分析**:如 2026-01-23 2026-02-04 的交易分析、亏损分析、策略执行分析等
- **已完成实施的总结**如「配置优化实施完成总结」「分步止盈状态细分实施完成总结」「ATR 配置优化完成总结」等
- **多版本文案只保留最终版后**:如「配置值格式统一」多个版本、「分步止盈」多篇分析
- **单次修复/单币种分析**如止损失效修复、某币种止损价错误分析、WebSocket/Redis 修复说明等
- **旧计划与建议**:如 newplan20260115、策略优化建议评估与实施方案等
## 使用方式
- 需要查 **当时为什么这样改****某次问题结论** 时,可在此目录按文件名或日期查找
- 当前策略与配置以 **docs/当前策略方案总结_2026-02-15.md****docs/INDEX.md** 为准
## 归档时间
2026-02-15

View File

@ -0,0 +1,75 @@
# 推荐服务 API Key 修复说明
## 问题描述
推荐服务(`recommendations_main.py`)仍然在使用真实的 API key导致可能使用错误的账户如 account_id=2进行下单。
## 根本原因
1. **推荐服务不应该使用任何账户的 API key**:推荐服务只需要获取公开行情数据,不需要认证。
2. **`config.py` 在导入时会读取 `ATS_ACCOUNT_ID`**:如果推荐服务的 supervisor 配置中设置了 `ATS_ACCOUNT_ID=2`,那么 `config.py` 会读取 account_id=2 的 API key。
3. **`BinanceClient.__init__` 可能被覆盖**:即使传入空字符串,如果 `config._config_manager` 存在,可能会在某个地方被覆盖。
## 修复方案
### 1. 修复 `BinanceClient.__init__` 逻辑
确保传入空字符串时不会被 config 覆盖:
```python
# 如果传入的是空字符串,保持为空字符串(不覆盖)
# 这样推荐服务可以使用空字符串来明确表示"只使用公开接口"
```
### 2. 修复 `connect` 方法
当 API key 为空时,跳过权限验证:
```python
# 验证API密钥权限仅当提供了有效的 API key 时)
if self.api_key and self.api_secret:
await self._verify_api_permissions()
else:
logger.info("✓ 使用公开 API跳过权限验证只能获取行情数据")
```
### 3. 在推荐服务中添加验证
`recommendations_main.py` 中添加验证逻辑,确保 API key 确实是空的:
```python
# 验证:确保 API key 确实是空的
if client.api_key:
logger.error(f"❌ 推荐服务 API Key 非空!当前值: {client.api_key[:4]}...")
logger.error(" 这可能导致推荐服务使用错误的账户密钥,请检查 BinanceClient.__init__ 逻辑")
else:
logger.info("✓ 推荐服务 API Key 确认为空,将只使用公开接口")
```
## 检查清单
1. ✅ 确保 `recommendations_main.py` 传入空字符串:`BinanceClient(api_key="", api_secret="")`
2. ✅ 确保 `BinanceClient.__init__` 不会覆盖空字符串
3. ✅ 确保 `connect` 方法在 API key 为空时跳过权限验证
4. ✅ 在推荐服务中添加验证逻辑,确保 API key 确实是空的
## 验证方法
1. 查看推荐服务的日志,确认显示:
- `✓ 推荐服务 API Key 确认为空,将只使用公开接口`
- `✓ 使用公开 API跳过权限验证只能获取行情数据`
2. 如果看到以下日志,说明仍有问题:
- `❌ 推荐服务 API Key 非空!`
- `初始化币安客户端: gqtx...sYmj, l3IB...I6NA`(显示真实的 API key
## 注意事项
1. **推荐服务不应该设置 `ATS_ACCOUNT_ID`**:推荐服务的 supervisor 配置不应该设置 `ATS_ACCOUNT_ID`,或者应该明确设置为空。
2. **推荐服务不应该下单**:推荐服务只生成推荐,不应该进行任何下单操作。
3. **如果推荐服务仍然使用真实的 API key**:检查 supervisor 配置,确保推荐服务进程没有设置 `ATS_ACCOUNT_ID`
## 后续优化
1. 考虑在 `config.py` 中添加一个标志,区分推荐服务和交易服务。
2. 考虑在推荐服务启动时,明确清除 `ATS_ACCOUNT_ID` 环境变量。

View File

@ -0,0 +1,166 @@
# Redis缓存问题修复说明
## 🔍 问题分析
即使执行了数据迁移,日志中仍然显示格式转换警告。原因是:
1. **Redis缓存中还有旧数据**即使数据库已经迁移为比例形式0.30Redis缓存中可能还存储着百分比形式30
2. **格式转换后没有更新缓存**:当检测到值>1时代码会转换为比例形式0.30但转换后的值没有写回Redis缓存
3. **下次读取时再次触发转换**下次从Redis读取时又会读到旧值30再次触发转换
---
## ✅ 修复方案
### 方案1在格式转换时更新Redis缓存已实现
**修改位置**`backend/config_manager.py:756-777`
**修复逻辑**
```python
if value > 1:
old_value = value
value = value / 100.0
logger.warning(...)
# ⚠️ 关键修复转换后立即更新Redis缓存
try:
if key in RISK_KNOBS_KEYS:
# 风险旋钮更新当前账号的Redis缓存
self._set_to_redis(key, value)
self._cache[key] = value
else:
# 全局配置更新全局配置的Redis缓存
global_config_mgr._set_to_redis(key, value)
global_config_mgr._cache[key] = value
except Exception as e:
logger.debug(f"更新Redis缓存失败不影响使用: {key} = {e}")
```
**效果**
- ✅ 转换后的值0.30会立即写回Redis缓存
- ✅ 下次读取时直接从Redis读取到正确的值0.30),不再触发转换
- ✅ 警告日志会逐渐减少,直到所有缓存都更新完成
---
### 方案2手动清除Redis缓存推荐配合使用
**执行命令**
```bash
# 清除所有配置缓存
redis-cli DEL "config:trading_config:*"
redis-cli DEL "config:global_strategy_config"
# 或者清除特定账号的缓存
redis-cli DEL "config:trading_config:1"
redis-cli DEL "config:trading_config:2"
redis-cli DEL "config:trading_config:3"
redis-cli DEL "config:trading_config:4"
```
**效果**
- ✅ 强制系统从数据库重新加载配置
- ✅ 如果数据库已经迁移加载的将是正确的比例形式0.30
- ✅ 新的配置值会写入Redis缓存
---
## 🔧 实施步骤
### 步骤1应用代码修复已完成
代码已经修复格式转换时会自动更新Redis缓存。
### 步骤2清除Redis缓存推荐
```bash
# 清除所有配置缓存
redis-cli DEL "config:trading_config:*"
redis-cli DEL "config:global_strategy_config"
```
### 步骤3重启服务
```bash
# 重启后端服务
supervisorctl restart backend
# 重启交易进程
supervisorctl restart auto_sys_acc1 auto_sys_acc2 auto_sys_acc3 auto_sys_acc4
```
### 步骤4验证
**检查日志**
```bash
# 查看日志,确认格式转换警告逐渐减少
tail -f /www/wwwroot/autosys_new/logs/trading_*.log | grep "配置值格式转换"
```
**预期结果**
- ✅ 第一次读取时可能会看到格式转换警告从Redis读取到旧值
- ✅ 转换后值会写回Redis缓存
- ✅ 下次读取时,不再触发转换,警告消失
---
## 📊 数据流
### 修复前
```
从Redis读取30旧数据
格式转换30 -> 0.30
使用0.30
⚠️ Redis缓存中还是30没有更新
下次读取30再次触发转换
```
### 修复后
```
从Redis读取30旧数据
格式转换30 -> 0.30
更新Redis缓存0.30 ✅
使用0.30
下次读取0.30(不再触发转换)✅
```
---
## ✅ 总结
### 修复内容
1. **代码修复**格式转换时自动更新Redis缓存
2. **手动清除**清除Redis缓存强制从数据库重新加载
### 效果
- ✅ 格式转换警告会逐渐减少
- ✅ Redis缓存会自动更新为正确的值
- ✅ 下次读取时不再触发转换
### 建议
1. **立即清除Redis缓存**:确保从数据库加载最新数据
2. **重启服务**:让新代码生效
3. **监控日志**:确认警告逐渐减少
---
## 🎯 最终效果
- ✅ 数据库中统一存储比例形式0.30
- ✅ Redis缓存中也是比例形式0.30
- ✅ 前端直接显示小数0.30),不带%符号
- ✅ 后端直接使用0.30),不需要转换
- ✅ 日志中不再出现格式转换警告

View File

@ -0,0 +1,130 @@
# SELL单止损价格计算错误修复
## 🚨 严重问题
### 问题描述
SELL单做空出现巨额亏损-91.93%),原因是止损价格计算逻辑错误,选择了"更宽松"的止损(更远离入场价),而不是"更紧"的止损(更接近入场价)。
### 具体案例
**AXLUSDT SELL 单交易ID: 1727**
- 入场价0.0731
- 出场价0.0815
- 方向SELL做空
- 盈亏比例:-91.93%(几乎亏光保证金)
**问题分析**
- 做空单价格从0.0731涨到0.0815涨幅11.22%
- 如果止损价格正确更接近入场价比如0.075应该在价格涨到0.075时止损亏损约5%
- 但实际亏损-91.93%,说明止损价格设置错误,选择了"更宽松"的止损更远离入场价比如0.082
---
## 🔍 根本原因
### 代码逻辑矛盾
**位置**`trading_system/risk_manager.py:689-757`
**问题**
1. **第689-700行**:选择"更紧的止损"(更接近入场价)
- BUY: 取最大值(更高的止损价,更接近入场价)✅
- SELL: 取最小值(更低的止损价,更接近入场价)✅
2. **第750-757行**:重新选择最终的止损价,保持"更宽松/更远"的选择规则 ❌
- BUY: 取最小值(更低的止损价,更远离入场价)❌
- SELL: 取最大值(更高的止损价,更远离入场价)❌
**结果**
- 第750-757行的逻辑会覆盖第689-700行的逻辑
- 导致SELL单选择了"更宽松"的止损(更远离入场价)
- 这就是为什么会出现-91.93%的巨额亏损
---
## ✅ 修复方案
### 修复内容
**修改位置**`trading_system/risk_manager.py:750-757`
**修复前**
```python
# 重新选择最终的止损价(包括技术止损)
# 仍保持"更宽松/更远"的选择规则
if side == 'BUY':
final_stop_loss = min(p[1] for p in candidate_prices) # ❌ 更宽松
selected_method = [p[0] for p in candidate_prices if p[1] == final_stop_loss][0]
else:
final_stop_loss = max(p[1] for p in candidate_prices) # ❌ 更宽松
selected_method = [p[0] for p in candidate_prices if p[1] == final_stop_loss][0]
```
**修复后**
```python
# ⚠️ 关键修复:重新选择最终的止损价(包括技术止损)
# 必须保持"更紧的止损"(更接近入场价)的选择规则,保护资金
# - 做多(BUY):止损价越低越紧 → 取最大值(更高的止损价,更接近入场价)
# - 做空(SELL):止损价越高越紧 → 取最小值(更低的止损价,更接近入场价)
if side == 'BUY':
# 做多:选择更高的止损价(更接近入场价,更紧)
final_stop_loss = max(p[1] for p in candidate_prices) # ✅ 更紧
selected_method = [p[0] for p in candidate_prices if p[1] == final_stop_loss][0]
else:
# 做空:选择更低的止损价(更接近入场价,更紧)
# ⚠️ 注意对于SELL单止损价高于入场价所以"更低的止损价"意味着更接近入场价
final_stop_loss = min(p[1] for p in candidate_prices) # ✅ 更紧
selected_method = [p[0] for p in candidate_prices if p[1] == final_stop_loss][0]
```
---
## 📊 修复效果
### 修复前
**SELL单止损价格选择**
- 入场价0.0731
- 候选止损价0.075保证金止损、0.082ATR止损
- 选择max(0.075, 0.082) = 0.082(更宽松,更远离入场价)❌
- 结果价格涨到0.0815时触发止损,亏损-91.93%
### 修复后
**SELL单止损价格选择**
- 入场价0.0731
- 候选止损价0.075保证金止损、0.082ATR止损
- 选择min(0.075, 0.082) = 0.075(更紧,更接近入场价)✅
- 结果价格涨到0.075时触发止损亏损约5%
---
## 🎯 预期效果
修复后预期:
- ✅ SELL单止损价格正确选择"更紧"的止损(更接近入场价)
- ✅ 不再出现巨额亏损(-91.93%
- ✅ 止损及时触发,保护资金
- ✅ 盈亏比改善从0.39:1提升到1.5:1+
---
## ⚠️ 注意事项
1. **立即重启交易进程**:修复后需要重启所有交易进程,让新代码生效
2. **监控SELL单**修复后需要密切监控SELL单的止损价格和止损触发情况
3. **检查现有持仓**如果有现有的SELL单持仓需要检查止损价格是否正确
---
## 📝 相关配置
当前配置:
- `STOP_LOSS_PERCENT`: 0.1515%
- `ATR_STOP_LOSS_MULTIPLIER`: 2.0
- `MIN_STOP_LOSS_PRICE_PCT`: 0.022%
建议:
- 保持当前配置,修复后应该能正常工作
- 如果仍然出现止损过宽的问题,可以考虑降低`ATR_STOP_LOSS_MULTIPLIER`到1.5

View File

@ -0,0 +1,128 @@
# 止损立即平仓修复说明
## 🔍 问题描述
**时间范围**23点后到早上
**症状**:系统检测到价格已触发止损价,但只记录错误日志,**没有执行平仓操作**,导致亏损持续扩大。
### 错误日志示例
```
NOMUSDT ⚠️ 当前价格(0.00944000)已触发止损价(0.00977746),无法挂止损单,应该立即平仓!
ZROUSDT ⚠️ 当前价格(2.02533560)已触发止损价(2.02531200),无法挂止损单,应该立即平仓!
WCTUSDT ⚠️ 当前价格(0.08786000)已触发止损价(0.08963080),无法挂止损单,应该立即平仓!
```
## 🎯 根本原因
`trading_system/position_manager.py``_ensure_exchange_sltp_orders()` 方法中:
1. **挂单逻辑死锁**:当 `current_price` 已经低于 `stop_loss_price`(做多时),币安 API 会拒绝 `STOP_MARKET` 订单,返回 `Order would immediately trigger`(错误代码 -2021
2. **只报警不执行**:代码检测到了这个情况并打印了警告日志,但**只设置了 `sl_order = None`,没有触发市价平仓**。
3. **依赖WebSocket延迟**:代码注释说"依赖WebSocket监控立即平仓"但WebSocket监控可能有延迟在深夜价格剧烈波动时无法及时止损。
## ✅ 修复方案
### 修复位置
`trading_system/position_manager.py``_ensure_exchange_sltp_orders()` 方法
### 修复内容
#### 1. 止损价触发时立即平仓第1199-1223行
**修复前**
```python
if current_price_val <= stop_loss_val:
logger.error(f"{symbol} ⚠️ 当前价格(...)已触发止损价(...),无法挂止损单,应该立即平仓!")
logger.error(f" 建议: 立即手动平仓或等待WebSocket监控触发平仓")
sl_order = None # ❌ 只设置None没有执行平仓
```
**修复后**
```python
if current_price_val <= stop_loss_val:
logger.error(f"{symbol} ⚠️ 当前价格({current_price_val:.8f})已触发止损价({stop_loss_val:.8f}),无法挂止损单,立即执行市价平仓保护!")
logger.error(f" 入场价: {entry_price_val:.8f if entry_price_val else 'N/A'}")
# ✅ 立即执行市价平仓
await self.close_position(symbol, reason='stop_loss')
return # 直接返回,不再尝试挂单
```
#### 2. 止盈价触发时立即平仓第1272-1288行
**新增逻辑**:在挂止盈单前,也检查价格是否已经达到止盈价,如果达到则立即执行市价平仓。
```python
# 在挂止盈单前,检查当前价格是否已经触发止盈
if current_price and take_profit:
try:
current_price_val = float(current_price)
take_profit_val = float(take_profit)
# 检查是否已经触发止盈
triggered_tp = False
if side == "BUY" and current_price_val >= take_profit_val:
triggered_tp = True
elif side == "SELL" and current_price_val <= take_profit_val:
triggered_tp = True
if triggered_tp:
logger.info(f"{symbol} 🎯 当前价格({current_price_val:.8f})已达到止盈价({take_profit_val:.8f}),立即执行市价止盈!")
await self.close_position(symbol, reason='take_profit')
return
except Exception as e:
logger.debug(f"{symbol} 检查止盈触发条件时出错: {e}")
```
## 📊 修复效果
### 修复前
- ❌ 检测到止损触发 → 只记录错误日志 → 等待WebSocket监控 → **可能延迟或失败**
- ❌ 价格继续下跌 → 亏损扩大 → 直到下次扫描才可能止损
### 修复后
- ✅ 检测到止损触发 → **立即执行市价平仓** → 止损保护立即生效
- ✅ 价格继续下跌 → **已平仓,不再亏损**
## 🔄 触发场景
这个修复会在以下场景生效:
1. **开仓后立即检查**:在 `_ensure_exchange_sltp_orders()` 被调用时(开仓后立即执行)
2. **系统重启后同步**:如果系统重启,同步持仓时会调用 `_ensure_exchange_sltp_orders()` 补挂保护单
3. **定期检查**`check_stop_loss_take_profit()` 方法会定期检查(通过扫描间隔)
## ⚠️ 注意事项
1. **市价平仓**:修复使用 `close_position()` 方法执行市价平仓,可能会有轻微滑点,但能确保及时止损。
2. **扫描间隔影响**如果扫描间隔较长如1小时在间隔期间价格暴跌穿透止损线等到下次扫描时`_ensure_exchange_sltp_orders()` 会被调用(例如系统重启后),此时会立即平仓。
3. **WebSocket监控**WebSocket监控仍然有效作为第二层保护。但修复后即使WebSocket延迟也能通过价格检查立即平仓。
## 🚀 部署建议
1. **重启交易进程**:修复后需要重启所有 `trading_system` 进程才能生效。
```bash
supervisorctl restart auto_sys_acc1 auto_sys_acc2 auto_sys_acc3 ...
```
2. **验证修复**:查看日志,确认当价格触发止损时,会看到:
```
{symbol} ⚠️ 当前价格(...)已触发止损价(...),无法挂止损单,立即执行市价平仓保护!
{symbol} [平仓] 开始平仓操作 (原因: stop_loss)
{symbol} [平仓] ✓ 平仓订单已提交
```
3. **监控效果**:观察后续交易,确认深夜价格波动时能及时止损,不再出现"只报警不平仓"的情况。
## 📝 相关文件
- `trading_system/position_manager.py`:主要修复文件
- `_ensure_exchange_sltp_orders()` 方法第1101-1320行
- `close_position()` 方法第669-769行
## ✅ 修复完成时间
2026-01-25

View File

@ -0,0 +1,246 @@
# 止损单挂单失败分析
## 📋 问题描述
INUSDT 止损单挂单失败系统将依赖WebSocket监控但可能无法及时止损。
**错误信息**
```
INUSDT ❌ 止损单挂单失败将依赖WebSocket监控但可能无法及时止损
```
---
## ⚠️ 风险
**止损单挂单失败的风险**
1. **没有交易所级别保护**:如果系统崩溃或网络中断,可能无法及时止损
2. **依赖WebSocket监控**如果WebSocket断开可能无法及时止损
3. **用户无法在币安界面看到止损单**:无法手动确认止损单是否已设置
---
## 🔍 可能的原因
### 1. 止损价格计算错误
**问题**
- 止损价格可能不在正确的一侧
- BUY时止损价应低于入场价SELL时止损价应高于入场价
- 如果止损价计算错误,币安会拒绝挂单
**检查**
- 查看日志中的止损价格和入场价格
- 确认止损价格方向是否正确
### 2. 价格精度问题
**问题**
- 止损价格可能不符合币安的精度要求tickSize
- 错误代码:-4014Price not increased by tick size
**检查**
- 查看日志中的价格精度信息
- 确认止损价格是否对齐到 tickSize
### 3. 持仓不存在或方向不对
**问题**
- 可能没有持仓或持仓方向不匹配
- 错误代码:-2022ReduceOnly Order is rejected
**检查**
- 确认币安账户中是否有持仓
- 确认持仓方向是否匹配
### 4. 对冲/单向模式问题
**问题**
- 币安账户可能是对冲模式,但代码按单向模式处理(或反之)
- 需要正确设置 `positionSide` 参数
**检查**
- 查看日志中的对冲模式信息
- 确认 `positionSide` 参数是否正确
### 5. 触发价格会导致立即触发
**问题**
- 止损价格太接近当前价格,会导致立即触发
- 币安会拒绝这种订单
**检查**
- 查看日志中的当前价格和止损价格
- 确认止损价格是否在正确的一侧
---
## ✅ 已完成的改进
### 1. 增强错误日志
**改进内容**
- 添加详细的错误信息(错误代码、错误消息)
- 记录止损价格、当前价格、持仓方向等关键信息
- 针对常见错误码提供具体的解决建议
**代码位置**
- `trading_system/binance_client.py:1535-1580`
- `trading_system/position_manager.py:1154-1155`
### 2. 添加止损价格验证
**改进内容**
- 在挂单前验证止损价格方向是否正确
- BUY时止损价应低于入场价SELL时止损价应高于入场价
- 如果验证失败,记录错误并跳过挂单
**代码位置**
- `trading_system/position_manager.py:1136-1148`
### 3. 改进重试逻辑
**改进内容**
- 如果首次挂单失败,尝试切换 `positionSide` 重试
- 记录重试过程和结果
- 如果所有重试都失败,记录详细参数用于调试
**代码位置**
- `trading_system/binance_client.py:1526-1549`
### 4. 自动获取当前价格
**改进内容**
- 如果未提供当前价格,自动从币安获取
- 确保止损价格验证和调整使用最新的价格
**代码位置**
- `trading_system/position_manager.py:1150-1155`
---
## 🔧 故障排查步骤
### 步骤1查看详细错误日志
检查交易日志,查找以下信息:
```
INUSDT ❌ 挂保护单失败(STOP_MARKET): ...
错误代码: ...
触发价格: ...
当前价格: ...
持仓方向: ...
平仓方向: ...
价格精度: ..., 价格步长: ...
```
### 步骤2检查止损价格计算
确认止损价格是否正确:
- **BUY订单**:止损价应 < 入场价
- **SELL订单**:止损价应 > 入场价
如果止损价格方向错误,检查:
1. `risk_manager.get_stop_loss_price()` 的计算逻辑
2. ATR 值是否正确
3. `ATR_STOP_LOSS_MULTIPLIER` 配置是否正确
### 步骤3检查持仓状态
确认币安账户中是否有持仓:
- 登录币安,查看是否有 INUSDT 的持仓
- 确认持仓方向LONG/SHORT是否匹配
### 步骤4检查价格精度
确认止损价格是否符合精度要求:
- 查看日志中的 `价格精度``价格步长`
- 确认止损价格是否对齐到 tickSize
### 步骤5检查对冲模式
确认币安账户的持仓模式:
- 查看日志中的 `对冲模式` 信息
- 确认 `positionSide` 参数是否正确
---
## 💡 解决方案
### 方案1修复止损价格计算如果计算错误
**如果止损价格方向错误**
1. 检查 `risk_manager.get_stop_loss_price()` 方法
2. 确认 ATR 计算是否正确
3. 确认 `ATR_STOP_LOSS_MULTIPLIER` 配置是否正确
### 方案2调整价格精度如果精度问题
**如果价格精度错误**
1. 检查 `_format_price_str_with_rounding()` 方法
2. 确认价格格式化是否正确
3. 确保止损价格对齐到 tickSize
### 方案3手动设置止损临时方案
**如果自动挂单失败**
1. 登录币安,手动设置止损单
2. 确保止损价格在正确的一侧
3. 等待系统修复后,再使用自动挂单
### 方案4检查持仓模式如果模式问题
**如果对冲模式问题**
1. 确认币安账户的持仓模式(对冲/单向)
2. 检查代码中的 `dual` 变量是否正确
3. 确保 `positionSide` 参数正确设置
---
## 📊 预期改善
改进后预期:
1. **详细的错误日志**:能够快速定位问题原因
2. **价格验证**:在挂单前验证止损价格,避免无效请求
3. **自动重试**:尝试切换 `positionSide` 重试,提高成功率
4. **更好的诊断**:记录所有关键参数,便于调试
---
## ⚠️ 重要提醒
**止损单挂单失败是严重问题**,因为:
1. 没有交易所级别保护,系统崩溃时可能无法止损
2. 依赖WebSocket监控网络中断时可能无法止损
3. 用户无法在币安界面看到止损单
**必须尽快修复**,否则可能导致大额亏损。
---
## 🔍 需要检查的信息
1. **交易日志**
- 止损单挂单失败的详细错误信息
- 错误代码和错误消息
- 止损价格、当前价格、持仓方向
2. **币安账户**
- 是否有 INUSDT 的持仓
- 持仓方向LONG/SHORT
- 持仓模式(对冲/单向)
3. **配置**
- `ATR_STOP_LOSS_MULTIPLIER` 的值
- `EXCHANGE_SLTP_ENABLED` 的值
- 止损价格计算逻辑
---
## 📝 下一步行动
1. **查看详细日志**:检查最新的错误日志,确认具体失败原因
2. **验证止损价格**:确认止损价格计算是否正确
3. **检查持仓状态**:确认币安账户中是否有持仓
4. **修复问题**:根据错误信息修复相应的问题
5. **测试验证**:修复后测试止损单挂单是否成功

View File

@ -0,0 +1,263 @@
# 交易策略逻辑完整分析
## 📊 当前策略参数配置
### 核心参数
| 参数 | 当前值 | 说明 |
|------|--------|------|
| `ATR_STOP_LOSS_MULTIPLIER` | 1.8 | ATR止损倍数止损距离 = ATR × 1.8 |
| `ATR_TAKE_PROFIT_MULTIPLIER` | 1.5 | ATR止盈倍数备选方法当无止损距离时使用 |
| `RISK_REWARD_RATIO` | 1.5 | 盈亏比(止盈距离 = 止损距离 × 1.5 |
| `MIN_TAKE_PROFIT_PRICE_PCT` | 0.02 (2%) | 最小止盈价格变动保护 |
| `MIN_HOLD_TIME_SEC` | 1800 (30分钟) | 最小持仓时间锁 |
| `USE_TRAILING_STOP` | False | 移动止损(已禁用) |
## 🎯 止盈计算逻辑(优先级顺序)
### 方法1基于止损距离和盈亏比优先使用
```
止盈距离 = 止损距离 × RISK_REWARD_RATIO (1.5)
止盈价 = 入场价 ± 止盈距离
```
**示例**
- 入场价100 USDT
- ATR3 USDT (3%)
- 止损距离100 × 0.03 × 1.8 = 5.4 USDT (5.4%)
- 止损价100 - 5.4 = 94.6 USDT
- **止盈距离**5.4 × 1.5 = **8.1 USDT (8.1%)**
- **止盈价**100 + 8.1 = **108.1 USDT**
### 方法2基于ATR倍数备选当无止损距离时
```
止盈距离 = ATR百分比 × ATR_TAKE_PROFIT_MULTIPLIER (1.5)
止盈价 = 入场价 × (1 ± 止盈距离百分比)
```
**示例**
- 入场价100 USDT
- ATR3 USDT (3%)
- **止盈距离**0.03 × 1.5 = **4.5%**
- **止盈价**100 × 1.045 = **104.5 USDT**
### 方法3基于保证金百分比兜底
```
止盈金额 = 保证金 × TAKE_PROFIT_PERCENT (25%)
止盈价 = 入场价 ± (止盈金额 / 数量)
```
### 方法4最小价格变动保护
```
止盈价 = 入场价 × (1 ± MIN_TAKE_PROFIT_PRICE_PCT) (2%)
```
**最终止盈价选择**:取以上方法中最宽松(最远)的价格
## 🔄 分步止盈策略
### 第一阶段50% 仓位在 1:1 盈亏比止盈
```
第一目标价 = 入场价 ± (入场价 - 止损价)
第一目标 = 盈亏比 1:1相对于保证金
```
**触发条件**
- 当前盈亏百分比(基于保证金)≥ 止损百分比(基于保证金)
- 平仓 50% 仓位
- **将剩余仓位止损移至入场价(保本)**
### 第二阶段:剩余 50% 仓位在 1.5:1 盈亏比止盈
```
第二目标价 = 原始止盈价(基于止损距离 × 1.5
第二目标 = 盈亏比 1.5:1相对于剩余仓位的保证金
```
**触发条件**
- 剩余仓位盈亏百分比(基于剩余保证金)≥ 1.5 × 止损百分比
- 平仓剩余 50% 仓位
## 📈 胜率要求分析
### 理论盈亏比计算
假设:
- 止损损失:-1 单位(基于保证金)
- 第一目标盈利50%仓位):+1 单位1:1
- 第二目标盈利50%仓位):+1.5 单位1.5:1
**完整交易期望收益**
- 如果第一目标触发(概率 P1第二目标也触发概率 P2
- 总盈利 = 0.5 × 1 + 0.5 × 1.5 = **1.25 单位**
- 如果第一目标触发,但第二目标未触发(概率 P1 × (1-P2)
- 总盈利 = 0.5 × 1 + 0.5 × 0 = **0.5 单位**
- 如果第一目标未触发,直接止损:
- 总损失 = **-1 单位**
### 盈亏平衡点计算
**最理想情况**第一目标100%触发第二目标100%触发):
```
胜率 × 1.25 = 败率 × 1
胜率 × 1.25 = (1 - 胜率) × 1
胜率 × 2.25 = 1
胜率 = 44.4%
```
**保守情况**第一目标100%触发第二目标50%触发):
```
平均盈利 = 0.5 × 1.25 + 0.5 × 0.5 = 0.875 单位
胜率 × 0.875 = (1 - 胜率) × 1
胜率 × 1.875 = 1
胜率 = 53.3%
```
**最保守情况**第一目标100%触发第二目标0%触发):
```
平均盈利 = 0.5 单位
胜率 × 0.5 = (1 - 胜率) × 1
胜率 × 1.5 = 1
胜率 = 66.7%
```
### 实际胜率要求评估
**关键因素**
1. **第一目标触发率**1:1 盈亏比相对容易触发(预期 60-70%
2. **第二目标触发率**1.5:1 盈亏比需要趋势延续(预期 40-50%
3. **保本保护**:第一目标触发后,剩余仓位止损移至入场价,**彻底杜绝亏损可能**
**实际期望**
- 如果第一目标触发率 = 65%,第二目标触发率 = 45%
- 平均盈利 = 0.65 × (0.45 × 1.25 + 0.55 × 0.5) = **0.65 × 0.8375 = 0.544 单位**
- 盈亏平衡点:胜率 × 0.544 = (1 - 胜率) × 1
- **胜率 = 64.8%**
## ⚠️ 潜在问题分析
### 1. 胜率要求较高
**问题**:如果第一目标触发率低,或第二目标触发率低,需要更高的胜率才能盈利。
**缓解措施**
- ✅ 分步止盈确保至少锁定部分利润
- ✅ 保本保护确保第一目标触发后不会亏损
- ✅ 最小持仓时间锁30分钟避免过早平仓
- ⚠️ **需要监控实际第一/第二目标触发率**
### 2. ATR_TAKE_PROFIT_MULTIPLIER 与 RISK_REWARD_RATIO 的关系
**当前逻辑**
- 优先使用 `止损距离 × RISK_REWARD_RATIO (1.5)` 计算止盈
- `ATR_TAKE_PROFIT_MULTIPLIER (1.5)` 仅作为备选(当无止损距离时)
**潜在问题**
- 如果 ATR 很小,`ATR_TAKE_PROFIT_MULTIPLIER` 可能计算出过小的止盈距离
- 但 `MIN_TAKE_PROFIT_PRICE_PCT (2%)` 提供了保护
**建议**
- ✅ 当前逻辑合理,`ATR_TAKE_PROFIT_MULTIPLIER` 主要作为备选
- ✅ `MIN_TAKE_PROFIT_PRICE_PCT` 确保最小止盈距离
### 3. 分步止盈的保本逻辑
**当前实现**
- 第一目标触发后,剩余仓位止损移至入场价(保本)
- **无论 `USE_TRAILING_STOP` 是否启用,都会移至保本**
**优势**
- ✅ 彻底杜绝第一目标触发后的亏损可能
- ✅ 剩余仓位可以追求更高收益
**潜在问题**
- ⚠️ 如果价格在入场价附近震荡,可能频繁触发保本止损
- ⚠️ 但这是可接受的因为已经锁定了50%的利润
### 4. 止盈价选择逻辑
**当前实现**:取所有方法中最宽松(最远)的价格
**潜在问题**
- 如果 `TAKE_PROFIT_PERCENT (25%)` 计算出的止盈价很远,可能难以触发
- 但 ATR 方法通常会给出更合理的价格
**建议**
- ✅ 当前逻辑合理,优先使用 ATR 方法
- ⚠️ 需要监控实际止盈触发率
## 📋 策略逻辑流程图
```
开仓
计算止损ATR × 1.8
计算止盈(止损距离 × 1.5 或 ATR × 1.5
设置第一目标1:1 盈亏比50%仓位)
设置第二目标1.5:1 盈亏比剩余50%仓位)
监控持仓
├─→ 触发止损 → 平仓(损失 -1 单位)
├─→ 触发第一目标 → 平仓50% → 止损移至保本 → 继续监控
│ │
│ └─→ 触发第二目标 → 平仓剩余50%(总盈利 1.25 单位)
│ └─→ 触发保本止损 → 平仓剩余50%(总盈利 0.5 单位)
└─→ 最小持仓时间未到 → 继续监控
```
## 🎯 优化建议
### 1. 监控关键指标
- **第一目标触发率**:目标 ≥ 60%
- **第二目标触发率**:目标 ≥ 40%
- **实际盈亏比**:目标 ≥ 1.2
- **盈利因子**:目标 ≥ 1.1
### 2. 如果胜率不足
**选项A**:提高第一目标触发率
- 降低第一目标到 0.8:1 盈亏比
- 但会降低平均盈利
**选项B**:提高第二目标触发率
- 降低第二目标到 1.2:1 盈亏比
- 但会降低平均盈利
**选项C**:提高入场信号质量
- 提高 `MIN_SIGNAL_STRENGTH`(当前 8
- 仅在 `marketRegime=trending` 时交易
- 提高 `MIN_SIGNAL_STRENGTH` 到 9 或 10
### 3. 如果第一目标触发率低
- 检查是否因为最小持仓时间锁导致过早平仓
- 检查止损是否过紧ATR_STOP_LOSS_MULTIPLIER = 1.8 是否合理)
- 考虑降低第一目标到 0.9:1
### 4. 如果第二目标触发率低
- 检查止盈价是否过远
- 考虑降低第二目标到 1.3:1 或 1.2:1
- 但需要权衡:降低目标会降低平均盈利
## ✅ 总结
### 当前策略的优势
1. ✅ **分步止盈**:锁定部分利润,降低风险
2. ✅ **保本保护**:第一目标触发后不会亏损
3. ✅ **动态止损**:基于 ATR适应市场波动
4. ✅ **最小持仓时间**:避免过早平仓
### 当前策略的挑战
1. ⚠️ **胜率要求**:需要 45-65% 胜率(取决于第二目标触发率)
2. ⚠️ **第二目标触发率**:需要趋势延续,可能较低
3. ⚠️ **需要监控**:实际触发率可能与理论不符
### 建议
1. **先运行观察**:收集实际数据(第一/第二目标触发率、实际盈亏比)
2. **根据数据调整**
- 如果第一目标触发率 < 60%考虑降低到 0.9:1
- 如果第二目标触发率 < 40%考虑降低到 1.3:1
- 如果胜率 < 50%提高入场信号质量
3. **目标指标**
- 第一目标触发率 ≥ 60%
- 第二目标触发率 ≥ 40%
- 实际盈亏比 ≥ 1.2
- 盈利因子 ≥ 1.1

View File

@ -0,0 +1,247 @@
# 交易策略优化计划
## 📋 优化目标
根据专业建议,系统化提升:
1. **入场信号质量** (Win Rate Up)
2. **利润捕获能力** (Profit Up)
3. **风险控制** (Survival First)
4. **系统可靠性** (Reliability Up)
5. **小众币专项优化**
---
## 1. 动态过滤:提升入场信号质量 (Win Rate Up)
### 1.1 大盘共振Beta Filter✅ 优先实现
**目标**当BTC或ETH在15min/1h周期剧烈下跌时自动屏蔽所有多单信号
**实现方案**
- 在 `strategy.py``_analyze_trade_signal` 中增加大盘检查
- 获取BTCUSDT和ETHUSDT的15min和1h K线
- 计算最近N根K线的涨跌幅
- 如果BTC或ETH在15min/1h周期下跌超过阈值如-3%),屏蔽所有多单
- 配置项:`BETA_FILTER_ENABLED`, `BETA_FILTER_THRESHOLD`
**代码位置**
- `trading_system/strategy.py` - `_analyze_trade_signal()`
- `trading_system/market_scanner.py` - 增加大盘数据获取
### 1.2 波动率阈值
**目标**避开成交量极低或ATR异常激增的时刻
**实现方案**
- 在 `market_scanner.py` 中增加波动率检查
- ATR异常激增当前ATR / 平均ATR > 阈值如2.0
- 成交量极低24H Volume < 配置阈值如1000万美金
- 配置项:`ATR_SPIKE_THRESHOLD`, `MIN_VOLUME_24H_STRICT`
**代码位置**
- `trading_system/market_scanner.py` - `_get_symbol_change()`
- `trading_system/indicators.py` - ATR计算
### 1.3 信号强度分级
**目标**9-10分信号分配更高权重8分信号仅作为轻仓试探
**实现方案**
- 在 `risk_manager.py``calculate_position_size` 中根据信号强度调整仓位
- 9-10分使用100%仓位MAX_POSITION_PERCENT
- 8分使用50%仓位MAX_POSITION_PERCENT * 0.5
- 配置项:`SIGNAL_STRENGTH_POSITION_MULTIPLIER`
**代码位置**
- `trading_system/risk_manager.py` - `calculate_position_size()`
- `trading_system/strategy.py` - 传递信号强度
---
## 2. 策略优化:从"固定止盈"到"动态追踪" (Profit Up)
### 2.1 追踪止损Trailing Stop
**目标**当价格达到1:1目标后利用币安Trailing Stop Order或代码层面根据ATR向上移动止损线
**实现方案**
- 检查币安是否支持 `TRAILING_STOP_MARKET` 订单类型
- 如果支持在分步止盈后挂币安Trailing Stop Order
- 如果不支持代码层面实现根据ATR动态调整止损价
- 配置项:`USE_TRAILING_STOP_AFTER_PARTIAL_PROFIT`, `TRAILING_STOP_ATR_MULTIPLIER`
**代码位置**
- `trading_system/position_manager.py` - 分步止盈后逻辑
- `trading_system/binance_client.py` - Trailing Stop Order支持
### 2.2 ADX趋势强度判断
**目标**如果ADX > 25且处于上升趋势延迟第一止盈位触发或取消50%减仓
**实现方案**
- 在 `indicators.py` 中计算ADX
- 在 `position_manager.py` 的止盈检查中如果ADX > 25且趋势向上跳过第一止盈50%减仓)
- 配置项:`ADX_STRONG_TREND_THRESHOLD`, `ADX_SKIP_PARTIAL_PROFIT`
**代码位置**
- `trading_system/indicators.py` - ADX计算
- `trading_system/position_manager.py` - 止盈逻辑
---
## 3. 仓位管理:基于风险的头寸缩放 (Survival First)
### 3.1 凯利公式/固定风险百分比
**目标**根据止损距离反算仓位确保每笔单子赔掉的钱占总资金的比例恒定如2%
**实现方案**
- 在 `risk_manager.py``calculate_position_size` 中实现
- 公式:`仓位大小 = (总资金 * 每笔单子承受的风险%) / (入场价 - 止损价)`
- 配置项:`FIXED_RISK_PERCENT`, `USE_FIXED_RISK_SIZING`
**代码位置**
- `trading_system/risk_manager.py` - `calculate_position_size()`
### 3.2 阶梯杠杆
**目标**针对小众币强制限制最高杠杆如3-5倍
**实现方案**
- 在 `risk_manager.py``calculate_dynamic_leverage` 中增加波动率检查
- 如果ATR过高或成交量过低限制最高杠杆
- 配置项:`MAX_LEVERAGE_SMALL_CAP`, `ATR_LEVERAGE_REDUCTION_THRESHOLD`
**代码位置**
- `trading_system/risk_manager.py` - `calculate_dynamic_leverage()`
---
## 4. 基础设施与风控 (Reliability Up)
### 4.1 心跳检测与延迟监控
**目标**WebSocket断线重连机制 + 每1-2分钟兜底巡检
**实现方案**
- 在 `position_manager.py` 的WebSocket监控中增加心跳检测
- 如果WebSocket断线自动重连
- 增加独立的定时巡检任务每1-2分钟作为兜底
- 配置项:`WEBSOCKET_HEARTBEAT_INTERVAL`, `FALLBACK_CHECK_INTERVAL`
**代码位置**
- `trading_system/position_manager.py` - WebSocket监控逻辑
### 4.2 滑点保护
**目标**使用MARK_PRICE触发但执行时使用LIMIT单或带保护的MARKET单
**实现方案**
- 在 `position_manager.py` 的平仓逻辑中
- 使用MARK_PRICE判断是否触发止损/止盈
- 执行时使用LIMIT单当前价±滑点容差或带保护的MARKET单
- 配置项:`SLIPPAGE_TOLERANCE_PCT`, `USE_LIMIT_ON_CLOSE`
**代码位置**
- `trading_system/position_manager.py` - `close_position()`
---
## 5. 针对小众币的专项优化
### 5.1 资金费率避险
**目标**在费率结算前8:00, 16:00, 24:00如果费率过高>0.1%),提前止盈或暂缓入场
**实现方案**
- 在 `binance_client.py` 中获取资金费率
- 在 `strategy.py` 中检查是否接近结算时间8:00, 16:00, 24:00
- 如果费率 > 0.1%,提前止盈或暂缓入场
- 配置项:`FUNDING_RATE_THRESHOLD`, `FUNDING_RATE_EARLY_EXIT_HOURS`
**代码位置**
- `trading_system/binance_client.py` - 资金费率获取
- `trading_system/strategy.py` - 入场检查
- `trading_system/position_manager.py` - 止盈检查
### 5.2 成交量验证
**目标**24H Volume低于1000万美金直接剔除
**实现方案**
- 在 `market_scanner.py` 中增加严格成交量过滤
- 配置项:`MIN_VOLUME_24H_STRICT` (10000000)
**代码位置**
- `trading_system/market_scanner.py` - 扫描过滤
---
## 📊 实施优先级
### ✅ 高优先级(已完成)
1. ✅ **大盘共振Beta Filter** - 当BTC/ETH下跌超过-3%时,屏蔽所有多单
2. ✅ **成交量验证1000万美金** - 24H Volume低于1000万美金直接剔除
3. ✅ **固定风险百分比仓位计算** - 根据止损距离反算仓位每笔风险恒定2%
4. ✅ **信号强度分级** - 8分50%仓位9-10分100%仓位
5. ✅ **阶梯杠杆** - 小众币ATR>=5%限制最高杠杆5倍
**预期效果**
- ✅ 减少大盘暴跌时的损失
- ✅ 避免流动性差的币种减少滑点损失2-3%
- ✅ 每笔单子风险恒定2%避免30%的大额亏损
- ✅ 高质量信号获得更大收益,低质量信号降低风险
- ✅ 小众币风险降低,减少强平风险
### ⏳ 中优先级(待实施)
6. ⏳ 波动率阈值 - 避开ATR异常激增的时刻
7. ⏳ 心跳检测与兜底巡检 - WebSocket断线重连和兜底巡检
8. ⏳ 滑点保护 - 使用MARK_PRICE触发LIMIT单执行
### 中优先级(本周实施)
5. 波动率阈值
6. 信号强度分级
7. 阶梯杠杆(小众币)
8. 滑点保护
### 低优先级(后续优化)
9. 追踪止损Trailing Stop
10. ADX趋势强度判断
11. 资金费率避险
---
## 🔧 配置项汇总
```python
# 动态过滤
'BETA_FILTER_ENABLED': True,
'BETA_FILTER_THRESHOLD': -0.03, # -3%
'ATR_SPIKE_THRESHOLD': 2.0,
'MIN_VOLUME_24H_STRICT': 10000000, # 1000万美金
'SIGNAL_STRENGTH_POSITION_MULTIPLIER': {8: 0.5, 9: 1.0, 10: 1.0},
# 策略优化
'USE_TRAILING_STOP_AFTER_PARTIAL_PROFIT': True,
'TRAILING_STOP_ATR_MULTIPLIER': 1.5,
'ADX_STRONG_TREND_THRESHOLD': 25,
'ADX_SKIP_PARTIAL_PROFIT': True,
# 仓位管理
'USE_FIXED_RISK_SIZING': True,
'FIXED_RISK_PERCENT': 0.02, # 2%
'MAX_LEVERAGE_SMALL_CAP': 5,
'ATR_LEVERAGE_REDUCTION_THRESHOLD': 0.05, # 5%
# 基础设施
'WEBSOCKET_HEARTBEAT_INTERVAL': 30, # 30秒
'FALLBACK_CHECK_INTERVAL': 120, # 2分钟
'SLIPPAGE_TOLERANCE_PCT': 0.002, # 0.2%
'USE_LIMIT_ON_CLOSE': True,
# 小众币优化
'FUNDING_RATE_THRESHOLD': 0.001, # 0.1%
'FUNDING_RATE_EARLY_EXIT_HOURS': 1, # 结算前1小时
```

View File

@ -0,0 +1,109 @@
# 止盈时间锁分析与优化建议
## 🤔 问题:止盈时间锁是否有必要?
### 当前情况
- ✅ **止损**:已修复,不受时间锁限制,立即执行
- ⚠️ **止盈**仍然受30分钟时间锁限制
### 止盈时间锁的利弊分析
#### ✅ 支持保留的理由(原始设计意图)
1. **防止过早止盈**
- 避免价格刚达到止盈目标就立即平仓
- 给趋势更多时间发展,追求更大利润
- 符合"让利润奔跑"的交易理念
2. **避免分钟级平仓**
- 防止因短期波动触发止盈
- 强制波段持仓纪律
- 减少频繁交易成本
3. **配合分步止盈策略**
- 第一目标1:1在30分钟后才能触发
- 给市场更多时间达到第二目标1.5:1
#### ❌ 反对保留的理由(实际问题)
1. **错过最佳止盈时机**
- 如果价格在30分钟内达到止盈目标但之后回落
- 可能从盈利变成亏损
- **对于小众币价格波动剧烈30分钟可能错过最佳退出点**
2. **与交易所级别止盈单冲突**
- 币安交易所级别的止盈单不受时间锁限制
- 如果交易所止盈单触发,但本地监控被时间锁阻止,可能造成不一致
3. **降低资金效率**
- 资金被锁定30分钟即使已经达到目标
- 无法及时释放资金用于新机会
4. **实际案例**
- 用户反馈亏损严重,可能也与止盈不及时有关
- 如果止盈能及时执行,可能减少亏损
## 📊 数据驱动的决策建议
### 方案A完全移除止盈时间锁推荐
**优点**
- ✅ 止盈立即执行,不错过最佳退出点
- ✅ 与交易所级别止盈单一致
- ✅ 提高资金效率
- ✅ 减少因价格回落导致的利润回吐
**缺点**
- ❌ 可能过早止盈,错过更大利润
- ❌ 可能因短期波动触发止盈
**适用场景**
- 小众币(波动剧烈,需要及时止盈)
- 短期交易策略
- 追求稳定收益而非最大化利润
### 方案B缩短时间锁折中方案
**建议**将30分钟缩短到5-10分钟
**优点**
- ✅ 保留防止过早止盈的保护
- ✅ 减少错过最佳退出点的风险
- ✅ 平衡利润最大化与及时止盈
**缺点**
- ❌ 仍然可能错过最佳退出点
- ❌ 需要测试确定最佳时长
### 方案C保留但可配置灵活方案
**建议**:将时间锁设为可配置,默认值降低
**优点**
- ✅ 灵活性高,可根据市场调整
- ✅ 可以针对不同币种设置不同值
- ✅ 保留原始设计意图
**缺点**
- ❌ 增加配置复杂度
- ❌ 需要用户理解并正确配置
## 🎯 推荐方案:完全移除止盈时间锁 ✅ 已实施
### 理由
1. **止损已不受限制**:如果止损可以立即执行,止盈也应该可以
2. **交易所级别保护**:币安交易所级别的止盈单已经提供保护
3. **分步止盈策略**分步止盈本身已经提供了利润保护50%在1:1止盈剩余保本
4. **实际需求**:用户反馈亏损严重,需要及时止盈保护利润
### ✅ 已实施
1. ✅ **完全移除**:已移除所有止盈时间锁限制
2. ✅ **保留分步止盈**:分步止盈策略仍然有效,提供利润保护
3. ✅ **依赖交易所级别止盈单**:主要依赖币安交易所级别的止盈单
4. ✅ **修复位置**
- `check_stop_loss_take_profit()` - 定期检查
- `_check_single_position()` - WebSocket实时监控两处
## 📈 预期效果
移除止盈时间锁后:
- ✅ 止盈能及时执行,保护利润
- ✅ 减少因价格回落导致的利润回吐
- ✅ 提高资金效率
- ✅ 与止损逻辑一致(都不受时间锁限制)
- ⚠️ 可能错过一些更大利润的机会(但分步止盈策略会部分补偿)

View File

@ -0,0 +1,127 @@
# 交易流程分析与优化方案
## 🔴 当前严重问题亏损达到30%以上
### 问题分析
根据最近的交易记录:
- CLOUSDT SELL: -17.54% (手动平仓)
- ICNTUSDT BUY: -19.60% (手动平仓)
- 0GUSDT BUY: -31.34% (手动平仓)
- ALCHUSDT BUY: -30.95% (同步平仓)
**核心问题止损没有及时触发导致亏损远超止损设置通常止损设置为8-10%**
### 根本原因
1. **最小持仓时间锁阻止止损触发** ⚠️ **最严重**
- `MIN_HOLD_TIME_SEC = 1800秒30分钟`
- 在持仓前30分钟内即使触发止损系统也会**禁止平仓**
- 这导致止损单无法执行,亏损持续扩大
- **对于小众币30分钟内价格可能剧烈波动亏损可能达到30%以上**
2. **交易所级别止损单可能未正确挂单**
- 如果 `_ensure_exchange_sltp_orders` 失败,只有本地监控
- 本地监控被时间锁阻止,无法平仓
3. **止损检查逻辑在时间锁之后**
- 代码顺序:先检查时间锁 → 如果不足30分钟直接 `continue`/`return`
- 止损检查逻辑永远不会执行
## 📊 当前交易流程
### 开仓流程
1. 市场扫描每30分钟
2. 信号筛选MIN_SIGNAL_STRENGTH >= 8
3. 计算止损止盈基于ATR或保证金
4. 挂限价单开仓
5. 订单成交后:
- 保存交易记录到数据库
- 在币安挂止损/止盈保护单(`_ensure_exchange_sltp_orders`
- 启动WebSocket实时监控
### 平仓流程(当前有严重问题)
#### 方式1交易所级别止损/止盈单(最可靠)
- 币安自动触发,不受时间锁影响
- **但如果挂单失败,就没有保护**
#### 方式2本地监控检查被时间锁阻止
- `check_stop_loss_take_profit()` 定期检查
- `_check_single_position()` WebSocket实时监控
- **都被 `MIN_HOLD_TIME_SEC` 阻止前30分钟无法平仓**
## ✅ 优化方案(已实施)
### 1. ✅ 完全移除最小持仓时间锁(已修复)
**问题**:时间锁阻止止损和止盈,导致亏损扩大和利润回吐
**解决方案**:✅ **完全移除时间锁限制**
- ✅ 止损检查在时间锁之前执行,立即平仓
- ✅ 止盈也立即执行,不受时间锁限制
- ✅ 止损和止盈逻辑一致,都立即执行
- ✅ 修复了三个位置:`check_stop_loss_take_profit()`、`_check_single_position()` 和移动止损检查
**移除理由**
1. 止损和止盈都应该立即执行,保护资金和利润
2. 交易所级别的止损/止盈单已提供保护
3. 分步止盈策略本身已提供利润保护50%在1:1止盈剩余保本
4. 及时执行可以避免价格回落导致的利润回吐
5. 如果需要防止秒级平仓可以通过提高入场信号质量MIN_SIGNAL_STRENGTH来实现
### 2. 确保交易所级别止损单正确挂单
- 增加日志,记录挂单成功/失败
- 如果挂单失败,重试机制
- 定期检查并补挂止损单
### 3. 优化止损逻辑
- 止损检查应该在时间锁之前如果采用选项B
- 或者完全移除时间锁对止损的限制
### 4. 针对小众币的优化
- 提高最小成交量要求(避免流动性差的币)
- 增大止损距离ATR倍数以应对高波动
- 降低杠杆倍数(降低风险)
## 🎯 具体修复建议
### 立即修复(高优先级)
1. **移除时间锁对止损的限制**
- 止损应该立即执行,不受时间锁影响
- 时间锁只应用于止盈(防止过早止盈)
2. **增强止损单挂单可靠性**
- 增加重试机制
- 增加失败告警
- 定期检查并补挂
3. **优化止损检查逻辑**
- 确保止损检查在时间锁之前(如果保留时间锁)
- 或者完全移除时间锁
### 中期优化
1. **提高入场信号质量**
- 提高 `MIN_SIGNAL_STRENGTH` 到 9-10
- 只交易高质量信号
2. **优化止损距离**
- 对于小众币使用更大的ATR倍数2.0-2.5
- 确保止损距离足够,不会被正常波动触发
3. **降低杠杆**
- 对于小众币降低杠杆到5-8倍
- 降低单笔仓位到5%
## 📈 预期效果
修复后:
- ✅ 止损能及时触发亏损控制在8-10%以内
- ✅ 不会出现30%以上的大额亏损
- ✅ 胜率提升(及时止损,避免大亏)
- ✅ 盈亏比改善(小亏大赚)

View File

@ -0,0 +1,222 @@
# 交易亏损分析报告 - 2026-01-23第二批
## 📊 亏损交易详情
### 交易 #1278 (INUSDT)
- **方向**BUY
- **入场价**0.0937 USDT
- **出场价**0.0914 USDT
- **价格跌幅****2.45%**
- **盈亏**-2.54 USDT
- **盈亏比例****-37.00%**相对于保证金6.87 USDT
- **持仓时间**10分钟16:03 - 16:13
- **平仓类型**:手动平仓 ❌
### 交易 #1275 (INUSDT)
- **方向**BUY
- **入场价**0.0970 USDT
- **出场价**0.0952 USDT
- **价格跌幅****1.86%**
- **盈亏**-1.97 USDT
- **盈亏比例****-28.60%**相对于保证金6.90 USDT
- **持仓时间**10分钟15:33 - 15:43
- **平仓类型**:手动平仓 ❌
---
## ⚠️ 核心问题分析
### 问题1手动平仓误判最严重
**现象**
- 两笔交易都被标记为"手动平仓"
- 但亏损比例极高(-37%和-28.6%),明显是止损触发
- 持仓时间只有10分钟符合止损触发的特征
**根本原因**
1. **币安保护单触发机制**
- 币安的保护单STOP/TAKE_PROFIT触发后会生成一个 MARKET 订单
- 这个 MARKET 订单的 `reduceOnly` 字段可能为 `false`币安API的bug或特殊情况
- 导致系统误判为手动平仓
2. **价格匹配逻辑失效**
- 代码使用 `_close_to(ep, sl, max_pct=0.05)` 判断平仓价格是否接近止损价
- 但这两笔交易的平仓价格可能离止损价较远超过5%),导致无法匹配
- 如果止损价设置错误或滑点太大,价格匹配会失败
**代码位置**`trading_system/position_manager.py:1918-1967`
---
### 问题2止损距离可能太紧
**分析**
- 交易 #1278:价格只跌了 2.45%,但亏损比例达到 -37%
- 交易 #1275:价格只跌了 1.86%,但亏损比例达到 -28.6%
**可能原因**
1. **ATR 太小**
- 如果 ATR 只有 0.5-1%,即使使用 2.5 倍 ATR止损距离也只有 1.25-2.5%
- 对于波动较大的币种,这个止损距离太紧了
2. **固定风险百分比未生效**
- 如果固定风险2%生效每笔亏损应该限制在总资金的2%左右
- 但实际亏损比例(相对于保证金)达到 28-37%,说明固定风险可能没有生效
3. **仓位过大**
- 如果固定风险计算失败,回退到传统方法(基于 MAX_POSITION_PERCENT
- 可能导致仓位过大,止损距离相对较小
---
### 问题3价格匹配容忍度可能不够
**当前逻辑**
```python
def _close_to(a: float, b: float, max_pct: float = 0.05) -> bool:
return abs((a - b) / b) <= max_pct # 5%容忍度
```
**问题**
- 如果止损价是 0.0900,平仓价是 0.0914,差距是 1.56%
- 但如果止损价计算错误(比如是 0.0920),平仓价 0.0914 与止损价 0.0920 的差距是 0.65%
- 在极端行情下滑点可能超过5%,导致价格匹配失败
---
## 💡 解决方案
### 方案1改进手动平仓识别逻辑最高优先级
**问题**:当前逻辑依赖 `reduceOnly` 字段但币安API可能不准确。
**解决方案**
1. **优先使用价格匹配**:如果平仓价格接近止损/止盈价5%范围内),直接标记为对应类型
2. **检查持仓时间**:如果持仓时间很短(< 30分钟且亏损更可能是止损触发
3. **检查亏损比例**:如果亏损比例超过止损目标(如 -10%),更可能是止损触发
4. **检查订单来源**:如果是系统自动下单,不应该标记为手动平仓
**代码修改**
```python
# 在 sync_positions_with_binance 中
# 1. 优先检查价格匹配(已实现,但需要提高优先级)
# 2. 如果价格不匹配,但满足以下条件,也标记为止损:
# - 持仓时间 < 30分钟
# - 亏损比例 > 止损目标
# - 是系统自动下单(有 trade_id
```
---
### 方案2放宽止损距离提高 ATR 倍数)
**当前配置**
- `ATR_STOP_LOSS_MULTIPLIER = 2.5`
**建议调整**
- 提高到 **3.0-3.5**,给波动留出更多空间
- 或者根据币种波动率动态调整
**风险**
- 止损距离放宽后,单笔亏损会增加
- 但如果固定风险2%生效,总亏损仍然可控
---
### 方案3增强价格匹配逻辑
**当前问题**
- 5%的容忍度可能不够(极端滑点)
- 只检查平仓价与止损价,没有检查实际亏损比例
**改进方案**
1. **提高容忍度**从5%提高到8-10%
2. **检查亏损比例**:如果实际亏损比例接近止损目标(如 -8% vs -10%),也标记为止损
3. **检查价格方向**如果平仓价在止损价的方向上BUY时平仓价 < 止损价更可能是止损触发
---
### 方案4确保固定风险百分比生效
**检查点**
1. 确认 `USE_FIXED_RISK_SIZING = true`
2. 确认 `FIXED_RISK_PERCENT = 0.02`2%
3. 检查交易日志,确认是否显示"使用固定风险百分比计算仓位"
4. 如果固定风险计算失败需要修复bug
---
## 🎯 立即行动
### 1. 修复手动平仓识别逻辑(紧急)
**修改文件**`trading_system/position_manager.py`
**修改位置**`sync_positions_with_binance` 方法中的 `exit_reason` 判断逻辑
**修改内容**
1. 提高价格匹配的优先级
2. 增加持仓时间和亏损比例的检查
3. 如果满足止损特征,即使 `reduceOnly=false`,也标记为止损
---
### 2. 检查并调整止损距离
**检查**
1. 查看这两笔交易的 ATR 值
2. 计算实际止损距离
3. 确认是否使用了 2.5 倍 ATR
**调整**
- 如果 ATR 太小,考虑提高 `ATR_STOP_LOSS_MULTIPLIER` 到 3.0-3.5
- 或者设置最小止损距离(如 3%
---
### 3. 验证固定风险百分比
**检查**
1. 查看交易日志,确认是否使用固定风险计算
2. 如果未使用,检查失败原因
3. 修复bug确保固定风险生效
---
## 📋 预期改善
修复后预期:
1. **准确识别平仓原因**:止损触发不再被误判为手动平仓
2. **止损距离更合理**:减少被随机波动扫损的概率
3. **单笔亏损可控**固定风险2%生效每笔亏损限制在总资金的2%左右
---
## 🔍 需要检查的数据
1. **交易日志**
- 这两笔交易的 ATR 值
- 止损价格
- 是否使用固定风险计算
- 币安订单的 `reduceOnly` 字段
2. **配置快照**
- `ATR_STOP_LOSS_MULTIPLIER` 的实际值
- `USE_FIXED_RISK_SIZING` 的实际值
- `FIXED_RISK_PERCENT` 的实际值
3. **数据库记录**
- 这两笔交易的 `stop_loss_price` 字段
- `atr` 字段
- `exit_reason` 字段
---
## ⚠️ 重要提醒
这两笔交易亏损比例极高(-37%和-28.6%),说明:
1. **止损距离太紧**价格只跌了1.86-2.45%就触发止损
2. **固定风险可能未生效**如果固定风险2%生效,亏损比例不应该这么高
3. **手动平仓误判**:这两笔明显是止损触发,不应该标记为手动平仓
**必须立即修复**,否则系统会继续产生大额亏损。

View File

@ -0,0 +1,250 @@
# 交易亏损分析报告 - 2026-01-23
## 📊 统计数据
- **总交易数**107
- **胜率**33.68% ❌远低于盈亏平衡点50%
- **总盈亏**-4.97 USDT亏损率 8.3%本金60 USDT
- **平均盈亏**-0.05 USDT
- **平均持仓时长**65分钟
- **平仓原因**:止损 23 / 止盈 21 / 移动止损 2 / **同步 49**45.8%
- **平均盈利/平均亏损**1.22:1 ❌远低于期望的3:1
- **总交易量(名义)**1538.25 USDT
---
## ⚠️ 核心问题分析
### 问题1止损距离过紧导致大额亏损
**典型案例**
- **订单 #1246 (MANAUSDT)**
- 入场价0.1815出场价0.1793
- 价格跌幅:**仅 1.21%**
- 但盈亏比例:**-18.18%**(相对于保证金)
- 说明:止损距离太紧,价格稍微波动就触发止损
- **订单 #1245 (IOUSDT)**
- 入场价0.1681出场价0.1661
- 价格跌幅:**仅 1.19%**
- 但盈亏比例:**-17.85%**
**根本原因**
1. **可能使用了旧的ATR止损倍数**0.5或1.8而不是新的2.5
2. **固定风险百分比可能没有生效**,或者被最大仓位限制覆盖
3. **止损距离计算错误**,导致止损价太接近入场价
---
### 问题2固定风险百分比可能没有生效
**理论计算**
- 本金60 USDT
- 固定风险2% = 1.2 USDT
- 如果止损距离 = 2.5倍ATR假设ATR = 0.5%,止损距离 = 1.25%
- 仓位 = 1.2 / (入场价 × 1.25%) = 1.2 / (入场价 × 0.0125)
**实际情况**
- 大部分订单保证金0.9-1.0 USDT约占总资金的1.67%
- 但亏损比例相对于保证金15-31%
- **如果固定风险2%生效每笔亏损应该限制在总资金的2%左右而不是保证金的15-31%**
**可能原因**
1. **固定风险计算失败**回退到传统方法基于MAX_POSITION_PERCENT
2. **止损距离太紧**,导致即使使用固定风险,实际亏损比例仍然很高
3. **最大仓位限制覆盖了固定风险**如果固定风险计算的保证金超过MAX_POSITION_PERCENT会被调整为最大仓位但止损距离不变
---
### 问题3同步平仓过多49笔45.8%
**问题**
- 49笔订单被标记为"同步平仓",说明系统无法正确识别平仓原因
- 可能原因:
1. **滑点太大**超过了5%的容忍度
2. **币安订单历史获取不完整**
3. **WebSocket断线**,导致没有及时监控
**影响**
- 无法准确分析哪些是止损、哪些是止盈
- 无法优化策略参数
---
### 问题4胜率太低33.68%
**数学分析**
- 当前胜率33.68%
- 当前盈亏比1.22:1
- 盈亏平衡点 = 1 / (1 + 1.22) = **45.05%**
- **当前胜率低于盈亏平衡点,必然亏损**
**原因**
1. **止损距离太紧**,导致频繁被扫损
2. **入场信号质量不够**,或者市场环境不适合交易
3. **止盈目标可能设置太高**,导致大部分订单无法止盈
---
## 🔍 具体案例分析
### 案例1订单 #1246 (MANAUSDT)
```
入场价0.1815
出场价0.1793
价格跌幅1.21%
盈亏:-0.1716 USDT
盈亏比例:-18.18%相对于保证金0.9438 USDT
```
**分析**
- 如果使用固定风险2%本金60 USDT风险金额 = 1.2 USDT
- 如果止损距离 = 1.21%,那么仓位 = 1.2 / (0.1815 × 0.0121) = 546.5
- 实际数量78保证金0.9438 USDT
- **说明:可能使用了传统方法计算仓位,而不是固定风险**
### 案例2订单 #1245 (IOUSDT)
```
入场价0.1681
出场价0.1661
价格跌幅1.19%
盈亏:-0.1726 USDT
盈亏比例:-17.85%相对于保证金0.967 USDT
```
**分析**
- 价格只跌了1.19%就触发止损
- 如果使用2.5倍ATR止损ATR应该约为 1.19% / 2.5 = **0.48%**
- **但实际止损距离只有1.19%说明可能使用了更小的ATR倍数如0.5倍或1.8倍)**
---
## 💡 解决方案
### 方案1确认并应用新的策略配置最高优先级
**立即行动**
1. **在前端"全局配置"页面,重新应用"波段回归"方案**
- 确保 `ATR_STOP_LOSS_MULTIPLIER = 2.5`
- 确保 `USE_DYNAMIC_ATR_MULTIPLIER = false`
- 确保 `USE_FIXED_RISK_SIZING = true`
- 确保 `FIXED_RISK_PERCENT = 0.02`
2. **重启交易服务**,使新配置生效
3. **验证配置**
- 查看交易日志,确认是否显示"使用固定风险百分比计算仓位"
- 确认止损距离是否基于2.5倍ATR
---
### 方案2优化固定风险计算的逻辑
**问题**如果固定风险计算的保证金超过MAX_POSITION_PERCENT系统会调整为最大仓位但止损距离不变导致实际风险超过2%。
**建议**
- 当固定风险计算的保证金超过最大仓位时,应该**同时调整止损距离**确保实际风险仍然是2%
- 或者:**降低MAX_POSITION_PERCENT**,让固定风险计算有更多空间
---
### 方案3降低交易频率提高信号质量
**当前问题**
- 107笔交易平均持仓65分钟
- 胜率只有33.68%
**建议**
1. **提高信号强度门槛**`MIN_SIGNAL_STRENGTH` 从8提高到9
2. **增加扫描间隔**`SCAN_INTERVAL` 从1800秒30分钟增加到3600秒1小时
3. **减少TOP_N_SYMBOLS**从8减少到5只交易最优质的信号
---
### 方案4优化同步平仓识别逻辑
**问题**49笔同步平仓占比45.8%
**建议**
1. **增加滑点容忍度**从5%增加到8%,以应对极端行情
2. **增强WebSocket监控**:确保及时接收价格更新
3. **优化订单历史获取**:扩大时间范围,确保能获取到所有平仓订单
---
## 📋 预期改善
应用新的策略配置ATR_STOP_LOSS_MULTIPLIER = 2.5)后,预期:
1. **止损距离放宽**
- 从1.2%增加到约3%假设ATR = 1.2%
- 减少被随机波动扫损的概率
2. **胜率提升**
- 从33.68%提升到**50-60%**以上
- 因为止损距离放宽,给波动留出更多空间
3. **单笔亏损降低**
- 如果固定风险2%生效每笔亏损限制在总资金的2%左右
- 而不是保证金的15-31%
4. **盈亏比改善**
- 从1.22:1提升到**1.5:1以上**
- 配合止盈倍数1.5,更容易达成目标
---
## 🎯 立即行动清单
### 高优先级(立即执行)
1. ✅ **重新应用策略配置**
- 在"全局配置"页面,点击"应用"波段回归方案
- 确认 `ATR_STOP_LOSS_MULTIPLIER = 2.5`
- 确认 `USE_DYNAMIC_ATR_MULTIPLIER = false`
2. ✅ **重启交易服务**
- 使新配置立即生效
3. ✅ **验证配置**
- 查看交易日志,确认使用固定风险计算
- 确认止损距离基于2.5倍ATR
### 中优先级(本周执行)
4. ⏳ **提高信号质量门槛**
- `MIN_SIGNAL_STRENGTH`: 8 → 9
- 减少交易频率,提高胜率
5. ⏳ **优化固定风险计算逻辑**
- 当超过最大仓位时,同时调整止损距离
---
## 📝 总结
**当前主要问题**
1. ❌ 止损距离太紧可能使用了旧的0.5倍或1.8倍ATR
2. ❌ 固定风险百分比可能没有生效
3. ❌ 胜率太低33.68%
4. ❌ 盈亏比太低1.22:1
5. ❌ 同步平仓太多49笔45.8%
**解决方案**
1. ✅ 应用新的策略配置ATR_STOP_LOSS_MULTIPLIER = 2.5
2. ✅ 确保固定风险百分比生效
3. ✅ 提高信号质量门槛
4. ✅ 优化同步平仓识别逻辑
**预期改善**
- 胜率33.68% → **50-60%**
- 盈亏比1.22:1 → **1.5:1以上**
- 单笔亏损15-31% → **2%左右**(相对于总资金)
---
## ⚠️ 重要提醒
**当前配置可能仍在使用旧参数**ATR_STOP_LOSS_MULTIPLIER = 0.5或1.8),导致止损距离太紧。
**必须在前端重新应用策略方案**确保数据库和Redis中的配置更新为最新值。

View File

@ -0,0 +1,89 @@
# 交易异常与数据库优化分析报告 (2026-02-04)
## 1. 数据库连接异常分析
### 问题描述
用户反馈在 2026-02-04 02:28 左右出现 `(1040, 'Too many connections')` 错误。
### 原因分析
检查 `backend/database/connection.py` 代码发现,原有的数据库连接管理使用了 `pymysql.connect` 直接建立连接,虽然使用了 `contextmanager` 确保关闭,但在高并发或频繁请求下(如策略扫描、推荐系统同时运行),会频繁创建和销毁连接。
MySQL 建立连接开销较大,且如果不使用连接池,每一个 API 请求或后台任务都会占用一个物理连接。当并发量瞬间增加时,很容易达到 MySQL 的最大连接数限制(默认通常是 151
### 解决方案(已实施)
已修改 `backend/database/connection.py`,引入了 `SQLAlchemy` 的连接池 (`QueuePool`) 机制。
- **连接池配置**
- `pool_size=20`: 基础连接池大小保持 20 个活跃连接。
- `max_overflow=30`: 高峰期可临时增加 30 个连接(总计 50 个)。
- `pool_recycle=3600`: 连接存活 1 小时后回收,防止 MySQL 8小时超时断开问题。
- `pool_pre_ping=True`: 每次获取连接前自动检测有效性,防止获取到已断开的连接。
此优化将显著降低数据库连接开销,彻底解决 "Too many connections" 问题。
---
## 2. ZROUSDT 50% 亏损交易分析
### 交易详情
- **交易对**: ZROUSDT
- **方向**: 做多 (BUY)
- **开仓时间**: 2026-02-04 02:28 (大致)
- **入场价**: 1.7978
- **止损价**: 1.68307
- **止损触发价**: 1.6819 (实际平仓价)
- **杠杆倍数**: 8x
- **盈亏比例**: -51.57%
### 亏损原因深度拆解
1. **价格波动幅度**:
止损距离 = (1.7978 - 1.68307) / 1.7978 ≈ **6.38%**
实际平仓跌幅 = (1.7978 - 1.6819) / 1.7978 ≈ **6.45%**
2. **杠杆放大效应**:
盈亏比例 = 价格跌幅 × 杠杆倍数
盈亏比例 = 6.45% × 8 ≈ **51.6%**
**结论**: 此次 -51.57% 的亏损在数学上是完全符合预期的。
当策略允许 **6.4%** 的止损宽度,并且强制使用 **8倍** 杠杆时,一旦止损触发,本金(保证金)必然损失 **51%**
3. **资金风险 vs 本金风险**:
虽然单笔交易损失了 50% 的保证金,但系统是基于 `FIXED_RISK_PERCENT` (默认 2%) 来计算仓位的。
- 假设账户余额 100 U风险 2 U。
- 止损距离 6.4%,风险 2 U => 仓位价值 = 2 / 6.4% ≈ 31.25 U。
- 杠杆 8x => 占用保证金 = 31.25 / 8 ≈ 3.9 U。
- 亏损 2 U。
- 亏损占保证金比例 = 2 / 3.9 ≈ **51%**
**关键点**: 实际上账户总资产仅损失了预设的 **2%**符合风控但对于这笔具体的交易单其保证金损失过半视觉冲击力极强且接近强平线8倍杠杆强平线约在 -12.5% 跌幅6.4% 已经走了一半)。
### 优化建议
为了避免出现单笔交易亏损 > 30% 甚至接近强平的情况,建议引入 **动态杠杆 (Dynamic Leverage)** 机制。
#### 建议方案:基于止损宽度的动态杠杆
不应固定使用 8x 杠杆,而应根据止损距离自动调整杠杆。
**公式**: `建议杠杆 = 目标最大单单亏损率 / 止损宽度`
假设我们希望单笔交易止损时,保证金亏损不超过 **20%** (MAX_ROE_LOSS = 0.2)
- **当前案例 (ZRO)**: 止损宽度 6.4%。
- 建议杠杆 = 20% / 6.4% ≈ **3.1倍**
- 如果使用 3x 杠杆:
- 仓位价值不变(仍由 2% 总账户风险决定)。
- 保证金占用增加(从 3.9U 变为 10.4U)。
- 亏损仍为 2U。
- **保证金亏损率 = 2 / 10.4 ≈ 19.2%**
**优点**:
1. 降低强平风险3x 杠杆强平线在 -33%,远低于 -6.4%)。
2. 心理体验更好(亏损比例控制在 20% 以内)。
3. 资金利用率更健康(高波动币种自动降低杠杆,低波动币种可维持高杠杆)。
#### 后续行动计划
1. 在 `config.py` 中添加 `DYNAMIC_LEVERAGE_ENABLED = True``MAX_SINGLE_TRADE_LOSS_PERCENT = 20` (单单最大本金亏损率)。
2. 修改 `risk_manager.py`,在计算仓位前,先根据 `stop_loss_price` 计算最大允许杠杆,并取 `min(CONFIG_LEVERAGE, dynamic_leverage)`
---
**总结**:
- 数据库连接问题已通过引入连接池修复。
- ZROUSDT 交易逻辑正常,大比例亏损源于 **宽止损 + 高固定杠杆** 的组合。建议实施动态杠杆优化。

View File

@ -0,0 +1,290 @@
# 交易表现分析 - 2026-01-23
## 📊 今日统计
- **总交易数**35
- **胜率**44.00%
- **总盈亏**2.03 USDT
- **平均盈亏**0.08 USDT
- **平均持仓时长**35分钟
- **平仓原因**:止盈 1 / 手动 9 / 同步 15
- **平均盈利/平均亏损**1.35 : 1期望 3:1
- **总交易量(名义)**3512.05 USDT
## ⚠️ 严重问题分析
### 问题1盈亏比严重失衡1.35:1 vs 期望3:1
**现状**
- 平均盈利/平均亏损 = 1.35:1
- 胜率 = 44%
- 期望盈亏比 = 3:1
**数学分析**
- 盈亏平衡点 = 1 / (1 + 盈亏比) = 1 / (1 + 1.35) = **42.55%**
- 当前胜率 44% 仅略高于盈亏平衡点,所以总盈亏只有 2.03 USDT几乎不盈利
- 如果盈亏比达到 3:1盈亏平衡点 = 1 / (1 + 3) = **25%**
- 在胜率 44% 的情况下,盈亏比 3:1 的期望收益 = (0.44 × 3) - (0.56 × 1) = **0.76**每笔亏损赚0.76倍)
**结论**:盈亏比 1.35:1 太低了,必须提升到至少 2:1 才能稳定盈利。
---
### 问题2大额亏损30-50%)说明止损失效
**具体案例**
- #1138 DUSKUSDT: **-31.28%**(同步平仓)
- #1135 RIVERUSDT: **-49.06%**(同步平仓)
- #1133 BDXNUSDT: **-31.69%**(手动平仓)
**问题分析**
1. **固定风险百分比应该限制亏损为2%**但实际亏损达到30-50%
2. **说明止损没有及时执行**,或者止损价格计算错误
3. **"同步平仓"** 可能是在止损触发后,系统同步币安状态时发现已经亏损很大
**可能原因**
1. 止损单没有正确挂到交易所
2. 止损价格计算错误(可能基于价格百分比而不是保证金百分比)
3. WebSocket 监控断线,没有及时触发止损
4. 固定风险百分比计算时,止损距离估算错误
---
### 问题3止盈太少35笔只有1笔止盈
**现状**
- 35笔交易只有1笔止盈2.86%
- 15笔同步平仓9笔手动平仓
**问题分析**
1. **止盈目标可能设置太高**`ATR_TAKE_PROFIT_MULTIPLIER = 1.5` 可能仍然太高
2. **大部分订单被提前平仓**15笔同步平仓可能是止损触发9笔手动平仓可能是用户干预
3. **止盈单可能没有正确挂到交易所**
---
### 问题4固定风险百分比可能没有生效
**理论**
- 固定风险百分比 = 2%
- 如果止损距离 = 5%,那么仓位 = (总资金 × 2%) / 5% = 总资金的 40%
- 如果止损触发,亏损 = 总资金的 2%(符合预期)
**实际情况**
- 亏损达到 30-50%,说明:
1. 固定风险百分比没有生效
2. 或者止损距离计算错误(止损距离太小,导致仓位过大)
3. 或者止损没有及时触发
---
## 🔍 根本原因分析
### 1. 止损执行问题
**可能原因**
- 止损单没有正确挂到交易所
- WebSocket 监控断线,没有及时触发止损
- 止损价格计算错误
**验证方法**
- 查看日志,确认止损单是否成功挂到交易所
- 检查 WebSocket 监控是否正常运行
- 检查止损价格计算逻辑
### 2. 固定风险百分比可能没有生效
**验证方法**
- 检查 `USE_FIXED_RISK_SIZING` 是否启用
- 检查开仓日志,确认是否使用了固定风险计算
- 检查止损距离估算是否准确
### 3. 止盈目标设置问题
**当前配置**
- `ATR_TAKE_PROFIT_MULTIPLIER = 1.5`
- `TAKE_PROFIT_PERCENT = 25%`(相对于保证金)
**问题**
- 如果 ATR 很大1.5倍 ATR 的止盈目标可能很难达到
- 25% 的止盈目标对于小币种可能太高
---
## 💡 解决方案
### 方案1确保止损正确执行最高优先级
1. **检查止损单是否挂到交易所**
- 在开仓后立即检查止损单状态
- 如果挂单失败,重试或报警
2. **增强 WebSocket 监控可靠性**
- 增加心跳检测
- 增加断线重连机制
- 增加兜底巡检每1-2分钟检查一次
3. **修复止损价格计算**
- 确保止损基于保证金百分比,而不是价格百分比
- 确保止损距离估算准确
### 方案2验证并修复固定风险百分比
1. **检查配置**
- 确认 `USE_FIXED_RISK_SIZING = True`
- 确认 `FIXED_RISK_PERCENT = 0.02`2%
2. **检查计算逻辑**
- 确认止损距离估算准确
- 确认仓位计算使用了固定风险公式
3. **增加日志**
- 记录固定风险计算的详细过程
- 记录实际止损距离和仓位大小
### 方案3调整止盈目标
1. **降低止盈目标**
- `ATR_TAKE_PROFIT_MULTIPLIER` 从 1.5 降到 1.2
- `TAKE_PROFIT_PERCENT` 从 25% 降到 20%
2. **确保止盈单正确挂到交易所**
- 在开仓后立即挂止盈单
- 检查止盈单状态
---
## 📋 立即行动清单
### 高优先级(立即执行)
1. ✅ **检查止损单挂单状态**
- 在开仓后立即检查止损单是否成功挂到交易所
- 如果失败,重试或报警
2. ✅ **验证固定风险百分比是否生效**
- 检查开仓日志,确认是否使用了固定风险计算
- 如果未生效,修复计算逻辑
3. ✅ **增强止损执行可靠性**
- 增加 WebSocket 心跳检测
- 增加兜底巡检每1-2分钟检查一次
### 中优先级(本周执行)
4. ⏳ **调整止盈目标**
- 降低 `ATR_TAKE_PROFIT_MULTIPLIER` 到 1.2
- 降低 `TAKE_PROFIT_PERCENT` 到 20%
5. ⏳ **增加诊断日志**
- 记录止损单挂单状态
- 记录固定风险计算过程
- 记录实际止损距离
---
## 🎯 目标指标
### 当前表现
- 盈亏比1.35:1 ❌
- 胜率44% ✅
- 总盈亏2.03 USDT几乎不盈利
### 目标表现
- 盈亏比:≥ 2.0:1理想 3:1
- 胜率:≥ 40% ✅
- 单笔最大亏损:≤ 5%固定风险2% + 滑点)✅
- 止盈率:≥ 30%35笔中至少10笔止盈
---
## 📝 下一步
1. ✅ **已修复**:固定风险百分比计算逻辑(已添加到代码中)
2. ✅ **已增强**:止损单挂单状态日志(成功/失败都会记录)
3. ⏳ **待验证**重新运行后观察是否还有30%以上的亏损
4. ⏳ **待调整**:降低止盈目标,提高止盈率
---
## 🔧 已实施的修复
### 1. ✅ 修复固定风险百分比计算逻辑
**问题**:固定风险百分比计算逻辑缺失,导致系统没有使用固定风险公式计算仓位。
**修复**
- 在 `risk_manager.py``calculate_position_size()` 中添加了完整的固定风险百分比计算逻辑
- 如果 `USE_FIXED_RISK_SIZING = True` 且提供了 `entry_price``side`,会使用固定风险公式
- 公式:`quantity = (总资金 × 2%) / (入场价 - 止损价)`
### 2. ✅ 增强止损单挂单状态日志
**问题**:无法知道止损单是否成功挂到交易所。
**修复**
- 在 `position_manager.py``_ensure_exchange_sltp_orders()` 中增加了日志
- 止损单和止盈单挂单成功/失败都会记录日志
- 如果挂单失败会明确提示将依赖WebSocket监控
---
## ⚠️ 关键发现
### 为什么会出现30-50%的亏损?
**根本原因**
1. **固定风险百分比没有生效**(已修复)
- 如果固定风险百分比生效每笔亏损应该限制在2%
- 但实际亏损达到30-50%,说明固定风险百分比没有生效
2. **止损单可能没有正确挂到交易所**
- 如果止损单挂单失败系统只能依赖WebSocket监控
- 如果WebSocket断线止损可能无法及时执行
3. **止损价格计算可能有问题**
- 止损可能基于价格百分比而不是保证金百分比
- 或者止损距离估算错误
### 为什么盈亏比只有1.35:1
**原因**
1. **止盈目标设置太高**`ATR_TAKE_PROFIT_MULTIPLIER = 1.5` 可能仍然太高
2. **止盈单可能没有正确挂到交易所**只有1笔止盈说明大部分订单没有达到止盈目标
3. **大部分订单被提前平仓**15笔同步平仓可能是止损9笔手动平仓用户干预
---
## 🎯 预期改善
修复后,预期:
- ✅ **单笔最大亏损**从30-50%降低到≤5%固定风险2% + 滑点)
- ✅ **盈亏比**从1.35:1提升到≥2.0:1通过降低止盈目标
- ✅ **止盈率**从2.86%提升到≥30%(通过降低止盈目标)
---
## 📋 建议的配置调整
### 立即调整在GlobalConfig中
1. **降低止盈目标**
- `ATR_TAKE_PROFIT_MULTIPLIER`: 1.5 → **1.2**
- `TAKE_PROFIT_PERCENT`: 25% → **20%**
2. **确保固定风险百分比启用**
- `USE_FIXED_RISK_SIZING`: **True**
- `FIXED_RISK_PERCENT`: **0.02** (2%)
3. **确保止损单挂单**
- `EXCHANGE_SLTP_ENABLED`: **True**(默认已启用)
---
## 🔍 验证方法
修复后,请观察:
1. **开仓日志**:是否显示"使用固定风险百分比计算仓位"
2. **止损单日志**:是否显示"止损单已成功挂到交易所"
3. **实际亏损**是否还有30%以上的亏损
4. **止盈率**是否提升到30%以上

View File

@ -0,0 +1,103 @@
# WebSocket监控 hold_time_minutes 变量未初始化修复
## 🔍 问题描述
**错误信息**
```
trading_system.position_manager - WARNING - FLUIDUSDT WebSocket监控出错 (重试 1/5):
cannot access local variable 'hold_time_minutes' where it is not associated with a value
```
**问题根源**
`_check_single_position()` 方法中,`hold_time_minutes` 变量只在**止损分支**第2725-2734行中被初始化但在**止盈分支**第2889行中也被使用。当代码走止盈路径时变量未被初始化导致 `UnboundLocalError`
## ✅ 修复方案
### 修复位置
`trading_system/position_manager.py``_check_single_position()` 方法
### 修复内容
在止盈分支第2877行之后添加 `hold_time_minutes` 的初始化逻辑:
**修复前**
```python
# 直接比较当前盈亏百分比与止盈目标(基于保证金)
if pnl_percent_margin >= take_profit_pct_margin:
should_close = True
exit_reason = 'take_profit'
# 详细诊断日志:记录平仓时的所有关键信息
logger.info("=" * 80)
logger.info(f"{symbol} [实时监控-平仓诊断日志] ===== 触发止盈平仓 =====")
# ...
logger.info(f" 持仓时间: {hold_time_minutes:.1f} 分钟") # ❌ 变量未初始化
# ...
```
**修复后**
```python
# 直接比较当前盈亏百分比与止盈目标(基于保证金)
if pnl_percent_margin >= take_profit_pct_margin:
should_close = True
exit_reason = 'take_profit'
# 计算持仓时间(用于日志)
entry_time = position_info.get('entryTime')
hold_time_minutes = 0
if entry_time:
try:
if isinstance(entry_time, datetime):
hold_time_sec = int((get_beijing_time() - entry_time).total_seconds())
else:
hold_time_sec = int(time.time() - (float(entry_time) if isinstance(entry_time, (int, float)) else 0))
hold_time_minutes = hold_time_sec / 60.0
except Exception:
hold_time_minutes = 0
# 详细诊断日志:记录平仓时的所有关键信息
logger.info("=" * 80)
logger.info(f"{symbol} [实时监控-平仓诊断日志] ===== 触发止盈平仓 =====")
# ...
logger.info(f" 持仓时间: {hold_time_minutes:.1f} 分钟") # ✅ 变量已初始化
# ...
```
## 📊 修复效果
### 修复前
- ❌ 止盈触发时 → 尝试使用 `hold_time_minutes``UnboundLocalError` → WebSocket监控出错 → 重试
### 修复后
- ✅ 止盈触发时 → `hold_time_minutes` 已初始化 → 正常记录日志 → WebSocket监控正常
## 🔄 相关代码路径
1. **止损分支**第2725-2734行已正确初始化 `hold_time_minutes`
2. **止盈分支**第2877-2907行**已修复**,现在也会初始化 `hold_time_minutes`
3. **第二目标止盈分支**第2838-2846行未使用 `hold_time_minutes`,无需修复
## ⚠️ 注意事项
1. **变量作用域**`hold_time_minutes` 是局部变量,只在各自的代码分支内有效。
2. **初始化逻辑**:与止损分支的初始化逻辑保持一致,确保计算方式统一。
3. **异常处理**:如果计算持仓时间失败,默认设置为 0避免程序崩溃。
## 🚀 部署建议
1. **重启交易进程**:修复后需要重启所有 `trading_system` 进程才能生效。
```bash
supervisorctl restart auto_sys_acc1 auto_sys_acc2 auto_sys_acc3 ...
```
2. **验证修复**:查看日志,确认 WebSocket 监控不再出现 `hold_time_minutes` 相关错误。
## 📝 相关文件
- `trading_system/position_manager.py`:主要修复文件
- `_check_single_position()` 方法第2580-2960行
- `_monitor_position_price()` 方法第2499-2578行
## ✅ 修复完成时间
2026-01-25

View File

@ -0,0 +1,135 @@
# 亏损分析 - ZENUSDT
## 📊 当前情况
### 交易信息
```
ZENUSDT [实时监控] 诊断: 亏损-10.19% of margin |
当前价: 9.6910 |
入场价: 9.8160 |
止损价: 9.6197 (目标: -16.00% of margin) |
方向: BUY |
是否触发: False |
监控状态: 运行中
```
### 计算验证
**价格变化**
- 入场价9.8160
- 当前价9.6910
- 价格跌幅 = (9.8160 - 9.6910) / 9.8160 = **1.27%**
**保证金亏损**
- 假设杠杆8倍山寨币策略默认
- 保证金亏损 = 1.27% × 8 = **10.16%**(接近-10.19%
**止损价计算**
- 止损价9.6197
- 止损距离 = 9.8160 - 9.6197 = 0.1963
- 止损百分比(价格)= 0.1963 / 9.8160 = **2.00%**
- 止损目标(保证金)= 2.00% × 8 = **16.00%**
---
## ✅ 是否正常?
### 结论:**正常** ✅
**理由**
1. **亏损-10.19%是正常的市场波动**
- 价格只下跌了1.27%,这是正常的市场波动
- 山寨币波动大1-2%的价格波动很常见
2. **止损目标-16%符合策略配置**
- 山寨币策略配置:`STOP_LOSS_PERCENT = 15%`(固定止损)
- 实际止损目标-16%可能是因为:
- ATR止损计算的结果`ATR_STOP_LOSS_MULTIPLIER = 2.0`
- 止损价选择逻辑选择了ATR止损而不是固定止损
- 16%接近15%,在合理范围内
3. **止损未触发是正常的**
- 当前亏损-10.19% < 止损目标-16%
- 止损机制正常工作,会在亏损达到-16%时触发
4. **符合山寨币策略设计**
- 山寨币策略设计宽止损15%+ 高盈亏比4:1
- 允许较大的价格波动,避免被正常波动扫损
---
## 📈 止损价分析
### 止损价计算方式
根据山寨币策略配置:
- `STOP_LOSS_PERCENT = 15%`固定止损15%
- `ATR_STOP_LOSS_MULTIPLIER = 2.0`ATR止损2.0倍)
**实际止损价**9.6197
- 止损距离 = 9.8160 - 9.6197 = 0.1963
- 止损百分比 = 0.1963 / 9.8160 = 2.00%
- 保证金亏损 = 2.00% × 8 = 16.00%
**分析**
- 如果使用固定止损15%止损价应该是9.8160 × (1 - 0.15/8) = 9.8160 × 0.98125 = 9.6315
- 实际止损价9.6197 < 9.6315说明可能使用了ATR止损
- ATR止损可能计算出了更远的止损价更宽松
---
## ⚠️ 需要注意的情况
### 1. 如果亏损继续扩大
**如果价格继续下跌**
- 当前亏损:-10.19%
- 止损目标:-16.00%
- **还有5.81%的缓冲空间**
**建议**
- 如果亏损达到-15%,接近止损目标,可以关注
- 如果亏损达到-16%,止损会自动触发
### 2. 如果止损未及时触发
**如果价格快速下跌,止损未及时触发**
- 检查WebSocket监控是否正常工作
- 检查止损单是否正常挂到交易所
- 如果止损单失效系统会通过WebSocket监控触发平仓
---
## 🎯 总结
### ✅ 当前情况:**正常**
1. **亏损-10.19%是正常的市场波动**
- 价格只下跌了1.27%,这是正常的
- 山寨币波动大1-2%的价格波动很常见
2. **止损目标-16%符合策略配置**
- 接近15%的固定止损设置
- 可能是ATR止损计算的结果
3. **止损机制正常工作**
- 当前亏损-10.19% < 止损目标-16%
- 止损会在亏损达到-16%时自动触发
4. **符合山寨币策略设计**
- 宽止损15%)允许较大的价格波动
- 避免被正常波动扫损
### 💡 建议
1. **继续观察**:如果亏损继续扩大,接近-15%时可以关注
2. **信任策略**:止损机制正常工作,会在达到-16%时自动触发
3. **不要手动干预**:除非系统故障,否则不要手动平仓
---
## ✅ 完成时间
2026-01-25

Some files were not shown because too many files have changed in this diff Show More