[ Splunk Project ] Splunk로 주식 분석 | Phase 11. 주식 매매 시뮬레이션
본문 바로가기

Splunk/Splunk Project

[ Splunk Project ] Splunk로 주식 분석 | Phase 11. 주식 매매 시뮬레이션

728x90
반응형

🌞 TODAY'S GOAL🌞

    🐇 주식 매매 전략을 세우고,

    🐇 매일 정해진 시간에 주식을 (가상으로) 매매하고,

    🐇 매매한 내용에 대해서 기록하고,

    🐇 알림 설정

 

 

🌳 STEP 0 🌳 readstock 명령을 다음처럼 업데이트

    def generate(self):
        url = self.url.format(self.code)
        self.logger.info("Generating event with code %s" % (self.code))
        with urlopen(url) as doc:
            content = doc.read()
            content = content.decode('utf-8', 'ignore')
            soup = BeautifulSoup(content, 'lxml')
            cur_price = soup.find('strong', id='_nowVal').text.replace(",", "")
            cur_rate = soup.find('strong', id='_rate').text.strip()
            cur_time = soup.find('span', id='time').text.strip()
            #stock = soup.find('title')
            stock_name = self.code

        raw = {"price": cur_price, "rate": cur_rate, "code":stock_name, "time":cur_time }
        yield {'_time': time.time(), '_raw': raw}

 

🌳 STEP 0 🌳 Phase 10의 ML을 카테고리 선정 알고리즘에서
                           값 예측 알고리즘으로 변경

index="kospi" code="$code$.KS" earliest=0
| sort _time  
| `mylag(Close, 10)` 
| table _time, Close, lag_* 
| where isnotnull(lag_9)
| fit StandardScaler "lag_0" "lag_1" "lag_2" "lag_3" "lag_4" "lag_5" "lag_6" "lag_7" "lag_8" "lag_9" "Close" with_mean=true with_std=true into "model_stock_predict_SS_$code$"
| fit RandomForestRegressor "SS_Close" from "SS_lag_0" "SS_lag_1" "SS_lag_2" "SS_lag_3" "SS_lag_4" "SS_lag_5" "SS_lag_6" "SS_lag_7" "SS_lag_8" "SS_lag_9" into "model_stock_predict_RF_$code$"

이를 apply 하는 방법은 "predict_stock_with_ml: ML을 이용한 예측" 를 참조하자.

해당 savedsearch 는 쉘을 만들고 linux cron 을 이용해 매 주 한번 실행해서 모델을 업데이트 한다.

$ vi run_build_model.sh

#!/bin/sh

home=/data/prj/stock

curl -k -u admin:changeme https://localhost:8089/servicesNS/admin/stock/search/jobs/export -d search="| inputlookup my_stock | table code" -d "output_mode=csv" -o "${home}/mystock.csv"

while read line; do
        code=${line}
        if [ ${code} != "code" ]; then
                curl -k -u admin:changeme https://localhost:8089/servicesNS/admin/stock/search/jobs/export -d search="savedsearch make_ml_model%20code%3D$code" >> ${home}/logs/buildmodel_`date '+%y%m%d'`.log
        fi

done < ${home}/mystock.csv

 

 

 

 

🌳 STEP 1 🌳 Key-Value Store 추가

      먼저 시뮬레이터를 만들기 위해서 데이터를 저장하고 업데이트 할 공간을 만들어 준다.

 

Lookup 에서 "key-value" 스토어를 정의해 주었다. (3장 참조)

$ vi $SPLUNK_HOME/etc/apps/stock/local/collections.conf

[my_asset_history]

🐳 my_asset_history 🐳

       통장과 비슷한 역할로 현재 계좌에 있는 현금과 주식 매매시 매매된 내역을 확인할 수 있는 스토어

       🔹 date: 거래 된 시간

       🔹 balance: 계좌 잔액

       🔹code: 주식 코드

       🔹 method: "BUY or SELL" 기록

       🔹 count: 매매한 주식 갯수

       🔹 cost: 매매한 거래 액수

 

