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}