2013년 2월 13일 수요일

Excel에서 HTS DDE 활용하기 5 - Chart와 지표 V

※ Elliott Pattern Helper Add In
  • Download Add In for Excel 2007 
  • Download Add In for Excel 2003 

  '차트와 지표' 다섯 번째 글입니다. 이 글부터 기술적 지표(Technical Indicator)를 구현하여 활용하는 과정을 풀어나가도록 하겠습니다.

  우선 지표를 계산하는 것부터 시작해야겠지요? 많은 지표를 다루진 않을 것입니다. 그리고 각각의 지표가 가진 의미나 알고리즘을 거론하지도 않을 것입니다. 단지, 몇 개의 지표를 쌓을 수 있도록 하고, 그 중 하나를 선택하여 차트에 출력하는 것만 진행하겠습니다. 지표를 활용하는 과정은 다음 글에서 다룰 것입니다.


▶ 기술적 지표 구현 방법 

  '기술적 지표를 계산하여 축적'하는 것을 어떻게 구현해야 할까요? 물론, 지난 몇 편의 글을 거치면서 만들어 왔던 Excel VBA 프로그램의 맥락에서 고민하는 것입니다. 


  우선 지표 자체만 볼까요? ① 이미 완성된 캔들, 즉 과거의 캔들에 대해서는 한 번 계산하여 저장해 두면 그만입니다. ② 현재 형성 중인 캔들은 DDE 이벤트에 따라 고가와 저가 및 종가가 달라지므로 그에 대응하여 지표를 계속 update해주어야 합니다.

  그런데 ③ 지표는 하나의 종목, 하나의 차트(캔들, 바 차트 등)에 대해서만 계산되어야 합니다. 동일한 종목이라도, 예를 들어, 15분 차트와 60분 차트 각각에 대해 계산해야 되는 것이죠. 이 뿐만 아니라, 종목과 차트가 정해졌더라도 ④ 여러가지 지표에 대해 이동평균 방법이나 이동평균 기간을 여러가지로 다르게 지정할 수 있어야 하며, ⑦ 지표 자체의 이동평균(Signal)을 계산할 수도 있어야 합니다.

  구체적인 구현 방법을 생각해 보겠습니다. 

  우선, 캔들이 생성될 때마다 이전 캔들에서 계산했던 값을 재사용해야 할 필요가 있고, 다수의 지표를 동시에 독립적으로 계산해야 하므로 ⓐ 클래스 모듈로 작성하는 것이 유리할 것입니다. 

  그리고, 우리의 VBA 프로그램은 증권사 HTS의 DDE와 연동한다는 측면에서 'CCandleFeeder' 클래스가 가장 중요한 역할을 하는데, 이 또한 (특정 종목)×(특정 캔들 또는 차트)에 한하여 작동하므로, ⓑ 'CCandleFeeder' 클래스가 다수의 지표 클래스를 멤버로 갖도록 하는 것이 자연스러울 것입니다.