🐳 my_stock 🐳

       현재 주식 보유 수량 및 목표 기록

       🔹code: 주식 코드

       🔹 interest: "0 or 1" 관심 종목 플래그

       🔹 share: 보유 주식 수

       🔹 target: 목표 주식 비율 (포트폴리오 생성 시 결정)

 

 

🐳 stock_ml_predict 🐳

      ML을 통한 주식 예측(10장 참조)

       🔹date: 예측 실행한 날짜

       🔹code: 주식 코드

       🔹close: 예측한 시점의 종가

       🔹predict: 예측 가격

 

 

🐳 stock_predict 🐳

      predict 문을 통한 주식 예측 (5장 참조)

       🔹date: 예측 실행한 날짜

       🔹code주식 코드

       🔹price: 예측한 시점의 종가

       🔹predict: 예측 가격

       🔹expected_value: 5일 후 까지 예상되는 가격 변화율

 

 

🐳 stock_statics_predict 🐳

     삼중창을 통한 주식 예측 (9장 참조)

       🔹date: 예측 실행한 날짜

       🔹code주식 코드

       🔹close: 예측한 시점의 종가

       🔹step1: 1 창을 통한 전략

       🔹step2: 2 창을 통한 전략

       🔹step3: 3 창을 통한 전략

       🔹slow_d: 단기 현재 가격 평가 지수

 

 

🐳 today_target 🐳

     오늘 목표 전략

       🔹code주식 코드

       🔹buy: 매매할 목표 주식 수 (BUY/SELL)

       🔹predict: 금일 목표 예상 주가

 

잘 들어간 귀염둥이들 1
잘 들어간 귀염둥이들 2

 

 

🌳 STEP 2🌳 예측 내용 저장 스케쥴 만들기

         savedsearch를 만들고 이를 스케쥴링에 등록해 매일 새로운 예상 결과를 만들도록 한다.

 

 

🐳 stock_predict 🐳

      predict 문을 통한 주식 예측 (5장 참조)

| inputlookup my_stock | map search="
| search index=kospi code=$code$.ks earliest=-365d
| sort _time
| predict "Close" as prediction algorithm=LLT future_timespan=5 upper95=upper95 lower95=lower95
| eventstats latest(Close) as f_close, latest(Date) as f_date, latest(code) as code
| stats latest(f_date) as date, latest(f_close) as Close, latest(prediction) as predict by code
| eval predict = round(predict, 0)
| eval expected_value = round(((predict - Close) / Close)* 100, 2)
"
| rex field=code "^(?<code>\d+).KS"
| eval _key = date + "_" + code
| rename Close as price
| table _key, date, code, price, predict, expected_value
| outputlookup stock_predict append=true override_if_empty=true

이 결과를 predict_stock이라는 제목의 리포트로 저장

 

 

 

 

 

🐳 predict_stock_with_ml 🐳

      ML을 통한 주식 예측(10장 참조)

 

10장에 있는 ML을 카테고리 선정 알고리즘에서 값 예측 알고리즘으로 변경하였다.

MLTK의 Experiment 클릭 > Predict Numeric Fields 클릭 > Create New Experiement

 

Experiment Title 작성 후 Create 클릭

 

index="kospi" code="068270.KS" earliest=0
| sort _time  
| `mylag(Close, 10)` 
| table _time, Close, lag_* 
| where isnotnull(lag_9)

search 창에 위의 검색어 입력 후 돋보기 클릭

 

위와 같이 설정 후 Apply

 

위와 같이 설정 후 Fit Model 클릭 > Save   &&   Show SPL

위에 생성된 SEARCH COMMAND 복사 후 stock 앱의 검색 창에서 아래와 같이 수정 후 검색

