[ Splunk Project ] Splunk로 주식 분석 | Phase 7. 주식 종목 추천
본문 바로가기

Splunk/Splunk Project

[ Splunk Project ] Splunk로 주식 분석 | Phase 7. 주식 종목 추천

728x90
반응형

포트폴리오 구축 방법은 "파이썬 증권 데이터 분석" 책을 참조해서 썼습니다.

 

🌞 Overview 🌞

  • Phase 6의 포트폴리오 알고리즘을 이용해서 Splunk 밖에서 무작위의 종목을 골라서 시뮬레이션을 돌린다.
  • 그 후에 그 중에서 가장 좋은 Sharpe 값을 가지 값을 선택해, Splunk에 다시 저장해서 종목을 추천

 

🌞 핵심 🌞

  1. 파이썬 용 Splunk SDK를 이용해 파이썬에서 Splunk 데이터 가져오기
  2. HEC를 이용해서 application에서 splunk로 데이터 입력하기
  3. Splunk 매크로 이용하기

 

🍀 STEP 1 : Splunk Python SDK 설치🍀

cd /opt/splunk/etc/apps/stock/bin

$ pip3 install splunk-sdk
OR
$ pip install splunk-sdk
OR
$ apt get install splunk-sdk
  • 설치를 어디에다가 꼭 하라는 말은 없었지만
  • 나는 stock 앱 밑에 bin 밑에 이미 splunk_sdk가 있어서 생략해줬다.

 

🍀 STEP 2 : Splunk에서 데이터를 읽을 수 있는 부분 만들기🍀

  • 어디에다가 꼭 하라는 말이 없어서 /opt/splunk/etc/apps/stock/bin 밑에 파일을 생성해줬다.
$ vi splunk_data_reader.py

# Splunk 에서 데이터를 불러오기위한 라이브러리 import
import splunklib.results as results
import splunklib.client as client

import io, os, sys, types, datetime, time
import pandas as pd

"""
스플렁크에 SPL 을 수행해서 데이터를 가져온다. 
@host : 스플렁크 설치 호스트
@port:  스플렁크 REST API 접속 포트 (8089)
@username: 스플렁크 접속 계정
@password: 스플렁크 접속 패스워드
"""
class SplunkDataReader():
    def __init__(self, host, port, username, password):
        self._host = host
        self._port = port
        self._username = username
        self._password = password
        self._service = None

    """
    스플렁크에 접속한다.
    """
    def connect(self):
        self._service = client.connect(host=self._host,
                port = self._port,
                username = self._username,
                password = self._password);

    """
    스플렁크에 SPL 을 수행해서 데이터를 읽어서 결과를 리턴한다. 
    @searchquery_normal:  일반적인 SPL  문
    """
    def execute_query(self, searchquery_normal,
                  kwargs_normalsearch={"exec_mode":"normal"},
                  kwargs_options={"output_mode":"csv", "count":100000}):
        # 스플렁크에 SPL 을 수행하면 해당 job 이 생성된다. 이 Job을 통해 async  하게 데이터를 가져와야 한다. 
        job = self._service.jobs.create(searchquery_normal, **kwargs_normalsearch)

        #  모든 작업이 끝날 때까지 대기하기 위해 Loop 수행 (@TODO 여러 쿼리를 수행해야 하는 경우 Thread 처리해야 함)
        while True:
            # 작업이 실행 준비 되었는지 체크하고  아니면 계속 모니터링
            while not job.is_ready():
                pass
            # 현재 작업이 수행 중에 있고, 작업의 상태를 모니터링 한다. 
            stats = {"isDone":job["isDone"], "doneProgress":float(job["doneProgress"]) *100,
                "scanCount":int(job["scanCount"]), "eventCount":int(job["eventCount"]),
                "resultCount":int(job["resultCount"])}

            status = ("\r%(doneProgress)03.1f%% %(scanCount)d scanned"
                 "%(eventCount)d matched %(resultCount)d results") % stats
            # 작업 상태를 사용자를 위해 출력
            sys.stdout.write(status)
            sys.stdout.flush()

            # 작업 상태가 완료가 되었다면 루프를 빠져나감
            if stats["isDone"] == "1":
                sys.stdout.write("\nDone!")
                break;
            time.sleep(0.5)

        # 작업 결과를 받아오고, 데이터프레임에 저장
        csv_results = job.results(**kwargs_options).read()
        df = pd.read_csv(io.BytesIO(csv_results), encoding='utf8', sep=',')
        # 작업을 제거
        job.cancel()
        # 결과 리턴
        return df

 

  • 간단하다는데 전혀 간단해보이지 않아서 당황스럽다.
  • 그런데 실제 product에서 사용하기 위해서는 예외 처리, 쓰레드 처리 등 더 많은 작업이 필요하다고 한다.

 

 

