From 29b0634c6df95fc5e1ae2d87770e0e6bb3a9472c Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 4 Jun 2026 09:57:25 +0800 Subject: [PATCH] feat: bidirectional daily auto-transfer with position skip Rebalance swap to AUTO_TRANSFER_AMOUNT at Beijing hour: top up from funding or sweep excess back. Skip and WeChat notify when active positions exist. Co-authored-by: Cursor --- auto_transfer_daily_lib.py | 130 +++++++++++++++++++ crypto_monitor_binance/.env.example | 2 +- crypto_monitor_binance/app.py | 82 +++--------- crypto_monitor_binance/templates/index.html | 2 +- crypto_monitor_gate/.env.example | 2 +- crypto_monitor_gate/app.py | 82 +++--------- crypto_monitor_gate/templates/index.html | 2 +- crypto_monitor_gate_bot/.env.example | 2 +- crypto_monitor_gate_bot/app.py | 82 +++--------- crypto_monitor_gate_bot/templates/index.html | 2 +- crypto_monitor_okx/.env.example | 2 +- crypto_monitor_okx/app.py | 82 +++--------- crypto_monitor_okx/templates/index.html | 2 +- deploy/README.md | 2 +- docs/auto-transfer-daily.md | 33 +++++ scripts/sync_four_exchange_transfer_env.py | 2 +- 16 files changed, 241 insertions(+), 270 deletions(-) create mode 100644 auto_transfer_daily_lib.py create mode 100644 docs/auto-transfer-daily.md diff --git a/auto_transfer_daily_lib.py b/auto_transfer_daily_lib.py new file mode 100644 index 0000000..da2050a --- /dev/null +++ b/auto_transfer_daily_lib.py @@ -0,0 +1,130 @@ +""" +每日自动划转:北京时间指定整点小时内,将交易账户(AUTO_TRANSFER_TO)余额调整至目标额。 + +- 交易账户 < 目标:从资金账户划入差额 +- 交易账户 > 目标:将多余划回资金账户 +- 有 active 持仓:不划转,写账簿并企业微信说明 +""" +from __future__ import annotations + +from typing import Any, Callable + + +def run_auto_transfer_once_per_day( + *, + enabled: bool, + bj_hour: int, + target_amount: float, + from_account: str, + to_account: str, + funds_decimals: int, + get_db: Callable[[], Any], + get_active_position_count: Callable[[Any], int], + get_account_usdt_total: Callable[[str], float | None], + execute_transfer_usdt: Callable[[float, str, str], tuple[bool, str, Any]], + send_wechat_msg: Callable[[str], None], + utc_now_dt: Callable[[], Any], + app_tz: Any, + utc_calendar_date_str: Callable[[], str], + app_now_str: Callable[[], str], + min_transfer: float = 0.01, +) -> None: + if not enabled: + return + utc_dt = utc_now_dt() + bj = utc_dt.astimezone(app_tz) + if bj.hour != bj_hour: + return + + transfer_day = utc_calendar_date_str() + conn = get_db() + exists = conn.execute( + "SELECT id FROM transfer_logs WHERE transfer_type=? AND transfer_day=?", + ("auto_daily", transfer_day), + ).fetchone() + if exists: + conn.close() + return + + def _log( + amount: float, + fr: str, + to: str, + status: str, + message: str, + *, + commit_close: bool = True, + ) -> None: + conn.execute( + "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", + ("auto_daily", transfer_day, amount, fr, to, status, message[:500]), + ) + conn.commit() + if commit_close: + conn.close() + + active = get_active_position_count(conn) + if active > 0: + msg = f"持仓中({active}笔),本次资金无划转" + _log(0, from_account, to_account, "skipped", msg) + send_wechat_msg( + f"自动划转:{msg}\n" + f"目标:{to_account} 调整至 {round(float(target_amount), funds_decimals)}U\n" + f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}" + ) + return + + target = round(float(target_amount), funds_decimals) + trade_bal = get_account_usdt_total(to_account) + if trade_bal is None: + _log( + 0, + from_account, + to_account, + "failed", + f"读取{to_account}账户USDT失败", + ) + return + + trade = round(float(trade_bal), funds_decimals) + diff = round(target - trade, funds_decimals) + + if abs(diff) < min_transfer: + _log( + 0, + from_account, + to_account, + "skipped", + f"{to_account}账户已为{trade}U(目标{target}U)", + ) + return + + if diff > 0: + fr, to, amount = from_account, to_account, diff + action = "划入" + else: + fr, to, amount = to_account, from_account, round(abs(diff), funds_decimals) + action = "划出" + + from_bal = get_account_usdt_total(fr) + if from_bal is not None and round(float(from_bal), funds_decimals) < amount: + cur = round(float(from_bal), funds_decimals) + _log(amount, fr, to, "failed", f"{fr}账户USDT不足,需{amount}U,当前{cur}U") + send_wechat_msg( + f"自动划转失败:{fr}余额不足,需{amount}U,当前{cur}U({action}至{to_account}目标{target}U)\n" + f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}" + ) + return + + ok, msg, _ = execute_transfer_usdt(amount, fr, to) + _log(amount, fr, to, "success" if ok else "failed", msg) + if ok: + send_wechat_msg( + f"自动划转成功:{to_account} {trade}U→目标{target}U,{action}{amount}U {fr}->{to}\n" + f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}" + ) + else: + send_wechat_msg( + f"自动划转失败:计划{action}{amount}U {fr}->{to}(目标{target}U)\n原因:{msg}\n" + f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}" + ) diff --git a/crypto_monitor_binance/.env.example b/crypto_monitor_binance/.env.example index 2e12178..95e109f 100644 --- a/crypto_monitor_binance/.env.example +++ b/crypto_monitor_binance/.env.example @@ -136,7 +136,7 @@ FULL_MARGIN_BUFFER_RATIO=0.98 # 自动划转(页顶「将 swap 补足到 XU」;与 DAILY_START_CAPITAL 独立,需一致时请设为相同值) # ============================================================================= AUTO_TRANSFER_ENABLED=false -# 合约/交易账户(AUTO_TRANSFER_TO)补足到的 USDT 总额,非每日开仓基数 +# 交易账户(swap)目标余额 U:每日 8 点(北京)自动划入或划出至 funding;持仓中不划转 AUTO_TRANSFER_AMOUNT=30 AUTO_TRANSFER_FROM=funding AUTO_TRANSFER_TO=swap diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 7654aa6..56d5c95 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -98,6 +98,7 @@ from key_monitor_full_margin_lib import ( monitor_type_disallowed_in_full_margin, purge_disallowed_key_monitors, ) +from auto_transfer_daily_lib import run_auto_transfer_once_per_day from key_monitor_lib import ( KEY_DIRECTION_WATCH, KEY_MONITOR_ALERT_ONLY_TYPES, @@ -2977,72 +2978,23 @@ def get_account_usdt_total(account_type): def auto_transfer_once_per_day(): - if not AUTO_TRANSFER_ENABLED: - return - utc_dt = utc_now_dt() - bj = utc_dt.astimezone(APP_TZ) - if bj.hour != AUTO_TRANSFER_BJ_HOUR: - return - transfer_day = utc_calendar_date_str() - conn = get_db() - exists = conn.execute( - "SELECT id FROM transfer_logs WHERE transfer_type=? AND transfer_day=?", - ("auto_daily", transfer_day) - ).fetchone() - if exists: - conn.close() - return - target_amount = AUTO_TRANSFER_AMOUNT - to_balance = get_account_usdt_total(AUTO_TRANSFER_TO) - from_balance = get_account_usdt_total(AUTO_TRANSFER_FROM) - if to_balance is None: - conn.execute( - "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", - ("auto_daily", transfer_day, 0, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "failed", f"读取{AUTO_TRANSFER_TO}账户USDT失败") - ) - conn.commit() - conn.close() - return - needed = round(max(target_amount - float(to_balance), 0), FUNDS_DECIMALS) - if needed <= 0: - conn.execute( - "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", - ("auto_daily", transfer_day, 0, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "skipped", f"{AUTO_TRANSFER_TO}账户已达到目标{target_amount}U") - ) - conn.commit() - conn.close() - return - if from_balance is not None and from_balance < needed: - conn.execute( - "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", - ("auto_daily", transfer_day, needed, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "failed", f"{AUTO_TRANSFER_FROM}账户USDT不足,需{needed}U,当前{round(from_balance, FUNDS_DECIMALS)}U") - ) - conn.commit() - conn.close() - send_wechat_msg( - f"自动划转失败:{AUTO_TRANSFER_FROM}余额不足,需{needed}U,当前{round(from_balance, FUNDS_DECIMALS)}U\n" - f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}" - ) - return - - ok, msg, _ = execute_transfer_usdt(needed, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO) - conn.execute( - "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", - ("auto_daily", transfer_day, needed, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "success" if ok else "failed", msg[:500]) + run_auto_transfer_once_per_day( + enabled=AUTO_TRANSFER_ENABLED, + bj_hour=AUTO_TRANSFER_BJ_HOUR, + target_amount=AUTO_TRANSFER_AMOUNT, + from_account=AUTO_TRANSFER_FROM, + to_account=AUTO_TRANSFER_TO, + funds_decimals=FUNDS_DECIMALS, + get_db=get_db, + get_active_position_count=get_active_position_count, + get_account_usdt_total=get_account_usdt_total, + execute_transfer_usdt=execute_transfer_usdt, + send_wechat_msg=send_wechat_msg, + utc_now_dt=utc_now_dt, + app_tz=APP_TZ, + utc_calendar_date_str=utc_calendar_date_str, + app_now_str=app_now_str, ) - conn.commit() - conn.close() - if ok: - send_wechat_msg( - f"自动划转成功:补足到{target_amount}U,实际划转{needed}U " - f"{AUTO_TRANSFER_FROM}->{AUTO_TRANSFER_TO}\n" - f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}" - ) - else: - send_wechat_msg( - f"自动划转失败:计划补足到{target_amount}U,需划转{needed}U\n原因:{msg}\n" - f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}" - ) def trading_day_reset_allows_new_open(now): diff --git a/crypto_monitor_binance/templates/index.html b/crypto_monitor_binance/templates/index.html index ff8e5bf..ee032f4 100644 --- a/crypto_monitor_binance/templates/index.html +++ b/crypto_monitor_binance/templates/index.html @@ -426,7 +426,7 @@ |移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
- 划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天北京时间 {{ auto_transfer_bj_hour }}:00起该整点小时内尝试;账簿按 UTC 自然日去重;界面时间为北京;将 {{ auto_transfer_to }} 补足到 {{ auto_transfer_amount }}U,来自 {{ auto_transfer_from }}) + 划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天北京时间 {{ auto_transfer_bj_hour }}:00起该整点小时内尝试;账簿按 UTC 自然日去重;将 {{ auto_transfer_to }} 调整至 {{ auto_transfer_amount }}U:不足从 {{ auto_transfer_from }} 划入、超出划回 {{ auto_transfer_from }};持仓中不划转并微信通知)
diff --git a/crypto_monitor_gate/.env.example b/crypto_monitor_gate/.env.example index 1d920d9..e0cd8e3 100644 --- a/crypto_monitor_gate/.env.example +++ b/crypto_monitor_gate/.env.example @@ -142,7 +142,7 @@ FULL_MARGIN_BUFFER_RATIO=0.98 # 自动划转(页顶「将 swap 补足到 XU」;与 DAILY_START_CAPITAL 独立,需一致时请设为相同值) # ============================================================================= AUTO_TRANSFER_ENABLED=false -# 交易账户(AUTO_TRANSFER_TO)补足到的 USDT 总额,非每日开仓基数 +# 交易账户(swap)目标余额 U:每日 8 点(北京)自动划入或划出至 funding;持仓中不划转 AUTO_TRANSFER_AMOUNT=30 AUTO_TRANSFER_FROM=funding AUTO_TRANSFER_TO=swap diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index e76da55..0599e12 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -97,6 +97,7 @@ from key_monitor_full_margin_lib import ( monitor_type_disallowed_in_full_margin, purge_disallowed_key_monitors, ) +from auto_transfer_daily_lib import run_auto_transfer_once_per_day from key_monitor_lib import ( KEY_DIRECTION_WATCH, KEY_MONITOR_ALERT_ONLY_TYPES, @@ -2670,72 +2671,23 @@ def get_account_usdt_total(account_type): def auto_transfer_once_per_day(): - if not AUTO_TRANSFER_ENABLED: - return - utc_dt = utc_now_dt() - bj = utc_dt.astimezone(APP_TZ) - if bj.hour != AUTO_TRANSFER_BJ_HOUR: - return - transfer_day = utc_calendar_date_str() - conn = get_db() - exists = conn.execute( - "SELECT id FROM transfer_logs WHERE transfer_type=? AND transfer_day=?", - ("auto_daily", transfer_day) - ).fetchone() - if exists: - conn.close() - return - target_amount = AUTO_TRANSFER_AMOUNT - to_balance = get_account_usdt_total(AUTO_TRANSFER_TO) - from_balance = get_account_usdt_total(AUTO_TRANSFER_FROM) - if to_balance is None: - conn.execute( - "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", - ("auto_daily", transfer_day, 0, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "failed", f"读取{AUTO_TRANSFER_TO}账户USDT失败") - ) - conn.commit() - conn.close() - return - needed = round(max(target_amount - float(to_balance), 0), 4) - if needed <= 0: - conn.execute( - "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", - ("auto_daily", transfer_day, 0, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "skipped", f"{AUTO_TRANSFER_TO}账户已达到目标{round(float(target_amount), 2)}U") - ) - conn.commit() - conn.close() - return - if from_balance is not None and from_balance < needed: - conn.execute( - "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", - ("auto_daily", transfer_day, needed, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "failed", f"{AUTO_TRANSFER_FROM}账户USDT不足,需{round(needed, 2)}U,当前{round(from_balance, 2)}U") - ) - conn.commit() - conn.close() - send_wechat_msg( - f"自动划转失败:{AUTO_TRANSFER_FROM}余额不足,需{round(needed, 2)}U,当前{round(from_balance, 2)}U\n" - f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}" - ) - return - - ok, msg, _ = execute_transfer_usdt(needed, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO) - conn.execute( - "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", - ("auto_daily", transfer_day, needed, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "success" if ok else "failed", msg[:500]) + run_auto_transfer_once_per_day( + enabled=AUTO_TRANSFER_ENABLED, + bj_hour=AUTO_TRANSFER_BJ_HOUR, + target_amount=AUTO_TRANSFER_AMOUNT, + from_account=AUTO_TRANSFER_FROM, + to_account=AUTO_TRANSFER_TO, + funds_decimals=2, + get_db=get_db, + get_active_position_count=get_active_position_count, + get_account_usdt_total=get_account_usdt_total, + execute_transfer_usdt=execute_transfer_usdt, + send_wechat_msg=send_wechat_msg, + utc_now_dt=utc_now_dt, + app_tz=APP_TZ, + utc_calendar_date_str=utc_calendar_date_str, + app_now_str=app_now_str, ) - conn.commit() - conn.close() - if ok: - send_wechat_msg( - f"自动划转成功:补足到{round(float(target_amount), 2)}U,实际划转{round(needed, 2)}U " - f"{AUTO_TRANSFER_FROM}->{AUTO_TRANSFER_TO}\n" - f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}" - ) - else: - send_wechat_msg( - f"自动划转失败:计划补足到{round(float(target_amount), 2)}U,需划转{round(needed, 2)}U\n原因:{msg}\n" - f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}" - ) def trading_day_reset_allows_new_open(now): diff --git a/crypto_monitor_gate/templates/index.html b/crypto_monitor_gate/templates/index.html index ff8e5bf..ee032f4 100644 --- a/crypto_monitor_gate/templates/index.html +++ b/crypto_monitor_gate/templates/index.html @@ -426,7 +426,7 @@ |移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
- 划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天北京时间 {{ auto_transfer_bj_hour }}:00起该整点小时内尝试;账簿按 UTC 自然日去重;界面时间为北京;将 {{ auto_transfer_to }} 补足到 {{ auto_transfer_amount }}U,来自 {{ auto_transfer_from }}) + 划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天北京时间 {{ auto_transfer_bj_hour }}:00起该整点小时内尝试;账簿按 UTC 自然日去重;将 {{ auto_transfer_to }} 调整至 {{ auto_transfer_amount }}U:不足从 {{ auto_transfer_from }} 划入、超出划回 {{ auto_transfer_from }};持仓中不划转并微信通知)
diff --git a/crypto_monitor_gate_bot/.env.example b/crypto_monitor_gate_bot/.env.example index 505af1a..894a438 100644 --- a/crypto_monitor_gate_bot/.env.example +++ b/crypto_monitor_gate_bot/.env.example @@ -110,7 +110,7 @@ FULL_MARGIN_BUFFER_RATIO=0.98 # 自动划转(页顶「将 swap 补足到 XU」;与 DAILY_START_CAPITAL 独立,需一致时请设为相同值) # ============================================================================= AUTO_TRANSFER_ENABLED=false -# 交易账户(AUTO_TRANSFER_TO)补足到的 USDT 总额,非每日开仓基数 +# 交易账户(swap)目标余额 U:每日 8 点(北京)自动划入或划出至 funding;持仓中不划转 AUTO_TRANSFER_AMOUNT=30 AUTO_TRANSFER_FROM=funding AUTO_TRANSFER_TO=swap diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index 67ddb65..7e97f7c 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -49,6 +49,7 @@ from key_monitor_full_margin_lib import ( monitor_type_disallowed_in_full_margin, purge_disallowed_key_monitors, ) +from auto_transfer_daily_lib import run_auto_transfer_once_per_day from form_submit_lib import check_duplicate_submit, submit_scope_add_key, submit_scope_add_order from order_monitor_display_lib import ( apply_order_price_display_fields, @@ -2675,72 +2676,23 @@ def get_account_usdt_total(account_type): def auto_transfer_once_per_day(): - if not AUTO_TRANSFER_ENABLED: - return - utc_dt = utc_now_dt() - bj = utc_dt.astimezone(APP_TZ) - if bj.hour != AUTO_TRANSFER_BJ_HOUR: - return - transfer_day = utc_calendar_date_str() - conn = get_db() - exists = conn.execute( - "SELECT id FROM transfer_logs WHERE transfer_type=? AND transfer_day=?", - ("auto_daily", transfer_day) - ).fetchone() - if exists: - conn.close() - return - target_amount = AUTO_TRANSFER_AMOUNT - to_balance = get_account_usdt_total(AUTO_TRANSFER_TO) - from_balance = get_account_usdt_total(AUTO_TRANSFER_FROM) - if to_balance is None: - conn.execute( - "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", - ("auto_daily", transfer_day, 0, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "failed", f"读取{AUTO_TRANSFER_TO}账户USDT失败") - ) - conn.commit() - conn.close() - return - needed = round(max(target_amount - float(to_balance), 0), 4) - if needed <= 0: - conn.execute( - "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", - ("auto_daily", transfer_day, 0, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "skipped", f"{AUTO_TRANSFER_TO}账户已达到目标{target_amount}U") - ) - conn.commit() - conn.close() - return - if from_balance is not None and from_balance < needed: - conn.execute( - "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", - ("auto_daily", transfer_day, needed, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "failed", f"{AUTO_TRANSFER_FROM}账户USDT不足,需{needed}U,当前{round(from_balance,4)}U") - ) - conn.commit() - conn.close() - send_wechat_msg( - f"自动划转失败:{AUTO_TRANSFER_FROM}余额不足,需{needed}U,当前{round(from_balance,4)}U\n" - f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}" - ) - return - - ok, msg, _ = execute_transfer_usdt(needed, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO) - conn.execute( - "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", - ("auto_daily", transfer_day, needed, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "success" if ok else "failed", msg[:500]) + run_auto_transfer_once_per_day( + enabled=AUTO_TRANSFER_ENABLED, + bj_hour=AUTO_TRANSFER_BJ_HOUR, + target_amount=AUTO_TRANSFER_AMOUNT, + from_account=AUTO_TRANSFER_FROM, + to_account=AUTO_TRANSFER_TO, + funds_decimals=2, + get_db=get_db, + get_active_position_count=get_active_position_count, + get_account_usdt_total=get_account_usdt_total, + execute_transfer_usdt=execute_transfer_usdt, + send_wechat_msg=send_wechat_msg, + utc_now_dt=utc_now_dt, + app_tz=APP_TZ, + utc_calendar_date_str=utc_calendar_date_str, + app_now_str=app_now_str, ) - conn.commit() - conn.close() - if ok: - send_wechat_msg( - f"自动划转成功:补足到{target_amount}U,实际划转{needed}U " - f"{AUTO_TRANSFER_FROM}->{AUTO_TRANSFER_TO}\n" - f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}" - ) - else: - send_wechat_msg( - f"自动划转失败:计划补足到{target_amount}U,需划转{needed}U\n原因:{msg}\n" - f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}" - ) def trading_day_reset_allows_new_open(now): diff --git a/crypto_monitor_gate_bot/templates/index.html b/crypto_monitor_gate_bot/templates/index.html index b527529..e484aca 100644 --- a/crypto_monitor_gate_bot/templates/index.html +++ b/crypto_monitor_gate_bot/templates/index.html @@ -311,7 +311,7 @@ |移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
- 划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天北京时间 {{ auto_transfer_bj_hour }}:00起该整点小时内尝试;账簿按 UTC 自然日去重;界面时间为北京;将 {{ auto_transfer_to }} 补足到 {{ money_fmt(auto_transfer_amount) }}U,来自 {{ auto_transfer_from }}) + 划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天北京时间 {{ auto_transfer_bj_hour }}:00起该整点小时内尝试;账簿按 UTC 自然日去重;将 {{ auto_transfer_to }} 调整至 {{ money_fmt(auto_transfer_amount) }}U:不足从 {{ auto_transfer_from }} 划入、超出划回 {{ auto_transfer_from }};持仓中不划转并微信通知)
diff --git a/crypto_monitor_okx/.env.example b/crypto_monitor_okx/.env.example index dbbfbe3..311e543 100644 --- a/crypto_monitor_okx/.env.example +++ b/crypto_monitor_okx/.env.example @@ -101,7 +101,7 @@ FULL_MARGIN_BUFFER_RATIO=0.98 # 自动划转(页顶「将 swap 补足到 XU」;与 DAILY_START_CAPITAL 独立,需一致时请设为相同值) # ============================================================================= AUTO_TRANSFER_ENABLED=false -# 交易账户(AUTO_TRANSFER_TO)补足到的 USDT 总额,非每日开仓基数 +# 交易账户(swap)目标余额 U:每日 8 点(北京)自动划入或划出至 funding;持仓中不划转 AUTO_TRANSFER_AMOUNT=30 AUTO_TRANSFER_FROM=funding AUTO_TRANSFER_TO=swap diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 05b0b7a..3def956 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -97,6 +97,7 @@ from key_monitor_full_margin_lib import ( monitor_type_disallowed_in_full_margin, purge_disallowed_key_monitors, ) +from auto_transfer_daily_lib import run_auto_transfer_once_per_day from key_monitor_lib import ( KEY_DIRECTION_WATCH, KEY_MONITOR_ALERT_ONLY_TYPES, @@ -2356,72 +2357,23 @@ def get_account_usdt_total(account_type): def auto_transfer_once_per_day(): - if not AUTO_TRANSFER_ENABLED: - return - utc_dt = utc_now_dt() - bj = utc_dt.astimezone(APP_TZ) - if bj.hour != AUTO_TRANSFER_BJ_HOUR: - return - transfer_day = utc_calendar_date_str() - conn = get_db() - exists = conn.execute( - "SELECT id FROM transfer_logs WHERE transfer_type=? AND transfer_day=?", - ("auto_daily", transfer_day) - ).fetchone() - if exists: - conn.close() - return - target_amount = AUTO_TRANSFER_AMOUNT - to_balance = get_account_usdt_total(AUTO_TRANSFER_TO) - from_balance = get_account_usdt_total(AUTO_TRANSFER_FROM) - if to_balance is None: - conn.execute( - "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", - ("auto_daily", transfer_day, 0, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "failed", f"读取{AUTO_TRANSFER_TO}账户USDT失败") - ) - conn.commit() - conn.close() - return - needed = round(max(target_amount - float(to_balance), 0), 4) - if needed <= 0: - conn.execute( - "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", - ("auto_daily", transfer_day, 0, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "skipped", f"{AUTO_TRANSFER_TO}账户已达到目标{target_amount}U") - ) - conn.commit() - conn.close() - return - if from_balance is not None and from_balance < needed: - conn.execute( - "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", - ("auto_daily", transfer_day, needed, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "failed", f"{AUTO_TRANSFER_FROM}账户USDT不足,需{round(needed, 2)}U,当前{round(from_balance, 2)}U") - ) - conn.commit() - conn.close() - send_wechat_msg( - f"自动划转失败:{AUTO_TRANSFER_FROM}余额不足,需{round(needed, 2)}U,当前{round(from_balance, 2)}U\n" - f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}" - ) - return - - ok, msg, _ = execute_transfer_usdt(needed, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO) - conn.execute( - "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", - ("auto_daily", transfer_day, needed, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "success" if ok else "failed", msg[:500]) + run_auto_transfer_once_per_day( + enabled=AUTO_TRANSFER_ENABLED, + bj_hour=AUTO_TRANSFER_BJ_HOUR, + target_amount=AUTO_TRANSFER_AMOUNT, + from_account=AUTO_TRANSFER_FROM, + to_account=AUTO_TRANSFER_TO, + funds_decimals=FUNDS_DECIMALS, + get_db=get_db, + get_active_position_count=get_active_position_count, + get_account_usdt_total=get_account_usdt_total, + execute_transfer_usdt=execute_transfer_usdt, + send_wechat_msg=send_wechat_msg, + utc_now_dt=utc_now_dt, + app_tz=APP_TZ, + utc_calendar_date_str=utc_calendar_date_str, + app_now_str=app_now_str, ) - conn.commit() - conn.close() - if ok: - send_wechat_msg( - f"自动划转成功:补足到{round(float(target_amount), 2)}U,实际划转{round(needed, 2)}U " - f"{AUTO_TRANSFER_FROM}->{AUTO_TRANSFER_TO}\n" - f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}" - ) - else: - send_wechat_msg( - f"自动划转失败:计划补足到{round(float(target_amount), 2)}U,需划转{round(needed, 2)}U\n原因:{msg}\n" - f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}" - ) def get_trading_day_reset_open_guard_enabled(conn=None): diff --git a/crypto_monitor_okx/templates/index.html b/crypto_monitor_okx/templates/index.html index 766c748..6bc7549 100644 --- a/crypto_monitor_okx/templates/index.html +++ b/crypto_monitor_okx/templates/index.html @@ -435,7 +435,7 @@ |移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
- 划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天北京时间 {{ auto_transfer_bj_hour }}:00起该整点小时内尝试;账簿按 UTC 自然日去重;界面时间为北京;将 {{ auto_transfer_to }} 补足到 {{ auto_transfer_amount }}U,来自 {{ auto_transfer_from }}) + 划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天北京时间 {{ auto_transfer_bj_hour }}:00起该整点小时内尝试;账簿按 UTC 自然日去重;将 {{ auto_transfer_to }} 调整至 {{ auto_transfer_amount }}U:不足从 {{ auto_transfer_from }} 划入、超出划回 {{ auto_transfer_from }};持仓中不划转并微信通知)
diff --git a/deploy/README.md b/deploy/README.md index 8541118..8d49521 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -87,7 +87,7 @@ bash deploy/setup_env.sh ## 四所 `.env` 自动划转项(已有 .env 时) -`AUTO_TRANSFER_AMOUNT` 等与 `DAILY_START_CAPITAL` **独立**;四所 `.env.example` 已统一注释。若服务器上已有 `.env`,可合并写入(不覆盖 API 密钥): +`AUTO_TRANSFER_AMOUNT` 等为交易账户目标余额(北京时间 8 点自动划入/划出,**持仓中不划转**并微信通知),与 `DAILY_START_CAPITAL` **独立**。若服务器上已有 `.env`,可合并写入(不覆盖 API 密钥): ```bash python scripts/sync_four_exchange_transfer_env.py diff --git a/docs/auto-transfer-daily.md b/docs/auto-transfer-daily.md new file mode 100644 index 0000000..3ec3fa9 --- /dev/null +++ b/docs/auto-transfer-daily.md @@ -0,0 +1,33 @@ +# 每日自动划转(四所统一) + +## 行为 + +在 `.env` 开启 `AUTO_TRANSFER_ENABLED=true` 后,监控轮询在**北京时间 `AUTO_TRANSFER_BJ_HOUR` 整点所在小时**内(默认 8:00–8:59)执行一次(按 **UTC 自然日** 去重): + +| 交易账户 (`AUTO_TRANSFER_TO`,默认 swap) | 动作 | +|------------------------------------------|------| +| 余额 **低于** `AUTO_TRANSFER_AMOUNT` | 从 `AUTO_TRANSFER_FROM`(默认 funding)划入差额 | +| 余额 **高于** `AUTO_TRANSFER_AMOUNT` | 将多余划回 `AUTO_TRANSFER_FROM` | +| 与目标相差 < 0.01U | 跳过,不写划转 | +| 存在 **active** 持仓(`order_monitors`) | **不划转**,写账簿 `skipped`,并**企业微信**说明「持仓中,本次资金无划转」 | + +## 配置示例(目标 50U) + +```env +AUTO_TRANSFER_ENABLED=true +AUTO_TRANSFER_AMOUNT=50 +AUTO_TRANSFER_FROM=funding +AUTO_TRANSFER_TO=swap +AUTO_TRANSFER_BJ_HOUR=8 +``` + +API Key 须具备万向划转权限(与手动划转相同)。 + +## 部署 + +```bash +git pull +python scripts/sync_four_exchange_transfer_env.py # 仅补全缺项 +# 编辑各所 .env:AUTO_TRANSFER_ENABLED=true、AUTO_TRANSFER_AMOUNT=50 +pm2 restart crypto-monitor-binance crypto-monitor-okx crypto-monitor-gate crypto-monitor-gate-bot +``` diff --git a/scripts/sync_four_exchange_transfer_env.py b/scripts/sync_four_exchange_transfer_env.py index 98ede86..8db57e8 100644 --- a/scripts/sync_four_exchange_transfer_env.py +++ b/scripts/sync_four_exchange_transfer_env.py @@ -23,7 +23,7 @@ INSTANCES = ( "crypto_monitor_gate_bot", ) -# 四所统一(页顶「将 swap 补足到 XU」= AUTO_TRANSFER_AMOUNT,与 DAILY_START_CAPITAL 独立) +# 四所统一(页顶「将 swap 调整至 XU」= AUTO_TRANSFER_AMOUNT,双向归集,与 DAILY_START_CAPITAL 独立) COMMON_KEYS = { "AUTO_TRANSFER_ENABLED": "false", "AUTO_TRANSFER_FROM": "funding",