Files
Your Name 6d2a706a48 커밋 1 — 실매 가격 TTL 구멍 (본체)
왜: 체결이 없어도 마지막가는 유지인데, TTL로 None 만들고 매도/EOD를 건너뛰어 8/5 돌파·금요일 leftover가 남음. 호가필터 TTL 구멍과 같은 병.

넣을 파일

신규: kis_trader/engine/live_sell_price.py
kis_trader/strategies/base.py (_ws_last_quote, _resolve_sell_price)
전략: momentum.py scalping.py tail_catch.py breakout.py range_break.py dart_strategy.py updow_strategy.py updown_feed.py us_momentum.py
WS: ws_manager.py kis_ws.py kiwoom_ws.py ls_ws.py kis_ws_overseas.py
kis_trader/web/live_config_schema.py (WS_PRICE_MAX_AGE_SEC 기본 0)
database.py (키 주석 + legacy/ sys.path)
kis_trader/execution/order_manager.py (잔고 있는데 40240000 ghost_purge 금지 — 같은 EOD 사고)
EOD가 min_hold에 안 막히게 손본 momentum_hts_logic.py / scalping_engine.py / tail_engine.py (이 대화에서 손본 부분만 확인 후)
문서: docs/like_mcp.md/db_erd.md code_architecture.md (가격 TTL 문구)
메시지 초안

fix: 매수·매도 현재가를 TTL로 버리지 않음 (마지막 RAM)
횡보·체결 공백을 죽은 캐시로 오인해 None 처리하면 손절·EOD가 스킵된다.
호가필터와 같이 나이는 무시하고 마지막 체결가를 유지한다. EOD는 매수가 폴백.
영향: 실매 O / 백테·옵투나 봉 경로 거의 무관 (엔진 식 변경 아님)

커밋 2 — 루트 정리 (remove/ vs legacy/)
왜: 루트 단독봇·테스트는 지울 보관함으로. 웹·알람이 아직 쓰는 모듈은 remove에 두면 나중에 폴더째 삭제 때 깨짐.

넣을 파일

이동: 미사용 → remove/legacy_root/ (래퍼, ETF/키움 옛봇, 테스트, kiwoom_rest_api 등)
이동: 사용 중 → legacy/ (holding_bot kis_holding_ver1 news_analyzer kis_long_ver1/2)
신규: kis_trader/utils/legacy_root.py legacy/README.md remove/README.md
import 경로: backtest_web.py mm_butler.py mm_remote.py updow_holding_cfg.py dbband_stock_cfg.py param_search_updow*.py dbband_param_search.py param_search_apply_snapshot.py verify_three_paths.py
docs/like_mcp.md/code_architecture.md 수동 노트
메시지 초안

chore: 미사용 루트는 remove/, 웹·알람 구모듈은 legacy/
remove는 나중에 통째 삭제 예정. holding_bot·news_analyzer·kis_long은
ensure_legacy_root로 legacy/만 본다.
빼기: scratch/set_ws_price_max_age_zero.py (일회성)
2026-08-18 00:13:17 +09:00

325 lines
11 KiB
Python

