[ Splunk Project ] Splunk로 주식 분석 | Phase 9. 주식 매매 결정 하기
본문 바로가기

Splunk/Splunk Project

[ Splunk Project ] Splunk로 주식 분석 | Phase 9. 주식 매매 결정 하기

728x90
반응형

삼중창 기법(Triple Screen Trading) 은

아래 책의 내용을 기반으로 스플렁크로 컨버팅 하였습니다.

 

https://book.naver.com/bookdb/book_detail.nhn?bid=16381920

 

파이썬 증권 데이터 분석 : 네이버 도서

네이버 도서 상세정보를 제공합니다.

search.shopping.naver.com

 

 

 

 

🔆 목표 🔆

삼중창 기법(Triple Screen Trading)을 이용해서

매수/매도 타이밍을 결정하는 방법을 알아보고

이를 스플렁크를 통해 구현해 보자.

 

 

 

🍏삼중창 기법🍏

삼중창 기법은 1982년 알레산더 엘더가 만든 방법으로

3번의 분석을 통해서 각 분석별로 의미를 도출하여 투자의 방향을 결정한다.

 

🍎 1창 🍎

장기 추세선에 따라서 상승 곡선일 때는 매수,

하락 곡선일 때는 매도하는 단순한 방법을 제공한다.

 

🍎 2창 🍎

1창의 추세에 대한 단기 추세를 보면서 역행하고 있는지 확인한다.

단기적으로 역행하고 있을 때 장기로 본다고 하면, 매도/매수 타이밍이 생긴다.

 

🍎 3창 🍎

1창/2창을 이용해 진입 타이밍을 결정하게 된다.

주간 추세가 상승하면 추적 매수 스탑을 하락 추세에서는 추적 매도 스톱을 이용한다.

 

말로해서는 어려우니 실제 구현해 보도록 하자.

 

 

🍎 1창 🍎

1창은 장기 추세선을 파악하는 것이다.

장기 추세선은 단순 이동 평균을 사용할 수 도 있고,

지수 이동 평균(EMA: Exponential Moving Average)을 사용할 수 있는데,

 

책에서는 지수 이동 평균을 사용하고 있다.

이는 최근 가격에 더 많은 가중치를 주기 때문에 더 정확하다고 한다.

지수 이동 평균은 아래와 같다.

기존의 streamstats 을 쓰기에는 재귀 함수가 있어서 무리가 있다.

다행히도 스플렁크는 "trendline" 이라는 명령을 통해서

단순 이동 평균(sma), 가중치 이동 평균(wma), 지수 이동 평균 함수(ema)를 미리 제공하고 있다.

그래서 아래 구문을 통해서 각 이동 평균을 구할 수 있다.

... | trendline sma30(Close), ema30(Close), wma30(Close)

장기 트렌트에서 상승 장에서만 매수를 하고,

하락할 때는 매도를 하기 때문에 장기 트렌드만 그릴 수 있으면 문제는 거의 해결이 된다.

index="kospi" earliest=-360d
          | eval k_code = code
          | rex field=code "^(?<code>\d+).KS"
          | lookup kospi_200 code OUTPUT name   
          | where name = "CHONGKUNDANG"
          | sort _time
          | trendline ema60(Close) as ema60, ema130(Close) as ema130
          | where isnotnull(ema130) | table _time, Close, ema130, ema60

종근당 주식에 대한 130일과 60일에 대한 지수 이동 평균을 이용해 트렌드를 그린다.

해당하는 이벤트가 없다는 오류가 떴다.

그럴 법도 한게 360일치 데이터가 없어서 130일 지수 이동 평균이 제대로 나올리가 없어

모든 항목에 ema130필드의 데이터가 존재하지 않는다.

 

그래서 데이터를 모조리 지우고 2022-01-01부터 2023-06-25까지의 데이터를 수집해줬다.

파이썬 코드가 있는 디렉토리에서 아래의 명령어로 조져줬다.

python3 stock_monitor.py --start_date 2022-01-01 --end_date 2023-06-25

자주색 130일선을 보면 지속적인 상승 장인 것을 확인할 수 있다. (매수각인정??)

 

 

🍎 2창 🍎

