auto: 일일 백업 2026-08-01 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,11 @@ def _pct(p: float, digits: int = 2) -> str:
|
||||
return f'{sign}{p:.{digits}f}%'
|
||||
|
||||
|
||||
def _num(p) -> str:
|
||||
"""계단 비율 표기 — 10.0 은 '10', 10.5 는 '10.5' 로. 불필요한 .0 을 지운다."""
|
||||
return f'{float(p or 0):g}'
|
||||
|
||||
|
||||
def format_card(request: dict, market_data: dict, card_id: str,
|
||||
estimate: Optional[dict] = None,
|
||||
budget_conversion: Optional[dict] = None,
|
||||
@@ -94,25 +99,34 @@ def format_card(request: dict, market_data: dict, card_id: str,
|
||||
lines.append(f'{marker} 조건: {_md_bold(_money(stop_p) + " 도달 시")}{cur_part}')
|
||||
lines.append(f'{marker} 매도가격: {_md_bold(_money(request["price"]))} (지정가)')
|
||||
elif request['order_type'] == 'TRAILING_STOP':
|
||||
# 승인 대상은 "트레일 폭" — 조건단가는 고점이 오르면 이 폭을 유지하며 따라 올라간다.
|
||||
stop_p = request.get('stop_price') or 0
|
||||
# 승인 대상은 "계단 정의" — 각 계단의 조건단가는 고점이 오르면 제 하락률을 유지하며
|
||||
# 함께 따라 올라간다. 계단이 1개면 종전의 단일 트레일링과 같다.
|
||||
cur = market_data.get('current_price')
|
||||
cur_part = f' (현재가 {_money(cur)})' if cur else ''
|
||||
# 초기 고점은 항상 등록 시점 현재가 — 과거 고점 기준 옵션은 2026-07-30 제거됨.
|
||||
peak_v = request.get('trail_peak') or 0
|
||||
trail_pct_part = f'고점 대비 −{request.get("trail_pct")}%'
|
||||
lines.append(f'{marker} 시작 고점: {_md_bold(_money(peak_v))} (등록 시점)')
|
||||
lines.append(f'{marker} 트레일 폭: {_md_bold(trail_pct_part)}')
|
||||
steps = request.get('trail_steps') or []
|
||||
lines.append(f'{marker} 시작 고점: {_md_bold(_money(peak_v))} (등록 시점){cur_part}')
|
||||
min_sell = request.get('min_sell_price')
|
||||
if min_sell:
|
||||
# 최저 매도가가 손절선을 결정했는지(=트레일보다 높았는지) 명시 — 둘 중 어느 게
|
||||
# 지배했는지 안 보이면 관리자님이 값을 잘못 읽는다.
|
||||
raw_trail = int(peak_v * (1 - float(request['trail_pct']) / 100)) if peak_v else 0
|
||||
who = '최저가 적용' if stop_p >= raw_trail else '트레일 적용'
|
||||
tied = sum(1 for s in steps if s.get('floor_applied'))
|
||||
who = f'{tied}개 계단 적용' if tied else '미적용 (트레일이 더 높음)'
|
||||
lines.append(f'{marker} 최저 매도가: {_md_bold(_money(min_sell))} ({who})')
|
||||
lines.append(f'{marker} 현재 손절선: {_md_bold(_money(stop_p) + " 도달 시")}{cur_part}')
|
||||
lines.append(f'{marker} 매도가격: {_md_bold(_money(request["price"]))} (지정가)')
|
||||
lines.append(f'{marker} 고점이 오르면 손절선도 따라 올라갑니다 (내려가지 않음)')
|
||||
lines.append('')
|
||||
lines.append(f'{marker} 매도 계단 {len(steps)}단')
|
||||
for s in steps:
|
||||
cum = s.get('cum', 0)
|
||||
cum_part = '전량' if cum >= 100 else f'누적 {_num(cum)}%'
|
||||
lines.append(f' {s["n"]}단계 −{_num(s["pct"])}% '
|
||||
f'{_md_bold(_money(s["cond_uv"]) + " 도달")} → '
|
||||
f'{s["qty"]:,}주 ({cum_part})')
|
||||
rest = request['qty'] - sum(s['qty'] for s in steps)
|
||||
if rest > 0:
|
||||
lines.append(f' ↳ 나머지 {rest:,}주는 스톱 없음 (계속 보유)')
|
||||
lines.append('')
|
||||
lines.append(f'{marker} 고점이 오르면 계단이 통째로 따라 올라갑니다 (내려가지 않음)')
|
||||
elif request['order_type'] == 'MARKET':
|
||||
lines.append(f'{marker} 가격: {_md_bold("시장가")}')
|
||||
else:
|
||||
@@ -245,6 +259,40 @@ def format_submitted(card_id: str, side: str, symbol_name: str, qty: int,
|
||||
return f'📨 [#{card_id}] {side_word} 접수: {symbol_name} {qty:,}주 @ {price_str}\n주문번호: {ord_no}'
|
||||
|
||||
|
||||
def format_trailing_submitted(info: dict) -> str:
|
||||
"""트레일링 계단 접수 결과 — 몇 단이 걸렸고 감시가 붙었는지를 한 화면에 담는다.
|
||||
|
||||
계단 일부만 접수될 수 있어(레그 독립 제출) 성공·실패를 나란히 적는다.
|
||||
실패분을 요약해 버리면 방어선이 몇 개인지 모르는 채로 남게 된다.
|
||||
"""
|
||||
placed = info.get('placed') or []
|
||||
failed = info.get('failed') or []
|
||||
acct = _account_display(info.get('account', ''))
|
||||
head = '🧪 [모의] 트레일링' if info.get('dry_run') else '✅ 트레일링'
|
||||
lines = [f'{head} {len(placed)}단 등록: {info["symbol_name"]}',
|
||||
f'[{acct}] 총 {info["total_qty"]:,}주 · 시작 고점 {_money(info["peak"])}']
|
||||
if info.get('min_sell_price'):
|
||||
lines.append(f'최저 매도가 {_money(info["min_sell_price"])}')
|
||||
for s in placed:
|
||||
cum = s.get('cum', 0)
|
||||
cum_part = '전량' if cum >= 100 else f'누적 {_num(cum)}%'
|
||||
no = f' · 주문 {s["ord_no"]}' if s.get('ord_no') else ''
|
||||
lines.append(f'{s["n"]}단계 −{_num(s["pct"])}% {_money(s["cond_uv"])} 도달 → '
|
||||
f'{s["qty"]:,}주 ({cum_part}){no}')
|
||||
rest = info['total_qty'] - sum(s['qty'] for s in placed)
|
||||
if rest > 0:
|
||||
lines.append(f'↳ 나머지 {rest:,}주는 스톱 없음 (계속 보유)')
|
||||
for s in failed:
|
||||
lines.append(f'⚠️ {s["n"]}단계 −{_num(s["pct"])}% {s["qty"]:,}주 접수 실패 '
|
||||
f'({s.get("reason")}) {s.get("error", "")}'.rstrip())
|
||||
if info.get('register_error'):
|
||||
lines.append('⚠️ 주문은 접수됐지만 트레일링 등록 실패 — 손절선이 따라 올라가지 않습니다. '
|
||||
f'({info["register_error"]})')
|
||||
elif info.get('res_id'):
|
||||
lines.append(f'감시 시작 ({info["res_id"]}) — 고점이 오르면 계단이 함께 올라갑니다')
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def format_expired(card_id: str, side: str) -> str:
|
||||
side_word = '매수' if side == 'BUY' else '매도'
|
||||
return f'⏱️ [#{card_id}] 승인 만료. {side_word}가 취소되었습니다.'
|
||||
@@ -323,7 +371,9 @@ def format_card_locked(active: Optional[dict] = None) -> str:
|
||||
elif order_type == 'AGGRESSIVE_LIMIT':
|
||||
price_str = f'공격적 지정가 {_money(price)}' if price else '공격적 지정가'
|
||||
elif order_type == 'TRAILING_STOP':
|
||||
price_str = f'트레일링 −{active.get("trail_pct")}% (손절 {_money(active.get("stop_price"))})'
|
||||
steps = active.get('trail_steps') or []
|
||||
pcts = '/'.join(f'−{_num(s["pct"])}%' for s in steps)
|
||||
price_str = f'트레일링 {len(steps)}단 {pcts} (1단계 손절 {_money(active.get("stop_price"))})'
|
||||
else:
|
||||
price_str = _money(price) if price is not None else '지정가'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user