import time
import json
import datetime
import pandas as pd
import numpy as np
import os
# =========================================================
# [Part 1] 데이터 표준화 (Data Class)
# 증권사마다 주는 데이터 이름이 다르므로, 우리 봇이 쓸 공통 이름으로 정의
# =========================================================
class StockData:
def __init__(self, code, name, price, open_p, high, low, close, volume):
self.code = code
self.name = name
self.current_price = price # 현재가
self.open = open_p # 시가
self.high = high # 고가
self.low = low # 저가
self.close = close # 종가
self.volume = volume # 거래량
class OrderbookData:
def __init__(self, total_bid, total_ask):
self.total_bid = total_bid # 총 매수 잔량 (살 사람)
self.total_ask = total_ask # 총 매도 잔량 (팔 사람)
# =========================================================
# [Part 2] 증권사 연결 인터페이스 (이 부분만 채우세요!)
# =========================================================
class BrokerAPI:
"""
[안내] 여기에 한국투자증권(KIS)이나 다른 API 코드를 연결합니다.
봇 로직은 이 함수들을 호출해서 데이터를 받아옵니다.
"""
def get_rank_list(self):
# [To-Do] 거래대금 상위 종목 리스트 반환 (API 호출)
# return [{"code": "005930", "price": 60000}, ...]
return [] # 테스트용 빈 리스트
def get_ohlcv_limit(self, code, timeframe='3m', limit=100):
# [To-Do] 특정 종목의 캔들(OHLCV) 데이터 반환 (RSI, MA 계산용)
# DataFrame 형태로 반환: columns=['open', 'high', 'low', 'close', 'volume']
return pd.DataFrame()
def get_daily_ohlc_yesterday(self, code):
# [To-Do] 어제 일봉 데이터 반환 (Dynamic K 계산용)
# return StockData(...)
pass
def get_current_data(self, code):
# [To-Do] 현재가 데이터 반환
pass
def get_orderbook(self, code):
# [To-Do] 호가창 데이터 반환
# return OrderbookData(bid, ask)
pass
def buy_market_order(self, code, qty):
# [To-Do] 시장가 매수 주문
print(f"✅ [API] {code} {qty}주 매수 주문 전송 완료")
def sell_market_order(self, code, qty):
# [To-Do] 시장가 매도 주문
print(f"✅ [API] {code} {qty}주 매도 주문 전송 완료")
# =========================================================
# [Part 3] 정훈님의 정글 서바이버 봇 (핵심 로직)
# =========================================================
class JungleSurvivorBot:
def __init__(self, broker_api, budget=100000):
self.api = broker_api # 증권사 API 객체 연결
self.budget = budget # 운용 자금 (10만원)
self.target_list = [] # 감시 종목 리스트
self.portfolio = {} # 보유 종목: {'code': {'buy_price': 1000, 'max_price': 1100, 'qty': 10}}
# --- [전략 파라미터] ---
self.rsi_period = 11 # RSI 기간 (남들 14보다 빠르게)
self.rsi_sell_std = 68 # RSI 매도 기준 (70보다 조금 낮게 선취매도)
self.trailing_gap = 0.01 # 트레일링 스탑 (고점 대비 1% 하락 시 매도)
self.target_profit = 0.035 # 목표 수익률 (3.5%면 무조건 익절)
self.stop_loss = -0.02 # 손절매 (-2%)
# -----------------------------------------------------
# [3-1] 계산기 (Indicators)
# -----------------------------------------------------
def calc_rsi(self, df):
"""RSI 지표 계산"""
delta = df['close'].diff()
gain = (delta.where(delta > 0, 0)).rolling(window=self.rsi_period).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=self.rsi_period).mean()
rs = gain / loss
return 100 - (100 / (1 + rs))
def calc_ma(self, df, period=20):
"""이동평균선 계산"""
return df['close'].rolling(window=period).mean()
def calc_dynamic_k(self, yesterday_data):
"""
변동성 돌파 K값 실시간 변경 (노이즈 비율 활용)
노이즈가 심하면 K값을 높여서 진입 장벽을 높임
"""
if yesterday_data is None: return 0.5 # 데이터 없으면 기본값
total_range = yesterday_data.high - yesterday_data.low
body_range = abs(yesterday_data.open - yesterday_data.close)
if total_range == 0: return 0.5
noise_ratio = 1 - (body_range / total_range)
# K값은 최소 0.3 ~ 최대 0.9 사이로 제한
return max(0.3, min(noise_ratio, 0.9))
# -----------------------------------------------------
# [3-2] 타겟 선정 (Dynamic Universe)
# -----------------------------------------------------
def update_universe(self):
print("\n🔄 [리스트 갱신] 실시간 주도주 스캔 중...")
raw_list = self.api.get_rank_list()
new_targets = []
for item in raw_list:
code = item['code']
price = item['price']
# [필터] 1. 가격 (2천원 ~ 5만원) / 10만원으로 살 수 있어야 함
if not (2000 <= price <= 50000) or (price > self.budget):
continue
# [필터] 2. 프로그램 매매 추이 (API 제공 시 추가)
# if item['prog_buy'] < 0: continue
new_targets.append(code)
# JSON 파일 저장 (로그용)
with open('target_universe.json', 'w') as f:
json.dump(new_targets, f)
self.target_list = new_targets
print(f"✅ 타겟 갱신 완료: {len(self.target_list)}개 종목 감시 시작")
# -----------------------------------------------------
# [3-3] 매수 판단 로직 (살까 말까?)
# -----------------------------------------------------
def check_buy(self, code):
# 1. 데이터 수집
df_candle = self.api.get_ohlcv_limit(code) # 3분봉
curr_data = self.api.get_current_data(code)
yesterday = self.api.get_daily_ohlc_yesterday(code)
orderbook = self.api.get_orderbook(code)
prog_net_buy = self.api.get_program_net_buy(code) # 프로그램 수급 확인
if prog_net_buy < 0:
print(f"🚫 {code} 패스: 프로그램 매도세 ({prog_net_buy})")
return False
if len(df_candle) < 20: return False # 데이터 부족
current_price = curr_data.current_price
# 2. 지표 계산
ma20 = self.calc_ma(df_candle, 20).iloc[-1]
k_val = self.calc_dynamic_k(yesterday)
# 3. 변동성 돌파 목표가 계산
# 목표가 = 오늘 시가 + (어제 변동폭 * K)
prev_range = yesterday.high - yesterday.low
breakout_price = curr_data.open + (prev_range * k_val)
print(f"🧐 {code} 분석: 현재가 {current_price} | 20선 {ma20:.0f} | 목표가 {breakout_price:.0f} (K:{k_val:.2f})")
# --- [매수 조건 (AND)] ---
# A. 추세: 현재가가 20일선 위에 있는가?
cond_trend = current_price >= ma20
# B. 변동성 돌파: 의미 있는 가격(목표가)을 넘었는가?
cond_breakout = current_price >= breakout_price
# C. 수급 안전판: 매수 잔량이 매도 잔량보다 2배 많은가?
cond_safe = orderbook.total_bid >= (orderbook.total_ask * 2)
if cond_trend and cond_breakout and cond_safe:
print(f"🚀 [매수 신호] {code} 발견! (Trend+Breakout+Safe)")
return True
return False
# -----------------------------------------------------
# [3-4] 매도 판단 로직 (팔까 말까?)
# -----------------------------------------------------
def check_sell(self, code):
if code not in self.portfolio: return False
info = self.portfolio[code]
buy_price = info['buy_price']
max_price = info['max_price'] # 트레일링 스탑용 고점
qty = info['qty']
curr_data = self.api.get_current_data(code)
curr_price = curr_data.current_price
# 고점 갱신 (트레일링 스탑용)
if curr_price > max_price:
self.portfolio[code]['max_price'] = curr_price
max_price = curr_price
# 지표 계산
df_candle = self.api.get_ohlcv_limit(code)
rsi = self.calc_rsi(df_candle).iloc[-1]
# 수익률 계산
profit_rate = (curr_price - buy_price) / buy_price
drop_from_high = (max_price - curr_price) / max_price
print(f"👀 {code} 보유중: 수익률 {profit_rate * 100:.2f}% | 고점대비하락 {drop_from_high * 100:.2f}% | RSI {rsi:.1f}")
# --- [매도 조건 (OR)] ---
# 1. [목표 달성] 3.5% 먹으면 묻지도 따지지도 말고 익절
if profit_rate >= self.target_profit:
print("💰 목표 수익 달성! 매도!")
return True
# 2. [트레일링 스탑] 고점 대비 1% 빠지면 익절/청산
if drop_from_high >= self.trailing_gap and profit_rate > 0:
print("📉 트레일링 스탑 발동! 매도!")
return True
# 3. [조기 퇴근] RSI가 68 넘으면 과열이므로 선취 매도
if rsi >= self.rsi_sell_std:
print("🔥 RSI 과열! 조기 매도!")
return True
# 4. [손절] -2% 살려주세요
if profit_rate <= self.stop_loss:
print("😭 손절매 실행...")
return True
return False
# -----------------------------------------------------
# [3-5] 메인 루프 (실행기)
# -----------------------------------------------------
def run(self):
print("🤖 정글 서바이버 봇 가동 시작!")
# [수정 1] 봇 켜자마자 일단 유니버스(감시 종목)부터 만들고 시작!
# 파일이 없거나 비어있을 수 있으니 강제 갱신 1회 실행
if not os.path.exists('target_universe.json'):
print("📂 타겟 파일이 없어서 새로 만듭니다...")
self.update_universe()
else:
# 파일이 있어도 비어있는지 확인
try:
with open('target_universe.json', 'r') as f:
json.load(f)
except json.JSONDecodeError:
print("📂 타겟 파일이 비어있어서 새로 채웁니다...")
self.update_universe()
while True:
# 1. 현재 시간 체크
now = datetime.datetime.now()
# 2. 유니버스 갱신 (30분마다)
if now.minute % 30 == 0 and now.second < 5:
self.update_universe()
time.sleep(5)
# 3. 매도 감시 (보유 종목 먼저 체크)
sell_list = []
for code in list(self.portfolio.keys()):
if self.check_sell(code):
# 매도 실행
qty = self.portfolio[code]['qty']
self.api.sell_market_order(code, qty)
sell_list.append(code)
# 포트폴리오에서 삭제
for code in sell_list:
del self.portfolio[code]
# 4. 매수 감시 (보유 종목 없을 때 or 자금 남을 때)
if len(self.portfolio) < 3:
# [수정 2] 읽을 때도 안전장치 추가
try:
with open('target_universe.json', 'r') as f:
targets = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
targets = [] # 에러 나면 그냥 빈 리스트로 처리
for code in targets:
if code in self.portfolio: continue # 이미 가진 건 패스
if self.check_buy(code):
# 매수 실행 (예: 10만원어치 계산)
curr_price = self.api.get_current_data(code).current_price
qty = int(self.budget / 3 / curr_price) # 자금의 1/3 투입
if qty > 0:
self.api.buy_market_order(code, qty)
# 포트폴리오 등록
self.portfolio[code] = {
'buy_price': curr_price,
'max_price': curr_price,
'qty': qty
}
break # 한 턴에 하나만 산다 (서버 부하 방지)
time.sleep(1) # 1초 대기
# =========================================================
# 실행부
# =========================================================
if __name__ == "__main__":
# 1. API 객체 생성 (나중에 키움/한투 코드로 교체될 부분)
my_broker = BrokerAPI()
# 2. 봇 생성 및 실행
bot = JungleSurvivorBot(my_broker, budget=100000)
bot.run()