feat(cli): enter --forced-close-at 시간 기반 자동청산 옵션 (issue #17, BAT-024)

trex-core enter 에 --forced-close-at <ISO8601>(절대시간) 옵션 추가.
지정 시 buy intent payload 의 기존 forced_close_at 에 매핑(계약·도메인·
forced-close 루프 REQ-037 불변, CLI 노출 갭만 보완). 잘못된 형식은 제출
전 거부, 미지정 시 미포함(하위호환). 상대시간 옵션은 미채택.

REQ-032 change-req(CHG-20260609-01/02) → BAT-024(batch-lite) →
Implemented. 단위 353 passed, ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 13:10:21 +09:00
co-authored by Claude Opus 4.8
parent fec9411d33
commit 1cbbb89091
10 changed files with 275 additions and 5 deletions
+20 -1
View File
@@ -179,8 +179,19 @@ def _intent_guard(args, cfg, summary: dict, action: str, confirm) -> int | None:
def _cmd_enter(args, cfg, rpc, confirm) -> int:
# REQ-032 AC-6: validate the absolute auto-close timestamp BEFORE submitting,
# so a malformed value is rejected at the CLI instead of failing admission.
forced_close_at = getattr(args, "forced_close_at", None)
if forced_close_at is not None:
try:
forced_close_at = datetime.fromisoformat(forced_close_at).isoformat()
except ValueError:
_fail(f"invalid --forced-close-at (expected ISO8601): {args.forced_close_at!r}",
args.json)
return 1
summary = {"portfolio_id": args.portfolio_id, "qty": args.qty,
"allocated_capital": args.capital, "entry_price": args.entry_price}
"allocated_capital": args.capital, "entry_price": args.entry_price,
"forced_close_at": forced_close_at}
short = _intent_guard(args, cfg, summary, "enter(buy)", confirm)
if short is not None:
return short
@@ -192,6 +203,9 @@ def _cmd_enter(args, cfg, rpc, confirm) -> int:
"sell_strategy_id": args.sell_strategy_id,
"stop_loss_price": args.stop_loss, "take_profit_price": args.take_profit,
}
# REQ-032 AC-5/6: only carry forced_close_at when specified (backward compat).
if forced_close_at is not None:
payload["forced_close_at"] = forced_close_at
resp = rpc(topology.intent_queue(_resolve_instance(args)), payload,
correlation_id=payload["intent_id"], timeout=args.timeout)
return _report_intent(resp, args.json)
@@ -274,6 +288,11 @@ def _build_parser() -> argparse.ArgumentParser:
p_enter.add_argument("--strategy-id", dest="strategy_id", default=None)
p_enter.add_argument("--stop-loss", dest="stop_loss", type=float, default=None)
p_enter.add_argument("--take-profit", dest="take_profit", type=float, default=None)
p_enter.add_argument(
"--forced-close-at", dest="forced_close_at", default=None,
help="absolute auto-close timestamp (ISO8601, e.g. 2026-06-09T15:20:00+09:00) "
"→ buy intent forced_close_at",
)
_add_intent_guards(p_enter)
_add_common(p_enter)