refactor: 将共用代码迁入 lib/ 模块化目录
统一 strategy、key_monitor、trade、hub 等共用库到 lib/ 子包,并补充 lib-structure 文档,便于四所与中控维护。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -53,8 +53,11 @@ bash deploy/setup_env.sh --install-system-deps
|
|||||||
| `crypto_monitor_gate_bot/` | Gate 机器人 / 趋势户 | [部署文档.md](./crypto_monitor_gate_bot/部署文档.md) |
|
| `crypto_monitor_gate_bot/` | Gate 机器人 / 趋势户 | [部署文档.md](./crypto_monitor_gate_bot/部署文档.md) |
|
||||||
| `crypto_monitor_okx/` | OKX 永续 | [部署文档.md](./crypto_monitor_okx/部署文档.md) |
|
| `crypto_monitor_okx/` | OKX 永续 | [部署文档.md](./crypto_monitor_okx/部署文档.md) |
|
||||||
| `manual_trading_hub/` | 中控 + 子代理 | [部署文档.md](./manual_trading_hub/部署文档.md) |
|
| `manual_trading_hub/` | 中控 + 子代理 | [部署文档.md](./manual_trading_hub/部署文档.md) |
|
||||||
| 根目录 `strategy_*.py` | 策略共用库 | [策略交易说明.md](./策略交易说明.md) |
|
| `lib/` | **共用模块**(策略、关键位、交易、中控库、AI、静态与模板) | **[docs/lib-structure.md](./docs/lib-structure.md)** |
|
||||||
| 根目录 `key_*_lib.py` | 关键位 / 止盈止损共用库 | [关键位止盈止损与移动保本更新说明.md](./关键位止盈止损与移动保本更新说明.md) |
|
| `brand/` | 各所共用图标与 manifest | — |
|
||||||
|
| `docs/`、`deploy/`、`scripts/`、`tests/` | 文档、环境、脚本、单元测试 | — |
|
||||||
|
|
||||||
|
共用代码 import 示例:`from lib.strategy.strategy_db import init_strategy_tables`(各所启动时仍将仓库根加入 `PYTHONPATH`)。详见 **[docs/lib-structure.md](./docs/lib-structure.md)**。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -34,14 +34,15 @@ import sys
|
|||||||
|
|
||||||
if _REPO_ROOT not in sys.path:
|
if _REPO_ROOT not in sys.path:
|
||||||
sys.path.insert(0, _REPO_ROOT)
|
sys.path.insert(0, _REPO_ROOT)
|
||||||
from ai_client import ai_generate, ai_review, ai_short_advice
|
from lib.paths import common_static_dir
|
||||||
from ai_review_lib import (
|
from lib.ai.ai_client import ai_generate, ai_review, ai_short_advice
|
||||||
|
from lib.ai.ai_review_lib import (
|
||||||
build_journal_ai_chart_path,
|
build_journal_ai_chart_path,
|
||||||
collect_images_for_ai_review,
|
collect_images_for_ai_review,
|
||||||
journal_row_lines_for_ai,
|
journal_row_lines_for_ai,
|
||||||
)
|
)
|
||||||
from form_submit_lib import check_duplicate_submit, submit_scope_add_key, submit_scope_add_order
|
from lib.common.form_submit_lib import check_duplicate_submit, submit_scope_add_key, submit_scope_add_order
|
||||||
from fib_key_monitor_lib import (
|
from lib.key_monitor.fib_key_monitor_lib import (
|
||||||
FIB_KEY_MONITOR_TYPES,
|
FIB_KEY_MONITOR_TYPES,
|
||||||
backfill_missing_key_signal_types,
|
backfill_missing_key_signal_types,
|
||||||
calc_fib_plan,
|
calc_fib_plan,
|
||||||
@@ -52,7 +53,7 @@ from fib_key_monitor_lib import (
|
|||||||
key_signal_type_for_trade_record,
|
key_signal_type_for_trade_record,
|
||||||
stored_key_signal_type,
|
stored_key_signal_type,
|
||||||
)
|
)
|
||||||
from false_breakout_key_monitor_lib import (
|
from lib.key_monitor.false_breakout_key_monitor_lib import (
|
||||||
FALSE_BREAKOUT_MONITOR_TYPE,
|
FALSE_BREAKOUT_MONITOR_TYPE,
|
||||||
FALSE_BREAKOUT_VALIDITY_HOURS,
|
FALSE_BREAKOUT_VALIDITY_HOURS,
|
||||||
calc_false_breakout_plan,
|
calc_false_breakout_plan,
|
||||||
@@ -65,7 +66,7 @@ from false_breakout_key_monitor_lib import (
|
|||||||
normalize_false_breakout_symbol,
|
normalize_false_breakout_symbol,
|
||||||
storage_bounds_from_key_price,
|
storage_bounds_from_key_price,
|
||||||
)
|
)
|
||||||
from strategy_trade_labels import (
|
from lib.strategy.strategy_trade_labels import (
|
||||||
STRATEGY_ENTRY_REASON_OPTIONS,
|
STRATEGY_ENTRY_REASON_OPTIONS,
|
||||||
apply_order_monitor_source_labels,
|
apply_order_monitor_source_labels,
|
||||||
entry_reason_for_monitor_type,
|
entry_reason_for_monitor_type,
|
||||||
@@ -74,7 +75,7 @@ from strategy_trade_labels import (
|
|||||||
trade_record_monitor_type as resolve_trade_record_monitor_type,
|
trade_record_monitor_type as resolve_trade_record_monitor_type,
|
||||||
trend_plan_id_from_monitor_row,
|
trend_plan_id_from_monitor_row,
|
||||||
)
|
)
|
||||||
from journal_chart_lib import (
|
from lib.instance.journal_chart_lib import (
|
||||||
JOURNAL_CHART_DEFAULT_LIMIT,
|
JOURNAL_CHART_DEFAULT_LIMIT,
|
||||||
JOURNAL_CHART_DEFAULT_TF1,
|
JOURNAL_CHART_DEFAULT_TF1,
|
||||||
JOURNAL_CHART_DEFAULT_TF2,
|
JOURNAL_CHART_DEFAULT_TF2,
|
||||||
@@ -90,7 +91,7 @@ from journal_chart_lib import (
|
|||||||
trade_review_fetch_window,
|
trade_review_fetch_window,
|
||||||
trim_rows_for_trade_review,
|
trim_rows_for_trade_review,
|
||||||
)
|
)
|
||||||
from key_sl_tp_lib import (
|
from lib.key_monitor.key_sl_tp_lib import (
|
||||||
breakeven_enabled_from_row,
|
breakeven_enabled_from_row,
|
||||||
normalize_sl_tp_mode,
|
normalize_sl_tp_mode,
|
||||||
parse_breakeven_enabled_form,
|
parse_breakeven_enabled_form,
|
||||||
@@ -99,7 +100,7 @@ from key_sl_tp_lib import (
|
|||||||
sl_tp_mode_label,
|
sl_tp_mode_label,
|
||||||
sl_tp_plan_summary_text,
|
sl_tp_plan_summary_text,
|
||||||
)
|
)
|
||||||
from time_close_lib import (
|
from lib.trade.time_close_lib import (
|
||||||
TIME_CLOSE_RESULT,
|
TIME_CLOSE_RESULT,
|
||||||
apply_time_close_to_payload,
|
apply_time_close_to_payload,
|
||||||
ensure_time_close_schema,
|
ensure_time_close_schema,
|
||||||
@@ -110,13 +111,13 @@ from time_close_lib import (
|
|||||||
time_close_label,
|
time_close_label,
|
||||||
time_close_settings_from_row,
|
time_close_settings_from_row,
|
||||||
)
|
)
|
||||||
from manual_sltp_lib import (
|
from lib.trade.manual_sltp_lib import (
|
||||||
normalize_open_sltp_mode,
|
normalize_open_sltp_mode,
|
||||||
resolve_entrust_sltp_prices,
|
resolve_entrust_sltp_prices,
|
||||||
resolve_open_sltp_prices,
|
resolve_open_sltp_prices,
|
||||||
)
|
)
|
||||||
from key_monitor_schema_lib import ensure_key_monitor_schema
|
from lib.key_monitor.key_monitor_schema_lib import ensure_key_monitor_schema
|
||||||
from trigger_entry_key_monitor_lib import (
|
from lib.key_monitor.trigger_entry_key_monitor_lib import (
|
||||||
BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE,
|
BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE,
|
||||||
CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE,
|
CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE,
|
||||||
TRIGGER_ENTRY_CLOSE_EXCHANGE_FAILED,
|
TRIGGER_ENTRY_CLOSE_EXCHANGE_FAILED,
|
||||||
@@ -139,7 +140,7 @@ from trigger_entry_key_monitor_lib import (
|
|||||||
validate_trigger_entry_geometry,
|
validate_trigger_entry_geometry,
|
||||||
validate_trigger_entry_rr,
|
validate_trigger_entry_rr,
|
||||||
)
|
)
|
||||||
from position_sizing_lib import (
|
from lib.trade.position_sizing_lib import (
|
||||||
OPEN_SOURCE_KEY_AUTO,
|
OPEN_SOURCE_KEY_AUTO,
|
||||||
OPEN_SOURCE_KEY_TRIGGER,
|
OPEN_SOURCE_KEY_TRIGGER,
|
||||||
OPEN_SOURCE_MANUAL,
|
OPEN_SOURCE_MANUAL,
|
||||||
@@ -155,12 +156,12 @@ from position_sizing_lib import (
|
|||||||
mode_label_zh,
|
mode_label_zh,
|
||||||
risk_percent_for_storage,
|
risk_percent_for_storage,
|
||||||
)
|
)
|
||||||
from key_monitor_full_margin_lib import (
|
from lib.key_monitor.key_monitor_full_margin_lib import (
|
||||||
monitor_type_disallowed_in_full_margin,
|
monitor_type_disallowed_in_full_margin,
|
||||||
purge_disallowed_key_monitors,
|
purge_disallowed_key_monitors,
|
||||||
)
|
)
|
||||||
from auto_transfer_daily_lib import run_auto_transfer_once_per_day
|
from lib.common.auto_transfer_daily_lib import run_auto_transfer_once_per_day
|
||||||
from key_monitor_lib import (
|
from lib.key_monitor.key_monitor_lib import (
|
||||||
KEY_DIRECTION_WATCH,
|
KEY_DIRECTION_WATCH,
|
||||||
KEY_MONITOR_ALERT_ONLY_TYPES,
|
KEY_MONITOR_ALERT_ONLY_TYPES,
|
||||||
KEY_MONITOR_AUTO_TYPES,
|
KEY_MONITOR_AUTO_TYPES,
|
||||||
@@ -180,15 +181,15 @@ from key_monitor_lib import (
|
|||||||
rs_break_from_direction,
|
rs_break_from_direction,
|
||||||
run_rs_level_alert_tick,
|
run_rs_level_alert_tick,
|
||||||
)
|
)
|
||||||
from order_monitor_display_lib import (
|
from lib.trade.order_monitor_display_lib import (
|
||||||
apply_order_price_display_fields,
|
apply_order_price_display_fields,
|
||||||
enrich_order_display_fields,
|
enrich_order_display_fields,
|
||||||
order_monitor_tpsl_needs_sync,
|
order_monitor_tpsl_needs_sync,
|
||||||
)
|
)
|
||||||
from wechat_notify_lib import build_wechat_rs_level_message, send_wechat_webhook
|
from lib.common.wechat_notify_lib import build_wechat_rs_level_message, send_wechat_webhook
|
||||||
from hub_auth import request_allowed as hub_request_allowed
|
from lib.hub.hub_auth import request_allowed as hub_request_allowed
|
||||||
from hub_volume_rank_lib import resolve_daily_volume_rank
|
from lib.hub.hub_volume_rank_lib import resolve_daily_volume_rank
|
||||||
from history_window_lib import (
|
from lib.common.history_window_lib import (
|
||||||
PRESET_CUSTOM,
|
PRESET_CUSTOM,
|
||||||
PRESET_UTC_LAST24H,
|
PRESET_UTC_LAST24H,
|
||||||
PRESET_UTC_LAST7D,
|
PRESET_UTC_LAST7D,
|
||||||
@@ -201,8 +202,8 @@ from history_window_lib import (
|
|||||||
utc_window_to_bj_sql_strings,
|
utc_window_to_bj_sql_strings,
|
||||||
utc_window_to_utc_sql_strings,
|
utc_window_to_utc_sql_strings,
|
||||||
)
|
)
|
||||||
from trade_result_lib import count_winning_trades, normalize_result_with_pnl
|
from lib.trade.trade_result_lib import count_winning_trades, normalize_result_with_pnl
|
||||||
from trade_exchange_stats_lib import (
|
from lib.trade.trade_exchange_stats_lib import (
|
||||||
attach_exchange_stats_to_trade,
|
attach_exchange_stats_to_trade,
|
||||||
filter_position_lifecycle_fills,
|
filter_position_lifecycle_fills,
|
||||||
sum_binance_commission_income,
|
sum_binance_commission_income,
|
||||||
@@ -353,7 +354,7 @@ ORDER_CHART_ENABLED = os.getenv("ORDER_CHART_ENABLED", "true").lower() == "true"
|
|||||||
ORDER_CHART_TFS = [x.strip() for x in (os.getenv("ORDER_CHART_TFS", "4h,1h,15m,5m") or "").split(",") if x.strip()]
|
ORDER_CHART_TFS = [x.strip() for x in (os.getenv("ORDER_CHART_TFS", "4h,1h,15m,5m") or "").split(",") if x.strip()]
|
||||||
ORDER_CHART_LIMIT = int(os.getenv("ORDER_CHART_LIMIT", "100"))
|
ORDER_CHART_LIMIT = int(os.getenv("ORDER_CHART_LIMIT", "100"))
|
||||||
ORDER_CHART_DIR = resolve_path(os.getenv("ORDER_CHART_DIR", "static/images/order_charts"))
|
ORDER_CHART_DIR = resolve_path(os.getenv("ORDER_CHART_DIR", "static/images/order_charts"))
|
||||||
from daily_open_limit_lib import (
|
from lib.trade.daily_open_limit_lib import (
|
||||||
build_daily_open_alert_prompt,
|
build_daily_open_alert_prompt,
|
||||||
can_trade_new_open,
|
can_trade_new_open,
|
||||||
check_daily_open_hard_limit,
|
check_daily_open_hard_limit,
|
||||||
@@ -1520,10 +1521,10 @@ def init_db():
|
|||||||
close_reason TEXT, closed_at TEXT)"""
|
close_reason TEXT, closed_at TEXT)"""
|
||||||
)
|
)
|
||||||
|
|
||||||
from strategy_db import init_strategy_tables
|
from lib.strategy.strategy_db import init_strategy_tables
|
||||||
|
|
||||||
init_strategy_tables(conn)
|
init_strategy_tables(conn)
|
||||||
from account_risk_lib import ensure_account_risk_schema
|
from lib.trade.account_risk_lib import ensure_account_risk_schema
|
||||||
|
|
||||||
ensure_account_risk_schema(conn)
|
ensure_account_risk_schema(conn)
|
||||||
backfill_missing_key_signal_types(conn, monitor_type=ORDER_MONITOR_TYPE_KEY_AUTO)
|
backfill_missing_key_signal_types(conn, monitor_type=ORDER_MONITOR_TYPE_KEY_AUTO)
|
||||||
@@ -1560,7 +1561,7 @@ def get_db():
|
|||||||
|
|
||||||
|
|
||||||
def hub_account_risk_status(conn):
|
def hub_account_risk_status(conn):
|
||||||
from account_risk_lib import (
|
from lib.trade.account_risk_lib import (
|
||||||
apply_position_limit_risk,
|
apply_position_limit_risk,
|
||||||
compute_account_risk_status,
|
compute_account_risk_status,
|
||||||
enrich_risk_status_countdown,
|
enrich_risk_status_countdown,
|
||||||
@@ -1576,7 +1577,7 @@ def hub_account_risk_status(conn):
|
|||||||
fmt_local_ms=ms_to_app_local_str,
|
fmt_local_ms=ms_to_app_local_str,
|
||||||
)
|
)
|
||||||
st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR)
|
st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR)
|
||||||
from strategy_trade_labels import count_position_limit_active_monitors
|
from lib.strategy.strategy_trade_labels import count_position_limit_active_monitors
|
||||||
|
|
||||||
return apply_position_limit_risk(
|
return apply_position_limit_risk(
|
||||||
st,
|
st,
|
||||||
@@ -1593,7 +1594,7 @@ def hub_user_initiated_close(
|
|||||||
trade_record_id=None,
|
trade_record_id=None,
|
||||||
closed_at_ms=None,
|
closed_at_ms=None,
|
||||||
):
|
):
|
||||||
from account_risk_lib import CLOSE_SOURCE_USER_HUB, on_user_initiated_close
|
from lib.trade.account_risk_lib import CLOSE_SOURCE_USER_HUB, on_user_initiated_close
|
||||||
|
|
||||||
src = (source or "").strip() or CLOSE_SOURCE_USER_HUB
|
src = (source or "").strip() or CLOSE_SOURCE_USER_HUB
|
||||||
on_user_initiated_close(
|
on_user_initiated_close(
|
||||||
@@ -2120,7 +2121,7 @@ def get_effective_trade_field(row, reviewed_key, base_key, default=None):
|
|||||||
|
|
||||||
def to_effective_trade_dict(row):
|
def to_effective_trade_dict(row):
|
||||||
item = row_to_dict(row)
|
item = row_to_dict(row)
|
||||||
from order_monitor_display_lib import snapshot_stop_loss
|
from lib.trade.order_monitor_display_lib import snapshot_stop_loss
|
||||||
|
|
||||||
open_stop = snapshot_stop_loss(item.get("initial_stop_loss"), item.get("stop_loss"))
|
open_stop = snapshot_stop_loss(item.get("initial_stop_loss"), item.get("stop_loss"))
|
||||||
item["display_open_stop_loss"] = open_stop
|
item["display_open_stop_loss"] = open_stop
|
||||||
@@ -2661,7 +2662,7 @@ def insert_trade_record(
|
|||||||
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
|
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
|
||||||
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
||||||
kst = key_signal_type_for_trade_record(key_signal_type, KEY_MONITOR_AUTO_TYPES)
|
kst = key_signal_type_for_trade_record(key_signal_type, KEY_MONITOR_AUTO_TYPES)
|
||||||
from order_monitor_display_lib import snapshot_stop_loss
|
from lib.trade.order_monitor_display_lib import snapshot_stop_loss
|
||||||
|
|
||||||
snap_sl = snapshot_stop_loss(initial_stop_loss, stop_loss)
|
snap_sl = snapshot_stop_loss(initial_stop_loss, stop_loss)
|
||||||
er = (
|
er = (
|
||||||
@@ -3193,7 +3194,7 @@ def resolve_capital_base_for_key_open(conn, trading_day, live_capital):
|
|||||||
|
|
||||||
def precheck_risk(conn, symbol, direction):
|
def precheck_risk(conn, symbol, direction):
|
||||||
now = app_now()
|
now = app_now()
|
||||||
from account_risk_lib import account_risk_blocks_trading
|
from lib.trade.account_risk_lib import account_risk_blocks_trading
|
||||||
|
|
||||||
ok_risk, risk_reason = account_risk_blocks_trading(
|
ok_risk, risk_reason = account_risk_blocks_trading(
|
||||||
conn,
|
conn,
|
||||||
@@ -3205,7 +3206,7 @@ def precheck_risk(conn, symbol, direction):
|
|||||||
return False, risk_reason
|
return False, risk_reason
|
||||||
if not trading_day_reset_allows_new_open(now):
|
if not trading_day_reset_allows_new_open(now):
|
||||||
return False, f"北京时间 {TRADING_DAY_RESET_HOUR}:00 前不允许持仓"
|
return False, f"北京时间 {TRADING_DAY_RESET_HOUR}:00 前不允许持仓"
|
||||||
from account_risk_lib import position_limit_reached
|
from lib.trade.account_risk_lib import position_limit_reached
|
||||||
|
|
||||||
reached, active_count, mx = position_limit_reached(conn, max_active_positions=MAX_ACTIVE_POSITIONS)
|
reached, active_count, mx = position_limit_reached(conn, max_active_positions=MAX_ACTIVE_POSITIONS)
|
||||||
if reached:
|
if reached:
|
||||||
@@ -3898,7 +3899,7 @@ def list_orphan_live_positions(conn):
|
|||||||
ex = normalize_exchange_symbol(r["exchange_symbol"] or r["symbol"])
|
ex = normalize_exchange_symbol(r["exchange_symbol"] or r["symbol"])
|
||||||
active_keys.add((ex, (r["direction"] or "long").strip().lower()))
|
active_keys.add((ex, (r["direction"] or "long").strip().lower()))
|
||||||
|
|
||||||
from hub_position_metrics import parse_position_entry_price
|
from lib.hub.hub_position_metrics import parse_position_entry_price
|
||||||
|
|
||||||
orphans = []
|
orphans = []
|
||||||
for lp in live_rows:
|
for lp in live_rows:
|
||||||
@@ -4097,7 +4098,7 @@ def parse_ccxt_position_metrics(position, order_leverage=None):
|
|||||||
cs = float(get_contract_size(sym)) if sym else 1.0
|
cs = float(get_contract_size(sym)) if sym else 1.0
|
||||||
except Exception:
|
except Exception:
|
||||||
cs = 1.0
|
cs = 1.0
|
||||||
from hub_position_metrics import enrich_ccxt_position_metrics_out
|
from lib.hub.hub_position_metrics import enrich_ccxt_position_metrics_out
|
||||||
|
|
||||||
enrich_ccxt_position_metrics_out(
|
enrich_ccxt_position_metrics_out(
|
||||||
p, out, contract_size=cs, funds_decimals=FUNDS_DECIMALS
|
p, out, contract_size=cs, funds_decimals=FUNDS_DECIMALS
|
||||||
@@ -6807,14 +6808,14 @@ def background_task():
|
|||||||
check_trigger_entry_key_monitors()
|
check_trigger_entry_key_monitors()
|
||||||
_roll_cfg = app.extensions.get("strategy_roll_cfg")
|
_roll_cfg = app.extensions.get("strategy_roll_cfg")
|
||||||
if _roll_cfg:
|
if _roll_cfg:
|
||||||
from strategy_roll_monitor_lib import check_roll_monitors
|
from lib.strategy.strategy_roll_monitor_lib import check_roll_monitors
|
||||||
|
|
||||||
check_roll_monitors(_roll_cfg)
|
check_roll_monitors(_roll_cfg)
|
||||||
check_key_monitors()
|
check_key_monitors()
|
||||||
check_order_monitors()
|
check_order_monitors()
|
||||||
cfg = app.extensions.get("strategy_trend_cfg")
|
cfg = app.extensions.get("strategy_trend_cfg")
|
||||||
if cfg:
|
if cfg:
|
||||||
from strategy_trend_register import check_trend_pullback_plans
|
from lib.strategy.strategy_trend_register import check_trend_pullback_plans
|
||||||
|
|
||||||
check_trend_pullback_plans(cfg)
|
check_trend_pullback_plans(cfg)
|
||||||
except:
|
except:
|
||||||
@@ -7006,7 +7007,7 @@ def render_main_page(page="trade", embed_mode=None):
|
|||||||
conn = get_db()
|
conn = get_db()
|
||||||
session_row = ensure_session(conn, trading_day)
|
session_row = ensure_session(conn, trading_day)
|
||||||
local_current_capital = float(session_row["current_capital"])
|
local_current_capital = float(session_row["current_capital"])
|
||||||
from instance_embed_context_lib import (
|
from lib.instance.instance_embed_context_lib import (
|
||||||
embed_render_plan,
|
embed_render_plan,
|
||||||
minimal_stats_bundle,
|
minimal_stats_bundle,
|
||||||
trade_records_summary,
|
trade_records_summary,
|
||||||
@@ -7070,7 +7071,7 @@ def render_main_page(page="trade", embed_mode=None):
|
|||||||
records = []
|
records = []
|
||||||
total = miss_count = rate = occupied_miss_total = 0
|
total = miss_count = rate = occupied_miss_total = 0
|
||||||
active_count = len(order_list)
|
active_count = len(order_list)
|
||||||
from strategy_trade_labels import count_position_limit_active_monitors
|
from lib.strategy.strategy_trade_labels import count_position_limit_active_monitors
|
||||||
|
|
||||||
position_limit_count = count_position_limit_active_monitors(conn)
|
position_limit_count = count_position_limit_active_monitors(conn)
|
||||||
opens_today = count_opens_for_trading_day(conn, trading_day)
|
opens_today = count_opens_for_trading_day(conn, trading_day)
|
||||||
@@ -7101,7 +7102,7 @@ def render_main_page(page="trade", embed_mode=None):
|
|||||||
)
|
)
|
||||||
strategy_extra = {}
|
strategy_extra = {}
|
||||||
if plan.strategy:
|
if plan.strategy:
|
||||||
from strategy_ui import strategy_render_extras
|
from lib.strategy.strategy_ui import strategy_render_extras
|
||||||
|
|
||||||
strategy_extra = strategy_render_extras(
|
strategy_extra = strategy_render_extras(
|
||||||
conn,
|
conn,
|
||||||
@@ -7114,7 +7115,7 @@ def render_main_page(page="trade", embed_mode=None):
|
|||||||
if plan.orphan_live and not order_list and exchange_private_api_configured():
|
if plan.orphan_live and not order_list and exchange_private_api_configured():
|
||||||
orphan_live_positions = list_orphan_live_positions(conn)
|
orphan_live_positions = list_orphan_live_positions(conn)
|
||||||
conn.close()
|
conn.close()
|
||||||
from instance_embed_lib import embed_context_extras
|
from lib.instance.instance_embed_lib import embed_context_extras
|
||||||
|
|
||||||
template_ctx = dict(
|
template_ctx = dict(
|
||||||
page=page,
|
page=page,
|
||||||
@@ -7236,7 +7237,7 @@ def api_account_snapshot():
|
|||||||
funding_usdt = round(funding_capital, FUNDS_DECIMALS) if funding_capital is not None else None
|
funding_usdt = round(funding_capital, FUNDS_DECIMALS) if funding_capital is not None else None
|
||||||
current_capital = round(trading_capital, FUNDS_DECIMALS) if trading_capital is not None else round(local_current_capital, FUNDS_DECIMALS)
|
current_capital = round(trading_capital, FUNDS_DECIMALS) if trading_capital is not None else round(local_current_capital, FUNDS_DECIMALS)
|
||||||
recommended_capital = get_recommended_capital(current_capital)
|
recommended_capital = get_recommended_capital(current_capital)
|
||||||
from strategy_trade_labels import count_position_limit_active_monitors
|
from lib.strategy.strategy_trade_labels import count_position_limit_active_monitors
|
||||||
|
|
||||||
position_limit_count = count_position_limit_active_monitors(conn)
|
position_limit_count = count_position_limit_active_monitors(conn)
|
||||||
opens_today = count_opens_for_trading_day(conn, trading_day)
|
opens_today = count_opens_for_trading_day(conn, trading_day)
|
||||||
@@ -7523,7 +7524,7 @@ def api_price_snapshot():
|
|||||||
pass
|
pass
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
from hub_position_metrics import build_position_marks_list
|
from lib.hub.hub_position_metrics import build_position_marks_list
|
||||||
|
|
||||||
position_marks = build_position_marks_list(
|
position_marks = build_position_marks_list(
|
||||||
all_swap_positions,
|
all_swap_positions,
|
||||||
@@ -7782,7 +7783,7 @@ def api_order_kline():
|
|||||||
"volume": float(bar[5]),
|
"volume": float(bar[5]),
|
||||||
})
|
})
|
||||||
|
|
||||||
from focus_chart_lib import (
|
from lib.instance.focus_chart_lib import (
|
||||||
build_order_kline_order_payload,
|
build_order_kline_order_payload,
|
||||||
load_swap_positions_for_order_kline,
|
load_swap_positions_for_order_kline,
|
||||||
metrics_for_order_item,
|
metrics_for_order_item,
|
||||||
@@ -7810,7 +7811,7 @@ def api_order_kline():
|
|||||||
ex_metrics=ex_metrics,
|
ex_metrics=ex_metrics,
|
||||||
)
|
)
|
||||||
|
|
||||||
from focus_chart_lib import kline_api_price_fields
|
from lib.instance.focus_chart_lib import kline_api_price_fields
|
||||||
|
|
||||||
price_fields = kline_api_price_fields(
|
price_fields = kline_api_price_fields(
|
||||||
exchange,
|
exchange,
|
||||||
@@ -7927,7 +7928,7 @@ def api_key_kline():
|
|||||||
"lower_pct": lower_pct,
|
"lower_pct": lower_pct,
|
||||||
}
|
}
|
||||||
|
|
||||||
from focus_chart_lib import enrich_key_kline_response
|
from lib.instance.focus_chart_lib import enrich_key_kline_response
|
||||||
|
|
||||||
price_display, key_info = enrich_key_kline_response(
|
price_display, key_info = enrich_key_kline_response(
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
@@ -7936,7 +7937,7 @@ def api_key_kline():
|
|||||||
format_price_fn=format_price_for_symbol,
|
format_price_fn=format_price_for_symbol,
|
||||||
)
|
)
|
||||||
|
|
||||||
from focus_chart_lib import kline_api_price_fields
|
from lib.instance.focus_chart_lib import kline_api_price_fields
|
||||||
|
|
||||||
price_fields = kline_api_price_fields(
|
price_fields = kline_api_price_fields(
|
||||||
exchange,
|
exchange,
|
||||||
@@ -8859,7 +8860,7 @@ def del_order(id):
|
|||||||
opened_at=opened_at,
|
opened_at=opened_at,
|
||||||
closed_at=closed_at,
|
closed_at=closed_at,
|
||||||
)
|
)
|
||||||
from account_risk_lib import CLOSE_SOURCE_USER_INSTANCE, insert_trade_record_id, on_user_initiated_close
|
from lib.trade.account_risk_lib import CLOSE_SOURCE_USER_INSTANCE, insert_trade_record_id, on_user_initiated_close
|
||||||
|
|
||||||
on_user_initiated_close(
|
on_user_initiated_close(
|
||||||
conn,
|
conn,
|
||||||
@@ -8873,7 +8874,7 @@ def del_order(id):
|
|||||||
try:
|
try:
|
||||||
_rcfg = app.extensions.get("strategy_roll_cfg")
|
_rcfg = app.extensions.get("strategy_roll_cfg")
|
||||||
if isinstance(_rcfg, dict):
|
if isinstance(_rcfg, dict):
|
||||||
from strategy_register import roll_sync_after_external_close
|
from lib.strategy.strategy_register import roll_sync_after_external_close
|
||||||
|
|
||||||
roll_sync_after_external_close(_rcfg, conn, row["symbol"], row["direction"])
|
roll_sync_after_external_close(_rcfg, conn, row["symbol"], row["direction"])
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -8934,7 +8935,7 @@ def del_order(id):
|
|||||||
opened_at=opened_at,
|
opened_at=opened_at,
|
||||||
closed_at=closed_at,
|
closed_at=closed_at,
|
||||||
)
|
)
|
||||||
from account_risk_lib import CLOSE_SOURCE_USER_INSTANCE, insert_trade_record_id, on_user_initiated_close
|
from lib.trade.account_risk_lib import CLOSE_SOURCE_USER_INSTANCE, insert_trade_record_id, on_user_initiated_close
|
||||||
|
|
||||||
on_user_initiated_close(
|
on_user_initiated_close(
|
||||||
conn,
|
conn,
|
||||||
@@ -8948,7 +8949,7 @@ def del_order(id):
|
|||||||
try:
|
try:
|
||||||
_rcfg = app.extensions.get("strategy_roll_cfg")
|
_rcfg = app.extensions.get("strategy_roll_cfg")
|
||||||
if isinstance(_rcfg, dict):
|
if isinstance(_rcfg, dict):
|
||||||
from strategy_register import roll_sync_after_external_close
|
from lib.strategy.strategy_register import roll_sync_after_external_close
|
||||||
|
|
||||||
roll_sync_after_external_close(_rcfg, conn, row["symbol"], row["direction"])
|
roll_sync_after_external_close(_rcfg, conn, row["symbol"], row["direction"])
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -9109,7 +9110,7 @@ def add_journal():
|
|||||||
d.get("post_breakeven_stare"), d.get("new_trade_while_occupied"), d.get("note"), image_filename
|
d.get("post_breakeven_stare"), d.get("new_trade_while_occupied"), d.get("note"), image_filename
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
from account_risk_lib import on_journal_saved
|
from lib.trade.account_risk_lib import on_journal_saved
|
||||||
|
|
||||||
on_journal_saved(
|
on_journal_saved(
|
||||||
conn,
|
conn,
|
||||||
@@ -9197,7 +9198,7 @@ def api_reviews():
|
|||||||
return jsonify([row_to_dict(r) for r in rows])
|
return jsonify([row_to_dict(r) for r in rows])
|
||||||
|
|
||||||
|
|
||||||
_REPO_STATIC_DIR = os.path.join(os.path.dirname(BASE_DIR), "static")
|
_REPO_STATIC_DIR = common_static_dir(os.path.dirname(BASE_DIR))
|
||||||
_AI_REVIEW_RENDER_JS = os.path.join(_REPO_STATIC_DIR, "ai_review_render.js")
|
_AI_REVIEW_RENDER_JS = os.path.join(_REPO_STATIC_DIR, "ai_review_render.js")
|
||||||
_FORM_SUBMIT_GUARD_JS = os.path.join(_REPO_STATIC_DIR, "form_submit_guard.js")
|
_FORM_SUBMIT_GUARD_JS = os.path.join(_REPO_STATIC_DIR, "form_submit_guard.js")
|
||||||
_MANUAL_ORDER_RR_PREVIEW_JS = os.path.join(_REPO_STATIC_DIR, "manual_order_rr_preview.js")
|
_MANUAL_ORDER_RR_PREVIEW_JS = os.path.join(_REPO_STATIC_DIR, "manual_order_rr_preview.js")
|
||||||
@@ -9422,7 +9423,7 @@ def api_trade_record_review_update():
|
|||||||
tuple(base_params + [rec_id]),
|
tuple(base_params + [rec_id]),
|
||||||
)
|
)
|
||||||
if reviewed_result == "手动平仓" and reviewed_miss_reason:
|
if reviewed_result == "手动平仓" and reviewed_miss_reason:
|
||||||
from account_risk_lib import apply_manual_close_journal_cooloff
|
from lib.trade.account_risk_lib import apply_manual_close_journal_cooloff
|
||||||
|
|
||||||
apply_manual_close_journal_cooloff(
|
apply_manual_close_journal_cooloff(
|
||||||
conn,
|
conn,
|
||||||
@@ -9571,7 +9572,7 @@ def _hub_account_bundle():
|
|||||||
|
|
||||||
|
|
||||||
def _hub_fetch_market(base=""):
|
def _hub_fetch_market(base=""):
|
||||||
from hub_market_info_lib import fetch_usdt_swap_market_info
|
from lib.hub.hub_market_info_lib import fetch_usdt_swap_market_info
|
||||||
|
|
||||||
return fetch_usdt_swap_market_info(
|
return fetch_usdt_swap_market_info(
|
||||||
base_or_symbol=base,
|
base_or_symbol=base,
|
||||||
@@ -9584,7 +9585,7 @@ def _hub_fetch_market(base=""):
|
|||||||
|
|
||||||
|
|
||||||
def _hub_fetch_ohlcv(symbol, timeframe, since_ms=None, limit=500):
|
def _hub_fetch_ohlcv(symbol, timeframe, since_ms=None, limit=500):
|
||||||
from hub_ohlcv_lib import fetch_ohlcv_for_hub
|
from lib.hub.hub_ohlcv_lib import fetch_ohlcv_for_hub
|
||||||
|
|
||||||
return fetch_ohlcv_for_hub(
|
return fetch_ohlcv_for_hub(
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
@@ -9600,7 +9601,7 @@ def _hub_fetch_ohlcv(symbol, timeframe, since_ms=None, limit=500):
|
|||||||
|
|
||||||
|
|
||||||
def _hub_fetch_volume_rank(top_n=20):
|
def _hub_fetch_volume_rank(top_n=20):
|
||||||
from hub_volume_rank_lib import fetch_usdt_swap_volume_rank
|
from lib.hub.hub_volume_rank_lib import fetch_usdt_swap_volume_rank
|
||||||
|
|
||||||
return fetch_usdt_swap_volume_rank(
|
return fetch_usdt_swap_volume_rank(
|
||||||
exchange=exchange,
|
exchange=exchange,
|
||||||
@@ -9617,7 +9618,7 @@ try:
|
|||||||
_repo_root = Path(__file__).resolve().parent.parent
|
_repo_root = Path(__file__).resolve().parent.parent
|
||||||
if str(_repo_root) not in sys.path:
|
if str(_repo_root) not in sys.path:
|
||||||
sys.path.insert(0, str(_repo_root))
|
sys.path.insert(0, str(_repo_root))
|
||||||
from hub_bridge import install_on_app
|
from lib.hub.hub_bridge import install_on_app
|
||||||
|
|
||||||
install_on_app(
|
install_on_app(
|
||||||
app,
|
app,
|
||||||
@@ -9660,8 +9661,8 @@ def strategy_roll_page():
|
|||||||
return redirect("/strategy")
|
return redirect("/strategy")
|
||||||
|
|
||||||
|
|
||||||
from strategy_register import install_strategy_trading
|
from lib.strategy.strategy_register import install_strategy_trading
|
||||||
from strategy_trend_register import install_strategy_trend
|
from lib.strategy.strategy_trend_register import install_strategy_trend
|
||||||
|
|
||||||
install_strategy_trading(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
install_strategy_trading(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
||||||
install_strategy_trend(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
install_strategy_trend(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
||||||
|
|||||||
+65
-64
@@ -34,14 +34,15 @@ import sys
|
|||||||
|
|
||||||
if _REPO_ROOT not in sys.path:
|
if _REPO_ROOT not in sys.path:
|
||||||
sys.path.insert(0, _REPO_ROOT)
|
sys.path.insert(0, _REPO_ROOT)
|
||||||
from ai_client import ai_generate, ai_review, ai_short_advice
|
from lib.paths import common_static_dir
|
||||||
from ai_review_lib import (
|
from lib.ai.ai_client import ai_generate, ai_review, ai_short_advice
|
||||||
|
from lib.ai.ai_review_lib import (
|
||||||
build_journal_ai_chart_path,
|
build_journal_ai_chart_path,
|
||||||
collect_images_for_ai_review,
|
collect_images_for_ai_review,
|
||||||
journal_row_lines_for_ai,
|
journal_row_lines_for_ai,
|
||||||
)
|
)
|
||||||
from form_submit_lib import check_duplicate_submit, submit_scope_add_key, submit_scope_add_order
|
from lib.common.form_submit_lib import check_duplicate_submit, submit_scope_add_key, submit_scope_add_order
|
||||||
from fib_key_monitor_lib import (
|
from lib.key_monitor.fib_key_monitor_lib import (
|
||||||
FIB_KEY_MONITOR_TYPES,
|
FIB_KEY_MONITOR_TYPES,
|
||||||
KEY_ENTRY_REASON_BY_SIGNAL,
|
KEY_ENTRY_REASON_BY_SIGNAL,
|
||||||
backfill_missing_key_signal_types,
|
backfill_missing_key_signal_types,
|
||||||
@@ -53,7 +54,7 @@ from fib_key_monitor_lib import (
|
|||||||
key_signal_type_for_trade_record,
|
key_signal_type_for_trade_record,
|
||||||
stored_key_signal_type,
|
stored_key_signal_type,
|
||||||
)
|
)
|
||||||
from false_breakout_key_monitor_lib import (
|
from lib.key_monitor.false_breakout_key_monitor_lib import (
|
||||||
FALSE_BREAKOUT_MONITOR_TYPE,
|
FALSE_BREAKOUT_MONITOR_TYPE,
|
||||||
FALSE_BREAKOUT_VALIDITY_HOURS,
|
FALSE_BREAKOUT_VALIDITY_HOURS,
|
||||||
calc_false_breakout_plan,
|
calc_false_breakout_plan,
|
||||||
@@ -66,7 +67,7 @@ from false_breakout_key_monitor_lib import (
|
|||||||
normalize_false_breakout_symbol,
|
normalize_false_breakout_symbol,
|
||||||
storage_bounds_from_key_price,
|
storage_bounds_from_key_price,
|
||||||
)
|
)
|
||||||
from strategy_trade_labels import (
|
from lib.strategy.strategy_trade_labels import (
|
||||||
STRATEGY_ENTRY_REASON_OPTIONS,
|
STRATEGY_ENTRY_REASON_OPTIONS,
|
||||||
apply_order_monitor_source_labels,
|
apply_order_monitor_source_labels,
|
||||||
entry_reason_for_monitor_type,
|
entry_reason_for_monitor_type,
|
||||||
@@ -75,7 +76,7 @@ from strategy_trade_labels import (
|
|||||||
trade_record_monitor_type as resolve_trade_record_monitor_type,
|
trade_record_monitor_type as resolve_trade_record_monitor_type,
|
||||||
trend_plan_id_from_monitor_row,
|
trend_plan_id_from_monitor_row,
|
||||||
)
|
)
|
||||||
from journal_chart_lib import (
|
from lib.instance.journal_chart_lib import (
|
||||||
JOURNAL_CHART_DEFAULT_LIMIT,
|
JOURNAL_CHART_DEFAULT_LIMIT,
|
||||||
JOURNAL_CHART_DEFAULT_TF1,
|
JOURNAL_CHART_DEFAULT_TF1,
|
||||||
JOURNAL_CHART_DEFAULT_TF2,
|
JOURNAL_CHART_DEFAULT_TF2,
|
||||||
@@ -91,7 +92,7 @@ from journal_chart_lib import (
|
|||||||
trade_review_fetch_window,
|
trade_review_fetch_window,
|
||||||
trim_rows_for_trade_review,
|
trim_rows_for_trade_review,
|
||||||
)
|
)
|
||||||
from key_sl_tp_lib import (
|
from lib.key_monitor.key_sl_tp_lib import (
|
||||||
breakeven_enabled_from_row,
|
breakeven_enabled_from_row,
|
||||||
normalize_sl_tp_mode,
|
normalize_sl_tp_mode,
|
||||||
parse_breakeven_enabled_form,
|
parse_breakeven_enabled_form,
|
||||||
@@ -100,7 +101,7 @@ from key_sl_tp_lib import (
|
|||||||
sl_tp_mode_label,
|
sl_tp_mode_label,
|
||||||
sl_tp_plan_summary_text,
|
sl_tp_plan_summary_text,
|
||||||
)
|
)
|
||||||
from time_close_lib import (
|
from lib.trade.time_close_lib import (
|
||||||
TIME_CLOSE_RESULT,
|
TIME_CLOSE_RESULT,
|
||||||
apply_time_close_to_payload,
|
apply_time_close_to_payload,
|
||||||
ensure_time_close_schema,
|
ensure_time_close_schema,
|
||||||
@@ -111,13 +112,13 @@ from time_close_lib import (
|
|||||||
time_close_label,
|
time_close_label,
|
||||||
time_close_settings_from_row,
|
time_close_settings_from_row,
|
||||||
)
|
)
|
||||||
from manual_sltp_lib import (
|
from lib.trade.manual_sltp_lib import (
|
||||||
normalize_open_sltp_mode,
|
normalize_open_sltp_mode,
|
||||||
resolve_entrust_sltp_prices,
|
resolve_entrust_sltp_prices,
|
||||||
resolve_open_sltp_prices,
|
resolve_open_sltp_prices,
|
||||||
)
|
)
|
||||||
from key_monitor_schema_lib import ensure_key_monitor_schema
|
from lib.key_monitor.key_monitor_schema_lib import ensure_key_monitor_schema
|
||||||
from trigger_entry_key_monitor_lib import (
|
from lib.key_monitor.trigger_entry_key_monitor_lib import (
|
||||||
BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE,
|
BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE,
|
||||||
CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE,
|
CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE,
|
||||||
TRIGGER_ENTRY_CLOSE_EXCHANGE_FAILED,
|
TRIGGER_ENTRY_CLOSE_EXCHANGE_FAILED,
|
||||||
@@ -140,7 +141,7 @@ from trigger_entry_key_monitor_lib import (
|
|||||||
validate_trigger_entry_geometry,
|
validate_trigger_entry_geometry,
|
||||||
validate_trigger_entry_rr,
|
validate_trigger_entry_rr,
|
||||||
)
|
)
|
||||||
from position_sizing_lib import (
|
from lib.trade.position_sizing_lib import (
|
||||||
OPEN_SOURCE_KEY_AUTO,
|
OPEN_SOURCE_KEY_AUTO,
|
||||||
OPEN_SOURCE_KEY_TRIGGER,
|
OPEN_SOURCE_KEY_TRIGGER,
|
||||||
OPEN_SOURCE_MANUAL,
|
OPEN_SOURCE_MANUAL,
|
||||||
@@ -154,12 +155,12 @@ from position_sizing_lib import (
|
|||||||
mode_label_zh,
|
mode_label_zh,
|
||||||
risk_percent_for_storage,
|
risk_percent_for_storage,
|
||||||
)
|
)
|
||||||
from key_monitor_full_margin_lib import (
|
from lib.key_monitor.key_monitor_full_margin_lib import (
|
||||||
monitor_type_disallowed_in_full_margin,
|
monitor_type_disallowed_in_full_margin,
|
||||||
purge_disallowed_key_monitors,
|
purge_disallowed_key_monitors,
|
||||||
)
|
)
|
||||||
from auto_transfer_daily_lib import run_auto_transfer_once_per_day
|
from lib.common.auto_transfer_daily_lib import run_auto_transfer_once_per_day
|
||||||
from key_monitor_lib import (
|
from lib.key_monitor.key_monitor_lib import (
|
||||||
KEY_DIRECTION_WATCH,
|
KEY_DIRECTION_WATCH,
|
||||||
KEY_MONITOR_ALERT_ONLY_TYPES,
|
KEY_MONITOR_ALERT_ONLY_TYPES,
|
||||||
KEY_MONITOR_AUTO_TYPES,
|
KEY_MONITOR_AUTO_TYPES,
|
||||||
@@ -179,16 +180,16 @@ from key_monitor_lib import (
|
|||||||
rs_break_from_direction,
|
rs_break_from_direction,
|
||||||
run_rs_level_alert_tick,
|
run_rs_level_alert_tick,
|
||||||
)
|
)
|
||||||
from order_monitor_display_lib import (
|
from lib.trade.order_monitor_display_lib import (
|
||||||
apply_order_price_display_fields,
|
apply_order_price_display_fields,
|
||||||
enrich_order_display_fields,
|
enrich_order_display_fields,
|
||||||
order_monitor_tpsl_needs_sync,
|
order_monitor_tpsl_needs_sync,
|
||||||
)
|
)
|
||||||
from wechat_notify_lib import build_wechat_rs_level_message, send_wechat_webhook
|
from lib.common.wechat_notify_lib import build_wechat_rs_level_message, send_wechat_webhook
|
||||||
from hub_auth import request_allowed as hub_request_allowed
|
from lib.hub.hub_auth import request_allowed as hub_request_allowed
|
||||||
from instance_nav_lib import request_is_hub_soft_nav
|
from lib.instance.instance_nav_lib import request_is_hub_soft_nav
|
||||||
from hub_volume_rank_lib import resolve_daily_volume_rank
|
from lib.hub.hub_volume_rank_lib import resolve_daily_volume_rank
|
||||||
from history_window_lib import (
|
from lib.common.history_window_lib import (
|
||||||
PRESET_CUSTOM,
|
PRESET_CUSTOM,
|
||||||
PRESET_UTC_LAST24H,
|
PRESET_UTC_LAST24H,
|
||||||
PRESET_UTC_LAST7D,
|
PRESET_UTC_LAST7D,
|
||||||
@@ -201,8 +202,8 @@ from history_window_lib import (
|
|||||||
utc_window_to_bj_sql_strings,
|
utc_window_to_bj_sql_strings,
|
||||||
utc_window_to_utc_sql_strings,
|
utc_window_to_utc_sql_strings,
|
||||||
)
|
)
|
||||||
from trade_result_lib import count_winning_trades, normalize_result_with_pnl
|
from lib.trade.trade_result_lib import count_winning_trades, normalize_result_with_pnl
|
||||||
from trade_exchange_stats_lib import attach_exchange_stats_to_trade, filter_position_lifecycle_fills
|
from lib.trade.trade_exchange_stats_lib import attach_exchange_stats_to_trade, filter_position_lifecycle_fills
|
||||||
|
|
||||||
|
|
||||||
def load_env_file(path):
|
def load_env_file(path):
|
||||||
@@ -343,7 +344,7 @@ ORDER_CHART_ENABLED = os.getenv("ORDER_CHART_ENABLED", "true").lower() == "true"
|
|||||||
ORDER_CHART_TFS = [x.strip() for x in (os.getenv("ORDER_CHART_TFS", "4h,1h,15m,5m") or "").split(",") if x.strip()]
|
ORDER_CHART_TFS = [x.strip() for x in (os.getenv("ORDER_CHART_TFS", "4h,1h,15m,5m") or "").split(",") if x.strip()]
|
||||||
ORDER_CHART_LIMIT = int(os.getenv("ORDER_CHART_LIMIT", "100"))
|
ORDER_CHART_LIMIT = int(os.getenv("ORDER_CHART_LIMIT", "100"))
|
||||||
ORDER_CHART_DIR = resolve_path(os.getenv("ORDER_CHART_DIR", "static/images/order_charts"))
|
ORDER_CHART_DIR = resolve_path(os.getenv("ORDER_CHART_DIR", "static/images/order_charts"))
|
||||||
from daily_open_limit_lib import (
|
from lib.trade.daily_open_limit_lib import (
|
||||||
build_daily_open_alert_prompt,
|
build_daily_open_alert_prompt,
|
||||||
can_trade_new_open,
|
can_trade_new_open,
|
||||||
check_daily_open_hard_limit,
|
check_daily_open_hard_limit,
|
||||||
@@ -1506,10 +1507,10 @@ def init_db():
|
|||||||
close_reason TEXT, closed_at TEXT)"""
|
close_reason TEXT, closed_at TEXT)"""
|
||||||
)
|
)
|
||||||
|
|
||||||
from strategy_db import init_strategy_tables
|
from lib.strategy.strategy_db import init_strategy_tables
|
||||||
|
|
||||||
init_strategy_tables(conn)
|
init_strategy_tables(conn)
|
||||||
from account_risk_lib import ensure_account_risk_schema
|
from lib.trade.account_risk_lib import ensure_account_risk_schema
|
||||||
|
|
||||||
ensure_account_risk_schema(conn)
|
ensure_account_risk_schema(conn)
|
||||||
backfill_missing_key_signal_types(conn, monitor_type=ORDER_MONITOR_TYPE_KEY_AUTO)
|
backfill_missing_key_signal_types(conn, monitor_type=ORDER_MONITOR_TYPE_KEY_AUTO)
|
||||||
@@ -1546,7 +1547,7 @@ def get_db():
|
|||||||
|
|
||||||
|
|
||||||
def hub_account_risk_status(conn):
|
def hub_account_risk_status(conn):
|
||||||
from account_risk_lib import (
|
from lib.trade.account_risk_lib import (
|
||||||
apply_position_limit_risk,
|
apply_position_limit_risk,
|
||||||
compute_account_risk_status,
|
compute_account_risk_status,
|
||||||
enrich_risk_status_countdown,
|
enrich_risk_status_countdown,
|
||||||
@@ -1562,7 +1563,7 @@ def hub_account_risk_status(conn):
|
|||||||
fmt_local_ms=ms_to_app_local_str,
|
fmt_local_ms=ms_to_app_local_str,
|
||||||
)
|
)
|
||||||
st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR)
|
st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR)
|
||||||
from strategy_trade_labels import count_position_limit_active_monitors
|
from lib.strategy.strategy_trade_labels import count_position_limit_active_monitors
|
||||||
|
|
||||||
return apply_position_limit_risk(
|
return apply_position_limit_risk(
|
||||||
st,
|
st,
|
||||||
@@ -1579,7 +1580,7 @@ def hub_user_initiated_close(
|
|||||||
trade_record_id=None,
|
trade_record_id=None,
|
||||||
closed_at_ms=None,
|
closed_at_ms=None,
|
||||||
):
|
):
|
||||||
from account_risk_lib import CLOSE_SOURCE_USER_HUB, on_user_initiated_close
|
from lib.trade.account_risk_lib import CLOSE_SOURCE_USER_HUB, on_user_initiated_close
|
||||||
|
|
||||||
src = (source or "").strip() or CLOSE_SOURCE_USER_HUB
|
src = (source or "").strip() or CLOSE_SOURCE_USER_HUB
|
||||||
on_user_initiated_close(
|
on_user_initiated_close(
|
||||||
@@ -2072,7 +2073,7 @@ def get_effective_trade_field(row, reviewed_key, base_key, default=None):
|
|||||||
|
|
||||||
def to_effective_trade_dict(row):
|
def to_effective_trade_dict(row):
|
||||||
item = row_to_dict(row)
|
item = row_to_dict(row)
|
||||||
from order_monitor_display_lib import snapshot_stop_loss
|
from lib.trade.order_monitor_display_lib import snapshot_stop_loss
|
||||||
|
|
||||||
open_stop = snapshot_stop_loss(item.get("initial_stop_loss"), item.get("stop_loss"))
|
open_stop = snapshot_stop_loss(item.get("initial_stop_loss"), item.get("stop_loss"))
|
||||||
item["display_open_stop_loss"] = open_stop
|
item["display_open_stop_loss"] = open_stop
|
||||||
@@ -2370,7 +2371,7 @@ def insert_trade_record(
|
|||||||
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
|
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
|
||||||
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
||||||
kst = key_signal_type_for_trade_record(key_signal_type, KEY_MONITOR_AUTO_TYPES)
|
kst = key_signal_type_for_trade_record(key_signal_type, KEY_MONITOR_AUTO_TYPES)
|
||||||
from order_monitor_display_lib import snapshot_stop_loss
|
from lib.trade.order_monitor_display_lib import snapshot_stop_loss
|
||||||
|
|
||||||
snap_sl = snapshot_stop_loss(initial_stop_loss, stop_loss)
|
snap_sl = snapshot_stop_loss(initial_stop_loss, stop_loss)
|
||||||
er = (
|
er = (
|
||||||
@@ -2761,7 +2762,7 @@ def get_exchange_capitals(force=False):
|
|||||||
|
|
||||||
|
|
||||||
def execute_transfer_usdt(amount, from_account, to_account):
|
def execute_transfer_usdt(amount, from_account, to_account):
|
||||||
from gate_transfer_lib import execute_transfer_usdt as _gate_execute_transfer_usdt
|
from lib.exchange.gate_transfer_lib import execute_transfer_usdt as _gate_execute_transfer_usdt
|
||||||
|
|
||||||
return _gate_execute_transfer_usdt(
|
return _gate_execute_transfer_usdt(
|
||||||
exchange,
|
exchange,
|
||||||
@@ -2794,7 +2795,7 @@ def get_account_usdt_total(account_type):
|
|||||||
|
|
||||||
|
|
||||||
def _auto_transfer_active_count(conn):
|
def _auto_transfer_active_count(conn):
|
||||||
from gate_transfer_lib import count_auto_transfer_blockers
|
from lib.exchange.gate_transfer_lib import count_auto_transfer_blockers
|
||||||
|
|
||||||
return count_auto_transfer_blockers(conn, count_order_monitors=get_active_position_count)
|
return count_auto_transfer_blockers(conn, count_order_monitors=get_active_position_count)
|
||||||
|
|
||||||
@@ -2878,7 +2879,7 @@ def resolve_capital_base_for_key_open(conn, trading_day, live_capital):
|
|||||||
|
|
||||||
def precheck_risk(conn, symbol, direction):
|
def precheck_risk(conn, symbol, direction):
|
||||||
now = app_now()
|
now = app_now()
|
||||||
from account_risk_lib import account_risk_blocks_trading
|
from lib.trade.account_risk_lib import account_risk_blocks_trading
|
||||||
|
|
||||||
ok_risk, risk_reason = account_risk_blocks_trading(
|
ok_risk, risk_reason = account_risk_blocks_trading(
|
||||||
conn,
|
conn,
|
||||||
@@ -2890,7 +2891,7 @@ def precheck_risk(conn, symbol, direction):
|
|||||||
return False, risk_reason
|
return False, risk_reason
|
||||||
if not trading_day_reset_allows_new_open(now):
|
if not trading_day_reset_allows_new_open(now):
|
||||||
return False, f"北京时间 {TRADING_DAY_RESET_HOUR}:00 前不允许持仓"
|
return False, f"北京时间 {TRADING_DAY_RESET_HOUR}:00 前不允许持仓"
|
||||||
from account_risk_lib import position_limit_reached
|
from lib.trade.account_risk_lib import position_limit_reached
|
||||||
|
|
||||||
reached, active_count, mx = position_limit_reached(conn, max_active_positions=MAX_ACTIVE_POSITIONS)
|
reached, active_count, mx = position_limit_reached(conn, max_active_positions=MAX_ACTIVE_POSITIONS)
|
||||||
if reached:
|
if reached:
|
||||||
@@ -3670,7 +3671,7 @@ def parse_ccxt_position_metrics(position, order_leverage=None):
|
|||||||
cs = float(get_contract_size(sym)) if sym else 1.0
|
cs = float(get_contract_size(sym)) if sym else 1.0
|
||||||
except Exception:
|
except Exception:
|
||||||
cs = 1.0
|
cs = 1.0
|
||||||
from hub_position_metrics import enrich_ccxt_position_metrics_out
|
from lib.hub.hub_position_metrics import enrich_ccxt_position_metrics_out
|
||||||
|
|
||||||
enrich_ccxt_position_metrics_out(p, out, contract_size=cs, funds_decimals=2)
|
enrich_ccxt_position_metrics_out(p, out, contract_size=cs, funds_decimals=2)
|
||||||
return out or None
|
return out or None
|
||||||
@@ -3854,7 +3855,7 @@ def fetch_latest_closing_fill(exchange_symbol, direction, opened_at_str, opened_
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
from gate_position_history_lib import pick_gate_position_close
|
from lib.exchange.gate_position_history_lib import pick_gate_position_close
|
||||||
|
|
||||||
pos = pick_gate_position_close(
|
pos = pick_gate_position_close(
|
||||||
fetch_gate_positions_close_history(),
|
fetch_gate_positions_close_history(),
|
||||||
@@ -4114,7 +4115,7 @@ def reconcile_hub_external_close(conn, symbol, direction):
|
|||||||
"""中控市价全平后:立即同步匹配 order_monitor,并读 Gate 平仓历史。"""
|
"""中控市价全平后:立即同步匹配 order_monitor,并读 Gate 平仓历史。"""
|
||||||
if not exchange_private_api_configured():
|
if not exchange_private_api_configured():
|
||||||
return {"ok": False, "msg": "未配置 GATE_API_KEY / GATE_API_SECRET", "synced": 0}
|
return {"ok": False, "msg": "未配置 GATE_API_KEY / GATE_API_SECRET", "synced": 0}
|
||||||
from gate_position_history_lib import unified_symbol_for_match
|
from lib.exchange.gate_position_history_lib import unified_symbol_for_match
|
||||||
|
|
||||||
sym_u = unified_symbol_for_match(symbol)
|
sym_u = unified_symbol_for_match(symbol)
|
||||||
dir_l = (direction or "").strip().lower()
|
dir_l = (direction or "").strip().lower()
|
||||||
@@ -6513,14 +6514,14 @@ def background_task():
|
|||||||
check_trigger_entry_key_monitors()
|
check_trigger_entry_key_monitors()
|
||||||
_roll_cfg = app.extensions.get("strategy_roll_cfg")
|
_roll_cfg = app.extensions.get("strategy_roll_cfg")
|
||||||
if _roll_cfg:
|
if _roll_cfg:
|
||||||
from strategy_roll_monitor_lib import check_roll_monitors
|
from lib.strategy.strategy_roll_monitor_lib import check_roll_monitors
|
||||||
|
|
||||||
check_roll_monitors(_roll_cfg)
|
check_roll_monitors(_roll_cfg)
|
||||||
check_key_monitors()
|
check_key_monitors()
|
||||||
check_order_monitors()
|
check_order_monitors()
|
||||||
cfg = app.extensions.get("strategy_trend_cfg")
|
cfg = app.extensions.get("strategy_trend_cfg")
|
||||||
if cfg:
|
if cfg:
|
||||||
from strategy_trend_register import check_trend_pullback_plans
|
from lib.strategy.strategy_trend_register import check_trend_pullback_plans
|
||||||
|
|
||||||
check_trend_pullback_plans(cfg)
|
check_trend_pullback_plans(cfg)
|
||||||
except:
|
except:
|
||||||
@@ -6848,7 +6849,7 @@ def render_main_page(page="trade", embed_mode=None):
|
|||||||
conn = get_db()
|
conn = get_db()
|
||||||
session_row = ensure_session(conn, trading_day)
|
session_row = ensure_session(conn, trading_day)
|
||||||
local_current_capital = float(session_row["current_capital"])
|
local_current_capital = float(session_row["current_capital"])
|
||||||
from instance_embed_context_lib import (
|
from lib.instance.instance_embed_context_lib import (
|
||||||
embed_render_plan,
|
embed_render_plan,
|
||||||
minimal_stats_bundle,
|
minimal_stats_bundle,
|
||||||
trade_records_summary,
|
trade_records_summary,
|
||||||
@@ -6921,7 +6922,7 @@ def render_main_page(page="trade", embed_mode=None):
|
|||||||
records = []
|
records = []
|
||||||
total = miss_count = rate = occupied_miss_total = 0
|
total = miss_count = rate = occupied_miss_total = 0
|
||||||
active_count = len(order_list)
|
active_count = len(order_list)
|
||||||
from strategy_trade_labels import count_position_limit_active_monitors
|
from lib.strategy.strategy_trade_labels import count_position_limit_active_monitors
|
||||||
|
|
||||||
position_limit_count = count_position_limit_active_monitors(conn)
|
position_limit_count = count_position_limit_active_monitors(conn)
|
||||||
opens_today = count_opens_for_trading_day(conn, trading_day)
|
opens_today = count_opens_for_trading_day(conn, trading_day)
|
||||||
@@ -6952,7 +6953,7 @@ def render_main_page(page="trade", embed_mode=None):
|
|||||||
)
|
)
|
||||||
strategy_extra = {}
|
strategy_extra = {}
|
||||||
if plan.strategy:
|
if plan.strategy:
|
||||||
from strategy_ui import strategy_render_extras
|
from lib.strategy.strategy_ui import strategy_render_extras
|
||||||
|
|
||||||
strategy_extra = strategy_render_extras(
|
strategy_extra = strategy_render_extras(
|
||||||
conn,
|
conn,
|
||||||
@@ -6962,7 +6963,7 @@ def render_main_page(page="trade", embed_mode=None):
|
|||||||
trend_cfg=app.extensions.get("strategy_trend_cfg"),
|
trend_cfg=app.extensions.get("strategy_trend_cfg"),
|
||||||
)
|
)
|
||||||
conn.close()
|
conn.close()
|
||||||
from instance_embed_lib import embed_context_extras
|
from lib.instance.instance_embed_lib import embed_context_extras
|
||||||
|
|
||||||
template_ctx = dict(
|
template_ctx = dict(
|
||||||
page=page,
|
page=page,
|
||||||
@@ -7104,7 +7105,7 @@ def api_account_snapshot():
|
|||||||
funding_usdt = round(funding_capital, 2) if funding_capital is not None else None
|
funding_usdt = round(funding_capital, 2) if funding_capital is not None else None
|
||||||
current_capital = round(trading_capital, 2) if trading_capital is not None else round(local_current_capital, 2)
|
current_capital = round(trading_capital, 2) if trading_capital is not None else round(local_current_capital, 2)
|
||||||
recommended_capital = round(float(get_recommended_capital(current_capital)), 2)
|
recommended_capital = round(float(get_recommended_capital(current_capital)), 2)
|
||||||
from strategy_trade_labels import count_position_limit_active_monitors
|
from lib.strategy.strategy_trade_labels import count_position_limit_active_monitors
|
||||||
|
|
||||||
position_limit_count = count_position_limit_active_monitors(conn)
|
position_limit_count = count_position_limit_active_monitors(conn)
|
||||||
opens_today = count_opens_for_trading_day(conn, trading_day)
|
opens_today = count_opens_for_trading_day(conn, trading_day)
|
||||||
@@ -7414,7 +7415,7 @@ def api_price_snapshot():
|
|||||||
pass
|
pass
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
from hub_position_metrics import build_position_marks_list
|
from lib.hub.hub_position_metrics import build_position_marks_list
|
||||||
|
|
||||||
position_marks = build_position_marks_list(
|
position_marks = build_position_marks_list(
|
||||||
all_swap_positions,
|
all_swap_positions,
|
||||||
@@ -7647,7 +7648,7 @@ def api_order_kline():
|
|||||||
"volume": float(bar[5]),
|
"volume": float(bar[5]),
|
||||||
})
|
})
|
||||||
|
|
||||||
from focus_chart_lib import (
|
from lib.instance.focus_chart_lib import (
|
||||||
build_order_kline_order_payload,
|
build_order_kline_order_payload,
|
||||||
load_swap_positions_for_order_kline,
|
load_swap_positions_for_order_kline,
|
||||||
metrics_for_order_item,
|
metrics_for_order_item,
|
||||||
@@ -7675,7 +7676,7 @@ def api_order_kline():
|
|||||||
ex_metrics=ex_metrics,
|
ex_metrics=ex_metrics,
|
||||||
)
|
)
|
||||||
|
|
||||||
from focus_chart_lib import kline_api_price_fields
|
from lib.instance.focus_chart_lib import kline_api_price_fields
|
||||||
|
|
||||||
price_fields = kline_api_price_fields(
|
price_fields = kline_api_price_fields(
|
||||||
exchange,
|
exchange,
|
||||||
@@ -7792,7 +7793,7 @@ def api_key_kline():
|
|||||||
"lower_pct": lower_pct,
|
"lower_pct": lower_pct,
|
||||||
}
|
}
|
||||||
|
|
||||||
from focus_chart_lib import enrich_key_kline_response
|
from lib.instance.focus_chart_lib import enrich_key_kline_response
|
||||||
|
|
||||||
price_display, key_info = enrich_key_kline_response(
|
price_display, key_info = enrich_key_kline_response(
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
@@ -7801,7 +7802,7 @@ def api_key_kline():
|
|||||||
format_price_fn=format_price_for_symbol,
|
format_price_fn=format_price_for_symbol,
|
||||||
)
|
)
|
||||||
|
|
||||||
from focus_chart_lib import kline_api_price_fields
|
from lib.instance.focus_chart_lib import kline_api_price_fields
|
||||||
|
|
||||||
price_fields = kline_api_price_fields(
|
price_fields = kline_api_price_fields(
|
||||||
exchange,
|
exchange,
|
||||||
@@ -8756,7 +8757,7 @@ def del_order(id):
|
|||||||
opened_at=opened_at,
|
opened_at=opened_at,
|
||||||
closed_at=closed_at,
|
closed_at=closed_at,
|
||||||
)
|
)
|
||||||
from account_risk_lib import CLOSE_SOURCE_USER_INSTANCE, insert_trade_record_id, on_user_initiated_close
|
from lib.trade.account_risk_lib import CLOSE_SOURCE_USER_INSTANCE, insert_trade_record_id, on_user_initiated_close
|
||||||
|
|
||||||
on_user_initiated_close(
|
on_user_initiated_close(
|
||||||
conn,
|
conn,
|
||||||
@@ -8770,7 +8771,7 @@ def del_order(id):
|
|||||||
try:
|
try:
|
||||||
_rcfg = app.extensions.get("strategy_roll_cfg")
|
_rcfg = app.extensions.get("strategy_roll_cfg")
|
||||||
if isinstance(_rcfg, dict):
|
if isinstance(_rcfg, dict):
|
||||||
from strategy_register import roll_sync_after_external_close
|
from lib.strategy.strategy_register import roll_sync_after_external_close
|
||||||
|
|
||||||
roll_sync_after_external_close(_rcfg, conn, row["symbol"], row["direction"])
|
roll_sync_after_external_close(_rcfg, conn, row["symbol"], row["direction"])
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -8832,7 +8833,7 @@ def del_order(id):
|
|||||||
opened_at=opened_at,
|
opened_at=opened_at,
|
||||||
closed_at=closed_at,
|
closed_at=closed_at,
|
||||||
)
|
)
|
||||||
from account_risk_lib import CLOSE_SOURCE_USER_INSTANCE, insert_trade_record_id, on_user_initiated_close
|
from lib.trade.account_risk_lib import CLOSE_SOURCE_USER_INSTANCE, insert_trade_record_id, on_user_initiated_close
|
||||||
|
|
||||||
on_user_initiated_close(
|
on_user_initiated_close(
|
||||||
conn,
|
conn,
|
||||||
@@ -8846,7 +8847,7 @@ def del_order(id):
|
|||||||
try:
|
try:
|
||||||
_rcfg = app.extensions.get("strategy_roll_cfg")
|
_rcfg = app.extensions.get("strategy_roll_cfg")
|
||||||
if isinstance(_rcfg, dict):
|
if isinstance(_rcfg, dict):
|
||||||
from strategy_register import roll_sync_after_external_close
|
from lib.strategy.strategy_register import roll_sync_after_external_close
|
||||||
|
|
||||||
roll_sync_after_external_close(_rcfg, conn, row["symbol"], row["direction"])
|
roll_sync_after_external_close(_rcfg, conn, row["symbol"], row["direction"])
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -9020,7 +9021,7 @@ def add_journal():
|
|||||||
d.get("post_breakeven_stare"), d.get("new_trade_while_occupied"), d.get("note"), image_filename
|
d.get("post_breakeven_stare"), d.get("new_trade_while_occupied"), d.get("note"), image_filename
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
from account_risk_lib import on_journal_saved
|
from lib.trade.account_risk_lib import on_journal_saved
|
||||||
|
|
||||||
on_journal_saved(
|
on_journal_saved(
|
||||||
conn,
|
conn,
|
||||||
@@ -9108,7 +9109,7 @@ def api_reviews():
|
|||||||
return jsonify([row_to_dict(r) for r in rows])
|
return jsonify([row_to_dict(r) for r in rows])
|
||||||
|
|
||||||
|
|
||||||
_REPO_STATIC_DIR = os.path.join(os.path.dirname(BASE_DIR), "static")
|
_REPO_STATIC_DIR = common_static_dir(os.path.dirname(BASE_DIR))
|
||||||
_AI_REVIEW_RENDER_JS = os.path.join(_REPO_STATIC_DIR, "ai_review_render.js")
|
_AI_REVIEW_RENDER_JS = os.path.join(_REPO_STATIC_DIR, "ai_review_render.js")
|
||||||
_FORM_SUBMIT_GUARD_JS = os.path.join(_REPO_STATIC_DIR, "form_submit_guard.js")
|
_FORM_SUBMIT_GUARD_JS = os.path.join(_REPO_STATIC_DIR, "form_submit_guard.js")
|
||||||
_MANUAL_ORDER_RR_PREVIEW_JS = os.path.join(_REPO_STATIC_DIR, "manual_order_rr_preview.js")
|
_MANUAL_ORDER_RR_PREVIEW_JS = os.path.join(_REPO_STATIC_DIR, "manual_order_rr_preview.js")
|
||||||
@@ -9342,7 +9343,7 @@ def api_trade_record_review_update():
|
|||||||
tuple(base_params + [rec_id]),
|
tuple(base_params + [rec_id]),
|
||||||
)
|
)
|
||||||
if reviewed_result == "手动平仓" and reviewed_miss_reason:
|
if reviewed_result == "手动平仓" and reviewed_miss_reason:
|
||||||
from account_risk_lib import apply_manual_close_journal_cooloff
|
from lib.trade.account_risk_lib import apply_manual_close_journal_cooloff
|
||||||
|
|
||||||
apply_manual_close_journal_cooloff(
|
apply_manual_close_journal_cooloff(
|
||||||
conn,
|
conn,
|
||||||
@@ -9491,7 +9492,7 @@ def _hub_account_bundle():
|
|||||||
|
|
||||||
|
|
||||||
def _hub_fetch_market(base=""):
|
def _hub_fetch_market(base=""):
|
||||||
from hub_market_info_lib import fetch_usdt_swap_market_info
|
from lib.hub.hub_market_info_lib import fetch_usdt_swap_market_info
|
||||||
|
|
||||||
return fetch_usdt_swap_market_info(
|
return fetch_usdt_swap_market_info(
|
||||||
base_or_symbol=base,
|
base_or_symbol=base,
|
||||||
@@ -9504,7 +9505,7 @@ def _hub_fetch_market(base=""):
|
|||||||
|
|
||||||
|
|
||||||
def _hub_fetch_ohlcv(symbol, timeframe, since_ms=None, limit=500):
|
def _hub_fetch_ohlcv(symbol, timeframe, since_ms=None, limit=500):
|
||||||
from hub_ohlcv_lib import fetch_ohlcv_for_hub
|
from lib.hub.hub_ohlcv_lib import fetch_ohlcv_for_hub
|
||||||
|
|
||||||
return fetch_ohlcv_for_hub(
|
return fetch_ohlcv_for_hub(
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
@@ -9520,7 +9521,7 @@ def _hub_fetch_ohlcv(symbol, timeframe, since_ms=None, limit=500):
|
|||||||
|
|
||||||
|
|
||||||
def _hub_fetch_volume_rank(top_n=20):
|
def _hub_fetch_volume_rank(top_n=20):
|
||||||
from hub_volume_rank_lib import fetch_usdt_swap_volume_rank
|
from lib.hub.hub_volume_rank_lib import fetch_usdt_swap_volume_rank
|
||||||
|
|
||||||
return fetch_usdt_swap_volume_rank(
|
return fetch_usdt_swap_volume_rank(
|
||||||
exchange=exchange,
|
exchange=exchange,
|
||||||
@@ -9537,7 +9538,7 @@ try:
|
|||||||
_repo_root = Path(__file__).resolve().parent.parent
|
_repo_root = Path(__file__).resolve().parent.parent
|
||||||
if str(_repo_root) not in sys.path:
|
if str(_repo_root) not in sys.path:
|
||||||
sys.path.insert(0, str(_repo_root))
|
sys.path.insert(0, str(_repo_root))
|
||||||
from hub_bridge import install_on_app
|
from lib.hub.hub_bridge import install_on_app
|
||||||
|
|
||||||
install_on_app(
|
install_on_app(
|
||||||
app,
|
app,
|
||||||
@@ -9581,8 +9582,8 @@ def strategy_roll_page():
|
|||||||
return redirect("/strategy")
|
return redirect("/strategy")
|
||||||
|
|
||||||
|
|
||||||
from strategy_register import install_strategy_trading
|
from lib.strategy.strategy_register import install_strategy_trading
|
||||||
from strategy_trend_register import install_strategy_trend
|
from lib.strategy.strategy_trend_register import install_strategy_trend
|
||||||
|
|
||||||
install_strategy_trading(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
install_strategy_trading(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
||||||
install_strategy_trend(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
install_strategy_trend(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
||||||
|
|||||||
@@ -34,14 +34,15 @@ import sys
|
|||||||
|
|
||||||
if _REPO_ROOT not in sys.path:
|
if _REPO_ROOT not in sys.path:
|
||||||
sys.path.insert(0, _REPO_ROOT)
|
sys.path.insert(0, _REPO_ROOT)
|
||||||
from ai_client import ai_generate, ai_review, ai_short_advice
|
from lib.paths import common_static_dir
|
||||||
from ai_review_lib import (
|
from lib.ai.ai_client import ai_generate, ai_review, ai_short_advice
|
||||||
|
from lib.ai.ai_review_lib import (
|
||||||
build_journal_ai_chart_path,
|
build_journal_ai_chart_path,
|
||||||
collect_images_for_ai_review,
|
collect_images_for_ai_review,
|
||||||
journal_row_lines_for_ai,
|
journal_row_lines_for_ai,
|
||||||
)
|
)
|
||||||
from form_submit_lib import check_duplicate_submit, submit_scope_add_key, submit_scope_add_order
|
from lib.common.form_submit_lib import check_duplicate_submit, submit_scope_add_key, submit_scope_add_order
|
||||||
from fib_key_monitor_lib import (
|
from lib.key_monitor.fib_key_monitor_lib import (
|
||||||
FIB_KEY_MONITOR_TYPES,
|
FIB_KEY_MONITOR_TYPES,
|
||||||
KEY_ENTRY_REASON_BY_SIGNAL,
|
KEY_ENTRY_REASON_BY_SIGNAL,
|
||||||
backfill_missing_key_signal_types,
|
backfill_missing_key_signal_types,
|
||||||
@@ -53,7 +54,7 @@ from fib_key_monitor_lib import (
|
|||||||
key_signal_type_for_trade_record,
|
key_signal_type_for_trade_record,
|
||||||
stored_key_signal_type,
|
stored_key_signal_type,
|
||||||
)
|
)
|
||||||
from false_breakout_key_monitor_lib import (
|
from lib.key_monitor.false_breakout_key_monitor_lib import (
|
||||||
FALSE_BREAKOUT_MONITOR_TYPE,
|
FALSE_BREAKOUT_MONITOR_TYPE,
|
||||||
FALSE_BREAKOUT_VALIDITY_HOURS,
|
FALSE_BREAKOUT_VALIDITY_HOURS,
|
||||||
calc_false_breakout_plan,
|
calc_false_breakout_plan,
|
||||||
@@ -66,7 +67,7 @@ from false_breakout_key_monitor_lib import (
|
|||||||
normalize_false_breakout_symbol,
|
normalize_false_breakout_symbol,
|
||||||
storage_bounds_from_key_price,
|
storage_bounds_from_key_price,
|
||||||
)
|
)
|
||||||
from strategy_trade_labels import (
|
from lib.strategy.strategy_trade_labels import (
|
||||||
STRATEGY_ENTRY_REASON_OPTIONS,
|
STRATEGY_ENTRY_REASON_OPTIONS,
|
||||||
apply_order_monitor_source_labels,
|
apply_order_monitor_source_labels,
|
||||||
entry_reason_for_monitor_type,
|
entry_reason_for_monitor_type,
|
||||||
@@ -75,7 +76,7 @@ from strategy_trade_labels import (
|
|||||||
trade_record_monitor_type as resolve_trade_record_monitor_type,
|
trade_record_monitor_type as resolve_trade_record_monitor_type,
|
||||||
trend_plan_id_from_monitor_row,
|
trend_plan_id_from_monitor_row,
|
||||||
)
|
)
|
||||||
from journal_chart_lib import (
|
from lib.instance.journal_chart_lib import (
|
||||||
JOURNAL_CHART_DEFAULT_LIMIT,
|
JOURNAL_CHART_DEFAULT_LIMIT,
|
||||||
JOURNAL_CHART_DEFAULT_TF1,
|
JOURNAL_CHART_DEFAULT_TF1,
|
||||||
JOURNAL_CHART_DEFAULT_TF2,
|
JOURNAL_CHART_DEFAULT_TF2,
|
||||||
@@ -91,7 +92,7 @@ from journal_chart_lib import (
|
|||||||
trade_review_fetch_window,
|
trade_review_fetch_window,
|
||||||
trim_rows_for_trade_review,
|
trim_rows_for_trade_review,
|
||||||
)
|
)
|
||||||
from key_sl_tp_lib import (
|
from lib.key_monitor.key_sl_tp_lib import (
|
||||||
breakeven_enabled_from_row,
|
breakeven_enabled_from_row,
|
||||||
normalize_sl_tp_mode,
|
normalize_sl_tp_mode,
|
||||||
parse_breakeven_enabled_form,
|
parse_breakeven_enabled_form,
|
||||||
@@ -100,7 +101,7 @@ from key_sl_tp_lib import (
|
|||||||
sl_tp_mode_label,
|
sl_tp_mode_label,
|
||||||
sl_tp_plan_summary_text,
|
sl_tp_plan_summary_text,
|
||||||
)
|
)
|
||||||
from time_close_lib import (
|
from lib.trade.time_close_lib import (
|
||||||
TIME_CLOSE_RESULT,
|
TIME_CLOSE_RESULT,
|
||||||
apply_time_close_to_payload,
|
apply_time_close_to_payload,
|
||||||
ensure_time_close_schema,
|
ensure_time_close_schema,
|
||||||
@@ -111,13 +112,13 @@ from time_close_lib import (
|
|||||||
time_close_label,
|
time_close_label,
|
||||||
time_close_settings_from_row,
|
time_close_settings_from_row,
|
||||||
)
|
)
|
||||||
from manual_sltp_lib import (
|
from lib.trade.manual_sltp_lib import (
|
||||||
normalize_open_sltp_mode,
|
normalize_open_sltp_mode,
|
||||||
resolve_entrust_sltp_prices,
|
resolve_entrust_sltp_prices,
|
||||||
resolve_open_sltp_prices,
|
resolve_open_sltp_prices,
|
||||||
)
|
)
|
||||||
from key_monitor_schema_lib import ensure_key_monitor_schema
|
from lib.key_monitor.key_monitor_schema_lib import ensure_key_monitor_schema
|
||||||
from trigger_entry_key_monitor_lib import (
|
from lib.key_monitor.trigger_entry_key_monitor_lib import (
|
||||||
BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE,
|
BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE,
|
||||||
CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE,
|
CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE,
|
||||||
TRIGGER_ENTRY_CLOSE_EXCHANGE_FAILED,
|
TRIGGER_ENTRY_CLOSE_EXCHANGE_FAILED,
|
||||||
@@ -140,7 +141,7 @@ from trigger_entry_key_monitor_lib import (
|
|||||||
validate_trigger_entry_geometry,
|
validate_trigger_entry_geometry,
|
||||||
validate_trigger_entry_rr,
|
validate_trigger_entry_rr,
|
||||||
)
|
)
|
||||||
from position_sizing_lib import (
|
from lib.trade.position_sizing_lib import (
|
||||||
OPEN_SOURCE_KEY_AUTO,
|
OPEN_SOURCE_KEY_AUTO,
|
||||||
OPEN_SOURCE_KEY_TRIGGER,
|
OPEN_SOURCE_KEY_TRIGGER,
|
||||||
OPEN_SOURCE_MANUAL,
|
OPEN_SOURCE_MANUAL,
|
||||||
@@ -154,12 +155,12 @@ from position_sizing_lib import (
|
|||||||
mode_label_zh,
|
mode_label_zh,
|
||||||
risk_percent_for_storage,
|
risk_percent_for_storage,
|
||||||
)
|
)
|
||||||
from key_monitor_full_margin_lib import (
|
from lib.key_monitor.key_monitor_full_margin_lib import (
|
||||||
monitor_type_disallowed_in_full_margin,
|
monitor_type_disallowed_in_full_margin,
|
||||||
purge_disallowed_key_monitors,
|
purge_disallowed_key_monitors,
|
||||||
)
|
)
|
||||||
from auto_transfer_daily_lib import run_auto_transfer_once_per_day
|
from lib.common.auto_transfer_daily_lib import run_auto_transfer_once_per_day
|
||||||
from key_monitor_lib import (
|
from lib.key_monitor.key_monitor_lib import (
|
||||||
KEY_DIRECTION_WATCH,
|
KEY_DIRECTION_WATCH,
|
||||||
KEY_MONITOR_ALERT_ONLY_TYPES,
|
KEY_MONITOR_ALERT_ONLY_TYPES,
|
||||||
KEY_MONITOR_AUTO_TYPES,
|
KEY_MONITOR_AUTO_TYPES,
|
||||||
@@ -179,16 +180,16 @@ from key_monitor_lib import (
|
|||||||
rs_break_from_direction,
|
rs_break_from_direction,
|
||||||
run_rs_level_alert_tick,
|
run_rs_level_alert_tick,
|
||||||
)
|
)
|
||||||
from order_monitor_display_lib import (
|
from lib.trade.order_monitor_display_lib import (
|
||||||
apply_order_price_display_fields,
|
apply_order_price_display_fields,
|
||||||
enrich_order_display_fields,
|
enrich_order_display_fields,
|
||||||
order_monitor_tpsl_needs_sync,
|
order_monitor_tpsl_needs_sync,
|
||||||
)
|
)
|
||||||
from wechat_notify_lib import build_wechat_rs_level_message, send_wechat_webhook
|
from lib.common.wechat_notify_lib import build_wechat_rs_level_message, send_wechat_webhook
|
||||||
from hub_auth import request_allowed as hub_request_allowed
|
from lib.hub.hub_auth import request_allowed as hub_request_allowed
|
||||||
from instance_nav_lib import request_is_hub_soft_nav
|
from lib.instance.instance_nav_lib import request_is_hub_soft_nav
|
||||||
from hub_volume_rank_lib import resolve_daily_volume_rank
|
from lib.hub.hub_volume_rank_lib import resolve_daily_volume_rank
|
||||||
from history_window_lib import (
|
from lib.common.history_window_lib import (
|
||||||
PRESET_CUSTOM,
|
PRESET_CUSTOM,
|
||||||
PRESET_UTC_LAST24H,
|
PRESET_UTC_LAST24H,
|
||||||
PRESET_UTC_LAST7D,
|
PRESET_UTC_LAST7D,
|
||||||
@@ -201,8 +202,8 @@ from history_window_lib import (
|
|||||||
utc_window_to_bj_sql_strings,
|
utc_window_to_bj_sql_strings,
|
||||||
utc_window_to_utc_sql_strings,
|
utc_window_to_utc_sql_strings,
|
||||||
)
|
)
|
||||||
from trade_result_lib import count_winning_trades, normalize_result_with_pnl
|
from lib.trade.trade_result_lib import count_winning_trades, normalize_result_with_pnl
|
||||||
from trade_exchange_stats_lib import attach_exchange_stats_to_trade, filter_position_lifecycle_fills
|
from lib.trade.trade_exchange_stats_lib import attach_exchange_stats_to_trade, filter_position_lifecycle_fills
|
||||||
|
|
||||||
|
|
||||||
def load_env_file(path):
|
def load_env_file(path):
|
||||||
@@ -343,7 +344,7 @@ ORDER_CHART_ENABLED = os.getenv("ORDER_CHART_ENABLED", "true").lower() == "true"
|
|||||||
ORDER_CHART_TFS = [x.strip() for x in (os.getenv("ORDER_CHART_TFS", "4h,1h,15m,5m") or "").split(",") if x.strip()]
|
ORDER_CHART_TFS = [x.strip() for x in (os.getenv("ORDER_CHART_TFS", "4h,1h,15m,5m") or "").split(",") if x.strip()]
|
||||||
ORDER_CHART_LIMIT = int(os.getenv("ORDER_CHART_LIMIT", "100"))
|
ORDER_CHART_LIMIT = int(os.getenv("ORDER_CHART_LIMIT", "100"))
|
||||||
ORDER_CHART_DIR = resolve_path(os.getenv("ORDER_CHART_DIR", "static/images/order_charts"))
|
ORDER_CHART_DIR = resolve_path(os.getenv("ORDER_CHART_DIR", "static/images/order_charts"))
|
||||||
from daily_open_limit_lib import (
|
from lib.trade.daily_open_limit_lib import (
|
||||||
build_daily_open_alert_prompt,
|
build_daily_open_alert_prompt,
|
||||||
can_trade_new_open,
|
can_trade_new_open,
|
||||||
check_daily_open_hard_limit,
|
check_daily_open_hard_limit,
|
||||||
@@ -1506,10 +1507,10 @@ def init_db():
|
|||||||
close_reason TEXT, closed_at TEXT)"""
|
close_reason TEXT, closed_at TEXT)"""
|
||||||
)
|
)
|
||||||
|
|
||||||
from strategy_db import init_strategy_tables
|
from lib.strategy.strategy_db import init_strategy_tables
|
||||||
|
|
||||||
init_strategy_tables(conn)
|
init_strategy_tables(conn)
|
||||||
from account_risk_lib import ensure_account_risk_schema
|
from lib.trade.account_risk_lib import ensure_account_risk_schema
|
||||||
|
|
||||||
ensure_account_risk_schema(conn)
|
ensure_account_risk_schema(conn)
|
||||||
backfill_missing_key_signal_types(conn, monitor_type=ORDER_MONITOR_TYPE_KEY_AUTO)
|
backfill_missing_key_signal_types(conn, monitor_type=ORDER_MONITOR_TYPE_KEY_AUTO)
|
||||||
@@ -1546,7 +1547,7 @@ def get_db():
|
|||||||
|
|
||||||
|
|
||||||
def hub_account_risk_status(conn):
|
def hub_account_risk_status(conn):
|
||||||
from account_risk_lib import (
|
from lib.trade.account_risk_lib import (
|
||||||
apply_position_limit_risk,
|
apply_position_limit_risk,
|
||||||
compute_account_risk_status,
|
compute_account_risk_status,
|
||||||
enrich_risk_status_countdown,
|
enrich_risk_status_countdown,
|
||||||
@@ -1562,7 +1563,7 @@ def hub_account_risk_status(conn):
|
|||||||
fmt_local_ms=ms_to_app_local_str,
|
fmt_local_ms=ms_to_app_local_str,
|
||||||
)
|
)
|
||||||
st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR)
|
st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR)
|
||||||
from strategy_trade_labels import count_position_limit_active_monitors
|
from lib.strategy.strategy_trade_labels import count_position_limit_active_monitors
|
||||||
|
|
||||||
return apply_position_limit_risk(
|
return apply_position_limit_risk(
|
||||||
st,
|
st,
|
||||||
@@ -1579,7 +1580,7 @@ def hub_user_initiated_close(
|
|||||||
trade_record_id=None,
|
trade_record_id=None,
|
||||||
closed_at_ms=None,
|
closed_at_ms=None,
|
||||||
):
|
):
|
||||||
from account_risk_lib import CLOSE_SOURCE_USER_HUB, on_user_initiated_close
|
from lib.trade.account_risk_lib import CLOSE_SOURCE_USER_HUB, on_user_initiated_close
|
||||||
|
|
||||||
src = (source or "").strip() or CLOSE_SOURCE_USER_HUB
|
src = (source or "").strip() or CLOSE_SOURCE_USER_HUB
|
||||||
on_user_initiated_close(
|
on_user_initiated_close(
|
||||||
@@ -2072,7 +2073,7 @@ def get_effective_trade_field(row, reviewed_key, base_key, default=None):
|
|||||||
|
|
||||||
def to_effective_trade_dict(row):
|
def to_effective_trade_dict(row):
|
||||||
item = row_to_dict(row)
|
item = row_to_dict(row)
|
||||||
from order_monitor_display_lib import snapshot_stop_loss
|
from lib.trade.order_monitor_display_lib import snapshot_stop_loss
|
||||||
|
|
||||||
open_stop = snapshot_stop_loss(item.get("initial_stop_loss"), item.get("stop_loss"))
|
open_stop = snapshot_stop_loss(item.get("initial_stop_loss"), item.get("stop_loss"))
|
||||||
item["display_open_stop_loss"] = open_stop
|
item["display_open_stop_loss"] = open_stop
|
||||||
@@ -2370,7 +2371,7 @@ def insert_trade_record(
|
|||||||
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
|
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
|
||||||
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
||||||
kst = key_signal_type_for_trade_record(key_signal_type, KEY_MONITOR_AUTO_TYPES)
|
kst = key_signal_type_for_trade_record(key_signal_type, KEY_MONITOR_AUTO_TYPES)
|
||||||
from order_monitor_display_lib import snapshot_stop_loss
|
from lib.trade.order_monitor_display_lib import snapshot_stop_loss
|
||||||
|
|
||||||
snap_sl = snapshot_stop_loss(initial_stop_loss, stop_loss)
|
snap_sl = snapshot_stop_loss(initial_stop_loss, stop_loss)
|
||||||
er = (
|
er = (
|
||||||
@@ -2761,7 +2762,7 @@ def get_exchange_capitals(force=False):
|
|||||||
|
|
||||||
|
|
||||||
def execute_transfer_usdt(amount, from_account, to_account):
|
def execute_transfer_usdt(amount, from_account, to_account):
|
||||||
from gate_transfer_lib import execute_transfer_usdt as _gate_execute_transfer_usdt
|
from lib.exchange.gate_transfer_lib import execute_transfer_usdt as _gate_execute_transfer_usdt
|
||||||
|
|
||||||
return _gate_execute_transfer_usdt(
|
return _gate_execute_transfer_usdt(
|
||||||
exchange,
|
exchange,
|
||||||
@@ -2794,7 +2795,7 @@ def get_account_usdt_total(account_type):
|
|||||||
|
|
||||||
|
|
||||||
def _auto_transfer_active_count(conn):
|
def _auto_transfer_active_count(conn):
|
||||||
from gate_transfer_lib import count_auto_transfer_blockers
|
from lib.exchange.gate_transfer_lib import count_auto_transfer_blockers
|
||||||
|
|
||||||
return count_auto_transfer_blockers(conn, count_order_monitors=get_active_position_count)
|
return count_auto_transfer_blockers(conn, count_order_monitors=get_active_position_count)
|
||||||
|
|
||||||
@@ -2878,7 +2879,7 @@ def resolve_capital_base_for_key_open(conn, trading_day, live_capital):
|
|||||||
|
|
||||||
def precheck_risk(conn, symbol, direction):
|
def precheck_risk(conn, symbol, direction):
|
||||||
now = app_now()
|
now = app_now()
|
||||||
from account_risk_lib import account_risk_blocks_trading
|
from lib.trade.account_risk_lib import account_risk_blocks_trading
|
||||||
|
|
||||||
ok_risk, risk_reason = account_risk_blocks_trading(
|
ok_risk, risk_reason = account_risk_blocks_trading(
|
||||||
conn,
|
conn,
|
||||||
@@ -2890,7 +2891,7 @@ def precheck_risk(conn, symbol, direction):
|
|||||||
return False, risk_reason
|
return False, risk_reason
|
||||||
if not trading_day_reset_allows_new_open(now):
|
if not trading_day_reset_allows_new_open(now):
|
||||||
return False, f"北京时间 {TRADING_DAY_RESET_HOUR}:00 前不允许持仓"
|
return False, f"北京时间 {TRADING_DAY_RESET_HOUR}:00 前不允许持仓"
|
||||||
from account_risk_lib import position_limit_reached
|
from lib.trade.account_risk_lib import position_limit_reached
|
||||||
|
|
||||||
reached, active_count, mx = position_limit_reached(conn, max_active_positions=MAX_ACTIVE_POSITIONS)
|
reached, active_count, mx = position_limit_reached(conn, max_active_positions=MAX_ACTIVE_POSITIONS)
|
||||||
if reached:
|
if reached:
|
||||||
@@ -3670,7 +3671,7 @@ def parse_ccxt_position_metrics(position, order_leverage=None):
|
|||||||
cs = float(get_contract_size(sym)) if sym else 1.0
|
cs = float(get_contract_size(sym)) if sym else 1.0
|
||||||
except Exception:
|
except Exception:
|
||||||
cs = 1.0
|
cs = 1.0
|
||||||
from hub_position_metrics import enrich_ccxt_position_metrics_out
|
from lib.hub.hub_position_metrics import enrich_ccxt_position_metrics_out
|
||||||
|
|
||||||
enrich_ccxt_position_metrics_out(p, out, contract_size=cs, funds_decimals=2)
|
enrich_ccxt_position_metrics_out(p, out, contract_size=cs, funds_decimals=2)
|
||||||
return out or None
|
return out or None
|
||||||
@@ -3854,7 +3855,7 @@ def fetch_latest_closing_fill(exchange_symbol, direction, opened_at_str, opened_
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
from gate_position_history_lib import pick_gate_position_close
|
from lib.exchange.gate_position_history_lib import pick_gate_position_close
|
||||||
|
|
||||||
pos = pick_gate_position_close(
|
pos = pick_gate_position_close(
|
||||||
fetch_gate_positions_close_history(),
|
fetch_gate_positions_close_history(),
|
||||||
@@ -4114,7 +4115,7 @@ def reconcile_hub_external_close(conn, symbol, direction):
|
|||||||
"""中控市价全平后:立即同步匹配 order_monitor,并读 Gate 平仓历史。"""
|
"""中控市价全平后:立即同步匹配 order_monitor,并读 Gate 平仓历史。"""
|
||||||
if not exchange_private_api_configured():
|
if not exchange_private_api_configured():
|
||||||
return {"ok": False, "msg": "未配置 GATE_API_KEY / GATE_API_SECRET", "synced": 0}
|
return {"ok": False, "msg": "未配置 GATE_API_KEY / GATE_API_SECRET", "synced": 0}
|
||||||
from gate_position_history_lib import unified_symbol_for_match
|
from lib.exchange.gate_position_history_lib import unified_symbol_for_match
|
||||||
|
|
||||||
sym_u = unified_symbol_for_match(symbol)
|
sym_u = unified_symbol_for_match(symbol)
|
||||||
dir_l = (direction or "").strip().lower()
|
dir_l = (direction or "").strip().lower()
|
||||||
@@ -6513,14 +6514,14 @@ def background_task():
|
|||||||
check_trigger_entry_key_monitors()
|
check_trigger_entry_key_monitors()
|
||||||
_roll_cfg = app.extensions.get("strategy_roll_cfg")
|
_roll_cfg = app.extensions.get("strategy_roll_cfg")
|
||||||
if _roll_cfg:
|
if _roll_cfg:
|
||||||
from strategy_roll_monitor_lib import check_roll_monitors
|
from lib.strategy.strategy_roll_monitor_lib import check_roll_monitors
|
||||||
|
|
||||||
check_roll_monitors(_roll_cfg)
|
check_roll_monitors(_roll_cfg)
|
||||||
check_key_monitors()
|
check_key_monitors()
|
||||||
check_order_monitors()
|
check_order_monitors()
|
||||||
cfg = app.extensions.get("strategy_trend_cfg")
|
cfg = app.extensions.get("strategy_trend_cfg")
|
||||||
if cfg:
|
if cfg:
|
||||||
from strategy_trend_register import check_trend_pullback_plans
|
from lib.strategy.strategy_trend_register import check_trend_pullback_plans
|
||||||
|
|
||||||
check_trend_pullback_plans(cfg)
|
check_trend_pullback_plans(cfg)
|
||||||
except:
|
except:
|
||||||
@@ -6848,7 +6849,7 @@ def render_main_page(page="trade", embed_mode=None):
|
|||||||
conn = get_db()
|
conn = get_db()
|
||||||
session_row = ensure_session(conn, trading_day)
|
session_row = ensure_session(conn, trading_day)
|
||||||
local_current_capital = float(session_row["current_capital"])
|
local_current_capital = float(session_row["current_capital"])
|
||||||
from instance_embed_context_lib import (
|
from lib.instance.instance_embed_context_lib import (
|
||||||
embed_render_plan,
|
embed_render_plan,
|
||||||
minimal_stats_bundle,
|
minimal_stats_bundle,
|
||||||
trade_records_summary,
|
trade_records_summary,
|
||||||
@@ -6921,7 +6922,7 @@ def render_main_page(page="trade", embed_mode=None):
|
|||||||
records = []
|
records = []
|
||||||
total = miss_count = rate = occupied_miss_total = 0
|
total = miss_count = rate = occupied_miss_total = 0
|
||||||
active_count = len(order_list)
|
active_count = len(order_list)
|
||||||
from strategy_trade_labels import count_position_limit_active_monitors
|
from lib.strategy.strategy_trade_labels import count_position_limit_active_monitors
|
||||||
|
|
||||||
position_limit_count = count_position_limit_active_monitors(conn)
|
position_limit_count = count_position_limit_active_monitors(conn)
|
||||||
opens_today = count_opens_for_trading_day(conn, trading_day)
|
opens_today = count_opens_for_trading_day(conn, trading_day)
|
||||||
@@ -6952,7 +6953,7 @@ def render_main_page(page="trade", embed_mode=None):
|
|||||||
)
|
)
|
||||||
strategy_extra = {}
|
strategy_extra = {}
|
||||||
if plan.strategy:
|
if plan.strategy:
|
||||||
from strategy_ui import strategy_render_extras
|
from lib.strategy.strategy_ui import strategy_render_extras
|
||||||
|
|
||||||
strategy_extra = strategy_render_extras(
|
strategy_extra = strategy_render_extras(
|
||||||
conn,
|
conn,
|
||||||
@@ -6962,7 +6963,7 @@ def render_main_page(page="trade", embed_mode=None):
|
|||||||
trend_cfg=app.extensions.get("strategy_trend_cfg"),
|
trend_cfg=app.extensions.get("strategy_trend_cfg"),
|
||||||
)
|
)
|
||||||
conn.close()
|
conn.close()
|
||||||
from instance_embed_lib import embed_context_extras
|
from lib.instance.instance_embed_lib import embed_context_extras
|
||||||
|
|
||||||
template_ctx = dict(
|
template_ctx = dict(
|
||||||
page=page,
|
page=page,
|
||||||
@@ -7100,7 +7101,7 @@ def api_account_snapshot():
|
|||||||
funding_usdt = round(funding_capital, 2) if funding_capital is not None else None
|
funding_usdt = round(funding_capital, 2) if funding_capital is not None else None
|
||||||
current_capital = round(trading_capital, 2) if trading_capital is not None else round(local_current_capital, 2)
|
current_capital = round(trading_capital, 2) if trading_capital is not None else round(local_current_capital, 2)
|
||||||
recommended_capital = round(float(get_recommended_capital(current_capital)), 2)
|
recommended_capital = round(float(get_recommended_capital(current_capital)), 2)
|
||||||
from strategy_trade_labels import count_position_limit_active_monitors
|
from lib.strategy.strategy_trade_labels import count_position_limit_active_monitors
|
||||||
|
|
||||||
position_limit_count = count_position_limit_active_monitors(conn)
|
position_limit_count = count_position_limit_active_monitors(conn)
|
||||||
opens_today = count_opens_for_trading_day(conn, trading_day)
|
opens_today = count_opens_for_trading_day(conn, trading_day)
|
||||||
@@ -7410,7 +7411,7 @@ def api_price_snapshot():
|
|||||||
pass
|
pass
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
from hub_position_metrics import build_position_marks_list
|
from lib.hub.hub_position_metrics import build_position_marks_list
|
||||||
|
|
||||||
position_marks = build_position_marks_list(
|
position_marks = build_position_marks_list(
|
||||||
all_swap_positions,
|
all_swap_positions,
|
||||||
@@ -7643,7 +7644,7 @@ def api_order_kline():
|
|||||||
"volume": float(bar[5]),
|
"volume": float(bar[5]),
|
||||||
})
|
})
|
||||||
|
|
||||||
from focus_chart_lib import (
|
from lib.instance.focus_chart_lib import (
|
||||||
build_order_kline_order_payload,
|
build_order_kline_order_payload,
|
||||||
load_swap_positions_for_order_kline,
|
load_swap_positions_for_order_kline,
|
||||||
metrics_for_order_item,
|
metrics_for_order_item,
|
||||||
@@ -7671,7 +7672,7 @@ def api_order_kline():
|
|||||||
ex_metrics=ex_metrics,
|
ex_metrics=ex_metrics,
|
||||||
)
|
)
|
||||||
|
|
||||||
from focus_chart_lib import kline_api_price_fields
|
from lib.instance.focus_chart_lib import kline_api_price_fields
|
||||||
|
|
||||||
price_fields = kline_api_price_fields(
|
price_fields = kline_api_price_fields(
|
||||||
exchange,
|
exchange,
|
||||||
@@ -7788,7 +7789,7 @@ def api_key_kline():
|
|||||||
"lower_pct": lower_pct,
|
"lower_pct": lower_pct,
|
||||||
}
|
}
|
||||||
|
|
||||||
from focus_chart_lib import enrich_key_kline_response
|
from lib.instance.focus_chart_lib import enrich_key_kline_response
|
||||||
|
|
||||||
price_display, key_info = enrich_key_kline_response(
|
price_display, key_info = enrich_key_kline_response(
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
@@ -7797,7 +7798,7 @@ def api_key_kline():
|
|||||||
format_price_fn=format_price_for_symbol,
|
format_price_fn=format_price_for_symbol,
|
||||||
)
|
)
|
||||||
|
|
||||||
from focus_chart_lib import kline_api_price_fields
|
from lib.instance.focus_chart_lib import kline_api_price_fields
|
||||||
|
|
||||||
price_fields = kline_api_price_fields(
|
price_fields = kline_api_price_fields(
|
||||||
exchange,
|
exchange,
|
||||||
@@ -8752,7 +8753,7 @@ def del_order(id):
|
|||||||
opened_at=opened_at,
|
opened_at=opened_at,
|
||||||
closed_at=closed_at,
|
closed_at=closed_at,
|
||||||
)
|
)
|
||||||
from account_risk_lib import CLOSE_SOURCE_USER_INSTANCE, insert_trade_record_id, on_user_initiated_close
|
from lib.trade.account_risk_lib import CLOSE_SOURCE_USER_INSTANCE, insert_trade_record_id, on_user_initiated_close
|
||||||
|
|
||||||
on_user_initiated_close(
|
on_user_initiated_close(
|
||||||
conn,
|
conn,
|
||||||
@@ -8766,7 +8767,7 @@ def del_order(id):
|
|||||||
try:
|
try:
|
||||||
_rcfg = app.extensions.get("strategy_roll_cfg")
|
_rcfg = app.extensions.get("strategy_roll_cfg")
|
||||||
if isinstance(_rcfg, dict):
|
if isinstance(_rcfg, dict):
|
||||||
from strategy_register import roll_sync_after_external_close
|
from lib.strategy.strategy_register import roll_sync_after_external_close
|
||||||
|
|
||||||
roll_sync_after_external_close(_rcfg, conn, row["symbol"], row["direction"])
|
roll_sync_after_external_close(_rcfg, conn, row["symbol"], row["direction"])
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -8828,7 +8829,7 @@ def del_order(id):
|
|||||||
opened_at=opened_at,
|
opened_at=opened_at,
|
||||||
closed_at=closed_at,
|
closed_at=closed_at,
|
||||||
)
|
)
|
||||||
from account_risk_lib import CLOSE_SOURCE_USER_INSTANCE, insert_trade_record_id, on_user_initiated_close
|
from lib.trade.account_risk_lib import CLOSE_SOURCE_USER_INSTANCE, insert_trade_record_id, on_user_initiated_close
|
||||||
|
|
||||||
on_user_initiated_close(
|
on_user_initiated_close(
|
||||||
conn,
|
conn,
|
||||||
@@ -8842,7 +8843,7 @@ def del_order(id):
|
|||||||
try:
|
try:
|
||||||
_rcfg = app.extensions.get("strategy_roll_cfg")
|
_rcfg = app.extensions.get("strategy_roll_cfg")
|
||||||
if isinstance(_rcfg, dict):
|
if isinstance(_rcfg, dict):
|
||||||
from strategy_register import roll_sync_after_external_close
|
from lib.strategy.strategy_register import roll_sync_after_external_close
|
||||||
|
|
||||||
roll_sync_after_external_close(_rcfg, conn, row["symbol"], row["direction"])
|
roll_sync_after_external_close(_rcfg, conn, row["symbol"], row["direction"])
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -9016,7 +9017,7 @@ def add_journal():
|
|||||||
d.get("post_breakeven_stare"), d.get("new_trade_while_occupied"), d.get("note"), image_filename
|
d.get("post_breakeven_stare"), d.get("new_trade_while_occupied"), d.get("note"), image_filename
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
from account_risk_lib import on_journal_saved
|
from lib.trade.account_risk_lib import on_journal_saved
|
||||||
|
|
||||||
on_journal_saved(
|
on_journal_saved(
|
||||||
conn,
|
conn,
|
||||||
@@ -9104,7 +9105,7 @@ def api_reviews():
|
|||||||
return jsonify([row_to_dict(r) for r in rows])
|
return jsonify([row_to_dict(r) for r in rows])
|
||||||
|
|
||||||
|
|
||||||
_REPO_STATIC_DIR = os.path.join(os.path.dirname(BASE_DIR), "static")
|
_REPO_STATIC_DIR = common_static_dir(os.path.dirname(BASE_DIR))
|
||||||
_AI_REVIEW_RENDER_JS = os.path.join(_REPO_STATIC_DIR, "ai_review_render.js")
|
_AI_REVIEW_RENDER_JS = os.path.join(_REPO_STATIC_DIR, "ai_review_render.js")
|
||||||
_FORM_SUBMIT_GUARD_JS = os.path.join(_REPO_STATIC_DIR, "form_submit_guard.js")
|
_FORM_SUBMIT_GUARD_JS = os.path.join(_REPO_STATIC_DIR, "form_submit_guard.js")
|
||||||
_MANUAL_ORDER_RR_PREVIEW_JS = os.path.join(_REPO_STATIC_DIR, "manual_order_rr_preview.js")
|
_MANUAL_ORDER_RR_PREVIEW_JS = os.path.join(_REPO_STATIC_DIR, "manual_order_rr_preview.js")
|
||||||
@@ -9338,7 +9339,7 @@ def api_trade_record_review_update():
|
|||||||
tuple(base_params + [rec_id]),
|
tuple(base_params + [rec_id]),
|
||||||
)
|
)
|
||||||
if reviewed_result == "手动平仓" and reviewed_miss_reason:
|
if reviewed_result == "手动平仓" and reviewed_miss_reason:
|
||||||
from account_risk_lib import apply_manual_close_journal_cooloff
|
from lib.trade.account_risk_lib import apply_manual_close_journal_cooloff
|
||||||
|
|
||||||
apply_manual_close_journal_cooloff(
|
apply_manual_close_journal_cooloff(
|
||||||
conn,
|
conn,
|
||||||
@@ -9487,7 +9488,7 @@ def _hub_account_bundle():
|
|||||||
|
|
||||||
|
|
||||||
def _hub_fetch_market(base=""):
|
def _hub_fetch_market(base=""):
|
||||||
from hub_market_info_lib import fetch_usdt_swap_market_info
|
from lib.hub.hub_market_info_lib import fetch_usdt_swap_market_info
|
||||||
|
|
||||||
return fetch_usdt_swap_market_info(
|
return fetch_usdt_swap_market_info(
|
||||||
base_or_symbol=base,
|
base_or_symbol=base,
|
||||||
@@ -9500,7 +9501,7 @@ def _hub_fetch_market(base=""):
|
|||||||
|
|
||||||
|
|
||||||
def _hub_fetch_ohlcv(symbol, timeframe, since_ms=None, limit=500):
|
def _hub_fetch_ohlcv(symbol, timeframe, since_ms=None, limit=500):
|
||||||
from hub_ohlcv_lib import fetch_ohlcv_for_hub
|
from lib.hub.hub_ohlcv_lib import fetch_ohlcv_for_hub
|
||||||
|
|
||||||
return fetch_ohlcv_for_hub(
|
return fetch_ohlcv_for_hub(
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
@@ -9516,7 +9517,7 @@ def _hub_fetch_ohlcv(symbol, timeframe, since_ms=None, limit=500):
|
|||||||
|
|
||||||
|
|
||||||
def _hub_fetch_volume_rank(top_n=20):
|
def _hub_fetch_volume_rank(top_n=20):
|
||||||
from hub_volume_rank_lib import fetch_usdt_swap_volume_rank
|
from lib.hub.hub_volume_rank_lib import fetch_usdt_swap_volume_rank
|
||||||
|
|
||||||
return fetch_usdt_swap_volume_rank(
|
return fetch_usdt_swap_volume_rank(
|
||||||
exchange=exchange,
|
exchange=exchange,
|
||||||
@@ -9533,7 +9534,7 @@ try:
|
|||||||
_repo_root = Path(__file__).resolve().parent.parent
|
_repo_root = Path(__file__).resolve().parent.parent
|
||||||
if str(_repo_root) not in sys.path:
|
if str(_repo_root) not in sys.path:
|
||||||
sys.path.insert(0, str(_repo_root))
|
sys.path.insert(0, str(_repo_root))
|
||||||
from hub_bridge import install_on_app
|
from lib.hub.hub_bridge import install_on_app
|
||||||
|
|
||||||
install_on_app(
|
install_on_app(
|
||||||
app,
|
app,
|
||||||
@@ -9577,8 +9578,8 @@ def strategy_roll_page():
|
|||||||
return redirect("/strategy")
|
return redirect("/strategy")
|
||||||
|
|
||||||
|
|
||||||
from strategy_register import install_strategy_trading
|
from lib.strategy.strategy_register import install_strategy_trading
|
||||||
from strategy_trend_register import install_strategy_trend
|
from lib.strategy.strategy_trend_register import install_strategy_trend
|
||||||
|
|
||||||
install_strategy_trading(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
install_strategy_trading(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
||||||
install_strategy_trend(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
install_strategy_trend(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
||||||
|
|||||||
+63
-62
@@ -34,14 +34,15 @@ import sys
|
|||||||
|
|
||||||
if _REPO_ROOT not in sys.path:
|
if _REPO_ROOT not in sys.path:
|
||||||
sys.path.insert(0, _REPO_ROOT)
|
sys.path.insert(0, _REPO_ROOT)
|
||||||
from ai_client import ai_generate, ai_review, ai_short_advice
|
from lib.paths import common_static_dir
|
||||||
from ai_review_lib import (
|
from lib.ai.ai_client import ai_generate, ai_review, ai_short_advice
|
||||||
|
from lib.ai.ai_review_lib import (
|
||||||
build_journal_ai_chart_path,
|
build_journal_ai_chart_path,
|
||||||
collect_images_for_ai_review,
|
collect_images_for_ai_review,
|
||||||
journal_row_lines_for_ai,
|
journal_row_lines_for_ai,
|
||||||
)
|
)
|
||||||
from form_submit_lib import check_duplicate_submit, submit_scope_add_key, submit_scope_add_order
|
from lib.common.form_submit_lib import check_duplicate_submit, submit_scope_add_key, submit_scope_add_order
|
||||||
from fib_key_monitor_lib import (
|
from lib.key_monitor.fib_key_monitor_lib import (
|
||||||
FIB_KEY_MONITOR_TYPES,
|
FIB_KEY_MONITOR_TYPES,
|
||||||
backfill_missing_key_signal_types,
|
backfill_missing_key_signal_types,
|
||||||
calc_fib_plan,
|
calc_fib_plan,
|
||||||
@@ -52,7 +53,7 @@ from fib_key_monitor_lib import (
|
|||||||
key_signal_type_for_trade_record,
|
key_signal_type_for_trade_record,
|
||||||
stored_key_signal_type,
|
stored_key_signal_type,
|
||||||
)
|
)
|
||||||
from false_breakout_key_monitor_lib import (
|
from lib.key_monitor.false_breakout_key_monitor_lib import (
|
||||||
FALSE_BREAKOUT_MONITOR_TYPE,
|
FALSE_BREAKOUT_MONITOR_TYPE,
|
||||||
FALSE_BREAKOUT_VALIDITY_HOURS,
|
FALSE_BREAKOUT_VALIDITY_HOURS,
|
||||||
calc_false_breakout_plan,
|
calc_false_breakout_plan,
|
||||||
@@ -65,7 +66,7 @@ from false_breakout_key_monitor_lib import (
|
|||||||
normalize_false_breakout_symbol,
|
normalize_false_breakout_symbol,
|
||||||
storage_bounds_from_key_price,
|
storage_bounds_from_key_price,
|
||||||
)
|
)
|
||||||
from strategy_trade_labels import (
|
from lib.strategy.strategy_trade_labels import (
|
||||||
STRATEGY_ENTRY_REASON_OPTIONS,
|
STRATEGY_ENTRY_REASON_OPTIONS,
|
||||||
apply_order_monitor_source_labels,
|
apply_order_monitor_source_labels,
|
||||||
entry_reason_for_monitor_type,
|
entry_reason_for_monitor_type,
|
||||||
@@ -74,8 +75,8 @@ from strategy_trade_labels import (
|
|||||||
trade_record_monitor_type as resolve_trade_record_monitor_type,
|
trade_record_monitor_type as resolve_trade_record_monitor_type,
|
||||||
trend_plan_id_from_monitor_row,
|
trend_plan_id_from_monitor_row,
|
||||||
)
|
)
|
||||||
from okx_orders_lib import cancel_okx_all_open_orders, fetch_okx_all_open_orders
|
from lib.exchange.okx_orders_lib import cancel_okx_all_open_orders, fetch_okx_all_open_orders
|
||||||
from journal_chart_lib import (
|
from lib.instance.journal_chart_lib import (
|
||||||
JOURNAL_CHART_DEFAULT_LIMIT,
|
JOURNAL_CHART_DEFAULT_LIMIT,
|
||||||
JOURNAL_CHART_DEFAULT_TF1,
|
JOURNAL_CHART_DEFAULT_TF1,
|
||||||
JOURNAL_CHART_DEFAULT_TF2,
|
JOURNAL_CHART_DEFAULT_TF2,
|
||||||
@@ -91,7 +92,7 @@ from journal_chart_lib import (
|
|||||||
trade_review_fetch_window,
|
trade_review_fetch_window,
|
||||||
trim_rows_for_trade_review,
|
trim_rows_for_trade_review,
|
||||||
)
|
)
|
||||||
from key_sl_tp_lib import (
|
from lib.key_monitor.key_sl_tp_lib import (
|
||||||
breakeven_enabled_from_row,
|
breakeven_enabled_from_row,
|
||||||
normalize_sl_tp_mode,
|
normalize_sl_tp_mode,
|
||||||
parse_breakeven_enabled_form,
|
parse_breakeven_enabled_form,
|
||||||
@@ -100,7 +101,7 @@ from key_sl_tp_lib import (
|
|||||||
sl_tp_mode_label,
|
sl_tp_mode_label,
|
||||||
sl_tp_plan_summary_text,
|
sl_tp_plan_summary_text,
|
||||||
)
|
)
|
||||||
from time_close_lib import (
|
from lib.trade.time_close_lib import (
|
||||||
TIME_CLOSE_RESULT,
|
TIME_CLOSE_RESULT,
|
||||||
apply_time_close_to_payload,
|
apply_time_close_to_payload,
|
||||||
ensure_time_close_schema,
|
ensure_time_close_schema,
|
||||||
@@ -111,13 +112,13 @@ from time_close_lib import (
|
|||||||
time_close_label,
|
time_close_label,
|
||||||
time_close_settings_from_row,
|
time_close_settings_from_row,
|
||||||
)
|
)
|
||||||
from manual_sltp_lib import (
|
from lib.trade.manual_sltp_lib import (
|
||||||
normalize_open_sltp_mode,
|
normalize_open_sltp_mode,
|
||||||
resolve_entrust_sltp_prices,
|
resolve_entrust_sltp_prices,
|
||||||
resolve_open_sltp_prices,
|
resolve_open_sltp_prices,
|
||||||
)
|
)
|
||||||
from key_monitor_schema_lib import ensure_key_monitor_schema
|
from lib.key_monitor.key_monitor_schema_lib import ensure_key_monitor_schema
|
||||||
from trigger_entry_key_monitor_lib import (
|
from lib.key_monitor.trigger_entry_key_monitor_lib import (
|
||||||
BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE,
|
BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE,
|
||||||
CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE,
|
CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE,
|
||||||
TRIGGER_ENTRY_CLOSE_EXCHANGE_FAILED,
|
TRIGGER_ENTRY_CLOSE_EXCHANGE_FAILED,
|
||||||
@@ -140,7 +141,7 @@ from trigger_entry_key_monitor_lib import (
|
|||||||
validate_trigger_entry_geometry,
|
validate_trigger_entry_geometry,
|
||||||
validate_trigger_entry_rr,
|
validate_trigger_entry_rr,
|
||||||
)
|
)
|
||||||
from position_sizing_lib import (
|
from lib.trade.position_sizing_lib import (
|
||||||
OPEN_SOURCE_KEY_AUTO,
|
OPEN_SOURCE_KEY_AUTO,
|
||||||
OPEN_SOURCE_MANUAL,
|
OPEN_SOURCE_MANUAL,
|
||||||
assert_open_source_allowed,
|
assert_open_source_allowed,
|
||||||
@@ -153,12 +154,12 @@ from position_sizing_lib import (
|
|||||||
mode_label_zh,
|
mode_label_zh,
|
||||||
risk_percent_for_storage,
|
risk_percent_for_storage,
|
||||||
)
|
)
|
||||||
from key_monitor_full_margin_lib import (
|
from lib.key_monitor.key_monitor_full_margin_lib import (
|
||||||
monitor_type_disallowed_in_full_margin,
|
monitor_type_disallowed_in_full_margin,
|
||||||
purge_disallowed_key_monitors,
|
purge_disallowed_key_monitors,
|
||||||
)
|
)
|
||||||
from auto_transfer_daily_lib import run_auto_transfer_once_per_day
|
from lib.common.auto_transfer_daily_lib import run_auto_transfer_once_per_day
|
||||||
from key_monitor_lib import (
|
from lib.key_monitor.key_monitor_lib import (
|
||||||
KEY_DIRECTION_WATCH,
|
KEY_DIRECTION_WATCH,
|
||||||
KEY_MONITOR_ALERT_ONLY_TYPES,
|
KEY_MONITOR_ALERT_ONLY_TYPES,
|
||||||
KEY_MONITOR_AUTO_TYPES,
|
KEY_MONITOR_AUTO_TYPES,
|
||||||
@@ -178,16 +179,16 @@ from key_monitor_lib import (
|
|||||||
rs_break_from_direction,
|
rs_break_from_direction,
|
||||||
run_rs_level_alert_tick,
|
run_rs_level_alert_tick,
|
||||||
)
|
)
|
||||||
from order_monitor_display_lib import (
|
from lib.trade.order_monitor_display_lib import (
|
||||||
apply_order_price_display_fields,
|
apply_order_price_display_fields,
|
||||||
enrich_order_display_fields,
|
enrich_order_display_fields,
|
||||||
order_monitor_tpsl_needs_sync,
|
order_monitor_tpsl_needs_sync,
|
||||||
)
|
)
|
||||||
from wechat_notify_lib import build_wechat_rs_level_message, send_wechat_webhook
|
from lib.common.wechat_notify_lib import build_wechat_rs_level_message, send_wechat_webhook
|
||||||
from hub_auth import request_allowed as hub_request_allowed
|
from lib.hub.hub_auth import request_allowed as hub_request_allowed
|
||||||
from instance_nav_lib import request_is_hub_soft_nav
|
from lib.instance.instance_nav_lib import request_is_hub_soft_nav
|
||||||
from hub_volume_rank_lib import resolve_daily_volume_rank
|
from lib.hub.hub_volume_rank_lib import resolve_daily_volume_rank
|
||||||
from history_window_lib import (
|
from lib.common.history_window_lib import (
|
||||||
PRESET_CUSTOM,
|
PRESET_CUSTOM,
|
||||||
PRESET_UTC_LAST24H,
|
PRESET_UTC_LAST24H,
|
||||||
PRESET_UTC_LAST7D,
|
PRESET_UTC_LAST7D,
|
||||||
@@ -200,8 +201,8 @@ from history_window_lib import (
|
|||||||
utc_window_to_bj_sql_strings,
|
utc_window_to_bj_sql_strings,
|
||||||
utc_window_to_utc_sql_strings,
|
utc_window_to_utc_sql_strings,
|
||||||
)
|
)
|
||||||
from trade_result_lib import count_winning_trades, normalize_result_with_pnl
|
from lib.trade.trade_result_lib import count_winning_trades, normalize_result_with_pnl
|
||||||
from trade_exchange_stats_lib import attach_exchange_stats_to_trade, filter_position_lifecycle_fills
|
from lib.trade.trade_exchange_stats_lib import attach_exchange_stats_to_trade, filter_position_lifecycle_fills
|
||||||
|
|
||||||
|
|
||||||
def load_env_file(path):
|
def load_env_file(path):
|
||||||
@@ -323,7 +324,7 @@ ORDER_CHART_ENABLED = os.getenv("ORDER_CHART_ENABLED", "true").lower() == "true"
|
|||||||
ORDER_CHART_TFS = [x.strip() for x in (os.getenv("ORDER_CHART_TFS", "4h,1h,15m,5m") or "").split(",") if x.strip()]
|
ORDER_CHART_TFS = [x.strip() for x in (os.getenv("ORDER_CHART_TFS", "4h,1h,15m,5m") or "").split(",") if x.strip()]
|
||||||
ORDER_CHART_LIMIT = int(os.getenv("ORDER_CHART_LIMIT", "100"))
|
ORDER_CHART_LIMIT = int(os.getenv("ORDER_CHART_LIMIT", "100"))
|
||||||
ORDER_CHART_DIR = resolve_path(os.getenv("ORDER_CHART_DIR", "static/images/order_charts"))
|
ORDER_CHART_DIR = resolve_path(os.getenv("ORDER_CHART_DIR", "static/images/order_charts"))
|
||||||
from daily_open_limit_lib import (
|
from lib.trade.daily_open_limit_lib import (
|
||||||
build_daily_open_alert_prompt,
|
build_daily_open_alert_prompt,
|
||||||
can_trade_new_open,
|
can_trade_new_open,
|
||||||
check_daily_open_hard_limit,
|
check_daily_open_hard_limit,
|
||||||
@@ -1493,10 +1494,10 @@ def init_db():
|
|||||||
close_reason TEXT, closed_at TEXT)"""
|
close_reason TEXT, closed_at TEXT)"""
|
||||||
)
|
)
|
||||||
|
|
||||||
from strategy_db import init_strategy_tables
|
from lib.strategy.strategy_db import init_strategy_tables
|
||||||
|
|
||||||
init_strategy_tables(conn)
|
init_strategy_tables(conn)
|
||||||
from account_risk_lib import ensure_account_risk_schema
|
from lib.trade.account_risk_lib import ensure_account_risk_schema
|
||||||
|
|
||||||
ensure_account_risk_schema(conn)
|
ensure_account_risk_schema(conn)
|
||||||
backfill_missing_key_signal_types(conn, monitor_type=ORDER_MONITOR_TYPE_KEY_AUTO)
|
backfill_missing_key_signal_types(conn, monitor_type=ORDER_MONITOR_TYPE_KEY_AUTO)
|
||||||
@@ -1533,7 +1534,7 @@ def get_db():
|
|||||||
|
|
||||||
|
|
||||||
def hub_account_risk_status(conn):
|
def hub_account_risk_status(conn):
|
||||||
from account_risk_lib import (
|
from lib.trade.account_risk_lib import (
|
||||||
apply_position_limit_risk,
|
apply_position_limit_risk,
|
||||||
compute_account_risk_status,
|
compute_account_risk_status,
|
||||||
enrich_risk_status_countdown,
|
enrich_risk_status_countdown,
|
||||||
@@ -1549,7 +1550,7 @@ def hub_account_risk_status(conn):
|
|||||||
fmt_local_ms=ms_to_app_local_str,
|
fmt_local_ms=ms_to_app_local_str,
|
||||||
)
|
)
|
||||||
st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR)
|
st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR)
|
||||||
from strategy_trade_labels import count_position_limit_active_monitors
|
from lib.strategy.strategy_trade_labels import count_position_limit_active_monitors
|
||||||
|
|
||||||
return apply_position_limit_risk(
|
return apply_position_limit_risk(
|
||||||
st,
|
st,
|
||||||
@@ -1566,7 +1567,7 @@ def hub_user_initiated_close(
|
|||||||
trade_record_id=None,
|
trade_record_id=None,
|
||||||
closed_at_ms=None,
|
closed_at_ms=None,
|
||||||
):
|
):
|
||||||
from account_risk_lib import CLOSE_SOURCE_USER_HUB, on_user_initiated_close
|
from lib.trade.account_risk_lib import CLOSE_SOURCE_USER_HUB, on_user_initiated_close
|
||||||
|
|
||||||
src = (source or "").strip() or CLOSE_SOURCE_USER_HUB
|
src = (source or "").strip() or CLOSE_SOURCE_USER_HUB
|
||||||
on_user_initiated_close(
|
on_user_initiated_close(
|
||||||
@@ -2021,7 +2022,7 @@ def get_effective_trade_field(row, reviewed_key, base_key, default=None):
|
|||||||
|
|
||||||
def to_effective_trade_dict(row):
|
def to_effective_trade_dict(row):
|
||||||
item = row_to_dict(row)
|
item = row_to_dict(row)
|
||||||
from order_monitor_display_lib import snapshot_stop_loss
|
from lib.trade.order_monitor_display_lib import snapshot_stop_loss
|
||||||
|
|
||||||
open_stop = snapshot_stop_loss(item.get("initial_stop_loss"), item.get("stop_loss"))
|
open_stop = snapshot_stop_loss(item.get("initial_stop_loss"), item.get("stop_loss"))
|
||||||
item["display_open_stop_loss"] = open_stop
|
item["display_open_stop_loss"] = open_stop
|
||||||
@@ -2266,7 +2267,7 @@ def insert_trade_record(
|
|||||||
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
|
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
|
||||||
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
||||||
kst = key_signal_type_for_trade_record(key_signal_type, KEY_MONITOR_AUTO_TYPES)
|
kst = key_signal_type_for_trade_record(key_signal_type, KEY_MONITOR_AUTO_TYPES)
|
||||||
from order_monitor_display_lib import snapshot_stop_loss
|
from lib.trade.order_monitor_display_lib import snapshot_stop_loss
|
||||||
|
|
||||||
snap_sl = snapshot_stop_loss(initial_stop_loss, stop_loss)
|
snap_sl = snapshot_stop_loss(initial_stop_loss, stop_loss)
|
||||||
er = (
|
er = (
|
||||||
@@ -2590,7 +2591,7 @@ def trading_day_reset_allows_new_open(now, conn=None):
|
|||||||
|
|
||||||
def precheck_risk(conn, symbol, direction):
|
def precheck_risk(conn, symbol, direction):
|
||||||
now = app_now()
|
now = app_now()
|
||||||
from account_risk_lib import account_risk_blocks_trading
|
from lib.trade.account_risk_lib import account_risk_blocks_trading
|
||||||
|
|
||||||
ok_risk, risk_reason = account_risk_blocks_trading(
|
ok_risk, risk_reason = account_risk_blocks_trading(
|
||||||
conn,
|
conn,
|
||||||
@@ -2602,7 +2603,7 @@ def precheck_risk(conn, symbol, direction):
|
|||||||
return False, risk_reason
|
return False, risk_reason
|
||||||
if not trading_day_reset_allows_new_open(now):
|
if not trading_day_reset_allows_new_open(now):
|
||||||
return False, f"北京时间 {TRADING_DAY_RESET_HOUR}:00 前不允许持仓"
|
return False, f"北京时间 {TRADING_DAY_RESET_HOUR}:00 前不允许持仓"
|
||||||
from account_risk_lib import position_limit_reached
|
from lib.trade.account_risk_lib import position_limit_reached
|
||||||
|
|
||||||
reached, active_count, mx = position_limit_reached(conn, max_active_positions=MAX_ACTIVE_POSITIONS)
|
reached, active_count, mx = position_limit_reached(conn, max_active_positions=MAX_ACTIVE_POSITIONS)
|
||||||
if reached:
|
if reached:
|
||||||
@@ -2973,7 +2974,7 @@ def parse_ccxt_position_metrics(position, order_leverage=None):
|
|||||||
cs = float(get_contract_size(sym)) if sym else 1.0
|
cs = float(get_contract_size(sym)) if sym else 1.0
|
||||||
except Exception:
|
except Exception:
|
||||||
cs = 1.0
|
cs = 1.0
|
||||||
from hub_position_metrics import enrich_ccxt_position_metrics_out
|
from lib.hub.hub_position_metrics import enrich_ccxt_position_metrics_out
|
||||||
|
|
||||||
enrich_ccxt_position_metrics_out(
|
enrich_ccxt_position_metrics_out(
|
||||||
p, out, contract_size=cs, funds_decimals=FUNDS_DECIMALS
|
p, out, contract_size=cs, funds_decimals=FUNDS_DECIMALS
|
||||||
@@ -6252,14 +6253,14 @@ def background_task():
|
|||||||
check_trigger_entry_key_monitors()
|
check_trigger_entry_key_monitors()
|
||||||
_roll_cfg = app.extensions.get("strategy_roll_cfg")
|
_roll_cfg = app.extensions.get("strategy_roll_cfg")
|
||||||
if _roll_cfg:
|
if _roll_cfg:
|
||||||
from strategy_roll_monitor_lib import check_roll_monitors
|
from lib.strategy.strategy_roll_monitor_lib import check_roll_monitors
|
||||||
|
|
||||||
check_roll_monitors(_roll_cfg)
|
check_roll_monitors(_roll_cfg)
|
||||||
check_key_monitors()
|
check_key_monitors()
|
||||||
check_order_monitors()
|
check_order_monitors()
|
||||||
cfg = app.extensions.get("strategy_trend_cfg")
|
cfg = app.extensions.get("strategy_trend_cfg")
|
||||||
if cfg:
|
if cfg:
|
||||||
from strategy_trend_register import check_trend_pullback_plans
|
from lib.strategy.strategy_trend_register import check_trend_pullback_plans
|
||||||
|
|
||||||
check_trend_pullback_plans(cfg)
|
check_trend_pullback_plans(cfg)
|
||||||
except:
|
except:
|
||||||
@@ -6348,7 +6349,7 @@ def render_main_page(page="trade", embed_mode=None):
|
|||||||
conn = get_db()
|
conn = get_db()
|
||||||
session_row = ensure_session(conn, trading_day)
|
session_row = ensure_session(conn, trading_day)
|
||||||
local_current_capital = float(session_row["current_capital"])
|
local_current_capital = float(session_row["current_capital"])
|
||||||
from instance_embed_context_lib import (
|
from lib.instance.instance_embed_context_lib import (
|
||||||
embed_render_plan,
|
embed_render_plan,
|
||||||
minimal_stats_bundle,
|
minimal_stats_bundle,
|
||||||
trade_records_summary,
|
trade_records_summary,
|
||||||
@@ -6420,7 +6421,7 @@ def render_main_page(page="trade", embed_mode=None):
|
|||||||
records = []
|
records = []
|
||||||
total = miss_count = rate = occupied_miss_total = 0
|
total = miss_count = rate = occupied_miss_total = 0
|
||||||
active_count = len(order_list)
|
active_count = len(order_list)
|
||||||
from strategy_trade_labels import count_position_limit_active_monitors
|
from lib.strategy.strategy_trade_labels import count_position_limit_active_monitors
|
||||||
|
|
||||||
position_limit_count = count_position_limit_active_monitors(conn)
|
position_limit_count = count_position_limit_active_monitors(conn)
|
||||||
open_guard_enabled = get_trading_day_reset_open_guard_enabled(conn)
|
open_guard_enabled = get_trading_day_reset_open_guard_enabled(conn)
|
||||||
@@ -6453,7 +6454,7 @@ def render_main_page(page="trade", embed_mode=None):
|
|||||||
)
|
)
|
||||||
strategy_extra = {}
|
strategy_extra = {}
|
||||||
if plan.strategy:
|
if plan.strategy:
|
||||||
from strategy_ui import strategy_render_extras
|
from lib.strategy.strategy_ui import strategy_render_extras
|
||||||
|
|
||||||
strategy_extra = strategy_render_extras(
|
strategy_extra = strategy_render_extras(
|
||||||
conn,
|
conn,
|
||||||
@@ -6463,7 +6464,7 @@ def render_main_page(page="trade", embed_mode=None):
|
|||||||
trend_cfg=app.extensions.get("strategy_trend_cfg"),
|
trend_cfg=app.extensions.get("strategy_trend_cfg"),
|
||||||
)
|
)
|
||||||
conn.close()
|
conn.close()
|
||||||
from instance_embed_lib import embed_context_extras
|
from lib.instance.instance_embed_lib import embed_context_extras
|
||||||
|
|
||||||
template_ctx = dict(
|
template_ctx = dict(
|
||||||
page=page,
|
page=page,
|
||||||
@@ -6600,7 +6601,7 @@ def api_account_snapshot():
|
|||||||
funding_usdt = round(funding_capital, FUNDS_DECIMALS) if funding_capital is not None else None
|
funding_usdt = round(funding_capital, FUNDS_DECIMALS) if funding_capital is not None else None
|
||||||
current_capital = round(trading_capital, FUNDS_DECIMALS) if trading_capital is not None else round(local_current_capital, FUNDS_DECIMALS)
|
current_capital = round(trading_capital, FUNDS_DECIMALS) if trading_capital is not None else round(local_current_capital, FUNDS_DECIMALS)
|
||||||
recommended_capital = get_recommended_capital(current_capital)
|
recommended_capital = get_recommended_capital(current_capital)
|
||||||
from strategy_trade_labels import count_position_limit_active_monitors
|
from lib.strategy.strategy_trade_labels import count_position_limit_active_monitors
|
||||||
|
|
||||||
position_limit_count = count_position_limit_active_monitors(conn)
|
position_limit_count = count_position_limit_active_monitors(conn)
|
||||||
open_guard_enabled = get_trading_day_reset_open_guard_enabled(conn)
|
open_guard_enabled = get_trading_day_reset_open_guard_enabled(conn)
|
||||||
@@ -6651,7 +6652,7 @@ def api_settings_open_guard():
|
|||||||
now = app_now()
|
now = app_now()
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
trading_day = get_trading_day(now)
|
trading_day = get_trading_day(now)
|
||||||
from strategy_trade_labels import count_position_limit_active_monitors
|
from lib.strategy.strategy_trade_labels import count_position_limit_active_monitors
|
||||||
|
|
||||||
position_limit_count = count_position_limit_active_monitors(conn)
|
position_limit_count = count_position_limit_active_monitors(conn)
|
||||||
guard_on = get_trading_day_reset_open_guard_enabled(conn)
|
guard_on = get_trading_day_reset_open_guard_enabled(conn)
|
||||||
@@ -6954,7 +6955,7 @@ def api_price_snapshot():
|
|||||||
pass
|
pass
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
from hub_position_metrics import build_position_marks_list
|
from lib.hub.hub_position_metrics import build_position_marks_list
|
||||||
|
|
||||||
position_marks = build_position_marks_list(
|
position_marks = build_position_marks_list(
|
||||||
all_swap_positions,
|
all_swap_positions,
|
||||||
@@ -7108,7 +7109,7 @@ def api_order_kline():
|
|||||||
"volume": float(bar[5]),
|
"volume": float(bar[5]),
|
||||||
})
|
})
|
||||||
|
|
||||||
from focus_chart_lib import (
|
from lib.instance.focus_chart_lib import (
|
||||||
build_order_kline_order_payload,
|
build_order_kline_order_payload,
|
||||||
load_swap_positions_for_order_kline,
|
load_swap_positions_for_order_kline,
|
||||||
metrics_for_order_item,
|
metrics_for_order_item,
|
||||||
@@ -7136,7 +7137,7 @@ def api_order_kline():
|
|||||||
ex_metrics=ex_metrics,
|
ex_metrics=ex_metrics,
|
||||||
)
|
)
|
||||||
|
|
||||||
from focus_chart_lib import kline_api_price_fields
|
from lib.instance.focus_chart_lib import kline_api_price_fields
|
||||||
|
|
||||||
price_fields = kline_api_price_fields(
|
price_fields = kline_api_price_fields(
|
||||||
exchange,
|
exchange,
|
||||||
@@ -7252,7 +7253,7 @@ def api_key_kline():
|
|||||||
"lower_pct": lower_pct,
|
"lower_pct": lower_pct,
|
||||||
}
|
}
|
||||||
|
|
||||||
from focus_chart_lib import enrich_key_kline_response
|
from lib.instance.focus_chart_lib import enrich_key_kline_response
|
||||||
|
|
||||||
price_display, key_info = enrich_key_kline_response(
|
price_display, key_info = enrich_key_kline_response(
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
@@ -7261,7 +7262,7 @@ def api_key_kline():
|
|||||||
format_price_fn=format_price_for_symbol,
|
format_price_fn=format_price_for_symbol,
|
||||||
)
|
)
|
||||||
|
|
||||||
from focus_chart_lib import kline_api_price_fields
|
from lib.instance.focus_chart_lib import kline_api_price_fields
|
||||||
|
|
||||||
price_fields = kline_api_price_fields(
|
price_fields = kline_api_price_fields(
|
||||||
exchange,
|
exchange,
|
||||||
@@ -8241,7 +8242,7 @@ def del_order(id):
|
|||||||
opened_at=opened_at,
|
opened_at=opened_at,
|
||||||
closed_at=closed_at,
|
closed_at=closed_at,
|
||||||
)
|
)
|
||||||
from account_risk_lib import CLOSE_SOURCE_USER_INSTANCE, insert_trade_record_id, on_user_initiated_close
|
from lib.trade.account_risk_lib import CLOSE_SOURCE_USER_INSTANCE, insert_trade_record_id, on_user_initiated_close
|
||||||
|
|
||||||
on_user_initiated_close(
|
on_user_initiated_close(
|
||||||
conn,
|
conn,
|
||||||
@@ -8255,7 +8256,7 @@ def del_order(id):
|
|||||||
try:
|
try:
|
||||||
_rcfg = app.extensions.get("strategy_roll_cfg")
|
_rcfg = app.extensions.get("strategy_roll_cfg")
|
||||||
if isinstance(_rcfg, dict):
|
if isinstance(_rcfg, dict):
|
||||||
from strategy_register import roll_sync_after_external_close
|
from lib.strategy.strategy_register import roll_sync_after_external_close
|
||||||
|
|
||||||
roll_sync_after_external_close(_rcfg, conn, row["symbol"], row["direction"])
|
roll_sync_after_external_close(_rcfg, conn, row["symbol"], row["direction"])
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -8314,7 +8315,7 @@ def del_order(id):
|
|||||||
opened_at=opened_at,
|
opened_at=opened_at,
|
||||||
closed_at=closed_at,
|
closed_at=closed_at,
|
||||||
)
|
)
|
||||||
from account_risk_lib import CLOSE_SOURCE_USER_INSTANCE, insert_trade_record_id, on_user_initiated_close
|
from lib.trade.account_risk_lib import CLOSE_SOURCE_USER_INSTANCE, insert_trade_record_id, on_user_initiated_close
|
||||||
|
|
||||||
on_user_initiated_close(
|
on_user_initiated_close(
|
||||||
conn,
|
conn,
|
||||||
@@ -8328,7 +8329,7 @@ def del_order(id):
|
|||||||
try:
|
try:
|
||||||
_rcfg = app.extensions.get("strategy_roll_cfg")
|
_rcfg = app.extensions.get("strategy_roll_cfg")
|
||||||
if isinstance(_rcfg, dict):
|
if isinstance(_rcfg, dict):
|
||||||
from strategy_register import roll_sync_after_external_close
|
from lib.strategy.strategy_register import roll_sync_after_external_close
|
||||||
|
|
||||||
roll_sync_after_external_close(_rcfg, conn, row["symbol"], row["direction"])
|
roll_sync_after_external_close(_rcfg, conn, row["symbol"], row["direction"])
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -8489,7 +8490,7 @@ def add_journal():
|
|||||||
d.get("post_breakeven_stare"), d.get("new_trade_while_occupied"), d.get("note"), image_filename
|
d.get("post_breakeven_stare"), d.get("new_trade_while_occupied"), d.get("note"), image_filename
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
from account_risk_lib import on_journal_saved
|
from lib.trade.account_risk_lib import on_journal_saved
|
||||||
|
|
||||||
on_journal_saved(
|
on_journal_saved(
|
||||||
conn,
|
conn,
|
||||||
@@ -8577,7 +8578,7 @@ def api_reviews():
|
|||||||
return jsonify([row_to_dict(r) for r in rows])
|
return jsonify([row_to_dict(r) for r in rows])
|
||||||
|
|
||||||
|
|
||||||
_REPO_STATIC_DIR = os.path.join(os.path.dirname(BASE_DIR), "static")
|
_REPO_STATIC_DIR = common_static_dir(os.path.dirname(BASE_DIR))
|
||||||
_AI_REVIEW_RENDER_JS = os.path.join(_REPO_STATIC_DIR, "ai_review_render.js")
|
_AI_REVIEW_RENDER_JS = os.path.join(_REPO_STATIC_DIR, "ai_review_render.js")
|
||||||
_FORM_SUBMIT_GUARD_JS = os.path.join(_REPO_STATIC_DIR, "form_submit_guard.js")
|
_FORM_SUBMIT_GUARD_JS = os.path.join(_REPO_STATIC_DIR, "form_submit_guard.js")
|
||||||
_MANUAL_ORDER_RR_PREVIEW_JS = os.path.join(_REPO_STATIC_DIR, "manual_order_rr_preview.js")
|
_MANUAL_ORDER_RR_PREVIEW_JS = os.path.join(_REPO_STATIC_DIR, "manual_order_rr_preview.js")
|
||||||
@@ -8802,7 +8803,7 @@ def api_trade_record_review_update():
|
|||||||
tuple(base_params + [rec_id]),
|
tuple(base_params + [rec_id]),
|
||||||
)
|
)
|
||||||
if reviewed_result == "手动平仓" and reviewed_miss_reason:
|
if reviewed_result == "手动平仓" and reviewed_miss_reason:
|
||||||
from account_risk_lib import apply_manual_close_journal_cooloff
|
from lib.trade.account_risk_lib import apply_manual_close_journal_cooloff
|
||||||
|
|
||||||
apply_manual_close_journal_cooloff(
|
apply_manual_close_journal_cooloff(
|
||||||
conn,
|
conn,
|
||||||
@@ -8952,7 +8953,7 @@ def _hub_account_bundle():
|
|||||||
|
|
||||||
|
|
||||||
def _hub_fetch_market(base=""):
|
def _hub_fetch_market(base=""):
|
||||||
from hub_market_info_lib import fetch_usdt_swap_market_info
|
from lib.hub.hub_market_info_lib import fetch_usdt_swap_market_info
|
||||||
|
|
||||||
return fetch_usdt_swap_market_info(
|
return fetch_usdt_swap_market_info(
|
||||||
base_or_symbol=base,
|
base_or_symbol=base,
|
||||||
@@ -8965,7 +8966,7 @@ def _hub_fetch_market(base=""):
|
|||||||
|
|
||||||
|
|
||||||
def _hub_fetch_ohlcv(symbol, timeframe, since_ms=None, limit=500):
|
def _hub_fetch_ohlcv(symbol, timeframe, since_ms=None, limit=500):
|
||||||
from hub_ohlcv_lib import fetch_ohlcv_for_hub
|
from lib.hub.hub_ohlcv_lib import fetch_ohlcv_for_hub
|
||||||
|
|
||||||
return fetch_ohlcv_for_hub(
|
return fetch_ohlcv_for_hub(
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
@@ -8981,7 +8982,7 @@ def _hub_fetch_ohlcv(symbol, timeframe, since_ms=None, limit=500):
|
|||||||
|
|
||||||
|
|
||||||
def _hub_fetch_volume_rank(top_n=20):
|
def _hub_fetch_volume_rank(top_n=20):
|
||||||
from hub_volume_rank_lib import fetch_usdt_swap_volume_rank
|
from lib.hub.hub_volume_rank_lib import fetch_usdt_swap_volume_rank
|
||||||
|
|
||||||
return fetch_usdt_swap_volume_rank(
|
return fetch_usdt_swap_volume_rank(
|
||||||
exchange=exchange,
|
exchange=exchange,
|
||||||
@@ -8998,7 +8999,7 @@ try:
|
|||||||
_repo_root = Path(__file__).resolve().parent.parent
|
_repo_root = Path(__file__).resolve().parent.parent
|
||||||
if str(_repo_root) not in sys.path:
|
if str(_repo_root) not in sys.path:
|
||||||
sys.path.insert(0, str(_repo_root))
|
sys.path.insert(0, str(_repo_root))
|
||||||
from hub_bridge import install_on_app
|
from lib.hub.hub_bridge import install_on_app
|
||||||
|
|
||||||
install_on_app(
|
install_on_app(
|
||||||
app,
|
app,
|
||||||
@@ -9045,8 +9046,8 @@ def strategy_roll_page():
|
|||||||
normalize_exchange_symbol = normalize_okx_symbol
|
normalize_exchange_symbol = normalize_okx_symbol
|
||||||
ensure_exchange_live_ready = ensure_okx_live_ready
|
ensure_exchange_live_ready = ensure_okx_live_ready
|
||||||
|
|
||||||
from strategy_register import install_strategy_trading
|
from lib.strategy.strategy_register import install_strategy_trading
|
||||||
from strategy_trend_register import install_strategy_trend
|
from lib.strategy.strategy_trend_register import install_strategy_trend
|
||||||
|
|
||||||
install_strategy_trading(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
install_strategy_trading(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
||||||
install_strategy_trend(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
install_strategy_trend(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
# lib/ 共用模块结构
|
||||||
|
|
||||||
|
四所实例与中控共用的 Python 库、模板与静态资源统一放在仓库根目录的 **`lib/`** 下。部署单元(`crypto_monitor_*`、`manual_trading_hub`)仍保持独立目录与 PM2 配置不变。
|
||||||
|
|
||||||
|
**重构前快照 Git 标签**:`pre-lib-modularization`(可用 `git checkout pre-lib-modularization` 查看旧布局)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 顶层目录
|
||||||
|
|
||||||
|
```
|
||||||
|
crypto_monitor/
|
||||||
|
├── crypto_monitor_binance/ # 四所:各自 app + .env + PM2
|
||||||
|
├── crypto_monitor_gate/
|
||||||
|
├── crypto_monitor_gate_bot/
|
||||||
|
├── crypto_monitor_okx/
|
||||||
|
├── manual_trading_hub/ # 中控 + 子代理 agent
|
||||||
|
│
|
||||||
|
├── lib/ # 共用模块(本说明)
|
||||||
|
│ ├── strategy/
|
||||||
|
│ ├── key_monitor/
|
||||||
|
│ ├── trade/
|
||||||
|
│ ├── hub/
|
||||||
|
│ ├── ai/
|
||||||
|
│ ├── instance/
|
||||||
|
│ ├── exchange/
|
||||||
|
│ ├── common/
|
||||||
|
│ └── paths.py
|
||||||
|
│
|
||||||
|
├── brand/ # 各所共用图标
|
||||||
|
├── docs/
|
||||||
|
├── deploy/
|
||||||
|
├── scripts/
|
||||||
|
├── tests/
|
||||||
|
├── requirements.txt
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## lib/ 子包说明
|
||||||
|
|
||||||
|
| 子包 | 职责 | 主要模块 |
|
||||||
|
|------|------|----------|
|
||||||
|
| **`lib/strategy/`** | 策略交易(顺势加仓、趋势回调、快照与记录) | `strategy_register.py`、`strategy_trend_register.py`、`strategy_db.py`、`strategy_roll_*`、`strategy_trend_*` |
|
||||||
|
| **`lib/strategy/templates/`** | 策略页 Jinja 模板(原 `strategy_templates/`) | `strategy_trading_page.html`、`strategy_roll_panel.html` 等 |
|
||||||
|
| **`lib/key_monitor/`** | 关键位监控、斐波、假突破、止盈止损方案 | `key_monitor_lib.py`、`fib_key_monitor_lib.py`、`key_sl_tp_lib.py` 等 |
|
||||||
|
| **`lib/trade/`** | 下单监控展示、计仓、账户风控、手动 SL/TP | `order_monitor_display_lib.py`、`position_sizing_lib.py`、`account_risk_lib.py` 等 |
|
||||||
|
| **`lib/hub/`** | 中控 API、K 线、归档、计仓器、SSO/Bridge | `hub_bridge.py`、`hub_kline_store.py`、`hub_trades_lib.py` 等 |
|
||||||
|
| **`lib/ai/`** | AI 复盘与文本生成 | `ai_client.py`、`ai_review_lib.py` |
|
||||||
|
| **`lib/instance/`** | 中控 iframe 嵌入、导航、复盘图表 | `instance_embed_lib.py`、`focus_chart_lib.py`、`journal_chart_lib.py` |
|
||||||
|
| **`lib/instance/templates/`** | 嵌入页片段(原 `embed_templates/`) | `embed_page_fragment.html` |
|
||||||
|
| **`lib/exchange/`** | 特定交易所工具 | `gate_transfer_lib.py`、`okx_orders_lib.py` 等 |
|
||||||
|
| **`lib/common/`** | 跨功能小工具 | `form_submit_lib.py`、`wechat_notify_lib.py` 等 |
|
||||||
|
| **`lib/common/static/`** | 四所与中控共用的 JS/CSS(原根目录 `static/`) | `instance_theme.js`、`strategy_roll.js` 等 |
|
||||||
|
|
||||||
|
> **说明**:`hub_*` 命名表示「中控侧能力或行情聚合」,但部分模块(如 `hub_volume_rank_lib`、`hub_market_info_lib`)四所 `app.py` 也会调用,并非中控独占。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 路径辅助函数
|
||||||
|
|
||||||
|
`lib/paths.py` 集中维护资源目录,避免硬编码:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from lib.paths import strategy_templates_dir, embed_templates_dir, common_static_dir
|
||||||
|
|
||||||
|
strategy_templates_dir() # .../lib/strategy/templates
|
||||||
|
embed_templates_dir() # .../lib/instance/templates
|
||||||
|
common_static_dir() # .../lib/common/static
|
||||||
|
```
|
||||||
|
|
||||||
|
可选传入 `repo_root`(字符串或 `Path`),默认使用 `lib/` 的上级目录即仓库根。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Python 导入约定
|
||||||
|
|
||||||
|
各部署目录在启动时将 **仓库根** 加入 `sys.path`(与重构前相同):
|
||||||
|
|
||||||
|
```python
|
||||||
|
_REPO_ROOT = os.path.dirname(BASE_DIR) # 或 Path(__file__).resolve().parent.parent
|
||||||
|
if _REPO_ROOT not in sys.path:
|
||||||
|
sys.path.insert(0, _REPO_ROOT)
|
||||||
|
```
|
||||||
|
|
||||||
|
之后使用 **`lib.<子包>.<模块>`** 形式导入,例如:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from lib.strategy.strategy_db import init_strategy_tables
|
||||||
|
from lib.key_monitor.key_monitor_lib import check_key_monitors
|
||||||
|
from lib.hub.hub_bridge import install_on_app
|
||||||
|
from lib.ai.ai_client import ai_review
|
||||||
|
```
|
||||||
|
|
||||||
|
策略注册仍在各所 `app.py` 末尾:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from lib.strategy.strategy_register import install_strategy_trading
|
||||||
|
from lib.strategy.strategy_trend_register import install_strategy_trend
|
||||||
|
|
||||||
|
install_strategy_trading(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
||||||
|
install_strategy_trend(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 静态资源与 URL
|
||||||
|
|
||||||
|
- 四所页面仍通过 **`/static/...`** 访问共用脚本;`hub_bridge.install_instance_theme_static` 从 `lib/common/static/` 提供部分根级静态路由。
|
||||||
|
- 各所目录下 **`static/`**(图标、上传图片等)仍为实例私有,未迁入 `lib/`。
|
||||||
|
- 中控 `manual_trading_hub/hub.py` 通过 `_REPO_ROOT / "lib" / "common" / "static"` 挂载与四所共用的 badge、复盘 JS 等。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
在仓库根执行(需将根目录置于 Python 路径,或从根目录运行):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/crypto_monitor
|
||||||
|
python -m unittest discover -s tests -p "test_*.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
测试文件内统一 `from lib.<子包>.<模块> import ...`。使用 `@patch` 时目标写完整模块路径,例如 `lib.hub.hub_calculator_lib._resolve_market`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 迁移脚本
|
||||||
|
|
||||||
|
一次性迁移由 `scripts/migrate_to_lib.py` 完成(移动文件 + 批量改写 import)。**不要在已迁移后的仓库上重复执行**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 后续可选整理
|
||||||
|
|
||||||
|
- 四所 `app.py` 体量接近,可逐步抽取公共 `exchange_app` 基座(改动面大,单独规划)。
|
||||||
|
- `manual_trading_hub/okx_orders_lib.py` 为 agent 本地副本,可与 `lib/exchange/okx_orders_lib.py` 合并去重。
|
||||||
|
- 可引入 `pyproject.toml` + `pip install -e .`,替代 `sys.path.insert`(长期维护更规范)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [README.md](../README.md) — 总览与部署
|
||||||
|
- [策略交易说明.md](../策略交易说明.md)
|
||||||
|
- [manual_trading_hub/使用说明.md](../manual_trading_hub/使用说明.md)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""crypto_monitor shared libraries."""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Shared library package."""
|
||||||
@@ -1,180 +1,180 @@
|
|||||||
"""AI 日复盘 / 周复盘:附图收集与 journal 文本格式化(四所共用)。"""
|
"""AI 日复盘 / 周复盘:附图收集与 journal 文本格式化(四所共用)。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Any, Callable, List, Mapping, Optional, Sequence
|
from typing import Any, Callable, List, Mapping, Optional, Sequence
|
||||||
|
|
||||||
from journal_chart_lib import (
|
from lib.instance.journal_chart_lib import (
|
||||||
JOURNAL_CHART_ANCHOR_CLOSE,
|
JOURNAL_CHART_ANCHOR_CLOSE,
|
||||||
JOURNAL_CHART_DEFAULT_LIMIT,
|
JOURNAL_CHART_DEFAULT_LIMIT,
|
||||||
JOURNAL_CHART_DEFAULT_TF1,
|
JOURNAL_CHART_DEFAULT_TF1,
|
||||||
JOURNAL_CHART_DEFAULT_TF2,
|
JOURNAL_CHART_DEFAULT_TF2,
|
||||||
normalize_chart_timeframe,
|
normalize_chart_timeframe,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _journal_nz(v: Any, default: str = "无") -> str:
|
def _journal_nz(v: Any, default: str = "无") -> str:
|
||||||
if v is None:
|
if v is None:
|
||||||
return default
|
return default
|
||||||
s = str(v).strip()
|
s = str(v).strip()
|
||||||
return s if s else default
|
return s if s else default
|
||||||
|
|
||||||
|
|
||||||
def _row_get(row: Any, key: str, default: Any = None) -> Any:
|
def _row_get(row: Any, key: str, default: Any = None) -> Any:
|
||||||
"""兼容 dict 与 sqlite3.Row(Row 无 .get 方法)。"""
|
"""兼容 dict 与 sqlite3.Row(Row 无 .get 方法)。"""
|
||||||
if row is None:
|
if row is None:
|
||||||
return default
|
return default
|
||||||
getter = getattr(row, "get", None)
|
getter = getattr(row, "get", None)
|
||||||
if callable(getter):
|
if callable(getter):
|
||||||
return getter(key, default)
|
return getter(key, default)
|
||||||
try:
|
try:
|
||||||
keys = row.keys() if hasattr(row, "keys") else ()
|
keys = row.keys() if hasattr(row, "keys") else ()
|
||||||
if key in keys:
|
if key in keys:
|
||||||
return row[key]
|
return row[key]
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
return row[key]
|
return row[key]
|
||||||
except (KeyError, TypeError, IndexError):
|
except (KeyError, TypeError, IndexError):
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
def journal_row_lines_for_ai(
|
def journal_row_lines_for_ai(
|
||||||
idx: int,
|
idx: int,
|
||||||
row: Any,
|
row: Any,
|
||||||
*,
|
*,
|
||||||
include_hold_duration: bool = True,
|
include_hold_duration: bool = True,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""把 journal 字段拼成给 AI 的文本;四所日复盘/周复盘共用。"""
|
"""把 journal 字段拼成给 AI 的文本;四所日复盘/周复盘共用。"""
|
||||||
lines = [
|
lines = [
|
||||||
(
|
(
|
||||||
f"{idx}. {_journal_nz(_row_get(row, 'coin'))} {_journal_nz(_row_get(row, 'tf'))} "
|
f"{idx}. {_journal_nz(_row_get(row, 'coin'))} {_journal_nz(_row_get(row, 'tf'))} "
|
||||||
f"| 盈亏:{_journal_nz(_row_get(row, 'pnl'))}U "
|
f"| 盈亏:{_journal_nz(_row_get(row, 'pnl'))}U "
|
||||||
f"| 实际RR:{_journal_nz(_row_get(row, 'real_rr'))} "
|
f"| 实际RR:{_journal_nz(_row_get(row, 'real_rr'))} "
|
||||||
f"| 预期RR:{_journal_nz(_row_get(row, 'expect_rr'))}"
|
f"| 预期RR:{_journal_nz(_row_get(row, 'expect_rr'))}"
|
||||||
),
|
),
|
||||||
f" 开仓逻辑:{_journal_nz(_row_get(row, 'entry_reason'))}",
|
f" 开仓逻辑:{_journal_nz(_row_get(row, 'entry_reason'))}",
|
||||||
f" 平仓/离场(交易员自述):{_journal_nz(_row_get(row, 'exit_reason'))}",
|
f" 平仓/离场(交易员自述):{_journal_nz(_row_get(row, 'exit_reason'))}",
|
||||||
]
|
]
|
||||||
if include_hold_duration:
|
if include_hold_duration:
|
||||||
lines.append(f" 持仓时长:{_journal_nz(_row_get(row, 'hold_duration'))}")
|
lines.append(f" 持仓时长:{_journal_nz(_row_get(row, 'hold_duration'))}")
|
||||||
ee_bits = [
|
ee_bits = [
|
||||||
_journal_nz(_row_get(row, "early_exit")),
|
_journal_nz(_row_get(row, "early_exit")),
|
||||||
_journal_nz(_row_get(row, "early_exit_reason")),
|
_journal_nz(_row_get(row, "early_exit_reason")),
|
||||||
_journal_nz(_row_get(row, "early_exit_trigger")),
|
_journal_nz(_row_get(row, "early_exit_trigger")),
|
||||||
_journal_nz(_row_get(row, "early_exit_note")),
|
_journal_nz(_row_get(row, "early_exit_note")),
|
||||||
]
|
]
|
||||||
if any(x != "无" for x in ee_bits):
|
if any(x != "无" for x in ee_bits):
|
||||||
lines.append(
|
lines.append(
|
||||||
" 提前离场记录:"
|
" 提前离场记录:"
|
||||||
f"{ee_bits[0]} | 原因:{ee_bits[1]} | 触发:{ee_bits[2]} | 备注:{ee_bits[3]}"
|
f"{ee_bits[0]} | 原因:{ee_bits[1]} | 触发:{ee_bits[2]} | 备注:{ee_bits[3]}"
|
||||||
)
|
)
|
||||||
mood_bits = f"心态标签:{_journal_nz(_row_get(row, 'mood_issues'))}"
|
mood_bits = f"心态标签:{_journal_nz(_row_get(row, 'mood_issues'))}"
|
||||||
mood_score = _row_get(row, "mood_score")
|
mood_score = _row_get(row, "mood_score")
|
||||||
if mood_score is not None:
|
if mood_score is not None:
|
||||||
mood_bits += f" | 自评心态分:{mood_score}"
|
mood_bits += f" | 自评心态分:{mood_score}"
|
||||||
lines.append(f" {mood_bits}")
|
lines.append(f" {mood_bits}")
|
||||||
if _journal_nz(_row_get(row, "post_breakeven_stare")) != "无":
|
if _journal_nz(_row_get(row, "post_breakeven_stare")) != "无":
|
||||||
lines.append(f" 保本后盯盘:{_journal_nz(_row_get(row, 'post_breakeven_stare'))}")
|
lines.append(f" 保本后盯盘:{_journal_nz(_row_get(row, 'post_breakeven_stare'))}")
|
||||||
if _journal_nz(_row_get(row, "new_trade_while_occupied")) != "无":
|
if _journal_nz(_row_get(row, "new_trade_while_occupied")) != "无":
|
||||||
lines.append(f" 占用时新开仓:{_journal_nz(_row_get(row, 'new_trade_while_occupied'))}")
|
lines.append(f" 占用时新开仓:{_journal_nz(_row_get(row, 'new_trade_while_occupied'))}")
|
||||||
if _journal_nz(_row_get(row, "note")) != "无":
|
if _journal_nz(_row_get(row, "note")) != "无":
|
||||||
lines.append(f" 备注:{_journal_nz(_row_get(row, 'note'))}")
|
lines.append(f" 备注:{_journal_nz(_row_get(row, 'note'))}")
|
||||||
return "\n".join(lines) + "\n"
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
def collect_images_for_ai_review(
|
def collect_images_for_ai_review(
|
||||||
rows: Sequence,
|
rows: Sequence,
|
||||||
upload_folder: str,
|
upload_folder: str,
|
||||||
*,
|
*,
|
||||||
build_chart_if_missing: Optional[Callable] = None,
|
build_chart_if_missing: Optional[Callable] = None,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""
|
"""
|
||||||
收集传给视觉模型的本地图片路径。
|
收集传给视觉模型的本地图片路径。
|
||||||
- 优先 journal_entries.image 已存附图;
|
- 优先 journal_entries.image 已存附图;
|
||||||
- 若无附图且提供 build_chart_if_missing,则临时生成 K 线图。
|
- 若无附图且提供 build_chart_if_missing,则临时生成 K 线图。
|
||||||
"""
|
"""
|
||||||
paths: List[str] = []
|
paths: List[str] = []
|
||||||
seen = set()
|
seen = set()
|
||||||
upload_folder = os.path.abspath(upload_folder or "")
|
upload_folder = os.path.abspath(upload_folder or "")
|
||||||
for row in rows or []:
|
for row in rows or []:
|
||||||
candidate = None
|
candidate = None
|
||||||
try:
|
try:
|
||||||
keys = row.keys() if hasattr(row, "keys") else []
|
keys = row.keys() if hasattr(row, "keys") else []
|
||||||
except Exception:
|
except Exception:
|
||||||
keys = []
|
keys = []
|
||||||
img = row["image"] if "image" in keys else None
|
img = row["image"] if "image" in keys else None
|
||||||
if img:
|
if img:
|
||||||
candidate = os.path.join(upload_folder, str(img).strip())
|
candidate = os.path.join(upload_folder, str(img).strip())
|
||||||
elif build_chart_if_missing:
|
elif build_chart_if_missing:
|
||||||
try:
|
try:
|
||||||
candidate = build_chart_if_missing(row)
|
candidate = build_chart_if_missing(row)
|
||||||
except Exception:
|
except Exception:
|
||||||
candidate = None
|
candidate = None
|
||||||
if not candidate:
|
if not candidate:
|
||||||
continue
|
continue
|
||||||
candidate = os.path.abspath(candidate)
|
candidate = os.path.abspath(candidate)
|
||||||
if os.path.isfile(candidate) and candidate not in seen:
|
if os.path.isfile(candidate) and candidate not in seen:
|
||||||
seen.add(candidate)
|
seen.add(candidate)
|
||||||
paths.append(candidate)
|
paths.append(candidate)
|
||||||
return paths
|
return paths
|
||||||
|
|
||||||
|
|
||||||
def build_journal_ai_chart_path(
|
def build_journal_ai_chart_path(
|
||||||
row,
|
row,
|
||||||
upload_folder: str,
|
upload_folder: str,
|
||||||
*,
|
*,
|
||||||
order_chart_enabled: bool,
|
order_chart_enabled: bool,
|
||||||
normalize_exchange_symbol_fn: Callable[[str], str],
|
normalize_exchange_symbol_fn: Callable[[str], str],
|
||||||
generate_chart_fn: Callable,
|
generate_chart_fn: Callable,
|
||||||
local_datetime_to_ms_fn: Callable[[str], Optional[int]],
|
local_datetime_to_ms_fn: Callable[[str], Optional[int]],
|
||||||
now_ts_ms_fn: Callable[[], int],
|
now_ts_ms_fn: Callable[[], int],
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
"""无已存附图时,按复盘记录开平仓时间临时生成 K 线图路径。"""
|
"""无已存附图时,按复盘记录开平仓时间临时生成 K 线图路径。"""
|
||||||
if not order_chart_enabled:
|
if not order_chart_enabled:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
keys = row.keys() if hasattr(row, "keys") else []
|
keys = row.keys() if hasattr(row, "keys") else []
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
coin = (row["coin"] if "coin" in keys else "") or ""
|
coin = (row["coin"] if "coin" in keys else "") or ""
|
||||||
coin = str(coin).strip()
|
coin = str(coin).strip()
|
||||||
if not coin:
|
if not coin:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
symbol = normalize_exchange_symbol_fn(coin)
|
symbol = normalize_exchange_symbol_fn(coin)
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
open_dt = row["open_datetime"] if "open_datetime" in keys else ""
|
open_dt = row["open_datetime"] if "open_datetime" in keys else ""
|
||||||
close_dt = row["close_datetime"] if "close_datetime" in keys else ""
|
close_dt = row["close_datetime"] if "close_datetime" in keys else ""
|
||||||
entry_ms = local_datetime_to_ms_fn(open_dt)
|
entry_ms = local_datetime_to_ms_fn(open_dt)
|
||||||
exit_ms = local_datetime_to_ms_fn(close_dt)
|
exit_ms = local_datetime_to_ms_fn(close_dt)
|
||||||
if not entry_ms:
|
if not entry_ms:
|
||||||
return None
|
return None
|
||||||
row_tf = row["tf"] if "tf" in keys else ""
|
row_tf = row["tf"] if "tf" in keys else ""
|
||||||
tf1 = normalize_chart_timeframe(row_tf) or JOURNAL_CHART_DEFAULT_TF1
|
tf1 = normalize_chart_timeframe(row_tf) or JOURNAL_CHART_DEFAULT_TF1
|
||||||
tf2 = JOURNAL_CHART_DEFAULT_TF2 if tf1 != JOURNAL_CHART_DEFAULT_TF2 else "1h"
|
tf2 = JOURNAL_CHART_DEFAULT_TF2 if tf1 != JOURNAL_CHART_DEFAULT_TF2 else "1h"
|
||||||
row_id = str(row["id"] if "id" in keys else "")[:8] or uuid.uuid4().hex[:8]
|
row_id = str(row["id"] if "id" in keys else "")[:8] or uuid.uuid4().hex[:8]
|
||||||
marker = {
|
marker = {
|
||||||
"entry_ts_ms": entry_ms,
|
"entry_ts_ms": entry_ms,
|
||||||
"exit_ts_ms": exit_ms,
|
"exit_ts_ms": exit_ms,
|
||||||
"chart_anchor": JOURNAL_CHART_ANCHOR_CLOSE,
|
"chart_anchor": JOURNAL_CHART_ANCHOR_CLOSE,
|
||||||
"now_ts_ms": int(now_ts_ms_fn()),
|
"now_ts_ms": int(now_ts_ms_fn()),
|
||||||
}
|
}
|
||||||
fname = f"ai_rev_{row_id}_{uuid.uuid4().hex[:6]}.png"
|
fname = f"ai_rev_{row_id}_{uuid.uuid4().hex[:6]}.png"
|
||||||
saved = generate_chart_fn(
|
saved = generate_chart_fn(
|
||||||
symbol,
|
symbol,
|
||||||
f"AI复盘 {coin}",
|
f"AI复盘 {coin}",
|
||||||
timeframes=[tf1, tf2],
|
timeframes=[tf1, tf2],
|
||||||
limit=JOURNAL_CHART_DEFAULT_LIMIT,
|
limit=JOURNAL_CHART_DEFAULT_LIMIT,
|
||||||
out_dir=upload_folder,
|
out_dir=upload_folder,
|
||||||
filename=fname,
|
filename=fname,
|
||||||
marker_payload=marker,
|
marker_payload=marker,
|
||||||
marker_timeframes={tf1, tf2},
|
marker_timeframes={tf1, tf2},
|
||||||
layout="vertical",
|
layout="vertical",
|
||||||
)
|
)
|
||||||
if not saved:
|
if not saved:
|
||||||
return None
|
return None
|
||||||
path = os.path.join(upload_folder, saved)
|
path = os.path.join(upload_folder, saved)
|
||||||
return path if os.path.isfile(path) else None
|
return path if os.path.isfile(path) else None
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Shared library package."""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Shared library package."""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Shared library package."""
|
||||||
@@ -1,36 +1,36 @@
|
|||||||
"""中控调用实例 API 时的鉴权(Flask request 头 X-Hub-Token)。SSO 见 hub_sso.py。"""
|
"""中控调用实例 API 时的鉴权(Flask request 头 X-Hub-Token)。SSO 见 hub_sso.py。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from hub_sso import (
|
from lib.hub.hub_sso import (
|
||||||
HUB_SSO_TTL_SEC,
|
HUB_SSO_TTL_SEC,
|
||||||
hub_bridge_token,
|
hub_bridge_token,
|
||||||
mint_hub_sso_token,
|
mint_hub_sso_token,
|
||||||
safe_next_path,
|
safe_next_path,
|
||||||
verify_hub_sso_token,
|
verify_hub_sso_token,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"HUB_SSO_TTL_SEC",
|
"HUB_SSO_TTL_SEC",
|
||||||
"hub_bridge_token",
|
"hub_bridge_token",
|
||||||
"mint_hub_sso_token",
|
"mint_hub_sso_token",
|
||||||
"safe_next_path",
|
"safe_next_path",
|
||||||
"verify_hub_sso_token",
|
"verify_hub_sso_token",
|
||||||
"request_allowed",
|
"request_allowed",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def request_allowed(session_logged_in: bool, auth_disabled: bool) -> bool:
|
def request_allowed(session_logged_in: bool, auth_disabled: bool) -> bool:
|
||||||
if auth_disabled or session_logged_in:
|
if auth_disabled or session_logged_in:
|
||||||
return True
|
return True
|
||||||
tok = hub_bridge_token()
|
tok = hub_bridge_token()
|
||||||
if not tok:
|
if not tok:
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
from flask import request
|
from flask import request
|
||||||
except ImportError:
|
except ImportError:
|
||||||
return False
|
return False
|
||||||
if request.headers.get("X-Hub-Token") == tok:
|
if request.headers.get("X-Hub-Token") == tok:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
+1027
-1025
File diff suppressed because it is too large
Load Diff
@@ -1,498 +1,498 @@
|
|||||||
"""中控历史测算:趋势回调 / 滚仓,以损定仓(按交易所精度与张数规则)。"""
|
"""中控历史测算:趋势回调 / 滚仓,以损定仓(按交易所精度与张数规则)。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Callable, Optional, Tuple
|
from typing import Any, Callable, Optional, Tuple
|
||||||
|
|
||||||
from strategy_roll_lib import max_roll_legs
|
from lib.strategy.strategy_roll_lib import max_roll_legs
|
||||||
from strategy_trend_lib import (
|
from lib.strategy.strategy_trend_lib import (
|
||||||
build_trend_preview_level_rows,
|
build_trend_preview_level_rows,
|
||||||
calc_risk_fraction,
|
calc_risk_fraction,
|
||||||
compute_trend_plan_core,
|
compute_trend_plan_core,
|
||||||
validate_trend_bounds,
|
validate_trend_bounds,
|
||||||
)
|
)
|
||||||
|
|
||||||
DEFAULT_DCA_LEGS = 5
|
DEFAULT_DCA_LEGS = 5
|
||||||
MARGIN_BUFFER = 0.95
|
MARGIN_BUFFER = 0.95
|
||||||
|
|
||||||
|
|
||||||
def _resolve_market(
|
def _resolve_market(
|
||||||
exchange_id: str,
|
exchange_id: str,
|
||||||
base: str,
|
base: str,
|
||||||
) -> Tuple[Optional[dict[str, Any]], Optional[Callable[[float], Optional[float]]], Optional[str]]:
|
) -> Tuple[Optional[dict[str, Any]], Optional[Callable[[float], Optional[float]]], Optional[str]]:
|
||||||
from hub_calculator_market_lib import get_calculator_market, make_amount_precise_fn_from_market
|
from lib.hub.hub_calculator_market_lib import get_calculator_market, make_amount_precise_fn_from_market
|
||||||
|
|
||||||
market, err = get_calculator_market(exchange_id, base)
|
market, err = get_calculator_market(exchange_id, base)
|
||||||
if err or not market:
|
if err or not market:
|
||||||
return None, None, err or "无法解析合约"
|
return None, None, err or "无法解析合约"
|
||||||
amount_precise = make_amount_precise_fn_from_market(market)
|
amount_precise = make_amount_precise_fn_from_market(market)
|
||||||
return market, amount_precise, None
|
return market, amount_precise, None
|
||||||
|
|
||||||
|
|
||||||
def calc_trend_calculator(
|
def calc_trend_calculator(
|
||||||
*,
|
*,
|
||||||
direction: str,
|
direction: str,
|
||||||
capital_usdt: float,
|
capital_usdt: float,
|
||||||
risk_percent: float,
|
risk_percent: float,
|
||||||
leverage: int,
|
leverage: int,
|
||||||
entry_price: float,
|
entry_price: float,
|
||||||
stop_loss: float,
|
stop_loss: float,
|
||||||
add_upper: float,
|
add_upper: float,
|
||||||
take_profit: float,
|
take_profit: float,
|
||||||
dca_legs: int = DEFAULT_DCA_LEGS,
|
dca_legs: int = DEFAULT_DCA_LEGS,
|
||||||
exchange_id: str = "0",
|
exchange_id: str = "0",
|
||||||
base: str = "ETH",
|
base: str = "ETH",
|
||||||
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
|
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
|
||||||
market, amount_precise, merr = _resolve_market(exchange_id, base)
|
market, amount_precise, merr = _resolve_market(exchange_id, base)
|
||||||
if merr or not market or not amount_precise:
|
if merr or not market or not amount_precise:
|
||||||
return None, merr or "无法解析合约"
|
return None, merr or "无法解析合约"
|
||||||
contract_size = float(market.get("contract_size") or 1.0)
|
contract_size = float(market.get("contract_size") or 1.0)
|
||||||
exchange_symbol = market["exchange_symbol"]
|
exchange_symbol = market["exchange_symbol"]
|
||||||
|
|
||||||
direction = (direction or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
if direction not in ("long", "short"):
|
if direction not in ("long", "short"):
|
||||||
return None, "方向须为 long 或 short"
|
return None, "方向须为 long 或 short"
|
||||||
try:
|
try:
|
||||||
capital = float(capital_usdt)
|
capital = float(capital_usdt)
|
||||||
rp = float(risk_percent)
|
rp = float(risk_percent)
|
||||||
lev = int(leverage)
|
lev = int(leverage)
|
||||||
entry = float(entry_price)
|
entry = float(entry_price)
|
||||||
sl = float(stop_loss)
|
sl = float(stop_loss)
|
||||||
upper = float(add_upper)
|
upper = float(add_upper)
|
||||||
tp = float(take_profit)
|
tp = float(take_profit)
|
||||||
legs = max(1, int(dca_legs))
|
legs = max(1, int(dca_legs))
|
||||||
cs = float(contract_size) if contract_size else 1.0
|
cs = float(contract_size) if contract_size else 1.0
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None, "参数格式错误"
|
return None, "参数格式错误"
|
||||||
if capital <= 0 or rp <= 0 or lev <= 0 or entry <= 0 or sl <= 0 or upper <= 0 or tp <= 0:
|
if capital <= 0 or rp <= 0 or lev <= 0 or entry <= 0 or sl <= 0 or upper <= 0 or tp <= 0:
|
||||||
return None, "资金、风险、杠杆与价格须大于 0"
|
return None, "资金、风险、杠杆与价格须大于 0"
|
||||||
|
|
||||||
bound_err = validate_trend_bounds(direction, sl, upper)
|
bound_err = validate_trend_bounds(direction, sl, upper)
|
||||||
if bound_err:
|
if bound_err:
|
||||||
return None, bound_err
|
return None, bound_err
|
||||||
|
|
||||||
rf = calc_risk_fraction(direction, upper, sl)
|
rf = calc_risk_fraction(direction, upper, sl)
|
||||||
if rf is None or rf <= 0:
|
if rf is None or rf <= 0:
|
||||||
return None, "止损与补仓区间边界组合无法计算风险比例"
|
return None, "止损与补仓区间边界组合无法计算风险比例"
|
||||||
|
|
||||||
risk_budget = capital * (rp / 100.0)
|
risk_budget = capital * (rp / 100.0)
|
||||||
notional = risk_budget / rf
|
notional = risk_budget / rf
|
||||||
margin_plan = min(notional / float(lev), capital * MARGIN_BUFFER)
|
margin_plan = min(notional / float(lev), capital * MARGIN_BUFFER)
|
||||||
if margin_plan <= 0:
|
if margin_plan <= 0:
|
||||||
return None, "计划保证金过小"
|
return None, "计划保证金过小"
|
||||||
|
|
||||||
target_amt = _amount_from_margin(margin_plan, lev, entry, cs)
|
target_amt = _amount_from_margin(margin_plan, lev, entry, cs)
|
||||||
if target_amt is None or target_amt <= 0:
|
if target_amt is None or target_amt <= 0:
|
||||||
return None, "无法计算计划张数,请检查入场价与杠杆"
|
return None, "无法计算计划张数,请检查入场价与杠杆"
|
||||||
target_amt = amount_precise(target_amt)
|
target_amt = amount_precise(target_amt)
|
||||||
if target_amt is None or target_amt <= 0:
|
if target_amt is None or target_amt <= 0:
|
||||||
return None, "计划张数低于交易所最小精度"
|
return None, "计划张数低于交易所最小精度"
|
||||||
|
|
||||||
def _amount_precise(_symbol: str, amount: float) -> Optional[float]:
|
def _amount_precise(_symbol: str, amount: float) -> Optional[float]:
|
||||||
return amount_precise(amount)
|
return amount_precise(amount)
|
||||||
|
|
||||||
payload, err = compute_trend_plan_core(
|
payload, err = compute_trend_plan_core(
|
||||||
direction=direction,
|
direction=direction,
|
||||||
stop_loss=sl,
|
stop_loss=sl,
|
||||||
add_upper=upper,
|
add_upper=upper,
|
||||||
risk_percent=rp,
|
risk_percent=rp,
|
||||||
snapshot_usdt=capital,
|
snapshot_usdt=capital,
|
||||||
leverage=lev,
|
leverage=lev,
|
||||||
live_price=entry,
|
live_price=entry,
|
||||||
target_order_amount=target_amt,
|
target_order_amount=target_amt,
|
||||||
exchange_symbol=exchange_symbol,
|
exchange_symbol=exchange_symbol,
|
||||||
dca_legs=legs,
|
dca_legs=legs,
|
||||||
amount_precise=_amount_precise,
|
amount_precise=_amount_precise,
|
||||||
min_amount=float(market.get("min_amount") or 0.0),
|
min_amount=float(market.get("min_amount") or 0.0),
|
||||||
full_margin_buffer_ratio=MARGIN_BUFFER,
|
full_margin_buffer_ratio=MARGIN_BUFFER,
|
||||||
)
|
)
|
||||||
if err:
|
if err:
|
||||||
return None, err
|
return None, err
|
||||||
|
|
||||||
payload["take_profit"] = tp
|
payload["take_profit"] = tp
|
||||||
payload["leverage"] = lev
|
payload["leverage"] = lev
|
||||||
payload["contract_size"] = cs
|
payload["contract_size"] = cs
|
||||||
preview, rows = build_trend_preview_level_rows(payload)
|
preview, rows = build_trend_preview_level_rows(payload)
|
||||||
|
|
||||||
px_dec = int(market.get("price_decimals") or 4)
|
px_dec = int(market.get("price_decimals") or 4)
|
||||||
amt_dec = int(market.get("amount_decimals") or 4)
|
amt_dec = int(market.get("amount_decimals") or 4)
|
||||||
|
|
||||||
def _f(v: Any, nd: int | None = None) -> Any:
|
def _f(v: Any, nd: int | None = None) -> Any:
|
||||||
if v is None:
|
if v is None:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
return round(float(v), nd if nd is not None else 8)
|
return round(float(v), nd if nd is not None else 8)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return v
|
return v
|
||||||
|
|
||||||
table = []
|
table = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
table.append(
|
table.append(
|
||||||
{
|
{
|
||||||
"label": row.get("label"),
|
"label": row.get("label"),
|
||||||
"price": _f(row.get("price"), px_dec),
|
"price": _f(row.get("price"), px_dec),
|
||||||
"contracts": _f(row.get("contracts"), amt_dec),
|
"contracts": _f(row.get("contracts"), amt_dec),
|
||||||
"avg_entry": _f(row.get("avg_entry"), px_dec),
|
"avg_entry": _f(row.get("avg_entry"), px_dec),
|
||||||
"profit_u": _f(row.get("profit_u")),
|
"profit_u": _f(row.get("profit_u")),
|
||||||
"risk_u": _f(row.get("risk_u")),
|
"risk_u": _f(row.get("risk_u")),
|
||||||
"rr": _f(row.get("rr"), 4),
|
"rr": _f(row.get("rr"), 4),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"direction": direction,
|
"direction": direction,
|
||||||
"capital_usdt": _f(capital),
|
"capital_usdt": _f(capital),
|
||||||
"risk_percent": _f(rp, 2),
|
"risk_percent": _f(rp, 2),
|
||||||
"risk_budget_u": _f(preview.get("preview_risk_amount_u")),
|
"risk_budget_u": _f(preview.get("preview_risk_amount_u")),
|
||||||
"leverage": lev,
|
"leverage": lev,
|
||||||
"entry_price": _f(entry, px_dec),
|
"entry_price": _f(entry, px_dec),
|
||||||
"stop_loss": _f(sl, px_dec),
|
"stop_loss": _f(sl, px_dec),
|
||||||
"add_upper": _f(upper, px_dec),
|
"add_upper": _f(upper, px_dec),
|
||||||
"take_profit": _f(tp, px_dec),
|
"take_profit": _f(tp, px_dec),
|
||||||
"plan_margin_u": _f(preview.get("plan_margin_capital")),
|
"plan_margin_u": _f(preview.get("plan_margin_capital")),
|
||||||
"target_contracts": _f(preview.get("target_order_amount"), amt_dec),
|
"target_contracts": _f(preview.get("target_order_amount"), amt_dec),
|
||||||
"first_contracts": _f(preview.get("first_order_amount"), amt_dec),
|
"first_contracts": _f(preview.get("first_order_amount"), amt_dec),
|
||||||
"dca_legs": int(preview.get("dca_legs") or legs),
|
"dca_legs": int(preview.get("dca_legs") or legs),
|
||||||
"first_profit_u": _f(preview.get("preview_first_profit_u")),
|
"first_profit_u": _f(preview.get("preview_first_profit_u")),
|
||||||
"first_rr": _f(preview.get("preview_target_rr"), 4),
|
"first_rr": _f(preview.get("preview_target_rr"), 4),
|
||||||
"market": market,
|
"market": market,
|
||||||
"rows": table,
|
"rows": table,
|
||||||
}, None
|
}, None
|
||||||
|
|
||||||
|
|
||||||
def _amount_from_margin(
|
def _amount_from_margin(
|
||||||
margin_capital: float,
|
margin_capital: float,
|
||||||
leverage: int,
|
leverage: int,
|
||||||
price: float,
|
price: float,
|
||||||
contract_size: float,
|
contract_size: float,
|
||||||
) -> Optional[float]:
|
) -> Optional[float]:
|
||||||
try:
|
try:
|
||||||
margin = float(margin_capital)
|
margin = float(margin_capital)
|
||||||
lev = int(leverage)
|
lev = int(leverage)
|
||||||
px = float(price)
|
px = float(price)
|
||||||
cs = float(contract_size) if contract_size else 1.0
|
cs = float(contract_size) if contract_size else 1.0
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None
|
return None
|
||||||
if margin <= 0 or lev <= 0 or px <= 0 or cs <= 0:
|
if margin <= 0 or lev <= 0 or px <= 0 or cs <= 0:
|
||||||
return None
|
return None
|
||||||
notional = margin * lev
|
notional = margin * lev
|
||||||
return notional / (px * cs)
|
return notional / (px * cs)
|
||||||
|
|
||||||
|
|
||||||
def _round(v: Any, nd: int = 4) -> Any:
|
def _round(v: Any, nd: int = 4) -> Any:
|
||||||
if v is None:
|
if v is None:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
return round(float(v), nd)
|
return round(float(v), nd)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
def _money_rr(profit_u: Optional[float], risk_u: Optional[float]) -> Optional[float]:
|
def _money_rr(profit_u: Optional[float], risk_u: Optional[float]) -> Optional[float]:
|
||||||
try:
|
try:
|
||||||
if risk_u is None or float(risk_u) <= 0 or profit_u is None:
|
if risk_u is None or float(risk_u) <= 0 or profit_u is None:
|
||||||
return None
|
return None
|
||||||
return round(float(profit_u) / float(risk_u), 4)
|
return round(float(profit_u) / float(risk_u), 4)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def calc_initial_roll_qty(
|
def calc_initial_roll_qty(
|
||||||
direction: str,
|
direction: str,
|
||||||
entry_price: float,
|
entry_price: float,
|
||||||
stop_loss: float,
|
stop_loss: float,
|
||||||
risk_budget_usdt: float,
|
risk_budget_usdt: float,
|
||||||
contract_size: float = 1.0,
|
contract_size: float = 1.0,
|
||||||
) -> Tuple[Optional[float], Optional[str]]:
|
) -> Tuple[Optional[float], Optional[str]]:
|
||||||
"""首仓以损定仓:打到初始止损亏损 = 风险预算。"""
|
"""首仓以损定仓:打到初始止损亏损 = 风险预算。"""
|
||||||
try:
|
try:
|
||||||
entry = float(entry_price)
|
entry = float(entry_price)
|
||||||
sl = float(stop_loss)
|
sl = float(stop_loss)
|
||||||
budget = float(risk_budget_usdt)
|
budget = float(risk_budget_usdt)
|
||||||
cs = float(contract_size) if contract_size else 1.0
|
cs = float(contract_size) if contract_size else 1.0
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None, "参数格式错误"
|
return None, "参数格式错误"
|
||||||
if entry <= 0 or sl <= 0 or budget <= 0 or cs <= 0:
|
if entry <= 0 or sl <= 0 or budget <= 0 or cs <= 0:
|
||||||
return None, "入场价、止损与风险预算须大于 0"
|
return None, "入场价、止损与风险预算须大于 0"
|
||||||
direction = (direction or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
if direction == "short":
|
if direction == "short":
|
||||||
per_unit = (sl - entry) * cs
|
per_unit = (sl - entry) * cs
|
||||||
if per_unit <= 0:
|
if per_unit <= 0:
|
||||||
return None, "做空:止损价须高于首仓入场价"
|
return None, "做空:止损价须高于首仓入场价"
|
||||||
else:
|
else:
|
||||||
per_unit = (entry - sl) * cs
|
per_unit = (entry - sl) * cs
|
||||||
if per_unit <= 0:
|
if per_unit <= 0:
|
||||||
return None, "做多:止损价须低于首仓入场价"
|
return None, "做多:止损价须低于首仓入场价"
|
||||||
return budget / per_unit, None
|
return budget / per_unit, None
|
||||||
|
|
||||||
|
|
||||||
def solve_add_amount_for_total_risk(
|
def solve_add_amount_for_total_risk(
|
||||||
direction: str,
|
direction: str,
|
||||||
qty_existing: float,
|
qty_existing: float,
|
||||||
entry_existing: float,
|
entry_existing: float,
|
||||||
add_price: float,
|
add_price: float,
|
||||||
new_stop: float,
|
new_stop: float,
|
||||||
risk_budget_usdt: float,
|
risk_budget_usdt: float,
|
||||||
contract_size: float = 1.0,
|
contract_size: float = 1.0,
|
||||||
) -> Tuple[Optional[float], Optional[str]]:
|
) -> Tuple[Optional[float], Optional[str]]:
|
||||||
"""合并持仓打到新止损总亏损 = 风险预算,反推本次加仓张数。"""
|
"""合并持仓打到新止损总亏损 = 风险预算,反推本次加仓张数。"""
|
||||||
try:
|
try:
|
||||||
q1 = float(qty_existing)
|
q1 = float(qty_existing)
|
||||||
e1 = float(entry_existing)
|
e1 = float(entry_existing)
|
||||||
e2 = float(add_price)
|
e2 = float(add_price)
|
||||||
sl = float(new_stop)
|
sl = float(new_stop)
|
||||||
b = float(risk_budget_usdt)
|
b = float(risk_budget_usdt)
|
||||||
cs = float(contract_size) if contract_size else 1.0
|
cs = float(contract_size) if contract_size else 1.0
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None, "参数格式错误"
|
return None, "参数格式错误"
|
||||||
if q1 <= 0 or e1 <= 0 or e2 <= 0 or b <= 0 or cs <= 0:
|
if q1 <= 0 or e1 <= 0 or e2 <= 0 or b <= 0 or cs <= 0:
|
||||||
return None, "持仓或风险预算无效"
|
return None, "持仓或风险预算无效"
|
||||||
direction = (direction or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
if direction == "short":
|
if direction == "short":
|
||||||
denom = sl - e2
|
denom = sl - e2
|
||||||
numer = b / cs - q1 * (sl - e1)
|
numer = b / cs - q1 * (sl - e1)
|
||||||
if denom <= 0:
|
if denom <= 0:
|
||||||
return None, "做空:新止损须高于限价加仓价"
|
return None, "做空:新止损须高于限价加仓价"
|
||||||
else:
|
else:
|
||||||
denom = e2 - sl
|
denom = e2 - sl
|
||||||
numer = b / cs - q1 * (e1 - sl)
|
numer = b / cs - q1 * (e1 - sl)
|
||||||
if denom <= 0:
|
if denom <= 0:
|
||||||
return None, "做多:新止损须低于限价/市价加仓价"
|
return None, "做多:新止损须低于限价/市价加仓价"
|
||||||
q2 = numer / denom
|
q2 = numer / denom
|
||||||
if q2 <= 0:
|
if q2 <= 0:
|
||||||
return None, "按当前新止损与总风险%,无需加仓或无法再加(已满足风险上限)"
|
return None, "按当前新止损与总风险%,无需加仓或无法再加(已满足风险上限)"
|
||||||
return q2, None
|
return q2, None
|
||||||
|
|
||||||
|
|
||||||
def _roll_leg_preview(
|
def _roll_leg_preview(
|
||||||
*,
|
*,
|
||||||
direction: str,
|
direction: str,
|
||||||
qty_existing: float,
|
qty_existing: float,
|
||||||
entry_existing: float,
|
entry_existing: float,
|
||||||
take_profit: float,
|
take_profit: float,
|
||||||
add_price: float,
|
add_price: float,
|
||||||
new_stop_loss: float,
|
new_stop_loss: float,
|
||||||
risk_budget: float,
|
risk_budget: float,
|
||||||
contract_size: float,
|
contract_size: float,
|
||||||
amount_precise: Callable[[float], Optional[float]],
|
amount_precise: Callable[[float], Optional[float]],
|
||||||
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
|
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
|
||||||
direction = (direction or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
try:
|
try:
|
||||||
tp = float(take_profit)
|
tp = float(take_profit)
|
||||||
sl = float(new_stop_loss)
|
sl = float(new_stop_loss)
|
||||||
entry_add = float(add_price)
|
entry_add = float(add_price)
|
||||||
e1 = float(entry_existing)
|
e1 = float(entry_existing)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None, "止损/止盈格式错误"
|
return None, "止损/止盈格式错误"
|
||||||
if sl <= 0 or tp <= 0 or entry_add <= 0:
|
if sl <= 0 or tp <= 0 or entry_add <= 0:
|
||||||
return None, "止损与首仓止盈须大于0"
|
return None, "止损与首仓止盈须大于0"
|
||||||
if direction == "long":
|
if direction == "long":
|
||||||
if sl >= entry_add:
|
if sl >= entry_add:
|
||||||
return None, "做多:新止损须低于加仓价"
|
return None, "做多:新止损须低于加仓价"
|
||||||
if tp <= e1:
|
if tp <= e1:
|
||||||
return None, "做多:首仓止盈须高于当前持仓均价参考"
|
return None, "做多:首仓止盈须高于当前持仓均价参考"
|
||||||
else:
|
else:
|
||||||
if sl <= entry_add:
|
if sl <= entry_add:
|
||||||
return None, "做空:新止损须高于加仓价"
|
return None, "做空:新止损须高于加仓价"
|
||||||
if tp >= e1:
|
if tp >= e1:
|
||||||
return None, "做空:首仓止盈须低于当前持仓均价参考"
|
return None, "做空:首仓止盈须低于当前持仓均价参考"
|
||||||
|
|
||||||
q2_raw, err = solve_add_amount_for_total_risk(
|
q2_raw, err = solve_add_amount_for_total_risk(
|
||||||
direction,
|
direction,
|
||||||
qty_existing,
|
qty_existing,
|
||||||
entry_existing,
|
entry_existing,
|
||||||
entry_add,
|
entry_add,
|
||||||
sl,
|
sl,
|
||||||
risk_budget,
|
risk_budget,
|
||||||
contract_size,
|
contract_size,
|
||||||
)
|
)
|
||||||
if err:
|
if err:
|
||||||
return None, err
|
return None, err
|
||||||
q2 = amount_precise(float(q2_raw))
|
q2 = amount_precise(float(q2_raw))
|
||||||
if q2 is None or q2 <= 0:
|
if q2 is None or q2 <= 0:
|
||||||
return None, "加仓张数低于交易所最小精度"
|
return None, "加仓张数低于交易所最小精度"
|
||||||
new_qty = float(qty_existing) + float(q2)
|
new_qty = float(qty_existing) + float(q2)
|
||||||
new_avg = (float(qty_existing) * float(entry_existing) + float(q2) * entry_add) / new_qty
|
new_avg = (float(qty_existing) * float(entry_existing) + float(q2) * entry_add) / new_qty
|
||||||
cs = float(contract_size) if contract_size else 1.0
|
cs = float(contract_size) if contract_size else 1.0
|
||||||
if direction == "long":
|
if direction == "long":
|
||||||
loss_at_sl = (new_avg - sl) * new_qty * cs
|
loss_at_sl = (new_avg - sl) * new_qty * cs
|
||||||
reward_at_tp = (tp - new_avg) * new_qty * cs
|
reward_at_tp = (tp - new_avg) * new_qty * cs
|
||||||
else:
|
else:
|
||||||
loss_at_sl = (sl - new_avg) * new_qty * cs
|
loss_at_sl = (sl - new_avg) * new_qty * cs
|
||||||
reward_at_tp = (new_avg - tp) * new_qty * cs
|
reward_at_tp = (new_avg - tp) * new_qty * cs
|
||||||
return {
|
return {
|
||||||
"add_amount_raw": q2,
|
"add_amount_raw": q2,
|
||||||
"qty_after": new_qty,
|
"qty_after": new_qty,
|
||||||
"avg_entry_after": new_avg,
|
"avg_entry_after": new_avg,
|
||||||
"add_price": entry_add,
|
"add_price": entry_add,
|
||||||
"new_stop_loss": sl,
|
"new_stop_loss": sl,
|
||||||
"loss_at_sl_usdt": loss_at_sl,
|
"loss_at_sl_usdt": loss_at_sl,
|
||||||
"reward_at_tp_usdt": reward_at_tp,
|
"reward_at_tp_usdt": reward_at_tp,
|
||||||
}, None
|
}, None
|
||||||
|
|
||||||
|
|
||||||
def calc_roll_calculator(
|
def calc_roll_calculator(
|
||||||
*,
|
*,
|
||||||
direction: str,
|
direction: str,
|
||||||
capital_usdt: float,
|
capital_usdt: float,
|
||||||
risk_percent: float,
|
risk_percent: float,
|
||||||
entry_price: float,
|
entry_price: float,
|
||||||
stop_loss: float,
|
stop_loss: float,
|
||||||
take_profit: float,
|
take_profit: float,
|
||||||
add_legs: list[dict[str, float]] | None = None,
|
add_legs: list[dict[str, float]] | None = None,
|
||||||
legs_done: int = 0,
|
legs_done: int = 0,
|
||||||
exchange_id: str = "0",
|
exchange_id: str = "0",
|
||||||
base: str = "ETH",
|
base: str = "ETH",
|
||||||
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
|
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
|
||||||
"""
|
"""
|
||||||
滚仓历史测算:首仓自动以损定仓;止盈锁定首仓价;最多 3 次滚仓加仓。
|
滚仓历史测算:首仓自动以损定仓;止盈锁定首仓价;最多 3 次滚仓加仓。
|
||||||
add_legs: [{add_price, new_stop_loss}, ...],按顺序链式计算。
|
add_legs: [{add_price, new_stop_loss}, ...],按顺序链式计算。
|
||||||
legs_done: 已完成滚仓次数(仅标记,仍参与链式状态推进)。
|
legs_done: 已完成滚仓次数(仅标记,仍参与链式状态推进)。
|
||||||
"""
|
"""
|
||||||
market, amount_precise, merr = _resolve_market(exchange_id, base)
|
market, amount_precise, merr = _resolve_market(exchange_id, base)
|
||||||
if merr or not market or not amount_precise:
|
if merr or not market or not amount_precise:
|
||||||
return None, merr or "无法解析合约"
|
return None, merr or "无法解析合约"
|
||||||
contract_size = float(market.get("contract_size") or 1.0)
|
contract_size = float(market.get("contract_size") or 1.0)
|
||||||
px_dec = int(market.get("price_decimals") or 4)
|
px_dec = int(market.get("price_decimals") or 4)
|
||||||
amt_dec = int(market.get("amount_decimals") or 4)
|
amt_dec = int(market.get("amount_decimals") or 4)
|
||||||
|
|
||||||
direction = (direction or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
if direction not in ("long", "short"):
|
if direction not in ("long", "short"):
|
||||||
return None, "方向须为 long 或 short"
|
return None, "方向须为 long 或 short"
|
||||||
try:
|
try:
|
||||||
capital = float(capital_usdt)
|
capital = float(capital_usdt)
|
||||||
rp = float(risk_percent)
|
rp = float(risk_percent)
|
||||||
entry = float(entry_price)
|
entry = float(entry_price)
|
||||||
initial_sl = float(stop_loss)
|
initial_sl = float(stop_loss)
|
||||||
tp = float(take_profit)
|
tp = float(take_profit)
|
||||||
done = max(0, int(legs_done))
|
done = max(0, int(legs_done))
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None, "参数格式错误"
|
return None, "参数格式错误"
|
||||||
if capital <= 0 or rp <= 0 or entry <= 0 or initial_sl <= 0 or tp <= 0:
|
if capital <= 0 or rp <= 0 or entry <= 0 or initial_sl <= 0 or tp <= 0:
|
||||||
return None, "资金、风险与价格须大于 0"
|
return None, "资金、风险与价格须大于 0"
|
||||||
if done > max_roll_legs(direction):
|
if done > max_roll_legs(direction):
|
||||||
return None, f"已完成滚仓次数不能超过 {max_roll_legs(direction)} 次"
|
return None, f"已完成滚仓次数不能超过 {max_roll_legs(direction)} 次"
|
||||||
|
|
||||||
legs_in: list[dict[str, float]] = []
|
legs_in: list[dict[str, float]] = []
|
||||||
for raw in add_legs or []:
|
for raw in add_legs or []:
|
||||||
if not isinstance(raw, dict):
|
if not isinstance(raw, dict):
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
ap = float(raw.get("add_price"))
|
ap = float(raw.get("add_price"))
|
||||||
nsl = float(raw.get("new_stop_loss"))
|
nsl = float(raw.get("new_stop_loss"))
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None, "加仓价与新止损须为有效数字"
|
return None, "加仓价与新止损须为有效数字"
|
||||||
if ap <= 0 or nsl <= 0:
|
if ap <= 0 or nsl <= 0:
|
||||||
return None, "加仓价与新止损须大于 0"
|
return None, "加仓价与新止损须大于 0"
|
||||||
legs_in.append({"add_price": ap, "new_stop_loss": nsl})
|
legs_in.append({"add_price": ap, "new_stop_loss": nsl})
|
||||||
|
|
||||||
if done + len(legs_in) > max_roll_legs(direction):
|
if done + len(legs_in) > max_roll_legs(direction):
|
||||||
return None, f"已完成 {done} 次 + 待测算 {len(legs_in)} 次,合计不能超过 {max_roll_legs(direction)} 次滚仓"
|
return None, f"已完成 {done} 次 + 待测算 {len(legs_in)} 次,合计不能超过 {max_roll_legs(direction)} 次滚仓"
|
||||||
|
|
||||||
if direction == "long":
|
if direction == "long":
|
||||||
if tp <= entry:
|
if tp <= entry:
|
||||||
return None, "做多:止盈价须高于首仓入场价"
|
return None, "做多:止盈价须高于首仓入场价"
|
||||||
else:
|
else:
|
||||||
if tp >= entry:
|
if tp >= entry:
|
||||||
return None, "做空:止盈价须低于首仓入场价"
|
return None, "做空:止盈价须低于首仓入场价"
|
||||||
|
|
||||||
risk_budget = capital * (rp / 100.0)
|
risk_budget = capital * (rp / 100.0)
|
||||||
qty, err = calc_initial_roll_qty(direction, entry, initial_sl, risk_budget, contract_size)
|
qty, err = calc_initial_roll_qty(direction, entry, initial_sl, risk_budget, contract_size)
|
||||||
if err:
|
if err:
|
||||||
return None, err
|
return None, err
|
||||||
if qty is None or qty <= 0:
|
if qty is None or qty <= 0:
|
||||||
return None, "无法计算首仓张数"
|
return None, "无法计算首仓张数"
|
||||||
qty_p = amount_precise(float(qty))
|
qty_p = amount_precise(float(qty))
|
||||||
if qty_p is None or qty_p <= 0:
|
if qty_p is None or qty_p <= 0:
|
||||||
return None, "首仓张数低于交易所最小精度"
|
return None, "首仓张数低于交易所最小精度"
|
||||||
|
|
||||||
qty_f = float(qty_p)
|
qty_f = float(qty_p)
|
||||||
avg = entry
|
avg = entry
|
||||||
rows: list[dict[str, Any]] = []
|
rows: list[dict[str, Any]] = []
|
||||||
cs = contract_size
|
cs = contract_size
|
||||||
|
|
||||||
if direction == "long":
|
if direction == "long":
|
||||||
first_loss = (avg - initial_sl) * qty_f * cs
|
first_loss = (avg - initial_sl) * qty_f * cs
|
||||||
first_profit = (tp - avg) * qty_f * cs
|
first_profit = (tp - avg) * qty_f * cs
|
||||||
else:
|
else:
|
||||||
first_loss = (initial_sl - avg) * qty_f * cs
|
first_loss = (initial_sl - avg) * qty_f * cs
|
||||||
first_profit = (avg - tp) * qty_f * cs
|
first_profit = (avg - tp) * qty_f * cs
|
||||||
|
|
||||||
rows.append(
|
rows.append(
|
||||||
{
|
{
|
||||||
"label": "首仓",
|
"label": "首仓",
|
||||||
"leg_index": 0,
|
"leg_index": 0,
|
||||||
"already_done": False,
|
"already_done": False,
|
||||||
"entry_or_add_price": _round(entry, px_dec),
|
"entry_or_add_price": _round(entry, px_dec),
|
||||||
"stop_loss": _round(initial_sl, px_dec),
|
"stop_loss": _round(initial_sl, px_dec),
|
||||||
"add_contracts": _round(qty_f, amt_dec),
|
"add_contracts": _round(qty_f, amt_dec),
|
||||||
"total_contracts": _round(qty_f, amt_dec),
|
"total_contracts": _round(qty_f, amt_dec),
|
||||||
"avg_entry": _round(avg, px_dec),
|
"avg_entry": _round(avg, px_dec),
|
||||||
"take_profit": _round(tp, px_dec),
|
"take_profit": _round(tp, px_dec),
|
||||||
"loss_at_sl_u": _round(first_loss),
|
"loss_at_sl_u": _round(first_loss),
|
||||||
"profit_at_tp_u": _round(first_profit),
|
"profit_at_tp_u": _round(first_profit),
|
||||||
"rr": _money_rr(first_profit, first_loss),
|
"rr": _money_rr(first_profit, first_loss),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
current_qty = qty_f
|
current_qty = qty_f
|
||||||
current_avg = avg
|
current_avg = avg
|
||||||
|
|
||||||
for i, leg in enumerate(legs_in):
|
for i, leg in enumerate(legs_in):
|
||||||
leg_no = i + 1
|
leg_no = i + 1
|
||||||
preview, err = _roll_leg_preview(
|
preview, err = _roll_leg_preview(
|
||||||
direction=direction,
|
direction=direction,
|
||||||
qty_existing=current_qty,
|
qty_existing=current_qty,
|
||||||
entry_existing=current_avg,
|
entry_existing=current_avg,
|
||||||
take_profit=tp,
|
take_profit=tp,
|
||||||
add_price=leg["add_price"],
|
add_price=leg["add_price"],
|
||||||
new_stop_loss=leg["new_stop_loss"],
|
new_stop_loss=leg["new_stop_loss"],
|
||||||
risk_budget=risk_budget,
|
risk_budget=risk_budget,
|
||||||
contract_size=cs,
|
contract_size=cs,
|
||||||
amount_precise=amount_precise,
|
amount_precise=amount_precise,
|
||||||
)
|
)
|
||||||
if err:
|
if err:
|
||||||
return None, f"滚仓第 {leg_no} 次:{err}"
|
return None, f"滚仓第 {leg_no} 次:{err}"
|
||||||
if not preview:
|
if not preview:
|
||||||
return None, f"滚仓第 {leg_no} 次计算失败"
|
return None, f"滚仓第 {leg_no} 次计算失败"
|
||||||
|
|
||||||
current_qty = float(preview["qty_after"])
|
current_qty = float(preview["qty_after"])
|
||||||
current_avg = float(preview["avg_entry_after"])
|
current_avg = float(preview["avg_entry_after"])
|
||||||
loss = preview.get("loss_at_sl_usdt")
|
loss = preview.get("loss_at_sl_usdt")
|
||||||
reward = preview.get("reward_at_tp_usdt")
|
reward = preview.get("reward_at_tp_usdt")
|
||||||
rows.append(
|
rows.append(
|
||||||
{
|
{
|
||||||
"label": f"滚仓{leg_no}",
|
"label": f"滚仓{leg_no}",
|
||||||
"leg_index": leg_no,
|
"leg_index": leg_no,
|
||||||
"already_done": leg_no <= done,
|
"already_done": leg_no <= done,
|
||||||
"entry_or_add_price": _round(preview.get("add_price"), px_dec),
|
"entry_or_add_price": _round(preview.get("add_price"), px_dec),
|
||||||
"stop_loss": _round(preview.get("new_stop_loss"), px_dec),
|
"stop_loss": _round(preview.get("new_stop_loss"), px_dec),
|
||||||
"add_contracts": _round(preview.get("add_amount_raw"), amt_dec),
|
"add_contracts": _round(preview.get("add_amount_raw"), amt_dec),
|
||||||
"total_contracts": _round(current_qty, amt_dec),
|
"total_contracts": _round(current_qty, amt_dec),
|
||||||
"avg_entry": _round(current_avg, px_dec),
|
"avg_entry": _round(current_avg, px_dec),
|
||||||
"take_profit": _round(tp, px_dec),
|
"take_profit": _round(tp, px_dec),
|
||||||
"loss_at_sl_u": _round(loss),
|
"loss_at_sl_u": _round(loss),
|
||||||
"profit_at_tp_u": _round(reward),
|
"profit_at_tp_u": _round(reward),
|
||||||
"rr": _money_rr(reward, loss),
|
"rr": _money_rr(reward, loss),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
last = rows[-1]
|
last = rows[-1]
|
||||||
return {
|
return {
|
||||||
"direction": direction,
|
"direction": direction,
|
||||||
"capital_usdt": _round(capital),
|
"capital_usdt": _round(capital),
|
||||||
"risk_percent": _round(rp, 2),
|
"risk_percent": _round(rp, 2),
|
||||||
"risk_budget_u": _round(risk_budget),
|
"risk_budget_u": _round(risk_budget),
|
||||||
"entry_price": _round(entry, px_dec),
|
"entry_price": _round(entry, px_dec),
|
||||||
"stop_loss": _round(initial_sl, px_dec),
|
"stop_loss": _round(initial_sl, px_dec),
|
||||||
"take_profit": _round(tp, px_dec),
|
"take_profit": _round(tp, px_dec),
|
||||||
"legs_done": done,
|
"legs_done": done,
|
||||||
"roll_legs_planned": len(legs_in),
|
"roll_legs_planned": len(legs_in),
|
||||||
"first_contracts": _round(qty_f, amt_dec),
|
"first_contracts": _round(qty_f, amt_dec),
|
||||||
"final_contracts": last.get("total_contracts"),
|
"final_contracts": last.get("total_contracts"),
|
||||||
"final_avg_entry": last.get("avg_entry"),
|
"final_avg_entry": last.get("avg_entry"),
|
||||||
"final_loss_at_sl_u": last.get("loss_at_sl_u"),
|
"final_loss_at_sl_u": last.get("loss_at_sl_u"),
|
||||||
"final_profit_at_tp_u": last.get("profit_at_tp_u"),
|
"final_profit_at_tp_u": last.get("profit_at_tp_u"),
|
||||||
"final_rr": last.get("rr"),
|
"final_rr": last.get("rr"),
|
||||||
"market": market,
|
"market": market,
|
||||||
"rows": rows,
|
"rows": rows,
|
||||||
}, None
|
}, None
|
||||||
@@ -1,257 +1,257 @@
|
|||||||
"""计算器:从已配置交易实例读取 USDT 永续合约精度与张数规则。"""
|
"""计算器:从已配置交易实例读取 USDT 永续合约精度与张数规则。"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from typing import Any, Callable, Optional, Tuple
|
from typing import Any, Callable, Optional, Tuple
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from settings_store import enabled_exchanges, load_settings
|
from settings_store import enabled_exchanges, load_settings
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from manual_trading_hub.settings_store import enabled_exchanges, load_settings
|
from manual_trading_hub.settings_store import enabled_exchanges, load_settings
|
||||||
|
|
||||||
MARKET_CACHE: dict[str, tuple[float, dict[str, Any]]] = {}
|
MARKET_CACHE: dict[str, tuple[float, dict[str, Any]]] = {}
|
||||||
MARKET_LOCK = threading.Lock()
|
MARKET_LOCK = threading.Lock()
|
||||||
MARKET_TTL_SEC = 300.0
|
MARKET_TTL_SEC = 300.0
|
||||||
HUB_FLASK_TIMEOUT = float(__import__("os").getenv("HUB_FLASK_TIMEOUT", "20"))
|
HUB_FLASK_TIMEOUT = float(__import__("os").getenv("HUB_FLASK_TIMEOUT", "20"))
|
||||||
|
|
||||||
|
|
||||||
def normalize_base_symbol(text: str) -> str:
|
def normalize_base_symbol(text: str) -> str:
|
||||||
s = str(text or "").upper().strip()
|
s = str(text or "").upper().strip()
|
||||||
for suf in ("USDT:USDT", "/USDT:USDT", "/USDT", "USDT", "-USDT-SWAP"):
|
for suf in ("USDT:USDT", "/USDT:USDT", "/USDT", "USDT", "-USDT-SWAP"):
|
||||||
if s.endswith(suf) and len(s) > len(suf):
|
if s.endswith(suf) and len(s) > len(suf):
|
||||||
s = s[: -len(suf)].strip("-/")
|
s = s[: -len(suf)].strip("-/")
|
||||||
break
|
break
|
||||||
if "/" in s:
|
if "/" in s:
|
||||||
s = s.split("/", 1)[0].strip()
|
s = s.split("/", 1)[0].strip()
|
||||||
if ":" in s:
|
if ":" in s:
|
||||||
s = s.split(":", 1)[0].strip()
|
s = s.split(":", 1)[0].strip()
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
def resolve_usdt_perp_symbol(exchange: Any, base: str) -> Tuple[Optional[str], Optional[str]]:
|
def resolve_usdt_perp_symbol(exchange: Any, base: str) -> Tuple[Optional[str], Optional[str]]:
|
||||||
base_u = normalize_base_symbol(base)
|
base_u = normalize_base_symbol(base)
|
||||||
if not base_u:
|
if not base_u:
|
||||||
return None, "请输入币种,如 ETH"
|
return None, "请输入币种,如 ETH"
|
||||||
candidates = [f"{base_u}/USDT:USDT", f"{base_u}/USDT"]
|
candidates = [f"{base_u}/USDT:USDT", f"{base_u}/USDT"]
|
||||||
markets = getattr(exchange, "markets", None) or {}
|
markets = getattr(exchange, "markets", None) or {}
|
||||||
for sym in candidates:
|
for sym in candidates:
|
||||||
m = markets.get(sym)
|
m = markets.get(sym)
|
||||||
if not m:
|
if not m:
|
||||||
continue
|
continue
|
||||||
if m.get("active") is False:
|
if m.get("active") is False:
|
||||||
continue
|
continue
|
||||||
if m.get("swap") or m.get("linear") or m.get("contract"):
|
if m.get("swap") or m.get("linear") or m.get("contract"):
|
||||||
return sym, None
|
return sym, None
|
||||||
for sym, m in markets.items():
|
for sym, m in markets.items():
|
||||||
if m.get("active") is False:
|
if m.get("active") is False:
|
||||||
continue
|
continue
|
||||||
if not (m.get("swap") or m.get("linear")):
|
if not (m.get("swap") or m.get("linear")):
|
||||||
continue
|
continue
|
||||||
if (m.get("quote") or "").upper() != "USDT":
|
if (m.get("quote") or "").upper() != "USDT":
|
||||||
continue
|
continue
|
||||||
if (m.get("base") or "").upper() == base_u:
|
if (m.get("base") or "").upper() == base_u:
|
||||||
return sym, None
|
return sym, None
|
||||||
return None, f"未找到 {base_u}/USDT 永续合约"
|
return None, f"未找到 {base_u}/USDT 永续合约"
|
||||||
|
|
||||||
|
|
||||||
def _decimals_from_precision_value(value: Any) -> Optional[int]:
|
def _decimals_from_precision_value(value: Any) -> Optional[int]:
|
||||||
if value in (None, ""):
|
if value in (None, ""):
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
p = float(value)
|
p = float(value)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None
|
return None
|
||||||
if p >= 1 and abs(p - round(p)) < 1e-9 and p <= 12:
|
if p >= 1 and abs(p - round(p)) < 1e-9 and p <= 12:
|
||||||
return int(round(p))
|
return int(round(p))
|
||||||
if 0 < p < 1:
|
if 0 < p < 1:
|
||||||
s = f"{p:.12f}".rstrip("0")
|
s = f"{p:.12f}".rstrip("0")
|
||||||
if "." in s:
|
if "." in s:
|
||||||
return min(12, len(s.split(".", 1)[1]))
|
return min(12, len(s.split(".", 1)[1]))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _decimals_from_ccxt_str(text: str) -> int:
|
def _decimals_from_ccxt_str(text: str) -> int:
|
||||||
s = str(text or "").strip()
|
s = str(text or "").strip()
|
||||||
if not s or "." not in s:
|
if not s or "." not in s:
|
||||||
return 0
|
return 0
|
||||||
frac = s.split(".", 1)[1]
|
frac = s.split(".", 1)[1]
|
||||||
if not frac:
|
if not frac:
|
||||||
return 0
|
return 0
|
||||||
return min(12, len(frac.rstrip("0") or frac))
|
return min(12, len(frac.rstrip("0") or frac))
|
||||||
|
|
||||||
|
|
||||||
def amount_decimals_from_exchange(exchange: Any, exchange_symbol: str) -> int:
|
def amount_decimals_from_exchange(exchange: Any, exchange_symbol: str) -> int:
|
||||||
try:
|
try:
|
||||||
return _decimals_from_ccxt_str(exchange.amount_to_precision(exchange_symbol, 1.23456789))
|
return _decimals_from_ccxt_str(exchange.amount_to_precision(exchange_symbol, 1.23456789))
|
||||||
except Exception:
|
except Exception:
|
||||||
market = exchange.market(exchange_symbol)
|
market = exchange.market(exchange_symbol)
|
||||||
prec = (market.get("precision") or {}).get("amount")
|
prec = (market.get("precision") or {}).get("amount")
|
||||||
d = _decimals_from_precision_value(prec)
|
d = _decimals_from_precision_value(prec)
|
||||||
return d if d is not None else 4
|
return d if d is not None else 4
|
||||||
|
|
||||||
|
|
||||||
def price_decimals_from_exchange(
|
def price_decimals_from_exchange(
|
||||||
exchange: Any, exchange_symbol: str, price_tick: Optional[float]
|
exchange: Any, exchange_symbol: str, price_tick: Optional[float]
|
||||||
) -> int:
|
) -> int:
|
||||||
from hub_ohlcv_lib import normalize_price_tick
|
from lib.hub.hub_ohlcv_lib import normalize_price_tick
|
||||||
|
|
||||||
tick = normalize_price_tick(price_tick)
|
tick = normalize_price_tick(price_tick)
|
||||||
if tick and tick > 0:
|
if tick and tick > 0:
|
||||||
if tick >= 1:
|
if tick >= 1:
|
||||||
return 0
|
return 0
|
||||||
s = f"{tick:.12f}".rstrip("0")
|
s = f"{tick:.12f}".rstrip("0")
|
||||||
if "." in s:
|
if "." in s:
|
||||||
return min(12, len(s.split(".", 1)[1]))
|
return min(12, len(s.split(".", 1)[1]))
|
||||||
try:
|
try:
|
||||||
return _decimals_from_ccxt_str(exchange.price_to_precision(exchange_symbol, 12345.678901234))
|
return _decimals_from_ccxt_str(exchange.price_to_precision(exchange_symbol, 12345.678901234))
|
||||||
except Exception:
|
except Exception:
|
||||||
market = exchange.market(exchange_symbol)
|
market = exchange.market(exchange_symbol)
|
||||||
prec = (market.get("precision") or {}).get("price")
|
prec = (market.get("precision") or {}).get("price")
|
||||||
d = _decimals_from_precision_value(prec)
|
d = _decimals_from_precision_value(prec)
|
||||||
return d if d is not None else 4
|
return d if d is not None else 4
|
||||||
|
|
||||||
|
|
||||||
def make_amount_precise_fn_from_market(market: dict[str, Any]) -> Callable[[float], Optional[float]]:
|
def make_amount_precise_fn_from_market(market: dict[str, Any]) -> Callable[[float], Optional[float]]:
|
||||||
dec = max(0, int(market.get("amount_decimals") or 4))
|
dec = max(0, int(market.get("amount_decimals") or 4))
|
||||||
min_amt = market.get("min_amount")
|
min_amt = market.get("min_amount")
|
||||||
|
|
||||||
def _fn(amount: float) -> Optional[float]:
|
def _fn(amount: float) -> Optional[float]:
|
||||||
try:
|
try:
|
||||||
v = float(amount)
|
v = float(amount)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None
|
return None
|
||||||
if v <= 0:
|
if v <= 0:
|
||||||
return None
|
return None
|
||||||
factor = 10**dec
|
factor = 10**dec
|
||||||
v = int(v * factor + 1e-12) / factor
|
v = int(v * factor + 1e-12) / factor
|
||||||
if min_amt is not None:
|
if min_amt is not None:
|
||||||
try:
|
try:
|
||||||
if v < float(min_amt):
|
if v < float(min_amt):
|
||||||
return None
|
return None
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
pass
|
pass
|
||||||
if v <= 0:
|
if v <= 0:
|
||||||
return None
|
return None
|
||||||
return v
|
return v
|
||||||
|
|
||||||
return _fn
|
return _fn
|
||||||
|
|
||||||
|
|
||||||
def find_exchange(exchange_id: str) -> dict | None:
|
def find_exchange(exchange_id: str) -> dict | None:
|
||||||
needle = str(exchange_id or "").strip()
|
needle = str(exchange_id or "").strip()
|
||||||
if not needle:
|
if not needle:
|
||||||
return None
|
return None
|
||||||
for ex in load_settings().get("exchanges") or []:
|
for ex in load_settings().get("exchanges") or []:
|
||||||
if str(ex.get("id") or "").strip() == needle:
|
if str(ex.get("id") or "").strip() == needle:
|
||||||
return ex
|
return ex
|
||||||
if str(ex.get("key") or "").strip().lower() == needle.lower():
|
if str(ex.get("key") or "").strip().lower() == needle.lower():
|
||||||
return ex
|
return ex
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def list_calculator_exchanges() -> list[dict[str, Any]]:
|
def list_calculator_exchanges() -> list[dict[str, Any]]:
|
||||||
rows: list[dict[str, Any]] = []
|
rows: list[dict[str, Any]] = []
|
||||||
for ex in enabled_exchanges():
|
for ex in enabled_exchanges():
|
||||||
rows.append(
|
rows.append(
|
||||||
{
|
{
|
||||||
"id": str(ex.get("id") or ""),
|
"id": str(ex.get("id") or ""),
|
||||||
"key": str(ex.get("key") or ""),
|
"key": str(ex.get("key") or ""),
|
||||||
"name": str(ex.get("name") or ex.get("key") or ""),
|
"name": str(ex.get("name") or ex.get("key") or ""),
|
||||||
"enabled": bool(ex.get("enabled")),
|
"enabled": bool(ex.get("enabled")),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
def _hub_headers() -> dict[str, str]:
|
def _hub_headers() -> dict[str, str]:
|
||||||
import os
|
import os
|
||||||
|
|
||||||
token = (os.getenv("HUB_BRIDGE_TOKEN") or os.getenv("CONTROL_TOKEN") or "").strip()
|
token = (os.getenv("HUB_BRIDGE_TOKEN") or os.getenv("CONTROL_TOKEN") or "").strip()
|
||||||
if token:
|
if token:
|
||||||
return {"X-Hub-Token": token}
|
return {"X-Hub-Token": token}
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def fetch_instance_market_sync(ex: dict, *, base: str) -> dict[str, Any]:
|
def fetch_instance_market_sync(ex: dict, *, base: str) -> dict[str, Any]:
|
||||||
base_url = (ex.get("flask_url") or "").rstrip("/")
|
base_url = (ex.get("flask_url") or "").rstrip("/")
|
||||||
if not base_url:
|
if not base_url:
|
||||||
return {"ok": False, "msg": "未配置 flask_url"}
|
return {"ok": False, "msg": "未配置 flask_url"}
|
||||||
params = urlencode({"base": normalize_base_symbol(base) or base})
|
params = urlencode({"base": normalize_base_symbol(base) or base})
|
||||||
url = f"{base_url}/api/hub/market?{params}"
|
url = f"{base_url}/api/hub/market?{params}"
|
||||||
req = urllib.request.Request(url, headers=_hub_headers(), method="GET")
|
req = urllib.request.Request(url, headers=_hub_headers(), method="GET")
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(req, timeout=HUB_FLASK_TIMEOUT) as resp:
|
with urllib.request.urlopen(req, timeout=HUB_FLASK_TIMEOUT) as resp:
|
||||||
status = int(getattr(resp, "status", 200) or 200)
|
status = int(getattr(resp, "status", 200) or 200)
|
||||||
raw = resp.read().decode("utf-8", errors="replace")
|
raw = resp.read().decode("utf-8", errors="replace")
|
||||||
data = json.loads(raw) if raw else {}
|
data = json.loads(raw) if raw else {}
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
return {"ok": False, "msg": "无效 JSON"}
|
return {"ok": False, "msg": "无效 JSON"}
|
||||||
if status >= 400:
|
if status >= 400:
|
||||||
data.setdefault("ok", False)
|
data.setdefault("ok", False)
|
||||||
return data
|
return data
|
||||||
except urllib.error.HTTPError as exc:
|
except urllib.error.HTTPError as exc:
|
||||||
try:
|
try:
|
||||||
raw = exc.read().decode("utf-8", errors="replace")
|
raw = exc.read().decode("utf-8", errors="replace")
|
||||||
body = json.loads(raw) if raw else {}
|
body = json.loads(raw) if raw else {}
|
||||||
except Exception:
|
except Exception:
|
||||||
body = {"ok": False, "msg": raw if "raw" in locals() else str(exc)}
|
body = {"ok": False, "msg": raw if "raw" in locals() else str(exc)}
|
||||||
if isinstance(body, dict):
|
if isinstance(body, dict):
|
||||||
body.setdefault("ok", False)
|
body.setdefault("ok", False)
|
||||||
return body
|
return body
|
||||||
return {"ok": False, "msg": f"HTTP {exc.code}"}
|
return {"ok": False, "msg": f"HTTP {exc.code}"}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return {"ok": False, "msg": str(exc)}
|
return {"ok": False, "msg": str(exc)}
|
||||||
|
|
||||||
|
|
||||||
def _enrich_market_from_settings(ex: dict, payload: dict[str, Any]) -> dict[str, Any]:
|
def _enrich_market_from_settings(ex: dict, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
out = dict(payload)
|
out = dict(payload)
|
||||||
out["exchange_id"] = str(ex.get("id") or "")
|
out["exchange_id"] = str(ex.get("id") or "")
|
||||||
out["exchange_key"] = str(ex.get("key") or "")
|
out["exchange_key"] = str(ex.get("key") or "")
|
||||||
out["exchange_name"] = str(ex.get("name") or ex.get("key") or "")
|
out["exchange_name"] = str(ex.get("name") or ex.get("key") or "")
|
||||||
out["exchange_label"] = out["exchange_name"]
|
out["exchange_label"] = out["exchange_name"]
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def get_calculator_market(
|
def get_calculator_market(
|
||||||
exchange_id: str,
|
exchange_id: str,
|
||||||
base: str,
|
base: str,
|
||||||
*,
|
*,
|
||||||
ex: dict | None = None,
|
ex: dict | None = None,
|
||||||
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
|
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
|
||||||
"""从系统设置中的交易实例拉取合约精度(与实盘一致)。"""
|
"""从系统设置中的交易实例拉取合约精度(与实盘一致)。"""
|
||||||
row = ex or find_exchange(exchange_id)
|
row = ex or find_exchange(exchange_id)
|
||||||
if not row:
|
if not row:
|
||||||
return None, "未找到该交易所配置"
|
return None, "未找到该交易所配置"
|
||||||
if not row.get("enabled"):
|
if not row.get("enabled"):
|
||||||
return None, f"{row.get('name') or exchange_id} 未启用"
|
return None, f"{row.get('name') or exchange_id} 未启用"
|
||||||
|
|
||||||
base_u = normalize_base_symbol(base)
|
base_u = normalize_base_symbol(base)
|
||||||
if not base_u:
|
if not base_u:
|
||||||
return None, "请输入币种,如 ETH"
|
return None, "请输入币种,如 ETH"
|
||||||
|
|
||||||
cache_key = f"{row.get('id')}:{base_u}"
|
cache_key = f"{row.get('id')}:{base_u}"
|
||||||
now = time.time()
|
now = time.time()
|
||||||
with MARKET_LOCK:
|
with MARKET_LOCK:
|
||||||
cached = MARKET_CACHE.get(cache_key)
|
cached = MARKET_CACHE.get(cache_key)
|
||||||
if cached and now - cached[0] < MARKET_TTL_SEC:
|
if cached and now - cached[0] < MARKET_TTL_SEC:
|
||||||
return dict(cached[1]), None
|
return dict(cached[1]), None
|
||||||
|
|
||||||
remote = fetch_instance_market_sync(row, base=base_u)
|
remote = fetch_instance_market_sync(row, base=base_u)
|
||||||
if not remote.get("ok"):
|
if not remote.get("ok"):
|
||||||
return None, str(remote.get("msg") or "实例返回失败")
|
return None, str(remote.get("msg") or "实例返回失败")
|
||||||
|
|
||||||
data = _enrich_market_from_settings(row, remote)
|
data = _enrich_market_from_settings(row, remote)
|
||||||
with MARKET_LOCK:
|
with MARKET_LOCK:
|
||||||
MARKET_CACHE[cache_key] = (now, data)
|
MARKET_CACHE[cache_key] = (now, data)
|
||||||
return data, None
|
return data, None
|
||||||
|
|
||||||
|
|
||||||
def clear_market_cache() -> None:
|
def clear_market_cache() -> None:
|
||||||
with MARKET_LOCK:
|
with MARKET_LOCK:
|
||||||
MARKET_CACHE.clear()
|
MARKET_CACHE.clear()
|
||||||
@@ -1,407 +1,407 @@
|
|||||||
"""中控资金概况:分户日快照(180 交易日)、总资金曲线与回撤。"""
|
"""中控资金概况:分户日快照(180 交易日)、总资金曲线与回撤。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from hub_trades_lib import current_trading_day
|
from lib.hub.hub_trades_lib import current_trading_day
|
||||||
|
|
||||||
HUB_DIR = Path(__file__).resolve().parent / "manual_trading_hub"
|
HUB_DIR = Path(__file__).resolve().parent / "manual_trading_hub"
|
||||||
FUND_HISTORY_PATH = HUB_DIR / "hub_fund_history.json"
|
FUND_HISTORY_PATH = HUB_DIR / "hub_fund_history.json"
|
||||||
LEGACY_FUND_HISTORY_PATH = HUB_DIR / "hub_ai_fund_history.json"
|
LEGACY_FUND_HISTORY_PATH = HUB_DIR / "hub_ai_fund_history.json"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
FUND_HISTORY_DAYS = max(30, int(os.getenv("HUB_FUND_HISTORY_DAYS", "180") or "180"))
|
FUND_HISTORY_DAYS = max(30, int(os.getenv("HUB_FUND_HISTORY_DAYS", "180") or "180"))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
FUND_HISTORY_DAYS = 180
|
FUND_HISTORY_DAYS = 180
|
||||||
|
|
||||||
FUND_HISTORY_START_DAY = (os.getenv("HUB_FUND_HISTORY_START_DAY") or "2026-06-09").strip()[:10]
|
FUND_HISTORY_START_DAY = (os.getenv("HUB_FUND_HISTORY_START_DAY") or "2026-06-09").strip()[:10]
|
||||||
|
|
||||||
|
|
||||||
def fund_history_start_day() -> str:
|
def fund_history_start_day() -> str:
|
||||||
return FUND_HISTORY_START_DAY or "2026-06-09"
|
return FUND_HISTORY_START_DAY or "2026-06-09"
|
||||||
|
|
||||||
|
|
||||||
def _now_str() -> str:
|
def _now_str() -> str:
|
||||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
def _safe_float(value: Any) -> Optional[float]:
|
def _safe_float(value: Any) -> Optional[float]:
|
||||||
try:
|
try:
|
||||||
v = float(value)
|
v = float(value)
|
||||||
return v if v >= 0 else None
|
return v if v >= 0 else None
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def account_total_usdt(funding: Any, trading: Any) -> Optional[float]:
|
def account_total_usdt(funding: Any, trading: Any) -> Optional[float]:
|
||||||
"""资金户 + 交易户;任一侧缺失则不计入(返回 None)。"""
|
"""资金户 + 交易户;任一侧缺失则不计入(返回 None)。"""
|
||||||
fu = _safe_float(funding)
|
fu = _safe_float(funding)
|
||||||
tu = _safe_float(trading)
|
tu = _safe_float(trading)
|
||||||
if fu is None or tu is None:
|
if fu is None or tu is None:
|
||||||
return None
|
return None
|
||||||
return round(fu + tu, 4)
|
return round(fu + tu, 4)
|
||||||
|
|
||||||
|
|
||||||
def compute_drawdown(values: list[float]) -> dict[str, Any]:
|
def compute_drawdown(values: list[float]) -> dict[str, Any]:
|
||||||
"""基于资金权益序列计算峰值回撤(U 与 %)。"""
|
"""基于资金权益序列计算峰值回撤(U 与 %)。"""
|
||||||
peak = 0.0
|
peak = 0.0
|
||||||
max_dd_u = 0.0
|
max_dd_u = 0.0
|
||||||
peak_at_end = 0.0
|
peak_at_end = 0.0
|
||||||
for v in values:
|
for v in values:
|
||||||
if not isinstance(v, (int, float)):
|
if not isinstance(v, (int, float)):
|
||||||
continue
|
continue
|
||||||
fv = float(v)
|
fv = float(v)
|
||||||
if fv > peak:
|
if fv > peak:
|
||||||
peak = fv
|
peak = fv
|
||||||
dd = peak - fv
|
dd = peak - fv
|
||||||
if dd > max_dd_u:
|
if dd > max_dd_u:
|
||||||
max_dd_u = dd
|
max_dd_u = dd
|
||||||
peak_at_end = peak
|
peak_at_end = peak
|
||||||
max_dd_u = round(max_dd_u, 4)
|
max_dd_u = round(max_dd_u, 4)
|
||||||
peak_at_end = round(peak_at_end, 4)
|
peak_at_end = round(peak_at_end, 4)
|
||||||
max_dd_pct = round((max_dd_u / peak_at_end) * 100, 2) if peak_at_end > 0 else None
|
max_dd_pct = round((max_dd_u / peak_at_end) * 100, 2) if peak_at_end > 0 else None
|
||||||
return {
|
return {
|
||||||
"peak_usdt": peak_at_end,
|
"peak_usdt": peak_at_end,
|
||||||
"max_drawdown_u": max_dd_u,
|
"max_drawdown_u": max_dd_u,
|
||||||
"max_drawdown_pct": max_dd_pct,
|
"max_drawdown_pct": max_dd_pct,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _atomic_write(path: Path, data: dict) -> None:
|
def _atomic_write(path: Path, data: dict) -> None:
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
tmp = path.with_suffix(path.suffix + ".tmp")
|
tmp = path.with_suffix(path.suffix + ".tmp")
|
||||||
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
os.replace(tmp, path)
|
os.replace(tmp, path)
|
||||||
|
|
||||||
|
|
||||||
def _prune_days(
|
def _prune_days(
|
||||||
days: dict,
|
days: dict,
|
||||||
*,
|
*,
|
||||||
keep_days: int,
|
keep_days: int,
|
||||||
anchor_day: str,
|
anchor_day: str,
|
||||||
start_day: Optional[str] = None,
|
start_day: Optional[str] = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
try:
|
try:
|
||||||
anchor = datetime.strptime(anchor_day[:10], "%Y-%m-%d")
|
anchor = datetime.strptime(anchor_day[:10], "%Y-%m-%d")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
anchor = datetime.now()
|
anchor = datetime.now()
|
||||||
rolling_cutoff = (anchor - timedelta(days=max(1, keep_days) - 1)).strftime("%Y-%m-%d")
|
rolling_cutoff = (anchor - timedelta(days=max(1, keep_days) - 1)).strftime("%Y-%m-%d")
|
||||||
start = (start_day or fund_history_start_day()).strip()[:10]
|
start = (start_day or fund_history_start_day()).strip()[:10]
|
||||||
cutoff = max(rolling_cutoff, start) if start else rolling_cutoff
|
cutoff = max(rolling_cutoff, start) if start else rolling_cutoff
|
||||||
return {k: v for k, v in (days or {}).items() if str(k) >= cutoff}
|
return {k: v for k, v in (days or {}).items() if str(k) >= cutoff}
|
||||||
|
|
||||||
|
|
||||||
def _migrate_legacy_store(days: dict) -> dict:
|
def _migrate_legacy_store(days: dict) -> dict:
|
||||||
if not LEGACY_FUND_HISTORY_PATH.is_file():
|
if not LEGACY_FUND_HISTORY_PATH.is_file():
|
||||||
return days
|
return days
|
||||||
try:
|
try:
|
||||||
loaded = json.loads(LEGACY_FUND_HISTORY_PATH.read_text(encoding="utf-8"))
|
loaded = json.loads(LEGACY_FUND_HISTORY_PATH.read_text(encoding="utf-8"))
|
||||||
legacy_days = loaded.get("days") if isinstance(loaded, dict) else {}
|
legacy_days = loaded.get("days") if isinstance(loaded, dict) else {}
|
||||||
if not isinstance(legacy_days, dict):
|
if not isinstance(legacy_days, dict):
|
||||||
return days
|
return days
|
||||||
merged = dict(days)
|
merged = dict(days)
|
||||||
for day, block in legacy_days.items():
|
for day, block in legacy_days.items():
|
||||||
if day in merged:
|
if day in merged:
|
||||||
continue
|
continue
|
||||||
if isinstance(block, dict) and block.get("accounts"):
|
if isinstance(block, dict) and block.get("accounts"):
|
||||||
merged[day] = block
|
merged[day] = block
|
||||||
return merged
|
return merged
|
||||||
except Exception:
|
except Exception:
|
||||||
return days
|
return days
|
||||||
|
|
||||||
|
|
||||||
def _load_store() -> dict:
|
def _load_store() -> dict:
|
||||||
if not FUND_HISTORY_PATH.is_file():
|
if not FUND_HISTORY_PATH.is_file():
|
||||||
store = {"version": 1, "days": _migrate_legacy_store({})}
|
store = {"version": 1, "days": _migrate_legacy_store({})}
|
||||||
if store["days"]:
|
if store["days"]:
|
||||||
_atomic_write(FUND_HISTORY_PATH, store)
|
_atomic_write(FUND_HISTORY_PATH, store)
|
||||||
return store
|
return store
|
||||||
try:
|
try:
|
||||||
loaded = json.loads(FUND_HISTORY_PATH.read_text(encoding="utf-8"))
|
loaded = json.loads(FUND_HISTORY_PATH.read_text(encoding="utf-8"))
|
||||||
if isinstance(loaded, dict):
|
if isinstance(loaded, dict):
|
||||||
loaded.setdefault("version", 1)
|
loaded.setdefault("version", 1)
|
||||||
days = dict(loaded.get("days") or {})
|
days = dict(loaded.get("days") or {})
|
||||||
loaded["days"] = _migrate_legacy_store(days)
|
loaded["days"] = _migrate_legacy_store(days)
|
||||||
return loaded
|
return loaded
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return {"version": 1, "days": {}}
|
return {"version": 1, "days": {}}
|
||||||
|
|
||||||
|
|
||||||
def record_fund_snapshot(
|
def record_fund_snapshot(
|
||||||
trading_day: str,
|
trading_day: str,
|
||||||
accounts: list[dict],
|
accounts: list[dict],
|
||||||
*,
|
*,
|
||||||
keep_days: int = FUND_HISTORY_DAYS,
|
keep_days: int = FUND_HISTORY_DAYS,
|
||||||
reset_hour: int = 8,
|
reset_hour: int = 8,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""写入当日各户资金账户/交易账户余额,并裁剪历史。"""
|
"""写入当日各户资金账户/交易账户余额,并裁剪历史。"""
|
||||||
day = (trading_day or "").strip()[:10] or current_trading_day(reset_hour=reset_hour)
|
day = (trading_day or "").strip()[:10] or current_trading_day(reset_hour=reset_hour)
|
||||||
start = fund_history_start_day()
|
start = fund_history_start_day()
|
||||||
if start and day < start:
|
if start and day < start:
|
||||||
return _load_store().get("days") or {}
|
return _load_store().get("days") or {}
|
||||||
store = _load_store()
|
store = _load_store()
|
||||||
days = dict(store.get("days") or {})
|
days = dict(store.get("days") or {})
|
||||||
row_accounts: dict[str, dict] = {}
|
row_accounts: dict[str, dict] = {}
|
||||||
for ac in accounts or []:
|
for ac in accounts or []:
|
||||||
key = str(ac.get("key") or ac.get("id") or "").strip()
|
key = str(ac.get("key") or ac.get("id") or "").strip()
|
||||||
if not key:
|
if not key:
|
||||||
continue
|
continue
|
||||||
if not ac.get("monitored"):
|
if not ac.get("monitored"):
|
||||||
continue
|
continue
|
||||||
fu = _safe_float(ac.get("funding_usdt"))
|
fu = _safe_float(ac.get("funding_usdt"))
|
||||||
tu = _safe_float(ac.get("trading_usdt"))
|
tu = _safe_float(ac.get("trading_usdt"))
|
||||||
total = account_total_usdt(fu, tu)
|
total = account_total_usdt(fu, tu)
|
||||||
if total is None:
|
if total is None:
|
||||||
continue
|
continue
|
||||||
row_accounts[key] = {
|
row_accounts[key] = {
|
||||||
"name": ac.get("name"),
|
"name": ac.get("name"),
|
||||||
"funding_usdt": fu,
|
"funding_usdt": fu,
|
||||||
"trading_usdt": tu,
|
"trading_usdt": tu,
|
||||||
"total_usdt": total,
|
"total_usdt": total,
|
||||||
"recorded_at": _now_str(),
|
"recorded_at": _now_str(),
|
||||||
}
|
}
|
||||||
if row_accounts:
|
if row_accounts:
|
||||||
days[day] = {"accounts": row_accounts, "updated_at": _now_str()}
|
days[day] = {"accounts": row_accounts, "updated_at": _now_str()}
|
||||||
days = _prune_days(
|
days = _prune_days(
|
||||||
days, keep_days=keep_days, anchor_day=day, start_day=fund_history_start_day()
|
days, keep_days=keep_days, anchor_day=day, start_day=fund_history_start_day()
|
||||||
)
|
)
|
||||||
_atomic_write(FUND_HISTORY_PATH, {"version": 1, "days": days})
|
_atomic_write(FUND_HISTORY_PATH, {"version": 1, "days": days})
|
||||||
return days
|
return days
|
||||||
|
|
||||||
|
|
||||||
def record_fund_snapshot_from_board(
|
def record_fund_snapshot_from_board(
|
||||||
rows: list[dict],
|
rows: list[dict],
|
||||||
*,
|
*,
|
||||||
keep_days: int = FUND_HISTORY_DAYS,
|
keep_days: int = FUND_HISTORY_DAYS,
|
||||||
reset_hour: int = 8,
|
reset_hour: int = 8,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""监控板行写入当日快照(仅 account_ok 且资金/交易户齐全)。"""
|
"""监控板行写入当日快照(仅 account_ok 且资金/交易户齐全)。"""
|
||||||
day = current_trading_day(reset_hour=reset_hour)
|
day = current_trading_day(reset_hour=reset_hour)
|
||||||
accounts = []
|
accounts = []
|
||||||
for row in rows or []:
|
for row in rows or []:
|
||||||
if not isinstance(row, dict):
|
if not isinstance(row, dict):
|
||||||
continue
|
continue
|
||||||
if not row.get("account_ok"):
|
if not row.get("account_ok"):
|
||||||
continue
|
continue
|
||||||
accounts.append(
|
accounts.append(
|
||||||
{
|
{
|
||||||
"key": row.get("key") or row.get("id"),
|
"key": row.get("key") or row.get("id"),
|
||||||
"name": row.get("name"),
|
"name": row.get("name"),
|
||||||
"funding_usdt": row.get("funding_usdt"),
|
"funding_usdt": row.get("funding_usdt"),
|
||||||
"trading_usdt": row.get("trading_usdt"),
|
"trading_usdt": row.get("trading_usdt"),
|
||||||
"monitored": True,
|
"monitored": True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return record_fund_snapshot(day, accounts, keep_days=keep_days, reset_hour=reset_hour)
|
return record_fund_snapshot(day, accounts, keep_days=keep_days, reset_hour=reset_hour)
|
||||||
|
|
||||||
|
|
||||||
def get_fund_history(*, anchor_day: str, keep_days: int = FUND_HISTORY_DAYS) -> dict[str, dict]:
|
def get_fund_history(*, anchor_day: str, keep_days: int = FUND_HISTORY_DAYS) -> dict[str, dict]:
|
||||||
store = _load_store()
|
store = _load_store()
|
||||||
return _prune_days(
|
return _prune_days(
|
||||||
dict(store.get("days") or {}),
|
dict(store.get("days") or {}),
|
||||||
keep_days=keep_days,
|
keep_days=keep_days,
|
||||||
anchor_day=anchor_day,
|
anchor_day=anchor_day,
|
||||||
start_day=fund_history_start_day(),
|
start_day=fund_history_start_day(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _exchange_monitored(ex: dict) -> bool:
|
def _exchange_monitored(ex: dict) -> bool:
|
||||||
return bool(ex.get("enabled")) and not bool(ex.get("env_disabled"))
|
return bool(ex.get("enabled")) and not bool(ex.get("env_disabled"))
|
||||||
|
|
||||||
|
|
||||||
def _live_row_for_exchange(ex: dict, rows_by_key: dict[str, dict]) -> Optional[dict]:
|
def _live_row_for_exchange(ex: dict, rows_by_key: dict[str, dict]) -> Optional[dict]:
|
||||||
key = str(ex.get("key") or "").strip()
|
key = str(ex.get("key") or "").strip()
|
||||||
if not key:
|
if not key:
|
||||||
return None
|
return None
|
||||||
return rows_by_key.get(key)
|
return rows_by_key.get(key)
|
||||||
|
|
||||||
|
|
||||||
def _series_from_history(
|
def _series_from_history(
|
||||||
history: dict[str, dict],
|
history: dict[str, dict],
|
||||||
account_keys: list[str],
|
account_keys: list[str],
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
out: list[dict[str, Any]] = []
|
out: list[dict[str, Any]] = []
|
||||||
for day in sorted(history.keys()):
|
for day in sorted(history.keys()):
|
||||||
block = history.get(day) or {}
|
block = history.get(day) or {}
|
||||||
ac_map = block.get("accounts") or {}
|
ac_map = block.get("accounts") or {}
|
||||||
total = 0.0
|
total = 0.0
|
||||||
n = 0
|
n = 0
|
||||||
for key in account_keys:
|
for key in account_keys:
|
||||||
ac = ac_map.get(key) or {}
|
ac = ac_map.get(key) or {}
|
||||||
t = account_total_usdt(ac.get("funding_usdt"), ac.get("trading_usdt"))
|
t = account_total_usdt(ac.get("funding_usdt"), ac.get("trading_usdt"))
|
||||||
if t is None:
|
if t is None:
|
||||||
t = _safe_float(ac.get("total_usdt"))
|
t = _safe_float(ac.get("total_usdt"))
|
||||||
if t is None:
|
if t is None:
|
||||||
continue
|
continue
|
||||||
total += t
|
total += t
|
||||||
n += 1
|
n += 1
|
||||||
if n > 0:
|
if n > 0:
|
||||||
out.append({"day": day, "total_usdt": round(total, 4)})
|
out.append({"day": day, "total_usdt": round(total, 4)})
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _account_series(history: dict[str, dict], key: str) -> list[dict[str, Any]]:
|
def _account_series(history: dict[str, dict], key: str) -> list[dict[str, Any]]:
|
||||||
out: list[dict[str, Any]] = []
|
out: list[dict[str, Any]] = []
|
||||||
for day in sorted(history.keys()):
|
for day in sorted(history.keys()):
|
||||||
ac = (history.get(day) or {}).get("accounts", {}).get(key) or {}
|
ac = (history.get(day) or {}).get("accounts", {}).get(key) or {}
|
||||||
t = account_total_usdt(ac.get("funding_usdt"), ac.get("trading_usdt"))
|
t = account_total_usdt(ac.get("funding_usdt"), ac.get("trading_usdt"))
|
||||||
if t is None:
|
if t is None:
|
||||||
t = _safe_float(ac.get("total_usdt"))
|
t = _safe_float(ac.get("total_usdt"))
|
||||||
if t is None:
|
if t is None:
|
||||||
continue
|
continue
|
||||||
out.append(
|
out.append(
|
||||||
{
|
{
|
||||||
"day": day,
|
"day": day,
|
||||||
"total_usdt": t,
|
"total_usdt": t,
|
||||||
"funding_usdt": _safe_float(ac.get("funding_usdt")),
|
"funding_usdt": _safe_float(ac.get("funding_usdt")),
|
||||||
"trading_usdt": _safe_float(ac.get("trading_usdt")),
|
"trading_usdt": _safe_float(ac.get("trading_usdt")),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def build_fund_overview(
|
def build_fund_overview(
|
||||||
exchanges: list[dict],
|
exchanges: list[dict],
|
||||||
*,
|
*,
|
||||||
board_rows: Optional[list[dict]] = None,
|
board_rows: Optional[list[dict]] = None,
|
||||||
trading_day: Optional[str] = None,
|
trading_day: Optional[str] = None,
|
||||||
keep_days: int = FUND_HISTORY_DAYS,
|
keep_days: int = FUND_HISTORY_DAYS,
|
||||||
reset_hour: int = 8,
|
reset_hour: int = 8,
|
||||||
updated_at: Optional[str] = None,
|
updated_at: Optional[str] = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
day = (trading_day or "").strip()[:10] or current_trading_day(reset_hour=reset_hour)
|
day = (trading_day or "").strip()[:10] or current_trading_day(reset_hour=reset_hour)
|
||||||
history = get_fund_history(anchor_day=day, keep_days=keep_days)
|
history = get_fund_history(anchor_day=day, keep_days=keep_days)
|
||||||
rows_by_key: dict[str, dict] = {}
|
rows_by_key: dict[str, dict] = {}
|
||||||
for row in board_rows or []:
|
for row in board_rows or []:
|
||||||
if isinstance(row, dict):
|
if isinstance(row, dict):
|
||||||
k = str(row.get("key") or "").strip()
|
k = str(row.get("key") or "").strip()
|
||||||
if k:
|
if k:
|
||||||
rows_by_key[k] = row
|
rows_by_key[k] = row
|
||||||
|
|
||||||
monitored_keys: list[str] = []
|
monitored_keys: list[str] = []
|
||||||
accounts_out: list[dict[str, Any]] = []
|
accounts_out: list[dict[str, Any]] = []
|
||||||
live_total = 0.0
|
live_total = 0.0
|
||||||
live_known = 0
|
live_known = 0
|
||||||
|
|
||||||
for ex in exchanges or []:
|
for ex in exchanges or []:
|
||||||
if not _exchange_monitored(ex):
|
if not _exchange_monitored(ex):
|
||||||
continue
|
continue
|
||||||
key = str(ex.get("key") or "").strip()
|
key = str(ex.get("key") or "").strip()
|
||||||
monitored = True
|
monitored = True
|
||||||
row = _live_row_for_exchange(ex, rows_by_key)
|
row = _live_row_for_exchange(ex, rows_by_key)
|
||||||
fu = tu = total = None
|
fu = tu = total = None
|
||||||
data_ok = False
|
data_ok = False
|
||||||
if row and row.get("account_ok"):
|
if row and row.get("account_ok"):
|
||||||
fu = _safe_float(row.get("funding_usdt"))
|
fu = _safe_float(row.get("funding_usdt"))
|
||||||
tu = _safe_float(row.get("trading_usdt"))
|
tu = _safe_float(row.get("trading_usdt"))
|
||||||
total = account_total_usdt(fu, tu)
|
total = account_total_usdt(fu, tu)
|
||||||
data_ok = total is not None
|
data_ok = total is not None
|
||||||
if data_ok:
|
if data_ok:
|
||||||
live_total += total
|
live_total += total
|
||||||
live_known += 1
|
live_known += 1
|
||||||
|
|
||||||
series = _account_series(history, key) if key else []
|
series = _account_series(history, key) if key else []
|
||||||
dd = compute_drawdown([p["total_usdt"] for p in series]) if series else {
|
dd = compute_drawdown([p["total_usdt"] for p in series]) if series else {
|
||||||
"peak_usdt": None,
|
"peak_usdt": None,
|
||||||
"max_drawdown_u": None,
|
"max_drawdown_u": None,
|
||||||
"max_drawdown_pct": None,
|
"max_drawdown_pct": None,
|
||||||
}
|
}
|
||||||
day_delta = None
|
day_delta = None
|
||||||
if series:
|
if series:
|
||||||
if len(series) >= 2:
|
if len(series) >= 2:
|
||||||
day_delta = round(series[-1]["total_usdt"] - series[-2]["total_usdt"], 4)
|
day_delta = round(series[-1]["total_usdt"] - series[-2]["total_usdt"], 4)
|
||||||
elif data_ok and total is not None:
|
elif data_ok and total is not None:
|
||||||
day_delta = round(total - series[-1]["total_usdt"], 4)
|
day_delta = round(total - series[-1]["total_usdt"], 4)
|
||||||
|
|
||||||
accounts_out.append(
|
accounts_out.append(
|
||||||
{
|
{
|
||||||
"id": ex.get("id"),
|
"id": ex.get("id"),
|
||||||
"key": key,
|
"key": key,
|
||||||
"name": ex.get("name") or key,
|
"name": ex.get("name") or key,
|
||||||
"monitored": monitored,
|
"monitored": monitored,
|
||||||
"data_ok": data_ok,
|
"data_ok": data_ok,
|
||||||
"funding_usdt": fu,
|
"funding_usdt": fu,
|
||||||
"trading_usdt": tu,
|
"trading_usdt": tu,
|
||||||
"total_usdt": total,
|
"total_usdt": total,
|
||||||
"series": series,
|
"series": series,
|
||||||
"drawdown": dd,
|
"drawdown": dd,
|
||||||
"day_delta_usdt": day_delta,
|
"day_delta_usdt": day_delta,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if key:
|
if key:
|
||||||
monitored_keys.append(key)
|
monitored_keys.append(key)
|
||||||
|
|
||||||
total_series = _series_from_history(history, monitored_keys)
|
total_series = _series_from_history(history, monitored_keys)
|
||||||
if live_known > 0:
|
if live_known > 0:
|
||||||
last_day = total_series[-1]["day"] if total_series else None
|
last_day = total_series[-1]["day"] if total_series else None
|
||||||
live_point = round(live_total, 4)
|
live_point = round(live_total, 4)
|
||||||
if last_day == day and total_series:
|
if last_day == day and total_series:
|
||||||
total_series[-1]["total_usdt"] = live_point
|
total_series[-1]["total_usdt"] = live_point
|
||||||
total_series[-1]["live"] = True
|
total_series[-1]["live"] = True
|
||||||
else:
|
else:
|
||||||
total_series.append({"day": day, "total_usdt": live_point, "live": True})
|
total_series.append({"day": day, "total_usdt": live_point, "live": True})
|
||||||
|
|
||||||
total_dd = compute_drawdown([p["total_usdt"] for p in total_series]) if total_series else {
|
total_dd = compute_drawdown([p["total_usdt"] for p in total_series]) if total_series else {
|
||||||
"peak_usdt": None,
|
"peak_usdt": None,
|
||||||
"max_drawdown_u": None,
|
"max_drawdown_u": None,
|
||||||
"max_drawdown_pct": None,
|
"max_drawdown_pct": None,
|
||||||
}
|
}
|
||||||
total_day_delta = None
|
total_day_delta = None
|
||||||
if total_series:
|
if total_series:
|
||||||
if len(total_series) >= 2:
|
if len(total_series) >= 2:
|
||||||
total_day_delta = round(
|
total_day_delta = round(
|
||||||
total_series[-1]["total_usdt"] - total_series[-2]["total_usdt"], 4
|
total_series[-1]["total_usdt"] - total_series[-2]["total_usdt"], 4
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"trading_day": day,
|
"trading_day": day,
|
||||||
"reset_hour": reset_hour,
|
"reset_hour": reset_hour,
|
||||||
"keep_days": keep_days,
|
"keep_days": keep_days,
|
||||||
"history_start_day": fund_history_start_day(),
|
"history_start_day": fund_history_start_day(),
|
||||||
"updated_at": updated_at,
|
"updated_at": updated_at,
|
||||||
"totals": {
|
"totals": {
|
||||||
"monitored_count": len(monitored_keys),
|
"monitored_count": len(monitored_keys),
|
||||||
"live_known_count": live_known,
|
"live_known_count": live_known,
|
||||||
"total_usdt": round(live_total, 4) if live_known > 0 else None,
|
"total_usdt": round(live_total, 4) if live_known > 0 else None,
|
||||||
"day_delta_usdt": total_day_delta,
|
"day_delta_usdt": total_day_delta,
|
||||||
"series": total_series,
|
"series": total_series,
|
||||||
"drawdown": total_dd,
|
"drawdown": total_dd,
|
||||||
},
|
},
|
||||||
"accounts": accounts_out,
|
"accounts": accounts_out,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def format_fund_history_text(
|
def format_fund_history_text(
|
||||||
history: dict[str, dict],
|
history: dict[str, dict],
|
||||||
*,
|
*,
|
||||||
account_names: Optional[dict[str, str]] = None,
|
account_names: Optional[dict[str, str]] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
if not history:
|
if not history:
|
||||||
return "(暂无资金历史快照)"
|
return "(暂无资金历史快照)"
|
||||||
names = account_names or {}
|
names = account_names or {}
|
||||||
lines = ["【资金快照(资金账户 + 交易账户 USDT)】"]
|
lines = ["【资金快照(资金账户 + 交易账户 USDT)】"]
|
||||||
for day in sorted(history.keys()):
|
for day in sorted(history.keys()):
|
||||||
block = history.get(day) or {}
|
block = history.get(day) or {}
|
||||||
ac_map = block.get("accounts") or {}
|
ac_map = block.get("accounts") or {}
|
||||||
if not ac_map:
|
if not ac_map:
|
||||||
continue
|
continue
|
||||||
parts = []
|
parts = []
|
||||||
for key, ac in ac_map.items():
|
for key, ac in ac_map.items():
|
||||||
label = names.get(key) or ac.get("name") or key
|
label = names.get(key) or ac.get("name") or key
|
||||||
fu = ac.get("funding_usdt")
|
fu = ac.get("funding_usdt")
|
||||||
tu = ac.get("trading_usdt")
|
tu = ac.get("trading_usdt")
|
||||||
tot = ac.get("total_usdt")
|
tot = ac.get("total_usdt")
|
||||||
if tot is None:
|
if tot is None:
|
||||||
tot = account_total_usdt(fu, tu)
|
tot = account_total_usdt(fu, tu)
|
||||||
fu_txt = f"{fu}U" if fu is not None else "未知"
|
fu_txt = f"{fu}U" if fu is not None else "未知"
|
||||||
tu_txt = f"{tu}U" if tu is not None else "未知"
|
tu_txt = f"{tu}U" if tu is not None else "未知"
|
||||||
tot_txt = f"{tot}U" if tot is not None else "未知"
|
tot_txt = f"{tot}U" if tot is not None else "未知"
|
||||||
parts.append(f"{label}: 合计{tot_txt}(资金{fu_txt}/交易{tu_txt})")
|
parts.append(f"{label}: 合计{tot_txt}(资金{fu_txt}/交易{tu_txt})")
|
||||||
lines.append(f"- {day}: " + ";".join(parts))
|
lines.append(f"- {day}: " + ";".join(parts))
|
||||||
return "\n".join(lines) if len(lines) > 1 else "(暂无资金历史快照)"
|
return "\n".join(lines) if len(lines) > 1 else "(暂无资金历史快照)"
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,311 +1,311 @@
|
|||||||
"""中控宏观关键数据日历:手动录入 FOMC / CPI / 非农档发布时间,±1h 风控前置窗口。"""
|
"""中控宏观关键数据日历:手动录入 FOMC / CPI / 非农档发布时间,±1h 风控前置窗口。"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from hub_symbol_archive_lib import parse_wall_clock_ms
|
from lib.hub.hub_symbol_archive_lib import parse_wall_clock_ms
|
||||||
|
|
||||||
DISPLAY_TZ = ZoneInfo(os.getenv("APP_TIMEZONE", "Asia/Shanghai"))
|
DISPLAY_TZ = ZoneInfo(os.getenv("APP_TIMEZONE", "Asia/Shanghai"))
|
||||||
|
|
||||||
MACRO_EVENT_TYPES = ("fomc", "cpi", "employment")
|
MACRO_EVENT_TYPES = ("fomc", "cpi", "employment")
|
||||||
|
|
||||||
MACRO_EVENT_LABELS: dict[str, str] = {
|
MACRO_EVENT_LABELS: dict[str, str] = {
|
||||||
"fomc": "FOMC 联邦基金利率",
|
"fomc": "FOMC 联邦基金利率",
|
||||||
"cpi": "美国 CPI 通胀",
|
"cpi": "美国 CPI 通胀",
|
||||||
"employment": "就业与劳工数据",
|
"employment": "就业与劳工数据",
|
||||||
}
|
}
|
||||||
|
|
||||||
WINDOW_BEFORE_MS = int(os.getenv("HUB_MACRO_WINDOW_BEFORE_SEC", str(3600))) * 1000
|
WINDOW_BEFORE_MS = int(os.getenv("HUB_MACRO_WINDOW_BEFORE_SEC", str(3600))) * 1000
|
||||||
WINDOW_AFTER_MS = int(os.getenv("HUB_MACRO_WINDOW_AFTER_SEC", str(3600))) * 1000
|
WINDOW_AFTER_MS = int(os.getenv("HUB_MACRO_WINDOW_AFTER_SEC", str(3600))) * 1000
|
||||||
IMMINENT_BEFORE_MS = int(os.getenv("HUB_MACRO_IMMINENT_BEFORE_SEC", str(1800))) * 1000
|
IMMINENT_BEFORE_MS = int(os.getenv("HUB_MACRO_IMMINENT_BEFORE_SEC", str(1800))) * 1000
|
||||||
LIST_FUTURE_DAYS = int(os.getenv("HUB_MACRO_LIST_FUTURE_DAYS", "60"))
|
LIST_FUTURE_DAYS = int(os.getenv("HUB_MACRO_LIST_FUTURE_DAYS", "60"))
|
||||||
|
|
||||||
|
|
||||||
def default_db_path() -> Path:
|
def default_db_path() -> Path:
|
||||||
raw = (os.getenv("HUB_MACRO_CALENDAR_DB_PATH") or "").strip()
|
raw = (os.getenv("HUB_MACRO_CALENDAR_DB_PATH") or "").strip()
|
||||||
if raw:
|
if raw:
|
||||||
return Path(raw)
|
return Path(raw)
|
||||||
hub_dir = Path(__file__).resolve().parent / "manual_trading_hub" / "data"
|
hub_dir = Path(__file__).resolve().parent / "manual_trading_hub" / "data"
|
||||||
hub_dir.mkdir(parents=True, exist_ok=True)
|
hub_dir.mkdir(parents=True, exist_ok=True)
|
||||||
return hub_dir / "hub_macro_calendar.db"
|
return hub_dir / "hub_macro_calendar.db"
|
||||||
|
|
||||||
|
|
||||||
def _connect(db_path: Path | None = None) -> sqlite3.Connection:
|
def _connect(db_path: Path | None = None) -> sqlite3.Connection:
|
||||||
path = db_path or default_db_path()
|
path = db_path or default_db_path()
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
conn = sqlite3.connect(str(path), timeout=30, isolation_level=None)
|
conn = sqlite3.connect(str(path), timeout=30, isolation_level=None)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
conn.execute("PRAGMA journal_mode=WAL")
|
conn.execute("PRAGMA journal_mode=WAL")
|
||||||
conn.execute("PRAGMA synchronous=NORMAL")
|
conn.execute("PRAGMA synchronous=NORMAL")
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
|
|
||||||
def init_db(db_path: Path | None = None) -> None:
|
def init_db(db_path: Path | None = None) -> None:
|
||||||
conn = _connect(db_path)
|
conn = _connect(db_path)
|
||||||
try:
|
try:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS macro_events (
|
CREATE TABLE IF NOT EXISTS macro_events (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
event_type TEXT NOT NULL,
|
event_type TEXT NOT NULL,
|
||||||
event_at_ms INTEGER NOT NULL,
|
event_at_ms INTEGER NOT NULL,
|
||||||
note TEXT NOT NULL DEFAULT '',
|
note TEXT NOT NULL DEFAULT '',
|
||||||
created_at_ms INTEGER NOT NULL,
|
created_at_ms INTEGER NOT NULL,
|
||||||
updated_at_ms INTEGER NOT NULL
|
updated_at_ms INTEGER NOT NULL
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"CREATE INDEX IF NOT EXISTS idx_macro_events_at ON macro_events(event_at_ms)"
|
"CREATE INDEX IF NOT EXISTS idx_macro_events_at ON macro_events(event_at_ms)"
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def normalize_event_type(raw: str) -> str:
|
def normalize_event_type(raw: str) -> str:
|
||||||
key = (raw or "").strip().lower()
|
key = (raw or "").strip().lower()
|
||||||
if key not in MACRO_EVENT_TYPES:
|
if key not in MACRO_EVENT_TYPES:
|
||||||
raise ValueError(f"事件类型须为: {', '.join(MACRO_EVENT_LABELS.values())}")
|
raise ValueError(f"事件类型须为: {', '.join(MACRO_EVENT_LABELS.values())}")
|
||||||
return key
|
return key
|
||||||
|
|
||||||
|
|
||||||
def parse_event_at_ms(raw: Any) -> int:
|
def parse_event_at_ms(raw: Any) -> int:
|
||||||
ms = parse_wall_clock_ms(raw, tz=DISPLAY_TZ)
|
ms = parse_wall_clock_ms(raw, tz=DISPLAY_TZ)
|
||||||
if ms is None:
|
if ms is None:
|
||||||
raise ValueError("发布时间格式错误,请使用 YYYY-MM-DD HH:MM 或 YYYY-MM-DDTHH:MM")
|
raise ValueError("发布时间格式错误,请使用 YYYY-MM-DD HH:MM 或 YYYY-MM-DDTHH:MM")
|
||||||
return int(ms)
|
return int(ms)
|
||||||
|
|
||||||
|
|
||||||
def format_event_at(ms: int) -> str:
|
def format_event_at(ms: int) -> str:
|
||||||
dt = datetime.fromtimestamp(ms / 1000, tz=DISPLAY_TZ)
|
dt = datetime.fromtimestamp(ms / 1000, tz=DISPLAY_TZ)
|
||||||
return dt.strftime("%Y-%m-%d %H:%M")
|
return dt.strftime("%Y-%m-%d %H:%M")
|
||||||
|
|
||||||
|
|
||||||
def _row_to_dict(row: sqlite3.Row) -> dict[str, Any]:
|
def _row_to_dict(row: sqlite3.Row) -> dict[str, Any]:
|
||||||
ms = int(row["event_at_ms"])
|
ms = int(row["event_at_ms"])
|
||||||
et = str(row["event_type"])
|
et = str(row["event_type"])
|
||||||
return {
|
return {
|
||||||
"id": int(row["id"]),
|
"id": int(row["id"]),
|
||||||
"event_type": et,
|
"event_type": et,
|
||||||
"event_type_label": MACRO_EVENT_LABELS.get(et, et),
|
"event_type_label": MACRO_EVENT_LABELS.get(et, et),
|
||||||
"event_at_ms": ms,
|
"event_at_ms": ms,
|
||||||
"event_at": format_event_at(ms),
|
"event_at": format_event_at(ms),
|
||||||
"note": str(row["note"] or ""),
|
"note": str(row["note"] or ""),
|
||||||
"created_at_ms": int(row["created_at_ms"]),
|
"created_at_ms": int(row["created_at_ms"]),
|
||||||
"updated_at_ms": int(row["updated_at_ms"]),
|
"updated_at_ms": int(row["updated_at_ms"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _window_bounds(event_at_ms: int) -> tuple[int, int]:
|
def _window_bounds(event_at_ms: int) -> tuple[int, int]:
|
||||||
start = int(event_at_ms) - WINDOW_BEFORE_MS
|
start = int(event_at_ms) - WINDOW_BEFORE_MS
|
||||||
end = int(event_at_ms) + WINDOW_AFTER_MS
|
end = int(event_at_ms) + WINDOW_AFTER_MS
|
||||||
return start, end
|
return start, end
|
||||||
|
|
||||||
|
|
||||||
def enrich_alert(row: dict[str, Any], now_ms: int | None = None) -> dict[str, Any] | None:
|
def enrich_alert(row: dict[str, Any], now_ms: int | None = None) -> dict[str, Any] | None:
|
||||||
now = int(now_ms if now_ms is not None else time.time() * 1000)
|
now = int(now_ms if now_ms is not None else time.time() * 1000)
|
||||||
event_at_ms = int(row["event_at_ms"])
|
event_at_ms = int(row["event_at_ms"])
|
||||||
window_start, window_end = _window_bounds(event_at_ms)
|
window_start, window_end = _window_bounds(event_at_ms)
|
||||||
if now < window_start or now > window_end:
|
if now < window_start or now > window_end:
|
||||||
return None
|
return None
|
||||||
imminent = now >= (event_at_ms - IMMINENT_BEFORE_MS) and now <= window_end
|
imminent = now >= (event_at_ms - IMMINENT_BEFORE_MS) and now <= window_end
|
||||||
mins_to_event = max(0, int((event_at_ms - now) / 60000))
|
mins_to_event = max(0, int((event_at_ms - now) / 60000))
|
||||||
mins_from_event = max(0, int((now - event_at_ms) / 60000))
|
mins_from_event = max(0, int((now - event_at_ms) / 60000))
|
||||||
return {
|
return {
|
||||||
**row,
|
**row,
|
||||||
"window_start_ms": window_start,
|
"window_start_ms": window_start,
|
||||||
"window_end_ms": window_end,
|
"window_end_ms": window_end,
|
||||||
"window_start": format_event_at(window_start),
|
"window_start": format_event_at(window_start),
|
||||||
"window_end": format_event_at(window_end),
|
"window_end": format_event_at(window_end),
|
||||||
"phase": "imminent" if imminent else "window",
|
"phase": "imminent" if imminent else "window",
|
||||||
"phase_label": "即将发布" if imminent and now < event_at_ms else "高波动窗口",
|
"phase_label": "即将发布" if imminent and now < event_at_ms else "高波动窗口",
|
||||||
"minutes_to_event": mins_to_event if now < event_at_ms else 0,
|
"minutes_to_event": mins_to_event if now < event_at_ms else 0,
|
||||||
"minutes_from_event": mins_from_event if now >= event_at_ms else 0,
|
"minutes_from_event": mins_from_event if now >= event_at_ms else 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def list_events(
|
def list_events(
|
||||||
*,
|
*,
|
||||||
now_ms: int | None = None,
|
now_ms: int | None = None,
|
||||||
include_expired_hours: int = 24,
|
include_expired_hours: int = 24,
|
||||||
db_path: Path | None = None,
|
db_path: Path | None = None,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
init_db(db_path)
|
init_db(db_path)
|
||||||
now = int(now_ms if now_ms is not None else time.time() * 1000)
|
now = int(now_ms if now_ms is not None else time.time() * 1000)
|
||||||
horizon = now + LIST_FUTURE_DAYS * 86400 * 1000
|
horizon = now + LIST_FUTURE_DAYS * 86400 * 1000
|
||||||
expired_cutoff = now - max(0, int(include_expired_hours)) * 3600 * 1000 - WINDOW_AFTER_MS
|
expired_cutoff = now - max(0, int(include_expired_hours)) * 3600 * 1000 - WINDOW_AFTER_MS
|
||||||
conn = _connect(db_path)
|
conn = _connect(db_path)
|
||||||
try:
|
try:
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM macro_events
|
SELECT * FROM macro_events
|
||||||
WHERE event_at_ms >= ? AND event_at_ms <= ?
|
WHERE event_at_ms >= ? AND event_at_ms <= ?
|
||||||
ORDER BY event_at_ms ASC, id ASC
|
ORDER BY event_at_ms ASC, id ASC
|
||||||
""",
|
""",
|
||||||
(expired_cutoff, horizon),
|
(expired_cutoff, horizon),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [_row_to_dict(r) for r in rows]
|
return [_row_to_dict(r) for r in rows]
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def get_event(event_id: int, db_path: Path | None = None) -> dict[str, Any] | None:
|
def get_event(event_id: int, db_path: Path | None = None) -> dict[str, Any] | None:
|
||||||
init_db(db_path)
|
init_db(db_path)
|
||||||
conn = _connect(db_path)
|
conn = _connect(db_path)
|
||||||
try:
|
try:
|
||||||
row = conn.execute("SELECT * FROM macro_events WHERE id=?", (int(event_id),)).fetchone()
|
row = conn.execute("SELECT * FROM macro_events WHERE id=?", (int(event_id),)).fetchone()
|
||||||
return _row_to_dict(row) if row else None
|
return _row_to_dict(row) if row else None
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def _assert_no_duplicate(
|
def _assert_no_duplicate(
|
||||||
conn: sqlite3.Connection,
|
conn: sqlite3.Connection,
|
||||||
event_type: str,
|
event_type: str,
|
||||||
event_at_ms: int,
|
event_at_ms: int,
|
||||||
*,
|
*,
|
||||||
exclude_id: int | None = None,
|
exclude_id: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if exclude_id is None:
|
if exclude_id is None:
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"SELECT id FROM macro_events WHERE event_type=? AND event_at_ms=? LIMIT 1",
|
"SELECT id FROM macro_events WHERE event_type=? AND event_at_ms=? LIMIT 1",
|
||||||
(event_type, int(event_at_ms)),
|
(event_type, int(event_at_ms)),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
else:
|
else:
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT id FROM macro_events
|
SELECT id FROM macro_events
|
||||||
WHERE event_type=? AND event_at_ms=? AND id<>?
|
WHERE event_type=? AND event_at_ms=? AND id<>?
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""",
|
""",
|
||||||
(event_type, int(event_at_ms), int(exclude_id)),
|
(event_type, int(event_at_ms), int(exclude_id)),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if row:
|
if row:
|
||||||
raise ValueError("同类型、同发布时间的记录已存在")
|
raise ValueError("同类型、同发布时间的记录已存在")
|
||||||
|
|
||||||
|
|
||||||
def create_event(
|
def create_event(
|
||||||
event_type: str,
|
event_type: str,
|
||||||
event_at: Any,
|
event_at: Any,
|
||||||
*,
|
*,
|
||||||
note: str = "",
|
note: str = "",
|
||||||
db_path: Path | None = None,
|
db_path: Path | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
init_db(db_path)
|
init_db(db_path)
|
||||||
et = normalize_event_type(event_type)
|
et = normalize_event_type(event_type)
|
||||||
event_at_ms = parse_event_at_ms(event_at)
|
event_at_ms = parse_event_at_ms(event_at)
|
||||||
note_s = str(note or "").strip()[:500]
|
note_s = str(note or "").strip()[:500]
|
||||||
now_ms = int(time.time() * 1000)
|
now_ms = int(time.time() * 1000)
|
||||||
conn = _connect(db_path)
|
conn = _connect(db_path)
|
||||||
try:
|
try:
|
||||||
_assert_no_duplicate(conn, et, event_at_ms)
|
_assert_no_duplicate(conn, et, event_at_ms)
|
||||||
cur = conn.execute(
|
cur = conn.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO macro_events (event_type, event_at_ms, note, created_at_ms, updated_at_ms)
|
INSERT INTO macro_events (event_type, event_at_ms, note, created_at_ms, updated_at_ms)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(et, event_at_ms, note_s, now_ms, now_ms),
|
(et, event_at_ms, note_s, now_ms, now_ms),
|
||||||
)
|
)
|
||||||
eid = int(cur.lastrowid)
|
eid = int(cur.lastrowid)
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
row = get_event(eid, db_path=db_path)
|
row = get_event(eid, db_path=db_path)
|
||||||
assert row is not None
|
assert row is not None
|
||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
||||||
def update_event(
|
def update_event(
|
||||||
event_id: int,
|
event_id: int,
|
||||||
*,
|
*,
|
||||||
event_type: str | None = None,
|
event_type: str | None = None,
|
||||||
event_at: Any | None = None,
|
event_at: Any | None = None,
|
||||||
note: str | None = None,
|
note: str | None = None,
|
||||||
db_path: Path | None = None,
|
db_path: Path | None = None,
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
init_db(db_path)
|
init_db(db_path)
|
||||||
existing = get_event(event_id, db_path=db_path)
|
existing = get_event(event_id, db_path=db_path)
|
||||||
if not existing:
|
if not existing:
|
||||||
return None
|
return None
|
||||||
et = normalize_event_type(event_type if event_type is not None else existing["event_type"])
|
et = normalize_event_type(event_type if event_type is not None else existing["event_type"])
|
||||||
event_at_ms = (
|
event_at_ms = (
|
||||||
parse_event_at_ms(event_at) if event_at is not None else int(existing["event_at_ms"])
|
parse_event_at_ms(event_at) if event_at is not None else int(existing["event_at_ms"])
|
||||||
)
|
)
|
||||||
note_s = existing["note"] if note is None else str(note or "").strip()[:500]
|
note_s = existing["note"] if note is None else str(note or "").strip()[:500]
|
||||||
now_ms = int(time.time() * 1000)
|
now_ms = int(time.time() * 1000)
|
||||||
conn = _connect(db_path)
|
conn = _connect(db_path)
|
||||||
try:
|
try:
|
||||||
_assert_no_duplicate(conn, et, event_at_ms, exclude_id=int(event_id))
|
_assert_no_duplicate(conn, et, event_at_ms, exclude_id=int(event_id))
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE macro_events
|
UPDATE macro_events
|
||||||
SET event_type=?, event_at_ms=?, note=?, updated_at_ms=?
|
SET event_type=?, event_at_ms=?, note=?, updated_at_ms=?
|
||||||
WHERE id=?
|
WHERE id=?
|
||||||
""",
|
""",
|
||||||
(et, event_at_ms, note_s, now_ms, int(event_id)),
|
(et, event_at_ms, note_s, now_ms, int(event_id)),
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
return get_event(event_id, db_path=db_path)
|
return get_event(event_id, db_path=db_path)
|
||||||
|
|
||||||
|
|
||||||
def delete_event(event_id: int, db_path: Path | None = None) -> bool:
|
def delete_event(event_id: int, db_path: Path | None = None) -> bool:
|
||||||
init_db(db_path)
|
init_db(db_path)
|
||||||
conn = _connect(db_path)
|
conn = _connect(db_path)
|
||||||
try:
|
try:
|
||||||
cur = conn.execute("DELETE FROM macro_events WHERE id=?", (int(event_id),))
|
cur = conn.execute("DELETE FROM macro_events WHERE id=?", (int(event_id),))
|
||||||
return cur.rowcount > 0
|
return cur.rowcount > 0
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def list_active_alerts(
|
def list_active_alerts(
|
||||||
now_ms: int | None = None,
|
now_ms: int | None = None,
|
||||||
db_path: Path | None = None,
|
db_path: Path | None = None,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
now = int(now_ms if now_ms is not None else time.time() * 1000)
|
now = int(now_ms if now_ms is not None else time.time() * 1000)
|
||||||
lookback = now - WINDOW_BEFORE_MS - IMMINENT_BEFORE_MS
|
lookback = now - WINDOW_BEFORE_MS - IMMINENT_BEFORE_MS
|
||||||
lookahead = now + WINDOW_AFTER_MS
|
lookahead = now + WINDOW_AFTER_MS
|
||||||
init_db(db_path)
|
init_db(db_path)
|
||||||
conn = _connect(db_path)
|
conn = _connect(db_path)
|
||||||
try:
|
try:
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM macro_events
|
SELECT * FROM macro_events
|
||||||
WHERE event_at_ms >= ? AND event_at_ms <= ?
|
WHERE event_at_ms >= ? AND event_at_ms <= ?
|
||||||
ORDER BY event_at_ms ASC, id ASC
|
ORDER BY event_at_ms ASC, id ASC
|
||||||
""",
|
""",
|
||||||
(lookback, lookahead),
|
(lookback, lookahead),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
alerts: list[dict[str, Any]] = []
|
alerts: list[dict[str, Any]] = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
item = enrich_alert(_row_to_dict(row), now_ms=now)
|
item = enrich_alert(_row_to_dict(row), now_ms=now)
|
||||||
if item:
|
if item:
|
||||||
alerts.append(item)
|
alerts.append(item)
|
||||||
return alerts
|
return alerts
|
||||||
|
|
||||||
|
|
||||||
def build_banner_message(alert: dict[str, Any], *, has_positions: bool) -> str:
|
def build_banner_message(alert: dict[str, Any], *, has_positions: bool) -> str:
|
||||||
label = alert.get("event_type_label") or alert.get("event_type") or "宏观数据"
|
label = alert.get("event_type_label") or alert.get("event_type") or "宏观数据"
|
||||||
phase = alert.get("phase") or "window"
|
phase = alert.get("phase") or "window"
|
||||||
if has_positions:
|
if has_positions:
|
||||||
if phase == "imminent" and int(alert.get("minutes_to_event") or 0) > 0:
|
if phase == "imminent" and int(alert.get("minutes_to_event") or 0) > 0:
|
||||||
return (
|
return (
|
||||||
f"「{label}」即将发布(约 {alert['minutes_to_event']} 分钟),"
|
f"「{label}」即将发布(约 {alert['minutes_to_event']} 分钟),"
|
||||||
"注意仓位风险:勿加仓,检查止损/减仓"
|
"注意仓位风险:勿加仓,检查止损/减仓"
|
||||||
)
|
)
|
||||||
return f"「{label}」高波动窗口(±1h),注意仓位风险:勿加仓,检查止损/减仓"
|
return f"「{label}」高波动窗口(±1h),注意仓位风险:勿加仓,检查止损/减仓"
|
||||||
if phase == "imminent" and int(alert.get("minutes_to_event") or 0) > 0:
|
if phase == "imminent" and int(alert.get("minutes_to_event") or 0) > 0:
|
||||||
return (
|
return (
|
||||||
f"「{label}」即将发布(约 {alert['minutes_to_event']} 分钟),"
|
f"「{label}」即将发布(约 {alert['minutes_to_event']} 分钟),"
|
||||||
"建议等待,避免新开仓"
|
"建议等待,避免新开仓"
|
||||||
)
|
)
|
||||||
return f"「{label}」高波动窗口(±1h),建议等待,避免新开仓"
|
return f"「{label}」高波动窗口(±1h),建议等待,避免新开仓"
|
||||||
@@ -1,81 +1,81 @@
|
|||||||
"""实例 USDT 永续合约信息(与实盘 ccxt 精度一致)。"""
|
"""实例 USDT 永续合约信息(与实盘 ccxt 精度一致)。"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Callable, Optional, Tuple
|
from typing import Any, Callable, Optional, Tuple
|
||||||
|
|
||||||
from hub_calculator_market_lib import (
|
from lib.hub.hub_calculator_market_lib import (
|
||||||
amount_decimals_from_exchange,
|
amount_decimals_from_exchange,
|
||||||
normalize_base_symbol,
|
normalize_base_symbol,
|
||||||
price_decimals_from_exchange,
|
price_decimals_from_exchange,
|
||||||
resolve_usdt_perp_symbol,
|
resolve_usdt_perp_symbol,
|
||||||
)
|
)
|
||||||
from hub_ohlcv_lib import normalize_price_tick, price_tick_from_market
|
from lib.hub.hub_ohlcv_lib import normalize_price_tick, price_tick_from_market
|
||||||
|
|
||||||
|
|
||||||
def fetch_usdt_swap_market_info(
|
def fetch_usdt_swap_market_info(
|
||||||
*,
|
*,
|
||||||
base_or_symbol: str,
|
base_or_symbol: str,
|
||||||
normalize_symbol_input: Callable[[str], str],
|
normalize_symbol_input: Callable[[str], str],
|
||||||
normalize_exchange_symbol: Callable[[str], str],
|
normalize_exchange_symbol: Callable[[str], str],
|
||||||
ensure_markets_loaded: Callable[[], None],
|
ensure_markets_loaded: Callable[[], None],
|
||||||
exchange: Any,
|
exchange: Any,
|
||||||
exchange_id: str = "",
|
exchange_id: str = "",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""供各实例 /api/hub/market 调用。"""
|
"""供各实例 /api/hub/market 调用。"""
|
||||||
raw = str(base_or_symbol or "").strip()
|
raw = str(base_or_symbol or "").strip()
|
||||||
if not raw:
|
if not raw:
|
||||||
return {"ok": False, "msg": "请输入币种,如 ETH"}
|
return {"ok": False, "msg": "请输入币种,如 ETH"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ensure_markets_loaded()
|
ensure_markets_loaded()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return {"ok": False, "msg": f"加载市场失败: {exc}"}
|
return {"ok": False, "msg": f"加载市场失败: {exc}"}
|
||||||
|
|
||||||
base_u = normalize_base_symbol(raw)
|
base_u = normalize_base_symbol(raw)
|
||||||
hub_sym = normalize_symbol_input(raw if base_u else raw)
|
hub_sym = normalize_symbol_input(raw if base_u else raw)
|
||||||
try:
|
try:
|
||||||
ex_sym = normalize_exchange_symbol(hub_sym)
|
ex_sym = normalize_exchange_symbol(hub_sym)
|
||||||
except Exception:
|
except Exception:
|
||||||
ex_sym = hub_sym
|
ex_sym = hub_sym
|
||||||
|
|
||||||
sym, err = resolve_usdt_perp_symbol(exchange, base_u or hub_sym)
|
sym, err = resolve_usdt_perp_symbol(exchange, base_u or hub_sym)
|
||||||
if err and ex_sym:
|
if err and ex_sym:
|
||||||
markets = getattr(exchange, "markets", None) or {}
|
markets = getattr(exchange, "markets", None) or {}
|
||||||
if ex_sym in markets:
|
if ex_sym in markets:
|
||||||
sym = ex_sym
|
sym = ex_sym
|
||||||
err = None
|
err = None
|
||||||
if err or not sym:
|
if err or not sym:
|
||||||
return {"ok": False, "msg": err or f"未找到 {base_u or raw}/USDT 永续合约"}
|
return {"ok": False, "msg": err or f"未找到 {base_u or raw}/USDT 永续合约"}
|
||||||
|
|
||||||
market = exchange.market(sym)
|
market = exchange.market(sym)
|
||||||
try:
|
try:
|
||||||
contract_size = float(market.get("contractSize") or 1.0)
|
contract_size = float(market.get("contractSize") or 1.0)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
contract_size = 1.0
|
contract_size = 1.0
|
||||||
if contract_size <= 0:
|
if contract_size <= 0:
|
||||||
contract_size = 1.0
|
contract_size = 1.0
|
||||||
|
|
||||||
price_tick = normalize_price_tick(price_tick_from_market(exchange, sym))
|
price_tick = normalize_price_tick(price_tick_from_market(exchange, sym))
|
||||||
amt_dec = amount_decimals_from_exchange(exchange, sym)
|
amt_dec = amount_decimals_from_exchange(exchange, sym)
|
||||||
px_dec = price_decimals_from_exchange(exchange, sym, price_tick)
|
px_dec = price_decimals_from_exchange(exchange, sym, price_tick)
|
||||||
min_amount = None
|
min_amount = None
|
||||||
try:
|
try:
|
||||||
min_amount = float((market.get("limits") or {}).get("amount", {}).get("min"))
|
min_amount = float((market.get("limits") or {}).get("amount", {}).get("min"))
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
min_amount = None
|
min_amount = None
|
||||||
|
|
||||||
base_out = (market.get("base") or base_u or "").upper() or base_u
|
base_out = (market.get("base") or base_u or "").upper() or base_u
|
||||||
return {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"exchange": (exchange_id or "").strip().lower(),
|
"exchange": (exchange_id or "").strip().lower(),
|
||||||
"base": base_out,
|
"base": base_out,
|
||||||
"exchange_symbol": sym,
|
"exchange_symbol": sym,
|
||||||
"display_symbol": f"{base_out}/USDT" if base_out else sym,
|
"display_symbol": f"{base_out}/USDT" if base_out else sym,
|
||||||
"contract_size": contract_size,
|
"contract_size": contract_size,
|
||||||
"price_tick": price_tick,
|
"price_tick": price_tick,
|
||||||
"price_decimals": px_dec,
|
"price_decimals": px_dec,
|
||||||
"amount_decimals": amt_dec,
|
"amount_decimals": amt_dec,
|
||||||
"min_amount": min_amount,
|
"min_amount": min_amount,
|
||||||
}
|
}
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
"""Shared library package."""
|
||||||
@@ -1,187 +1,187 @@
|
|||||||
"""实盘/关键位放大 K 线:订单元数据与交易所浮盈、价格展示精度。"""
|
"""实盘/关键位放大 K 线:订单元数据与交易所浮盈、价格展示精度。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
from hub_ohlcv_lib import (
|
from lib.hub.hub_ohlcv_lib import (
|
||||||
normalize_price_tick,
|
normalize_price_tick,
|
||||||
price_tick_from_market,
|
price_tick_from_market,
|
||||||
round_ohlcv_bars_to_tick,
|
round_ohlcv_bars_to_tick,
|
||||||
)
|
)
|
||||||
from order_monitor_display_lib import (
|
from lib.trade.order_monitor_display_lib import (
|
||||||
apply_order_live_price_display,
|
apply_order_live_price_display,
|
||||||
apply_order_price_display_fields,
|
apply_order_price_display_fields,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def resolve_kline_price_tick(
|
def resolve_kline_price_tick(
|
||||||
exchange: Any,
|
exchange: Any,
|
||||||
exchange_symbol: str,
|
exchange_symbol: str,
|
||||||
*,
|
*,
|
||||||
ensure_markets_fn: Callable[[], None],
|
ensure_markets_fn: Callable[[], None],
|
||||||
) -> Optional[float]:
|
) -> Optional[float]:
|
||||||
"""交易所最小价格变动单位,供 lightweight-charts 右侧刻度与标记线对齐。"""
|
"""交易所最小价格变动单位,供 lightweight-charts 右侧刻度与标记线对齐。"""
|
||||||
if not exchange_symbol:
|
if not exchange_symbol:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
ensure_markets_fn()
|
ensure_markets_fn()
|
||||||
return normalize_price_tick(price_tick_from_market(exchange, exchange_symbol))
|
return normalize_price_tick(price_tick_from_market(exchange, exchange_symbol))
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def align_candles_to_price_tick(
|
def align_candles_to_price_tick(
|
||||||
candles: list[dict[str, Any]],
|
candles: list[dict[str, Any]],
|
||||||
price_tick: Optional[float],
|
price_tick: Optional[float],
|
||||||
) -> None:
|
) -> None:
|
||||||
if price_tick is not None and candles:
|
if price_tick is not None and candles:
|
||||||
round_ohlcv_bars_to_tick(candles, price_tick)
|
round_ohlcv_bars_to_tick(candles, price_tick)
|
||||||
|
|
||||||
|
|
||||||
def kline_api_price_fields(
|
def kline_api_price_fields(
|
||||||
exchange: Any,
|
exchange: Any,
|
||||||
exchange_symbol: str,
|
exchange_symbol: str,
|
||||||
candles: list[dict[str, Any]],
|
candles: list[dict[str, Any]],
|
||||||
*,
|
*,
|
||||||
ensure_markets_fn: Callable[[], None],
|
ensure_markets_fn: Callable[[], None],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
tick = resolve_kline_price_tick(
|
tick = resolve_kline_price_tick(
|
||||||
exchange, exchange_symbol, ensure_markets_fn=ensure_markets_fn
|
exchange, exchange_symbol, ensure_markets_fn=ensure_markets_fn
|
||||||
)
|
)
|
||||||
align_candles_to_price_tick(candles, tick)
|
align_candles_to_price_tick(candles, tick)
|
||||||
return {"price_tick": tick}
|
return {"price_tick": tick}
|
||||||
|
|
||||||
|
|
||||||
def load_swap_positions_for_order_kline(
|
def load_swap_positions_for_order_kline(
|
||||||
exchange: Any,
|
exchange: Any,
|
||||||
*,
|
*,
|
||||||
private_configured: bool,
|
private_configured: bool,
|
||||||
ensure_markets_fn: Callable[[], None],
|
ensure_markets_fn: Callable[[], None],
|
||||||
settle: str = "usdt",
|
settle: str = "usdt",
|
||||||
) -> list:
|
) -> list:
|
||||||
if not private_configured:
|
if not private_configured:
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
ensure_markets_fn()
|
ensure_markets_fn()
|
||||||
try:
|
try:
|
||||||
return exchange.fetch_positions(None, {"settle": settle}) or []
|
return exchange.fetch_positions(None, {"settle": settle}) or []
|
||||||
except Exception:
|
except Exception:
|
||||||
return exchange.fetch_positions() or []
|
return exchange.fetch_positions() or []
|
||||||
except Exception:
|
except Exception:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def metrics_for_order_item(
|
def metrics_for_order_item(
|
||||||
order_item: dict[str, Any],
|
order_item: dict[str, Any],
|
||||||
positions: list,
|
positions: list,
|
||||||
*,
|
*,
|
||||||
resolve_ex_sym_fn: Callable[[Any], str],
|
resolve_ex_sym_fn: Callable[[Any], str],
|
||||||
select_live_fn: Callable[[list, str, str], Any],
|
select_live_fn: Callable[[list, str, str], Any],
|
||||||
parse_metrics_fn: Callable[..., Optional[dict]],
|
parse_metrics_fn: Callable[..., Optional[dict]],
|
||||||
) -> Optional[dict]:
|
) -> Optional[dict]:
|
||||||
if not positions:
|
if not positions:
|
||||||
return None
|
return None
|
||||||
ex_sym = resolve_ex_sym_fn(order_item)
|
ex_sym = resolve_ex_sym_fn(order_item)
|
||||||
direction = order_item.get("direction") or "long"
|
direction = order_item.get("direction") or "long"
|
||||||
prow = select_live_fn(positions, ex_sym, direction)
|
prow = select_live_fn(positions, ex_sym, direction)
|
||||||
if not prow:
|
if not prow:
|
||||||
return None
|
return None
|
||||||
lev = order_item.get("leverage")
|
lev = order_item.get("leverage")
|
||||||
return parse_metrics_fn(prow, order_leverage=lev)
|
return parse_metrics_fn(prow, order_leverage=lev)
|
||||||
|
|
||||||
|
|
||||||
def build_order_kline_order_payload(
|
def build_order_kline_order_payload(
|
||||||
order_item: dict[str, Any],
|
order_item: dict[str, Any],
|
||||||
*,
|
*,
|
||||||
ticker_price: Any,
|
ticker_price: Any,
|
||||||
format_price_fn: Callable[[Any, Any], str],
|
format_price_fn: Callable[[Any, Any], str],
|
||||||
calc_pnl_fn: Callable[..., float],
|
calc_pnl_fn: Callable[..., float],
|
||||||
calc_rr_ratio_fn: Callable[..., Optional[float]],
|
calc_rr_ratio_fn: Callable[..., Optional[float]],
|
||||||
ex_metrics: Optional[dict] = None,
|
ex_metrics: Optional[dict] = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
sym = order_item.get("symbol") or ""
|
sym = order_item.get("symbol") or ""
|
||||||
direction = order_item.get("direction") or "long"
|
direction = order_item.get("direction") or "long"
|
||||||
margin = float(order_item.get("margin_capital") or 0)
|
margin = float(order_item.get("margin_capital") or 0)
|
||||||
leverage = float(order_item.get("leverage") or 0)
|
leverage = float(order_item.get("leverage") or 0)
|
||||||
entry = float(order_item.get("trigger_price") or 0)
|
entry = float(order_item.get("trigger_price") or 0)
|
||||||
|
|
||||||
float_pnl = 0.0
|
float_pnl = 0.0
|
||||||
float_pct = 0.0
|
float_pct = 0.0
|
||||||
if ticker_price and entry > 0:
|
if ticker_price and entry > 0:
|
||||||
float_pnl = float(
|
float_pnl = float(
|
||||||
calc_pnl_fn(direction, entry, ticker_price, margin, leverage)
|
calc_pnl_fn(direction, entry, ticker_price, margin, leverage)
|
||||||
)
|
)
|
||||||
float_pct = round((float_pnl / margin * 100), 4) if margin > 0 else 0.0
|
float_pct = round((float_pnl / margin * 100), 4) if margin > 0 else 0.0
|
||||||
|
|
||||||
px_for_fmt = ticker_price
|
px_for_fmt = ticker_price
|
||||||
mark_raw = None
|
mark_raw = None
|
||||||
if ex_metrics and ex_metrics.get("mark_price") is not None:
|
if ex_metrics and ex_metrics.get("mark_price") is not None:
|
||||||
mark_raw = ex_metrics["mark_price"]
|
mark_raw = ex_metrics["mark_price"]
|
||||||
try:
|
try:
|
||||||
px_for_fmt = float(mark_raw)
|
px_for_fmt = float(mark_raw)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if ex_metrics and ex_metrics.get("unrealized_pnl") is not None:
|
if ex_metrics and ex_metrics.get("unrealized_pnl") is not None:
|
||||||
float_pnl = round(float(ex_metrics["unrealized_pnl"]), 2)
|
float_pnl = round(float(ex_metrics["unrealized_pnl"]), 2)
|
||||||
denom = ex_metrics.get("initial_margin") or margin
|
denom = ex_metrics.get("initial_margin") or margin
|
||||||
float_pct = (
|
float_pct = (
|
||||||
round((float_pnl / float(denom)) * 100, 4)
|
round((float_pnl / float(denom)) * 100, 4)
|
||||||
if denom and float(denom) > 0
|
if denom and float(denom) > 0
|
||||||
else float_pct
|
else float_pct
|
||||||
)
|
)
|
||||||
|
|
||||||
payload: dict[str, Any] = {
|
payload: dict[str, Any] = {
|
||||||
"id": order_item["id"],
|
"id": order_item["id"],
|
||||||
"symbol": sym,
|
"symbol": sym,
|
||||||
"direction": direction,
|
"direction": direction,
|
||||||
"trigger_price": order_item.get("trigger_price"),
|
"trigger_price": order_item.get("trigger_price"),
|
||||||
"stop_loss": order_item.get("stop_loss"),
|
"stop_loss": order_item.get("stop_loss"),
|
||||||
"take_profit": order_item.get("take_profit"),
|
"take_profit": order_item.get("take_profit"),
|
||||||
"trigger_price_display": format_price_fn(sym, order_item.get("trigger_price")),
|
"trigger_price_display": format_price_fn(sym, order_item.get("trigger_price")),
|
||||||
"stop_loss_display": format_price_fn(sym, order_item.get("stop_loss")),
|
"stop_loss_display": format_price_fn(sym, order_item.get("stop_loss")),
|
||||||
"take_profit_display": format_price_fn(sym, order_item.get("take_profit")),
|
"take_profit_display": format_price_fn(sym, order_item.get("take_profit")),
|
||||||
"margin_capital": order_item.get("margin_capital"),
|
"margin_capital": order_item.get("margin_capital"),
|
||||||
"leverage": order_item.get("leverage"),
|
"leverage": order_item.get("leverage"),
|
||||||
"position_ratio": order_item.get("position_ratio"),
|
"position_ratio": order_item.get("position_ratio"),
|
||||||
"breakeven_enabled": bool(int(order_item.get("breakeven_enabled") or 0)),
|
"breakeven_enabled": bool(int(order_item.get("breakeven_enabled") or 0)),
|
||||||
"current_price": round(float(px_for_fmt), 8) if px_for_fmt is not None else None,
|
"current_price": round(float(px_for_fmt), 8) if px_for_fmt is not None else None,
|
||||||
"float_pnl": round(float(float_pnl), 2),
|
"float_pnl": round(float(float_pnl), 2),
|
||||||
"float_pct": float_pct,
|
"float_pct": float_pct,
|
||||||
}
|
}
|
||||||
apply_order_price_display_fields(
|
apply_order_price_display_fields(
|
||||||
payload,
|
payload,
|
||||||
direction=direction,
|
direction=direction,
|
||||||
entry_price=order_item.get("trigger_price"),
|
entry_price=order_item.get("trigger_price"),
|
||||||
initial_stop_loss=order_item.get("initial_stop_loss"),
|
initial_stop_loss=order_item.get("initial_stop_loss"),
|
||||||
stop_loss=order_item.get("stop_loss"),
|
stop_loss=order_item.get("stop_loss"),
|
||||||
take_profit=order_item.get("take_profit"),
|
take_profit=order_item.get("take_profit"),
|
||||||
calc_rr_ratio_fn=calc_rr_ratio_fn,
|
calc_rr_ratio_fn=calc_rr_ratio_fn,
|
||||||
)
|
)
|
||||||
apply_order_live_price_display(
|
apply_order_live_price_display(
|
||||||
payload,
|
payload,
|
||||||
sym,
|
sym,
|
||||||
ticker_price,
|
ticker_price,
|
||||||
mark_raw,
|
mark_raw,
|
||||||
format_price_fn,
|
format_price_fn,
|
||||||
)
|
)
|
||||||
payload["current_price_display"] = payload.get("price_display") or (
|
payload["current_price_display"] = payload.get("price_display") or (
|
||||||
format_price_fn(sym, px_for_fmt) if px_for_fmt is not None else None
|
format_price_fn(sym, px_for_fmt) if px_for_fmt is not None else None
|
||||||
)
|
)
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
def enrich_key_kline_response(
|
def enrich_key_kline_response(
|
||||||
*,
|
*,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
current_price: Any,
|
current_price: Any,
|
||||||
key_info: Optional[dict[str, Any]],
|
key_info: Optional[dict[str, Any]],
|
||||||
format_price_fn: Callable[[Any, Any], str],
|
format_price_fn: Callable[[Any, Any], str],
|
||||||
) -> tuple[Any, Optional[dict[str, Any]]]:
|
) -> tuple[Any, Optional[dict[str, Any]]]:
|
||||||
price_display = format_price_fn(symbol, current_price) if current_price is not None else None
|
price_display = format_price_fn(symbol, current_price) if current_price is not None else None
|
||||||
if key_info is None:
|
if key_info is None:
|
||||||
return price_display, None
|
return price_display, None
|
||||||
enriched = dict(key_info)
|
enriched = dict(key_info)
|
||||||
enriched["upper_display"] = format_price_fn(symbol, key_info.get("upper"))
|
enriched["upper_display"] = format_price_fn(symbol, key_info.get("upper"))
|
||||||
enriched["lower_display"] = format_price_fn(symbol, key_info.get("lower"))
|
enriched["lower_display"] = format_price_fn(symbol, key_info.get("lower"))
|
||||||
return price_display, enriched
|
return price_display, enriched
|
||||||
@@ -1,84 +1,84 @@
|
|||||||
"""embed 壳/片段:按 tab 裁剪 render_main_page 的数据加载,降内存与 API 压力。"""
|
"""embed 壳/片段:按 tab 裁剪 render_main_page 的数据加载,降内存与 API 压力。"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
EMBED_STRATEGY_PAGES = frozenset({"strategy", "strategy_trend", "strategy_roll", "strategy_records"})
|
EMBED_STRATEGY_PAGES = frozenset({"strategy", "strategy_trend", "strategy_roll", "strategy_records"})
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class EmbedRenderPlan:
|
class EmbedRenderPlan:
|
||||||
exchange_capitals: bool
|
exchange_capitals: bool
|
||||||
records_rows: bool
|
records_rows: bool
|
||||||
records_summary: bool
|
records_summary: bool
|
||||||
key_history: bool
|
key_history: bool
|
||||||
key_list: bool
|
key_list: bool
|
||||||
orders: bool
|
orders: bool
|
||||||
stats_bundle: bool
|
stats_bundle: bool
|
||||||
strategy: bool
|
strategy: bool
|
||||||
orphan_live: bool
|
orphan_live: bool
|
||||||
|
|
||||||
|
|
||||||
def embed_render_plan(page: str, embed_mode: str | None) -> EmbedRenderPlan:
|
def embed_render_plan(page: str, embed_mode: str | None) -> EmbedRenderPlan:
|
||||||
if embed_mode not in ("fragment", "shell"):
|
if embed_mode not in ("fragment", "shell"):
|
||||||
return EmbedRenderPlan(
|
return EmbedRenderPlan(
|
||||||
exchange_capitals=True,
|
exchange_capitals=True,
|
||||||
records_rows=True,
|
records_rows=True,
|
||||||
records_summary=False,
|
records_summary=False,
|
||||||
key_history=True,
|
key_history=True,
|
||||||
key_list=True,
|
key_list=True,
|
||||||
orders=True,
|
orders=True,
|
||||||
stats_bundle=True,
|
stats_bundle=True,
|
||||||
strategy=True,
|
strategy=True,
|
||||||
orphan_live=True,
|
orphan_live=True,
|
||||||
)
|
)
|
||||||
is_shell = embed_mode == "shell"
|
is_shell = embed_mode == "shell"
|
||||||
is_strategy = page in EMBED_STRATEGY_PAGES
|
is_strategy = page in EMBED_STRATEGY_PAGES
|
||||||
return EmbedRenderPlan(
|
return EmbedRenderPlan(
|
||||||
exchange_capitals=is_shell,
|
exchange_capitals=is_shell,
|
||||||
records_rows=page == "records",
|
records_rows=page == "records",
|
||||||
records_summary=is_shell and page != "records",
|
records_summary=is_shell and page != "records",
|
||||||
key_history=page == "key_monitor",
|
key_history=page == "key_monitor",
|
||||||
key_list=page in ("key_monitor", "trade") or is_strategy,
|
key_list=page in ("key_monitor", "trade") or is_strategy,
|
||||||
orders=page == "trade" or is_strategy,
|
orders=page == "trade" or is_strategy,
|
||||||
stats_bundle=page == "stats",
|
stats_bundle=page == "stats",
|
||||||
strategy=is_strategy,
|
strategy=is_strategy,
|
||||||
orphan_live=page == "trade" and is_shell,
|
orphan_live=page == "trade" and is_shell,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def trade_records_summary(conn, start_bj: str, end_bj: str, tr_ts: str) -> dict[str, Any]:
|
def trade_records_summary(conn, start_bj: str, end_bj: str, tr_ts: str) -> dict[str, Any]:
|
||||||
"""顶栏统计用 COUNT,避免 embed 壳拉 1000 行交易记录。"""
|
"""顶栏统计用 COUNT,避免 embed 壳拉 1000 行交易记录。"""
|
||||||
from trade_result_lib import sql_effective_pnl_expr
|
from lib.trade.trade_result_lib import sql_effective_pnl_expr
|
||||||
|
|
||||||
pnl_sql = sql_effective_pnl_expr()
|
pnl_sql = sql_effective_pnl_expr()
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
f"""
|
f"""
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) AS total,
|
COUNT(*) AS total,
|
||||||
SUM(CASE WHEN result = '错过' THEN 1 ELSE 0 END) AS miss_count,
|
SUM(CASE WHEN result = '错过' THEN 1 ELSE 0 END) AS miss_count,
|
||||||
SUM(CASE WHEN {pnl_sql} > 0 THEN 1 ELSE 0 END) AS wins,
|
SUM(CASE WHEN {pnl_sql} > 0 THEN 1 ELSE 0 END) AS wins,
|
||||||
SUM(CASE WHEN result = '错过' AND COALESCE(miss_reason,'') LIKE '%持仓占用%' THEN 1 ELSE 0 END) AS occupied_miss
|
SUM(CASE WHEN result = '错过' AND COALESCE(miss_reason,'') LIKE '%持仓占用%' THEN 1 ELSE 0 END) AS occupied_miss
|
||||||
FROM trade_records
|
FROM trade_records
|
||||||
WHERE {tr_ts} >= ? AND {tr_ts} <= ?
|
WHERE {tr_ts} >= ? AND {tr_ts} <= ?
|
||||||
""",
|
""",
|
||||||
(start_bj, end_bj),
|
(start_bj, end_bj),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
total = int(row["total"] or 0) if row else 0
|
total = int(row["total"] or 0) if row else 0
|
||||||
miss_count = int(row["miss_count"] or 0) if row else 0
|
miss_count = int(row["miss_count"] or 0) if row else 0
|
||||||
wins = int(row["wins"] or 0) if row else 0
|
wins = int(row["wins"] or 0) if row else 0
|
||||||
occupied_miss_total = int(row["occupied_miss"] or 0) if row else 0
|
occupied_miss_total = int(row["occupied_miss"] or 0) if row else 0
|
||||||
rate = round(wins / total * 100, 2) if total else 0
|
rate = round(wins / total * 100, 2) if total else 0
|
||||||
return {
|
return {
|
||||||
"records": [],
|
"records": [],
|
||||||
"total": total,
|
"total": total,
|
||||||
"miss_count": miss_count,
|
"miss_count": miss_count,
|
||||||
"rate": rate,
|
"rate": rate,
|
||||||
"occupied_miss_total": occupied_miss_total,
|
"occupied_miss_total": occupied_miss_total,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def minimal_stats_bundle(reset_hour: int) -> dict[str, Any]:
|
def minimal_stats_bundle(reset_hour: int) -> dict[str, Any]:
|
||||||
return {"stats_reset_hour": reset_hour, "segments": []}
|
return {"stats_reset_hour": reset_hour, "segments": []}
|
||||||
@@ -1,147 +1,148 @@
|
|||||||
"""中控 iframe:壳常驻 + tab 内容 API(/embed、/api/embed/page/<tab>)。"""
|
"""中控 iframe:壳常驻 + tab 内容 API(/embed、/api/embed/page/<tab>)。"""
|
||||||
|
from __future__ import annotations
|
||||||
from __future__ import annotations
|
|
||||||
|
from lib.paths import embed_templates_dir
|
||||||
import os
|
|
||||||
from typing import Callable
|
import os
|
||||||
from urllib.parse import parse_qsl, urlencode, urlsplit
|
from typing import Callable
|
||||||
|
from urllib.parse import parse_qsl, urlencode, urlsplit
|
||||||
from flask import Flask, Response, jsonify, redirect, request, session
|
|
||||||
from jinja2 import ChoiceLoader, FileSystemLoader
|
from flask import Flask, Response, jsonify, redirect, request, session
|
||||||
|
from jinja2 import ChoiceLoader, FileSystemLoader
|
||||||
EMBED_TABS: tuple[str, ...] = (
|
|
||||||
"key_monitor",
|
EMBED_TABS: tuple[str, ...] = (
|
||||||
"trade",
|
"key_monitor",
|
||||||
"strategy",
|
"trade",
|
||||||
"strategy_records",
|
"strategy",
|
||||||
"records",
|
"strategy_records",
|
||||||
"stats",
|
"records",
|
||||||
)
|
"stats",
|
||||||
|
)
|
||||||
PATH_TO_EMBED_TAB: dict[str, str] = {
|
|
||||||
"/": "trade",
|
PATH_TO_EMBED_TAB: dict[str, str] = {
|
||||||
"/trade": "trade",
|
"/": "trade",
|
||||||
"/key_monitor": "key_monitor",
|
"/trade": "trade",
|
||||||
"/strategy": "strategy",
|
"/key_monitor": "key_monitor",
|
||||||
"/strategy/trend": "strategy",
|
"/strategy": "strategy",
|
||||||
"/strategy/roll": "strategy",
|
"/strategy/trend": "strategy",
|
||||||
"/strategy/records": "strategy_records",
|
"/strategy/roll": "strategy",
|
||||||
"/records": "records",
|
"/strategy/records": "strategy_records",
|
||||||
"/stats": "stats",
|
"/records": "records",
|
||||||
}
|
"/stats": "stats",
|
||||||
|
}
|
||||||
ORDER_RULE_TIPS_BY_EXCHANGE: dict[str, str] = {
|
|
||||||
"gate": "order_monitor_rule_tips_gate.html",
|
ORDER_RULE_TIPS_BY_EXCHANGE: dict[str, str] = {
|
||||||
"gate_bot": "order_monitor_rule_tips_gate.html",
|
"gate": "order_monitor_rule_tips_gate.html",
|
||||||
"binance": "order_monitor_rule_tips_binance.html",
|
"gate_bot": "order_monitor_rule_tips_gate.html",
|
||||||
"okx": "order_monitor_rule_tips_okx.html",
|
"binance": "order_monitor_rule_tips_binance.html",
|
||||||
}
|
"okx": "order_monitor_rule_tips_okx.html",
|
||||||
|
}
|
||||||
|
|
||||||
def order_rule_tips_template(exchange_key: str) -> str:
|
|
||||||
ex = (exchange_key or "").strip().lower()
|
def order_rule_tips_template(exchange_key: str) -> str:
|
||||||
return ORDER_RULE_TIPS_BY_EXCHANGE.get(ex, "order_monitor_rule_tips_gate.html")
|
ex = (exchange_key or "").strip().lower()
|
||||||
|
return ORDER_RULE_TIPS_BY_EXCHANGE.get(ex, "order_monitor_rule_tips_gate.html")
|
||||||
|
|
||||||
def include_transfer_block(exchange_key: str) -> bool:
|
|
||||||
return (exchange_key or "").strip().lower() in ("gate", "gate_bot")
|
def include_transfer_block(exchange_key: str) -> bool:
|
||||||
|
return (exchange_key or "").strip().lower() in ("gate", "gate_bot")
|
||||||
|
|
||||||
def path_to_embed_tab(path: str) -> str | None:
|
|
||||||
p = (path or "/").strip()
|
def path_to_embed_tab(path: str) -> str | None:
|
||||||
if not p.startswith("/"):
|
p = (path or "/").strip()
|
||||||
p = "/" + p
|
if not p.startswith("/"):
|
||||||
base = urlsplit(p).path.rstrip("/") or "/"
|
p = "/" + p
|
||||||
return PATH_TO_EMBED_TAB.get(base)
|
base = urlsplit(p).path.rstrip("/") or "/"
|
||||||
|
return PATH_TO_EMBED_TAB.get(base)
|
||||||
|
|
||||||
def embed_shell_enabled() -> bool:
|
|
||||||
return (os.getenv("HUB_EMBED_SHELL") or "1").strip().lower() in ("1", "true", "yes", "on")
|
def embed_shell_enabled() -> bool:
|
||||||
|
return (os.getenv("HUB_EMBED_SHELL") or "1").strip().lower() in ("1", "true", "yes", "on")
|
||||||
|
|
||||||
def rewrite_embed_dest(path: str, hub_theme: str | None = None) -> str:
|
|
||||||
"""embed=1 打开时:/trade → /embed?tab=trade&embed=1"""
|
def rewrite_embed_dest(path: str, hub_theme: str | None = None) -> str:
|
||||||
if not embed_shell_enabled():
|
"""embed=1 打开时:/trade → /embed?tab=trade&embed=1"""
|
||||||
split = urlsplit(path or "/")
|
if not embed_shell_enabled():
|
||||||
q = dict(parse_qsl(split.query, keep_blank_values=True))
|
split = urlsplit(path or "/")
|
||||||
q["embed"] = "1"
|
q = dict(parse_qsl(split.query, keep_blank_values=True))
|
||||||
ht = (hub_theme or q.get("hub_theme") or "").strip().lower()
|
q["embed"] = "1"
|
||||||
if ht in ("light", "dark"):
|
ht = (hub_theme or q.get("hub_theme") or "").strip().lower()
|
||||||
q["hub_theme"] = ht
|
if ht in ("light", "dark"):
|
||||||
dest = split.path or "/"
|
q["hub_theme"] = ht
|
||||||
if q:
|
dest = split.path or "/"
|
||||||
return f"{dest}?{urlencode(q)}"
|
if q:
|
||||||
return dest + "?embed=1"
|
return f"{dest}?{urlencode(q)}"
|
||||||
split = urlsplit(path or "/")
|
return dest + "?embed=1"
|
||||||
tab = path_to_embed_tab(split.path)
|
split = urlsplit(path or "/")
|
||||||
q = dict(parse_qsl(split.query, keep_blank_values=True))
|
tab = path_to_embed_tab(split.path)
|
||||||
if tab:
|
q = dict(parse_qsl(split.query, keep_blank_values=True))
|
||||||
q["tab"] = tab
|
if tab:
|
||||||
q["embed"] = "1"
|
q["tab"] = tab
|
||||||
ht = (hub_theme or q.get("hub_theme") or "").strip().lower()
|
q["embed"] = "1"
|
||||||
if ht in ("light", "dark"):
|
ht = (hub_theme or q.get("hub_theme") or "").strip().lower()
|
||||||
q["hub_theme"] = ht
|
if ht in ("light", "dark"):
|
||||||
return f"/embed?{urlencode(q)}"
|
q["hub_theme"] = ht
|
||||||
q["embed"] = "1"
|
return f"/embed?{urlencode(q)}"
|
||||||
ht = (hub_theme or q.get("hub_theme") or "").strip().lower()
|
q["embed"] = "1"
|
||||||
if ht in ("light", "dark"):
|
ht = (hub_theme or q.get("hub_theme") or "").strip().lower()
|
||||||
q["hub_theme"] = ht
|
if ht in ("light", "dark"):
|
||||||
dest = split.path or "/"
|
q["hub_theme"] = ht
|
||||||
if split.query:
|
dest = split.path or "/"
|
||||||
dest += "?" + split.query
|
if split.query:
|
||||||
if "embed=1" not in dest:
|
dest += "?" + split.query
|
||||||
sep = "&" if "?" in dest else "?"
|
if "embed=1" not in dest:
|
||||||
dest += f"{sep}embed=1"
|
sep = "&" if "?" in dest else "?"
|
||||||
if ht in ("light", "dark") and "hub_theme=" not in dest:
|
dest += f"{sep}embed=1"
|
||||||
sep = "&" if "?" in dest else "?"
|
if ht in ("light", "dark") and "hub_theme=" not in dest:
|
||||||
dest += f"{sep}hub_theme={ht}"
|
sep = "&" if "?" in dest else "?"
|
||||||
return dest
|
dest += f"{sep}hub_theme={ht}"
|
||||||
|
return dest
|
||||||
|
|
||||||
def attach_embed_templates(app: Flask, repo_root: str) -> None:
|
|
||||||
embed_dir = os.path.join(repo_root, "embed_templates")
|
def attach_embed_templates(app: Flask, repo_root: str) -> None:
|
||||||
if not os.path.isdir(embed_dir):
|
embed_dir = embed_templates_dir(repo_root)
|
||||||
return
|
if not os.path.isdir(embed_dir):
|
||||||
existing = app.jinja_loader
|
return
|
||||||
loaders = [FileSystemLoader(embed_dir)]
|
existing = app.jinja_loader
|
||||||
if existing is not None:
|
loaders = [FileSystemLoader(embed_dir)]
|
||||||
if isinstance(existing, ChoiceLoader):
|
if existing is not None:
|
||||||
loaders = list(existing.loaders) + loaders
|
if isinstance(existing, ChoiceLoader):
|
||||||
else:
|
loaders = list(existing.loaders) + loaders
|
||||||
loaders.insert(0, existing)
|
else:
|
||||||
app.jinja_loader = ChoiceLoader(loaders)
|
loaders.insert(0, existing)
|
||||||
|
app.jinja_loader = ChoiceLoader(loaders)
|
||||||
|
|
||||||
def register_embed_routes(
|
|
||||||
app: Flask,
|
def register_embed_routes(
|
||||||
login_required: Callable,
|
app: Flask,
|
||||||
render_main_page_fn: Callable,
|
login_required: Callable,
|
||||||
) -> None:
|
render_main_page_fn: Callable,
|
||||||
app.config["RENDER_MAIN_PAGE_FN"] = render_main_page_fn
|
) -> None:
|
||||||
|
app.config["RENDER_MAIN_PAGE_FN"] = render_main_page_fn
|
||||||
@login_required
|
|
||||||
@app.route("/embed")
|
@login_required
|
||||||
def embed_shell_page():
|
@app.route("/embed")
|
||||||
tab = (request.args.get("tab") or "trade").strip()
|
def embed_shell_page():
|
||||||
if tab not in EMBED_TABS:
|
tab = (request.args.get("tab") or "trade").strip()
|
||||||
tab = "trade"
|
if tab not in EMBED_TABS:
|
||||||
session["hub_embed_shell"] = True
|
tab = "trade"
|
||||||
return render_main_page_fn(tab, embed_mode="shell")
|
session["hub_embed_shell"] = True
|
||||||
|
return render_main_page_fn(tab, embed_mode="shell")
|
||||||
@login_required
|
|
||||||
@app.route("/api/embed/page/<tab>")
|
@login_required
|
||||||
def api_embed_page(tab: str):
|
@app.route("/api/embed/page/<tab>")
|
||||||
tab = (tab or "").strip()
|
def api_embed_page(tab: str):
|
||||||
if tab not in EMBED_TABS:
|
tab = (tab or "").strip()
|
||||||
return jsonify({"ok": False, "msg": "unknown tab"}), 404
|
if tab not in EMBED_TABS:
|
||||||
html = render_main_page_fn(tab, embed_mode="fragment")
|
return jsonify({"ok": False, "msg": "unknown tab"}), 404
|
||||||
if isinstance(html, Response):
|
html = render_main_page_fn(tab, embed_mode="fragment")
|
||||||
html = html.get_data(as_text=True)
|
if isinstance(html, Response):
|
||||||
return jsonify({"ok": True, "page": tab, "html": html})
|
html = html.get_data(as_text=True)
|
||||||
|
return jsonify({"ok": True, "page": tab, "html": html})
|
||||||
|
|
||||||
def embed_context_extras(exchange_key: str) -> dict:
|
|
||||||
return {
|
def embed_context_extras(exchange_key: str) -> dict:
|
||||||
"order_rule_tips_tpl": order_rule_tips_template(exchange_key),
|
return {
|
||||||
"include_transfer_block": include_transfer_block(exchange_key),
|
"order_rule_tips_tpl": order_rule_tips_template(exchange_key),
|
||||||
}
|
"include_transfer_block": include_transfer_block(exchange_key),
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Shared library package."""
|
||||||
+145
-145
@@ -1,145 +1,145 @@
|
|||||||
"""假突破关键位监控:BTC/ETH 限价挂单(共享计算与校验)。"""
|
"""假突破关键位监控:BTC/ETH 限价挂单(共享计算与校验)。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
FALSE_BREAKOUT_MONITOR_TYPE = "假突破"
|
FALSE_BREAKOUT_MONITOR_TYPE = "假突破"
|
||||||
FALSE_BREAKOUT_SYMBOLS = frozenset({"BTC/USDT", "ETH/USDT"})
|
FALSE_BREAKOUT_SYMBOLS = frozenset({"BTC/USDT", "ETH/USDT"})
|
||||||
FALSE_BREAKOUT_OFFSET_PCT = 0.1
|
FALSE_BREAKOUT_OFFSET_PCT = 0.1
|
||||||
FALSE_BREAKOUT_SL_PCT = 0.5
|
FALSE_BREAKOUT_SL_PCT = 0.5
|
||||||
FALSE_BREAKOUT_RR = 1.5
|
FALSE_BREAKOUT_RR = 1.5
|
||||||
FALSE_BREAKOUT_VALIDITY_HOURS = 24
|
FALSE_BREAKOUT_VALIDITY_HOURS = 24
|
||||||
|
|
||||||
|
|
||||||
def is_false_breakout_key_monitor_type(monitor_type: Optional[str]) -> bool:
|
def is_false_breakout_key_monitor_type(monitor_type: Optional[str]) -> bool:
|
||||||
return (monitor_type or "").strip() == FALSE_BREAKOUT_MONITOR_TYPE
|
return (monitor_type or "").strip() == FALSE_BREAKOUT_MONITOR_TYPE
|
||||||
|
|
||||||
|
|
||||||
def is_limit_key_monitor_type(monitor_type: Optional[str]) -> bool:
|
def is_limit_key_monitor_type(monitor_type: Optional[str]) -> bool:
|
||||||
from fib_key_monitor_lib import is_fib_key_monitor_type
|
from lib.key_monitor.fib_key_monitor_lib import is_fib_key_monitor_type
|
||||||
|
|
||||||
return is_fib_key_monitor_type(monitor_type) or is_false_breakout_key_monitor_type(monitor_type)
|
return is_fib_key_monitor_type(monitor_type) or is_false_breakout_key_monitor_type(monitor_type)
|
||||||
|
|
||||||
|
|
||||||
def normalize_false_breakout_symbol(symbol: Optional[str]) -> Optional[str]:
|
def normalize_false_breakout_symbol(symbol: Optional[str]) -> Optional[str]:
|
||||||
s = (symbol or "").strip().upper()
|
s = (symbol or "").strip().upper()
|
||||||
if not s:
|
if not s:
|
||||||
return None
|
return None
|
||||||
if "/" not in s:
|
if "/" not in s:
|
||||||
s = f"{s}/USDT"
|
s = f"{s}/USDT"
|
||||||
return s if s in FALSE_BREAKOUT_SYMBOLS else None
|
return s if s in FALSE_BREAKOUT_SYMBOLS else None
|
||||||
|
|
||||||
|
|
||||||
def storage_bounds_from_key_price(direction: str, key_price: float) -> tuple[float, float]:
|
def storage_bounds_from_key_price(direction: str, key_price: float) -> tuple[float, float]:
|
||||||
k = float(key_price)
|
k = float(key_price)
|
||||||
if k <= 0:
|
if k <= 0:
|
||||||
raise ValueError("关键价位须为正数")
|
raise ValueError("关键价位须为正数")
|
||||||
d = (direction or "long").strip().lower()
|
d = (direction or "long").strip().lower()
|
||||||
if d == "short":
|
if d == "short":
|
||||||
return k, k * 0.9999
|
return k, k * 0.9999
|
||||||
if d == "long":
|
if d == "long":
|
||||||
return k * 1.0001, k
|
return k * 1.0001, k
|
||||||
raise ValueError("方向须为 long 或 short")
|
raise ValueError("方向须为 long 或 short")
|
||||||
|
|
||||||
|
|
||||||
def key_price_from_row(direction: str, upper: Any, lower: Any) -> Optional[float]:
|
def key_price_from_row(direction: str, upper: Any, lower: Any) -> Optional[float]:
|
||||||
d = (direction or "long").strip().lower()
|
d = (direction or "long").strip().lower()
|
||||||
try:
|
try:
|
||||||
if d == "short":
|
if d == "short":
|
||||||
v = float(upper)
|
v = float(upper)
|
||||||
else:
|
else:
|
||||||
v = float(lower)
|
v = float(lower)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None
|
return None
|
||||||
return v if v > 0 else None
|
return v if v > 0 else None
|
||||||
|
|
||||||
|
|
||||||
def calc_false_breakout_plan(direction: str, key_price: float) -> Optional[tuple[float, float, float]]:
|
def calc_false_breakout_plan(direction: str, key_price: float) -> Optional[tuple[float, float, float]]:
|
||||||
try:
|
try:
|
||||||
k = float(key_price)
|
k = float(key_price)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None
|
return None
|
||||||
if k <= 0:
|
if k <= 0:
|
||||||
return None
|
return None
|
||||||
d = (direction or "long").strip().lower()
|
d = (direction or "long").strip().lower()
|
||||||
off = FALSE_BREAKOUT_OFFSET_PCT / 100.0
|
off = FALSE_BREAKOUT_OFFSET_PCT / 100.0
|
||||||
sl_pct = FALSE_BREAKOUT_SL_PCT / 100.0
|
sl_pct = FALSE_BREAKOUT_SL_PCT / 100.0
|
||||||
rr = float(FALSE_BREAKOUT_RR)
|
rr = float(FALSE_BREAKOUT_RR)
|
||||||
if d == "short":
|
if d == "short":
|
||||||
entry = k * (1 + off)
|
entry = k * (1 + off)
|
||||||
sl = entry * (1 + sl_pct)
|
sl = entry * (1 + sl_pct)
|
||||||
risk = sl - entry
|
risk = sl - entry
|
||||||
if risk <= 0:
|
if risk <= 0:
|
||||||
return None
|
return None
|
||||||
tp = entry - risk * rr
|
tp = entry - risk * rr
|
||||||
return entry, sl, tp
|
return entry, sl, tp
|
||||||
if d == "long":
|
if d == "long":
|
||||||
entry = k * (1 - off)
|
entry = k * (1 - off)
|
||||||
sl = entry * (1 - sl_pct)
|
sl = entry * (1 - sl_pct)
|
||||||
risk = entry - sl
|
risk = entry - sl
|
||||||
if risk <= 0:
|
if risk <= 0:
|
||||||
return None
|
return None
|
||||||
tp = entry + risk * rr
|
tp = entry + risk * rr
|
||||||
return entry, sl, tp
|
return entry, sl, tp
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _parse_created_at(raw: Any) -> Optional[datetime]:
|
def _parse_created_at(raw: Any) -> Optional[datetime]:
|
||||||
s = str(raw or "").strip()
|
s = str(raw or "").strip()
|
||||||
if not s:
|
if not s:
|
||||||
return None
|
return None
|
||||||
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S"):
|
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S"):
|
||||||
try:
|
try:
|
||||||
return datetime.strptime(s[:26], fmt)
|
return datetime.strptime(s[:26], fmt)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
return datetime.fromisoformat(s.replace("Z", "+00:00")[:32])
|
return datetime.fromisoformat(s.replace("Z", "+00:00")[:32])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def is_false_breakout_expired(
|
def is_false_breakout_expired(
|
||||||
created_at: Any,
|
created_at: Any,
|
||||||
now: datetime,
|
now: datetime,
|
||||||
*,
|
*,
|
||||||
hours: int = FALSE_BREAKOUT_VALIDITY_HOURS,
|
hours: int = FALSE_BREAKOUT_VALIDITY_HOURS,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
dt = _parse_created_at(created_at)
|
dt = _parse_created_at(created_at)
|
||||||
if dt is None:
|
if dt is None:
|
||||||
return False
|
return False
|
||||||
return now >= dt + timedelta(hours=hours)
|
return now >= dt + timedelta(hours=hours)
|
||||||
|
|
||||||
|
|
||||||
def expires_at_text(created_at: Any, *, hours: int = FALSE_BREAKOUT_VALIDITY_HOURS) -> str:
|
def expires_at_text(created_at: Any, *, hours: int = FALSE_BREAKOUT_VALIDITY_HOURS) -> str:
|
||||||
dt = _parse_created_at(created_at)
|
dt = _parse_created_at(created_at)
|
||||||
if dt is None:
|
if dt is None:
|
||||||
return "—"
|
return "—"
|
||||||
return (dt + timedelta(hours=hours)).strftime("%Y-%m-%d %H:%M:%S")
|
return (dt + timedelta(hours=hours)).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
def false_breakout_gate_preview(
|
def false_breakout_gate_preview(
|
||||||
*,
|
*,
|
||||||
entry_display: str,
|
entry_display: str,
|
||||||
limit_order_id: Any = None,
|
limit_order_id: Any = None,
|
||||||
created_at: Any = None,
|
created_at: Any = None,
|
||||||
now: Optional[datetime] = None,
|
now: Optional[datetime] = None,
|
||||||
hours: int = FALSE_BREAKOUT_VALIDITY_HOURS,
|
hours: int = FALSE_BREAKOUT_VALIDITY_HOURS,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""假突破门控预览:限价挂单状态,不使用箱体/收敛的量破幅二确门控。"""
|
"""假突破门控预览:限价挂单状态,不使用箱体/收敛的量破幅二确门控。"""
|
||||||
now_dt = now or datetime.now()
|
now_dt = now or datetime.now()
|
||||||
expired = is_false_breakout_expired(created_at, now_dt, hours=hours)
|
expired = is_false_breakout_expired(created_at, now_dt, hours=hours)
|
||||||
exp_txt = expires_at_text(created_at, hours=hours)
|
exp_txt = expires_at_text(created_at, hours=hours)
|
||||||
status = "已过期" if expired else "等待成交"
|
status = "已过期" if expired else "等待成交"
|
||||||
metrics_parts: list[str] = []
|
metrics_parts: list[str] = []
|
||||||
oid = str(limit_order_id or "").strip()
|
oid = str(limit_order_id or "").strip()
|
||||||
if oid:
|
if oid:
|
||||||
metrics_parts.append(f"限价单:{oid}")
|
metrics_parts.append(f"限价单:{oid}")
|
||||||
if exp_txt != "—":
|
if exp_txt != "—":
|
||||||
metrics_parts.append(f"截至:{exp_txt}")
|
metrics_parts.append(f"截至:{exp_txt}")
|
||||||
return {
|
return {
|
||||||
"summary": f"假突破 挂E={entry_display} {status}",
|
"summary": f"假突破 挂E={entry_display} {status}",
|
||||||
"metrics": " ".join(metrics_parts),
|
"metrics": " ".join(metrics_parts),
|
||||||
"gate_ok": not expired,
|
"gate_ok": not expired,
|
||||||
}
|
}
|
||||||
@@ -1,140 +1,140 @@
|
|||||||
"""斐波关键位监控:纯计算与类型判断(Gate / Binance 主站共用)。"""
|
"""斐波关键位监控:纯计算与类型判断(Gate / Binance 主站共用)。"""
|
||||||
|
|
||||||
from key_monitor_lib import KEY_MONITOR_AUTO_TYPES
|
from lib.key_monitor.key_monitor_lib import KEY_MONITOR_AUTO_TYPES
|
||||||
|
|
||||||
FIB_KEY_MONITOR_TYPES = frozenset({"斐波回调0.618", "斐波回调0.786"})
|
FIB_KEY_MONITOR_TYPES = frozenset({"斐波回调0.618", "斐波回调0.786"})
|
||||||
KEY_MONITOR_TRADE_TYPE = "关键位监控"
|
KEY_MONITOR_TRADE_TYPE = "关键位监控"
|
||||||
|
|
||||||
FIB_RATIO_BY_TYPE = {
|
FIB_RATIO_BY_TYPE = {
|
||||||
"斐波回调0.618": 0.618,
|
"斐波回调0.618": 0.618,
|
||||||
"斐波回调0.786": 0.786,
|
"斐波回调0.786": 0.786,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def is_fib_key_monitor_type(monitor_type):
|
def is_fib_key_monitor_type(monitor_type):
|
||||||
return (monitor_type or "").strip() in FIB_KEY_MONITOR_TYPES
|
return (monitor_type or "").strip() in FIB_KEY_MONITOR_TYPES
|
||||||
|
|
||||||
|
|
||||||
def fib_ratio_from_type(monitor_type):
|
def fib_ratio_from_type(monitor_type):
|
||||||
return FIB_RATIO_BY_TYPE.get((monitor_type or "").strip())
|
return FIB_RATIO_BY_TYPE.get((monitor_type or "").strip())
|
||||||
|
|
||||||
|
|
||||||
def calc_fib_plan(direction, upper, lower, ratio):
|
def calc_fib_plan(direction, upper, lower, ratio):
|
||||||
"""
|
"""
|
||||||
上沿 H、下沿 L(H > L)。
|
上沿 H、下沿 L(H > L)。
|
||||||
做多:自 H 向下回撤 ratio,E = H - ratio*(H-L);SL=L,TP=H。
|
做多:自 H 向下回撤 ratio,E = H - ratio*(H-L);SL=L,TP=H。
|
||||||
做空:自 L 向上反弹 ratio,E = L + ratio*(H-L);SL=H,TP=L。
|
做空:自 L 向上反弹 ratio,E = L + ratio*(H-L);SL=H,TP=L。
|
||||||
返回 (entry, stop_loss, take_profit) 或 None。
|
返回 (entry, stop_loss, take_profit) 或 None。
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
h = float(upper)
|
h = float(upper)
|
||||||
l = float(lower)
|
l = float(lower)
|
||||||
r = float(ratio)
|
r = float(ratio)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None
|
return None
|
||||||
if h <= l or r <= 0 or r >= 1:
|
if h <= l or r <= 0 or r >= 1:
|
||||||
return None
|
return None
|
||||||
span = h - l
|
span = h - l
|
||||||
direction = (direction or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
if direction == "short":
|
if direction == "short":
|
||||||
entry = l + r * span
|
entry = l + r * span
|
||||||
return entry, h, l
|
return entry, h, l
|
||||||
entry = h - r * span
|
entry = h - r * span
|
||||||
return entry, l, h
|
return entry, l, h
|
||||||
|
|
||||||
|
|
||||||
def stored_key_signal_type(monitor_type):
|
def stored_key_signal_type(monitor_type):
|
||||||
"""写入 order_monitors / trade_records 的 key_signal_type(箱体/收敛/斐波/假突破/触价开仓)。"""
|
"""写入 order_monitors / trade_records 的 key_signal_type(箱体/收敛/斐波/假突破/触价开仓)。"""
|
||||||
mt = (monitor_type or "").strip()
|
mt = (monitor_type or "").strip()
|
||||||
if mt in FIB_KEY_MONITOR_TYPES:
|
if mt in FIB_KEY_MONITOR_TYPES:
|
||||||
return mt
|
return mt
|
||||||
if mt in ("假突破", "回调触价开仓", "突破触价开仓", "触价开仓"):
|
if mt in ("假突破", "回调触价开仓", "突破触价开仓", "触价开仓"):
|
||||||
return mt if mt != "触价开仓" else "回调触价开仓"
|
return mt if mt != "触价开仓" else "回调触价开仓"
|
||||||
if mt in KEY_MONITOR_AUTO_TYPES:
|
if mt in KEY_MONITOR_AUTO_TYPES:
|
||||||
return mt
|
return mt
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
KEY_ENTRY_REASON_BY_SIGNAL = {
|
KEY_ENTRY_REASON_BY_SIGNAL = {
|
||||||
"箱体突破": "关键位箱体突破",
|
"箱体突破": "关键位箱体突破",
|
||||||
"收敛突破": "关键位收敛突破",
|
"收敛突破": "关键位收敛突破",
|
||||||
"斐波回调0.618": "关键位斐波0.618",
|
"斐波回调0.618": "关键位斐波0.618",
|
||||||
"斐波回调0.786": "关键位斐波0.786",
|
"斐波回调0.786": "关键位斐波0.786",
|
||||||
"假突破": "关键位假突破",
|
"假突破": "关键位假突破",
|
||||||
"回调触价开仓": "关键位回调触价开仓",
|
"回调触价开仓": "关键位回调触价开仓",
|
||||||
"突破触价开仓": "关键位突破触价开仓",
|
"突破触价开仓": "关键位突破触价开仓",
|
||||||
"触价开仓": "关键位触价开仓",
|
"触价开仓": "关键位触价开仓",
|
||||||
"趋势回调": "趋势回调",
|
"趋势回调": "趋势回调",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def entry_reason_from_key_signal(key_signal_type):
|
def entry_reason_from_key_signal(key_signal_type):
|
||||||
return KEY_ENTRY_REASON_BY_SIGNAL.get((key_signal_type or "").strip())
|
return KEY_ENTRY_REASON_BY_SIGNAL.get((key_signal_type or "").strip())
|
||||||
|
|
||||||
|
|
||||||
def key_signal_type_for_trade_record(key_signal_type, box_auto_types):
|
def key_signal_type_for_trade_record(key_signal_type, box_auto_types):
|
||||||
"""平仓写入 trade_records 时保留箱体/收敛/斐波/假突破来源。"""
|
"""平仓写入 trade_records 时保留箱体/收敛/斐波/假突破来源。"""
|
||||||
kst = (key_signal_type or "").strip()
|
kst = (key_signal_type or "").strip()
|
||||||
if kst in FIB_KEY_MONITOR_TYPES:
|
if kst in FIB_KEY_MONITOR_TYPES:
|
||||||
return kst
|
return kst
|
||||||
if kst in ("假突破", "回调触价开仓", "突破触价开仓", "触价开仓"):
|
if kst in ("假突破", "回调触价开仓", "突破触价开仓", "触价开仓"):
|
||||||
return kst if kst != "触价开仓" else "回调触价开仓"
|
return kst if kst != "触价开仓" else "回调触价开仓"
|
||||||
if box_auto_types and kst in box_auto_types:
|
if box_auto_types and kst in box_auto_types:
|
||||||
return kst
|
return kst
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def backfill_missing_key_signal_types(conn, *, monitor_type: str = KEY_MONITOR_TRADE_TYPE) -> int:
|
def backfill_missing_key_signal_types(conn, *, monitor_type: str = KEY_MONITOR_TRADE_TYPE) -> int:
|
||||||
"""补全历史 trade_records / order_monitors 中缺失的箱体/收敛 key_signal_type。"""
|
"""补全历史 trade_records / order_monitors 中缺失的箱体/收敛 key_signal_type。"""
|
||||||
mt = (monitor_type or KEY_MONITOR_TRADE_TYPE).strip()
|
mt = (monitor_type or KEY_MONITOR_TRADE_TYPE).strip()
|
||||||
updated = 0
|
updated = 0
|
||||||
for signal in KEY_MONITOR_AUTO_TYPES:
|
for signal in KEY_MONITOR_AUTO_TYPES:
|
||||||
entry_reason = KEY_ENTRY_REASON_BY_SIGNAL.get(signal)
|
entry_reason = KEY_ENTRY_REASON_BY_SIGNAL.get(signal)
|
||||||
if entry_reason:
|
if entry_reason:
|
||||||
cur = conn.execute(
|
cur = conn.execute(
|
||||||
"""UPDATE trade_records SET key_signal_type=?
|
"""UPDATE trade_records SET key_signal_type=?
|
||||||
WHERE monitor_type=? AND (key_signal_type IS NULL OR TRIM(key_signal_type)='')
|
WHERE monitor_type=? AND (key_signal_type IS NULL OR TRIM(key_signal_type)='')
|
||||||
AND TRIM(COALESCE(entry_reason, ''))=?""",
|
AND TRIM(COALESCE(entry_reason, ''))=?""",
|
||||||
(signal, mt, entry_reason),
|
(signal, mt, entry_reason),
|
||||||
)
|
)
|
||||||
updated += int(cur.rowcount or 0)
|
updated += int(cur.rowcount or 0)
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"""SELECT id, symbol, opened_at FROM trade_records
|
"""SELECT id, symbol, opened_at FROM trade_records
|
||||||
WHERE monitor_type=? AND (key_signal_type IS NULL OR TRIM(key_signal_type)='')""",
|
WHERE monitor_type=? AND (key_signal_type IS NULL OR TRIM(key_signal_type)='')""",
|
||||||
(mt,),
|
(mt,),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
for row in rows:
|
for row in rows:
|
||||||
# init_db 连接未设 row_factory,结果为 tuple
|
# init_db 连接未设 row_factory,结果为 tuple
|
||||||
rid, sym, opened_at = row[0], row[1], row[2]
|
rid, sym, opened_at = row[0], row[1], row[2]
|
||||||
opened = (opened_at or "").strip()
|
opened = (opened_at or "").strip()
|
||||||
for signal in KEY_MONITOR_AUTO_TYPES:
|
for signal in KEY_MONITOR_AUTO_TYPES:
|
||||||
hist = conn.execute(
|
hist = conn.execute(
|
||||||
"""SELECT monitor_type FROM key_monitor_history
|
"""SELECT monitor_type FROM key_monitor_history
|
||||||
WHERE symbol=? AND monitor_type=? AND close_reason='auto_opened'
|
WHERE symbol=? AND monitor_type=? AND close_reason='auto_opened'
|
||||||
AND (?='' OR closed_at <= ?)
|
AND (?='' OR closed_at <= ?)
|
||||||
ORDER BY closed_at DESC LIMIT 1""",
|
ORDER BY closed_at DESC LIMIT 1""",
|
||||||
(sym, signal, opened, opened),
|
(sym, signal, opened, opened),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if not hist:
|
if not hist:
|
||||||
continue
|
continue
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE trade_records SET key_signal_type=? WHERE id=?",
|
"UPDATE trade_records SET key_signal_type=? WHERE id=?",
|
||||||
(signal, rid),
|
(signal, rid),
|
||||||
)
|
)
|
||||||
updated += 1
|
updated += 1
|
||||||
break
|
break
|
||||||
return updated
|
return updated
|
||||||
|
|
||||||
|
|
||||||
def fib_invalidate_by_mark(direction, mark_price, upper, lower):
|
def fib_invalidate_by_mark(direction, mark_price, upper, lower):
|
||||||
"""先触达止盈侧(标记价)则失效。多:mark>=H;空:mark<=L。"""
|
"""先触达止盈侧(标记价)则失效。多:mark>=H;空:mark<=L。"""
|
||||||
try:
|
try:
|
||||||
m = float(mark_price)
|
m = float(mark_price)
|
||||||
h = float(upper)
|
h = float(upper)
|
||||||
l = float(lower)
|
l = float(lower)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return False
|
return False
|
||||||
direction = (direction or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
if direction == "short":
|
if direction == "short":
|
||||||
return m <= l
|
return m <= l
|
||||||
return m >= h
|
return m >= h
|
||||||
@@ -1,61 +1,61 @@
|
|||||||
"""
|
"""
|
||||||
全仓杠杆模式下:撤销已添加的箱体/收敛/斐波关键位监控并微信说明。
|
全仓杠杆模式下:撤销已添加的箱体/收敛/斐波关键位监控并微信说明。
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Callable, Iterable, Optional
|
from typing import Any, Callable, Iterable, Optional
|
||||||
|
|
||||||
from fib_key_monitor_lib import FIB_KEY_MONITOR_TYPES, is_fib_key_monitor_type
|
from lib.key_monitor.fib_key_monitor_lib import FIB_KEY_MONITOR_TYPES, is_fib_key_monitor_type
|
||||||
from false_breakout_key_monitor_lib import is_false_breakout_key_monitor_type
|
from lib.key_monitor.false_breakout_key_monitor_lib import is_false_breakout_key_monitor_type
|
||||||
from key_monitor_lib import KEY_MONITOR_AUTO_TYPES
|
from lib.key_monitor.key_monitor_lib import KEY_MONITOR_AUTO_TYPES
|
||||||
from position_sizing_lib import is_full_margin_mode, mode_label_zh
|
from lib.trade.position_sizing_lib import is_full_margin_mode, mode_label_zh
|
||||||
|
|
||||||
|
|
||||||
def monitor_type_disallowed_in_full_margin(monitor_type: str) -> bool:
|
def monitor_type_disallowed_in_full_margin(monitor_type: str) -> bool:
|
||||||
mt = (monitor_type or "").strip()
|
mt = (monitor_type or "").strip()
|
||||||
if mt in KEY_MONITOR_AUTO_TYPES:
|
if mt in KEY_MONITOR_AUTO_TYPES:
|
||||||
return True
|
return True
|
||||||
if is_fib_key_monitor_type(mt):
|
if is_fib_key_monitor_type(mt):
|
||||||
return True
|
return True
|
||||||
return is_false_breakout_key_monitor_type(mt)
|
return is_false_breakout_key_monitor_type(mt)
|
||||||
|
|
||||||
|
|
||||||
def purge_disallowed_key_monitors(
|
def purge_disallowed_key_monitors(
|
||||||
conn: Any,
|
conn: Any,
|
||||||
*,
|
*,
|
||||||
sizing_mode: str,
|
sizing_mode: str,
|
||||||
select_rows: Callable[[Any], Iterable[Any]],
|
select_rows: Callable[[Any], Iterable[Any]],
|
||||||
cancel_fib_limit: Callable[[Any], None],
|
cancel_fib_limit: Callable[[Any], None],
|
||||||
delete_monitor: Callable[[Any, int], None],
|
delete_monitor: Callable[[Any, int], None],
|
||||||
send_wechat: Callable[[str], None],
|
send_wechat: Callable[[str], None],
|
||||||
row_symbol: Callable[[Any], str] = lambda r: str(r["symbol"] or ""),
|
row_symbol: Callable[[Any], str] = lambda r: str(r["symbol"] or ""),
|
||||||
row_monitor_type: Callable[[Any], str] = lambda r: str(r["monitor_type"] or ""),
|
row_monitor_type: Callable[[Any], str] = lambda r: str(r["monitor_type"] or ""),
|
||||||
row_id: Callable[[Any], int] = lambda r: int(r["id"]),
|
row_id: Callable[[Any], int] = lambda r: int(r["id"]),
|
||||||
) -> int:
|
) -> int:
|
||||||
if not is_full_margin_mode(sizing_mode):
|
if not is_full_margin_mode(sizing_mode):
|
||||||
return 0
|
return 0
|
||||||
removed = []
|
removed = []
|
||||||
for row in select_rows(conn):
|
for row in select_rows(conn):
|
||||||
mt = row_monitor_type(row)
|
mt = row_monitor_type(row)
|
||||||
if not monitor_type_disallowed_in_full_margin(mt):
|
if not monitor_type_disallowed_in_full_margin(mt):
|
||||||
continue
|
continue
|
||||||
sym = row_symbol(row)
|
sym = row_symbol(row)
|
||||||
kid = row_id(row)
|
kid = row_id(row)
|
||||||
if is_fib_key_monitor_type(mt) or is_false_breakout_key_monitor_type(mt):
|
if is_fib_key_monitor_type(mt) or is_false_breakout_key_monitor_type(mt):
|
||||||
try:
|
try:
|
||||||
cancel_fib_limit(row)
|
cancel_fib_limit(row)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
delete_monitor(conn, kid)
|
delete_monitor(conn, kid)
|
||||||
removed.append((sym, mt, kid))
|
removed.append((sym, mt, kid))
|
||||||
if removed:
|
if removed:
|
||||||
lines = [f"· {s} {t} (#{i})" for s, t, i in removed[:12]]
|
lines = [f"· {s} {t} (#{i})" for s, t, i in removed[:12]]
|
||||||
if len(removed) > 12:
|
if len(removed) > 12:
|
||||||
lines.append(f"… 共 {len(removed)} 条")
|
lines.append(f"… 共 {len(removed)} 条")
|
||||||
send_wechat(
|
send_wechat(
|
||||||
"# ⚠️ 全仓杠杆模式:已自动撤销关键位监控\n"
|
"# ⚠️ 全仓杠杆模式:已自动撤销关键位监控\n"
|
||||||
f"计仓模式:{mode_label_zh(sizing_mode)}(仅 env 可切换,须无仓)\n"
|
f"计仓模式:{mode_label_zh(sizing_mode)}(仅 env 可切换,须无仓)\n"
|
||||||
"已撤销:箱体突破 / 收敛突破 / 斐波回调 / 假突破监控(不可与全仓杠杆并存)\n"
|
"已撤销:箱体突破 / 收敛突破 / 斐波回调 / 假突破监控(不可与全仓杠杆并存)\n"
|
||||||
+ "\n".join(lines)
|
+ "\n".join(lines)
|
||||||
)
|
)
|
||||||
return len(removed)
|
return len(removed)
|
||||||
@@ -1,390 +1,390 @@
|
|||||||
"""
|
"""
|
||||||
关键位监控:阻力/支撑双向提醒与箱体/收敛自动门控的共享逻辑。
|
关键位监控:阻力/支撑双向提醒与箱体/收敛自动门控的共享逻辑。
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
KEY_MONITOR_AUTO_TYPES = frozenset({"箱体突破", "收敛突破"})
|
KEY_MONITOR_AUTO_TYPES = frozenset({"箱体突破", "收敛突破"})
|
||||||
KEY_MONITOR_RS_TYPE = "关键支撑阻力"
|
KEY_MONITOR_RS_TYPE = "关键支撑阻力"
|
||||||
KEY_MONITOR_RS_LEGACY_TYPES = frozenset({"关键阻力位", "关键支撑位"})
|
KEY_MONITOR_RS_LEGACY_TYPES = frozenset({"关键阻力位", "关键支撑位"})
|
||||||
KEY_MONITOR_RS_TYPES = frozenset({KEY_MONITOR_RS_TYPE}) | KEY_MONITOR_RS_LEGACY_TYPES
|
KEY_MONITOR_RS_TYPES = frozenset({KEY_MONITOR_RS_TYPE}) | KEY_MONITOR_RS_LEGACY_TYPES
|
||||||
KEY_MONITOR_ALERT_ONLY_TYPES = frozenset({KEY_MONITOR_RS_TYPE}) | KEY_MONITOR_RS_LEGACY_TYPES
|
KEY_MONITOR_ALERT_ONLY_TYPES = frozenset({KEY_MONITOR_RS_TYPE}) | KEY_MONITOR_RS_LEGACY_TYPES
|
||||||
KEY_DIRECTION_WATCH = "watch"
|
KEY_DIRECTION_WATCH = "watch"
|
||||||
|
|
||||||
|
|
||||||
def is_rs_key_monitor_type(monitor_type: str) -> bool:
|
def is_rs_key_monitor_type(monitor_type: str) -> bool:
|
||||||
return (monitor_type or "").strip() in KEY_MONITOR_RS_TYPES
|
return (monitor_type or "").strip() in KEY_MONITOR_RS_TYPES
|
||||||
|
|
||||||
|
|
||||||
def rs_monitor_type_label(monitor_type: str) -> str:
|
def rs_monitor_type_label(monitor_type: str) -> str:
|
||||||
"""展示用:旧库里的阻力/支撑合并为「关键支撑阻力」。"""
|
"""展示用:旧库里的阻力/支撑合并为「关键支撑阻力」。"""
|
||||||
if is_rs_key_monitor_type(monitor_type):
|
if is_rs_key_monitor_type(monitor_type):
|
||||||
return KEY_MONITOR_RS_TYPE
|
return KEY_MONITOR_RS_TYPE
|
||||||
return (monitor_type or "").strip()
|
return (monitor_type or "").strip()
|
||||||
|
|
||||||
|
|
||||||
def rs_monitor_type_for_storage(monitor_type: str) -> str:
|
def rs_monitor_type_for_storage(monitor_type: str) -> str:
|
||||||
if is_rs_key_monitor_type(monitor_type):
|
if is_rs_key_monitor_type(monitor_type):
|
||||||
return KEY_MONITOR_RS_TYPE
|
return KEY_MONITOR_RS_TYPE
|
||||||
return (monitor_type or "").strip()
|
return (monitor_type or "").strip()
|
||||||
|
|
||||||
|
|
||||||
def calc_breakout_breach_pct(direction: str, close: float, upper: float, lower: float) -> float:
|
def calc_breakout_breach_pct(direction: str, close: float, upper: float, lower: float) -> float:
|
||||||
"""突破 K 收盘相对关键位的越过幅度(%)。未越过对应边界时返回 0。"""
|
"""突破 K 收盘相对关键位的越过幅度(%)。未越过对应边界时返回 0。"""
|
||||||
direction = (direction or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
c = float(close)
|
c = float(close)
|
||||||
if direction == "long":
|
if direction == "long":
|
||||||
u = float(upper)
|
u = float(upper)
|
||||||
if u <= 0 or c <= u:
|
if u <= 0 or c <= u:
|
||||||
return 0.0
|
return 0.0
|
||||||
return (c - u) / u * 100.0
|
return (c - u) / u * 100.0
|
||||||
lo = float(lower)
|
lo = float(lower)
|
||||||
if lo <= 0 or c >= lo:
|
if lo <= 0 or c >= lo:
|
||||||
return 0.0
|
return 0.0
|
||||||
return (lo - c) / lo * 100.0
|
return (lo - c) / lo * 100.0
|
||||||
|
|
||||||
|
|
||||||
def auto_amp_ok(
|
def auto_amp_ok(
|
||||||
direction: str,
|
direction: str,
|
||||||
close_b: float,
|
close_b: float,
|
||||||
upper: float,
|
upper: float,
|
||||||
lower: float,
|
lower: float,
|
||||||
min_pct: float,
|
min_pct: float,
|
||||||
) -> tuple[bool, float]:
|
) -> tuple[bool, float]:
|
||||||
breach = calc_breakout_breach_pct(direction, close_b, upper, lower)
|
breach = calc_breakout_breach_pct(direction, close_b, upper, lower)
|
||||||
return breach > float(min_pct), breach
|
return breach > float(min_pct), breach
|
||||||
|
|
||||||
|
|
||||||
def auto_confirm_ok(direction: str, cfm_close: float, upper: float, lower: float) -> bool:
|
def auto_confirm_ok(direction: str, cfm_close: float, upper: float, lower: float) -> bool:
|
||||||
"""确认 K 收盘须在箱体外(不得回到 [lower, upper] 内)。"""
|
"""确认 K 收盘须在箱体外(不得回到 [lower, upper] 内)。"""
|
||||||
direction = (direction or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
c = float(cfm_close)
|
c = float(cfm_close)
|
||||||
if direction == "long":
|
if direction == "long":
|
||||||
return c > float(upper)
|
return c > float(upper)
|
||||||
return c < float(lower)
|
return c < float(lower)
|
||||||
|
|
||||||
|
|
||||||
BOX_BREAKOUT_CLOSE_OPPOSITE = "box_opposite_break"
|
BOX_BREAKOUT_CLOSE_OPPOSITE = "box_opposite_break"
|
||||||
|
|
||||||
|
|
||||||
def box_breakout_invalidate_by_mark(
|
def box_breakout_invalidate_by_mark(
|
||||||
direction: str, mark_price: float, upper: float, lower: float
|
direction: str, mark_price: float, upper: float, lower: float
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""箱体/收敛:标记价先突破反向边界则失效。多:mark<=L;空:mark>=H。"""
|
"""箱体/收敛:标记价先突破反向边界则失效。多:mark<=L;空:mark>=H。"""
|
||||||
try:
|
try:
|
||||||
m = float(mark_price)
|
m = float(mark_price)
|
||||||
h = float(upper)
|
h = float(upper)
|
||||||
lo = float(lower)
|
lo = float(lower)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return False
|
return False
|
||||||
direction = (direction or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
if direction == "short":
|
if direction == "short":
|
||||||
return m >= h
|
return m >= h
|
||||||
return m <= lo
|
return m <= lo
|
||||||
|
|
||||||
|
|
||||||
def box_breakout_invalidate_edge_label(direction: str) -> str:
|
def box_breakout_invalidate_edge_label(direction: str) -> str:
|
||||||
direction = (direction or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
return "下沿" if direction == "long" else "上沿"
|
return "下沿" if direction == "long" else "上沿"
|
||||||
|
|
||||||
|
|
||||||
def detect_rs_box_break(close: float, upper: float, lower: float) -> Optional[dict[str, Any]]:
|
def detect_rs_box_break(close: float, upper: float, lower: float) -> Optional[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
阻力/支撑人工盯盘:最近 5m 收盘突破上沿或下沿(严格 > / <)。
|
阻力/支撑人工盯盘:最近 5m 收盘突破上沿或下沿(严格 > / <)。
|
||||||
上沿优先:同一根 K 不可能同时满足两者。
|
上沿优先:同一根 K 不可能同时满足两者。
|
||||||
"""
|
"""
|
||||||
u, lo, c = float(upper), float(lower), float(close)
|
u, lo, c = float(upper), float(lower), float(close)
|
||||||
if c > u:
|
if c > u:
|
||||||
return {
|
return {
|
||||||
"break_side": "upper",
|
"break_side": "upper",
|
||||||
"direction": "long",
|
"direction": "long",
|
||||||
"edge_price": u,
|
"edge_price": u,
|
||||||
"key_price": u,
|
"key_price": u,
|
||||||
"break_label": "向上突破上沿",
|
"break_label": "向上突破上沿",
|
||||||
}
|
}
|
||||||
if c < lo:
|
if c < lo:
|
||||||
return {
|
return {
|
||||||
"break_side": "lower",
|
"break_side": "lower",
|
||||||
"direction": "short",
|
"direction": "short",
|
||||||
"edge_price": lo,
|
"edge_price": lo,
|
||||||
"key_price": lo,
|
"key_price": lo,
|
||||||
"break_label": "向下突破下沿",
|
"break_label": "向下突破下沿",
|
||||||
}
|
}
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def rs_break_from_direction(direction: str, upper: float, lower: float) -> Optional[dict[str, Any]]:
|
def rs_break_from_direction(direction: str, upper: float, lower: float) -> Optional[dict[str, Any]]:
|
||||||
"""已触发后根据入库方向还原突破边(long=上沿,short=下沿)。"""
|
"""已触发后根据入库方向还原突破边(long=上沿,short=下沿)。"""
|
||||||
d = (direction or "").strip().lower()
|
d = (direction or "").strip().lower()
|
||||||
if d == "long":
|
if d == "long":
|
||||||
return {
|
return {
|
||||||
"break_side": "upper",
|
"break_side": "upper",
|
||||||
"direction": "long",
|
"direction": "long",
|
||||||
"edge_price": float(upper),
|
"edge_price": float(upper),
|
||||||
"key_price": float(upper),
|
"key_price": float(upper),
|
||||||
"break_label": "向上突破上沿",
|
"break_label": "向上突破上沿",
|
||||||
}
|
}
|
||||||
if d == "short":
|
if d == "short":
|
||||||
return {
|
return {
|
||||||
"break_side": "lower",
|
"break_side": "lower",
|
||||||
"direction": "short",
|
"direction": "short",
|
||||||
"edge_price": float(lower),
|
"edge_price": float(lower),
|
||||||
"key_price": float(lower),
|
"key_price": float(lower),
|
||||||
"break_label": "向下突破下沿",
|
"break_label": "向下突破下沿",
|
||||||
}
|
}
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def rs_break_infer_from_close(close: float, upper: float, lower: float) -> dict[str, Any]:
|
def rs_break_infer_from_close(close: float, upper: float, lower: float) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
续发提醒时价格已回到箱体内:按收盘价相对箱体中线推断首次突破边,
|
续发提醒时价格已回到箱体内:按收盘价相对箱体中线推断首次突破边,
|
||||||
保证第 2/3 次企业微信提醒仍能发出。
|
保证第 2/3 次企业微信提醒仍能发出。
|
||||||
"""
|
"""
|
||||||
mid = (float(upper) + float(lower)) / 2.0
|
mid = (float(upper) + float(lower)) / 2.0
|
||||||
if float(close) >= mid:
|
if float(close) >= mid:
|
||||||
br = rs_break_from_direction("long", upper, lower)
|
br = rs_break_from_direction("long", upper, lower)
|
||||||
else:
|
else:
|
||||||
br = rs_break_from_direction("short", upper, lower)
|
br = rs_break_from_direction("short", upper, lower)
|
||||||
if br:
|
if br:
|
||||||
return br
|
return br
|
||||||
return {
|
return {
|
||||||
"break_side": "upper",
|
"break_side": "upper",
|
||||||
"direction": "long",
|
"direction": "long",
|
||||||
"edge_price": float(upper),
|
"edge_price": float(upper),
|
||||||
"key_price": float(upper),
|
"key_price": float(upper),
|
||||||
"break_label": "向上突破上沿",
|
"break_label": "向上突破上沿",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _parse_notify_datetime(raw: Optional[str]) -> Optional[datetime]:
|
def _parse_notify_datetime(raw: Optional[str]) -> Optional[datetime]:
|
||||||
s = str(raw or "").strip()
|
s = str(raw or "").strip()
|
||||||
if not s:
|
if not s:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
dt = datetime.fromisoformat(s.replace("Z", "+00:00"))
|
dt = datetime.fromisoformat(s.replace("Z", "+00:00"))
|
||||||
if dt.tzinfo is not None:
|
if dt.tzinfo is not None:
|
||||||
dt = dt.replace(tzinfo=None)
|
dt = dt.replace(tzinfo=None)
|
||||||
return dt
|
return dt
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"):
|
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"):
|
||||||
try:
|
try:
|
||||||
return datetime.strptime(s[:19], fmt)
|
return datetime.strptime(s[:19], fmt)
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def claim_rs_level_notify(
|
def claim_rs_level_notify(
|
||||||
conn: Any,
|
conn: Any,
|
||||||
monitor_id: int,
|
monitor_id: int,
|
||||||
notify_index: int,
|
notify_index: int,
|
||||||
direction: str,
|
direction: str,
|
||||||
notified_at: str,
|
notified_at: str,
|
||||||
bar_ts: Optional[int],
|
bar_ts: Optional[int],
|
||||||
*,
|
*,
|
||||||
prior_count: Optional[int] = None,
|
prior_count: Optional[int] = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
原子占位:仅在 notification_count 仍为 prior_count 时推进到 notify_index。
|
原子占位:仅在 notification_count 仍为 prior_count 时推进到 notify_index。
|
||||||
须在发送企业微信之前调用并 commit,避免 (2/3) 重复刷屏。
|
须在发送企业微信之前调用并 commit,避免 (2/3) 重复刷屏。
|
||||||
"""
|
"""
|
||||||
prior = int(prior_count if prior_count is not None else notify_index - 1)
|
prior = int(prior_count if prior_count is not None else notify_index - 1)
|
||||||
if prior < 0 or notify_index != prior + 1:
|
if prior < 0 or notify_index != prior + 1:
|
||||||
return False
|
return False
|
||||||
bar_val: Optional[int] = None
|
bar_val: Optional[int] = None
|
||||||
if bar_ts is not None:
|
if bar_ts is not None:
|
||||||
try:
|
try:
|
||||||
bar_val = int(bar_ts)
|
bar_val = int(bar_ts)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
bar_val = None
|
bar_val = None
|
||||||
cur = conn.execute(
|
cur = conn.execute(
|
||||||
"UPDATE key_monitors SET notification_count=?, direction=?, last_notified_at=?, last_rs_bar_ts=? "
|
"UPDATE key_monitors SET notification_count=?, direction=?, last_notified_at=?, last_rs_bar_ts=? "
|
||||||
"WHERE id=? AND COALESCE(notification_count,0)=?",
|
"WHERE id=? AND COALESCE(notification_count,0)=?",
|
||||||
(notify_index, direction, notified_at, bar_val, int(monitor_id), prior),
|
(notify_index, direction, notified_at, bar_val, int(monitor_id), prior),
|
||||||
)
|
)
|
||||||
return int(cur.rowcount or 0) > 0
|
return int(cur.rowcount or 0) > 0
|
||||||
|
|
||||||
|
|
||||||
def parse_last_rs_bar_ts(row: Any) -> Optional[int]:
|
def parse_last_rs_bar_ts(row: Any) -> Optional[int]:
|
||||||
if row is None:
|
if row is None:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
keys = row.keys() if hasattr(row, "keys") else []
|
keys = row.keys() if hasattr(row, "keys") else []
|
||||||
except Exception:
|
except Exception:
|
||||||
keys = []
|
keys = []
|
||||||
raw = row["last_rs_bar_ts"] if "last_rs_bar_ts" in keys else None
|
raw = row["last_rs_bar_ts"] if "last_rs_bar_ts" in keys else None
|
||||||
if raw is None:
|
if raw is None:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
return int(raw)
|
return int(raw)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def run_rs_level_alert_tick(
|
def run_rs_level_alert_tick(
|
||||||
row: Any,
|
row: Any,
|
||||||
close: float,
|
close: float,
|
||||||
bar_ts: Optional[int],
|
bar_ts: Optional[int],
|
||||||
now_dt: datetime,
|
now_dt: datetime,
|
||||||
*,
|
*,
|
||||||
default_max_notify: int,
|
default_max_notify: int,
|
||||||
default_interval_min: int,
|
default_interval_min: int,
|
||||||
) -> Optional[dict[str, Any]]:
|
) -> Optional[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
判定本轮回合是否应推送阻力/支撑提醒。
|
判定本轮回合是否应推送阻力/支撑提醒。
|
||||||
首条:仅在新闭合 K 越线时触发;发送前须 claim_rs_level_notify 占位防轮询/多进程重复。
|
首条:仅在新闭合 K 越线时触发;发送前须 claim_rs_level_notify 占位防轮询/多进程重复。
|
||||||
"""
|
"""
|
||||||
up, lo = float(row["upper"]), float(row["lower"])
|
up, lo = float(row["upper"]), float(row["lower"])
|
||||||
if up <= lo:
|
if up <= lo:
|
||||||
return None
|
return None
|
||||||
count = int(row["notification_count"] or 0)
|
count = int(row["notification_count"] or 0)
|
||||||
max_n = max(1, int(row["max_notify"] or default_max_notify))
|
max_n = max(1, int(row["max_notify"] or default_max_notify))
|
||||||
interval = max(1, int(row["notify_interval_min"] or default_interval_min))
|
interval = max(1, int(row["notify_interval_min"] or default_interval_min))
|
||||||
if count >= max_n:
|
if count >= max_n:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
bar_ts_i: Optional[int] = None
|
bar_ts_i: Optional[int] = None
|
||||||
if bar_ts is not None:
|
if bar_ts is not None:
|
||||||
try:
|
try:
|
||||||
bar_ts_i = int(bar_ts)
|
bar_ts_i = int(bar_ts)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
bar_ts_i = None
|
bar_ts_i = None
|
||||||
last_bar_i = parse_last_rs_bar_ts(row)
|
last_bar_i = parse_last_rs_bar_ts(row)
|
||||||
|
|
||||||
if count == 0:
|
if count == 0:
|
||||||
br = detect_rs_box_break(close, up, lo)
|
br = detect_rs_box_break(close, up, lo)
|
||||||
if not br:
|
if not br:
|
||||||
return None
|
return None
|
||||||
if bar_ts_i is not None and last_bar_i is not None and bar_ts_i == last_bar_i:
|
if bar_ts_i is not None and last_bar_i is not None and bar_ts_i == last_bar_i:
|
||||||
return None
|
return None
|
||||||
return {
|
return {
|
||||||
"break_info": br,
|
"break_info": br,
|
||||||
"notify_index": 1,
|
"notify_index": 1,
|
||||||
"prior_count": 0,
|
"prior_count": 0,
|
||||||
"notify_max": max_n,
|
"notify_max": max_n,
|
||||||
"interval_min": interval,
|
"interval_min": interval,
|
||||||
"bar_ts": bar_ts_i,
|
"bar_ts": bar_ts_i,
|
||||||
}
|
}
|
||||||
|
|
||||||
if not notify_interval_elapsed(row["last_notified_at"], interval, now_dt):
|
if not notify_interval_elapsed(row["last_notified_at"], interval, now_dt):
|
||||||
return None
|
return None
|
||||||
br = resolve_rs_break_for_alert(count, row["direction"], close, up, lo)
|
br = resolve_rs_break_for_alert(count, row["direction"], close, up, lo)
|
||||||
if not br:
|
if not br:
|
||||||
return None
|
return None
|
||||||
return {
|
return {
|
||||||
"break_info": br,
|
"break_info": br,
|
||||||
"notify_index": count + 1,
|
"notify_index": count + 1,
|
||||||
"prior_count": count,
|
"prior_count": count,
|
||||||
"notify_max": max_n,
|
"notify_max": max_n,
|
||||||
"interval_min": interval,
|
"interval_min": interval,
|
||||||
"bar_ts": bar_ts_i,
|
"bar_ts": bar_ts_i,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def resolve_rs_break_for_alert(
|
def resolve_rs_break_for_alert(
|
||||||
notification_count: int,
|
notification_count: int,
|
||||||
direction: Optional[str],
|
direction: Optional[str],
|
||||||
close: float,
|
close: float,
|
||||||
upper: float,
|
upper: float,
|
||||||
lower: float,
|
lower: float,
|
||||||
) -> Optional[dict[str, Any]]:
|
) -> Optional[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
阻力/支撑提醒:首次用 5m 收盘越线判定;后续用已存方向,兼容 direction=watch。
|
阻力/支撑提醒:首次用 5m 收盘越线判定;后续用已存方向,兼容 direction=watch。
|
||||||
"""
|
"""
|
||||||
count = int(notification_count or 0)
|
count = int(notification_count or 0)
|
||||||
up, lo, c = float(upper), float(lower), float(close)
|
up, lo, c = float(upper), float(lower), float(close)
|
||||||
if count <= 0:
|
if count <= 0:
|
||||||
return detect_rs_box_break(c, up, lo)
|
return detect_rs_box_break(c, up, lo)
|
||||||
br = rs_break_from_direction(direction, up, lo)
|
br = rs_break_from_direction(direction, up, lo)
|
||||||
if br:
|
if br:
|
||||||
return br
|
return br
|
||||||
d = (direction or "").strip().lower()
|
d = (direction or "").strip().lower()
|
||||||
if d not in ("", KEY_DIRECTION_WATCH):
|
if d not in ("", KEY_DIRECTION_WATCH):
|
||||||
return None
|
return None
|
||||||
br = detect_rs_box_break(c, up, lo)
|
br = detect_rs_box_break(c, up, lo)
|
||||||
if br:
|
if br:
|
||||||
return br
|
return br
|
||||||
return rs_break_infer_from_close(c, up, lo)
|
return rs_break_infer_from_close(c, up, lo)
|
||||||
|
|
||||||
|
|
||||||
def notify_interval_elapsed(
|
def notify_interval_elapsed(
|
||||||
last_notified_at: Optional[str],
|
last_notified_at: Optional[str],
|
||||||
interval_min: int,
|
interval_min: int,
|
||||||
now_dt: datetime,
|
now_dt: datetime,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not last_notified_at:
|
if not last_notified_at:
|
||||||
return False
|
return False
|
||||||
last_dt = _parse_notify_datetime(last_notified_at)
|
last_dt = _parse_notify_datetime(last_notified_at)
|
||||||
if last_dt is None:
|
if last_dt is None:
|
||||||
return False
|
return False
|
||||||
return (now_dt - last_dt).total_seconds() >= max(1, int(interval_min)) * 60
|
return (now_dt - last_dt).total_seconds() >= max(1, int(interval_min)) * 60
|
||||||
|
|
||||||
|
|
||||||
def format_auto_amp_line(amp_ok: bool, amp_pct: float, min_pct: float) -> str:
|
def format_auto_amp_line(amp_ok: bool, amp_pct: float, min_pct: float) -> str:
|
||||||
return (
|
return (
|
||||||
f"突破越过幅度:{'通过' if amp_ok else '不通过'}"
|
f"突破越过幅度:{'通过' if amp_ok else '不通过'}"
|
||||||
f"({round(float(amp_pct), 4)}%,要求 > {min_pct}%)"
|
f"({round(float(amp_pct), 4)}%,要求 > {min_pct}%)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def format_auto_confirm_line(confirm_ok: bool, cfm_close, edge_price, direction: str) -> str:
|
def format_auto_confirm_line(confirm_ok: bool, cfm_close, edge_price, direction: str) -> str:
|
||||||
side = "箱外上方" if (direction or "").lower() == "long" else "箱外下方"
|
side = "箱外上方" if (direction or "").lower() == "long" else "箱外下方"
|
||||||
return (
|
return (
|
||||||
f"第二根确认:{'通过' if confirm_ok else '不通过'}"
|
f"第二根确认:{'通过' if confirm_ok else '不通过'}"
|
||||||
f"(确认收盘 {cfm_close},须收于{side},关键位 {edge_price})"
|
f"(确认收盘 {cfm_close},须收于{side},关键位 {edge_price})"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def key_monitor_rule_template_context(
|
def key_monitor_rule_template_context(
|
||||||
*,
|
*,
|
||||||
kline_timeframe: str,
|
kline_timeframe: str,
|
||||||
key_breakout_amp_min_pct: float,
|
key_breakout_amp_min_pct: float,
|
||||||
key_volume_ma_bars: int,
|
key_volume_ma_bars: int,
|
||||||
key_volume_ratio_min: float,
|
key_volume_ratio_min: float,
|
||||||
key_auto_min_planned_rr: float,
|
key_auto_min_planned_rr: float,
|
||||||
key_daily_volume_rank_max: int,
|
key_daily_volume_rank_max: int,
|
||||||
key_confirm_breakout_bar: int,
|
key_confirm_breakout_bar: int,
|
||||||
key_confirm_bar: int,
|
key_confirm_bar: int,
|
||||||
key_alert_max_times: int,
|
key_alert_max_times: int,
|
||||||
key_alert_interval_minutes: int,
|
key_alert_interval_minutes: int,
|
||||||
key_stop_outside_breakout_pct: float,
|
key_stop_outside_breakout_pct: float,
|
||||||
key_trend_stop_outside_pct: float,
|
key_trend_stop_outside_pct: float,
|
||||||
false_breakout_validity_hours: int,
|
false_breakout_validity_hours: int,
|
||||||
trigger_entry_validity_hours: int | None = None,
|
trigger_entry_validity_hours: int | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""关键位监控页规则说明表格(Jinja key_rule_ctx)。"""
|
"""关键位监控页规则说明表格(Jinja key_rule_ctx)。"""
|
||||||
from false_breakout_key_monitor_lib import (
|
from lib.key_monitor.false_breakout_key_monitor_lib import (
|
||||||
FALSE_BREAKOUT_OFFSET_PCT,
|
FALSE_BREAKOUT_OFFSET_PCT,
|
||||||
FALSE_BREAKOUT_RR,
|
FALSE_BREAKOUT_RR,
|
||||||
FALSE_BREAKOUT_SL_PCT,
|
FALSE_BREAKOUT_SL_PCT,
|
||||||
)
|
)
|
||||||
from trigger_entry_key_monitor_lib import TRIGGER_ENTRY_VALIDITY_HOURS
|
from lib.key_monitor.trigger_entry_key_monitor_lib import TRIGGER_ENTRY_VALIDITY_HOURS
|
||||||
|
|
||||||
te_hours = (
|
te_hours = (
|
||||||
int(trigger_entry_validity_hours)
|
int(trigger_entry_validity_hours)
|
||||||
if trigger_entry_validity_hours is not None
|
if trigger_entry_validity_hours is not None
|
||||||
else TRIGGER_ENTRY_VALIDITY_HOURS
|
else TRIGGER_ENTRY_VALIDITY_HOURS
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"tf": (kline_timeframe or "5m").strip(),
|
"tf": (kline_timeframe or "5m").strip(),
|
||||||
"amp_min_pct": key_breakout_amp_min_pct,
|
"amp_min_pct": key_breakout_amp_min_pct,
|
||||||
"vol_ma_bars": key_volume_ma_bars,
|
"vol_ma_bars": key_volume_ma_bars,
|
||||||
"vol_ratio_min": key_volume_ratio_min,
|
"vol_ratio_min": key_volume_ratio_min,
|
||||||
"min_rr": key_auto_min_planned_rr,
|
"min_rr": key_auto_min_planned_rr,
|
||||||
"vol_rank_max": key_daily_volume_rank_max,
|
"vol_rank_max": key_daily_volume_rank_max,
|
||||||
"breakout_bar": key_confirm_breakout_bar,
|
"breakout_bar": key_confirm_breakout_bar,
|
||||||
"confirm_bar": key_confirm_bar,
|
"confirm_bar": key_confirm_bar,
|
||||||
"alert_max": key_alert_max_times,
|
"alert_max": key_alert_max_times,
|
||||||
"alert_interval_min": key_alert_interval_minutes,
|
"alert_interval_min": key_alert_interval_minutes,
|
||||||
"stop_outside_pct": key_stop_outside_breakout_pct,
|
"stop_outside_pct": key_stop_outside_breakout_pct,
|
||||||
"trend_stop_outside_pct": key_trend_stop_outside_pct,
|
"trend_stop_outside_pct": key_trend_stop_outside_pct,
|
||||||
"fb_offset_pct": FALSE_BREAKOUT_OFFSET_PCT,
|
"fb_offset_pct": FALSE_BREAKOUT_OFFSET_PCT,
|
||||||
"fb_sl_pct": FALSE_BREAKOUT_SL_PCT,
|
"fb_sl_pct": FALSE_BREAKOUT_SL_PCT,
|
||||||
"fb_rr": FALSE_BREAKOUT_RR,
|
"fb_rr": FALSE_BREAKOUT_RR,
|
||||||
"fb_valid_hours": false_breakout_validity_hours,
|
"fb_valid_hours": false_breakout_validity_hours,
|
||||||
"trigger_entry_validity_hours": te_hours,
|
"trigger_entry_validity_hours": te_hours,
|
||||||
}
|
}
|
||||||
+296
-296
@@ -1,296 +1,296 @@
|
|||||||
"""回调/突破触价开仓关键位监控:程序盯价、触达计划入场后市价成交(四所共用逻辑)。"""
|
"""回调/突破触价开仓关键位监控:程序盯价、触达计划入场后市价成交(四所共用逻辑)。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
from false_breakout_key_monitor_lib import (
|
from lib.key_monitor.false_breakout_key_monitor_lib import (
|
||||||
_parse_created_at,
|
_parse_created_at,
|
||||||
expires_at_text,
|
expires_at_text,
|
||||||
is_false_breakout_expired,
|
is_false_breakout_expired,
|
||||||
)
|
)
|
||||||
from strategy_trend_lib import trend_dca_level_reached
|
from lib.strategy.strategy_trend_lib import trend_dca_level_reached
|
||||||
|
|
||||||
# 回调触价(原「触价开仓」)
|
# 回调触价(原「触价开仓」)
|
||||||
CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE = "回调触价开仓"
|
CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE = "回调触价开仓"
|
||||||
LEGACY_TRIGGER_ENTRY_MONITOR_TYPE = "触价开仓"
|
LEGACY_TRIGGER_ENTRY_MONITOR_TYPE = "触价开仓"
|
||||||
|
|
||||||
# 突破触价:标记价穿越 E 后立即市价开仓
|
# 突破触价:标记价穿越 E 后立即市价开仓
|
||||||
BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE = "突破触价开仓"
|
BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE = "突破触价开仓"
|
||||||
|
|
||||||
TRIGGER_ENTRY_MONITOR_TYPES = frozenset(
|
TRIGGER_ENTRY_MONITOR_TYPES = frozenset(
|
||||||
{
|
{
|
||||||
CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE,
|
CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE,
|
||||||
BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE,
|
BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE,
|
||||||
LEGACY_TRIGGER_ENTRY_MONITOR_TYPE,
|
LEGACY_TRIGGER_ENTRY_MONITOR_TYPE,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
TRIGGER_ENTRY_VALIDITY_HOURS = 24
|
TRIGGER_ENTRY_VALIDITY_HOURS = 24
|
||||||
TRIGGER_ENTRY_CLOSE_FILLED = "trigger_entry_filled"
|
TRIGGER_ENTRY_CLOSE_FILLED = "trigger_entry_filled"
|
||||||
TRIGGER_ENTRY_CLOSE_TP_INVALIDATE = "trigger_tp_invalidate"
|
TRIGGER_ENTRY_CLOSE_TP_INVALIDATE = "trigger_tp_invalidate"
|
||||||
TRIGGER_ENTRY_CLOSE_SL_INVALIDATE = "trigger_sl_invalidate"
|
TRIGGER_ENTRY_CLOSE_SL_INVALIDATE = "trigger_sl_invalidate"
|
||||||
TRIGGER_ENTRY_CLOSE_EXPIRED = "trigger_entry_expired"
|
TRIGGER_ENTRY_CLOSE_EXPIRED = "trigger_entry_expired"
|
||||||
TRIGGER_ENTRY_CLOSE_EXCHANGE_FAILED = "trigger_exchange_failed"
|
TRIGGER_ENTRY_CLOSE_EXCHANGE_FAILED = "trigger_exchange_failed"
|
||||||
|
|
||||||
KEY_ENTRY_REASON_CALLBACK = "关键位回调触价开仓"
|
KEY_ENTRY_REASON_CALLBACK = "关键位回调触价开仓"
|
||||||
KEY_ENTRY_REASON_BREAKOUT = "关键位突破触价开仓"
|
KEY_ENTRY_REASON_BREAKOUT = "关键位突破触价开仓"
|
||||||
KEY_ENTRY_REASON_TRIGGER_LEGACY = "关键位触价开仓"
|
KEY_ENTRY_REASON_TRIGGER_LEGACY = "关键位触价开仓"
|
||||||
|
|
||||||
|
|
||||||
def normalize_trigger_entry_monitor_type(monitor_type: Optional[str]) -> str:
|
def normalize_trigger_entry_monitor_type(monitor_type: Optional[str]) -> str:
|
||||||
mt = (monitor_type or "").strip()
|
mt = (monitor_type or "").strip()
|
||||||
if mt == LEGACY_TRIGGER_ENTRY_MONITOR_TYPE:
|
if mt == LEGACY_TRIGGER_ENTRY_MONITOR_TYPE:
|
||||||
return CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE
|
return CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE
|
||||||
return mt
|
return mt
|
||||||
|
|
||||||
|
|
||||||
def is_trigger_entry_key_monitor_type(monitor_type: Optional[str]) -> bool:
|
def is_trigger_entry_key_monitor_type(monitor_type: Optional[str]) -> bool:
|
||||||
return (monitor_type or "").strip() in TRIGGER_ENTRY_MONITOR_TYPES
|
return (monitor_type or "").strip() in TRIGGER_ENTRY_MONITOR_TYPES
|
||||||
|
|
||||||
|
|
||||||
def is_callback_trigger_entry_key_monitor_type(monitor_type: Optional[str]) -> bool:
|
def is_callback_trigger_entry_key_monitor_type(monitor_type: Optional[str]) -> bool:
|
||||||
mt = normalize_trigger_entry_monitor_type(monitor_type)
|
mt = normalize_trigger_entry_monitor_type(monitor_type)
|
||||||
return mt == CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE
|
return mt == CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE
|
||||||
|
|
||||||
|
|
||||||
def is_breakout_trigger_entry_key_monitor_type(monitor_type: Optional[str]) -> bool:
|
def is_breakout_trigger_entry_key_monitor_type(monitor_type: Optional[str]) -> bool:
|
||||||
return (monitor_type or "").strip() == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE
|
return (monitor_type or "").strip() == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE
|
||||||
|
|
||||||
|
|
||||||
def key_entry_reason_for_monitor_type(monitor_type: Optional[str]) -> str:
|
def key_entry_reason_for_monitor_type(monitor_type: Optional[str]) -> str:
|
||||||
if is_breakout_trigger_entry_key_monitor_type(monitor_type):
|
if is_breakout_trigger_entry_key_monitor_type(monitor_type):
|
||||||
return KEY_ENTRY_REASON_BREAKOUT
|
return KEY_ENTRY_REASON_BREAKOUT
|
||||||
if is_trigger_entry_key_monitor_type(monitor_type):
|
if is_trigger_entry_key_monitor_type(monitor_type):
|
||||||
return KEY_ENTRY_REASON_CALLBACK
|
return KEY_ENTRY_REASON_CALLBACK
|
||||||
return KEY_ENTRY_REASON_TRIGGER_LEGACY
|
return KEY_ENTRY_REASON_TRIGGER_LEGACY
|
||||||
|
|
||||||
|
|
||||||
def trigger_entry_reached(direction: str, mark_price: float, entry: float) -> bool:
|
def trigger_entry_reached(direction: str, mark_price: float, entry: float) -> bool:
|
||||||
"""回调触价:多=价跌至 E;空=价涨至 E。"""
|
"""回调触价:多=价跌至 E;空=价涨至 E。"""
|
||||||
return trend_dca_level_reached(direction, mark_price, entry)
|
return trend_dca_level_reached(direction, mark_price, entry)
|
||||||
|
|
||||||
|
|
||||||
def breakout_trigger_entry_crossed(
|
def breakout_trigger_entry_crossed(
|
||||||
direction: str,
|
direction: str,
|
||||||
prev_mark: Optional[float],
|
prev_mark: Optional[float],
|
||||||
mark: float,
|
mark: float,
|
||||||
entry: float,
|
entry: float,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""突破触价:多=向上穿越 E;空=向下穿越 E。"""
|
"""突破触价:多=向上穿越 E;空=向下穿越 E。"""
|
||||||
try:
|
try:
|
||||||
m = float(mark)
|
m = float(mark)
|
||||||
e = float(entry)
|
e = float(entry)
|
||||||
pm = float(prev_mark) if prev_mark is not None else None
|
pm = float(prev_mark) if prev_mark is not None else None
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return False
|
return False
|
||||||
direction = (direction or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
if direction == "long":
|
if direction == "long":
|
||||||
if pm is None:
|
if pm is None:
|
||||||
return m > e
|
return m > e
|
||||||
return pm <= e and m > e
|
return pm <= e and m > e
|
||||||
if pm is None:
|
if pm is None:
|
||||||
return m < e
|
return m < e
|
||||||
return pm >= e and m < e
|
return pm >= e and m < e
|
||||||
|
|
||||||
|
|
||||||
def trigger_should_fire(
|
def trigger_should_fire(
|
||||||
monitor_type: Optional[str],
|
monitor_type: Optional[str],
|
||||||
direction: str,
|
direction: str,
|
||||||
mark: float,
|
mark: float,
|
||||||
entry: float,
|
entry: float,
|
||||||
prev_mark: Optional[float] = None,
|
prev_mark: Optional[float] = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if is_breakout_trigger_entry_key_monitor_type(monitor_type):
|
if is_breakout_trigger_entry_key_monitor_type(monitor_type):
|
||||||
return breakout_trigger_entry_crossed(direction, prev_mark, mark, entry)
|
return breakout_trigger_entry_crossed(direction, prev_mark, mark, entry)
|
||||||
return trigger_entry_reached(direction, mark, entry)
|
return trigger_entry_reached(direction, mark, entry)
|
||||||
|
|
||||||
|
|
||||||
def trigger_entry_invalidate_by_tp(direction: str, mark_price: float, take_profit: float) -> bool:
|
def trigger_entry_invalidate_by_tp(direction: str, mark_price: float, take_profit: float) -> bool:
|
||||||
"""未开仓前标记价先触达止盈侧则失效。"""
|
"""未开仓前标记价先触达止盈侧则失效。"""
|
||||||
try:
|
try:
|
||||||
m = float(mark_price)
|
m = float(mark_price)
|
||||||
tp = float(take_profit)
|
tp = float(take_profit)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return False
|
return False
|
||||||
d = (direction or "long").strip().lower()
|
d = (direction or "long").strip().lower()
|
||||||
if d == "short":
|
if d == "short":
|
||||||
return m <= tp
|
return m <= tp
|
||||||
return m >= tp
|
return m >= tp
|
||||||
|
|
||||||
|
|
||||||
def trigger_entry_invalidate_by_sl(direction: str, mark_price: float, stop_loss: float) -> bool:
|
def trigger_entry_invalidate_by_sl(direction: str, mark_price: float, stop_loss: float) -> bool:
|
||||||
"""突破触价:未到 E 先触达止损侧则失效。"""
|
"""突破触价:未到 E 先触达止损侧则失效。"""
|
||||||
try:
|
try:
|
||||||
m = float(mark_price)
|
m = float(mark_price)
|
||||||
sl = float(stop_loss)
|
sl = float(stop_loss)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return False
|
return False
|
||||||
d = (direction or "long").strip().lower()
|
d = (direction or "long").strip().lower()
|
||||||
if d == "long":
|
if d == "long":
|
||||||
return m <= sl
|
return m <= sl
|
||||||
return m >= sl
|
return m >= sl
|
||||||
|
|
||||||
|
|
||||||
def trigger_entry_invalidate(
|
def trigger_entry_invalidate(
|
||||||
monitor_type: Optional[str],
|
monitor_type: Optional[str],
|
||||||
direction: str,
|
direction: str,
|
||||||
mark: float,
|
mark: float,
|
||||||
stop_loss: float,
|
stop_loss: float,
|
||||||
take_profit: float,
|
take_profit: float,
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
if trigger_entry_invalidate_by_tp(direction, mark, take_profit):
|
if trigger_entry_invalidate_by_tp(direction, mark, take_profit):
|
||||||
return "tp"
|
return "tp"
|
||||||
if is_breakout_trigger_entry_key_monitor_type(monitor_type):
|
if is_breakout_trigger_entry_key_monitor_type(monitor_type):
|
||||||
if trigger_entry_invalidate_by_sl(direction, mark, stop_loss):
|
if trigger_entry_invalidate_by_sl(direction, mark, stop_loss):
|
||||||
return "sl"
|
return "sl"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def validate_trigger_entry_geometry(
|
def validate_trigger_entry_geometry(
|
||||||
direction: str,
|
direction: str,
|
||||||
entry: float,
|
entry: float,
|
||||||
stop_loss: float,
|
stop_loss: float,
|
||||||
take_profit: float,
|
take_profit: float,
|
||||||
mark_at_add: Optional[float] = None,
|
mark_at_add: Optional[float] = None,
|
||||||
*,
|
*,
|
||||||
monitor_type: Optional[str] = None,
|
monitor_type: Optional[str] = None,
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
"""返回错误文案;合法则 None。"""
|
"""返回错误文案;合法则 None。"""
|
||||||
try:
|
try:
|
||||||
e = float(entry)
|
e = float(entry)
|
||||||
sl = float(stop_loss)
|
sl = float(stop_loss)
|
||||||
tp = float(take_profit)
|
tp = float(take_profit)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return "入场价、止损、止盈须为有效数字"
|
return "入场价、止损、止盈须为有效数字"
|
||||||
if e <= 0 or sl <= 0 or tp <= 0:
|
if e <= 0 or sl <= 0 or tp <= 0:
|
||||||
return "入场价、止损、止盈须大于 0"
|
return "入场价、止损、止盈须大于 0"
|
||||||
d = (direction or "long").strip().lower()
|
d = (direction or "long").strip().lower()
|
||||||
mt = normalize_trigger_entry_monitor_type(monitor_type)
|
mt = normalize_trigger_entry_monitor_type(monitor_type)
|
||||||
label = "突破触价开仓" if mt == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE else "回调触价开仓"
|
label = "突破触价开仓" if mt == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE else "回调触价开仓"
|
||||||
if d == "long":
|
if d == "long":
|
||||||
if not (sl < e < tp):
|
if not (sl < e < tp):
|
||||||
return "做多:须满足 止损 < 入场价 < 止盈"
|
return "做多:须满足 止损 < 入场价 < 止盈"
|
||||||
if mark_at_add is not None:
|
if mark_at_add is not None:
|
||||||
m = float(mark_at_add)
|
m = float(mark_at_add)
|
||||||
if m >= tp:
|
if m >= tp:
|
||||||
return f"做多:当前价已不低于止盈,无法添加{label}"
|
return f"做多:当前价已不低于止盈,无法添加{label}"
|
||||||
if mt == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE and m >= e:
|
if mt == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE and m >= e:
|
||||||
return "做多:当前价须低于入场价(等待向上突破)"
|
return "做多:当前价须低于入场价(等待向上突破)"
|
||||||
elif d == "short":
|
elif d == "short":
|
||||||
if not (tp < e < sl):
|
if not (tp < e < sl):
|
||||||
return "做空:须满足 止盈 < 入场价 < 止损"
|
return "做空:须满足 止盈 < 入场价 < 止损"
|
||||||
if mark_at_add is not None:
|
if mark_at_add is not None:
|
||||||
m = float(mark_at_add)
|
m = float(mark_at_add)
|
||||||
if m <= tp:
|
if m <= tp:
|
||||||
return f"做空:当前价已不高于止盈,无法添加{label}"
|
return f"做空:当前价已不高于止盈,无法添加{label}"
|
||||||
if mt == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE and m <= e:
|
if mt == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE and m <= e:
|
||||||
return "做空:当前价须高于入场价(等待向下跌破)"
|
return "做空:当前价须高于入场价(等待向下跌破)"
|
||||||
else:
|
else:
|
||||||
return "方向须为 long 或 short"
|
return "方向须为 long 或 short"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def validate_trigger_entry_rr(
|
def validate_trigger_entry_rr(
|
||||||
direction: str,
|
direction: str,
|
||||||
entry: float,
|
entry: float,
|
||||||
stop_loss: float,
|
stop_loss: float,
|
||||||
take_profit: float,
|
take_profit: float,
|
||||||
min_rr: float,
|
min_rr: float,
|
||||||
calc_rr_ratio: Callable[..., Optional[float]],
|
calc_rr_ratio: Callable[..., Optional[float]],
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
rr = calc_rr_ratio(direction, entry, stop_loss, take_profit)
|
rr = calc_rr_ratio(direction, entry, stop_loss, take_profit)
|
||||||
if rr is None or rr <= float(min_rr):
|
if rr is None or rr <= float(min_rr):
|
||||||
fmt = f"{rr:.4f}" if rr is not None else "无法计算"
|
fmt = f"{rr:.4f}" if rr is not None else "无法计算"
|
||||||
return f"计划盈亏比 {fmt}:1 未达要求(>{float(min_rr)}:1)"
|
return f"计划盈亏比 {fmt}:1 未达要求(>{float(min_rr)}:1)"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def is_trigger_entry_expired(
|
def is_trigger_entry_expired(
|
||||||
created_at: Any,
|
created_at: Any,
|
||||||
now: datetime,
|
now: datetime,
|
||||||
*,
|
*,
|
||||||
hours: int = TRIGGER_ENTRY_VALIDITY_HOURS,
|
hours: int = TRIGGER_ENTRY_VALIDITY_HOURS,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
return is_false_breakout_expired(created_at, now, hours=hours)
|
return is_false_breakout_expired(created_at, now, hours=hours)
|
||||||
|
|
||||||
|
|
||||||
def trigger_entry_expires_at_text(
|
def trigger_entry_expires_at_text(
|
||||||
created_at: Any,
|
created_at: Any,
|
||||||
*,
|
*,
|
||||||
hours: int = TRIGGER_ENTRY_VALIDITY_HOURS,
|
hours: int = TRIGGER_ENTRY_VALIDITY_HOURS,
|
||||||
) -> str:
|
) -> str:
|
||||||
return expires_at_text(created_at, hours=hours)
|
return expires_at_text(created_at, hours=hours)
|
||||||
|
|
||||||
|
|
||||||
def count_pending_trigger_entries(conn: Any, trading_day: str) -> int:
|
def count_pending_trigger_entries(conn: Any, trading_day: str) -> int:
|
||||||
td = (trading_day or "").strip()
|
td = (trading_day or "").strip()
|
||||||
if not td:
|
if not td:
|
||||||
return 0
|
return 0
|
||||||
placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES))
|
placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES))
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
f"SELECT COUNT(*) FROM key_monitors WHERE monitor_type IN ({placeholders}) AND session_date=?",
|
f"SELECT COUNT(*) FROM key_monitors WHERE monitor_type IN ({placeholders}) AND session_date=?",
|
||||||
(*TRIGGER_ENTRY_MONITOR_TYPES, td),
|
(*TRIGGER_ENTRY_MONITOR_TYPES, td),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
return int(row[0] if row else 0)
|
return int(row[0] if row else 0)
|
||||||
|
|
||||||
|
|
||||||
def check_trigger_entry_intent_limit(
|
def check_trigger_entry_intent_limit(
|
||||||
conn: Any,
|
conn: Any,
|
||||||
trading_day: str,
|
trading_day: str,
|
||||||
opens_today: int,
|
opens_today: int,
|
||||||
hard_limit: int,
|
hard_limit: int,
|
||||||
) -> tuple[bool, str]:
|
) -> tuple[bool, str]:
|
||||||
"""当日开仓意图:已成交次数 + 待触发触价条数。"""
|
"""当日开仓意图:已成交次数 + 待触发触价条数。"""
|
||||||
if int(hard_limit) <= 0:
|
if int(hard_limit) <= 0:
|
||||||
return True, ""
|
return True, ""
|
||||||
pending = count_pending_trigger_entries(conn, trading_day)
|
pending = count_pending_trigger_entries(conn, trading_day)
|
||||||
total = int(opens_today) + pending
|
total = int(opens_today) + pending
|
||||||
if total >= int(hard_limit):
|
if total >= int(hard_limit):
|
||||||
return (
|
return (
|
||||||
False,
|
False,
|
||||||
f"本交易日开仓意图已达上限(已开 {int(opens_today)} + 待触发 {pending} / 硬上限 {int(hard_limit)})",
|
f"本交易日开仓意图已达上限(已开 {int(opens_today)} + 待触发 {pending} / 硬上限 {int(hard_limit)})",
|
||||||
)
|
)
|
||||||
return True, ""
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
def trigger_entry_gate_preview(
|
def trigger_entry_gate_preview(
|
||||||
*,
|
*,
|
||||||
monitor_type: Optional[str] = None,
|
monitor_type: Optional[str] = None,
|
||||||
entry_display: str,
|
entry_display: str,
|
||||||
take_profit_display: str,
|
take_profit_display: str,
|
||||||
created_at: Any = None,
|
created_at: Any = None,
|
||||||
now: Optional[datetime] = None,
|
now: Optional[datetime] = None,
|
||||||
expired: bool = False,
|
expired: bool = False,
|
||||||
tp_invalidated: bool = False,
|
tp_invalidated: bool = False,
|
||||||
sl_invalidated: bool = False,
|
sl_invalidated: bool = False,
|
||||||
hours: int = TRIGGER_ENTRY_VALIDITY_HOURS,
|
hours: int = TRIGGER_ENTRY_VALIDITY_HOURS,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
now_dt = now or datetime.now()
|
now_dt = now or datetime.now()
|
||||||
is_exp = expired or is_trigger_entry_expired(created_at, now_dt, hours=hours)
|
is_exp = expired or is_trigger_entry_expired(created_at, now_dt, hours=hours)
|
||||||
exp_txt = trigger_entry_expires_at_text(created_at, hours=hours)
|
exp_txt = trigger_entry_expires_at_text(created_at, hours=hours)
|
||||||
mt = normalize_trigger_entry_monitor_type(monitor_type)
|
mt = normalize_trigger_entry_monitor_type(monitor_type)
|
||||||
if tp_invalidated:
|
if tp_invalidated:
|
||||||
status = "止盈侧失效"
|
status = "止盈侧失效"
|
||||||
elif sl_invalidated:
|
elif sl_invalidated:
|
||||||
status = "止损侧失效"
|
status = "止损侧失效"
|
||||||
elif is_exp:
|
elif is_exp:
|
||||||
status = "已过期"
|
status = "已过期"
|
||||||
elif mt == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE:
|
elif mt == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE:
|
||||||
status = "突破待触发"
|
status = "突破待触发"
|
||||||
else:
|
else:
|
||||||
status = "回调待触发"
|
status = "回调待触发"
|
||||||
mode = "突破" if mt == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE else "回调"
|
mode = "突破" if mt == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE else "回调"
|
||||||
metrics_parts: list[str] = [f"TP:{take_profit_display}"]
|
metrics_parts: list[str] = [f"TP:{take_profit_display}"]
|
||||||
if exp_txt != "—":
|
if exp_txt != "—":
|
||||||
metrics_parts.append(f"截至:{exp_txt}")
|
metrics_parts.append(f"截至:{exp_txt}")
|
||||||
return {
|
return {
|
||||||
"summary": f"{mode}触价 E={entry_display} {status}",
|
"summary": f"{mode}触价 E={entry_display} {status}",
|
||||||
"metrics": " ".join(metrics_parts),
|
"metrics": " ".join(metrics_parts),
|
||||||
"gate_ok": not is_exp and not tp_invalidated and not sl_invalidated,
|
"gate_ok": not is_exp and not tp_invalidated and not sl_invalidated,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# 兼容旧 import
|
# 兼容旧 import
|
||||||
TRIGGER_ENTRY_MONITOR_TYPE = CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE
|
TRIGGER_ENTRY_MONITOR_TYPE = CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE
|
||||||
KEY_ENTRY_REASON_TRIGGER = KEY_ENTRY_REASON_CALLBACK
|
KEY_ENTRY_REASON_TRIGGER = KEY_ENTRY_REASON_CALLBACK
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
"""Repository path helpers for lib/ assets."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
LIB_DIR = Path(__file__).resolve().parent
|
||||||
|
REPO_ROOT = LIB_DIR.parent
|
||||||
|
|
||||||
|
|
||||||
|
def strategy_templates_dir(repo_root: str | Path | None = None) -> str:
|
||||||
|
root = Path(repo_root) if repo_root is not None else REPO_ROOT
|
||||||
|
return str(root / "lib" / "strategy" / "templates")
|
||||||
|
|
||||||
|
|
||||||
|
def embed_templates_dir(repo_root: str | Path | None = None) -> str:
|
||||||
|
root = Path(repo_root) if repo_root is not None else REPO_ROOT
|
||||||
|
return str(root / "lib" / "instance" / "templates")
|
||||||
|
|
||||||
|
|
||||||
|
def common_static_dir(repo_root: str | Path | None = None) -> str:
|
||||||
|
root = Path(repo_root) if repo_root is not None else REPO_ROOT
|
||||||
|
return str(root / "lib" / "common" / "static")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Shared library package."""
|
||||||
@@ -1,164 +1,164 @@
|
|||||||
"""策略交易相关表结构(各所 crypto.db 共用 schema)。"""
|
"""策略交易相关表结构(各所 crypto.db 共用 schema)。"""
|
||||||
|
|
||||||
ROLL_GROUPS_SQL = """
|
ROLL_GROUPS_SQL = """
|
||||||
CREATE TABLE IF NOT EXISTS roll_groups (
|
CREATE TABLE IF NOT EXISTS roll_groups (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
order_monitor_id INTEGER,
|
order_monitor_id INTEGER,
|
||||||
symbol TEXT NOT NULL,
|
symbol TEXT NOT NULL,
|
||||||
exchange_symbol TEXT,
|
exchange_symbol TEXT,
|
||||||
direction TEXT NOT NULL,
|
direction TEXT NOT NULL,
|
||||||
initial_take_profit REAL,
|
initial_take_profit REAL,
|
||||||
initial_stop_loss REAL,
|
initial_stop_loss REAL,
|
||||||
current_stop_loss REAL,
|
current_stop_loss REAL,
|
||||||
risk_percent REAL DEFAULT 2,
|
risk_percent REAL DEFAULT 2,
|
||||||
leg_count INTEGER DEFAULT 0,
|
leg_count INTEGER DEFAULT 0,
|
||||||
status TEXT DEFAULT 'active',
|
status TEXT DEFAULT 'active',
|
||||||
created_at TEXT,
|
created_at TEXT,
|
||||||
updated_at TEXT
|
updated_at TEXT
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ROLL_LEGS_SQL = """
|
ROLL_LEGS_SQL = """
|
||||||
CREATE TABLE IF NOT EXISTS roll_legs (
|
CREATE TABLE IF NOT EXISTS roll_legs (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
roll_group_id INTEGER NOT NULL,
|
roll_group_id INTEGER NOT NULL,
|
||||||
leg_index INTEGER NOT NULL,
|
leg_index INTEGER NOT NULL,
|
||||||
add_mode TEXT NOT NULL,
|
add_mode TEXT NOT NULL,
|
||||||
fib_upper REAL,
|
fib_upper REAL,
|
||||||
fib_lower REAL,
|
fib_lower REAL,
|
||||||
limit_price REAL,
|
limit_price REAL,
|
||||||
fill_price REAL,
|
fill_price REAL,
|
||||||
amount REAL,
|
amount REAL,
|
||||||
new_stop_loss REAL,
|
new_stop_loss REAL,
|
||||||
exchange_order_id TEXT,
|
exchange_order_id TEXT,
|
||||||
status TEXT DEFAULT 'filled',
|
status TEXT DEFAULT 'filled',
|
||||||
created_at TEXT,
|
created_at TEXT,
|
||||||
FOREIGN KEY (roll_group_id) REFERENCES roll_groups(id)
|
FOREIGN KEY (roll_group_id) REFERENCES roll_groups(id)
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
TREND_PLANS_SQL = """
|
TREND_PLANS_SQL = """
|
||||||
CREATE TABLE IF NOT EXISTS trend_pullback_plans (
|
CREATE TABLE IF NOT EXISTS trend_pullback_plans (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
status TEXT DEFAULT 'active',
|
status TEXT DEFAULT 'active',
|
||||||
symbol TEXT NOT NULL,
|
symbol TEXT NOT NULL,
|
||||||
exchange_symbol TEXT,
|
exchange_symbol TEXT,
|
||||||
direction TEXT NOT NULL DEFAULT 'long',
|
direction TEXT NOT NULL DEFAULT 'long',
|
||||||
leverage INTEGER NOT NULL,
|
leverage INTEGER NOT NULL,
|
||||||
stop_loss REAL NOT NULL,
|
stop_loss REAL NOT NULL,
|
||||||
add_upper REAL NOT NULL,
|
add_upper REAL NOT NULL,
|
||||||
take_profit REAL NOT NULL,
|
take_profit REAL NOT NULL,
|
||||||
risk_percent REAL DEFAULT 5,
|
risk_percent REAL DEFAULT 5,
|
||||||
snapshot_available_usdt REAL,
|
snapshot_available_usdt REAL,
|
||||||
snapshot_at TEXT,
|
snapshot_at TEXT,
|
||||||
plan_margin_capital REAL,
|
plan_margin_capital REAL,
|
||||||
target_order_amount REAL,
|
target_order_amount REAL,
|
||||||
first_order_amount REAL,
|
first_order_amount REAL,
|
||||||
remainder_total REAL,
|
remainder_total REAL,
|
||||||
dca_legs INTEGER DEFAULT 5,
|
dca_legs INTEGER DEFAULT 5,
|
||||||
per_leg_amount REAL,
|
per_leg_amount REAL,
|
||||||
grid_prices_json TEXT,
|
grid_prices_json TEXT,
|
||||||
leg_amounts_json TEXT,
|
leg_amounts_json TEXT,
|
||||||
legs_done INTEGER DEFAULT 0,
|
legs_done INTEGER DEFAULT 0,
|
||||||
first_order_done INTEGER DEFAULT 0,
|
first_order_done INTEGER DEFAULT 0,
|
||||||
last_mark_price REAL,
|
last_mark_price REAL,
|
||||||
avg_entry_price REAL,
|
avg_entry_price REAL,
|
||||||
order_amount_open REAL,
|
order_amount_open REAL,
|
||||||
opened_at TEXT,
|
opened_at TEXT,
|
||||||
opened_at_ms INTEGER,
|
opened_at_ms INTEGER,
|
||||||
session_date TEXT,
|
session_date TEXT,
|
||||||
message TEXT,
|
message TEXT,
|
||||||
initial_stop_loss REAL,
|
initial_stop_loss REAL,
|
||||||
breakeven_applied INTEGER DEFAULT 0,
|
breakeven_applied INTEGER DEFAULT 0,
|
||||||
breakeven_applied_at TEXT
|
breakeven_applied_at TEXT
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
TREND_PREVIEWS_SQL = """
|
TREND_PREVIEWS_SQL = """
|
||||||
CREATE TABLE IF NOT EXISTS trend_pullback_previews (
|
CREATE TABLE IF NOT EXISTS trend_pullback_previews (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
symbol TEXT NOT NULL,
|
symbol TEXT NOT NULL,
|
||||||
exchange_symbol TEXT NOT NULL,
|
exchange_symbol TEXT NOT NULL,
|
||||||
direction TEXT NOT NULL,
|
direction TEXT NOT NULL,
|
||||||
leverage INTEGER NOT NULL,
|
leverage INTEGER NOT NULL,
|
||||||
stop_loss REAL NOT NULL,
|
stop_loss REAL NOT NULL,
|
||||||
add_upper REAL NOT NULL,
|
add_upper REAL NOT NULL,
|
||||||
take_profit REAL NOT NULL,
|
take_profit REAL NOT NULL,
|
||||||
risk_percent REAL NOT NULL,
|
risk_percent REAL NOT NULL,
|
||||||
snapshot_available_usdt REAL NOT NULL,
|
snapshot_available_usdt REAL NOT NULL,
|
||||||
snapshot_at TEXT,
|
snapshot_at TEXT,
|
||||||
live_price_ref REAL,
|
live_price_ref REAL,
|
||||||
plan_margin_capital REAL,
|
plan_margin_capital REAL,
|
||||||
target_order_amount REAL,
|
target_order_amount REAL,
|
||||||
first_order_amount REAL,
|
first_order_amount REAL,
|
||||||
remainder_total REAL,
|
remainder_total REAL,
|
||||||
dca_legs INTEGER,
|
dca_legs INTEGER,
|
||||||
per_leg_amount REAL,
|
per_leg_amount REAL,
|
||||||
grid_prices_json TEXT,
|
grid_prices_json TEXT,
|
||||||
leg_amounts_json TEXT,
|
leg_amounts_json TEXT,
|
||||||
expires_at_ms INTEGER NOT NULL,
|
expires_at_ms INTEGER NOT NULL,
|
||||||
created_at TEXT
|
created_at TEXT
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
TREND_PREVIEW_SNAPSHOTS_SQL = """
|
TREND_PREVIEW_SNAPSHOTS_SQL = """
|
||||||
CREATE TABLE IF NOT EXISTS trend_pullback_preview_snapshots (
|
CREATE TABLE IF NOT EXISTS trend_pullback_preview_snapshots (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
preview_id TEXT NOT NULL UNIQUE,
|
preview_id TEXT NOT NULL UNIQUE,
|
||||||
symbol TEXT NOT NULL,
|
symbol TEXT NOT NULL,
|
||||||
exchange_symbol TEXT NOT NULL,
|
exchange_symbol TEXT NOT NULL,
|
||||||
direction TEXT NOT NULL,
|
direction TEXT NOT NULL,
|
||||||
leverage INTEGER NOT NULL,
|
leverage INTEGER NOT NULL,
|
||||||
stop_loss REAL NOT NULL,
|
stop_loss REAL NOT NULL,
|
||||||
add_upper REAL NOT NULL,
|
add_upper REAL NOT NULL,
|
||||||
take_profit REAL NOT NULL,
|
take_profit REAL NOT NULL,
|
||||||
risk_percent REAL NOT NULL,
|
risk_percent REAL NOT NULL,
|
||||||
snapshot_available_usdt REAL NOT NULL,
|
snapshot_available_usdt REAL NOT NULL,
|
||||||
snapshot_at TEXT,
|
snapshot_at TEXT,
|
||||||
live_price_ref REAL,
|
live_price_ref REAL,
|
||||||
plan_margin_capital REAL,
|
plan_margin_capital REAL,
|
||||||
target_order_amount REAL,
|
target_order_amount REAL,
|
||||||
first_order_amount REAL,
|
first_order_amount REAL,
|
||||||
remainder_total REAL,
|
remainder_total REAL,
|
||||||
dca_legs INTEGER,
|
dca_legs INTEGER,
|
||||||
per_leg_amount REAL,
|
per_leg_amount REAL,
|
||||||
grid_prices_json TEXT,
|
grid_prices_json TEXT,
|
||||||
leg_amounts_json TEXT,
|
leg_amounts_json TEXT,
|
||||||
expires_at_ms INTEGER NOT NULL,
|
expires_at_ms INTEGER NOT NULL,
|
||||||
preview_created_at TEXT,
|
preview_created_at TEXT,
|
||||||
outcome TEXT DEFAULT 'open',
|
outcome TEXT DEFAULT 'open',
|
||||||
executed_plan_id INTEGER
|
executed_plan_id INTEGER
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def init_strategy_tables(conn) -> None:
|
def init_strategy_tables(conn) -> None:
|
||||||
from strategy_snapshot_lib import init_strategy_snapshot_table
|
from lib.strategy.strategy_snapshot_lib import init_strategy_snapshot_table
|
||||||
|
|
||||||
conn.execute(ROLL_GROUPS_SQL)
|
conn.execute(ROLL_GROUPS_SQL)
|
||||||
conn.execute(ROLL_LEGS_SQL)
|
conn.execute(ROLL_LEGS_SQL)
|
||||||
conn.execute(TREND_PLANS_SQL)
|
conn.execute(TREND_PLANS_SQL)
|
||||||
conn.execute(TREND_PREVIEWS_SQL)
|
conn.execute(TREND_PREVIEWS_SQL)
|
||||||
conn.execute(TREND_PREVIEW_SNAPSHOTS_SQL)
|
conn.execute(TREND_PREVIEW_SNAPSHOTS_SQL)
|
||||||
init_strategy_snapshot_table(conn)
|
init_strategy_snapshot_table(conn)
|
||||||
for ddl in (
|
for ddl in (
|
||||||
"ALTER TABLE trend_pullback_plans ADD COLUMN leg_amounts_json TEXT",
|
"ALTER TABLE trend_pullback_plans ADD COLUMN leg_amounts_json TEXT",
|
||||||
"ALTER TABLE trend_pullback_plans ADD COLUMN initial_stop_loss REAL",
|
"ALTER TABLE trend_pullback_plans ADD COLUMN initial_stop_loss REAL",
|
||||||
"ALTER TABLE trend_pullback_plans ADD COLUMN breakeven_applied INTEGER DEFAULT 0",
|
"ALTER TABLE trend_pullback_plans ADD COLUMN breakeven_applied INTEGER DEFAULT 0",
|
||||||
"ALTER TABLE trend_pullback_plans ADD COLUMN breakeven_applied_at TEXT",
|
"ALTER TABLE trend_pullback_plans ADD COLUMN breakeven_applied_at TEXT",
|
||||||
"ALTER TABLE trend_pullback_preview_snapshots ADD COLUMN preview_created_at TEXT",
|
"ALTER TABLE trend_pullback_preview_snapshots ADD COLUMN preview_created_at TEXT",
|
||||||
"ALTER TABLE trend_pullback_preview_snapshots ADD COLUMN outcome TEXT DEFAULT 'open'",
|
"ALTER TABLE trend_pullback_preview_snapshots ADD COLUMN outcome TEXT DEFAULT 'open'",
|
||||||
"ALTER TABLE trend_pullback_preview_snapshots ADD COLUMN executed_plan_id INTEGER",
|
"ALTER TABLE trend_pullback_preview_snapshots ADD COLUMN executed_plan_id INTEGER",
|
||||||
"ALTER TABLE trade_records ADD COLUMN trend_plan_id INTEGER",
|
"ALTER TABLE trade_records ADD COLUMN trend_plan_id INTEGER",
|
||||||
"ALTER TABLE order_monitors ADD COLUMN trend_plan_id INTEGER",
|
"ALTER TABLE order_monitors ADD COLUMN trend_plan_id INTEGER",
|
||||||
"ALTER TABLE order_monitors ADD COLUMN monitor_type TEXT",
|
"ALTER TABLE order_monitors ADD COLUMN monitor_type TEXT",
|
||||||
"ALTER TABLE order_monitors ADD COLUMN key_signal_type TEXT",
|
"ALTER TABLE order_monitors ADD COLUMN key_signal_type TEXT",
|
||||||
"ALTER TABLE trend_pullback_plans ADD COLUMN leg_fill_prices_json TEXT",
|
"ALTER TABLE trend_pullback_plans ADD COLUMN leg_fill_prices_json TEXT",
|
||||||
"ALTER TABLE roll_legs ADD COLUMN stop_offset_pct REAL",
|
"ALTER TABLE roll_legs ADD COLUMN stop_offset_pct REAL",
|
||||||
"ALTER TABLE roll_legs ADD COLUMN breakthrough_price REAL",
|
"ALTER TABLE roll_legs ADD COLUMN breakthrough_price REAL",
|
||||||
"ALTER TABLE roll_legs ADD COLUMN last_mark_price REAL",
|
"ALTER TABLE roll_legs ADD COLUMN last_mark_price REAL",
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
conn.execute(ddl)
|
conn.execute(ddl)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Binance USDT-M 永续 — 策略交易交易所适配(见 strategy_config.build_strategy_config)。"""
|
"""Binance USDT-M 永续 — 策略交易交易所适配(见 strategy_config.build_strategy_config)。"""
|
||||||
from strategy_exchange_base import StrategyExchangeAdapter
|
from lib.strategy.strategy_exchange_base import StrategyExchangeAdapter
|
||||||
|
|
||||||
__all__ = ["StrategyExchangeAdapter"]
|
__all__ = ["StrategyExchangeAdapter"]
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
Gate.io USDT 永续 — 策略交易交易所侧能力。
|
Gate.io USDT 永续 — 策略交易交易所侧能力。
|
||||||
|
|
||||||
实现方式:各 Gate 实例 app 通过 strategy_config.build_strategy_config(app_module) 注入
|
实现方式:各 Gate 实例 app 通过 strategy_config.build_strategy_config(app_module) 注入
|
||||||
ccxt 下单、精度、换 TP/SL;本文件为文档与类型锚点,避免在四个 app 重复实现滚仓公式。
|
ccxt 下单、精度、换 TP/SL;本文件为文档与类型锚点,避免在四个 app 重复实现滚仓公式。
|
||||||
"""
|
"""
|
||||||
from strategy_exchange_base import StrategyExchangeAdapter
|
from lib.strategy.strategy_exchange_base import StrategyExchangeAdapter
|
||||||
|
|
||||||
__all__ = ["StrategyExchangeAdapter"]
|
__all__ = ["StrategyExchangeAdapter"]
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""OKX 永续 — 策略交易交易所适配(见 strategy_config.build_strategy_config)。"""
|
"""OKX 永续 — 策略交易交易所适配(见 strategy_config.build_strategy_config)。"""
|
||||||
from strategy_exchange_base import StrategyExchangeAdapter
|
from lib.strategy.strategy_exchange_base import StrategyExchangeAdapter
|
||||||
|
|
||||||
__all__ = ["StrategyExchangeAdapter"]
|
__all__ = ["StrategyExchangeAdapter"]
|
||||||
@@ -1,72 +1,72 @@
|
|||||||
"""策略交易记录页:已结束趋势 / 顺势加仓快照(四所统一)。"""
|
"""策略交易记录页:已结束趋势 / 顺势加仓快照(四所统一)。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from flask import flash, redirect, url_for
|
from flask import flash, redirect, url_for
|
||||||
|
|
||||||
from strategy_snapshot_lib import (
|
from lib.strategy.strategy_snapshot_lib import (
|
||||||
STRATEGY_SNAPSHOTS_MAX_ROWS,
|
STRATEGY_SNAPSHOTS_MAX_ROWS,
|
||||||
dedupe_strategy_snapshots,
|
dedupe_strategy_snapshots,
|
||||||
list_strategy_snapshots_split,
|
list_strategy_snapshots_split,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_strategy_records_page(
|
def load_strategy_records_page(
|
||||||
conn, *, limit: int = STRATEGY_SNAPSHOTS_MAX_ROWS
|
conn, *, limit: int = STRATEGY_SNAPSHOTS_MAX_ROWS
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
if dedupe_strategy_snapshots(conn):
|
if dedupe_strategy_snapshots(conn):
|
||||||
conn.commit()
|
conn.commit()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
trend, roll, symbols = list_strategy_snapshots_split(conn, limit=limit)
|
trend, roll, symbols = list_strategy_snapshots_split(conn, limit=limit)
|
||||||
return {
|
return {
|
||||||
"strategy_trend_records": trend,
|
"strategy_trend_records": trend,
|
||||||
"strategy_roll_records": roll,
|
"strategy_roll_records": roll,
|
||||||
"strategy_record_symbols": symbols,
|
"strategy_record_symbols": symbols,
|
||||||
"strategy_records_limit": limit,
|
"strategy_records_limit": limit,
|
||||||
"strategy_snapshots": trend + roll,
|
"strategy_snapshots": trend + roll,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def register_strategy_records(app, cfg: dict[str, Any]) -> None:
|
def register_strategy_records(app, cfg: dict[str, Any]) -> None:
|
||||||
login_required = cfg["login_required"]
|
login_required = cfg["login_required"]
|
||||||
get_db = cfg["get_db"]
|
get_db = cfg["get_db"]
|
||||||
|
|
||||||
def _lr(f):
|
def _lr(f):
|
||||||
return login_required(f)
|
return login_required(f)
|
||||||
|
|
||||||
@_lr
|
@_lr
|
||||||
@app.route("/strategy/records")
|
@app.route("/strategy/records")
|
||||||
def strategy_records_page():
|
def strategy_records_page():
|
||||||
m = cfg.get("app_module")
|
m = cfg.get("app_module")
|
||||||
fn = getattr(m, "render_main_page", None)
|
fn = getattr(m, "render_main_page", None)
|
||||||
if not callable(fn):
|
if not callable(fn):
|
||||||
flash("render_main_page 未配置")
|
flash("render_main_page 未配置")
|
||||||
return redirect(url_for("strategy_trading_page"))
|
return redirect(url_for("strategy_trading_page"))
|
||||||
return fn("strategy_records")
|
return fn("strategy_records")
|
||||||
|
|
||||||
@_lr
|
@_lr
|
||||||
@app.route("/strategy/records/<int:snap_id>")
|
@app.route("/strategy/records/<int:snap_id>")
|
||||||
def strategy_records_detail(snap_id: int):
|
def strategy_records_detail(snap_id: int):
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"SELECT * FROM strategy_trade_snapshots WHERE id=?",
|
"SELECT * FROM strategy_trade_snapshots WHERE id=?",
|
||||||
(int(snap_id),),
|
(int(snap_id),),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
if not row:
|
if not row:
|
||||||
flash("未找到该策略快照")
|
flash("未找到该策略快照")
|
||||||
return redirect(url_for("strategy_records_page"))
|
return redirect(url_for("strategy_records_page"))
|
||||||
try:
|
try:
|
||||||
snap = json.loads(row["snapshot_json"] or "{}")
|
snap = json.loads(row["snapshot_json"] or "{}")
|
||||||
except Exception:
|
except Exception:
|
||||||
snap = {}
|
snap = {}
|
||||||
dca = snap.get("dca_levels") or []
|
dca = snap.get("dca_levels") or []
|
||||||
flash(
|
flash(
|
||||||
f"快照 #{snap_id} {row['strategy_type']} {row['symbol']} "
|
f"快照 #{snap_id} {row['strategy_type']} {row['symbol']} "
|
||||||
f"{row['result_label']} · 补仓档 {len(dca)} 项(详情见列表页)"
|
f"{row['result_label']} · 补仓档 {len(dca)} 项(详情见列表页)"
|
||||||
)
|
)
|
||||||
return redirect(url_for("strategy_records_page"))
|
return redirect(url_for("strategy_records_page"))
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,385 +1,385 @@
|
|||||||
"""顺势加仓(滚仓):纯计算。人工触发;止盈锁定首仓;程序监控触价市价成交。"""
|
"""顺势加仓(滚仓):纯计算。人工触发;止盈锁定首仓;程序监控触价市价成交。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Optional, Tuple
|
from typing import Any, Optional, Tuple
|
||||||
|
|
||||||
from fib_key_monitor_lib import calc_fib_plan, fib_invalidate_by_mark
|
from lib.key_monitor.fib_key_monitor_lib import calc_fib_plan, fib_invalidate_by_mark
|
||||||
|
|
||||||
ROLL_MAX_LEGS_LONG = 3
|
ROLL_MAX_LEGS_LONG = 3
|
||||||
ROLL_MAX_LEGS_SHORT = 3
|
ROLL_MAX_LEGS_SHORT = 3
|
||||||
|
|
||||||
MARKET_MODE = "market"
|
MARKET_MODE = "market"
|
||||||
FIB_MODES = frozenset({"fib_618", "fib_786"})
|
FIB_MODES = frozenset({"fib_618", "fib_786"})
|
||||||
BREAKOUT_MODE = "breakout"
|
BREAKOUT_MODE = "breakout"
|
||||||
|
|
||||||
MODE_LABELS = {
|
MODE_LABELS = {
|
||||||
MARKET_MODE: "市价加仓",
|
MARKET_MODE: "市价加仓",
|
||||||
"fib_618": "斐波0.618",
|
"fib_618": "斐波0.618",
|
||||||
"fib_786": "斐波0.786",
|
"fib_786": "斐波0.786",
|
||||||
BREAKOUT_MODE: "突破加仓",
|
BREAKOUT_MODE: "突破加仓",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def fib_ratio_from_mode(mode: str) -> Optional[float]:
|
def fib_ratio_from_mode(mode: str) -> Optional[float]:
|
||||||
m = (mode or "").strip().lower()
|
m = (mode or "").strip().lower()
|
||||||
if m in ("fib_618", "618", "0.618"):
|
if m in ("fib_618", "618", "0.618"):
|
||||||
return 0.618
|
return 0.618
|
||||||
if m in ("fib_786", "786", "0.786"):
|
if m in ("fib_786", "786", "0.786"):
|
||||||
return 0.786
|
return 0.786
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def mode_label(mode: str) -> str:
|
def mode_label(mode: str) -> str:
|
||||||
m = (mode or MARKET_MODE).strip().lower()
|
m = (mode or MARKET_MODE).strip().lower()
|
||||||
return MODE_LABELS.get(m, m)
|
return MODE_LABELS.get(m, m)
|
||||||
|
|
||||||
|
|
||||||
def fib_limit_entry(direction: str, upper: float, lower: float, mode: str) -> Tuple[Optional[float], Optional[str]]:
|
def fib_limit_entry(direction: str, upper: float, lower: float, mode: str) -> Tuple[Optional[float], Optional[str]]:
|
||||||
"""H/L 仅用于计算限价加仓价;多:下沿=止损侧;空:上沿=止损侧。"""
|
"""H/L 仅用于计算限价加仓价;多:下沿=止损侧;空:上沿=止损侧。"""
|
||||||
ratio = fib_ratio_from_mode(mode)
|
ratio = fib_ratio_from_mode(mode)
|
||||||
if ratio is None:
|
if ratio is None:
|
||||||
return None, "斐波档位无效"
|
return None, "斐波档位无效"
|
||||||
h, l = float(upper), float(lower)
|
h, l = float(upper), float(lower)
|
||||||
if h <= l:
|
if h <= l:
|
||||||
return None, "上沿须大于下沿"
|
return None, "上沿须大于下沿"
|
||||||
direction = (direction or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
if direction == "short":
|
if direction == "short":
|
||||||
plan = calc_fib_plan("short", h, l, ratio)
|
plan = calc_fib_plan("short", h, l, ratio)
|
||||||
else:
|
else:
|
||||||
plan = calc_fib_plan("long", h, l, ratio)
|
plan = calc_fib_plan("long", h, l, ratio)
|
||||||
if not plan:
|
if not plan:
|
||||||
return None, "无法计算斐波限价"
|
return None, "无法计算斐波限价"
|
||||||
entry, _sl, _tp = plan
|
entry, _sl, _tp = plan
|
||||||
return float(entry), None
|
return float(entry), None
|
||||||
|
|
||||||
|
|
||||||
def max_roll_legs(direction: str) -> int:
|
def max_roll_legs(direction: str) -> int:
|
||||||
return ROLL_MAX_LEGS_LONG if (direction or "long").strip().lower() == "long" else ROLL_MAX_LEGS_SHORT
|
return ROLL_MAX_LEGS_LONG if (direction or "long").strip().lower() == "long" else ROLL_MAX_LEGS_SHORT
|
||||||
|
|
||||||
|
|
||||||
def avg_entry_after_add(
|
def avg_entry_after_add(
|
||||||
qty_existing: float,
|
qty_existing: float,
|
||||||
entry_existing: float,
|
entry_existing: float,
|
||||||
add_qty: float,
|
add_qty: float,
|
||||||
add_price: float,
|
add_price: float,
|
||||||
) -> float:
|
) -> float:
|
||||||
q1 = float(qty_existing)
|
q1 = float(qty_existing)
|
||||||
e1 = float(entry_existing)
|
e1 = float(entry_existing)
|
||||||
q2 = float(add_qty)
|
q2 = float(add_qty)
|
||||||
e2 = float(add_price)
|
e2 = float(add_price)
|
||||||
total = q1 + q2
|
total = q1 + q2
|
||||||
if total <= 0:
|
if total <= 0:
|
||||||
return 0.0
|
return 0.0
|
||||||
return (q1 * e1 + q2 * e2) / total
|
return (q1 * e1 + q2 * e2) / total
|
||||||
|
|
||||||
|
|
||||||
def calc_risk_budget_usdt(capital_base_usdt: float, risk_percent: float) -> float:
|
def calc_risk_budget_usdt(capital_base_usdt: float, risk_percent: float) -> float:
|
||||||
return float(capital_base_usdt) * (float(risk_percent) / 100.0)
|
return float(capital_base_usdt) * (float(risk_percent) / 100.0)
|
||||||
|
|
||||||
|
|
||||||
def solve_add_amount_for_total_risk(
|
def solve_add_amount_for_total_risk(
|
||||||
direction: str,
|
direction: str,
|
||||||
qty_existing: float,
|
qty_existing: float,
|
||||||
entry_existing: float,
|
entry_existing: float,
|
||||||
add_price: float,
|
add_price: float,
|
||||||
new_stop: float,
|
new_stop: float,
|
||||||
risk_budget_usdt: float,
|
risk_budget_usdt: float,
|
||||||
contract_size: float = 1.0,
|
contract_size: float = 1.0,
|
||||||
) -> Tuple[Optional[float], Optional[str]]:
|
) -> Tuple[Optional[float], Optional[str]]:
|
||||||
"""
|
"""
|
||||||
合并持仓打到 new_stop 时总亏损 ≈ risk_budget(方案 C)。
|
合并持仓打到 new_stop 时总亏损 ≈ risk_budget(方案 C)。
|
||||||
long: (avg - SL) * (Q1+Q2) * cs = B => Q2 = (B/cs - Q1*(E1-SL)) / (E2-SL)
|
long: (avg - SL) * (Q1+Q2) * cs = B => Q2 = (B/cs - Q1*(E1-SL)) / (E2-SL)
|
||||||
short: (SL - avg) * (Q1+Q2) * cs = B => Q2 = (B/cs - Q1*(SL-E1)) / (SL-E2)
|
short: (SL - avg) * (Q1+Q2) * cs = B => Q2 = (B/cs - Q1*(SL-E1)) / (SL-E2)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
q1 = float(qty_existing)
|
q1 = float(qty_existing)
|
||||||
e1 = float(entry_existing)
|
e1 = float(entry_existing)
|
||||||
e2 = float(add_price)
|
e2 = float(add_price)
|
||||||
sl = float(new_stop)
|
sl = float(new_stop)
|
||||||
b = float(risk_budget_usdt)
|
b = float(risk_budget_usdt)
|
||||||
cs = float(contract_size) if contract_size else 1.0
|
cs = float(contract_size) if contract_size else 1.0
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None, "参数格式错误"
|
return None, "参数格式错误"
|
||||||
if q1 <= 0 or e1 <= 0 or e2 <= 0 or b <= 0 or cs <= 0:
|
if q1 <= 0 or e1 <= 0 or e2 <= 0 or b <= 0 or cs <= 0:
|
||||||
return None, "持仓或风险预算无效"
|
return None, "持仓或风险预算无效"
|
||||||
direction = (direction or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
if direction == "short":
|
if direction == "short":
|
||||||
denom = sl - e2
|
denom = sl - e2
|
||||||
numer = b / cs - q1 * (sl - e1)
|
numer = b / cs - q1 * (sl - e1)
|
||||||
if denom <= 0:
|
if denom <= 0:
|
||||||
return None, "做空:新止损须高于加仓价"
|
return None, "做空:新止损须高于加仓价"
|
||||||
else:
|
else:
|
||||||
denom = e2 - sl
|
denom = e2 - sl
|
||||||
numer = b / cs - q1 * (e1 - sl)
|
numer = b / cs - q1 * (e1 - sl)
|
||||||
if denom <= 0:
|
if denom <= 0:
|
||||||
return None, "做多:新止损须低于加仓价"
|
return None, "做多:新止损须低于加仓价"
|
||||||
q2 = numer / denom
|
q2 = numer / denom
|
||||||
if q2 <= 0:
|
if q2 <= 0:
|
||||||
return None, "按当前新止损与风险预算,无需加仓或无法再加(已满足风险上限)"
|
return None, "按当前新止损与风险预算,无需加仓或无法再加(已满足风险上限)"
|
||||||
return q2, None
|
return q2, None
|
||||||
|
|
||||||
|
|
||||||
def loss_at_stop_usdt(
|
def loss_at_stop_usdt(
|
||||||
direction: str,
|
direction: str,
|
||||||
avg: float,
|
avg: float,
|
||||||
qty: float,
|
qty: float,
|
||||||
stop: float,
|
stop: float,
|
||||||
contract_size: float = 1.0,
|
contract_size: float = 1.0,
|
||||||
) -> float:
|
) -> float:
|
||||||
cs = float(contract_size or 1.0)
|
cs = float(contract_size or 1.0)
|
||||||
direction = (direction or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
if direction == "short":
|
if direction == "short":
|
||||||
return (float(stop) - float(avg)) * float(qty) * cs
|
return (float(stop) - float(avg)) * float(qty) * cs
|
||||||
return (float(avg) - float(stop)) * float(qty) * cs
|
return (float(avg) - float(stop)) * float(qty) * cs
|
||||||
|
|
||||||
|
|
||||||
def reward_at_tp_usdt(
|
def reward_at_tp_usdt(
|
||||||
direction: str,
|
direction: str,
|
||||||
avg: float,
|
avg: float,
|
||||||
take_profit: float,
|
take_profit: float,
|
||||||
qty: float,
|
qty: float,
|
||||||
contract_size: float = 1.0,
|
contract_size: float = 1.0,
|
||||||
) -> float:
|
) -> float:
|
||||||
cs = float(contract_size or 1.0)
|
cs = float(contract_size or 1.0)
|
||||||
direction = (direction or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
if direction == "short":
|
if direction == "short":
|
||||||
return (float(avg) - float(take_profit)) * float(qty) * cs
|
return (float(avg) - float(take_profit)) * float(qty) * cs
|
||||||
return (float(take_profit) - float(avg)) * float(qty) * cs
|
return (float(take_profit) - float(avg)) * float(qty) * cs
|
||||||
|
|
||||||
|
|
||||||
def roll_fib_trigger_crossed(
|
def roll_fib_trigger_crossed(
|
||||||
direction: str,
|
direction: str,
|
||||||
prev_mark: Optional[float],
|
prev_mark: Optional[float],
|
||||||
mark: float,
|
mark: float,
|
||||||
limit_price: float,
|
limit_price: float,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""斐波:多=向下穿越限价;空=向上穿越限价。"""
|
"""斐波:多=向下穿越限价;空=向上穿越限价。"""
|
||||||
try:
|
try:
|
||||||
m = float(mark)
|
m = float(mark)
|
||||||
lv = float(limit_price)
|
lv = float(limit_price)
|
||||||
pm = float(prev_mark) if prev_mark is not None else None
|
pm = float(prev_mark) if prev_mark is not None else None
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return False
|
return False
|
||||||
direction = (direction or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
if direction == "long":
|
if direction == "long":
|
||||||
if pm is None:
|
if pm is None:
|
||||||
return m <= lv
|
return m <= lv
|
||||||
return pm > lv and m <= lv
|
return pm > lv and m <= lv
|
||||||
if pm is None:
|
if pm is None:
|
||||||
return m >= lv
|
return m >= lv
|
||||||
return pm < lv and m >= lv
|
return pm < lv and m >= lv
|
||||||
|
|
||||||
|
|
||||||
def roll_breakout_trigger_crossed(
|
def roll_breakout_trigger_crossed(
|
||||||
direction: str,
|
direction: str,
|
||||||
prev_mark: Optional[float],
|
prev_mark: Optional[float],
|
||||||
mark: float,
|
mark: float,
|
||||||
breakthrough_price: float,
|
breakthrough_price: float,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""突破:多=向上穿越突破价;空=向下穿越突破价。"""
|
"""突破:多=向上穿越突破价;空=向下穿越突破价。"""
|
||||||
try:
|
try:
|
||||||
m = float(mark)
|
m = float(mark)
|
||||||
bp = float(breakthrough_price)
|
bp = float(breakthrough_price)
|
||||||
pm = float(prev_mark) if prev_mark is not None else None
|
pm = float(prev_mark) if prev_mark is not None else None
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return False
|
return False
|
||||||
direction = (direction or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
if direction == "long":
|
if direction == "long":
|
||||||
if pm is None:
|
if pm is None:
|
||||||
return m > bp
|
return m > bp
|
||||||
return pm <= bp and m > bp
|
return pm <= bp and m > bp
|
||||||
if pm is None:
|
if pm is None:
|
||||||
return m < bp
|
return m < bp
|
||||||
return pm >= bp and m < bp
|
return pm >= bp and m < bp
|
||||||
|
|
||||||
|
|
||||||
def roll_fib_invalidate(direction: str, mark: float, upper: float, lower: float) -> bool:
|
def roll_fib_invalidate(direction: str, mark: float, upper: float, lower: float) -> bool:
|
||||||
"""斐波 pending 失效:止盈侧突破(多 mark>=H;空 mark<=L)。"""
|
"""斐波 pending 失效:止盈侧突破(多 mark>=H;空 mark<=L)。"""
|
||||||
return fib_invalidate_by_mark(direction, mark, upper, lower)
|
return fib_invalidate_by_mark(direction, mark, upper, lower)
|
||||||
|
|
||||||
|
|
||||||
def roll_breakout_invalidate(direction: str, mark: float, stop_loss: float) -> bool:
|
def roll_breakout_invalidate(direction: str, mark: float, stop_loss: float) -> bool:
|
||||||
"""突破 pending 失效:未到突破价先触达止损侧(多 mark<=S;空 mark>=S)。"""
|
"""突破 pending 失效:未到突破价先触达止损侧(多 mark<=S;空 mark>=S)。"""
|
||||||
try:
|
try:
|
||||||
m = float(mark)
|
m = float(mark)
|
||||||
sl = float(stop_loss)
|
sl = float(stop_loss)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return False
|
return False
|
||||||
direction = (direction or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
if direction == "long":
|
if direction == "long":
|
||||||
return m <= sl
|
return m <= sl
|
||||||
return m >= sl
|
return m >= sl
|
||||||
|
|
||||||
|
|
||||||
def validate_roll_geometry(
|
def validate_roll_geometry(
|
||||||
direction: str,
|
direction: str,
|
||||||
add_mode: str,
|
add_mode: str,
|
||||||
*,
|
*,
|
||||||
new_stop_loss: float,
|
new_stop_loss: float,
|
||||||
add_price: Optional[float] = None,
|
add_price: Optional[float] = None,
|
||||||
fib_upper: Optional[float] = None,
|
fib_upper: Optional[float] = None,
|
||||||
fib_lower: Optional[float] = None,
|
fib_lower: Optional[float] = None,
|
||||||
breakthrough_price: Optional[float] = None,
|
breakthrough_price: Optional[float] = None,
|
||||||
entry_existing: float = 0.0,
|
entry_existing: float = 0.0,
|
||||||
initial_take_profit: float = 0.0,
|
initial_take_profit: float = 0.0,
|
||||||
mark_price: Optional[float] = None,
|
mark_price: Optional[float] = None,
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
direction = (direction or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
mode = (add_mode or MARKET_MODE).strip().lower()
|
mode = (add_mode or MARKET_MODE).strip().lower()
|
||||||
try:
|
try:
|
||||||
sl = float(new_stop_loss)
|
sl = float(new_stop_loss)
|
||||||
tp = float(initial_take_profit)
|
tp = float(initial_take_profit)
|
||||||
e1 = float(entry_existing or 0)
|
e1 = float(entry_existing or 0)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return "止损/止盈格式错误"
|
return "止损/止盈格式错误"
|
||||||
if sl <= 0 or tp <= 0:
|
if sl <= 0 or tp <= 0:
|
||||||
return "止损与首仓止盈须大于0"
|
return "止损与首仓止盈须大于0"
|
||||||
if direction == "long":
|
if direction == "long":
|
||||||
if e1 > 0 and tp <= e1:
|
if e1 > 0 and tp <= e1:
|
||||||
return "做多:首仓止盈须高于当前持仓均价"
|
return "做多:首仓止盈须高于当前持仓均价"
|
||||||
else:
|
else:
|
||||||
if e1 > 0 and tp >= e1:
|
if e1 > 0 and tp >= e1:
|
||||||
return "做空:首仓止盈须低于当前持仓均价"
|
return "做空:首仓止盈须低于当前持仓均价"
|
||||||
|
|
||||||
if mode == MARKET_MODE:
|
if mode == MARKET_MODE:
|
||||||
if add_price is None or float(add_price) <= 0:
|
if add_price is None or float(add_price) <= 0:
|
||||||
return "市价加仓需要有效参考价"
|
return "市价加仓需要有效参考价"
|
||||||
entry_add = float(add_price)
|
entry_add = float(add_price)
|
||||||
elif mode in FIB_MODES:
|
elif mode in FIB_MODES:
|
||||||
if fib_upper is None or fib_lower is None:
|
if fib_upper is None or fib_lower is None:
|
||||||
return "斐波须填写上沿 H 与下沿 L"
|
return "斐波须填写上沿 H 与下沿 L"
|
||||||
entry_add, err = fib_limit_entry(direction, float(fib_upper), float(fib_lower), mode)
|
entry_add, err = fib_limit_entry(direction, float(fib_upper), float(fib_lower), mode)
|
||||||
if err:
|
if err:
|
||||||
return err
|
return err
|
||||||
if entry_add is None or entry_add <= 0:
|
if entry_add is None or entry_add <= 0:
|
||||||
return "无法计算斐波限价"
|
return "无法计算斐波限价"
|
||||||
elif mode == BREAKOUT_MODE:
|
elif mode == BREAKOUT_MODE:
|
||||||
if breakthrough_price is None:
|
if breakthrough_price is None:
|
||||||
return "突破加仓须填写突破价"
|
return "突破加仓须填写突破价"
|
||||||
try:
|
try:
|
||||||
bp = float(breakthrough_price)
|
bp = float(breakthrough_price)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return "突破价格式错误"
|
return "突破价格式错误"
|
||||||
if bp <= 0:
|
if bp <= 0:
|
||||||
return "突破价须大于0"
|
return "突破价须大于0"
|
||||||
entry_add = bp
|
entry_add = bp
|
||||||
if direction == "long":
|
if direction == "long":
|
||||||
if sl >= bp:
|
if sl >= bp:
|
||||||
return "做多:止损须低于突破价"
|
return "做多:止损须低于突破价"
|
||||||
if mark_price is not None and float(mark_price) >= bp:
|
if mark_price is not None and float(mark_price) >= bp:
|
||||||
return "做多:当前价须低于突破价(等待向上突破)"
|
return "做多:当前价须低于突破价(等待向上突破)"
|
||||||
else:
|
else:
|
||||||
if sl <= bp:
|
if sl <= bp:
|
||||||
return "做空:止损须高于突破价"
|
return "做空:止损须高于突破价"
|
||||||
if mark_price is not None and float(mark_price) <= bp:
|
if mark_price is not None and float(mark_price) <= bp:
|
||||||
return "做空:当前价须高于突破价(等待向下跌破)"
|
return "做空:当前价须高于突破价(等待向下跌破)"
|
||||||
else:
|
else:
|
||||||
return "加仓方式无效"
|
return "加仓方式无效"
|
||||||
|
|
||||||
if mode != BREAKOUT_MODE:
|
if mode != BREAKOUT_MODE:
|
||||||
entry_add = float(entry_add) # type: ignore[arg-type]
|
entry_add = float(entry_add) # type: ignore[arg-type]
|
||||||
if direction == "long":
|
if direction == "long":
|
||||||
if sl >= entry_add:
|
if sl >= entry_add:
|
||||||
return "做多:新止损须低于加仓价"
|
return "做多:新止损须低于加仓价"
|
||||||
else:
|
else:
|
||||||
if sl <= entry_add:
|
if sl <= entry_add:
|
||||||
return "做空:新止损须高于加仓价"
|
return "做空:新止损须高于加仓价"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def preview_roll(
|
def preview_roll(
|
||||||
*,
|
*,
|
||||||
direction: str,
|
direction: str,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
qty_existing: float,
|
qty_existing: float,
|
||||||
entry_existing: float,
|
entry_existing: float,
|
||||||
initial_take_profit: float,
|
initial_take_profit: float,
|
||||||
add_mode: str,
|
add_mode: str,
|
||||||
new_stop_loss: Optional[float] = None,
|
new_stop_loss: Optional[float] = None,
|
||||||
risk_percent: float,
|
risk_percent: float,
|
||||||
capital_base_usdt: float,
|
capital_base_usdt: float,
|
||||||
add_price: Optional[float] = None,
|
add_price: Optional[float] = None,
|
||||||
fib_upper: Optional[float] = None,
|
fib_upper: Optional[float] = None,
|
||||||
fib_lower: Optional[float] = None,
|
fib_lower: Optional[float] = None,
|
||||||
breakthrough_price: Optional[float] = None,
|
breakthrough_price: Optional[float] = None,
|
||||||
legs_done: int = 0,
|
legs_done: int = 0,
|
||||||
contract_size: float = 1.0,
|
contract_size: float = 1.0,
|
||||||
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
|
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
|
||||||
direction = (direction or "long").strip().lower()
|
direction = (direction or "long").strip().lower()
|
||||||
if legs_done >= max_roll_legs(direction):
|
if legs_done >= max_roll_legs(direction):
|
||||||
return None, f"{'做多' if direction == 'long' else '做空'}滚仓已达 {max_roll_legs(direction)} 次上限"
|
return None, f"{'做多' if direction == 'long' else '做空'}滚仓已达 {max_roll_legs(direction)} 次上限"
|
||||||
mode = (add_mode or MARKET_MODE).strip().lower()
|
mode = (add_mode or MARKET_MODE).strip().lower()
|
||||||
if new_stop_loss is None:
|
if new_stop_loss is None:
|
||||||
return None, "请填写新止损价"
|
return None, "请填写新止损价"
|
||||||
try:
|
try:
|
||||||
sl = float(new_stop_loss)
|
sl = float(new_stop_loss)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None, "止损价格式错误"
|
return None, "止损价格式错误"
|
||||||
if sl <= 0:
|
if sl <= 0:
|
||||||
return None, "止损须大于0"
|
return None, "止损须大于0"
|
||||||
|
|
||||||
geom_err = validate_roll_geometry(
|
geom_err = validate_roll_geometry(
|
||||||
direction,
|
direction,
|
||||||
mode,
|
mode,
|
||||||
new_stop_loss=sl,
|
new_stop_loss=sl,
|
||||||
add_price=add_price,
|
add_price=add_price,
|
||||||
fib_upper=fib_upper,
|
fib_upper=fib_upper,
|
||||||
fib_lower=fib_lower,
|
fib_lower=fib_lower,
|
||||||
breakthrough_price=breakthrough_price,
|
breakthrough_price=breakthrough_price,
|
||||||
entry_existing=entry_existing,
|
entry_existing=entry_existing,
|
||||||
initial_take_profit=initial_take_profit,
|
initial_take_profit=initial_take_profit,
|
||||||
mark_price=add_price if mode == BREAKOUT_MODE else add_price,
|
mark_price=add_price if mode == BREAKOUT_MODE else add_price,
|
||||||
)
|
)
|
||||||
if geom_err:
|
if geom_err:
|
||||||
return None, geom_err
|
return None, geom_err
|
||||||
|
|
||||||
if mode == MARKET_MODE:
|
if mode == MARKET_MODE:
|
||||||
entry_add = float(add_price) # validated
|
entry_add = float(add_price) # validated
|
||||||
elif mode in FIB_MODES:
|
elif mode in FIB_MODES:
|
||||||
entry_add, _ = fib_limit_entry(direction, float(fib_upper), float(fib_lower), mode)
|
entry_add, _ = fib_limit_entry(direction, float(fib_upper), float(fib_lower), mode)
|
||||||
entry_add = float(entry_add or 0)
|
entry_add = float(entry_add or 0)
|
||||||
else:
|
else:
|
||||||
entry_add = float(breakthrough_price or 0)
|
entry_add = float(breakthrough_price or 0)
|
||||||
|
|
||||||
risk_budget = calc_risk_budget_usdt(capital_base_usdt, risk_percent)
|
risk_budget = calc_risk_budget_usdt(capital_base_usdt, risk_percent)
|
||||||
q2_raw, err = solve_add_amount_for_total_risk(
|
q2_raw, err = solve_add_amount_for_total_risk(
|
||||||
direction,
|
direction,
|
||||||
qty_existing,
|
qty_existing,
|
||||||
entry_existing,
|
entry_existing,
|
||||||
entry_add,
|
entry_add,
|
||||||
sl,
|
sl,
|
||||||
risk_budget,
|
risk_budget,
|
||||||
contract_size,
|
contract_size,
|
||||||
)
|
)
|
||||||
if err:
|
if err:
|
||||||
return None, err
|
return None, err
|
||||||
q2 = float(q2_raw)
|
q2 = float(q2_raw)
|
||||||
new_qty = qty_existing + q2
|
new_qty = qty_existing + q2
|
||||||
new_avg = avg_entry_after_add(qty_existing, entry_existing, q2, entry_add)
|
new_avg = avg_entry_after_add(qty_existing, entry_existing, q2, entry_add)
|
||||||
cs = float(contract_size or 1.0)
|
cs = float(contract_size or 1.0)
|
||||||
loss_sl = loss_at_stop_usdt(direction, new_avg, new_qty, sl, cs)
|
loss_sl = loss_at_stop_usdt(direction, new_avg, new_qty, sl, cs)
|
||||||
reward_tp = reward_at_tp_usdt(direction, new_avg, initial_take_profit, new_qty, cs)
|
reward_tp = reward_at_tp_usdt(direction, new_avg, initial_take_profit, new_qty, cs)
|
||||||
return {
|
return {
|
||||||
"symbol": symbol,
|
"symbol": symbol,
|
||||||
"direction": direction,
|
"direction": direction,
|
||||||
"add_mode": mode,
|
"add_mode": mode,
|
||||||
"add_mode_label": mode_label(mode),
|
"add_mode_label": mode_label(mode),
|
||||||
"add_price": round(entry_add, 10),
|
"add_price": round(entry_add, 10),
|
||||||
"new_stop_loss": round(sl, 10),
|
"new_stop_loss": round(sl, 10),
|
||||||
"breakthrough_price": float(breakthrough_price) if breakthrough_price not in (None, "") else None,
|
"breakthrough_price": float(breakthrough_price) if breakthrough_price not in (None, "") else None,
|
||||||
"initial_take_profit": float(initial_take_profit),
|
"initial_take_profit": float(initial_take_profit),
|
||||||
"risk_percent": float(risk_percent),
|
"risk_percent": float(risk_percent),
|
||||||
"risk_budget_usdt": round(risk_budget, 4),
|
"risk_budget_usdt": round(risk_budget, 4),
|
||||||
"add_amount_raw": q2,
|
"add_amount_raw": q2,
|
||||||
"qty_existing": float(qty_existing),
|
"qty_existing": float(qty_existing),
|
||||||
"entry_existing": float(entry_existing),
|
"entry_existing": float(entry_existing),
|
||||||
"qty_after": new_qty,
|
"qty_after": new_qty,
|
||||||
"avg_entry_after": round(new_avg, 10),
|
"avg_entry_after": round(new_avg, 10),
|
||||||
"loss_at_sl_usdt": round(loss_sl, 4),
|
"loss_at_sl_usdt": round(loss_sl, 4),
|
||||||
"reward_at_tp_usdt": round(reward_tp, 4),
|
"reward_at_tp_usdt": round(reward_tp, 4),
|
||||||
"legs_done": int(legs_done),
|
"legs_done": int(legs_done),
|
||||||
"leg_index_next": int(legs_done) + 1,
|
"leg_index_next": int(legs_done) + 1,
|
||||||
"fib_upper": fib_upper,
|
"fib_upper": fib_upper,
|
||||||
"fib_lower": fib_lower,
|
"fib_lower": fib_lower,
|
||||||
"contract_size": cs,
|
"contract_size": cs,
|
||||||
}, None
|
}, None
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,144 +1,144 @@
|
|||||||
"""策略交易页:主站 index.html 所需数据(顺势加仓等)。"""
|
"""策略交易页:主站 index.html 所需数据(顺势加仓等)。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
from strategy_db import init_strategy_tables
|
from lib.strategy.strategy_db import init_strategy_tables
|
||||||
from strategy_roll_monitor_lib import roll_leg_status_label
|
from lib.strategy.strategy_roll_monitor_lib import roll_leg_status_label
|
||||||
|
|
||||||
|
|
||||||
def _row_to_dict(row) -> dict:
|
def _row_to_dict(row) -> dict:
|
||||||
if row is None:
|
if row is None:
|
||||||
return {}
|
return {}
|
||||||
try:
|
try:
|
||||||
return dict(row)
|
return dict(row)
|
||||||
except Exception:
|
except Exception:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def count_active_trend_plans(conn, count_fn: Optional[Callable] = None) -> int:
|
def count_active_trend_plans(conn, count_fn: Optional[Callable] = None) -> int:
|
||||||
if callable(count_fn):
|
if callable(count_fn):
|
||||||
return int(count_fn(conn) or 0)
|
return int(count_fn(conn) or 0)
|
||||||
try:
|
try:
|
||||||
return int(
|
return int(
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'"
|
"SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'"
|
||||||
).fetchone()[0]
|
).fetchone()[0]
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def fetch_roll_page_data(
|
def fetch_roll_page_data(
|
||||||
conn,
|
conn,
|
||||||
*,
|
*,
|
||||||
default_risk_percent: float = 2.0,
|
default_risk_percent: float = 2.0,
|
||||||
count_active_trends: Optional[Callable] = None,
|
count_active_trends: Optional[Callable] = None,
|
||||||
roll_cfg: dict | None = None,
|
roll_cfg: dict | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
init_strategy_tables(conn)
|
init_strategy_tables(conn)
|
||||||
monitors = []
|
monitors = []
|
||||||
for row in conn.execute(
|
for row in conn.execute(
|
||||||
"SELECT * FROM order_monitors WHERE status='active' ORDER BY id DESC"
|
"SELECT * FROM order_monitors WHERE status='active' ORDER BY id DESC"
|
||||||
).fetchall():
|
).fetchall():
|
||||||
monitors.append(_row_to_dict(row))
|
monitors.append(_row_to_dict(row))
|
||||||
roll_groups = []
|
roll_groups = []
|
||||||
for row in conn.execute(
|
for row in conn.execute(
|
||||||
"""SELECT g.* FROM roll_groups g
|
"""SELECT g.* FROM roll_groups g
|
||||||
INNER JOIN order_monitors m ON m.id = g.order_monitor_id AND m.status='active'
|
INNER JOIN order_monitors m ON m.id = g.order_monitor_id AND m.status='active'
|
||||||
WHERE g.status='active'
|
WHERE g.status='active'
|
||||||
ORDER BY g.id DESC"""
|
ORDER BY g.id DESC"""
|
||||||
).fetchall():
|
).fetchall():
|
||||||
roll_groups.append(_row_to_dict(row))
|
roll_groups.append(_row_to_dict(row))
|
||||||
active_gids = {int(g["id"]) for g in roll_groups if g.get("id") is not None}
|
active_gids = {int(g["id"]) for g in roll_groups if g.get("id") is not None}
|
||||||
roll_legs = []
|
roll_legs = []
|
||||||
for row in conn.execute(
|
for row in conn.execute(
|
||||||
"SELECT * FROM roll_legs ORDER BY id DESC LIMIT 80"
|
"SELECT * FROM roll_legs ORDER BY id DESC LIMIT 80"
|
||||||
).fetchall():
|
).fetchall():
|
||||||
leg = _row_to_dict(row)
|
leg = _row_to_dict(row)
|
||||||
gid = leg.get("roll_group_id")
|
gid = leg.get("roll_group_id")
|
||||||
if gid is not None and int(gid) not in active_gids:
|
if gid is not None and int(gid) not in active_gids:
|
||||||
continue
|
continue
|
||||||
leg["status_label"] = roll_leg_status_label(leg.get("status"))
|
leg["status_label"] = roll_leg_status_label(leg.get("status"))
|
||||||
roll_legs.append(leg)
|
roll_legs.append(leg)
|
||||||
roll_legs = roll_legs[:50]
|
roll_legs = roll_legs[:50]
|
||||||
out = {
|
out = {
|
||||||
"roll_monitors": monitors,
|
"roll_monitors": monitors,
|
||||||
"roll_groups": roll_groups,
|
"roll_groups": roll_groups,
|
||||||
"roll_legs": roll_legs,
|
"roll_legs": roll_legs,
|
||||||
"roll_trend_active": count_active_trend_plans(conn, count_active_trends),
|
"roll_trend_active": count_active_trend_plans(conn, count_active_trends),
|
||||||
"default_risk_percent": default_risk_percent,
|
"default_risk_percent": default_risk_percent,
|
||||||
}
|
}
|
||||||
if roll_cfg:
|
if roll_cfg:
|
||||||
from strategy_roll_ui_lib import enrich_roll_page_data
|
from lib.strategy.strategy_roll_ui_lib import enrich_roll_page_data
|
||||||
|
|
||||||
enrich_roll_page_data(conn, out, roll_cfg)
|
enrich_roll_page_data(conn, out, roll_cfg)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_TREND_DISABLED_NOTE = (
|
DEFAULT_TREND_DISABLED_NOTE = (
|
||||||
"趋势回调(预览、自动补仓、程序止盈)仅在 Gate 趋势机器人实例 "
|
"趋势回调(预览、自动补仓、程序止盈)仅在 Gate 趋势机器人实例 "
|
||||||
"(crypto_monitor_gate_bot,常见端口 5002)中启用。"
|
"(crypto_monitor_gate_bot,常见端口 5002)中启用。"
|
||||||
"币安 / Gate 主站 / OKX 可使用本页「顺势加仓」;完整趋势回调请打开该实例。"
|
"币安 / Gate 主站 / OKX 可使用本页「顺势加仓」;完整趋势回调请打开该实例。"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def strategy_render_extras(
|
def strategy_render_extras(
|
||||||
conn,
|
conn,
|
||||||
page: str,
|
page: str,
|
||||||
*,
|
*,
|
||||||
default_risk_percent: float = 2.0,
|
default_risk_percent: float = 2.0,
|
||||||
count_active_trends: Optional[Callable] = None,
|
count_active_trends: Optional[Callable] = None,
|
||||||
trend_disabled_note: str = "",
|
trend_disabled_note: str = "",
|
||||||
request_obj=None,
|
request_obj=None,
|
||||||
trend_cfg: Optional[dict] = None,
|
trend_cfg: Optional[dict] = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""render_main_page 策略相关页变量(含策略交易记录)。"""
|
"""render_main_page 策略相关页变量(含策略交易记录)。"""
|
||||||
if page == "strategy_records":
|
if page == "strategy_records":
|
||||||
from strategy_records_register import load_strategy_records_page
|
from lib.strategy.strategy_records_register import load_strategy_records_page
|
||||||
|
|
||||||
return load_strategy_records_page(conn)
|
return load_strategy_records_page(conn)
|
||||||
return strategy_page_template_vars(
|
return strategy_page_template_vars(
|
||||||
conn,
|
conn,
|
||||||
page,
|
page,
|
||||||
default_risk_percent=default_risk_percent,
|
default_risk_percent=default_risk_percent,
|
||||||
count_active_trends=count_active_trends,
|
count_active_trends=count_active_trends,
|
||||||
trend_disabled_note=trend_disabled_note,
|
trend_disabled_note=trend_disabled_note,
|
||||||
request_obj=request_obj,
|
request_obj=request_obj,
|
||||||
trend_cfg=trend_cfg,
|
trend_cfg=trend_cfg,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def strategy_page_template_vars(
|
def strategy_page_template_vars(
|
||||||
conn,
|
conn,
|
||||||
page: str,
|
page: str,
|
||||||
*,
|
*,
|
||||||
default_risk_percent: float = 2.0,
|
default_risk_percent: float = 2.0,
|
||||||
count_active_trends: Optional[Callable] = None,
|
count_active_trends: Optional[Callable] = None,
|
||||||
trend_disabled_note: str = "",
|
trend_disabled_note: str = "",
|
||||||
request_obj=None,
|
request_obj=None,
|
||||||
trend_cfg: Optional[dict] = None,
|
trend_cfg: Optional[dict] = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""render_main_page 在 conn.close() 前合并进 render_template 的变量。"""
|
"""render_main_page 在 conn.close() 前合并进 render_template 的变量。"""
|
||||||
if page not in ("strategy", "strategy_trend", "strategy_roll"):
|
if page not in ("strategy", "strategy_trend", "strategy_roll"):
|
||||||
return {}
|
return {}
|
||||||
roll_cfg = None
|
roll_cfg = None
|
||||||
try:
|
try:
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
roll_cfg = (current_app.extensions or {}).get("strategy_roll_cfg")
|
roll_cfg = (current_app.extensions or {}).get("strategy_roll_cfg")
|
||||||
except Exception:
|
except Exception:
|
||||||
roll_cfg = None
|
roll_cfg = None
|
||||||
out = fetch_roll_page_data(
|
out = fetch_roll_page_data(
|
||||||
conn,
|
conn,
|
||||||
default_risk_percent=default_risk_percent,
|
default_risk_percent=default_risk_percent,
|
||||||
count_active_trends=count_active_trends,
|
count_active_trends=count_active_trends,
|
||||||
roll_cfg=roll_cfg if isinstance(roll_cfg, dict) else None,
|
roll_cfg=roll_cfg if isinstance(roll_cfg, dict) else None,
|
||||||
)
|
)
|
||||||
if trend_cfg and request_obj is not None:
|
if trend_cfg and request_obj is not None:
|
||||||
from strategy_trend_register import load_trend_page_context
|
from lib.strategy.strategy_trend_register import load_trend_page_context
|
||||||
|
|
||||||
out.update(load_trend_page_context(conn, request_obj, trend_cfg))
|
out.update(load_trend_page_context(conn, request_obj, trend_cfg))
|
||||||
elif page == "strategy_trend":
|
elif page == "strategy_trend":
|
||||||
out["trend_disabled_note"] = trend_disabled_note or DEFAULT_TREND_DISABLED_NOTE
|
out["trend_disabled_note"] = trend_disabled_note or DEFAULT_TREND_DISABLED_NOTE
|
||||||
return out
|
return out
|
||||||
@@ -1,192 +1,192 @@
|
|||||||
"""策略计划(趋势回调 / 滚仓)开始与结束 — 企业微信推送(四所共用)。"""
|
"""策略计划(趋势回调 / 滚仓)开始与结束 — 企业微信推送(四所共用)。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from wechat_notify_lib import wechat_direction_label
|
from lib.common.wechat_notify_lib import wechat_direction_label
|
||||||
|
|
||||||
|
|
||||||
def _send(cfg: dict[str, Any], content: str) -> None:
|
def _send(cfg: dict[str, Any], content: str) -> None:
|
||||||
fn = cfg.get("send_wechat")
|
fn = cfg.get("send_wechat")
|
||||||
if callable(fn):
|
if callable(fn):
|
||||||
try:
|
try:
|
||||||
fn(content)
|
fn(content)
|
||||||
return
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
m = cfg.get("app_module")
|
m = cfg.get("app_module")
|
||||||
if m is not None:
|
if m is not None:
|
||||||
sw = getattr(m, "send_wechat_msg", None)
|
sw = getattr(m, "send_wechat_msg", None)
|
||||||
if callable(sw):
|
if callable(sw):
|
||||||
try:
|
try:
|
||||||
sw(content)
|
sw(content)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _account(cfg: dict[str, Any]) -> str:
|
def _account(cfg: dict[str, Any]) -> str:
|
||||||
fn = cfg.get("wechat_account_label")
|
fn = cfg.get("wechat_account_label")
|
||||||
if callable(fn):
|
if callable(fn):
|
||||||
try:
|
try:
|
||||||
return str(fn()).strip() or _exchange(cfg)
|
return str(fn()).strip() or _exchange(cfg)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return _exchange(cfg)
|
return _exchange(cfg)
|
||||||
|
|
||||||
|
|
||||||
def _exchange(cfg: dict[str, Any]) -> str:
|
def _exchange(cfg: dict[str, Any]) -> str:
|
||||||
return str(cfg.get("exchange_display") or "").strip() or "交易账户"
|
return str(cfg.get("exchange_display") or "").strip() or "交易账户"
|
||||||
|
|
||||||
|
|
||||||
def _dir_text(cfg: dict[str, Any], direction: str) -> str:
|
def _dir_text(cfg: dict[str, Any], direction: str) -> str:
|
||||||
fn = cfg.get("wechat_direction_text")
|
fn = cfg.get("wechat_direction_text")
|
||||||
if callable(fn):
|
if callable(fn):
|
||||||
try:
|
try:
|
||||||
return str(fn(direction))
|
return str(fn(direction))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return wechat_direction_label(direction)
|
return wechat_direction_label(direction)
|
||||||
|
|
||||||
|
|
||||||
def _fmt_price(cfg: dict[str, Any], symbol: str, price: Any) -> str:
|
def _fmt_price(cfg: dict[str, Any], symbol: str, price: Any) -> str:
|
||||||
if price is None or price == "":
|
if price is None or price == "":
|
||||||
return "—"
|
return "—"
|
||||||
fn = cfg.get("format_price") or cfg.get("price_fmt")
|
fn = cfg.get("format_price") or cfg.get("price_fmt")
|
||||||
if callable(fn):
|
if callable(fn):
|
||||||
try:
|
try:
|
||||||
return str(fn(symbol, price))
|
return str(fn(symbol, price))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
m = cfg.get("app_module")
|
m = cfg.get("app_module")
|
||||||
pf = getattr(m, "format_price_for_symbol", None) if m else None
|
pf = getattr(m, "format_price_for_symbol", None) if m else None
|
||||||
if callable(pf):
|
if callable(pf):
|
||||||
try:
|
try:
|
||||||
return str(pf(symbol, price))
|
return str(pf(symbol, price))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
return str(round(float(price), 8))
|
return str(round(float(price), 8))
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return str(price)
|
return str(price)
|
||||||
|
|
||||||
|
|
||||||
def _fmt_pnl(pnl: Any) -> str:
|
def _fmt_pnl(pnl: Any) -> str:
|
||||||
if pnl is None:
|
if pnl is None:
|
||||||
return "—"
|
return "—"
|
||||||
try:
|
try:
|
||||||
v = float(pnl)
|
v = float(pnl)
|
||||||
return f"{'+' if v > 0 else ''}{round(v, 2)} U"
|
return f"{'+' if v > 0 else ''}{round(v, 2)} U"
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return str(pnl)
|
return str(pnl)
|
||||||
|
|
||||||
|
|
||||||
def notify_trend_plan_started(
|
def notify_trend_plan_started(
|
||||||
cfg: dict[str, Any],
|
cfg: dict[str, Any],
|
||||||
*,
|
*,
|
||||||
plan_id: int,
|
plan_id: int,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
direction: str,
|
direction: str,
|
||||||
leverage: int,
|
leverage: int,
|
||||||
stop_loss: float,
|
stop_loss: float,
|
||||||
take_profit: float,
|
take_profit: float,
|
||||||
add_upper: float,
|
add_upper: float,
|
||||||
risk_percent: float,
|
risk_percent: float,
|
||||||
dca_legs: int,
|
dca_legs: int,
|
||||||
first_order_amount: float,
|
first_order_amount: float,
|
||||||
avg_entry: Optional[float] = None,
|
avg_entry: Optional[float] = None,
|
||||||
snapshot_usdt: Optional[float] = None,
|
snapshot_usdt: Optional[float] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
sym = symbol or "—"
|
sym = symbol or "—"
|
||||||
lines = [
|
lines = [
|
||||||
f"# 🚀 {sym} 趋势回调计划已开始",
|
f"# 🚀 {sym} 趋势回调计划已开始",
|
||||||
f"**账户:{_account(cfg)}**",
|
f"**账户:{_account(cfg)}**",
|
||||||
f"- 计划 ID:**{plan_id}**",
|
f"- 计划 ID:**{plan_id}**",
|
||||||
f"- 方向:{_dir_text(cfg, direction)}|杠杆 **{int(leverage or 1)}x**",
|
f"- 方向:{_dir_text(cfg, direction)}|杠杆 **{int(leverage or 1)}x**",
|
||||||
f"- 止损:{_fmt_price(cfg, sym, stop_loss)}|止盈:{_fmt_price(cfg, sym, take_profit)}",
|
f"- 止损:{_fmt_price(cfg, sym, stop_loss)}|止盈:{_fmt_price(cfg, sym, take_profit)}",
|
||||||
f"- 补仓区:{_fmt_price(cfg, sym, add_upper)}|补仓档 **{int(dca_legs or 0)}** 档",
|
f"- 补仓区:{_fmt_price(cfg, sym, add_upper)}|补仓档 **{int(dca_legs or 0)}** 档",
|
||||||
f"- 风险:**{risk_percent}%**|首仓张数:**{first_order_amount}**",
|
f"- 风险:**{risk_percent}%**|首仓张数:**{first_order_amount}**",
|
||||||
]
|
]
|
||||||
if avg_entry is not None:
|
if avg_entry is not None:
|
||||||
lines.append(f"- 首仓成交价:{_fmt_price(cfg, sym, avg_entry)}")
|
lines.append(f"- 首仓成交价:{_fmt_price(cfg, sym, avg_entry)}")
|
||||||
if snapshot_usdt is not None:
|
if snapshot_usdt is not None:
|
||||||
try:
|
try:
|
||||||
lines.append(f"- 启动时合约可用:**{round(float(snapshot_usdt), 2)} U**")
|
lines.append(f"- 启动时合约可用:**{round(float(snapshot_usdt), 2)} U**")
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
pass
|
pass
|
||||||
lines.append("- 说明:交易所已挂止损;止盈由程序监控;结束/保本将另行推送")
|
lines.append("- 说明:交易所已挂止损;止盈由程序监控;结束/保本将另行推送")
|
||||||
_send(cfg, "\n".join(lines))
|
_send(cfg, "\n".join(lines))
|
||||||
|
|
||||||
|
|
||||||
def notify_trend_plan_ended(
|
def notify_trend_plan_ended(
|
||||||
cfg: dict[str, Any],
|
cfg: dict[str, Any],
|
||||||
*,
|
*,
|
||||||
plan_id: int,
|
plan_id: int,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
direction: str,
|
direction: str,
|
||||||
end_type: str,
|
end_type: str,
|
||||||
result_label: Optional[str] = None,
|
result_label: Optional[str] = None,
|
||||||
exit_price: Optional[float] = None,
|
exit_price: Optional[float] = None,
|
||||||
pnl_amount: Optional[float] = None,
|
pnl_amount: Optional[float] = None,
|
||||||
extra: Optional[str] = None,
|
extra: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
sym = symbol or "—"
|
sym = symbol or "—"
|
||||||
res = (result_label or end_type or "—").strip()
|
res = (result_label or end_type or "—").strip()
|
||||||
lines = [
|
lines = [
|
||||||
f"# 🏁 {sym} 趋势回调计划已结束",
|
f"# 🏁 {sym} 趋势回调计划已结束",
|
||||||
f"**账户:{_account(cfg)}**",
|
f"**账户:{_account(cfg)}**",
|
||||||
f"- 计划 ID:**{plan_id}**",
|
f"- 计划 ID:**{plan_id}**",
|
||||||
f"- 方向:{_dir_text(cfg, direction)}",
|
f"- 方向:{_dir_text(cfg, direction)}",
|
||||||
f"- 结束方式:**{end_type}**",
|
f"- 结束方式:**{end_type}**",
|
||||||
f"- 结果:**{res}**",
|
f"- 结果:**{res}**",
|
||||||
]
|
]
|
||||||
if exit_price is not None:
|
if exit_price is not None:
|
||||||
lines.append(f"- 离场参考价:{_fmt_price(cfg, sym, exit_price)}")
|
lines.append(f"- 离场参考价:{_fmt_price(cfg, sym, exit_price)}")
|
||||||
if pnl_amount is not None:
|
if pnl_amount is not None:
|
||||||
lines.append(f"- 本单盈亏:**{_fmt_pnl(pnl_amount)}**")
|
lines.append(f"- 本单盈亏:**{_fmt_pnl(pnl_amount)}**")
|
||||||
if extra:
|
if extra:
|
||||||
lines.append(f"- {extra}")
|
lines.append(f"- {extra}")
|
||||||
_send(cfg, "\n".join(lines))
|
_send(cfg, "\n".join(lines))
|
||||||
|
|
||||||
|
|
||||||
def notify_roll_group_started(
|
def notify_roll_group_started(
|
||||||
cfg: dict[str, Any],
|
cfg: dict[str, Any],
|
||||||
*,
|
*,
|
||||||
group_id: int,
|
group_id: int,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
direction: str,
|
direction: str,
|
||||||
order_monitor_id: int,
|
order_monitor_id: int,
|
||||||
initial_take_profit: Optional[float] = None,
|
initial_take_profit: Optional[float] = None,
|
||||||
initial_stop_loss: Optional[float] = None,
|
initial_stop_loss: Optional[float] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
sym = symbol or "—"
|
sym = symbol or "—"
|
||||||
lines = [
|
lines = [
|
||||||
f"# 🚀 {sym} 滚仓计划已开始",
|
f"# 🚀 {sym} 滚仓计划已开始",
|
||||||
f"**账户:{_account(cfg)}**",
|
f"**账户:{_account(cfg)}**",
|
||||||
f"- 滚仓组 ID:**{group_id}**|绑定下单监控 **#{order_monitor_id}**",
|
f"- 滚仓组 ID:**{group_id}**|绑定下单监控 **#{order_monitor_id}**",
|
||||||
f"- 方向:{_dir_text(cfg, direction)}",
|
f"- 方向:{_dir_text(cfg, direction)}",
|
||||||
f"- 首仓止盈(锁定):{_fmt_price(cfg, sym, initial_take_profit)}",
|
f"- 首仓止盈(锁定):{_fmt_price(cfg, sym, initial_take_profit)}",
|
||||||
f"- 当前止损:{_fmt_price(cfg, sym, initial_stop_loss)}",
|
f"- 当前止损:{_fmt_price(cfg, sym, initial_stop_loss)}",
|
||||||
"- 说明:顺势加仓为人工触发;组结束(无持仓/监控结案)将另行推送",
|
"- 说明:顺势加仓为人工触发;组结束(无持仓/监控结案)将另行推送",
|
||||||
]
|
]
|
||||||
_send(cfg, "\n".join(lines))
|
_send(cfg, "\n".join(lines))
|
||||||
|
|
||||||
|
|
||||||
def notify_roll_group_ended(
|
def notify_roll_group_ended(
|
||||||
cfg: dict[str, Any],
|
cfg: dict[str, Any],
|
||||||
*,
|
*,
|
||||||
group_id: int,
|
group_id: int,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
direction: str,
|
direction: str,
|
||||||
reason: str,
|
reason: str,
|
||||||
leg_count: int = 0,
|
leg_count: int = 0,
|
||||||
) -> None:
|
) -> None:
|
||||||
sym = symbol or "—"
|
sym = symbol or "—"
|
||||||
lines = [
|
lines = [
|
||||||
f"# 🏁 {sym} 滚仓计划已结束",
|
f"# 🏁 {sym} 滚仓计划已结束",
|
||||||
f"**账户:{_account(cfg)}**",
|
f"**账户:{_account(cfg)}**",
|
||||||
f"- 滚仓组 ID:**{group_id}**",
|
f"- 滚仓组 ID:**{group_id}**",
|
||||||
f"- 方向:{_dir_text(cfg, direction)}",
|
f"- 方向:{_dir_text(cfg, direction)}",
|
||||||
f"- 结束原因:**{reason}**",
|
f"- 结束原因:**{reason}**",
|
||||||
f"- 已完成滚仓腿数:**{int(leg_count or 0)}**",
|
f"- 已完成滚仓腿数:**{int(leg_count or 0)}**",
|
||||||
]
|
]
|
||||||
_send(cfg, "\n".join(lines))
|
_send(cfg, "\n".join(lines))
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user