2013년 2월 21일 목요일

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

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

  지난 글에 이어 '시나리오 2'를 진행하겠습니다. 선택된 종목과 지표에 대해, 지표를 포함하는 캔들 차트를 출력할 수 있도록 할 것입니다.

▶ (시나리오 2). 'FChart' 수정  
    
  시나리오 2를 위한 폼 개체 편집을 시작합니다. 지난 과정에서 만들었던 폼 'FChart'의 빈 페이지를 활용하겠습니다. VBAProject 트리 목록에서 'FChart'의 Context 메뉴 → '개체 보기'를 선택한 후 'Page2'를 클릭합니다. 

  지표 선택을 위한 콤보상자를 만들겠습니다. 도구상자에서 콤보상자를 선택(그림 1의 1)하고, 폼 위에 적절히 위치시킨 후(그림 1의 2), 이름을 'ComboBoxTI'로 수정합니다(그림 1의 3). 레이블은 적당히 입력하십시요.

그림 1. 지표 선택을 위한 콤보상자 추가

  지표에 따라 설정해야할 기간(period)의 개수가 달라집니다. 시그널(지표의 이동평균)까지 최대 3 개가 필요하므로 텍스트 상자 세 개를 만들겠습니다.  

  도구상자에서 텍스트 상자를 선택(그림 2-1의 1)하고, 폼 위에 적절히 위치시킨 후(그림 2-1의 2), 텍스트 상자의 이름을 'TextBoxP1'으로 수정합니다(그림 2-1의 3).

그림 2-1. 기간 설정을 위한 텍스트 상자 추가 1
  
  동일한 방법으로 'TextBoxP2'와 'TextBoxP3'를 생성한 후 다음 그림 2-2와 같이 레이블을 적절히 입력합니다. ※ (주의) 입력한 이름이 정확한지 다시 한 번 확인하세요.
  
그림 2-2. 기간 설정을 위한 텍스트 상자 추가 2
  
  이동평균을 계산해야 하는 경우, 기본값으로 지수(exponential)이동평균을 사용하는데 이를 단순이동평균으로 변경할 때 사용할 체크박스(확인란)를 생성합니다.  


  도구상자에서 체크박스를 선택(그림 3의 1)하고, 폼 위에 적절히 위치시킨 후(그림 3의 2), 체크박스의 이름을 'CheckBoxSMA'로 수정합니다(그림 2-1의 3). Caption은 적절히 입력하십시요.