| inputlookup my_stock 
| map search="
| search index="kospi" code="$code$.KS" earliest=0 
| sort _time 
| `mylag(Close, 10)` 
| table _time, Close, lag_* 
| where isnotnull(lag_9) 
| fit StandardScaler "lag_0" "lag_1" "lag_2" "lag_3" "lag_4" "lag_5" "lag_6" "lag_7" "lag_8" "lag_9" "Close" with_mean=true with_std=true into "model_stock_predict_SS_$code$" 
| fit RandomForestRegressor "SS_Close" from "SS_lag_0" "SS_lag_1" "SS_lag_2" "SS_lag_3" "SS_lag_4" "SS_lag_5" "SS_lag_6" "SS_lag_7" "SS_lag_8" "SS_lag_9" into "model_stock_predict_RF_$code$"

 

이 부분을 SAVEDSEARCH로 저장한다.

 

이후 설정에서 검색/리포트/알람에 들어가서 쿼리를 다음처럼 수정해

파라미터를 통해 모델을 생성할 수 있도록 만들어 준다.

index="kospi" code="$code$.KS" earliest=0
| sort _time  
| `mylag(Close, 10)` 
| table _time, Close, lag_* 
| where isnotnull(lag_9)
 | fit StandardScaler "Close" "lag_0" "lag_1" "lag_2" "lag_3" "lag_4" "lag_5" "lag_6" "lag_7" "lag_8" "lag_9" with_mean=true with_std=true into "model_stock_predict_SS_$code$"
 | fit RandomForestRegressor "SS_Close" from "SS_lag_0" "SS_lag_1" "SS_lag_2" "SS_lag_3" "SS_lag_4" "SS_lag_5" "SS_lag_6" "SS_lag_7" "SS_lag_8" "SS_lag_9" into "model_stock_predict_RF_$code$"

성공

 