이제 2창으로 넘어가 보자.

2창은 쫌 더 복잡하다.

오실레이터를 구해야 하는데, 그게 뭥믜?

 

이론을 다 설명하면 힘들기 때문에 이는 책을 참조하기로 하고,

우선 SPL 을 통해서 무엇을 해야하는지 파악해 보자.

 

index="kospi" earliest=-360d
| eval k_code = code
| rex field=code "^(?<code>\d+).KS"
| lookup kospi_200 code OUTPUT name   
| where name = "CHONGKUNDANG"
| sort _time
| trendline ema60(Close) as ema60, ema130(Close) as ema130, ema5(Close) as ema5
| table _time, Close, ema60, ema130, ema5
| join _time [ | search index="kospi"  earliest=-360d
  | eval k_code = code
  | rex field=code "^(?<code>\d+).KS"
  | lookup kospi_200 code OUTPUT name   
  | where name = "CHONGKUNDANG"
  | sort _time
  | streamstats window=14 current=true max("High") as ndays_high, min("Low") as ndays_low
  | eval fast_k = (Close - ndays_low) / (ndays_high - ndays_low) * 100
  | streamstats window=3 avg(fast_k) as slow_d
  | table _time, fast_k, slow_d ]
| where isnotnull(ema130)
| table _time, fast_k, slow_d

 

 

🌳STEP 1🌳 14일 간의 최대값과 최소값을 구한다.

...
| streamstats window=14 current=true max("High") as ndays_high, min("Low") as ndays_low
...

 

 

 

🌳STEP 2🌳 이를 이용해서 값이 변화하는 속도를 구한다.

...
  | eval fast_k = (Close - ndays_low) / (ndays_high - ndays_low) * 100
...

 

 

 

🌳STEP 3🌳 값이 너무 급격하게 변할 수 있기 때문에

                          이를 3일 이동 평균을 구해서 좀 더 완만하게 변하도록 해준다.

...
 | streamstats window=3 avg(fast_k) as slow_d
...

 

 

 

🌳STEP 4🌳 이 두값을 _time 을 기준으로 "join" 구문을 이용해 합쳐 준다.