그림 3. 단순이동평균 지정을 위한 체크박스 추가

  페이지 이름 'Page2'를 적절히 수정하여 폼의 개체 편집을 완료합니다(그림 3의 4).

  다음은 위에서 생성한 페이지를 초기화하기 위한 코드를 입력하도록 하겠습니다. VBAProject 트리 목록에서 'FChart'의 Context 메뉴 → '코드 보기'를 선택하여 소스코드 편집 모드로 들어간 후 기존의 'UserForm_Initialize' 모듈을 아래와 같이 수정합니다.

  ※ (수정 후 'FChart' 초기화 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Private Sub UserForm_Initialize()
        TextBoxCW.Value = 550
        TextBoxCH.Value = 300
        TextBoxCL.Value = 80
       
        ComboBoxTI.AddItem "MACD", 0
        ComboBoxTI.AddItem "RSI", 1
        ComboBoxTI.AddItem "OBV", 2
       
        Call DisableAllP
    End Sub

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

  ☞ 'DisableAllP'는 다음 소스를 참고하십시요.
  
  'ComboBoxTI'에서 선택된 지표에 따라 'TextBoxP1', 'TextBoxP2' 및 'TextBoxP3'의 초기화 값을 달리 설정하도록 합니다. VBAProject 트리 목록에서 'FChart'의 Context 메뉴 → '개체 보기'를 선택하여 폼 개체편집 모드로 들어간 후 콤보상자 'ComboBoxTI'를 더블클릭합니다. 소스코드 편집 모드로 바뀌면서 이벤트 처리를 위한 Sub모듈의 뼈대가 생성되는데 다음 소스를 참고하여 입력합니다.
  
  ※ ('ComboBoxTI_Change' 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Private Sub ComboBoxTI_Change()
        Select Case ComboBoxTI.ListIndex
        Case 0
            Call EnableAllP
            TextBoxP1.Value = 5
            TextBoxP2.Value = 34
            TextBoxP3.Value = 5
        Case 1
            Call EnableP1
            TextBoxP1.Value = 13
        Case 2
            Call EnableP1
            TextBoxP1.Value = 9
        End Select
    End Sub

    Private Sub DisableAllP()
        TextBoxP1.Value = xlNullString
        TextBoxP2.Value = xlNullString
        TextBoxP3.Value = xlNullString
        TextBoxP1.Enabled = False
        TextBoxP2.Enabled = False
        TextBoxP3.Enabled = False
    End Sub

    Private Sub EnableAllP()
        TextBoxP1.Enabled = True
        TextBoxP2.Enabled = True
        TextBoxP3.Enabled = True
    End Sub

    Private Sub EnableP1()
        TextBoxP2.Value = xlNullString
        TextBoxP3.Value = xlNullString
        TextBoxP1.Enabled = True
        TextBoxP2.Enabled = False
        TextBoxP3.Enabled = False
    End Sub

    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
 
  그림 1~3 과정에서 만든 'ComboBoxTI'와 'TextBoxP1', 'TextBoxP2', 'TextBoxP3' 그리고 'CheckBoxSMA'에서 선택된 값들을 조합하여 'MACD (지수 5 34 5)'와 같은 형태의 formatted string을 만들기 위한 함수를 작성합니다. 아래 소스를 복사하여 입력하십시요.
  
  ※ (함수 'indicator_str' 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Private Function indicator_str() As String
        Dim ns As String
        On Error GoTo Skip:
      
        Select Case ComboBoxTI.ListIndex
        Case 0
            ns = "MACD ("
            If TextBoxP1.Value = xlNullString Then GoTo Skip:
            If CInt(TextBoxP1.Value) > 0 Then
                ns = ns & TextBoxP1.Value & " "
            Else
                GoTo Skip:
            End If
            If CInt(TextBoxP2.Value) > 0 Then
                ns = ns & TextBoxP2.Value & " "
            Else
                GoTo Skip:
            End If
            If Not TextBoxP3.Value = xlNullString Then
                ns = ns & CInt(TextBoxP3.Value) & ")"
            Else
                ns = Left(ns, Len(ns) - 1)
                ns = ns & ")"
            End If
        Case 1
            ns = "RSI ("
            If CInt(TextBoxP1.Value) > 0 Then
                ns = ns & TextBoxP1.Value & ")"
            Else
                GoTo Skip:
            End If
        Case 2
            ns = "OBV"
            If Not TextBoxP1.Value = xlNullString Then
             ns = ns & " (" & CInt(TextBoxP1.Value) & ")"
            Else
                GoTo SkipMA:
            End If
        Case Else
            GoTo Skip:
        End Select
      
        If CheckBoxSMA.Value = True Then
            ns = Replace(ns, "(", "(단순 ")
        Else
            ns = Replace(ns, "(", "(지수 ")
        End If
      
    SkipMA:
        indicator_str = ns
    Skip:

    End Function

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

 
  폼 'FChart' 수정의 마지막 단계로, 'OK' 버튼 이벤트 매크로를 수정하겠습니다. 선택된 지표, 기간, 단순이동평균 여부를 'indicator_str' 함수를 사용하여 정해진 포맷으로 만든 후 'doChartging' 함수의 인자로 추가하여 호출합니다.
  
  ※ (수정 후 'ButtonOK_Click' 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Private Sub ButtonOK_Click()
        Me.Hide
        sn = Selection.Cells(1, 1).Value
        If ActiveSheet.Name = CHART_POSTFIX Then
            If Cells(2, 2).Value <> xlNullString Then _
            sn = Left(Cells(2, 2).Value, InStr(Cells(2, 2).Value, "-") - 1)
        End If
        Call doCharting(sn, _
                        CInt(TextBoxCW.Value), _
                        CInt(TextBoxCH.Value), _
                        CInt(TextBoxCL.Value), _
                        indicator_str())
      
    End Sub

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

 
▶ (시나리오 2). Module 수정 
  

  폼 'FChart'의 'OK' 버튼 클릭 시 호출되는  'doCharting'을 수정합니다.  
  
  ※ (수정 후 'doCharting' 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Sub doCharting(sn, cw, ch, cl, istr)
        Call prepareIndicators(istr, False)
        istr = Replace(istr, "(", "")
        istr = Replace(istr, ")", "")
        istr = Replace(istr, " ", "-")
      
        sn_cs = sheets_cs(sn)
        sn_Arr = Split(sn_cs, ",")
        If UBound(sn_Arr) < 0 Then Exit Sub
      
        If cw = Empty Or cw = xlNullString Or cw < 50 Then cw = CHART_W
        If ch = Empty Or ch = xlNullString Or ch < 50 Then ch = CHART_H
        If cl = Empty Or cl = xlNullString Or cw < 0 Then cl = CHART_L
      
        If createChartSheet(CHART_POSTFIX, sn_cs, xlStockOHLC, cw, ch, cl) Then
            sheets(CHART_POSTFIX).Select
            Call onChartChange(xlStockOHLC, cw, ch, cl, sn_Arr(LBound(sn_Arr)), istr)
        End If
    End Sub

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

  ☞ 지난 글에서 'prepareIndicators' 함수를 작성했습니다. 호출하면서 두 번째 인자를 'False'로 넘겨주는데요, 이미 만들어진 'indicator_t' 구조체 배열('it_arr')에 해당 항목이 없을 경우에 추가하라는 의미입니다.
  ☞ 'onChartChange'를 호출할 때 지표 정보를 포함하는 인자를 추가로 넘겨줍니다.
  기존의 'onChartChange'는 ① 'fillChartRng' 함수를 호출하여 차트가 바라보는 데이터 범위-단일 Range 오브젝트를 선택된 종목의 데이터로 채우게 한 후, ② 'drawChartTWH'를 호출하여 차트를 출력하도록 합니다.

  이것을, 차트뿐만 아니라 지표까지 출력해야 하므로, 단일 Range가 아니라 두 개 또는 세 개의 Range에 대해서도 처리될 수 있도록 수정해야 합니다. ☞ 지표의 이동평균(Signal)까지 포함하는 경우 필요한 Range는 세 개 입니다.

  먼저 'fillChartRngMR' 함수를 신규로 작성하겠습니다.
  
  ※ (함수 'fillChartRngMR' 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Function fillChartRngMR(ParamArray varArgs() As Variant) As Variant
        Dim rstr As String, cols As Variant, col_add As Integer
      
        If UBound(varArgs) < 4 Then _
            Err.Raise 8902, "
fillChartRngMR", "ParamArray too short"
      
        dsheet = varArgs(0)     'sheet to which the chart pointing
        If dsheet = xlNullString Then _
            Err.Raise 8902, "
fillChartRngMR", "Sheet name null"
          
        osheet = varArgs(1)     'sheet including original data of the chart
                                'if xlNullString, = dsheet
        If osheet = xlNullString Then osheet = dsheet
        If Not sheetExist(osheet) Then _
            Err.Raise 8902, "
fillChartRngMR", "Data sheet not exist"
      
        On Error GoTo EH_fillChartRngMR:
        If Not sheetExist(dsheet) Then
            Set tmpWS = Worksheets.Add(After:=Worksheets(Worksheets.Count))
            tmpWS.Name = dsheet
            If varArgs(2) Then tmpWS.Visible = xlSheetHidden
        Else
            Set tmpWS = sheets(dsheet)
            If varArgs(3) < 3 Then tmpWS.Cells.Clear    'dsheet contains data
        End If
      
        k = varArgs(3) - 1
        With sheets(dsheet)
            endRow = sheets(osheet).Cells(1, 1).End(xlDown).Row
            .Cells(1, 1).value = WorksheetFunction.Max(endRow - varArgs(4) - 1, 0)
            endRow = WorksheetFunction.Min(endRow, varArgs(4) + 1)
          
            For j = 2 To 6
            .Cells(1, j).value = sheets(osheet).Cells(1, j - 1).value
            .Cells(2, j).FormulaR1C1 = "=IF(OFFSET('" & osheet & "'!RC[" & (-1) & _
                                       "],R1C" & k & ",0)<>"""",OFFSET('" & osheet & _
                                       "'!RC[" & (-1) & "],R1C" & k & ",0),"""")"
            Next j
            rstr = Cells(2, 2).Address + ":" + Cells(endRow, 6).Address
          
            If UBound(varArgs) > 4 Then
                cols = getIndicatorColumns(osheet, Replace(varArgs(5), "-", " "))
                For i = LBound(cols) To UBound(cols)
                    Col = CInt(cols(i))
                    .Cells(1, j).value = sheets(osheet).Cells(1, Col).value
                    .Cells(2, j).FormulaR1C1 = "=IF(OFFSET('" & osheet & "'!RC[" & (-j + Col) & _
                                               "],R1C" & k & ",0)<>"""",OFFSET('" & osheet & _
                                               "'!RC[" & (-j + Col) & "],R1C" & k & ",0),"""")"
                    rstr = rstr & "," _
                            & Cells(2, j).Address + ":" + Cells(endRow, j).Address
                    j = j + 1
                Next i
                col_add = UBound(cols) - LBound(cols) + 1
            End If
          
            .Cells(2, 2).Resize(1, 5 + col_add).AutoFill _
                                Destination:=.Cells(2, 2).Resize(endRow, 5 + col_add), _
                                Type:=xlFillDefault
        End With
        fillChartRngMR = Split(rstr, ",")
        Exit Function
      
    EH_fillChartRngMR:
        MsgBox "Error(" & Err.Number & ") " & Err.Description & " (on fillChartRngMR)"
    End Function
   
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
  ☞ 기존의 'fillChartRng'와 동일한 기능을 하지만, 캔들차트 데이터 범위와 지표 데이터 범위를 함께 구성합니다.
  ☞ 실제 지표 데이터의 위치를 찾기 위해 'getIndicatorColumns' 함수를 호출합니다. 아래 소스를 참고하여 입력하십시요.

  ※ (함수 'getIndicatorColumns' 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Function getIndicatorColumns(sn, istr) As Variant
        Dim sc As String, indi() As CStockIndicator, i_tp As indicator_t
        Dim tstr As String, cols As String
        On Error GoTo EH_getIndicatorColumn:
        For i = 1 To UBound(ss_list)
            If ss_list(i, 2) = sn Then sc = ss_list(i, 1)
        Next i
        i_tp = indicator_type(istr)
        If i_tp.i_type = 0 Then GoTo MyExit:
        tstr = i_tp.i_type & " " & i_tp.m_type
        For i = LBound(i_tp.peri) To UBound(i_tp.peri)
            tstr = tstr & " " & i_tp.peri(i)
        Next i
       
        indi = feeders.Item(sc).TIndicator
        For i = LBound(indi) To UBound(indi)
            Dim tin As CStockIndicator
            Set tin = indi(i)
            If tstr = tin.printMe() Then
                cols = tin.Col
                If (i_tp.i_type And idx_sig) = idx_sig Then _
                    cols = cols & " " & tin.Col + 1
                GoTo MyExit:
            End If
        Next i
       
    MyExit:
        getIndicatorColumns = Split(cols, " ")
        Exit Function
    EH_getIndicatorColumn:
        Debug.Print "Error(" & Err.Number & ") " & Err.Description & " [getIndicatorColumns]"
        GoTo MyExit:
    End Function
   
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
  ☞ 위 소스와 같이 전역변수 'ss_list'로부터 종목코드를 조회하고, 해당 종목코드의 'CCandleFeeder'로부터 'pTIndicator' 배열을 얻은 후, 일치하는 지표의 column 정보를 획득합니다.
  ☞ 위 함수는 전역변수 'ss_list'가 선언된 모듈에 작성해야 합니다.

  다음은 'drawChartTWHmR'을 신규로 작성할 차례입니다.
  
  ※ (함수 'drawChartTWHmR' 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Function drawChartTWHmR(ParamArray varArgs() As Variant)
        Dim cht As ChartObject
        Dim CHTsheet As Worksheet
      
        Set CHTsheet = ActiveSheet
        If UBound(varArgs) > 3 Then
            If varArgs(4) = xlNullString Then GoTo UseActiveSheet:
            If Not sheetExist(varArgs(4)) Then
                Set CHTsheet = Worksheets.Add(After:=Worksheets(Worksheets.Count))
                CHTsheet.Name = varArgs(4)
            Else
                Set CHTsheet = sheets(varArgs(4))
            End If
    UseActiveSheet:
        End If

        po_y = CHTsheet.ChartObjects.Count * (2 * Rows("1:1").RowHeight + varArgs(3)) _
               + 2 * Rows("1:1").RowHeight
        Set cht = CHTsheet.ChartObjects.Add(Left:=0, Width:=varArgs(2), _
                                            Top:=po_y + Rows("1:1").RowHeight, _
                                            Height:=varArgs(3))
      
        cht.Chart.SetSourceData Source:=varArgs(0)(LBound(varArgs(0)))
        cht.Chart.ChartType = varArgs(1)
      
        If UBound(varArgs) > 6 Then
            cht.Name = varArgs(7)
        End If
          
        For i = LBound(varArgs(0)) + 1 To UBound(varArgs(0))
            With cht.Chart.SeriesCollection.NewSeries
                .values = varArgs(0)(i)
                On Error Resume Next ''' For Excel2003 compatibility '''''''''''''''''''
                .AxisGroup = xlSecondary
                On Error GoTo 0      ''' For Excel2003 compatibility '''''''''''''''''''
                .Type = xlLine
                .MarkerStyle = xlNone
                If i > LBound(varArgs(0)) + 1 Then
                    .Border.LineStyle = xlDot
                    .Name = "Signal"
                Else
                    .Format.Line.Weight = 2#
                    .Name = "Indicator"
                End If
            End With
        Next i
      
        Call rescaleChart(cht)
        For k = 1 To 4
            cht.Chart.Legend.LegendEntries(1).Delete
        Next k
      
        If UBound(varArgs) > 4 Then
            If varArgs(5) <> xlNullString Then
            With CHTsheet.ScrollBars.Add(0, po_y, varArgs(2), Rows("1:1").RowHeight)
                '.Name = "ChartScroll"
                .Min = 0
                .Max = varArgs(6)
                .value = varArgs(6)
                .SmallChange = 1
                .LargeChange = 1
                .LinkedCell = varArgs(5)
                .Display3DShading = True
                .OnAction = "onChartRefresh"
            End With
            End If
        End If
      
        Set CHTsheet = Nothing
        Set cht = Nothing
    End Function
   
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

  ☞ 캔들 차트는 차트의 'xlPrimary' 영역에, 지표와 Signal은 'xlSecondary' 영역에 출력하도록 했습니다. 

  ☞ 차트의 캔들 영역과 지표 영역의 Scale 처리를 위해 'rescaleChart' 함수를 호출합니다. 아래 소스를 참고하십시요.
  ☞ 마찬가지로 차트가 스크롤될 때 Scale 처리를 위해 'onChartRefresh'를 호출합니다.
  
  ※ (함수 'rescaleChart' 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Function rescaleChart(ch)
        On Error GoTo EH_rescaleChart:
        scH = 2
        scL = 3
       
        s_max = WorksheetFunction.Round(WorksheetFunction.Max _
                                       (ch.Chart.SeriesCollection(scH).values), 0)
        s_min = WorksheetFunction.Round(WorksheetFunction.Min _
                                       (ch.Chart.SeriesCollection(scL).values), 0)
        pDiff = WorksheetFunction.Round((s_max - s_min) / 10, 0)
       
        s_max = s_max + pDiff
        s_min = s_min - pDiff
       
        If ch.Chart.SeriesCollection.Count > 4 Then
            scOSC = 5
            s_min = s_min - 4 * pDiff
           
            If InStr(ch.Name, "RSI") > 1 Then
                With ch.Chart.Axes(xlValue, xlSecondary)
                    .MaximumScale = 400
                    .MinimumScale = 0
                End With
            Else
                osc_max = WorksheetFunction.Max(ch.Chart.SeriesCollection(scOSC).values)
                osc_min = WorksheetFunction.Min(ch.Chart.SeriesCollection(scOSC).values)
                If ch.Chart.SeriesCollection.Count > 4 Then
                    sig_max = WorksheetFunction.Max _
                                            (ch.Chart.SeriesCollection(scOSC+1).values)
                    sig_min = WorksheetFunction.Min _
                                            (ch.Chart.SeriesCollection(scOSC+1).values)
                    osc_max = WorksheetFunction.Max(osc_max, sig_max)
                    osc_min = WorksheetFunction.Min(osc_min, sig_min)
                End If
                oDiff = (osc_max - osc_min) / 10
                With ch.Chart.Axes(xlValue, xlSecondary)
                    .MaximumScale = osc_min + 48 * oDiff
                    .MinimumScale = osc_min - oDiff
                    .TickLabelPosition = xlNone
                End With
            End If
        End If
       
        With ch.Chart.Axes(xlValue, xlPrimary)
            .MaximumScale = s_max
            .MinimumScale = s_min
        End With
       
        Exit Function
    EH_rescaleChart:
        Debug.Print "Error(" & Err.Number & ") " & Err.Description & " [
rescaleChart]"
    End Function

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

  ※ (스클롤바 이벤트 매크로 'onChartRefresh' 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Sub onChartRefresh()
        Debug.Print "[onChartRefresh]1 " & ", " & Timer
        Dim ch As ChartObject, x As Variant
       
        On Error GoTo EH_onChartRefresh:
        Set ch = ActiveSheet.ChartObjects(1)
        Call rescaleChart(ch)
       
        Set ch = Nothing
        Debug.Print "[onChartRefresh]2 " & ", " & Timer
        Exit Sub

    EH_onChartRefresh:
        Debug.Print "Error(" & Err.Number & ") " & Err.Description & " [onChartRefresh]"
    End Sub
   
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''   
  
  'onChartChange' 소스를 신규로 작성한 'fillCahrtRngMR'와 'drawChartTWHmR' 함수를 사용하도록 수정하겠습니다. 
  
  ※ (수정 후 'onChartChange' 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Sub onChartChange(ParamArray varArgs() As Variant)
        If UBound(varArgs) < 3 Then _
            Err.Raise 8902, "onChartChange", "ParamArray too short"
           
        Dim rngs() As Range, chtRng As Variant, sn As String
        Dim ch As ChartObject, sbar As ScrollBar, istr As String, cn As String
        On Error GoTo EH_onChartChange:
        Application.ScreenUpdating = False
       
        sn = Cells(2, 2).value
        If UBound(varArgs) > 3 Then sn = varArgs(4)
        If sn = xlNullString Then Exit Sub
       
        csheet = ActiveSheet.Name
        If Not sheets(csheet).ChartObjects.Count = 0 Then
            Set ch = sheets(csheet).ChartObjects(1)
            istr = ch.Name
            istr = Right(istr, Len(istr) - InStr(InStr(istr, "-") + 1, istr, "-"))
            ch.Delete
            Set sbar = Sheets(csheet).ScrollBars(1)
            sbar.Delete
        End If
       
        If UBound(varArgs) > 4 Then
            chtRng = fillChartRngMR(csheet & "_AUX", sn, True, 2, varArgs(3), varArgs(5))
            cn = sn & "-" & varArgs(5)
        Else
            chtRng = fillChartRngMR(csheet & "_AUX", sn, True, 2, varArgs(3), istr)
            cn = sn & "-" & istr
        End If
        sheets(csheet).Select
        ReDim rngs(LBound(chtRng) To UBound(chtRng))
        For i = LBound(chtRng) To UBound(chtRng)
            Set rngs(i) = sheets(csheet & "_AUX").Range(chtRng(i))
        Next i
       
        endRow = sheets(sn).Cells(1, 1).End(xlDown).Row
       
        drawChartTWHmR rngs, _
                     varArgs(0), varArgs(1), varArgs(2), csheet, _
                     csheet & "_AUX" & "!" & Cells(1, 1).Address, _
                     WorksheetFunction.Max(endRow - varArgs(3) - 1, 0), _
                     cn
       
        Set ch = Nothing
        Set sbar = Nothing
        Application.ScreenUpdating = True
        Exit Sub

    EH_onChartChange:
        MsgBox "Error(" & Err.Number & ") " & Err.Description & " [onChartChange]"
    End Sub

    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
  ☞ 차트 이름에 지표정보를 추가했습니다. 'CCandleFeeder'의 'refreshChart' 함수를 수정해야 합니다.

  ※ (수정 후 'CCandleFeeder'의 'refreshChart' 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    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 InStr(ch.Name, pCandleSheet) > 0 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

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

  ☞ 하이라이트된 부분이 수정된 곳입니다.
      
  지금까지 한 작업의 결과를 테스트해 보겠습니다. 먼저 'Schedule' 버튼을 클릭하여 'FSchedule' 폼이 팝업되면 종목과 지표를 선택한 후 'OK' 버튼을 클릭합니다. ☞ 이 과정을 선행하지 않으면 'CCandleFeeder' 오브젝트 목록(Collection)이 생성되지 않은 상태이므로 차트에 지표가 출력되지 않습니다.
  
 다음 그림과 같이  'Chart' 버튼을 클릭하여 폼이 팝업되면 지표를 선택하고 'OK' 버튼을 누릅니다.
  
그림 4-1. 테스트 결과 1
  
  그림 4-2와 같이 지표가 포함된 캔들차트를 확인할 수 있습니다.
   
그림 4-2. 테스트 결과 2
 
  일단 의도한대로 완성은 되었지만, 불편한 점이 있습니다. 차트 출력 시트('CHARTS')에서 지표를 전환하는 것이 불가능하군요.
  
  작업을 조금 더 해서 차트에 포함된 지표의 전환이 쉬워지도록 개선해 보겠습니다. 'CHARTS' 시트의 생성은 'createChartSheet' 함수가 담당하므로, 'createChartSheet'을 수정하면 될 것입니다.
  
  'CHARTS' 시트에 버튼을 하나 추가하고, 클릭할 경우 폼 'FChart'가 팝업되도록 하겠습니다. 
  
  ※ (수정 후 'createChartSheet' 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Function createChartSheet(ParamArray varArgs() As Variant) As Boolean
        If UBound(varArgs) < 5 Then _
            Err.Raise 8902, "createChartSheet", "ParamArray too short"
          
        Dim choices As String
        createChartSheet = False
      
        On Error GoTo EH_createChartSheet:
        csname = varArgs(0)
        If Not sheetExist(csname) Then
            Set tmpWS = Worksheets.Add(After:=Worksheets(Worksheets.Count))
            tmpWS.Name = csname
          
            If UBound(varArgs) < 1 Then GoTo SkipControl:
            If varArgs(1) = xlNullString Then GoTo SkipControl:
            tmpWS.Cells(2, 1).value = "Charts:"
            tmpWS.Cells(2, 2).Font.Size = 10
            tmpWS.Columns(2).ColumnWidth = tmpWS.Columns(1).ColumnWidth * 1.8
            tmpWS.Columns(3).ColumnWidth = tmpWS.Columns(1).ColumnWidth * 0.2
          
            Set btn = tmpWS.Buttons.Add(tmpWS.Cells(2, 4).Width * 3, _
                                        tmpWS.Cells(2, 4).Height, _
                                        tmpWS.Cells(2, 4).Width, _
                                        tmpWS.Cells(2, 4).Height)
            btn.Name = "ButtonChange"
            btn.OnAction = "'onChartChange " & varArgs(2) & ", " _
                           & varArgs(3) & ", " & varArgs(4) & ", " & varArgs(5) & "'"
            btn.Characters.Text = "Change"
            With btn.Characters.Font
            '    .Name = "Times New Roman"
            '    .FontStyle = "Regular"
                .Size = 10
            End With
          
            Set btn = tmpWS.Buttons.Add(tmpWS.Cells(2, 5).Width * 4, _
                                        tmpWS.Cells(2, 5).Height, _
                                        tmpWS.Cells(2, 5).Width, _
                                        tmpWS.Cells(2, 5).Height)
            btn.Name = "ButtonIndicator"
            btn.OnAction = "'Chart_Click True'"
            btn.Characters.Text = "Indicator"
            With btn.Characters.Font
            '    .Name = "Times New Roman"
            '    .FontStyle = "Regular"
                .Size = 10
            End With
      
        Else
            Set tmpWS = sheets(csname)
            On Error Resume Next
            For Each cht In tmpWS.ChartObjects
                cht.Delete
            Next cht
            For Each bar In tmpWS.ScrollBars
                bar.Delete
            Next bar
            On Error GoTo 0
            GoTo SkipControl:
        End If
      
        If UBound(varArgs) < 1 Then GoTo SkipControl:
        With tmpWS.Cells(2, 2).Validation
            .Delete
            .Add Type:=xlValidateList, AlertStyle:=xlValidAlertStop, _
                 Operator:=xlBetween, Formula1:=varArgs(1)
            .IgnoreBlank = True
            .InCellDropdown = True
            .InputTitle = ""
            .ErrorTitle = ""
            .InputMessage = ""
            .ErrorMessage = ""
            .ShowInput = True
            .ShowError = True
        End With
        tmpWS.Cells(2, 2).Font.Size = 9
          
    SkipControl:
        createChartSheet = True
      
    MyExit:
        Exit Function
      
    EH_createChartSheet:
        MsgBox "Error[" & Err.Number & "] " & Err.Description & " [createChartSheet]"
        GoTo MyExit:
    End Function

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

  ☞ 추가한 버튼의 이벤트 매크로를 'Chart_Click'으로 지정했는데, boolean형 인자로 true를 넘겨줍니다. 'FChart'의 차트 크기를 지정하는 페이지를 비활성화하기 위한 것입니다.
  ☞ 'Chart_Click' 매크로를 아래 소스로 수정하십시요.

  ※ (수정 후 'Chart_Click' 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Sub Chart_Click(ParamArray varArgs() As Variant)
        Dim frm As FChart
      
        Set frm = New FChart
        Load frm
        If Not UBound(varArgs) < 0 Then frm.MultiPage1.Pages(0).Enabled = False
        frm.Show
    End Sub

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

  ☞ 하이라이트된 부분이 수정된 곳입니다.
  
  모든 작업이 끝났습니다. 'CHARTS' 시트를 삭제한 후 다시 테스트해 보겠습니다.
     
그림 4-3. 테스트 결과 3
     
  위 그림 4-3과 같이 버튼이 추가된 'CHARTS' 시트를 확인할 수 있습니다.
  
  ※ 완성된 엑셀 파일입니다.  
  
  ※ 다음 글에서 지표의 활용과 관련된 내용을 다루겠습니다.

     

2013년 2월 14일 목요일

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

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


  지난 글에서 몇몇 기술적 지표의 구현을 위한 기초 작업 완료했습니다. 그렇다면 지금까지 작성해왔던 VBA 프로그램과 통합해 보아야 겠지요.

  우선 시나리오를 정리하면  두 개입니다. 하나는 지표를 축적하는 것이고, 다른 하나는 차트에 출력하는 것인데, 모두 DDE 이벤트를 반영해야 합니다. 즉, 축적되는 지표 자체나 이를 출력하는 차트 또한 DDE와 연동되어 update되어야 합니다.
  ▷ 시나리오 1. 지표 축적
  •  'Schedule' 버튼을 클릭하면 축적할 지표를 선택할 수 있는 폼이 팝업된다. ※ 'FSchedule' 폼에 새로운 페이지를 추가하여 사용합니다.
  •  지표를 선택하면 해당 지표의 기간(period)을 설정하는 텍스트 상자와 이동평균 사용시 단순이동평균 사용을 지시하는 체크박스가 활성화된다. ※ 지수이동평균이 default인데 이를 단순이동평균으로 변경할 경우에 사용합니다.
  •  'OK' 버튼을 클릭하면 선택된 지표가 축적된다. ※ 'FSchedule' 폼을 통해 선택된 (종목×시간단위)의 캔들정보 시트에 축적됩니다. 
  •  DDE 이벤트에 따라 지표가 refresh된다.
  시나리오 2. 지표 출력
  • 'Chart' 버튼을 클릭하면 세 가지 지표 중 하나를 선택하는 폼이 팝업된다.
  • 기간을 설정하고 'OK' 버튼을 클릭하면 차트출력 시트("CHARTS")에 지표를 포함하는 캔들차트가 생성된다. ※ 기간은 '시나리오 1'에서 지정한 시간과 다를 수 있습니다.
  • DDE 이벤트에 따라 차트가 refresh된다.
      
▶ (시나리오 1). 'FSchedule' 수정 

  시나리오 1을 위해 기존에 작성한 폼 'FSchedule'을 수정하겠습니다. 엑셀에서 메뉴 '개발 도구' → 'Visual Basic'을 선택하거나 키보드의 'Alt + F11'을 눌러 VBA 편집창을 띄웁니다.
  
  VBAProject 트리 목록에서 'FSchedule'의 Context 메뉴 → '개체 보기'를 선택한 후 다중 페이지의 Context 메뉴 → '새 페이지'를 클릭하여 페이지를 생성합니다. 페이지의 Caption은 적절히 입력하십시요.

  지표를 선택하기 위한 콤보상자를 생성합니다. 도구상자의 콤보상자를 선택하고(그림 1의 1), 새로 생성한 페이지에 위치시킨 후(그림 1의 2), 이름을 'ComboBoxTI'로 수정합니다(그림 1의 3).

그림 1. 지표 선택을 위한 콤보상자 생

  지표의 기간 설정을 위한 텍스트 상자 세 개를 생성하겠습니다. 도구상자의 텍스트 상자를 선택하고(그림 2-1의 1), 적절한 크기로 생성한 후(그림 2-1의 2), 이름을 'TextBoxP1'으로 수정합니다(그림 2-1의 3).
  
그림 2-1. 지표의 기간 설정을 위한 텍스트 상자 생성 1
  
  다음 그림 2-2와 같이 동일한 방법으로 'TextBoxP2'와 'TextBoxP3'를 생성하고 레이블을 적절히 입력합니다.
  
그림 2-2. 지표의 기간 설정을 위한 텍스트 상자 생성 2
  
  지표 계산 시 지수이동평균이 아닌 단순이동평균을 사용하기 위한 체크박스를 생성합니다. 도구상자를 선택하고 체크박스를 선택한 후(그림 3의 1), 다음 그림과 같이 생성합니다(그림 3의 2). 이름은 'CheckBoxSMA'로 합니다(그림 3의 3). Caption은 제거하고 레이블을 적절히 입력합니다.

그림 3. 단순이동평균 지정을 위한 체크박스 생성
 
  지표를 추가하기 위한 버튼을 생성합니다. 도구상자의 명령단추를 선택하고(그림 4의 1), 아래 그림과 같이 생성합니다(그림 4의 2). 버튼 이름은 'ButtonAdd'로 수정합니다(그림 4의 3).  Caption은 적절히 입력하십시요.
  
그림 4. 지표를 추가하기 위한 버튼 생성
  
  추가된 지표를 표시하기 위한 목록상자를 추가합니다. 도구상자에서 목록상자를 선택하고(그림 5의 1), 다음 그림과 같이 생성합니다(그림 5의 2). 목록상자 이름은 'ListBoxTI'로 합니다(그림 5의 3). 레이블은 적절히 추가하십시요.

그림 5. 선택된 지표를 표시하는 목록상자 생성
 
  위 그림 1에서 5까지 추가된 UI 콘트롤들의 이벤트 매크로를 작성하겠습니다. 

  먼저 선택 가능한 지표를 'ComboBoxTI'의 초기화 코드로 입력합니다. 더불어 콤보상자 'ComboBoxTI'를 통해 선택된 항목(지표)이 달라질 때마다 입력해야 할 기간(period)의 개수가 달라집니다. 이를 반영하기 위한 코드를 입력하겠습니다.

  ※ (폼 'FSchedule'의 수정 후 'UserForm_Initialize' 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Private Sub UserForm_Initialize()
        ComboBoxTS.AddItem "09:00:00"
        ComboBoxTS.AddItem "10:00:00"
        ComboBoxTS.AddItem "11:00:00"
        ComboBoxTS.AddItem "12:00:00"
        ComboBoxTS.AddItem "13:00:00"
        ComboBoxTS.AddItem "14:00:00"
        ComboBoxTS.AddItem "15:00:00"
        ComboBoxTS.Text = "09:00:00"
    
        ComboBoxTE.AddItem "09:00:00"
        ComboBoxTE.AddItem "10:00:00"
        ComboBoxTE.AddItem "11:00:00"
        ComboBoxTE.AddItem "12:00:00"
        ComboBoxTE.AddItem "13:00:00"
        ComboBoxTE.AddItem "14:00:00"
        ComboBoxTE.AddItem "15:00:00"
        ComboBoxTE.Text = "15:00:00"
    
        ComboBoxCT.AddItem "5분", 0
        ComboBoxCT.AddItem "15분", 1
        ComboBoxCT.AddItem "60분", 2
        ComboBoxCT.AddItem "일", 3
        ComboBoxCT.ListIndex = 0
    
        s_list = getStockList()
        Set s_code = New Collection
        For i = LBound(s_list, 1) To UBound(s_list, 1)
            If s_list(i, 1) <> "" Then
                s_code.Add s_list(i, 1), s_list(i, 2)
                ComboBoxSN.AddItem s_list(i, 2), i - 1
            End If
        Next i
        ComboBoxSN.ListIndex = 0
    
        s_list = Split(sheets_cs(xlNullString), ",")
        For i = LBound(s_list) To UBound(s_list)
            ListBoxSC.AddItem Replace(s_list(i), "-", " ")
        Next i
    
        ComboBoxTI.AddItem "MACD", 0
        ComboBoxTI.AddItem "RSI", 1
        ComboBoxTI.AddItem "OBV", 2
        ComboBoxTI.AddItem "FRTL", 3
        ComboBoxTI.AddItem "PFTW", 4
        Call DisableAllP
    
        ListBoxTI.AddItem "MACD (지수 5 34 5)"
        ListBoxTI.AddItem "RSI (지수 13)"
        ListBoxTI.AddItem "OBV (지수 9)"
        ListBoxTI.AddItem "FRTL (7)"
        ListBoxTI.AddItem "PFTW"
    End Sub

    Private Sub DisableAllP()
        TextBoxP1.Value = xlNullString
        TextBoxP2.Value = xlNullString
        TextBoxP3.Value = xlNullString
        TextBoxP1.Enabled = False
        TextBoxP2.Enabled = False
        TextBoxP3.Enabled = False
    End Sub

    Private Sub EnableAllP()
        TextBoxP1.Enabled = True
        TextBoxP2.Enabled = True
        TextBoxP3.Enabled = True
    End Sub

    Private Sub EnableP1()
        TextBoxP2.Value = xlNullString
        TextBoxP3.Value = xlNullString
        TextBoxP1.Enabled = True
        TextBoxP2.Enabled = False
        TextBoxP3.Enabled = False
    End Sub

    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
  ☞ 하이라이트 된 부분이 추가된 코드입니다. 
  
  콤보상자 'ComboBoxTI'에서 선택된 값에 따라 텍스트 상자 'TextBoxP1', 'TextBoxP2' 및 'TextBoxP3'의 초기화를 다르게 설정할 것입니다. 폼 'FSchedule'의 개체편집 모드에서 콤보상자 'ComboBoxTI'를 더블클릭하면 소스코드 편집 모드로 바뀌면서 'ComboBoxTI_Change' 매크로의 뼈대가 생성됩니다. 아래 소스를 참고하여 입력하십시요.
  
  ※ ('ComboBoxTI_Change' 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Private Sub ComboBoxTI_Change()
        Select Case ComboBoxTI.ListIndex
        Case 0
            Call EnableAllP
            TextBoxP1.value = 5
            TextBoxP2.value = 34
            TextBoxP3.value = 5
        Case 1
            Call EnableP1
            TextBoxP1.value = 13
        Case 2
            Call EnableP1
            TextBoxP1.value = 9
        Case 3
            Call EnableP1
            TextBoxP1.value = 7
        Case 4
            Call DisableAllP
        End Select
    End Sub

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

  ☞ 선택된 지표에 따라 기간(period) 지정을 위한 텍스트 상자를 다르게 초기화합니다.
 
  지표와 기간을 지정한 후 'Add' 버튼을 클릭하면 선택지표 목록상자 'ListBoxTI'에 목록이 추가되도록 해야 합니다. 폼 'FSchedule'의 개체편집 모드에서 'ButtonAdd'를 더블클릭하면 소스코드 편집 모드로 바뀌면서 'ButtonAdd_Click' 매크로의 뼈대가 생성됩니다. 아래 소스를 참고하여 입력하십시요.
  
  ※ ('ButtonAdd_Click' 이벤트 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Private Sub ButtonAdd_Click()
        Dim ns As String
        On Error GoTo Skip:
     
        Select Case ComboBoxTI.ListIndex
        Case 0
            ns = "MACD ("
            If TextBoxP1.value = xlNullString Then GoTo Skip:
            If CInt(TextBoxP1.value) > 0 Then
                ns = ns & TextBoxP1.value & " "
            Else
                GoTo Skip:
            End If
            If CInt(TextBoxP2.value) > 0 Then
                ns = ns & TextBoxP2.value & " "
            Else
                GoTo Skip:
            End If
            If Not TextBoxP3.value = xlNullString Then
                ns = ns & CInt(TextBoxP3.value) & ")"
            Else
                ns = Left(ns, Len(ns) - 1)
                ns = ns & ")"
            End If
        Case 1
            ns = "RSI ("
            If CInt(TextBoxP1.value) > 0 Then
                ns = ns & TextBoxP1.value & ")"
            Else
                GoTo Skip:
            End If
        Case 2
            ns = "OBV"
            If Not TextBoxP1.value = xlNullString Then
             ns = ns & " (" & CInt(TextBoxP1.value) & ")"
            Else
                GoTo SkipMA:
            End If
        Case 3
            ns = "FRTL ("
            If CInt(TextBoxP1.value) > 0 Then
                ns = ns & TextBoxP1.value & ")"
            Else
                GoTo Skip:
            End If
        Case 4
            ns = "PFTW"
        Case Else
            GoTo Skip:
        End Select
     
        If ComboBoxTI.value <> "FRTL" Or ComboBoxTI.value <> "PFTW" Then
            If CheckBoxSMA.value = True Then
                ns = Replace(ns, "(", "(단순 ")
            Else
                ns = Replace(ns, "(", "(지수 ")
            End If
        End If
     
    SkipMA:
        For i = 0 To ListBoxTI.ListCount - 1
            If ListBoxTI.List(i) = ns Then GoTo Skip:
        Next i
        ListBoxTI.AddItem ns
    Skip:

    End Sub

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

  ☞ 'MACD (지수 5 34 5)', 'RSI (단순 14)'와 같은 형태의 문자열로 목록상자 'ListBoxTI'에 추가됩니다.  
  
  선택지표 목록상자 'ListBoxTI'에 추가된 목록 중의 특정 항목을 더블클릭하면 해당 항목의 선택이 취소되면서 'ListBoxTI'에서 사라지도록 할 것입니다. 아래 'ListBoxTI_DblClick' 소스를 'ComboBoxTI_Change' 코드 밑에 입력하십시요.
  
  ※ ('ListBoxTI_DblClick' 이벤트 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Private Sub ListBoxTI_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
        If Not ListBoxTI.ListIndex < 0 Then _
            ListBoxTI.RemoveItem ListBoxTI.ListIndex
    End Sub

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

  
  폼 'FSchedule'의 'OK' 버튼 이벤트 매크로를 수정해야 합니다. 기존 소스는 캔들정보를 축적할 종목 리스트와 스케줄 시각을 전달하면서 'onSchedule'을 호출합니다. 선택된 지표 목록을 함께 전달하도록 수정합니다.
  
  ※ (수정 후 'ButtonOK_Click' 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Private Sub ButtonOK_Click()
        Dim s_list As String    'comma separated selected stock list
        Dim i_list As String    'comma separated selected indicator list
     
        Me.Hide
        For i = 0 To ListBoxSC.ListCount - 1
            sitem = ListBoxSC.List(i)
            sn = Split(sitem, " ")
            sc = s_code.Item(sn(LBound(sn)))
            sitem = Replace(sitem, " ", "(" & sc & ")")
            s_list = s_list & sitem & ","
        Next i
     
        For i = 0 To ListBoxTI.ListCount - 1
            i_list = i_list & ListBoxTI.List(i) & ","
        Next i
     
        If Len(s_list) > 0 Then
            s_list = Left(s_list, Len(s_list) - 1)
            If Len(i_list) > 0 Then i_list = Left(i_list, Len(i_list) - 1)
            Call onSchedule(ComboBoxTS.Text, ComboBoxTE.Text, _
                            s_list, i_list)
        End If
    End Sub

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

  ☞ 하이라이트 된 부분이 수정된 곳입니다. 
  ☞ 'i_list' 변수가 'MACD (지수 5 34 5),RSI (단순 14)'와 같이 comma-separated string 형태로 선택지표 목록을 저장하여 'onSchedule'에 전달됩니다.
  
  'FSchedule' 폼의 수정은 완료되었습니다.
  ※ 'FSchedule' 폼 소스


▶ (시나리오 1). Module 수정 

  폼 'FSchedule'의 'OK' 버튼 클릭 시 호출되는  'onSchedule'을 수정합니다.

  ※ (수정 후 'onSchedule' 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Sub onSchedule(ts, te, sc, il)
        Call registerLinks
        Call prepareSheet(sc, il)
     
        If Time > TimeValue(ts) And Time < TimeValue(te) Then
            Application.OnTime Now, "Start_Click"
        Else
            Application.OnTime TimeValue(ts), "Start_Click"
        End If
        Application.OnTime TimeValue(te), "Stop_Click"
    End Sub

    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
  ☞ 선택지표 목록(comma-separated string, 'il')을 저장하는 인자가 추가되었습니다.

  마찬가지로 'onSchedule'이 'prepareSheet'을 호출할 때 선택지표 목록을 추가로 넘겨주도록 수정합니다. 함수 'prepareIndicators'를 신규로 작성하고, 'prepareSheet'는 넘겨받은 지표 목록을 인자로 넘기면서 'prepareIndicators'를 호출하도록 하겠습니다.
 
  ※ (수정 후 'prepareSheet' 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Function prepareSheet(slist, ilist)
        Dim xlSheet As Worksheet
        Dim s_arr As Variant
        On Error GoTo EH_prepareSheet:
        dde_sheet = ActiveSheet.Name
     
        Call prepareIndicators(ilist, True)       'for indicators
        s_arr = Split(slist, ",")
        s_cnt = UBound(s_arr) - LBound(s_arr) + 1
        ReDim ss_list(1 To s_cnt, 1 To 2)
        Set feeders = New Collection
     
        For i = LBound(s_arr) To UBound(s_arr)
            sn = s_arr(i)
            pospl = InStr(sn, "(")
            pospr = InStr(sn, ")")
            cu = Right(sn, Len(sn) - pospr)
            s_code = Mid(sn, pospl + 1, pospr - pospl - 1)
            If s_code = Empty Or s_code = "" Then GoTo NextLoop:
         
            sh_name = Left(sn, pospl - 1)
            If sh_name = Empty Or sh_name = "" Then sh_name = s_code
            sh_name = sh_name & "-" & cu
         
            ss_list(i - LBound(s_arr) + 1, 1) = s_code
            ss_list(i - LBound(s_arr) + 1, 2) = sh_name
     
            Dim cfeeder As CCandleFeeder
            Set cfeeder = New CCandleFeeder
            cfeeder.StockCode = s_code
            cfeeder.CandleSheet = sh_name
            cfeeder.DDEsheet = dde_sheet
            cfeeder.CHTsheet = CHART_POSTFIX
            cfeeder.TIndicator = assignIndicators(s_code, sh_name)
            feeders.Add cfeeder, s_code
         
            If Not sheetExist(sh_name) Then
                Set xlSheet = Worksheets.Add(After:=Worksheets(Worksheets.Count))
                xlSheet.Name = sh_name
             
                With xlSheet
                    .Cells(1, 1).value = "일자 / 시간"
                    .Cells(1, 2).value = "시가"
                    .Cells(1, 3).value = "고가"
                    .Cells(1, 4).value = "저가"
                    .Cells(1, 5).value = "종가"
                    .Cells(1, 6).value = "거래량"
                End With
            End If
    NextLoop:
        Next i
     
        Set xlSheet = Nothing
        Exit Function
     
    EH_prepareSheet:
        MsgBox "Error(" & Err.Number & ") " & Err.Description & " [prepareSheet]"
        Err.Raise Err.Number, "prepareSheet", Err.Description
    End Function

    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
  ☞ 인자(선택지표 목록, 'ilist')가 추가되었습니다.
  ☞ 'prepareIndicators' 함수를 호출하는 코드가 추가되었습니다.
  ☞ 지난 글에서 'CCandleFeeder' 클래스에 멤버 변수(property) 'pTIndicator'를 추가했습니다. 이를 초기화하기 위해 함수 'assignIndicators'를 호출합니다. 

  
  신규 함수 'prepareIndicators'와 'assignIndicators'를 작성하겠습니다.

   'prepareIndicators'는 폼 'FSchedule'을 통해 입력받은 지표목록을 구조체 'indicator_t'의 배열로 저장함으로써 다른 함수에서 참조할 수 있도록합니다. 아래 소스를 복사하여 입력하십시요.


  ※ (함수 'prepareIndicators' 소스)

    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Function prepareIndicators(ilist, b)
        Dim indi_t As indicator_t
        On Error GoTo EH_prepareIndicators:
     
        If b Then
            si_list = Split(ilist, ",")
            If UBound(si_list) < 0 Then Exit Function
            ReDim it_arr(LBound(si_list) To UBound(si_list))
         
            For i = LBound(si_list) To UBound(si_list)
                indi_t = indicator_type(si_list(i))
                If indi_t.i_type <> 0 Then it_arr(i) = indi_t
    NextLoop:
            Next i
        Else
            On Error Resume Next
            ub = UBound(si_list)
            If Err.Number = 13 Then si_list = Split(ilist, ",")
            On Error GoTo 0
            For i = LBound(si_list) To UBound(si_list)
                If ilist = si_list(i) Then Exit Function
            Next i
         
            indi_t = indicator_type(ilist)
            If indi_t.i_type <> 0 Then
                ReDim Preserve si_list(LBound(si_list) To UBound(si_list) + 1)
                si_list(UBound(si_list)) = ilist
                ReDim Preserve it_arr(LBound(si_list) To UBound(si_list))
                it_arr(UBound(it_arr)) = indi_t
             
                For i = 1 To UBound(ss_list)
                    Dim fdr As CCandleFeeder
                    On Error Resume Next
                    Set fdr = feeders.Item(ss_list(i, 1))
                    If Err.Number = 5 Then GoTo Continue:
                    On Error GoTo 0
                    If Not fdr Is Nothing Then _
                        fdr.TIndicator = assignIndicators(ss_list(i, 1), ss_list(i, 2))
    Continue:
                Next i
            End If
        End If
     
        Exit Function
    EH_prepareIndicators:
        MsgBox "Error(" & Err.Number & ") " & Err.Description & " [prepareIndicators]"
        Err.Raise Err.Number, "prepareIndicators", Err.Description
    End Function


    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
  ☞ 지표목록(comma-separated string)을 구조체('indicator_t') 배열로 저장합니다.
  ☞ 'it_arr'이 'indicator_t' 구조체 배열입니다. 전역변수로 선언해야 합니다. 'prepareSheet' 함수가 있는 모듈의 선언부에 'Private it_arr() As indicator_t' 라인을 추가하십시요.
  ☞ 'si_list'는 지표목록을 string 배열로 저장하기 위한 전역변수입니다. 'prepareSheet' 함수가 있는 모듈의 선언부에 'Private si_list As Variant' 라인을 추가하십시요.
  ☞ 지표 string(예: 'MACD (지수 5 34 6)')을 'indicator_t'로 변환하기 위해 함수 'indicator_type'을 호출합니다. 아래 소스를 참고하십시요.

  ※ (함수 'indicator_type' 소스) 
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Function indicator_type(istr) As indicator_t 
        Dim indi_t As indicator_t, t_arr As Variant, p_arr() As Integer
        t_arr = Split(istr, " ")
        lB = LBound(t_arr)
        ub = UBound(t_arr)
        If ub < 0 Then Exit Function
     
        Dim mtp As ma_t
        mtp = 0
     
        If InStr(istr, "지수") > 1 Then
            mtp = ma_e                  'default MA type = EMA
        ElseIf InStr(istr, "단순") > 1 Then
            mtp = ma_s
        End If
     
        Select Case t_arr(lB)
        Case "MACD"
            indi_t.i_type = 2 ^ 1
            ReDim p_arr(1 To ub - lB - 1)
            For j = lB + 2 To ub
                p_arr(j - lB - 1) = CInt(Replace(t_arr(j), ")", ""))
            Next j
            indi_t.peri = p_arr
            If ub - lB - 1 > 2 Then indi_t.i_type = indi_t.i_type + 1
        Case "RSI"
            indi_t.i_type = 2 ^ 2
            ReDim p_arr(1 To 1)
            p_arr(1) = CInt(Replace(t_arr(ub), ")", ""))
            indi_t.peri = p_arr
        Case "OBV"
            indi_t.i_type = 2 ^ 3
            If mtp > 0 Then
                ReDim p_arr(1 To 1)
                p_arr(1) = CInt(Replace(t_arr(ub), ")", ""))
                indi_t.peri = p_arr
                indi_t.i_type = indi_t.i_type + 1
            End If
        Case "FRTL"
            indi_t.i_type = 2 ^ 4
            ReDim p_arr(1 To 1)
            p_arr(1) = CInt(Replace(Replace(t_arr(ub), ")", ""), "(", ""))
            indi_t.peri = p_arr
        Case "PFTW"
            indi_t.i_type = 2 ^ 5
        Case Else
            Exit Function
        End Select
     
        indi_t.m_type = mtp
        indicator_type = indi_t
    End Function

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


  'assignIndicators' 함수는 주어진 (종목×캔들정보 시트)에 대응하는 'CCandleFeeder' 클래스의 'pTIndicator' 멤버('CStockIndicator' 배열)를 초기화하기 위한 것입니다. 아래 소스를 참고하십시요.

  ※ (함수 'assignIndicators' 소스)
    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Function assignIndicators(s_code, c_sheet) As Variant
        Dim indi_arr() As CStockIndicator
      
        On Error GoTo EH_assignIndicators:
        If UBound(it_arr) < 0 Then Exit Function
        ReDim indi_arr(LBound(it_arr) To UBound(it_arr))
      
        icol = 7 'indicator column
        For i = LBound(it_arr) To UBound(it_arr)
            Dim sindicator As CStockIndicator, tArr As Variant
          
            Set sindicator = New CStockIndicator
            sindicator.CandleSheet = c_sheet
            sindicator.Indicator = it_arr(i).i_type
            sindicator.MA = it_arr(i).m_type
            sindicator.Peroids = it_arr(i).peri
            sindicator.Col = icol 'sindicator.init()
            icol = sindicator.init()
            Set indi_arr(i) = sindicator
    NextLoop:
        Next i
      
    MyExit:
        assignIndicators = indi_arr
        Debug.Print "assignIndicator total" & UBound(indi_arr) - LBound(indi_arr) + 1
        Exit Function
    EH_assignIndicators:
        If Err.Number = 8911 Then
            Debug.Print "Error(8911) " & Err.Source
            Err.Clear
            GoTo NextLoop:
        End If
        Debug.Print "Error(" & Err.Number & ") " & Err.Description & " [assignIndicators]"
        GoTo MyExit:
    End Function

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

  ☞ 'CStockIndicator' 오브젝트를 생성하면서 'init'을 호출하여 지표 초기계산을 완료합니다.

  

  시나리오 1의 구현이 완료되었습니다. 파일을 저장한 후 테스트해 보겠습니다.

그림 6. 테스트

  'Schedule' 버튼을 클릭하여 'FSchedule' 폼을 팝업시키고 위의 그림 6과 같이 지정하여 'OK' 버튼을 클릭하니 그림 7의 결과를 얻을 수 있었습니다.


그림 7. 테스트 결과

  ※ 현재까지 완성된 엑셀 파일입니다.

  ※ 다음 글에서 '시나리오 2'를 진행하겠습니다.