🍀 STEP 3 : API 연결 설정 (feat. server.conf)🍀

  • $SPLUNK_HOME/etc/system/local/server.conf 파일을 수정해야함.
  • 일단 아래 위치에 server.conf 파일이 있는지 부터 확인해보자.
  • 아, 그 전에 환경변수부터 설정을 좀 해보려고 한다.
# 실행할 계정의 .bashrc를 수정한다.
vi ~/.bashrc 	# 로 문서를 연 후
# 끝 부분에 다음과 같은 내용을 추가해 넣는다.

export SPLUNK_HOME=/opt/splunk		# Splunk 설치 경로
export PATH=${PATH}:${SPLUNK_HOME}/bin

# 내용을 추가해 넣은 다음 source ~/.barshrc를 해준다.

# 잘 설정되었는지 확인하기
echo $SPLUNK_HOME
cd $SPLUNK_HOME

성공

cd $SPLUNK_HOME/etc/system/local

원래 있었구만!

  • 이제 진짜 Splunk와 API를 연결하기 위한 설정을 한 번 해보자.
$  vi $SPLUNK_HOME/etc/system/local/server.conf

[general]
...
allowRemoteLogin = always

  • 오타에 주의하며 잘 작성을 해준다.

 

🍀 STEP 4 : Montecarlo Simulation 코드 입력🍀

  • 이 코드는 Phase 6에서 이미 작성한 것에서 조금 수정을 했다.
  • 마찬가지로 $SPLUNK_HOME/etc/apps/stock/bin/에 넣었다.
$ vi monte_sim.py

import pandas as pd
import numpy as np

class MonteCarloSim():
    def __init__(self, numiter=5000):
        self.counter = numiter

    def fit(self, df):
        """ Compute the Monte Carlo Simulator """
        # df contains all the search results, including hidden fields
        # but the requested are saved as self.feature_variables
        df.sort_values(by=['date'])
        df.set_index('date', inplace=True)
        codes = df.columns
        days = df.shape[0] / len(codes)

        daily_ret = df.pct_change()
        annual_ret = daily_ret.mean() * days
        daily_cov = daily_ret.cov()
        annual_cov = daily_cov * days

        port_ret = []
        port_risk = []
        port_weights = []
        port_sharpe = []

        for _ in range(self.counter):
            weights = np.random.random(len(codes))
            weights /= np.sum(weights)

            returns = np.dot(weights, annual_ret)
            risk = np.sqrt(np.dot(weights.T, np.dot(annual_cov, weights)))

            port_ret.append(returns)
            port_risk.append(risk)
            port_weights.append(weights)
            port_sharpe.append(returns/risk)

        portfolio = { 'Sharpe': port_sharpe, 'Returns' : port_ret, 'Risk': port_risk }
        for i, s in enumerate(codes):
            portfolio[s] = [weight[i] for weight in port_weights]
        output_df = pd.DataFrame(portfolio)
        output_df = output_df[['Sharpe', 'Returns', 'Risk'] + [s for s in codes]]
        return output_df

 

 

🍀 STEP 5 : 실제로 작업을 수행하는 코드 입력🍀

  • main 함수가 포함되어 있다.
  • 소스는 길지만 naive한 소스라서 이해하기는 어렵지 않다고 한다.
  • 마찬가지로 $SPLUNK_HOME/etc/apps/stock/bin/에 넣었다.
  • 아래 코드의 To do 부분을 직접 작성해야한다.
$ vi run_portpolio.py

# 방금 만들 파이썬 파일들 import
from splunk_data_reader import SplunkDataReader
from monte_sim import MonteCarloSim

import pandas as pd
import numpy as np
import argparse
from datetime import datetime, timedelta
import json
import os
import sys
from random import seed
from random import randinte
import time
import requests
import urllib3

