※ Elliott Pattern Helper Add In
'차트와 지표' 다섯 번째 글입니다. 이 글부터 기술적 지표(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의 '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 프로그램과 통합하는 과정을 진행하겠습니다.
댓글 없음:
댓글 쓰기