auto: 일일 백업 2026-07-22 02:00

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hyowons
2026-07-22 02:00:04 +09:00
parent 1d6fe0f70d
commit 2cf5b38da1
195 changed files with 11185 additions and 8580 deletions
@@ -66,6 +66,8 @@ def format_card(request: dict, market_data: dict, card_id: str,
type_marker = f' {cfg["warning_emoji"]} 시장가'
elif request['order_type'] == 'AGGRESSIVE_LIMIT':
type_marker = f' {cfg["warning_emoji"]} 공격적 지정가'
elif request['order_type'] == 'STOP_LIMIT':
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}', '']
@@ -83,6 +85,12 @@ def format_card(request: dict, market_data: dict, card_id: str,
ratio = (request['price'] - market_data['prev_close']) / market_data['prev_close'] * 100
price_part += f' (전일종가 {_pct(ratio)})'
lines.append(f'{marker} 가격: {price_part}')
elif request['order_type'] == 'STOP_LIMIT':
stop_p = request.get('stop_price') or 0
cur = market_data.get('current_price')
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'] == 'MARKET':
lines.append(f'{marker} 가격: {_md_bold("시장가")}')
else:
@@ -156,21 +156,37 @@ def collect_market_data(account_label: str, symbol: str, side: str, qty: int) ->
'stock_meta': stock_meta,
}
try:
open_orders = kc.get_open_orders(account_label)
except Exception:
open_orders = []
if side == 'BUY':
try:
bal = kc.get_balance(account_label)
md['balance_d2'] = _to_int(bal.get('d2_entra'))
d2 = _to_int(bal.get('d2_entra'))
# d2_entra는 미체결 매수 지정가로 묶인 예수금을 아직 포함 → 실제 가용액에서 차감.
locked = sum(_to_int(o.get('order_price')) * _to_int(o.get('unfilled_qty'))
for o in open_orders
if o.get('side') == 'BUY' and _to_int(o.get('order_price')) > 0)
md['balance_d2'] = max(0, d2 - locked)
except Exception:
md['balance_d2'] = 0
else:
try:
positions = kc.get_positions(account_label)
md['position_qty'] = next(
(_to_int(p.get('trde_able_qty') or p.get('qty') or p.get('hold_qty') or p.get('rmnd_qty'))
for p in positions
if (p.get('code') == symbol) or (p.get('symbol') == symbol) or (p.get('stk_cd') == symbol)),
0,
)
pos = next((p for p in positions
if (p.get('code') == symbol) or (p.get('symbol') == symbol) or (p.get('stk_cd') == symbol)),
None)
if pos:
hold = _to_int(pos.get('qty') or pos.get('hold_qty') or pos.get('rmnd_qty'))
trde_able = _to_int(pos.get('trde_able_qty') or hold)
pend_sell = sum(_to_int(o.get('unfilled_qty')) for o in open_orders
if o.get('side') == 'SELL' and o.get('code') == symbol)
# trde_able이 미체결 매도 제외 여부 불확실 → min으로 이중차감 방지 (check 핸들러와 동일).
md['position_qty'] = max(0, min(trde_able, hold - pend_sell))
else:
md['position_qty'] = 0
except Exception:
md['position_qty'] = 0
@@ -503,7 +503,7 @@ def validate_request(request: dict, market_data: dict) -> Result:
return r
if request['side'] not in ('BUY', 'SELL'):
return Result.REJECT('INVALID_SIDE', f'잘못된 방향: {request["side"]}')
if request['order_type'] not in ('LIMIT', 'MARKET', 'AGGRESSIVE_LIMIT'):
if request['order_type'] not in ('LIMIT', 'MARKET', 'AGGRESSIVE_LIMIT', 'STOP_LIMIT'):
return Result.REJECT('INVALID_ORDER_TYPE', f'잘못된 주문방식: {request["order_type"]}')
r = validate_trading_hours(market_data['now'], market_data.get('is_holiday', False),
market_data.get('nxt_eligible', False))
@@ -525,6 +525,27 @@ def validate_request(request: dict, market_data: dict) -> Result:
market_data['upper_limit'], market_data['lower_limit'])
if not r.ok:
return r
if request['order_type'] == 'STOP_LIMIT':
# 스톱지정가: 지정가(체결가)·조건단가(트리거) 둘 다 가격제한 이내 + 매도 전용.
# 보호매도(하락 시 매도)라 트리거는 현재가보다 낮아야 함 — 아니면 즉시 발동돼 스톱 의미 없음.
if request['side'] != 'SELL':
return Result.REJECT('STOP_SELL_ONLY', '스톱지정가는 현재 매도만 지원')
stop_price = request.get('stop_price')
if not stop_price or stop_price <= 0:
return Result.REJECT('STOP_PRICE_REQUIRED', '조건단가(트리거 가격)를 입력하세요')
r = validate_price_band(request['side'], request['price'],
market_data['upper_limit'], market_data['lower_limit'])
if not r.ok:
return r
r = validate_price_band(request['side'], stop_price,
market_data['upper_limit'], market_data['lower_limit'])
if not r.ok:
return r
cur = market_data.get('current_price') or 0
if cur and stop_price >= cur:
return Result.REJECT('STOP_TRIGGER_NOT_BELOW',
f'조건단가 {stop_price:,}원 ≥ 현재가 {cur:,}원 — '
f'하락 시 매도(스톱)는 현재가보다 낮게 설정해야 함')
if request['side'] == 'BUY':
basis = None
if request['order_type'] == 'MARKET':
@@ -164,16 +164,25 @@ def propose_trade(
price: Optional[int] = None,
routing_force: Optional[str] = None,
budget: Optional[int] = None,
stop_price: Optional[int] = None,
) -> dict:
sidecar.guard_or_raise()
_sweep_expired_and_notify()
if order_type not in ('LIMIT', 'MARKET', 'AGGRESSIVE_LIMIT'):
if order_type not in ('LIMIT', 'MARKET', 'AGGRESSIVE_LIMIT', 'STOP_LIMIT'):
return {'ok': False,
'message': card.format_rejected('INVALID_ORDER_TYPE', f'잘못된 주문방식: {order_type}')}
if side not in ('BUY', 'SELL'):
return {'ok': False,
'message': card.format_rejected('INVALID_SIDE', f'잘못된 방향: {side}')}
if order_type == 'STOP_LIMIT':
if side != 'SELL':
return {'ok': False,
'message': card.format_rejected('STOP_SELL_ONLY', '스톱지정가는 현재 매도만 지원')}
if not price or price <= 0 or not stop_price or stop_price <= 0:
return {'ok': False,
'message': card.format_rejected('STOP_INPUT',
'스톱지정가는 지정가와 조건단가(트리거)가 모두 필요')}
if budget is not None and qty is not None:
return {'ok': False,
'message': card.format_rejected('AMBIGUOUS_INPUT',
@@ -211,6 +220,9 @@ def propose_trade(
}
if order_type == 'LIMIT':
request['price'] = price
elif order_type == 'STOP_LIMIT':
request['price'] = price
request['stop_price'] = stop_price
r = guards.validate_request(request, md)
if not r.ok:
@@ -254,6 +266,8 @@ def propose_trade(
'account': account, 'side': side, 'symbol': symbol, 'symbol_name': symbol_name,
'qty': qty, 'price': final_price, 'order_type': order_type, 'routing_suffix': suffix,
}
if order_type == 'STOP_LIMIT':
payload['stop_price'] = stop_price
try:
pending = _pin_store.issue(account, payload)
@@ -450,6 +464,7 @@ def submit_with_pin(pin_input: str, dry_run: bool = True) -> dict:
routing_suffix=p['routing_suffix'],
dry_run=dry_run,
card_id=card_obj.card_id,
stop_price=p.get('stop_price'),
)
if res['ok']:
@@ -31,6 +31,7 @@ TR_CANCEL = 'kt10003'
# 매매구분 코드
TRDE_TP_LIMIT = '0' # 보통가 (지정가)
TRDE_TP_MARKET = '3' # 시장가
TRDE_TP_STOP = '28' # 스톱지정가 — cond_uv(조건단가) 도달 시 ord_uv(지정가)로 발주
# 거래소 코드 (라우팅 suffix → API 거래소 구분)
_EXCHANGE_BY_SUFFIX = {
@@ -52,27 +53,38 @@ def _exchange_for(suffix: str) -> str:
def submit(account_label: str, side: str, symbol: str, qty: int,
price: Optional[int], order_type: str, routing_suffix: str,
dry_run: bool = True, card_id: Optional[str] = None) -> dict:
dry_run: bool = True, card_id: Optional[str] = None,
stop_price: Optional[int] = None) -> dict:
sidecar.guard_or_raise()
if side not in ('BUY', 'SELL'):
raise ValueError(f'invalid side: {side}')
if order_type not in ('LIMIT', 'MARKET', 'AGGRESSIVE_LIMIT'):
if order_type not in ('LIMIT', 'MARKET', 'AGGRESSIVE_LIMIT', 'STOP_LIMIT'):
raise ValueError(f'invalid order_type: {order_type}')
if order_type in ('LIMIT', 'AGGRESSIVE_LIMIT') and not price:
raise ValueError(f'{order_type} requires price')
if order_type == 'STOP_LIMIT' and (not price or not stop_price):
raise ValueError('STOP_LIMIT requires price(지정가) and stop_price(조건단가)')
tr_id = TR_BUY if side == 'BUY' else TR_SELL
exchange = _exchange_for(routing_suffix)
is_market = order_type == 'MARKET'
is_stop = order_type == 'STOP_LIMIT'
if is_market:
trde_tp = TRDE_TP_MARKET
elif is_stop:
trde_tp = TRDE_TP_STOP
else:
trde_tp = TRDE_TP_LIMIT
body = {
'dmst_stex_tp': exchange,
'stk_cd': symbol,
'ord_qty': str(qty),
'ord_uv': '' if is_market else str(price),
'trde_tp': TRDE_TP_MARKET if is_market else TRDE_TP_LIMIT,
'cond_uv': '',
'trde_tp': trde_tp,
'cond_uv': str(stop_price) if is_stop else '',
}
payload = {
@@ -82,6 +94,7 @@ def submit(account_label: str, side: str, symbol: str, qty: int,
'symbol': symbol,
'qty': qty,
'price': price,
'stop_price': stop_price if is_stop else None,
'order_type': order_type,
'routing_suffix': routing_suffix,
'exchange': exchange,