# kospi_200. csv 파일을 읽기 위한 경로 설정
CONF_PATH=os.path.dirname(os.path.abspath(__file__))
LOG_PATH=CONF_PATH + '/stock'
# kospi_200.csv 에서 주식 종목 리스트를 읽어온다. 
def getCodeList():
    df = pd.read_csv(CONF_PATH + "/kospi_200.csv", dtype=str)
    return df.code.values

seed(datetime.now()) # 랜덤 시드 초기화
"""
kospi 200 목록에서 종목을 선택한다. 
 @codebook[] :  전체 종목 리스트
 @codes[]:  필수적으로 포함시키고 싶은 종목 목록
 @ num: 총 선택할 종목 수
"""
def genRandomCode(codebook, codes, num):
    random_ranges = len(codebook)
    # num=5   이고 필수적으로 포함시켜향 종목의 수가 2 이면 3개의 종목이 랜덤하게 선택된다. 
    itera = num - len(codes)
    ran_code = [ code for code in codes]

    for _ in range(itera):
        while True:
            # 랜덤하게 종목 코드 하나 선택
            code = codebook[randint(0, random_ranges-1)] + ".KS"
            # 이미 포함 된 코드이면  pass
            if code in ran_code: 
                pass
            # 새로운 코드면 추가
            else:
                ran_code.append(code) 
                break

    return ran_code

"""
스플렁크에서 필요한 데이터를 읽어온다. 
 @reader:  스플렁크 접속 객체
 @codes[]:   주식 종목 리스트
 @days:  가져올 데이터의 날 수 
"""
def readData(reader, codes, days=180):
    #스플렁크 SPL 문
    splunk_query="""
        search index=kospi {codes} earliest={days}
        | rename Date as date
        | chart latest(Close) as value by date, code
    """
    code_str = " OR ".join(codes)
    days_str = "-" + str(days) + "d@"

    # SPL을 수행해서 데이터를 가져온다. 
    df = reader.execute_query(splunk_query.format(codes=code_str, days=days_str))
    return df

"""
HEC 를 통해 스플렁크에 결과 데이터를 저장한다. 
 @host: 스플렁크 접속 서버
 @token: 스플렁크에서 발급한 접속 토근
"""
authToken = "XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"  # @TODO 나의 토큰으로 대체
splunkhost = "localhost" # @TODO 나의 서버 주소로 대체
def splunkHec(host, token, data):
    url="https://" + host + ":8088/services/collector/event"
    authHeader = {'Authorization': 'Splunk ' + token}
    payload = {}
    payload.update({"index":"monte"})   # "monte"  라는 인덱스에 저장
    payload.update({"sourcetype":"_json"})  # "_json" 소스타입으로 저장
    payload.update({"source":"monte_sim.py"}) # "monte_sim.py" 소스로 저장
    payload.update({"event": data}) # "저장할 데이터 메시지"
    r = requests.post(url, headers=authHeader, json=payload, verify=False)  # 스플렁크로 데이터 전송

"""
반복해서 임의의 코드 종목을 선택하여 최상의 종목 조합을 찾음
 @ reader: 스플렁크 접속 객체
 @ codebook: 주식 종목 리스트
 @  required_codes: 반드시 포트폴리오에 포함해야할 코드 목록
 @ num: 포트폴리오를 구성할 주식 종목의 수
 @ repeat: 반복 횟수 
"""
def findBestPortpolio(reader, codebook, required_codes, num, repeat=10000):
    # 몬테카를로 시뮬레이터 객체 생성
    sim = MonteCarloSim()
    # repeat  만큼 반복해서 주식 종목 선택 수행
    for idx in range(repeat): 
        # 랜덤한 주식 종목을 가져온다. 
        c = genRandomCode(codebook, required_codes, num)
        # 선택된 주식 종목에 대해서 스플렁크에서 해당 데이터를 가져온다. 
        df = readData(reader, c, days)
        # 몬테카를로 시뮬레이터를 수행한다. 
        monte_df = sim.fit(df)
       # Sharpe 값이 가장 좋은 항목을 선택한다. 
        monte_max = monte_df.loc[monte_df['Sharpe'].idxmax()]

        # 선택된 데이터에 대해서 스플렁크에 전달할 데이터 모양을 만든다. 
        dic = {}
        codes = []
        rates = []
        for key in monte_max.keys(): 
            if key not in ['Sharpe', 'Returns', 'Risk']:
                codes.append(key)
                rates.append(monte_max[key])
            else:
                dic[key] = monte_max[key]   
           
        dic['idx'] = idx
        dic["code"] = codes
        dic["rate"] = rates
        dic["date"] = datetime.now().strftime("%Y-%m-%d")

        data = json.dumps(dic)  
        # 스플렁크로 해당 데이터를 보낸다. 
        splunkHec(splunkhost, authToken, data)

