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:
57
src/coinhunter/services/portfolio_service.py
Normal file
57
src/coinhunter/services/portfolio_service.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user