...
| join _time [  ...

"join" 은 다음에 나오는 필드를 기준으로 해서 앞에 있는 SPL 문과 뒤에 있는 SPL 문을 합쳐준다.

일반 데이터 베이스와 같이 outer(type=outer), inner(type=inner, 디폴트), left(type=left) 조인을 이용할 수 있는데,

데이터베이스 join과 하는 일은 같지만, 내부 동작 메커니즘은 다르기 때문에(성능 이슈/조인 개수 제한)

적절하게 사용해야 한다. (기회가 있으면 join 보다 더 빠르게 처리할 수 있는 방법을 소개하도록 하겠다. )

자 위의 쿼리를 수행하면 다음과 같은 차트를 얻을 수 있다.

이제 1창과 2창을 조합해서 보자.

1창에서 상승 곡선이고 2창에서의 slow_d가 30보다 작으면 매수 타이밍이다.

그리고 1창에서 하락 곡선으로 2창에서 slow_d가 70보다 크면 매도 타이밍이다.

 

 

 

🍎 3 🍎

마지막 3창은 주간 지수 이동 평균을 구한다.

짧은 기간의 이동 평균을 통해서 추적 매수 스톱과 추적 매도 스톱 전략을 구사하게 된다고 하는데,

이번에는 하루 안에서의 가격 변동은 신경 쓰지 않기로 해서 크게 고려하지 않고,

일 단위 변동만 고려하도록 한다.

 

 

모두 엮기

자 이제 위의 상황들을 엮어서 매수/매도 타이밍을 선정해 보자.

index="kospi" earliest=-360d
          | eval k_code = code
          | rex field=code "^(?<code>\d+).KS"
          | lookup kospi_200 code OUTPUT name   
          | where name = "CHONGKUNDANG"
          | sort _time
          | trendline ema60(Close) as ema60, ema130(Close) as ema130, ema5(Close) as ema5
          | table _time, Close, ema60, ema130, ema5
          | join _time [ | search index="kospi"  earliest=-360d
          | eval k_code = code
          | rex field=code "^(?<code>\d+).KS"
          | lookup kospi_200 code OUTPUT name   
          | where name = "CHONGKUNDANG"
          | sort _time
          | streamstats window=14 current=true max("High") as ndays_high, min("Low") as ndays_low
          | eval fast_k = (Close - ndays_low) / (ndays_high - ndays_low) * 100
          | streamstats window=3 avg(fast_k) as slow_d
          | table _time, fast_k, slow_d ]
| where isnotnull(ema130) | table _time, fast_k, slow_d
| streamstats window=2 list(ema130) as ema130_list, list(ema5) as ema5_list, list(slow_d) as slow_d_list
| eval ema130_pre = tonumber(mvindex(ema130_list, 0)), ema130_cur = tonumber(mvindex(ema130_list, 1))
| eval ema5_pre = tonumber(mvindex(ema5_list, 0)), ema5_cur = tonumber(mvindex(ema5_list, 1))
| eval slow_d_pre = tonumber(mvindex(slow_d_list, 0)), slow_d_cur = tonumber(mvindex(slow_d_list, 1))
| eval step1 = if(ema130_pre > ema130_cur, -1, 1) 
| eval step2_pre = if(slow_d_pre > slow_d_cur, -1, 1) 
| eval step3_pre = if(ema5_pre > ema5_cur, -1, 1)  
| eval step2 = case(step1 > 0 AND slow_d < 30, 1, step1 < 0 AND slow_d < 70, -1, 1=1, 0)
| eval step3 = case(step2_pre < 0 AND step3_pre > 0, 1,  step2_pre > 0 AND step3_pre < 0, -1, 1 = 1, 0)
| table _time, ema130_list, step1, step2, step3
| eval annotation_category1 = case(step2 > 0, "BUY", step2 < 0, "SELL", 1=1, "HOLD")
| eval annotation_category2 = if(step3 !=0 , "TRACE_STOP", "") 
| eval annotation_category = annotation_category1 + "(" + annotation_category2 + ")"
| where step2 != 0
| table _time, step1, step2, step3, annotation_category

뭔가 복잡해 보이지만 실제 많이 복잡하지는 않다.

우선 상승 중인지 하락 중인지 판단해야 하는 지표들에 대해서

"streamstats" 을 이용해 2개씩 데이터를 묶는다.

 

...
| streamstats window=2 list(ema130) as ema130_list, list(ema5) as ema5_list, list(slow_d) as slow_d_list
...

그리고 이를 전/후 값으로 비교한다.

...
| eval ema130_pre = tonumber(mvindex(ema130_list, 0)), ema130_cur = tonumber(mvindex(ema130_list, 1))
| eval ema5_pre = tonumber(mvindex(ema5_list, 0)), ema5_cur = tonumber(mvindex(ema5_list, 1))
| eval slow_d_pre = tonumber(mvindex(slow_d_list, 0)), slow_d_cur = tonumber(mvindex(slow_d_list, 1))
| eval step1 = if(ema130_pre > ema130_cur, -1, 1) 
| eval step2_pre = if(slow_d_pre > slow_d_cur, -1, 1) 
| eval step3_pre = if(ema5_pre > ema5_cur, -1, 1)  
...

상승 중이면 1 을 하락 중이면 -1 값을 주었다.

1창에 의하면 step1 의 값이 1 이면 "BUY", -1 이면 "SELL" 이 된다.

이제 2창에 값에 의하면 1창이 1보다 크고 현재 값이 30 이하이면 "BUY" ,

1보다 작고 현재 값이 70 이상이면 "SELL" 이된다.

 

...
| eval step2 = case(step1 > 0 AND slow_d < 30, 1, step1 < 0 AND slow_d < 70, -1, 1=1, 0)
...

자, 3창의 경우에는 책의 내용과는 약간 다르지만, 흉내를 내보면,
주간 지표가 상승이고 slow_d가 하락이면 "추적 매수 스탑" 기법을
주간 지표가 하락이고 slow_d 가 상승이면 "추적 매도 스탑" 기법을 사용한다.

 

...
| eval step3 = case(step2_pre < 0 AND step3_pre > 0, 1,  step2_pre > 0 AND step3_pre < 0, -1, 1 = 1, 0)
...

자 이렇게 구해진 값을 차트로 표현해 보자.
종근당 주식 같은 경우에는 다음과 같이 매수 타이밍들이 나타나고 있다.

 

XML 형식으로 최종 결과만 공유하도록 하겠다.

새로운 "triple_screen" 이라는 Base 쿼리 추가

...
  </search>
  <search id="triple_screen">
    <query>
         index="kospi" earliest=-360d
          | eval k_code = code
          | rex field=code "^(?&lt;code&gt;\d+).KS"
          | lookup kospi_200 code OUTPUT name   
          | where name = "$stock_code$"
          | sort _time
          | trendline ema60(Close) as ema60, ema130(Close) as ema130, ema5(Close) as ema5
          | eval macd = ema60 - ema130
          | trendline ema45(macd) as signal
          | eval macdhist = macd - signal
          | table _time, Close, macd, signal, macdhist, ema60, ema130, ema5
          | join _time [ | search index="kospi"  earliest=-360d
          | eval k_code = code
          | rex field=code "^(?&lt;code&gt;\d+).KS"
          | lookup kospi_200 code OUTPUT name   
          | where name = "$stock_code$"
          | sort _time
          | streamstats window=14 current=true max("High") as ndays_high, min("Low") as ndays_low
          | eval fast_k = (Close - ndays_low) / (ndays_high - ndays_low) * 100
          | streamstats window=3 avg(fast_k) as slow_d
          | table _time, fast_k, slow_d ]
          | where isnotnull(ema130)
    </query>
  </search>
  <row>
...

 

 

 

지수 이동 평균 대시보드 추가하고 옵션 설정

  <row>
    <panel depends="$stock_code$">
      <title>$stock_code$ 지수이동평균</title>
      <chart>
        <search base="triple_screen">
          <query>
          | table _time, Close, ema130, ema60, ema5</query>
        </search>
        <search base="triple_screen" type="annotation">
          <query>
| table _time, Close ema130, ema60, ema5, macd, macdhist, signal, fast_k, slow_d
| streamstats window=2 list(ema130) as ema130_list, list(ema5) as ema5_list, list(slow_d) as slow_d_list
| eval ema130_pre = tonumber(mvindex(ema130_list, 0)), ema130_cur = tonumber(mvindex(ema130_list, 1))
| eval ema5_pre = tonumber(mvindex(ema5_list, 0)), ema5_cur = tonumber(mvindex(ema5_list, 1))
| eval slow_d_pre = tonumber(mvindex(slow_d_list, 0)), slow_d_cur = tonumber(mvindex(slow_d_list, 1))
| eval step1 = if(ema130_pre &gt; ema130_cur, -1, 1) 
| eval step2_pre = if(slow_d_pre &gt; slow_d_cur, -1, 1) 
| eval step3_pre = if(ema5_pre &gt; ema5_cur, -1, 1)  
| eval step2 = case(step1 &gt; 0 AND slow_d &lt; 20, 1, step1 &lt; 0 AND slow_d &gt; 80, -1, 1=1, 0)
| eval step3 = case(step2_pre &lt; 0 AND step3_pre &gt; 0, 1,  step2_pre &gt; 0 AND step3_pre &lt; 0, -1, 1 = 1, 0)
| table _time, ema130_list, step1, step2, step3
| eval annotation_category1 = case(step2 &gt; 0, "BUY", step2 &lt; 0, "SELL", 1=1, "HOLD")
| eval annotation_category2 = if(step3 !=0 , "TRACE_STOP", "") 
| eval annotation_category = annotation_category1 + "(" + annotation_category2 + ")"
| where step2 != 0
| table _time, step1, step2, step3, annotation_category
          </query>
        </search>
... (옵션 부분 생략)
        <option name="charting.fieldDashStyles">{"Close":"solid", "ema130":"shortDash", "ema60":"dash", "ema5":"dash"}</option>
        <option name="charting.annotation.categoryColors">{"BUY(TRACE_STOP)":"#A9180D", "BUY()":"#A9180D", "SELL()":"#1520A6", "SELL(TRACE_STOP)":"#1520A6"}</option>
      </chart>
    </panel>  <row>
    <panel depends="$stock_code$">
      <title>$stock_code$ 지수이동평균</title>
      <chart>
        <search base="triple_screen">
          <query>
          | table _time, Close, ema130, ema60, ema5</query>
        </search>
        <search base="triple_screen" type="annotation">
          <query>
| table _time, Close ema130, ema60, ema5, macd, macdhist, signal, fast_k, slow_d
| streamstats window=2 list(ema130) as ema130_list, list(ema5) as ema5_list, list(slow_d) as slow_d_list
| eval ema130_pre = tonumber(mvindex(ema130_list, 0)), ema130_cur = tonumber(mvindex(ema130_list, 1))
| eval ema5_pre = tonumber(mvindex(ema5_list, 0)), ema5_cur = tonumber(mvindex(ema5_list, 1))
| eval slow_d_pre = tonumber(mvindex(slow_d_list, 0)), slow_d_cur = tonumber(mvindex(slow_d_list, 1))
| eval step1 = if(ema130_pre &gt; ema130_cur, -1, 1) 
| eval step2_pre = if(slow_d_pre &gt; slow_d_cur, -1, 1) 
| eval step3_pre = if(ema5_pre &gt; ema5_cur, -1, 1)  
| eval step2 = case(step1 &gt; 0 AND slow_d &lt; 20, 1, step1 &lt; 0 AND slow_d &gt; 80, -1, 1=1, 0)
| eval step3 = case(step2_pre &lt; 0 AND step3_pre &gt; 0, 1,  step2_pre &gt; 0 AND step3_pre &lt; 0, -1, 1 = 1, 0)
| table _time, ema130_list, step1, step2, step3
| eval annotation_category1 = case(step2 &gt; 0, "BUY", step2 &lt; 0, "SELL", 1=1, "HOLD")
| eval annotation_category2 = if(step3 !=0 , "TRACE_STOP", "") 
| eval annotation_category = annotation_category1 + "(" + annotation_category2 + ")"
| where step2 != 0
| table _time, step1, step2, step3, annotation_category
          </query>
        </search>
... (옵션 부분 생략)
        <option name="charting.fieldDashStyles">{"Close":"solid", "ema130":"shortDash", "ema60":"dash", "ema5":"dash"}</option>
        <option name="charting.annotation.categoryColors">{"BUY(TRACE_STOP)":"#A9180D", "BUY()":"#A9180D", "SELL()":"#1520A6", "SELL(TRACE_STOP)":"#1520A6"}</option>
      </chart>
    </panel>

위에 보면 쿼리가 2개가 있는 것을 확인할 수 있는데,

아래 쿼리는 type="annotation" 으로 설정 되어 있다.

annotation은 위 차트처럼 특정 이벤트가 발생했을 때 선으로 그어 알려주는 역할을 한다.

이 때 꼭 필요한 필드가 _timeannotation_category 이다.

 

 

오실레이터 차트

...
    <panel depends="$stock_code$">
      <title>$stock_code$ 오실레이터</title>
      <chart>
        <search base="triple_screen">
          <query>
| table _time, fast_k, slow_d</query>
        </search>
    ...  (옵션 부분 생략)
      </chart>
    </panel>
...

 

 

 

MACD (Moving Average Convergence Divergence) 차트

- 여기에서는 매수/매도에 관여하지 않아서 생략했지만 base 쿼리에 구하는 식이 있음.

...
    <panel depends="$stock_code$">
      <title>$stock_code$ MACD</title>
      <chart>
        <search base="triple_screen">
          <query>| table _time, macdhist,  macd, signal</query>
        </search>
... (옵션 부분 생략)
      </chart>
    </panel>

 

최종결과물

1장 내용부터 따라 했거나, 스플렁크를 기존에 잘 알고 있으면 이 부분에 대해서 어렵지 않게 최종 결과물을 얻을 수 있을 것이라고 생각한다.

다음은 ML 을 이용한 매수/매도 분석을 해보도록 하겠다.

BYE~~

 

추가

요렇게 하나만 바 차트로 변경하고 싶으면

XML을 수정하거나

UI에서 차트오버레이 설정을 하면된다.

728x90
반응형