# 인자로 전달되는 코드의 유효성 체크
def verifyCode(required):
    codes = required.split(",")
    codes = [ v + ".KS" if len(v)==6  else v for v in codes]
    return codes;

if __name__ == "__main__":
    # 전달받는 파라미터 설정
    parser = argparse.ArgumentParser()
    parser.add_argument('--days', help='days help')
    parser.add_argument('--num', help='num help')
    parser.add_argument('--required', help='required help')
    parser.add_argument('--repeat', help='repeat help')

    args = parser.parse_args()
    
    if args.days:
        days = int(args.days)
    else:
        days = 180

    if args.num:
        num = int(args.num)
    else:
        num = 5

    if args.repeat:
        repeat = int(args.repeat)
    else:
        repeat = 1000

    codes =[]
    if args.required:
        codes = verifyCode(args.required);

    # 주식 종목 리스트 읽기
    codebook = getCodeList()
    # 스플렁크 접속 객체 생성 @TODO 나의 접속 정보로 수정
    reader = SplunkDataReader("localhost", 8089, "my_account", "my_password")
    # 스플렁크에 접속
    reader.connect()
    #  포트 폴리오 검색 수행
    findBestPortpolio(reader, codebook, codes, num, repeat)
  • 여기까지 했다면 splunk를 restart 해준다.(.conf를 수정한 이력이 있다면 splunk 재시작해야한다.)

 

🌞 다음과정 overview 🌞

        ➜ "monte" 인덱스 생성: 1부 참조

        ➜ "HEC " 토큰 생성

 

 

🍀 STEP 6 : "monte" 인덱스 생성🍀

  • 로그인 후 stock 클릭 후 설정의 인덱스 클릭!

 

 

 

 

🍀 STEP 7 : "HEC" 토큰 생성🍀

  • 설정의 데이터 입력 클릭!

 

 

 

  • "HTTP Event Collector"의  "새로 추가" 클릭

 

 

 

  • 이름에 "stock_portpolio" 입력 후 "다음" 클릭

 

 

  • 앱은 "stock" 인덱스는 "monte"로 설정하고 "검토(review)" 클릭

(좌) 네이버 카페 화면        (우) 내 화면

  • 나는 stock 앱으로 들어와서 설정하려고 해서 그런지 app 선택화면이 없었다.

 

 

  • 검토에 보니 자동으로 stock으로 설정되어 있는 걸 확인할 수 있었다.
  • 제출을 눌러 추가 완료!!

 

  • 토큰을 복사해서 파이썬 파일에 붙여넣어서 사용할 수 있다.

4a6d9b80-dc0f-4492-9e81-7f5c365c6199

 

 

  • 한번도 HEC 토큰을 생성하지 않았으면 HTTP Event Collector에서 "전역 설정"을 해야한다.

  • "전역 설정" 클릭!

 

  • 모든 토큰 사용 가능(Enable) 클릭
  • SSL 사용 (Enable SSL) 

모든 준비가 끝났다.

 

🍀 STEP 8 : run_portpolio.py 실행🍀

$ python3 run_portpolio.py --days 350 --num 5 --repeat 1000
  • $SPLUNK_HOME/etc/apps/stock/bin 에서 수행했다.
  • 오류가 발생했다.
더보기
  • run_portpolio.py의 13번째 줄을 보자!

 

  • 줄번호가 보이게 하기 위해서  ESC 누른 후 :set number 또는 :set nu 을 입력한다.
  • 가독성이 증가했다.
  • randinte가 아니라 randint다 변경해주자
root@splunkenterprise:/opt/splunk/etc/apps/stock/bin# python3 run_portpolio.py --days 350 --num 5 --repeat 1000

