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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hyowons
2026-08-26 02:00:02 +09:00
parent 600be2ee96
commit 992b85005d
162 changed files with 3704 additions and 1825 deletions
+69 -3
View File
@@ -75,6 +75,8 @@ def format_card(request: dict, market_data: dict, card_id: str,
type_marker = f' {cfg["warning_emoji"]} 스톱지정가(예약)'
elif request['order_type'] == 'TRAILING_STOP':
type_marker = f' {cfg["warning_emoji"]} 트레일링 스톱(예약)'
elif request['order_type'] == 'TRAILING_ARM':
type_marker = f' {cfg["warning_emoji"]} 예약 트레일링(발동 대기)'
amend_marker = ' ✏️ 수정됨' if amended else ''
lines = [f'{side_emoji} {_md_bold(side_word + " 미리보기")} [#{card_id}]{type_marker}{spouse_marker}{amend_marker}', '']
@@ -98,15 +100,29 @@ def format_card(request: dict, market_data: dict, card_id: str,
cur_part = f' (현재가 {_money(cur)})' if cur else ''
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':
elif request['order_type'] in ('TRAILING_STOP', 'TRAILING_ARM'):
# 승인 대상은 "계단 정의" — 각 계단의 조건단가는 고점이 오르면 제 하락률을 유지하며
# 함께 따라 올라간다. 계단이 1개면 종전의 단일 트레일링과 같다.
is_arm = request['order_type'] == 'TRAILING_ARM'
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
steps = request.get('trail_steps') or []
lines.append(f'{marker} 시작 고점: {_md_bold(_money(peak_v))} (등록 시점){cur_part}')
if is_arm:
gap = ''
if cur:
gap = f' — 발동까지 {_pct((peak_v - cur) / cur * 100)}'
lines.append(f'{marker} 발동가: {_md_bold(_money(peak_v) + " 도달 시 시작")}{cur_part}{gap}')
# 대기 예약이 당일 유효로 바뀐 뒤로는 상한가 초과 발동가가 '오늘 도달 불가' 확정이다.
# 거부하지는 않는다(관리자님 지시) — 무의미하다는 사실만 승인 전에 알린다.
up_lim = market_data.get('upper_limit') or 0
if up_lim and peak_v > up_lim:
lines.append(f'{cfg["warning_emoji"]} 오늘 상한가 {_money(up_lim)} 초과 — '
f'오늘은 도달 불가, 장 마감에 만료됩니다')
else:
lines.append(f'{marker} 시작 고점: {_md_bold(_money(peak_v))} (등록 시점){cur_part}')
min_sell = request.get('min_sell_price')
if min_sell:
# 최저 매도가가 손절선을 결정했는지(=트레일보다 높았는지) 명시 — 둘 중 어느 게
@@ -115,7 +131,10 @@ def format_card(request: dict, market_data: dict, card_id: str,
who = f'{tied}개 계단 적용' if tied else '미적용 (트레일이 더 높음)'
lines.append(f'{marker} 최저 매도가: {_md_bold(_money(min_sell))} ({who})')
lines.append('')
lines.append(f'{marker} 매도 계단 {len(steps)}단')
steps_head = f'{marker} 매도 계단 {len(steps)}단'
if is_arm:
steps_head += ' (발동가 기준 예상)'
lines.append(steps_head)
for s in steps:
cum = s.get('cum', 0)
cum_part = '전량' if cum >= 100 else f'누적 {_num(cum)}%'
@@ -127,6 +146,14 @@ def format_card(request: dict, market_data: dict, card_id: str,
lines.append(f' ↳ 나머지 {rest:,}주는 스톱 없음 (계속 보유)')
lines.append('')
lines.append(f'{marker} 고점이 오르면 계단이 통째로 따라 올라갑니다 (내려가지 않음)')
if is_arm:
# 이 기능의 유일한 위험(발동 시 PIN 재확인 없음)을 승인 화면에서 고지하는 자리다.
# 실제 손절선이 미리보기 이상인 이유: 발동 시점 현재가 ≥ 발동가.
lines.append(f'{marker} 지금은 주문이 나가지 않습니다 — '
f'발동가에 닿는 순간 PIN 재확인 없이 위 계단이 자동 발주됩니다')
lines.append(f'{marker} {_md_bold("오늘 장 마감까지만 유효")} — '
f'미발동이면 자동 취소됩니다 (스톱주문과 같은 당일 유효)')
lines.append(f'{marker} 실제 손절선은 발동 시점 현재가 기준이라 위 값 이상입니다')
elif request['order_type'] == 'MARKET':
lines.append(f'{marker} 가격: {_md_bold("시장가")}')
else:
@@ -293,6 +320,45 @@ def format_trailing_submitted(info: dict) -> str:
return '\n'.join(lines)
def format_arm_registered(info: dict) -> str:
"""예약 트레일링 등록 완료 — 아직 키움에 주문이 없다는 사실을 분명히 적는다."""
acct = _account_display(info.get('account', ''))
head = '🧪 [모의] 예약 트레일링' if info.get('dry_run') else '⏳ 예약 트레일링'
steps = info.get('steps') or []
lines = [f'{head} 등록: {info["symbol_name"]} ({info.get("symbol", "")})',
f'[{acct}] {info["qty"]:,}주 · 발동가 {_money(info["trigger_price"])} 도달 시 시작']
if info.get('cur_price'):
cur = info['cur_price']
gap = (info['trigger_price'] - cur) / cur * 100
lines.append(f'현재가 {_money(cur)} · 발동까지 {_pct(gap)}')
if info.get('min_sell_price'):
lines.append(f'최저 매도가 {_money(info["min_sell_price"])}')
step_parts = []
for s in steps:
cum = s.get('cum', 0)
cum_part = '전량' if cum >= 100 else f'누적 {_num(cum)}%'
step_parts.append(f'−{_num(s["pct"])}% {cum_part}')
lines.append(f'매도 계단 {len(steps)}단: ' + ' · '.join(step_parts))
if info.get('arm_id'):
lines.append(f'대기 시작 ({info["arm_id"]}) — **오늘 장 마감까지** 유효')
lines.append('지금은 키움에 주문이 없습니다. 발동가에 닿으면 PIN 재확인 없이 자동 발주됩니다.')
lines.append('미발동으로 장이 끝나면 자동 취소됩니다 (스톱주문과 같은 당일 유효).')
return '\n'.join(lines)
def format_arm_fired(info: dict) -> str:
"""예약 트레일링 발동 — 계단 접수 결과는 format_trailing_submitted 와 같은 모양이라 재사용하고,
발동 경위(발동가·실제 발동가·수량 축소)만 앞뒤에 덧붙인다."""
head = [f'🎯 예약 트레일링 발동 — {info["symbol_name"]} ({info.get("symbol", "")})',
f'발동가 {_money(info["trigger_price"])} 도달 (현재가 {_money(info["peak"])})']
if info.get('qty_reduced'):
head.append(f'⚠️ 매도가능 {info["total_qty"]:,}주 — '
f'예약 {info["arm_qty"]:,}주에서 줄여 재배분했습니다')
body = format_trailing_submitted(info)
tail = ['※ 스톱주문은 장 마감 시 소멸합니다 (자동 재등록 안 함)']
return '\n'.join(head + ['', body] + tail)
def format_expired(card_id: str, side: str) -> str:
side_word = '매수' if side == 'BUY' else '매도'
return f'⏱️ [#{card_id}] 승인 만료. {side_word}가 취소되었습니다.'