- 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
58 lines
1.9 KiB
Python
58 lines
1.9 KiB
Python
"""Portfolio state helpers (positions.json, reconcile with exchange)."""
|
|
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()
|
|
POSITIONS_FILE = PATHS.positions_file
|
|
POSITIONS_LOCK = PATHS.positions_lock
|
|
|
|
|
|
def load_positions() -> list:
|
|
return load_json_locked(POSITIONS_FILE, POSITIONS_LOCK, {"positions": []}).get("positions", [])
|
|
|
|
|
|
def save_positions(positions: list):
|
|
save_json_locked(POSITIONS_FILE, POSITIONS_LOCK, {"positions": positions})
|
|
|
|
|
|
def upsert_position(positions: list, position: dict):
|
|
sym = position["symbol"]
|
|
for i, existing in enumerate(positions):
|
|
if existing.get("symbol") == sym:
|
|
positions[i] = position
|
|
return positions
|
|
positions.append(position)
|
|
return positions
|
|
|
|
|
|
def reconcile_positions_with_exchange(ex, positions: list):
|
|
from .exchange_service import fetch_balances
|
|
|
|
balances = fetch_balances(ex)
|
|
existing_by_symbol = {p.get("symbol"): p for p in positions}
|
|
reconciled = []
|
|
for asset, qty in balances.items():
|
|
if asset == "USDT":
|
|
continue
|
|
if qty <= 0:
|
|
continue
|
|
sym = f"{asset}USDT"
|
|
old = existing_by_symbol.get(sym, {})
|
|
reconciled.append(
|
|
{
|
|
"account_id": old.get("account_id", "binance-main"),
|
|
"symbol": sym,
|
|
"base_asset": asset,
|
|
"quote_asset": "USDT",
|
|
"market_type": "spot",
|
|
"quantity": qty,
|
|
"avg_cost": old.get("avg_cost"),
|
|
"opened_at": old.get("opened_at", bj_now_iso()),
|
|
"updated_at": bj_now_iso(),
|
|
"note": old.get("note", "Reconciled from Binance balances"),
|
|
}
|
|
)
|
|
save_positions(reconciled)
|
|
return reconciled, balances
|