/opt/splunk/etc/apps/stock/bin/run_portpolio.py:26:
DeprecationWarning: Seeding based on hashing is deprecated
since Python 3.9 and will be removed in a subsequent version.
The only supported seed types are: None, int, float, str, bytes, and bytearray.
  seed(datetime.now()) # 랜덤 시드 초기화
  
Traceback (most recent call last):
  File "/opt/splunk/etc/apps/stock/bin/run_portpolio.py", line 168, in <module>
    codebook = getCodeList()
  File "/opt/splunk/etc/apps/stock/bin/run_portpolio.py", line 23, in getCodeList
    df = pd.read_csv(CONF_PATH + "/kospi_200.csv", dtype=str)
  File "/usr/local/lib/python3.10/dist-packages/pandas/io/parsers/readers.py", line 912, in read_csv
    return _read(filepath_or_buffer, kwds)
  File "/usr/local/lib/python3.10/dist-packages/pandas/io/parsers/readers.py", line 577, in _read
    parser = TextFileReader(filepath_or_buffer, **kwds)
  File "/usr/local/lib/python3.10/dist-packages/pandas/io/parsers/readers.py", line 1407, in __init__
    self._engine = self._make_engine(f, self.engine)
  File "/usr/local/lib/python3.10/dist-packages/pandas/io/parsers/readers.py", line 1661, in _make_engine
    self.handles = get_handle(
  File "/usr/local/lib/python3.10/dist-packages/pandas/io/common.py", line 859, in get_handle
    handle = open(

FileNotFoundError: [Errno 2] No such file or directory: '/opt/splunk/etc/apps/stock/bin/kospi_200.csv'
  • 이런 에러 메시지가 나왔다.

 

  • 첫번째는, 일단 버전 문제가 확실히 있는 것 같다.
  • DeprecationWarning:
    Seeding based on hashing is deprecated since Python 3.9 and will be removed in a subsequent version.
    The only supported seed types are: None, int, float, str, bytes, and bytearray.
    • 해싱 기반 시딩은 파이썬 3.9 버전 부터 사용되지 않고 제거되었다고 한다.
    • 내껀 3.10.6 버전임 (python3 -version으로 Linux환경에서 간단하게 확인 가능)
    • 지원되는 유일한 시드 유형은 None, 정수, 소수, 문자열, bytes, bytearray라고 한다. 
  • 두번째는, kospi_200.csv 파일이 /opt/splunk/etc/apps/stock/bin/kospi_200.csv 에 없는 것이 문제다.

 

🔔 첫번째 오류 해결하기

  • 보니 입력되어 있는 형태가 timedate() 형태라서 문제가 발생한 것 같다.
  • 근데 수정 없이 해결되었음... 할 수 없다.. 정말.. 자동으로 string으로 전환된걸까?
  • 아시는 분 답 좀..

 

🔔 두번째 오류 해결하기

1️⃣ 어디에 kospi_200.csv 파일이 있는지 확인한다

# find / -name kospi_200.csv

/home/splunk_is/kospi_200.csv
/opt/splunk/etc/apps/stock/lookups/kospi_200.csv
/opt/splunk/etc/users/admin_ahn/stock/lookups/lookup_file_backups/stock/admin_ahn/kospi_200.csv
/opt/splunk/bin/kospi_200.csv

2️⃣ 편한 곳에서 복사해온다. 

cp /opt/splunk/bin/kospi_200.csv .

성공!

 

 

정상적으로 수행된다면 다음과 같이 이벤트가 들어오는 것이 확인된다.

 

⭐️ 이벤트 내용 설명 ⭐️

  • Sharpe : Sharpe 값
  • Returns : 기대 수익
  • Risk : 위험도
  • idx : 반복된 횟차수
  • code : 선택된 코드 목록
  • rate : 목록에 대한 구성 비율
  • date : 수행된 날짜

 

{
    "Sharpe": 0.49486577087259837, 
    "Returns": 0.09653658218825922, 
    "Risk": 0.19507629719072297, 
    "idx": 999, 
    "code": ["008770.KS", "010620.KS", "011170.KS", "024110.KS", "192820.KS"], 
    "rate": [0.027731564480966874, 0.056647087277405216, 0.5564385158412494, 0.0045145824210048, 0.35466824997937374], 
    "date": "2021-01-11"
}
  • 위의 데이터는 하나의 event의 _raw data다.
  • 내가 하고 싶은 것은 위 데이터의 code와 rate를 독립된 열로 펼치고 싶다.
  • 배열형 데이터를 독립된 열 데이터로 만들기 위해 사요되는 것은 "mvexpand"라는 명령이다.

 

index="monte" 
| head 1 
| mvexpand code{} 
| rename code{} as code 
| table date, idx, code, Sharpe, Returns, Risk, rate{}

  • 위에처럼 code 는 열로 확장이 되었는데 rate 가 문제이다.
  • 이 두 개를 모두 짝을 맞춰서 확장 시키기 위해서는 약간의 꼼수가 필요하다.

 

index="monte" 
| head 1 
| rename code{} as codes
| rename rate{} as rates
| eval fields_value=mvzip(codes, rates)
| mvexpand fields_value
| eval fields_value = split(fields_value, ",")
| eval code = mvindex(fields_value, 0)
| eval rate = mvindex(fields_value, 1)
| rex field=code "^(?<code>\\d+).KS" 
| lookup kospi_200 code OUTPUT name 
| table date, idx, code, Sharpe, Returns, Risk, rate

코드 분석은 아래 포스팅을 참고하자.

 

 

2023.06.23 - [Splunk/Splunk Search] - [ Splunk Search ] 주식 프로젝트에 사용된 명령어 분석하기

 

[ Splunk Search ] 주식 프로젝트에 사용된 명령어 분석하기

🔆 오늘의 목표 🔆 🪨 아래의 배열형 데이터를 독립된 열 데이터로 만들자!! { "Sharpe": 0.49486577087259837, "Returns": 0.09653658218825922, "Risk": 0.19507629719072297, "idx": 999, "code": ["008770.KS", "010620.KS", "011170.KS"

authentic-information.tistory.com

 

 

그러나 매번 이런 작업을 하기는 번거롭기 때문에 이름 매크로를 이용해 만들어 놓자.

[ STEP 1 ] 설정 > 고급 검색

 

[ STEP 2 ] 검색 매크로 > 새로 추가

 

 

[ STEP 3 ] 설정

  • 대상 앱:  "stock" 으로 설정한다.
  • 두 개의 필드 이름을 변수로 지정해서 넘길 예정이기 때문에,
    매크로 이름 "my_mvexpand2"라는 이름 뒤에 명시적으로 2개의 인자를 넘기겠다고 "(2)" 를 추가한다.
  • 위에 있는 것처럼 "codes" 를 arg1 로 "rates" 를 arg2 로 전달 받을 것이고, 이는 "Arguments" 부분에 정의되어 있다.
  • 그리고 "정의" 부분에는 위에서 작성한 SPL 일부를 넣고 "codes" 와 "rates" 부분만 전달 받은 변수명으로 치환해 준다.
  • 이렇게 저장하고 나면 다음 처럼 매크로를 사용해서 똑같이 수행할 수 있고, 훨씬 SPL인 간단해진 것을 확인할 수 있다.

 

index="monte" 
| head 1 
| rename code{} as codes
| rename rate{} as rates
| `my_mvexpand2(codes, rates)`
| rex field=code "^(?<code>\\d+).KS" 
| lookup kospi_200 code OUTPUT name 
| table date, idx, codes, Sharpe, Returns, Risk, rates

  • 이제 마지막으로 다음 SPL을 수행해서 우리가 수행한 베스트 포트폴리오 추천 종목을 확인하자
index="monte"
| rename code{} as codes, rate{} as rates
| eventstats latest(date) as latest_date
| where date = latest_date
| table date, Sharpe, codes , rates
| sort -Sharpe limit=5
| streamstats count 
| `my_mvexpand2(codes, rates)`
| rex field=codes "^(?<code>\d+).KS"
| lookup kospi_200 code OUTPUT name
| table count, Sharpe, codes, rates, name

Sharp값이 가장 높은 최상위 5개를 선택해서 출력한다.

이를 Phase 6에서 만든 포트폴리오 대시보드에 저장하고 색을 조정하면 완성!

 

 

완성!

 

이제 자동화를 위해서 해당 추천 종목에 대해서 스케쥴을 걸어서,

해당 항목들을 나의 관심 항목 주가에 추가하고,

추가된 항목에 대해서 매도/매수 타이밍을 결정하는 일들의 작업들을 수행할 예정이다.

728x90
반응형