▶ 자료형 및 필요 함수 작성 

  먼저 구현의 편이성을 위해 자료형을 몇 개 작성하겠습니다. 아래 소스를 참고하십시요.  'idx_k'와 'ma_t'는 각각 지표의 종류와 이동평균 종류를 구분하기 위한 Enum형이며, 'indicator_t'는 지표를 식별하기 위한 구조체로서 앞으로 작성할 지표 클래스와는 별도로 사용될 것입니다.

  ※ (자료형 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Public Enum idx_k       'technical indicator type
        idx_sig = 1         'whether to use signal(MA of indicator) or not
        idx_macd = 2        'MACD
        idx_rsi = 4         'RSI
        idx_obv = 8         'OBV
        idx_frtl = 16       'for Fractal Count
        idx_pftw = 32       'for Profitunity Window
    End Enum

    Public Enum ma_t        'moving average type
        ma_s = 1            'simple ma
        ma_e = 2            'normal exponential ma
        ma_em = 4           'modified ema
        ma_ea = 8           'ema with smoothed 1st value
    End Enum

    Type indicator_t        'indicator structure
        i_type As Integer   'indicator type (idx_k)
        m_type As Integer   'MA type (ma_t)
        peri() As Integer   'array of periods
    End Type

    Public Enum frT         'fractal
        ffl = 0             'none
        fup = 1             'up
        fdo = 2             'down
        fdi = 4             'possible divergence
        fco = 8             'possible consolidation
        wcu = 16            'possible trend change, up to down
        wcd = 32            'possible trend change, down to up
    End Enum

    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''


  다음은 단순 및 지수 이동평균 계산을 위한 함수, 계산이 비교적 간단한 MACD와 OBV 지표 함수를 작성하도록 하겠습니다. 아래 소스를 참고하십시요.

  ※ 이동평균 및 간단한 지표 계산 함수 
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Public Function sma(y As Variant, N As Integer) As Variant
        Dim v As Variant
        lB = LBound(y)
        ub = UBound(y)
        ReDim v(lB To ub)
     
        v(lB) = y(lB) / N
        For i = lB + 1 To WorksheetFunction.Min(lB + N - 1, ub)
            v(i) = v(i - 1) + y(i) / N
        Next i
        For i = lB + N To ub
            v(i) = v(i - 1) - y(i - N) / N + y(i) / N
        Next i
     
        sma = v
    End Function

    Public Function ema(y As Variant, N As Integer, flag As ma_t) As Variant
        Dim v As Variant, alpha As Double
        lB = LBound(y)
        ReDim v(lB To UBound(y))
        alpha = ema_alpha(N, flag)
        v(lB) = ema_first(y, flag)
     
        For k = lB + 1 To UBound(y)
            v(k) = alpha * y(k) + (1 - alpha) * v(k - 1)
        Next k
     
        ema = v
    End Function

    Public Function ema_first(y As Variant, flag As ma_t) As Double
        lB = LBound(y)
        ema_first = y(lB)
        If UBound(y) - lB + 1 > 3 And (flag And ma_ea) = ma_ea Then
            For i = 1 To 3
                ema_first = ema_first + y(lB + i)
            Next i
            ema_first = ema_first / 4
        End If
    End Function

    Public Function ema_alpha(N As Integer, flag As ma_t) As Double
        ema_alpha = 2 / (N + 1)
        If (flag And ma_em) = ma_em Then ema_alpha = 1 / N
    End Function

    Public Function macd(y As Variant, n1 As Integer, n2 As Integer, flag As ma_t) As Variant
        Dim v As Variant, m1 As Double, m2 As Double
        lB = LBound(y)
        ub = UBound(y)
        ReDim v(lB To ub)
     
        Select Case flag
        Case ma_s
            m1 = y(lB) / n1
            m2 = y(lB) / n2
            v(lB) = (m1 - m2) '/ m1
         
            For k = lB + 1 To ub
                m1 = m1 + y(k) / n1
                If k - lB + 1 > n1 Then m1 = m1 - y(k - n1) / n1
                m2 = m2 + y(k) / n2
                If k - lB + 1 > n2 Then m2 = m2 - y(k - n2) / n2
                v(k) = (m1 - m2) '/ m1
            Next k
        Case ma_e
    Do_ma_e:
            m1 = ema_first(y, flag)
            m2 = ema_first(y, flag)
            v(lB) = (m1 - m2) '/ m1
            alpha1 = ema_alpha(n1, flag)
            alpha2 = ema_alpha(n2, flag)
         
            For k = lB + 1 To ub
                m1 = alpha1 * y(k) + (1 - alpha1) * m1
                m2 = alpha2 * y(k) + (1 - alpha2) * m2
                v(k) = (m1 - m2) '/ m1
            Next k
            GoTo Continue:
         
        Case ma_e + ma_em
            GoTo Do_ma_e:
        Case ma_e + ma_ea
            GoTo Do_ma_e:
        End Select
     
    Continue:
        macd = v
    End Function

    Public Function obv(y As Variant, yv As Variant) As Variant
        Dim v As Variant
        lB = LBound(y)
        ub = UBound(y)
        ReDim v(lB To ub)
     
        v(lB) = yv(lB)
        For k = lB + 1 To ub
            If y(k) = y(k - 1) Then
                v(k) = v(k - 1)
            Else
                v(k) = v(k - 1) + yv(k) * (y(k) - y(k - 1)) / Abs(y(k) - y(k - 1))
            End If
        Next k
     
        obv = v
    End Function

    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''


  ※ 위 소스(자료형 포함)가 담겨있는 bas 모듈

▶ 기술적 지표 클래스 모듈 구현 

  위에서 언급했던 구현방법의 ①과 ②에서 클래스의 method를, ③ ~ ⑦을 통해 작성해야 할 클래스의 멤버 변수(Property)를 파악할 수 있습니다. 우선 뼈대를 만들어 보겠습니다.

  ※ (클래스 'CStockIndicator' 구조)
    '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' 
    Private pCandleSheet    As String   'candle sheet name
    Private pIndicator      As idx_k    'indicator type
    Private pMA             As ma_t     'moving average type
    Private pPeriods()      As Integer  'periods
    Private values          As Variant  'array of indicator values
    Private sigs            As Variant  'array of sig values


    Function init()

    Function updateLatest(l, b)

    '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' 
  ☞ 최소한의 property와 method들만 나열했습니다.
  ☞ 'values'와 'sigs'는 실제 지표값과 그 이동평균(Signal)을 각각 저장하는 배열입니다. 당장은 필요하지 않아 보이지만, 다음 글의 주제 '지표의 활용'을 염두에 두고 일단은 포함시켰습니다.
  ☞ 'init' 함수가 지표의 초기 계산에 사용되며, 'updateLatest' 함수가 DDE 이벤트에 대응하여 마지막 지표값을 계산합니다.
  ☞ 다른 지표를 추가하려면 ① Enum형 'idx_k'에 항목을 추가하고 ② 지표 함수를 추가하거나 클래스를 작성한 후 ③ 'CStockIndicator'의 'init' 함수, 'updateLatest' 함수의 'Select Case' 문에 반영하면 됩니다.

  이 시점에서 고려해야 할 것이 있습니다. 앞에서도 잠시 언급했지만, 완성된 과거의 캔들에 대해서 계산된 값들을 재활용하는 것이 시스템의 자원 활용 측면에서 유리할텐데, 지표마다 그 값들이 다르다는 점입니다. 

  그들을 모두 'CStockIndicator' 클래스의 property로 포함시키는 것보다 별도의 클래스로 만들고 필요한 경우 해당 클래스의 오브젝트를 만들어 사용하는 것이 좋을 듯 합니다. 그리고 지표마다 실제 계산 방법이 다르므로 위에서 정의한 'init'이나 'updateLatest' method를 실제로 구현하는 것은 그 별도의 클래스에서 담당하는 것이 바람직할 것입니다.

  그런데, 모든 지표에 대해 별도의 클래스를 작성할 필요는 없어보입니다. 가령, MACD나 OBV 지표의 경우, 셀에 수식을 입력하는 것만으로도 충분하기 때문입니다. 따라서 이들을 제외한 나머지 지표에 대해서만 클래스를 작성합니다.

  ※ 클래스 'CStockIndicator' 소스
  ※ 클래스 'CStockIndicatorRSI' 소스 
      (RSI 지표를 구현한 클래스입니다.)
  ※ 클래스 'CStockIndicatorFRTL' 소스
      (Bill Williams의 'Elliott Wave Fractal' 개념을 지표화한 것입니다.)
  ※ 클래스 'CStockIndicatorPFTW' 소스 
      (Bill Williams의 'Profitunity Window' 개념을 지표화한 것입니다.)
  
  새로운 클래스의 작성은 완료되었습니다. 다음은 기존의 'CCandleFeeder' 클래스의 멤버(property)로 'CStockIndicator' 배열을 추가하고, 'fillCandle'과 'updateCandle'에서 'CStockIndicator'의 'updateLatest'를 호출하도록 수정합니다.

▶  'CCandleFeeder' 클래스 수정 

  'CCandleFeeder' 클래스에 'CStockIndicator' 배열을 멤버로 추가하고 초기화 함수를 입력합니다. 또한 'fillCandle'과 'updateCandle' 함수에서 지표를 update하기 위해 'CStockIndicator'의 'updateLatest'를 호출하도록 수정합니다.

  ※ 수정 후 'CCandleFeeder' 클래스 모듈 소스

    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Private pStockCode      As String
    Private pCandleSheet    As String
    Private pDDEsheet       As String
    Private pCHTsheet       As String
    Private pTIndicator()   As CStockIndicator

    Property Let StockCode(v As String)
        If v = Empty Or v = "" Then _
            Err.Raise 8901, "CCandleFeeder.Let StockCode", "Invalid parameter"
        pStockCode = v
    End Property

    Property Let CandleSheet(v As String)
        If v = Empty Or v = "" Then _
            Err.Raise 8901, "CCandleFeeder.Let CandleSheet", "Invalid parameter"
        pCandleSheet = v
    End Property

    Property Let DDEsheet(v As String)
        If v = Empty Or v = "" Then _
            Err.Raise 8901, "CCandleFeeder.Let DDEsheet", "Invalid parameter"
        pDDEsheet = v
    End Property

    Property Let CHTsheet(v As String)
        If v = Empty Or v = "" Then _
            Err.Raise 8901, "CCandleFeeder.Let CHTsheet", "Invalid parameter"
        pCHTsheet = v
    End Property

    Property Let TIndicator(v)
        pTIndicator = v
        Debug.Print "pTIndicator total" & UBound(pTIndicator)
    End Property


    Property Get TIndicator()
        TIndicator = pTIndicator
    End Property

    Sub store(s_code)
        Static dde_cell As Integer 'the row of DDE cells of selected stock
        Static prev_dt As String   'previous date
        Static prev_tu As Integer  'previous time unit
        Static prev_li As Integer  'row number of the row being updated
        Static prev_vo As Double   'sum of volumes of prev candles of the same day
        Static c_p_hr  As Integer  'the number of candles per hour
        On Error GoTo EH_storeCandleInfo:
     
        If s_code = pStockCode Then
            sh_name = pCandleSheet
            'Debug.Print "sh_name " & sh_name
            If dde_cell = 0 Then dde_cell = rowSearch(pDDEsheet, 2, "'" & s_code & "'")
            If c_p_hr = 0 Then c_p_hr = candlesPerHour(sh_name)
            'Debug.Print "c_p_hr " & c_p_hr
         
            With sheets(sh_name)
                'Debug.Print "prev_li " & prev_li
                If prev_li = 0 Then
                    If .Cells(2, 1).value = "" Then
                        prev_li = 1
                    Else
                        'prev_li = .UsedRange.Cells(.Cells.Count).Row '===> Overflow
                        prev_li = .Cells(1, 1).End(xlDown).Row
                    End If
                End If
                'Debug.Print "prev_li " & prev_li
             
                cp = sheets(pDDEsheet).Cells(dde_cell, 2).value      '현재가
                cv = sheets(pDDEsheet).Cells(dde_cell, 3).value      '거래량
                dv = Abs(sheets(pDDEsheet).Cells(dde_cell, 4).value) '체결량
                bt = sheets(pDDEsheet).Cells(dde_cell, 5).value      '체결시간
             
                If prev_li = 1 Then
                    If c_p_hr = -1 Then GoTo DailyNewCandle:
    NewCandle:
                    prev_tu = WorksheetFunction.Floor(TimeValue(Right(bt, 8)) _
                              * 24 * c_p_hr, 1)
                    Debug.Print "prev_tu " & prev_tu
                 
    HourlyNewCandle:
                    Dim hr As Integer, hrstr As String
                    hr = WorksheetFunction.Floor(prev_tu / c_p_hr, 1) 'prev_tu / c_p_hr
                    hrstr = hr
                    If hr < 10 Then hrstr = "0" & hr
                 
                    If c_p_hr > 1 Then
                        mt = (prev_tu Mod c_p_hr) * (60 / c_p_hr)
                        If Len(mt) = 1 Then mt = "0" & mt
                        hr_mt = "-" & hrstr & ":" & mt & ":00"
                    ElseIf c_p_hr > 0 Then
                        hr_mt = "-" & prev_tu & ":00:00"
                    End If
                 
    DailyNewCandle:
                    prev_dt = Replace(Format(Date, "yyyy/mm/dd"), "-", "/")
                    Debug.Print "prev_dt & hr_mt " & prev_dt & hr_mt
                    prev_li = prev_li + 1
                    Call fillCandle(sh_name, prev_li, prev_dt & hr_mt, cp, cv - prev_vo) 'dv)
                    'Exit Sub
                    GoTo ChartRefresh:
                End If
             
                If prev_dt = "" Then prev_dt = Left(.Cells(prev_li, 1).value, 10)
                If c_p_hr = -1 Then
                    If DateValue(prev_dt) <> Date Then
                        GoTo DailyNewCandle:
                    Else
                        Call updateCandle(sh_name, prev_li, cp, cv)
                        GoTo SkipChartRefreshing:
                    End If
                 
                Else
                    If prev_tu = 0 Then
                        Debug.Print "prev_li " & prev_li
                        Debug.Print Right(.Cells(prev_li, 1).value, 8)
                        prev_tu = WorksheetFunction.Floor(TimeValue( _
                                  Right(.Cells(prev_li, 1).value, 8) _
                                  ) * 24 * c_p_hr, 1)
                    End If
                            
                    If DateValue(prev_dt) <> Date Then
                        Debug.Print "Going to NewCandle"
                        GoTo NewCandle:
                    Else
                        cur_tu = WorksheetFunction.Floor(TimeValue(Right(bt, 8)) _
                                 * 24 * c_p_hr, 1)
                              
                        If prev_tu <> cur_tu Then
                            Debug.Print Right(bt, 8)
                            Debug.Print prev_tu & "," & cur_tu
                            prev_vo = cv - dv
                            prev_tu = cur_tu
                            GoTo HourlyNewCandle:
                        Else
                            If prev_vo = 0 Then prev_vo = cv - dv - .Cells(prev_li, 6).value
                            Call updateCandle(sh_name, prev_li, cp, cv - prev_vo)
                            GoTo SkipChartRefreshing:
                        End If
                    End If
                 
                End If
            End With
         
    ChartRefresh:
            Call refreshChart(pCHTsheet)
    SkipChartRefreshing:
        End If
     
        Exit Sub
    EH_storeCandleInfo:
        'MsgBox "Error(" & Err.Number & ") " & Err.Description & " [store]"
        Err.Raise Err.Number, "CCandleFeeder.store", Err.Description
    End Sub

    Private Function fillCandle(sh_name, l, d, p, v)
        With sheets(sh_name)
            .Cells(l, 1).value = d
            .Cells(l, 2).value = p
            .Cells(l, 3).value = p
            .Cells(l, 4).value = p
            .Cells(l, 5).value = p
            .Cells(l, 6).value = v
        End With
     
        For Each indi In pTIndicator
            Call indi.updateLatest(l, True)
        Next
     
    End Function

    Private Function updateCandle(sh_name, l, p, v)
        With sheets(sh_name)
            If p > .Cells(l, 3).value Then .Cells(l, 3).value = p
            If p < .Cells(l, 4).value Then .Cells(l, 4).value = p
            .Cells(l, 5).value = p
            .Cells(l, 6).value = v
        End With
     
        For Each indi In pTIndicator
            Call indi.updateLatest(l, p, False)
        Next
     
    End Function

    Private Function refreshChart(csheet)
        Dim ch As ChartObject, sbar As ScrollBar
        Debug.Print "Chart Refreshing..."
        If Not sheetExist(csheet) Then GoTo SkipChartRefreshing:
        On Error Resume Next
        Set ch = sheets(csheet).ChartObjects(1)
        If Err.Number <> 0 Then
            Err.Clear
            GoTo SkipChartRefreshing:
        End If
        If Not ch.Name = pCandleSheet Then GoTo SkipChartRefreshing:
        Set sbar = sheets(csheet).ScrollBars(1)
        sbar.Max = sbar.Max + 1
        sbar.value = sbar.value + 1
        Set ch = Nothing
        Set sbar = Nothing
    SkipChartRefreshing:
        Debug.Print "End Chart Refreshing"
    End Function

    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

  ☞ 하이라이트 된 부분이 수정된 곳입니다.

  ※ 다음 글에서 위에서 작업한 내용들을 기존의 VBA 프로그램과 통합하는 과정을 진행하겠습니다.

  

댓글 없음:

댓글 쓰기