refactor(smart_executor): split monolithic executor into clean service modules

- Extract 7 focused services from smart_executor.py:
  - trade_common: constants, timezone, logging, dry-run state
  - file_utils: file locking + atomic JSON helpers
  - smart_executor_parser: argparse + legacy argument compatibility
  - execution_state: decision deduplication (executions.json)
  - portfolio_service: positions.json + exchange reconciliation
  - exchange_service: ccxt wrapper, balances, order prep
  - trade_execution: buy/sell/rebalance/hold actions
- Turn smart_executor.py into a thin backward-compatible facade
- Fix critical dry-run bug: module-level DRY_RUN copy caused real orders
  in dry-run mode; replace with mutable dict + is_dry_run() function
- Fix dry-run polluting positions.json: skip save_positions() when dry-run
- Fix rebalance dry-run budget: use sell_order cost instead of real balance
- Add full legacy CLI compatibility for old --decision HOLD --dry-run style
This commit is contained in:
2026-04-15 18:39:08 +08:00
parent 893f0fb077
commit f69facde0c
9 changed files with 667 additions and 559 deletions

View File

@@ -0,0 +1,39 @@
"""Execution state helpers (decision deduplication, executions.json)."""
import hashlib
from ..runtime import get_runtime_paths
from .file_utils import load_json_locked, save_json_locked
from .trade_common import bj_now_iso
PATHS = get_runtime_paths()
EXECUTIONS_FILE = PATHS.executions_file
EXECUTIONS_LOCK = PATHS.executions_lock
def default_decision_id(action: str, argv_tail: list[str]) -> str:
from datetime import datetime
from .trade_common import CST
now = datetime.now(CST)
bucket_min = (now.minute // 15) * 15
bucket = now.strftime(f"%Y%m%dT%H{bucket_min:02d}")
raw = f"{bucket}|{action}|{'|'.join(argv_tail)}"
return hashlib.sha1(raw.encode()).hexdigest()[:16]
def load_executions() -> dict:
return load_json_locked(EXECUTIONS_FILE, EXECUTIONS_LOCK, {"executions": {}}).get("executions", {})
def save_executions(executions: dict):
save_json_locked(EXECUTIONS_FILE, EXECUTIONS_LOCK, {"executions": executions})
def record_execution_state(decision_id: str, payload: dict):
executions = load_executions()
executions[decision_id] = payload
save_executions(executions)
def get_execution_state(decision_id: str):
return load_executions().get(decision_id)