auto: 일일 백업 2026-08-01 02:00

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hyowons
2026-08-01 02:00:02 +09:00
parent d7042430c3
commit 30be97ab2f
177 changed files with 4124 additions and 3489 deletions
+61 -11
View File
@@ -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 '지정가'