| inputlookup my_stock 
| map search=" 
    | search index=kospi code=$code$.KS earliest=-20d
    | sort _time 
    | `mylag2(Close, 10, true)`
    | table _time, code, Date, Close, lag_*
    | eventstats latest(Date) as evaluate_date
    | where Date = evaluate_date
    | apply model_stock_predict_SS_$code$
    | apply model_stock_predict_RF_$code$
    | rename predicted(SS_Close) as predict
    | append [ | summary model_stock_predict_SS_$code$ | where fields = \"Close\"]
    | eventstats max(\"mean\") as ss_mean, max(var) as ss_var
    | eval predict = predict * sqrt(ss_var) + ss_mean
    | table Date, code, Close, predict"
| where isnotnull(Date)
| rex field=code \"^(?<code>\d+).KS\"
| rename Date as date, Close as close
| eval _key = date + "_" + code
| table _key, date, code, close, predict
| outputlookup stock_ml_predict append=true override_if_empty=true

이 결과를 predict_stock_with_ml이라는 제목의 리포트로 저장

 

 

🐳 predict_stock_with_statics 🐳

      삼중창을 이용한 예측 (9장 참조)

| inputlookup my_stock  |map search="| search index=kospi code=$code$.KS earliest=-360d
    | sort _time
    | trendline ema130(Close) as ema130, ema5(Close) as ema5
    | table _time, Date, code, Close, ema130, ema5
    | join _time [ | search index=kospi code=$code$.KS earliest=-360d
          | 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, Date, code, Close ema130, ema5, slow_d
| streamstats window=2 list(ema130) as ema130_list, list(ema5) as ema5_list, list(slow_d) as slow_d_list
| eventstats latest(Date) as evaluate_date
| where Date = evaluate_date "
| 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 > 20, 1, step1 < 0 AND slow_d > 80, -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)
| rex field=code "^(?<code>\d+).KS"  
| rename Date as date, Close as close
| eval _key = date + "_" + code
| table date, code, ,close,  step1, step2, step3, slow_d
| outputlookup stock_statics_predict append=true override_if_empty=true

이 결과를 predict_stock_with_statics이라는 제목의 리포트로 저장

 

완성된 리포트들

이들은 각각 다음처럼 스케쥴링 한다.

 

 

 

🌳 STEP 3🌳 매매 전략 세우기

 

매매 전략은 결국은 여러 분석을 통해서

몇 주를 얼마에 구매할 것인가를 결정하는 것으로 개인마다 노하우가 각각 다를 것이다.

그리고, 시뮬레이션이 진행되고, 수익률에 따라서 매매전략을 상황에 맞게 바꿔서 적용하게 된다.

 

| inputlookup stock_predict 
| eval _time = strptime(date, "%Y-%m-%d") 
| eventstats latest(date) as f_date 
| where date = f_date 
| rex field=code "^(?<code>\d+).KS" 
| eval p_score = case(expected_value >= 20 , 3, expected_value < 20 AND expected_value >= 10, 2, expected_value < 10 AND expected_value >= 1, 1, 
    expected_value <= -1 AND expected_value > -10, -1, expected_value <= -10 AND expected_value > 20, -2, expected_value <= -20 , -3, 1=1, 0) 
| table date, code, p_score 
| append 
    [| inputlookup stock_ml_predict 
    | eval _time = strptime(date, "%Y-%m-%d") 
    | eventstats latest(date) as f_date 
    | where date = f_date 
    | rex field=code "^(?<code>\d+).KS" 
    | eval expected_value = (predict - close) * 100 / close 
    | eval m_score = case(expected_value >= 10 , 2, expected_value < 10 AND expected_value >= 1, 1, 
        expected_value <= -1 AND expected_value > -10, -1, expected_value <= -10 , -2, 1=1, 0) 
    | table date, code, m_score, close, predict] 
| append 
    [| inputlookup stock_statics_predict 
    | eval _time = strptime(date, "%Y-%m-%d") 
    | eventstats latest(date) as f_date 
    | rex field=code "^(?<code>\d+).KS" 
    | eval s_score = case(step1 > 0 AND slow_d < 20, 5, step1 > 0 AND slow_d < 40, 4, step1 < 0 AND slow_d > 80, -5, step1 < 0 AND slow_d > 60, -4, 1= 1, 0) 
    | table date, code, s_score] 
| stats max(p_score) as p_score, max(m_score) as m_score, max(s_score) as s_score, max(close) as close, max(predict) as predict by date, code 
| eval score = p_score + m_score + s_score 
| append 
    [| inputlookup my_stock] 
| stats max(score) as score, max(share) as share, max(target) as target, max(close) as close, max(predict) as predict by code 
| append 
    [| inputlookup my_asset_history 
    | eval _time = strptime(date,"%Y-%m-%d %H:%M:%S") 
    | stats latest(balance) as balance ] 
| eval asset = close * share 
| eventstats max(balance) as balance, sum(asset) as total 
| eval my_total_asset = balance + total 
| eval rate = (asset / my_total_asset) * 100, share_rate = (asset / total) * 100 
| eval target_count = round(( round(my_total_asset * (target / 100)) - asset ) / close) 
| where isnotnull(code) 
| eval BUY = if(score > 0, target_count * (score /10), 0), SELL = if(score < 0, share * (abs(score)/10), 0) 
| eval BUY = if(BUY > 0, round(BUY), 0), SELL = if(SELL > 0, round(SELL), 0) 
| eval buy = BUY - SELL 
| lookup kospi_200 code OUTPUT name 
| eval predict = round(predict) 
| eval rate = round(rate, 2), share_rate = round(share_rate, 2) 
| eval _key = code 
| table _key, code, name, buy, predict
| outputlookup today_target

 

코드분석

더보기

1️⃣ 우선 예측된 내용을 최신 날짜를 기준으로 key-value 스토어에서 정보를 가져와서 모았다.

| inputlookup stock_predict 
| eval _time = strptime(date, "%Y-%m-%d") 
| eventstats latest(date) as f_date
| where date =  f_date 
...
| append [ | inputlookup stock_ml_predict  
    | eval _time = strptime(date, "%Y-%m-%d") 
    | eventstats latest(date) as f_date
    | where date =  f_date  
...]
| append [ | inputlookup stock_statics_predict 
    | eval _time = strptime(date, "%Y-%m-%d") 
    | eventstats latest(date) as f_date 
...]

 

2️⃣ 각 예측별로 Max 점수로 가중치를 두었다.

       삼중창 방법은 5~4점, 시간 예측은 3~1 점, ML은 2~1 점 사이로 점수를 매기고,
       + 점수이면 BUY, - 점수이면 SELL 을 하도록 한다.

      predict 스코어 결정은
      5일 후 예상되는 가격 변동이 20% 이상이면 3점

      10~20% 이면 2점
      1~10% 이면 1점

      그 외에는 0점이고, 반대도 마찬가지이다.

..
| eval p_score = case(expected_value >= 20 , 3, expected_value < 20 AND expected_value >= 10, 2, expected_value < 10 AND expected_value >= 1, 1,  
                         expected_value <= -1 AND expected_value > -10, -1, expected_value <= -10 AND expected_value > 20, -2, expected_value <= -20 , -3, 1=1, 0)  
...
  • 여기서 expected_value가 가격 변동 값이다.

 

ML 스코어 도 비슷한 형태로 입력된다.

...
    | eval m_score = case(expected_value >= 10 , 2, expected_value < 10 AND expected_value >= 1, 1,   
                         expected_value <= -1 AND expected_value > -10, -1, expected_value <= -10 , -2, 1=1, 0) 
...

 

삼중창 스코어는 다음과 같다.

...
 | eval s_score = case(step1 > 0 AND slow_d < 20, 5, step1 > 0 AND slow_d < 40, 4, step1 < 0 AND slow_d > 80, -5, step1 < 0 AND slow_d > 60, -4, 1= 1, 0) 
...

여러 알고리즘에 나오는 결과를 가지고 스코어를 어떻게 주는게 가장 수익률이 좋은가에 대한 ML 또는 DL을 만드는 것이 사실 가장 중요하고 AI 트레이딩의 핵심이 될 것이다. (이 부분은 스플렁크 scope을 넘어가기 때문에 ...)

 

3️⃣ 이후 각 스코어를 더해서 최종 예측 스코어를 만든다.

...
| stats max(p_score) as p_score, max(m_score) as m_score, max(s_score) as s_score,   max(close) as close, max(predict) as predict by date, code
| eval score = p_score + m_score + s_score
...

 

4️⃣ 포트폴리오 구축 계획을 기반으로 내 자산으로 구매할 수 있는 각 주식의 최대 주식 수를 구한다.

...
| append [ | inputlookup my_stock]
| stats  max(score) as score, max(share) as share, max(target) as target,  max(close) as close, max(predict) as predict   by code
| append [ | inputlookup my_asset_history 
    | eval _time = strptime(date,"%Y-%m-%d %H:%M:%S") 
    | stats latest(balance) as balance ]
| eval asset = close * share
| eventstats max(balance) as balance, sum(asset) as total
| eval rate = (asset / balance) * 100, share_rate = (asset / total) * 100
| eval target_count = round(balance * (target / 100) / close) - share
...

5️⃣ 스코어를 기반으로 주식을 얼마나 살지/팔지를 결정한다.

...
| eval BUY = if(score > 0, target_count * (score /10), 0), SELL = if(score < 0, share * (abs(score)/10), 0)
| eval BUY = if(BUY > 0, round(BUY), 0), SELL = if(SELL > 0, round(SELL), 0)
...

이 쿼리를 기반으로 다음과 같은 전략이 만들어진다.

다른건 모르겠고 Cellirion 팔라는 구만!ㅋㅋㅋ

 

 

 

🌳 STEP 4🌳 매매 실행하기

 

전략이 만들어졌으면 이제 실제 매매를 수행하는 시뮬레이션을 만든다.

사실 복잡한 체결과정도 없고 수수료도 없다.

어짜피 실제로 만들기 위해서는 외부 매매 시스템과 연결해야 하는데

어디까지나 지금 포스팅은 스플렁크 학습용이다.

 

로직을 간단하게 하기 위해서 우선은 한번 수행 될 때 하나의 주식만 거래되게 하였다.

앞에서 저장한 "today_target" 에서 데이터를 읽어오는 것으로 시작한다.

 

| inputlookup  today_target | where buy != 0 | head 1
| map search="| readstock code=$code$ 
    |  spath | eval buy = $buy$ , predict = $predict$ 
    | table code, price, time, buy, predict"
| rex field=time "^(?<online>\d{4}.\d{2}.\d{2}(\s?\d{2}?:\d{2})?)"
| eval online = if(len(time) > 10, 1, 0)
| eval B_DONE = if( buy > 0 AND online == 1, buy, 0) ,  S_DONE = if( buy < 0 AND online == 1, abs(buy), 0)
| eval outcome = price * B_DONE, income = price * S_DONE
| eval buy = buy - B_DONE + S_DONE
| append  [ | inputlookup my_asset_history | eval _time = strptime(date, "%Y-%m-%d %H:%M:%S") | stats latest(balance) as balance  ]
| eventstats max(balance) as balance 
| eval date=strftime(now(),"%Y-%m-%d %H:%M:%S") 
| eval balance =  balance - outcome + income, method = case (B_DONE != 0 , "BUY", S_DONE != 0 , "SELL", 1=1, ""), count = B_DONE - S_DONE, cost = income - outcome
| join code [ | inputlookup my_stock ]
| where online = 1
| eval share = share + B_DONE - S_DONE
| eval _key = code
| outputlookup today_target append=true override_if_empty=true
| outputlookup my_stock append=true override_if_empty=true 
| eval _key = strftime(_time, "%Y%m%d%H%M%S") + "_" + code
| outputlookup my_asset_history append=true

코드분석

더보기

1️⃣ 우선 "map" 구문과 2장에서 만든 "readstock" 명령을 이용해 현재 주식을 가져온다.

    | map search="| readstock code=$code$ 
        | spath | eval buy = $buy$ , predict = $predict$ 
        | table code, price, time, buy, predict"

 

 

2️⃣ 2장에 있는 코드를 약간 업데이트 해서 현재 시간을 가져온다.

...
| rex field=time "^(?<online>\d{4}.\d{2}.\d{2}(\s?\d{2}?:\d{2})?)"
| eval online = if(len(time) > 10, 1, 0)
...

이를 통해 장중인지, 장마감이 되었는지 확인한다.

현재 한글이 파싱이 되지 않아서(다양한 방법으로 테스트 해봤지만 아직 해결이 안된다. ㅠㅠ)

장 중일 때는 시간까지 나오고 장마감이면 날짜만 나오는 특성을 이용하기로 했다.

3️⃣ 거래 내용을 KV_STORE에 저장해준다.

...
| outputlookup today_target append=true override_if_empty=true
| outputlookup my_stock append=true override_if_empty=true 
| eval _key = strftime(_time, "%Y%m%d%H%M%S") + "_" + code
| outputlookup my_asset_history append=true

장 중일 경우 전략에 나온데로 주식을 거래하고 거래된 내용을,

"my_stock", "today_target", "my_asset_history" 에 각각 업데이트 해 준다.

 

아직은  buy가 0이 아닌 주식이 나오지 않아서 주식을 구매할 수도 팔 수도 없는 상황이라 실행되지 않는다.

그럼 그 결과를 보고서로 저장하고 어쨌든 위 쿼리는 저장하고

해당 부분을 alert 으로 만들고 스케쥴을 실행한다.

거래가 1건이라도 발생하면 카톡으로 알람을 보낸다.

 

 

🚫 주의 🚫 

테스트용으로 key-value 스토어를 이용해 각종 정보를 저장하고 있으나,
이 곳에 중요한 내용을 저장하는 것은 바람직해 보이지 않는다.

실수로 "| outpulookup kv-store-name" 을 실행하게 되면 여기 기록된 모든 데이터가 날아 간다.

복구할 방법도 없다.

 

 

그렇기 때문에, 백업으로 저장된 내용들, 다시 복구할 수 있는 내용에 대해서만 기록하는게 좋아보인다. 아니면 주기적으로 파일로 백업을 하던지....

key-value 스토어에 대해서 기본적으로 append=true 옵션이 적용되면 좋을 것 같긴 한데...

만약 key-value 스토어를 이용중이고 업데이트가 필요하다면 꼭 저 옵션을 까먹지 말자........

 

 

 

728x90
반응형