Open API 적용예제
트레이딩 전략 샘플코드 목록
널리 쓰이는 기술적 지표 5가지를 Open API 일봉 시세로 계산하는 샘플코드입니다.
- 이 예제는 투자권유, 투자자문, 매매 추천이 아니며 특정 종목의 매매를 권유하지 않습니다. 계산 결과는 투자 판단의 근거로 드리는 것이 아닙니다. 결과를 주문에 연결하려면 그 전에 데이터, 종목, 수량, 가격을 직접 확인하세요. 투자 판단과 그 결과는 이용자 본인의 책임입니다.
- 기간이나 기준선 같은 파라미터는 흔히 쓰는 기본값일 뿐 최적값이 아닙니다. 과거 수치가 앞으로의 성과를 보장하지도 않습니다.
- 아래 코드는 01. API 샘플코드 탭의 '01. 공통부 코드'(get_token, call)가 먼저 정의되어 있어야 합니다. 그다음 아래 '공통 코드'를 실행하고 나서 각 지표 예제를 실행하세요.
공통 코드: 일봉 로더, 이동평균, 교차 판정 (모든 지표 예제에서 같이 씁니다)
# 전제: API 샘플코드 > 01. 공통부 코드(BASE_URL, get_token, call 등)가 먼저 정의되어 있어야 합니다.
NOTICE = ("[고지] 이 출력은 공개된 표준 기술적 지표의 계산 결과입니다. "
"투자권유, 투자자문, 매매 추천이 아니며, 특정 종목의 매매를 권유하지 않습니다. "
"과거 수치는 미래 성과를 시사하지 않습니다. "
"투자 판단과 그 결과는 이용자 책임입니다.")
def load_candles(iscd, mkt="J"):
"""일봉을 조회해(GET /market/v1/candles/days) 과거에서 현재 순으로 정렬한 캔들 리스트를 돌려줍니다.
한 번에 약 40봉만 오며, 장 마감 후에 조회하면 당일 봉도 포함됩니다."""
res = call("GET", "/market/v1/candles/days", {"mrkt_div_code": mkt, "iscd": iscd})
rows = res["data"] if isinstance(res["data"], list) else [res["data"]]
candles = [{"date": r["date"], "open": float(r["oprc"]), "high": float(r["hprc"]),
"low": float(r["lprc"]), "close": float(r["stck_clpr"]), "volume": float(r["acml_vol"])}
for r in rows if r.get("date")]
candles.sort(key=lambda c: c["date"]) # 응답은 최근 일자부터 오므로 과거에서 현재 순으로 정렬
return candles
def sma(values, period):
"""단순이동평균. 계산할 수 없는 앞 구간은 None"""
out = [None] * len(values)
for i in range(period - 1, len(values)):
out[i] = sum(values[i - period + 1:i + 1]) / period
return out
def ema(values, period):
"""지수이동평균. 첫 period 구간의 단순평균에서 시작"""
out = [None] * len(values)
if len(values) < period:
return out
k = 2.0 / (period + 1)
prev = out[period - 1] = sum(values[:period]) / period
for i in range(period, len(values)):
prev = out[i] = values[i] * k + prev * (1 - k)
return out
def crossed_up(a, b):
"""직전 봉에서 a <= b 였다가 현재 봉에서 a > b 가 되었는지 (상향 교차)"""
if len(a) < 2 or None in (a[-2], a[-1], b[-2], b[-1]):
return False
return a[-2] <= b[-2] and a[-1] > b[-1]
def crossed_down(a, b):
"""직전 봉에서 a >= b 였다가 현재 봉에서 a < b 가 되었는지 (하향 교차)"""
if len(a) < 2 or None in (a[-2], a[-1], b[-2], b[-1]):
return False
return a[-2] >= b[-2] and a[-1] < b[-1]
-
01. RSI 과매수, 과매도 판정
RSI(14, Wilder 평활)가 30 미만이면 OVERSOLD(과매도), 70을 넘으면 OVERBOUGHT(과매수) 구간으로 봅니다. 지금 RSI 가 어느 구간에 있는지만 계산합니다.
def rsi(values, period=14): """RSI (Wilder 평활). period+1 개 이상의 종가가 필요하며 계산할 수 없는 구간은 None""" out = [None] * len(values) if len(values) <= period: return out gains = losses = 0.0 for i in range(1, period + 1): change = values[i] - values[i - 1] gains += max(change, 0.0) losses += max(-change, 0.0) avg_gain, avg_loss = gains / period, losses / period out[period] = 100.0 if avg_loss == 0 else 100 - 100 / (1 + avg_gain / avg_loss) for i in range(period + 1, len(values)): change = values[i] - values[i - 1] avg_gain = (avg_gain * (period - 1) + max(change, 0.0)) / period avg_loss = (avg_loss * (period - 1) + max(-change, 0.0)) / period out[i] = 100.0 if avg_loss == 0 else 100 - 100 / (1 + avg_gain / avg_loss) return out def evaluate_rsi(candles, period=14, low=30.0, high=70.0): closes = [c["close"] for c in candles] latest = rsi(closes, period)[-1] if closes else None if latest is None: return {"state": "NO_SIGNAL", "reason": f"데이터 부족(RSI {period} 계산 불가)", "detail": {}} if latest < low: state, reason = "OVERSOLD", f"하단 이탈 (RSI {latest:.1f} < {low:.0f})" elif latest > high: state, reason = "OVERBOUGHT", f"상단 이탈 (RSI {latest:.1f} > {high:.0f})" else: state, reason = "NEUTRAL", f"중립 구간 (RSI {latest:.1f})" return {"state": state, "reason": reason, "detail": {"rsi": round(latest, 1), "close": closes[-1]}} print(NOTICE) candles = load_candles("005930") r = evaluate_rsi(candles) print(f"[RSI] {candles[-1]['date']} 기준 ({len(candles)}봉)") print(f" 상태: {r['state']}") print(f" 근거: {r['reason']}") print(f" 상세: {r['detail']}")결과값 예시: 삼성전자(005930) 일봉 40봉으로 실행했을 때의 출력입니다. 숫자는 임의의 값입니다.
[고지] 이 출력은 공개된 표준 기술적 지표의 계산 결과입니다. 투자권유, 투자자문, 매매 추천이 아니며, 특정 종목의 매매를 권유하지 않습니다. 과거 수치는 미래 성과를 시사하지 않습니다. 투자 판단과 그 결과는 이용자 책임입니다. [RSI] 20260923 기준 (40봉) 상태: NEUTRAL 근거: 중립 구간 (RSI 55.0) 상세: {'rsi': 55.0, 'close': 100000.0} -
02. 이동평균 골든크로스, 데드크로스
5일 이동평균이 20일 이동평균을 위로 뚫고 올라가면 CROSS_UP(골든크로스), 아래로 뚫고 내려가면 CROSS_DOWN(데드크로스)입니다. 교차는 그 봉에서만 잡히고, 교차가 없는 날은 MIXED 로 단기선이 장기선 위인지 아래인지만 알려 줍니다.
def evaluate_golden_cross(candles, short=5, long=20): closes = [c["close"] for c in candles] if len(closes) < long + 1: return {"state": "NO_SIGNAL", "reason": f"데이터 부족(장기 {long}일 미만)", "detail": {}} s, l = sma(closes, short), sma(closes, long) if crossed_up(s, l): state, reason = "CROSS_UP", f"{short}일선이 {long}일선을 상향 교차 (골든크로스)" elif crossed_down(s, l): state, reason = "CROSS_DOWN", f"{short}일선이 {long}일선을 하향 교차 (데드크로스)" else: above = s[-1] > l[-1] state, reason = "MIXED", f"교차 없음 (단기선이 장기선 {'위' if above else '아래'})" return {"state": state, "reason": reason, "detail": {f"sma{short}": round(s[-1], 1), f"sma{long}": round(l[-1], 1), "close": closes[-1]}} print(NOTICE) candles = load_candles("005930") r = evaluate_golden_cross(candles) print(f"[골든크로스] {candles[-1]['date']} 기준 ({len(candles)}봉)") print(f" 상태: {r['state']}") print(f" 근거: {r['reason']}") print(f" 상세: {r['detail']}")결과값 예시: 삼성전자(005930) 일봉 40봉으로 실행했을 때의 출력입니다. 숫자는 임의의 값입니다.
[고지] 이 출력은 공개된 표준 기술적 지표의 계산 결과입니다. 투자권유, 투자자문, 매매 추천이 아니며, 특정 종목의 매매를 권유하지 않습니다. 과거 수치는 미래 성과를 시사하지 않습니다. 투자 판단과 그 결과는 이용자 책임입니다. [골든크로스] 20260923 기준 (40봉) 상태: MIXED 근거: 교차 없음 (단기선이 장기선 위) 상세: {'sma5': 99000.0, 'sma20': 97000.0, 'close': 100000.0} -
03. 볼린저밴드 이탈 판정
중심선은 20일 단순이동평균이고, 상단과 하단은 중심선에서 표준편차(모집단)의 2배만큼 위아래로 떨어진 선입니다. 종가가 상단보다 높으면 ABOVE_UPPER, 하단보다 낮으면 BELOW_LOWER, 그 사이면 INSIDE 입니다.
def bollinger(values, period=20, num_std=2.0): """볼린저밴드. (중심선, 상단, 하단)을 돌려주며, 계산할 수 없는 앞 구간은 None""" mid = sma(values, period) upper, lower = [None] * len(values), [None] * len(values) for i in range(period - 1, len(values)): window = values[i - period + 1:i + 1] sd = (sum((x - mid[i]) ** 2 for x in window) / period) ** 0.5 upper[i], lower[i] = mid[i] + num_std * sd, mid[i] - num_std * sd return mid, upper, lower def evaluate_bollinger(candles, period=20, num_std=2.0): closes = [c["close"] for c in candles] if len(closes) < period: return {"state": "NO_SIGNAL", "reason": f"데이터 부족(밴드 {period}일 미만)", "detail": {}} mid, upper, lower = bollinger(closes, period, num_std) close, up, lo = closes[-1], upper[-1], lower[-1] if close < lo: state, reason = "BELOW_LOWER", "하단밴드 이탈" elif close > up: state, reason = "ABOVE_UPPER", "상단밴드 이탈" else: state, reason = "INSIDE", "밴드 내부" reason += f" (종가 {close:,.0f}, 밴드 {lo:,.0f}~{up:,.0f})" return {"state": state, "reason": reason, "detail": {"close": close, "upper": round(up, 1), "lower": round(lo, 1), "mid": round(mid[-1], 1)}} print(NOTICE) candles = load_candles("005930") r = evaluate_bollinger(candles) print(f"[볼린저밴드] {candles[-1]['date']} 기준 ({len(candles)}봉)") print(f" 상태: {r['state']}") print(f" 근거: {r['reason']}") print(f" 상세: {r['detail']}")결과값 예시: 삼성전자(005930) 일봉 40봉으로 실행했을 때의 출력입니다. 숫자는 임의의 값입니다.
[고지] 이 출력은 공개된 표준 기술적 지표의 계산 결과입니다. 투자권유, 투자자문, 매매 추천이 아니며, 특정 종목의 매매를 권유하지 않습니다. 과거 수치는 미래 성과를 시사하지 않습니다. 투자 판단과 그 결과는 이용자 책임입니다. [볼린저밴드] 20260923 기준 (40봉) 상태: ABOVE_UPPER 근거: 상단밴드 이탈 (종가 100,000, 밴드 94,500~99,500) 상세: {'close': 100000.0, 'upper': 99500.0, 'lower': 94500.0, 'mid': 97000.0} -
04. MACD 시그널 교차
MACD(12, 26, 9)를 씁니다. MACD선은 EMA12 에서 EMA26 을 뺀 값이고, 시그널선은 MACD선의 EMA9 입니다. MACD선이 시그널선을 위로 뚫으면 CROSS_UP, 아래로 뚫으면 CROSS_DOWN 이고, 교차가 없으면 히스토그램(MACD선에서 시그널선을 뺀 값)이 양수인지 음수인지만 알려 줍니다.
def macd(values, fast=12, slow=26, signal=9): """MACD. (MACD선, 시그널선, 히스토그램)을 돌려주며, 계산할 수 없는 앞 구간은 None""" ema_fast, ema_slow = ema(values, fast), ema(values, slow) line = [None if a is None or b is None else a - b for a, b in zip(ema_fast, ema_slow)] start = next((i for i, v in enumerate(line) if v is not None), None) sig = [None] * len(values) if start is not None and len(values) - start >= signal: for j, v in enumerate(ema(line[start:], signal)): sig[start + j] = v hist = [None if a is None or b is None else a - b for a, b in zip(line, sig)] return line, sig, hist def evaluate_macd(candles, fast=12, slow=26, signal=9): closes = [c["close"] for c in candles] if len(closes) < slow + signal: return {"state": "NO_SIGNAL", "reason": "데이터 부족(MACD 계산 불가)", "detail": {}} line, sig, hist = macd(closes, fast, slow, signal) if crossed_up(line, sig): state, reason = "CROSS_UP", "MACD선이 시그널선을 상향 교차" elif crossed_down(line, sig): state, reason = "CROSS_DOWN", "MACD선이 시그널선을 하향 교차" else: state, reason = "MIXED", f"교차 없음 (히스토그램 {'양수' if (hist[-1] or 0) > 0 else '음수'})" return {"state": state, "reason": reason, "detail": {"macd": round(line[-1], 2), "signal": round(sig[-1], 2), "hist": round(hist[-1], 2)}} print(NOTICE) candles = load_candles("005930") r = evaluate_macd(candles) print(f"[MACD] {candles[-1]['date']} 기준 ({len(candles)}봉)") print(f" 상태: {r['state']}") print(f" 근거: {r['reason']}") print(f" 상세: {r['detail']}")결과값 예시: 삼성전자(005930) 일봉 40봉으로 실행했을 때의 출력입니다. 숫자는 임의의 값입니다.
[고지] 이 출력은 공개된 표준 기술적 지표의 계산 결과입니다. 투자권유, 투자자문, 매매 추천이 아니며, 특정 종목의 매매를 권유하지 않습니다. 과거 수치는 미래 성과를 시사하지 않습니다. 투자 판단과 그 결과는 이용자 책임입니다. [MACD] 20260923 기준 (40봉) 상태: MIXED 근거: 교차 없음 (히스토그램 양수) 상세: {'macd': 1500.0, 'signal': 1000.0, 'hist': 500.0} -
05. 가격 채널(돈치안) 돌파 판정
당일을 뺀 직전 20봉의 최고가와 최저가로 채널을 만들고, 종가가 채널보다 높으면 BREAK_UPPER, 낮으면 BREAK_LOWER, 그 안이면 INSIDE 입니다. 당일을 빼는 이유는 당일 고가까지 넣으면 신고가를 찍은 날 상단이 고가와 같아져서 돌파로 잡히지 않기 때문입니다.
def donchian(highs, lows, period=20): """돈치안 채널. 각 봉에서 당일을 뺀 직전 period 봉의 최고가와 최저가""" upper, lower = [None] * len(highs), [None] * len(highs) for i in range(period, len(highs)): upper[i], lower[i] = max(highs[i - period:i]), min(lows[i - period:i]) return upper, lower def evaluate_channel(candles, period=20): if len(candles) < period + 1: return {"state": "NO_SIGNAL", "reason": f"데이터 부족({period + 1}봉 미만)", "detail": {}} upper, lower = donchian([c["high"] for c in candles], [c["low"] for c in candles], period) up, lo, close = upper[-1], lower[-1], candles[-1]["close"] if close > up: state, reason = "BREAK_UPPER", f"종가 {close:,.0f} > {period}봉 최고가 {up:,.0f}" elif close < lo: state, reason = "BREAK_LOWER", f"종가 {close:,.0f} < {period}봉 최저가 {lo:,.0f}" else: state, reason = "INSIDE", f"종가 {close:,.0f}, 채널 {lo:,.0f}~{up:,.0f} 안" position = round((close - lo) / (up - lo) * 100, 1) if up > lo else None # 채널 안에서의 위치(%). 0 이 하단, 100 이 상단 return {"state": state, "reason": reason, "detail": {"upper": round(up), "lower": round(lo), "close": close, "position_pct": position}} print(NOTICE) candles = load_candles("005930") r = evaluate_channel(candles) print(f"[돈치안 채널] {candles[-1]['date']} 기준 ({len(candles)}봉)") print(f" 상태: {r['state']}") print(f" 근거: {r['reason']}") print(f" 상세: {r['detail']}")결과값 예시: 삼성전자(005930) 일봉 40봉으로 실행했을 때의 출력입니다. 숫자는 임의의 값입니다.
[고지] 이 출력은 공개된 표준 기술적 지표의 계산 결과입니다. 투자권유, 투자자문, 매매 추천이 아니며, 특정 종목의 매매를 권유하지 않습니다. 과거 수치는 미래 성과를 시사하지 않습니다. 투자 판단과 그 결과는 이용자 책임입니다. [돈치안 채널] 20260923 기준 (40봉) 상태: BREAK_UPPER 근거: 종가 100,000 > 20봉 최고가 99,500 상세: {'upper': 99500, 'lower': 94000, 'close': 100000.0, 'position_pct': 109.1}
메뉴