맨위로버튼이미지

1. 왜 scikit-learn인가?

scikit-learn을 제가 처음 접한 때는 아직 정식버전을 내놓지 않은 0.x 버전이었습니다. 또한 서점에 나와 있는 책이나 블로그의 내용들도 그러한 이유로 0.x버전의 책이었습니다. 현시점에 scikit-learn은 1.x 버전을 내놓은 상태입니다. 먼저 scikit-learn은 tensflow와 자신의 영역을 분명히 하고 있으며 가는 방향도 다르다고 선언을 했습니다. tensflow가 고성능의 고용량의 데이터에 대한 데이터 분석 및 학습을 목표로 한다면 tensflow를 사용할 수 없는 또는 시스템이 지원하지 않는 영역에서의 분석 및 학습이 scikit-learn의 영역임을 분명히 했습니다. 제가 개발하려는 환경 또한 위의 환경입니다. 저의 개발 및 운영 시스템으로 오라클 클라우드에서 제공받고 있는 평생 무료 서버를 사용하고 있습니다. 그 상황에서 데이터를 얻어서 TA-Lib로 매매데이터를 분석하고 매수 목표나 매도 목표를 찾는 시스템을 운영하고 있습니다. 따라서 당연히 GPU는 사용할 수 없으며 tensflow나 keras의 고급 머쉰 러닝은 사용할 수 없습니다. 하지만 이러한 제약으로 인하여 scikit-learn을 사용하려는 것은 아닙니다. scikit-learn의 구조는 매우 단순한 구조를 가집니다. 데이터의 획득, 학습 데이터와 테스트 데이터의 분류, 학습(fit), 학습 데이터 평가(score), 예측(predict)의 구조가 전부이며 각각의 구조가 scikit-learn의 함수들 각 하나로 이루어져 있습니다. 제가 초기에 scikit-learn을 배우고 적용하는 것이 힘들었던 이유가 바로 위의 내용이기도 합니다. 머쉰 러닝이 간단해도 이렇게 간단할 수 있을까? 하는 의심이 학습을 방해한 이유 중 하나였으며 너무 간단하다 보니 사실 책을 쓰시는 분이나 블로그들의 글도 많지 않을뿐더러 서점의 책도 별로 없는 상태였습니다. 그리고 머쉰 러닝 하면 그 당시 대부분이 tensflow에 대하여만 생각하는 분위기라 scikit-learn을 아는 사람도 별로 없었죠.

2. 목표를 분명하게

제가 하려는 건 분명합니다. 기존에 수집 및 TA분석중인 데이터를 scikit-learn로 재분석하여 가장 데이터에 맞는 scikit-learn의 분석모델을 찾는 것이며 데이터를 최적화하는 것입니다. 위의 왜 scikit-learn인가에서도 아셨겠지만 scikit-learn을 배우는 것은 어렵지 않습니다. 공식 사이트의 연습 문제만 풀어도 내용을 알 수 있지만 제가 중요하고 어렵다고 본 것은 scikit-learn이 아닙니다. 기존의 데이터를 scikit-learn의 데이터로 변환한 다음 모델의 정확도를 지속적으로 높일 수 있으면서 성능이 뛰어나며 실제 매매 엔진을 방해하지 않는 것입니다. 보통 매매 엔진은 1분 데이터를 수집하는 경우 데이터 수집과 매수, 보유, 매도를 결정하고 실행하는 데까지 모두 1분 안에 끝내야 합니다. 이 1분이 초과된다면 그 다음은 3분 데이터를 사용한 매매 엔진을 다시 개발해야겠죠. 각각의 분석의 정확성에 따라 기존의 매매전략을 다시 만들어야 할 수도 있으며 완전히 새로운 방식을 고민해야 할 수도 있을 것입니다.

3. scikit-learn의 학습이란?

scikit-learn의 학습에 대하여 생각하기전에 먼저 일반적인 딮런닝을 생각해 봐야겠네요.
y = a1 * x1 + a2 * x2 + a3 * x3 + ... + an * xn + b
위의 식의 y는 우리가 딮런닝 예측으로 구하려고 하는 값입니다. 딮런닝의 예측에는 분류(Classify)와 회귀(Regress)가 있습니다. 우리가 해결해야 하는 문제가 분류인지 회귀인지를 스스로 판단하고 알아야 합니다. 분류는 붓꼿의 품종을 예측하는 것처럼 집합의 범위가 적으며 정해져 있는 경우일 겁니다. 각 투수별 구종을 학습하여 야구 선수가 다음번에 어떤 공을 떤질지를 딮런닝으로 학습한다면 이런 경우는 범위는 조금 많지만 분류에 해당한다고 할 수 있습니다. 회귀는 무엇일까요? 주가를 예측하는 딮런닝 모델이 있다고 한다면 주가와 주변의 환경변수를 이용하여 주가를 예측할 수 있을 것입니다. 주가를 예측하기 전에 하나의 곡선(또는 다차원 평면)을 그릴 것입니다. 그리고 그 곡선에 학습에 사용한 환경변수를 대입하여 하나의 값을 구할 수 있을 것입니다. 위의 곡선을 구하는 것을 회귀라 하며 주가의 예측은 회귀에 의해서 그려진 곡선에 각각의 좌표를 이루는 값을 대입한 결과가 예측한 주가가 될 것입니다. 그런데 이 곡선은 우리가 수학 시간에 배웠던 2차원 직선이나 3차원의 곡선이 아닙니다. 환경변수의 수가 n이라고 하면 n차식의 곡선인 거죠 그리고 이 n차식에 들어가는 변수가 우리가 학습에 이용한 환경변수( ※ DB 테이블로 치면 컬럼에 해당할 것입니다.)입니다. 위의 식이 바로 이러한 딮런닝을 표현한 식입니다. 아마도 이 글을 읽는 순간 갑자기 어려워하시는 분들이 있을 수 있으나 우리가 해야 하는 일은 우리가 구하려는 값이 분류에 있는지 회귀에 있는지 잠깐 생각하는 게 전부입니다. 주가나 필라델피아 주택 가격이나 금의 시세나 또는 달러의 시세를 예측하는 프로그램이 있다면 이 것은 회귀일 가능성이 높습니다. 그렇다면 이런 문제는 어떨까요? 근 미래의 주가가 오를지 내릴지를 맞추는 딮런닝 프로그램이 있다면 이것은 분류일까요? 회귀일까요? 당신은 이 문제를 어떻게 접근하실 건가요?

4. 데이터 수집

한국투자신탁의 뱅키스API나 upbit API를 이용하여 1분 데이터를 수집하는 프로그램을 작성할 수 있을 것입니다. 사실 꼭 1분일 필요는 없습니다. 30분 데이터로도 훌륭한 데이터 수집 및 분석 프로그램을 만들 수 있습니다. 사실 시간은 의미 없는 눈속임일 수 있습니다. 저는 이렇게 가져온 데이터를 TA-lib(https://mrjbq7.github.io/ta-lib/install.html)를 사용하여 분석후 이용하고 있습니다. 하지만 한 가지 문제를 가지고 있습니다. TA(기술적 분석)은 사실문제를 가지고 있습니다. 그것은 바로 분석된 데이터의 후행성입니다. 예를 들면 60분 ma(이동평균선)을 보면 7~10분 정도 먼저 주가가 올라가고 나서 이동평균선이 상향으로 움직인다는 것을 알 수 있습니다. 반대로 주가가 빠질 때는 주가가 먼저 빠진 후 이동평균선이 하향으로 움직이는 것을 볼 수 있습니다. 이것은 TA 데이터가 거의 대부분이 과거 데이터의 평균값을 이용하고 있으며 미래의 예측이 아니기 때문에 발생하는 문제입니다. 주가 차트의 가장 저점은 사실 그 지점에서 반등한다면 최고의 이익을 선사할지도 모르지만 그 지점마저 무너진다면 우리에게 지옥의 맛을 보여줄 자리 일 수도 있습니다. 더는 내려갈 곳이 없다고 생각하는 것은 인간의 어리석음일 수 있습니다. 가치 투자자들은 자신의 판단에 맡겨 이 정도 값이면 더 내려도 괜찮아라고 생각하겠지만 매매 프로그램 자체가 이미 가치 투자가 아니므로 이런 매매 프로그램에게 그러한 자리는 아주 치명적일 수 있습니다. 모든 상품 및 현물은 성장기 확장기 감쇠기 쇠퇴 기를 반복하지만 거짓된 상품은 확장기에서도 갑자기 가치가 0에 수렴할 수도 있습니다. 그런 경우까지 매매 프로그램이 감지하는 경지까지 아직 딮런닝 프로그램들이 발전하지 않았습니다. 딮런닝 중의 한 부야에는 제한적 학습이라는 분야가 있습니다. 제한적 학습은 많은 경험과 함께 그런 제한적 학습을 어떻게 적용할 것인지를 알아야 될 뿐 아니라 scikit-learn의 학습으로는 불가능할 수 있습니다. scikit-learn은 0.x 버전 때보다는 지금은 엄청나게 많이 발전했지만 scikit-learn은 한 가지 단점이 있습니다. 다른 딮런닝 모듈보다는 확실히 사용하기 쉽지만 다른 일부 엔진처럼 각각의 모듈을 customize 하기에는 제한적이라고 할 수 있습니다. 그런 customize가 가능하려면 여러분 들은 모든 분류엔진과 회귀 엔진을 직접 구현할 수 있을 정도의 실력이 필요하지 않을까요?

async def read_all(sqlcon) -> pd.DataFrame:
    try:
        cursor = await sqlcon.cursor(aiomysql.cursors.DictCursor)
        str_query = """select 
            `time`, start, close, high, low, volume, sma1200, sma1200_degrees, wma1200, wma1200_degrees
            from %s""" % table_name
        await cursor.execute(str_query)
        data = await cursor.fetchall()
        datadf = pd.DataFrame(data)
        return datadf
    except Exception as ex:
        exc_type, exc_obj, exc_tb = sys.exc_info()
        fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
        logger.error("read from db exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))
    finally:
        await cursor.close()

async def trade_proc():
	pool = await aiomysql.create_pool(host="호스트ip 또는 이름", user="root", password='pass', db='dbname')
	async with pool.acquire() as sqlcon:
		df = await read_all(sqlcon)
		sqlcon.close()
        df['futher'] = df.close.shift(-5)
        df['futher_mid'] = df.close.shift(-5) - df.close
        df['predict'] = (df.close.shift(-5) - df.close).apply(lambda x: (-1 if x < 0 else (0 if x == 0 else 1))) 
        df = df.dropna()

        await regressor(df)  #실제 학습 함수
async def regressor(df):
    X_data = df.drop(['futher', 'futher_mid', 'predict'], axis=1)
    X_new = df.drop(['futher', 'futher_mid', 'predict'], axis=1).tail(5).head(1)
    X_clct = df.tail(5).head(1)
    y_data = df['futher']

    X_train, X_test, y_train, y_test = train_test_split(X_data, y_data, random_state=0)
    mlp = MLPRegressor(solver='lbfgs', max_iter=500, random_state=0)
    mlp.fit(X_train, y_train)  # 학습
    
    logger.info("MLP 훈련 세트의 정확도: {:.2f}".format(mlp.score(X_train, y_train)))
    logger.info("MLP 테스트 세트의 정확도: {:.2f}".format(mlp.score(X_test, y_test)))
    joblib.dump(mlp, 'mlp.jdmp')

if __name__ == '__main__':
    asyncio.run(trade_proc())

기존 DB의 데이터로만은 scikit-learn의 학습이 불가능할 수 있습니다. 학습에는 x1...xn에 들어가는 기본 필드도 필요하지만 답에 해당하는 y_data도 필요합니다. 이런 데이터는 pandas를 이용하면 쉽게 구할 수 있습니다. pandas는 기존의 관계형 DB의 속성과 함께 nosql의 속성 및 큐빅 DB에서 볼 수 있는 DB의 세로(칼럼) 간 연산도 가능합니다. pandas의 이러한 기능은 scikit-learn의 학습 데이터를 아주 쉽게 만들 수 있습니다. 위의 예제 소스는 기존의 DB를 이용하여 scikit-learn 학습 데이트를 만드는 예제입니다. regressor함수는 회귀 학습을 위한 예제입니다. 현재의 데이터로 미래의 값을 예측하기 위해서 과거의 데이터의 과거 시점 5분 후의 데이터를 이용하여 further와 further의 평균, 그리고 예측값을 만들어 y_data를 만든 다음 X_data에서는 해당 값을 제거하여 학습하는 것을 볼 수 있습니다. 여기서 주의할 점은 scikit-learn의 학습능력은 생각보다 정확하여 예측에 사용된 값을 그대로 넣고 학습을 할 경우 학습률 100%의 데이터를 생성하게 됩니다. 물론 실전능력은 떨어지게 됩니다. 학습 시 해당 예측값까지 이용하여 예측값 공식을 만들기 때문에 해당 필드의 가중치가 최고가 되어 결국은 학습이 의미가 없어지게 됩니다.

5. 지속 가능한 학습이란?

지속 가능한 학습을 위해서는 자료 수집과 학습 및 실전예측 프로그램의 설계가 중요합니다. 자료 수집 및 학습이 배치(BATCH) 작업화 되어야 하고 학습된 모델은 파일로 저장되어 실측 프로그램에서 사용되어야 합니다. 자료 수집 및 학습은 매우 큰 시간이 필요하므로 자료 수집과 학습을 실측 프로그램과 분리하여 운영해야 합니다. 아래 섹션 6에서는 모델을 저장하여 이용하는 방법을 소개하고 있습니다. scikit-learn은 모델을 학습하고 학습된 모델을 파일로 저장하고 저장된 파일 자체를 로딩하여 파이썬의 함수로 사용할 수 있는 모듈을 제공하고 있습니다.

6. 모델의 저장 및 학습모델로 예측하기

scikit-learn은 학습한 모델을 python liberary로 분석하여 파일로 저장하는 기능과 그 파일을 불러와서 예측을 하는 함수를 제공합니다. 많은 양의 데이터를 학습하면 모델의 정확도는 당연히 증가할 겁니다. 하지만 학습에 소요되는 시간은 엄청나게 길어집니다. 만약 매번 학습을 돌리고 그 학습 데이터를 이용하여 예측을 진행한다면 제가 2번 섹션에서 이야기한 1분의 시간은 넘어가 버릴 겁니다. 그렇다면 scikit-learn의 학습은 아무런 소용이 없습니다. 하지만 scikit-learn은 joblib.dump라는 함수를 제공합니다.

from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
import joblib

X_train, X_test, y_train, y_test = train_test_split(X_data, y_data, random_state=0)
knn = KNeighborsClassifier(n_neighbors=3)
knn.fit(X_train, y_train)   #학습
logger.info("KNN 훈련 세트의 정확도: {:.2f}".format(knn.score(X_train, y_train)))
logger.info("KNN 테스트 세트의 정확도: {:.2f}".format(knn.score(X_test, y_test)))
    
prediction = knn.predict(X_new)
logger.info("KNN 예측 결과:%d", prediction)
joblib.dump(knn, 'knn.jdmp') #모델 저장

위의 소스코드가 사실은 scikit-learn의 전부 입니다. tensflow의 keras 보다는 월등히 쉬운 코딩이 가능합니다. 6라인을 보시면 아시겠지만 scikit-learn의 모든 모델은 클래스 화해서 모듈화 되어 있습니다. 위에 이야기한 것처럼 scikit-learn이 문제가 아니라는 것을 여러분은 느끼실 겁니다. scikit-learn의 소스는 5라인부터 끝까지가 전부입니다. 모든 클래스는 분류이면 Classifier가 붙을 것이고 회귀이면 Regressor이 붙을 것입니다.

mlp = MLPRegressor(solver='lbfgs', max_iter=500, random_state=0)
mlp.fit(X_train, y_train)  # 학습
    
logger.info("MLP 훈련 세트의 정확도: {:.2f}".format(mlp.score(X_train, y_train)))
logger.info("MLP 테스트 세트의 정확도: {:.2f}".format(mlp.score(X_test, y_test)))

joblib.dump(mlp, 'mlp.jdmp') #모델 저장

위의 소스는 다층신경망 회귀를 이용하여 학습하고 그 학습된 모델을 모듈화 하여 mlp.jdmp파일로 저장하는 소스입니다.
여기서 중요한 부분은 그냥 단순히 학습된 모델을 파일로 저장하는 것이 아니라는 것이다. 저장된 파일을 로딩하면 바로 파이썬 함수로 사용이 가능합니다. 많은 데이터를 학습하려면 몇분에서 많게는 몇시간의 시간이 필요할 수도 있지만 학습이 끝나면 실제 데이터로 예측을 수행하는데는 그리 길지 않은 시간이면 됩니다.

mlp_from_model = joblib.load('mlp.jdmp') #파일로 저장된 모델을 파이썬 함수로 로딩
prct = mlp_from_model.predict(X_new) #로딩된 모델을 실제 값으로 예측
print("예측값:" + prct)   #예측 값은 prct변수에 들어 간다. prct값은 int나 double이 아닌 Dict type입니다

실제 운영되는 실행파일에 위의 내용이 있으면 될 것입니다. 학습에 오랜시간이 걸리겠지만 predict함수가 실행되는 데는 단 몇 초면 끝입니다. 위의 예제는 실제 사용 가능한 다층 신경망 회귀와 근접 이웃 분류의 소스 코드입니다.

7. 글을 마치며

프로그래머가 위의 소스를 보고도 딮러닝 프로그램을 작성하지 못하는 이유는 뭘까요? 사실 위의 소스는 scikit-learn학습을 구현한 소스 전체나 마찬가지 입니다. 차라리 수학, 통계학, 데이터 과학을 전공한 분들은 위의 소스를 보면 바로 scikit-learn 프로그램을 뚝딱 개발하실 수 있을 것입니다. 하지만 이게 다가 아닙니다. 위의 소스가 살아서 움직이려면 기초 데이터의 수집, Database의 연동, 그리고 기초 데이터를 다시 분류하고 스프레드 하는 작업이 필요할 것입니다. scikit-learn프로그램을 운영하려면 적어도 2개 이상의 프로그램이 필요함을 단번에 알 수 있습니다. 하나는 모델의 학습을 하나는 모델을 불러와서 실제 데이터에 적용하는 프로그램이 될 것입니다.

반응형
LIST

'python > 머쉰러닝' 카테고리의 다른 글

구글 colab에서 학습하기  (0) 2023.01.20

맨위로버튼이미지

1. 들어가기 전에

  오늘은 자동매매 프로그램 구현에서 중요하지만 주제와는 약간 빗겨간 DB 내용중 Upsert(Update or Insert)에 대하여 이야기 하고자 합니다. pandas의 DataFrame은 Nosql적 성격을 가진 아주 휼륭한 Lib입니다. 사실 pandas가 없다면 지금의 자동매매 프로그램의 구현은 힘들다고 해도 이견이 없을 것입니다. 하지만 pandas DataFrame을 사용하다보면 계산이 다 끝나서 이제는 실제 DB로 옮겨야 하는데 기존 제공하는 to_sql은 뭔가 약간은 부족합니다. to_sql함수는 중복이 있으면 insert 실패 처리 하거나 table 전체를 replace하거나 또는 그냥 append하는 것 입니다. 저는 지금은 pandas와 mysql을 사용하고 있지만 초창기는 sqlite3을 사용하였고 mysql로 DB를 변경하면서 문제가 발생했습니다. to_sql의 replace기능이 DB를 삭제하는 과정에 프로그램이 먹통이 되고 이때까지 모아온 소중한 데이터가 다 날아 가는 거죠. sqlite사용중에도 프로그램을 중간에 내릴때 타이밍이 안좋으면 table을 그대로 날려 먹는 경우가 생깁니다. 그래서 현재 인터넷에서는 pandas의 to_sql에 새로운 옵션인 upsert가 화재 입니다. 일부 개발 git에는 upsert가 구현되어 올라와 있지만 pandas의 to_sql을 ctrl + link로 따라 가보면 근본적인 sql lib에 있는 기능을 사용하고 있어 수정하는 것이 대공사라는 것을 금방 알 수 있습니다. 오늘은 제가 구현한 Upsert에 대하여 내용을 공유하고자 합니다. 

 

2. Upsert구현

  먼저 아래 소스를 보시죠.

def trade_data_update(df, dbcon, tb_name, if_exists: str='ignore'):
    try : 
        cols = [column for column in list(df.columns)]

        str_query = ""
        if if_exists == 'fail':
            str_query += "INSERT INTO %s (" % tb_name
        elif if_exists == 'replace':
            str_query += "REPLACE INTO %s (" % tb_name
        else:
            str_query += "INSERT IGNORE INTO %s (" % tb_name

        for col in cols:
            str_query += "`" + col + "`,"
        str_query = str_query[:-1]
        str_query += ")"

        rows_val = []
        for index in df.index:
            rowval = []
            for column in df.columns:
                rowval.append(df.loc[index, column])
            rows_val.append(rowval)
        
        str_query += " values "
        for rows in rows_val:
            str_query += "("
            for row in rows:
                if str(type(row)) == "<class 'str'>":
                    str_query += "'" + str(row) + "',"
                elif str(row) == "nan":
                    str_query += "NULL,"
                else:
                    str_query += str(row) + ","
            str_query = str_query[:-1]
            str_query += "),"
        str_query = str_query[:-1]

        cursor = dbcon.cursor()
        cnt = cursor.execute(str_query)
        # logger.info("apply row cnt:%d", cnt)
        cursor.close()

    except Exception as ex:
        exc_type, exc_obj, exc_tb = sys.exc_info()
        fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
        logger.error("trade_data_update -> exception! %s  : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

mysql기준 쿼리 입니다. mysql 에 INSERT에는 총 4가지의 방법이 있습니다. INSERT ON DUPLICATE KEY와 REPLACE INTO, INSERT IGNORE INTO, 그리고 그냥 INSERT입니다. 당연히 그냥 INSERT를 사용하면 INSERT과정에 Dup key가 발견되면 그 부분에서 바로 에러 후 다음으로 넘어 가겠죠. 나머지 3가지는 이렇게 Dup key가 발견되었을때의 예외 처리 방법을 기술한 것 입니다. 저는 보통 회사에서 프로그램할때는 INSERT INTO 보다는 REPLACE INTO를 많이 사용하는 편인데 지금 자동 매매에서는 INSERT IGNORE INTO를 많이 사용하고 있습니다. 

import pymysql
from lib.Upsert import trade_data_update

sqlcon = pymysql.connect(host='127.0.0.1', user='root', password='1234', db='main')

df = con.get_dataframe()
trade_data_update(df, sqlcon, "tb_training_data")
sqlcon.commit()

처럼 활용이 가능 합니다. 

 

3. 마치며

  다음 회차에서는 이렇게 모인 데이터를 활용하여 실제 training을 공유할까 합니다. 위의 Upsert를 이용하는 이유는 1분 데이터이다 보니 가끔 데이터 유실이 발생합니다. api나 프로그램의 문제가 아니라 upgrade를 위해서 잠시 프로그램을 내려 놓을때 수신하지 못한 데이터가 발생하는 거죠. 1회 수신 데이터를 100개 정도만 받아도 위의 방법을 사용하면 유실 없이 DB에 데이터를 저장할 수 있습니다. 

반응형
LIST

맨위로버튼이미지

1. 들어가지 전에

   여러분들이 파도를 탄다면 어떤 방식으로 파도를 탈까요? 여러분들은 모험을 좋아하나요? 저는 땅에서 발을 뛰는 행위 자체를 싫어 합니다. 그래도 돈에 대해서는 모험을 즐깁니다. 또 신기하게도 저는 오른손 잡이 지만 돈에 관련된 거의 대부분의 것은 왼손 잡이 입니다. 돈을 세는 것도 나도 모르게 왼손을 사용하고 있었습니다. 심지어는 화투폐도 왼손으로 돌립니다. 제 돈의 뇌는 아마도 오른쪽에 있나 봅니다. 자 여러분은 지금 Bitcoin이라는 파도를 타야 합니다. 어떤 보드를 선택하실 건가요? 저는 초기에는 5분데이터에 MACD로 파도타기를 했지만 성적은 좋지 않았습니다. 여러 파라메트를 변경해 보았지만 결국 MACD의 최대 약점은 가짜 매수, 매도가 발생하여 수익률이 떨어지지만 일정하게 매매가 발생하여 어느 정도 평균 수익을 만들 수는 있었습니다. 문제는 Bithum의 수수료였습니다. Bithum의 경우 0.25%로 매수,매도를 할 경우 0.5%의 수수료가 발생하고 요즘 주식 수수료가 0.015%인 것에 비하면 엄청난 금액입니다. 저 같은 경우 0.05% 수수료라는 말에 50만원짜리 정액 쿠폰을 싸서 거래를 했지만 결국 반 정도밖에 사용하지 못해서 수수료 할인을 거의 이용하지 못했습니다. 거래 금액 10억까지 무료였지만 거래금액이 5억이 되는데는 몇일이 걸리지 않았습니다. 지금은 upbit로도 프로그램을 돌리는데 upbit의 경우 모든 KRW시장거래에 0.05%의 수수료를 적용하고 있고 정액쿠폰 같은 것은 판매하지도 않기 때문에 훨씬 편리한것 같더군요. 결국 매매 후 금액이 수수료를 초과하지 않으면 이 게임은 실패입니다. 그래서 제가 생각한 방법은 종가최대최소값 밴딩 방법입니다.

 

2. 종가최대최소값 밴딩 이란?

  pandas의 rolling함수를 이용하면 특정 구간에 min값 max값 평균값을 구할 수 있습니다. 

self.logger.debug("Close Min Max 구하기:%s"  % time.ctime(time.time()))
df['clsmax'] =  df.high.rolling(window=Const.MINMAX_RANGE, min_periods=Const.MINMAX_MINIMUM_DATA).max()
df['clsmin'] =  df.low.rolling(window=Const.MINMAX_RANGE, min_periods=Const.MINMAX_MINIMUM_DATA).min()
df['clsmininc'] = df.clsmin - df.clsmin.shift()
df['clsmin_degrees'] = df['clsmininc'].apply(lambda x: math.degrees(math.atan2(x, Const.DEGREES_X))) 
df['clsmaxinc'] = df.clsmax - df.clsmax.shift()   
df['clsmax_degrees'] = df['clsmaxinc'].apply(lambda x: math.degrees(math.atan2(x, Const.DEGREES_X))) 
df['max_rate'] = df.apply(lambda x: (x['clsmax'] - x['close']) * 100 / (x['clsmax'] - x['clsmin']), axis=1)
df['min_rate'] = df.apply(lambda x: (x['close'] - x['clsmin']) * 100 / (x['clsmax'] - x['clsmin']), axis=1)

위의 내용처럼하면 해당 구간의 최고가와 최소가를 구할 수 있습니다. max값을 예로 들어보면 해당 구간의 max값은 한번 최고점을 찍으면 f(n+1)=f(n)의 공식이 성립되면서 min값과 같이 일종의 밴드(쌍으로 같이 움직이는 그래프)를 만듭니다. 두 곡선의 절대 만나지 않습니다. 비트코인이 망하지만 안는다면 말이죠. 비트코인처럼 변이가 심한 자산의 경우 절대 수익을 보장하는 매수,매도 공식이 가능하지만 가끔 박스권을 형성하는 경우가 발생하면 매매가 발생하지 않는게 약점이죠. 상단과 하단이 한번 형성된 후 계속 그 사이를 왔다 갔다하면서 상단과 하단을 터치하지 않으면서 가격만 계속 오르거나 내리는 경우가 가끔 있습니다. 그럼 그날 수익은 제로 인 거죠. 지금 이 공식은 제 프로그램중 ETF자동매매 프로그램에도 적용이 되어 있습니다. 이상적인 상황에서 이상적인 수익을 가져 주지만 급변 구간이 문제 입니다. 급변 구간의 해답은 앞 5장에 있는 내용을 참고 바랍니다. 하지만 sma1200_degrees의 경우 가격의 급변 보다는 에너지(돈의 수급)의 총량에 더 가깝다고 봐야 합니다. 급등시 급등 신호가 감지 되지만 전체 기간중 앞 15~30%에서 최고가가 형성됩니다. 그렇다고 매도를 하게 되면 그 순간 더 상당으로 달아 날 수도 있기 때문에 sma1200_degrees가 40º를 넘는 경우는 일단 매매를 멈춥니다. 50º가 넘었다면 무조건 멈춰야 합니다. 

3. 기존의 방식중 절대하지 말아야 하는 방법들

  1. 변동성 돌파 전략

   이 전략은 주식전용 전략 입니다. 절대로 코인에 적용하면 안됩니다. 주식에도 적용하기가 그렇습니다. 코인은 24시간 체재입니다. 절대로 변동성 전략이 통할리 없습니다. 주식에서도 이 전략은 한달안에 거지되기 쉽상입니다. 요즘 주식은 아고점저(아침에 고점 점심때 저점)의 시대 입니다. 함부로 변동성 돌파 전략을 사용하면 아침에 고점에 물려서 오후 3시20분에 매도 되어 손실을 기록하게 됩니다. 

 2. MACD 오실로스코프 매매

   이 매매 전략은 12, 30, 15 정도의 값으로 macd를 생성하고 하루 거래 발생 건수를 고려하여 배율을 조정하면 어느 정도의 수익을 보장합니다만 폭락장에서는 전혀 맞지 않습니다. 5월~7월사이에 코인 가격이 반토막이 난 것을 생각하면 지금은 깡통이겠죠?

3. 주식 보조 지표가 코인에서는 안맞는 이유

  어떻게 보면 당연한 이야기 입니다. 기존의 매매 방식은 주식의 것이고 주식의 경우 9시부터 3시반까지만 거래가 되고 거래가 끝나고 5시쯤에 찌라시가 돕니다. 지금은 기존의 보조지표 매매도 전혀 맞지 않는게 주식 시장입니다. 그런데 그 보조지표가 코인에서 맞을 가요? 저는 모든 지표를 거의 새로 만들었습니다. 주식에서 통용되던 보조 지표들은 코인의 것이 아니니까요?

 

4. 마치며

  수학자들 중에서는 미지의 소수를 찾는 작업을 평생연구하는 사람들이 꽤 많다는 말을 들었습니다. 게임의 법칙으로 노벨 경제학상을 탄 수학자 존 내시 박사도 소수에 어떤 공식이 있을 것이라며 평생을 받쳤지만 정작 친구들과 여자 꼬시기 방법을 재미로 연구한 게 노벨을 상을 받게 되었습니다. 기존의 확률은 존재하는 것에만 적용을 하지만 존 내시 박사의 게임에 법칙에서는 100명의 여자 중 최고의 신부를 고려는 방법으로 30명을 일단 만난 다음 차회 부터는 지금까지 만난 사람중 1이라도 나은 최고의 사람을 만난다면 무조건 결혼하는 법칙 즉 모수가 100일 수 없는 확률을 완성할 수 있는 방법을 제시합니다. 물론 소수를 구하는 공식 따위는 만들지 못했습니다. 일부 수학자는 소수가 짧은 주기와 긴 주기를 간격으로 해서 계속 반복되고 있다는 것은 밝혔지만 역시나 공식으로 이것을 증명하지는 못했습니다.  소수를 찾는 이론은 다양하게 많지만 지금의 슈퍼컴퓨터로도 다음 소수의 위치를 밝히지는 못했습니다. 근데 이 무모한 짓은 왜하는 것일까요? 우리처럼 주가나 비트코인의 가격을 예측하는 게 더 정상적이지 않을까요?

 

반응형
LIST

  

맨위로버튼이미지

1. 들어가기 전에

처음 Bitcoin 자동 매매 프로그램을 개발하고 벌써 5개월이란 시간이 흘렀네요.
자동 매매 프로그램의 시작은 주식이였지만 처음 성적은 별로 좋지 않았습니다.
그 때 배운게 인터넷에 떠 있는 소스는 절대 믿으면 안된다 였습니다.
생각해 보면 자동 매매 프로그램을 개발하시는 분들 대부분은 각자의 철학과 전략이 있기에 다른 어떤이와도 생각이 일치할 수도 없거니와 계속 변경되는 환경에서 이전의 소스가 맞을 리가 없었는데 말이죠.
그리고 나서 pyBithum이라는 python lib가 있다는 것을 알게 되었습니다.
고민도 할거 없이 나의 첫번째 자동 매매 프로그램은 그렇게 단 몇시간만에 탄생하게 되었습니다.
하지만 운영하면서 계속되는 원인을 알 수 없는 에러를 debugging하면서 Bithum API 자체 결함이 많다는 것을 서서히 눈치챘습니다.
pyBithum lib는 그저 껍데기 일뿐 Bithum API를 호출하는 구조였던 거죠.
결국 Bithum API가 개선 되지 않는 이상 이 문제들은 해결될 수 없다는 것도 알게 되었습니다. 그러면서 각 거래소별 API에 대한 이야기를 다른 블로그를 통해서 알게 되었습니다.
그나마 Bithum API가 업계에서는 괜찮은 편이라는 거죠.
음 이거 실화냥? ㅠㅠ;
비싼 수수료에 영세업체 수준의 API 이것이 저의 생각입니다.
Bithum 관계자가 이 글을 보면 저에게 소송을 걸까요?
그렇다면 이 문장을 수정해야 겠네요.
아실지 모르지만 우리나라에는 '사실즉시 명예훼손'이라는 법이 있습니다.
비록 그 내용이 '사실'이라 해도 그 내용을 공공에 게시하여 당사자의 명예를 훼손한다면 형사 처벌을 받는 법입니다.
헌법에 크게 위배된다고 생각한 많은 사람들이 헌법 소원을 했지만 작년 가을 헌제에서 '합헌 결정'이 났습니다.
민사로 충분히 처리가 가능하지만 형사처벌 조항이 있어 이중 처벌의 위험성이 있는데도 말이죠.
무엇이 무서웠을 까요?

2. Bithum API의 근본적인 문제점

1. version 정보 없음.

저도 현역 프로그래머로써 이 곳 저 곳을 다니면서 소위 RestAPI라는 것을 개발하고 있습니다.
처음에는 표준이 뭔지도 몰라 여기 저기 똥을 싸고 다녔지만 하다가 보니 저기는 왜 저렇게 했는지 여기는 왜 이렇게 했는지가 하나 둘씩 보이기 시작 했습니다.
그러면서 가장 먼저 눈에 띈게 바로 version정보 입니다.
API path 중간에 (예시 : '/api/v1/info/user_transaction') 버젼 정보를 표시하고 있다는 거죠.
혹시 tensorflow를 사용해 보신 분이 계신가요? tensorflow v1에서 v2로 되면서 거의 천지개벽 수준으로 api변경이 되었습니다.
keras의 위치가 중요하게 되면서 기존 v1의 기능은 대폭 축소 되고 모든 것을 keras에게 위임하는 형식으로 소스가 변경되고 간략화 되어 기존 v1소스는 폐기의 수준이 되었습죠. ㅠㅠ

from tensorflow.compat import v1 as tf tf.disable_v2_behavior() tf.random.set_random_seed(seed)

위 처럼 해주지 않으면 v2로 처리가 되어 거의 모든 함수에서 에러가 발생합니다.
눈치 채셨나요?
지금 거래소 API중 어떤것도 version 정보가 없습니다. (지금은 Upbit API로도 개발 중인데 Upbit API에는 버젼 정보가 있더군요. Upbit는 모기업이 KAKAO라 대기업의 냄새가 납니다.)
중간 중간에 실 운영기 소스를 수정하고 아무런 공지도 없습니다.
갑자기 잘 돌던 프로그램에서 type오류가 납니다. 원인은 API의 변경으로 리턴값이 바뀐겁니다.
현 체제로는 방법이 없는 거죠.

2. 오류 코드 묶음 처리

모든 API는 정상적으로 아주 잘 만들어 진 코드라고 해도 에러가 발생합니다.
정확하게는 예외사항이죠.
프로그래머도 컴퓨터도 어쩔 수 없는 사항이 발생한다는 거죠.
그런 때를 대비해서 오류코드가 있는 거죠.
제가 가장 황당했던건 Bithum API를 사용하면서 5500 에러와 5600에러 속에 수 많은 종류의 오류가 코드 없이 묶음으로 존재한다는 거였습니다.
사실 상당 수의 에러는 오류 처리를 할 필요도 없는데 말이죠.
아 그렇군 하는 정도면 되는데 말이죠.
근데 몇몇 프로그램에 아주 critical한 오류가 같이 섞여 있다는 게 문제 인거죠.
지금 제가 운영하고 있는 자동 매매 프로그램의 trading 부분 소스는 방어코드의 총망라 입죠.
오류코드가 세분화 안되어 있으니 프로그램에서 다 방어를 해야 한다는 느낌.
뭐 그렇습니다.

3. 운영 유지만 version up은 언제쯤?

사실 초기에는 Bithum에 몇 번 전화를 했었습니다.
전화 받는 분이 자기네 회사 오류코드 체계도 모르시더군요.
딱 봐도 전문 텔러가 아닌데 말이죠.
(저는 초기에 콜센터 프로그램만 5년을 넘게 했었습니다. 전화 응대만 들어도 전문 텔러 인지 아닌지 알 수 있습니다.)
운영이나 개발인력의 수준을 눈치 채는데는 그리 많은 시간이 필요하지 않았습니다.
군데 군데 보이는 critical한 문제가 과연 고쳐 질까?
마치 창문 없는 집을 보고 '저 집에 창문은 어떻게 낼 수 있을까?' 고민하는 수준 이랄까요?

3. 나의 Bithum API 극복기

1. 주문량이 사용가능 KRW를 초과하였습니다.

해쉬태그로 뭘 달까 고민할 필요도 없이 '주문량이 사용가능 KRW를 초과하였습니다.'를 선택했습니다.
다른 블러그나 게시판에 아주 논쟁이 뜨겁습니다. 어떤 사람은 자기는 발생하지 않는데 난다고 한다고 하고 저 같은 경우는 계속 발생합니다.
제가 알아 낸 사실은 총 현금 보유 금액의 85%만 거래가 가능하다는 겁니다.
확실히 버그라고 하기에는 어딘가에 하드코딩되어 있는 뭔가가 있는 것 같습니다.
저 같은 경우는 보유 KRW의 85%를 구해서 여러번 분할 매수를 하게 프로그램 되어 있습니다.
그래서 계속 매수 하다 보면 결국 보유 현금의 100%까지 매수가 가능 합니다.
물론 최소 거래 금액과 0.0001 미만의 코인은 매수 되지 않습니다.
(지금 up-bit 계좌로도 프로그램을 만들고 있는데 upbit는 최소 거래 금액이 bithum보다는 크지만 그것은 일회 거래금액의 단위이고 소수점 제약등은 존재하지 않습니다.)
 

balance = get_balance() #잔고 조회 if balance is not None: self.logger.info("잔고조회: %.5f %.5f %.2f %.2f", balance[0], balance[1], balance[2], balance[3]) krw = balance[2] #구매 가능 보유 금액 
buy_krw = buy_price * unit #구매할 krw를 계산 
remind_krw = krw - buy_krw #잔여 금액 계산 
possible_krw = krw 
if remind_krw <= buy_krw * 0.15: # 정액쿠폰 구매와 상관 없이 수수료 계산 
    possible_krw = krw - buy_krw * 0.15 
    unit = possible_krw/buy_price #구매 수량 계산 
    unit = int(unit * 10000)/10000 #unit은 소수점 네자리 아래 삭제 처리 
    order = bithumb.buy_limit_order(ticker, buy_price, unit, payment_currency="KRW") #매수 주문 
    if order is not None and type(order) is tuple: 
        self.logger.info("order success:%s, timing_price:%d, buy_price:%d" 
           % (str(order), timing_price, buy_price)) self.last_order_id = order; 
        for i in range(0,19): 
            resp = self.order_detail(order) 
            self.logger.info("order detail : " + str(resp)) 
            if resp is not None and resp.get('status') == '0000': 
                orderStaus = resp.get('data').get('order_status') 
                if orderStaus == 'Completed': 
                    slack.chat.post_message(slackkey.SLACKER_CHAN, time.ctime() + ":`order detail : " + str(resp) + "`") 
                    break 
        return

클래스로 구현된 trading 모듈을 간략화해서 붙이는 거라 소스중간에 self가 보이는 부분 양해 바랍니다.
소스를 저렇게 고치고 나니 그 후 '주문량이 사용가능 KRW를 초과하였습니다.' 오류는 발생하지 않았습니다.
시장가 주문에서도 지정가 주문에서도 동일한 부분이 필요 합니다.
시장가 주문을 할 경우는 orderbook의 가장 첫번째 ask값을 buy_price로 해서 계산하면 됩니다.
Bithum에서 수정한다면 거래를 태우기 전에 수수료를 먼저 검사해서 (아마도 app에서는 이 부분이 수정되어 있는 것 같습니다. app상에서는 이런 오류가 한번도 없었습니다.) 수수료 무료 고객은 수수료 부분까지도 구매가 가능하게 api를 수정하면 될 것 같습니다.
Bithum에서 수정이 안된다면 우리는 계속 위의 소스를 유지 해야 겠죠.

2. 홈페이지 API 문서에는 있지만 pyBithum에는 없는 API들

혹시 pyBithum에 없는 API를 직접 함수로 구현하려고 HttpRequest를 사용하려고 시도하신 분들이 있을 까요?
제 생각에는 아마도 안되실 겁니다.
이유는 public API는 아무 상관 없이 개방되어 있지만 ( public과 private는 같은 함수라도 정보 조회 건수가 다른건 아실 겁니다.) private는 HttpRequest Header에 HMAC 값이 있습니다.
혹시 알고 계셨던 분들도 있으신가요?
HMAC 계산은 아주 복잡하고 까다롭습니다.
하지만 pyBithum에는 이미 HMAC계산이 다 되어 있습니다. 아래 소스를 보시죠

from pybithumb.core import BithumbHttp
http = BithumbHttp(key.con_key, key.secu_key) #API KEY와 SECU KEY로 http초기화 
url = '/info/user_transactions' #API 문서에 있는 url 값 
                                #주의 : 중요한 것은 앞쪽에 있는 '/'가 없으면 type 에러 발생함.  
try: 
    resp = http.post(url, offset=offset, count=count, searchGb='2', order_currency=self.ticker, payment_currency='KRW') 
    print("user_transactions:" + str(resp)) 
    if resp != None and resp['status'] == '5600': 
        return None return resp 
except Exception as ex: 
	exc_type, exc_obj, exc_tb = sys.exc_info() 
    fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 
    print("user_transactions -> exception! %s : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

몇가지 import가 더 있지만 충분히 알 수 있는 내용이라 생략 했습니다.
생각보다 간단하죠?
pyBithum에는 이미 이런 내용이 다 반영이 되어 있습니다.
예를 들면 Bithum API문서에는 Stop-Limit라는 기능이 있습니다.
지정가로 호가가 있으면 그때마다 매수를 하게 지정하는 함수 인데 pyBithum에는 아직 기능이 없습니다.
아마도 제 생각에는 pyBithum은 더 이상의 버젼업이 없을 것 같습니다.
이 기능을 구현하려면 url부분에 '/trade/stop_limit' 를 넣고 해당 Request Param중에 apiKey와 secretKey를 제외한 나머지 부분중 필수 값을 채우고 필수가 아닌 값은 보고 판단하시면 될 것 같습니다.

4. 마치며

Bithum관계자에게 간곡히 부탁드립니다.
만약에 혹시라도 이 게시물을 보신다면 '사실즉시 명예훼손'으로 고소하지 말아 주세요.
하시기전에 저에게 email(yubank@naver.com)한통이면 이 글 중 명예 훼손이 되는 부분은 전부 삭제 처리 하고 다른 프로그램에 필요한 내용만 남겨 두겠습니다.
그 마저도 불편하다고 생각 하시면 전체를 비공개로 전환하겠습니다.
아직 저의 블로그 방문자는 10명 내외로 미미한 수준입니다.
혹시라도 이곳에서 이 글을 보신다면 제가 Bithum을 비방하려는 목적이 아님을 아실 겁니다.
저도 처음에는 '주문량이 사용가능 KRW를 초과하였습니다.' 오류로 프로그램에 중간에 멈추는 현상이 아주 잦았습니다.
원인을 알고 수정하고 주문을 여러번에 나눠서 하게 수정을 해서 겨우 해결을 했습니다.
아직도 이 문제를 해결하지 못하신 분들도 인터넷 게시판을 디지다 보면 많은 것을 알고 있습니다.
새로 시작하시는 분들도 상당 수 이 문제에 봉착 하시라 생각 됩니다.
그리고 Bithum뿐만 아니라 현재의 우리나라 Bitcoin 거래소들의 출발이나 현재의 모습이 어떨지 왜 그랬는지를 알고 앞으로가 어떨지도 알고 있습니다.
현재 시점 수 많은 거래소가 정부 가이드를 못 맞춰서 대표자 횡령혐의로 입건 되거나 폐쇄의 길을 걷고 있다는 뉴스는 쉽게 접할 수 있습니다.
그래도 대표 4개 거래소는 끝까지 살아 남았으면 하는 바램이 아주 큽니다.
정부에서는 증권거래소와 같이 중앙 거래소와 증권사를 두는 방식의 개편을 준비 중인 것 같습니다.
그렇게 되는 데만도 족히 4~5년은 걸릴 것 이고 은행들은 새로운 BIS기준 때문에 일부 Bitcoin 거래소의 전용계좌 개설 및 기존 계좌를 폐쇄하려고 하고 있죠.
그럼 아마도 Bitcoin거래소는 재계순위 4위까지만 살아 남고 나머지는 거의 다 폐쇄될 것으로 보입니다.
그 이후에는 진짜 제대로 모습을 갖춘 거래소들이 다시 생기겠죠.
사실 Bitcoin의 운명도 불투명합니다.
미국은 달러연계 암호화 화폐를 준비중입니다.
사실 미국이라는 나라는 남미의 국가들에게는 소리없는 죽음의 침략자입니다.
과거에는 전쟁으로 영토를 침략했다면 지금은 금융으로 타 국가의 이익을 소리도 없이 점령합니다.
수 많은 사람들이 죽음으로 내 몰리지만 저희는 알 수도 없습니다.
이유는 기축통화인 달러를 자기 나라의 이익을 위해 맘데로 운영을 하면서 남미의 국가 나 또는 폐그제를 운영하는 여러 나라에 막대한 피해를 입히고 있습니다.
그런데 이 영향을 안 받는게 Bitcoin이죠.
그래서 미국은 또 다른 침략인 달러 연계 암호화 화폐를 만들려고 합니다.
그리고 우리나라도 원화 연계 암호화 화폐(소위 말하는 전자 화폐)를 개발하고 있습니다.
Bitcoin은 악용되는 사례도 많습니다.
수 많은 나라에서 납치범들이 몸값을 요구하는 수단으로 Bitcoin을 사용하고 있고 마약거래 및 인신매매에도 사용되고 있죠.
그래서 부자들 중 워랜버핏 같은 사람은 Bitcoin혐오 주의자 중 한명이기도 합니다.
지금이 Bitcoin및 거래소들이 중요 갈림길이라고 봅니다.
4~5년 후에도 살아 남으려면 과연 무엇을 준비해야 할까요?

반응형
LIST

맨위로버튼이미지

1. 들어가기 전에

어떤 파도타기를 좋아하는 사람이 있다고 합시다. 보통의 파도는 너무도 잘 타지만 이 사람은  한계가 어디까지인지 궁금했습니다. 문제는 파도의 높이와 바람의  방향에 따라 잘못하면 파도를 타다가 죽을 수도 있다는 거겠죠. 파도가 아무리 높아도 바람이 세지 않다면 별 문제가 없을 거고요. 바람의 방향도 자신의 방향인지 반대 방향인지에 따라 파도의 높이가 문제가 안될 수도 생명이 위태로울 수도 있는 거죠. 바람의 방향이 내륙 쪽이고 파도가 높다면 즉시 파도타기를 중지하고 휴식을 취해야 하지만 파도가 높고 바람의 방향이 바다를 향한다면 그것은 서퍼에게는 최고의 도전이 되겠죠. 눈치 채셨나요? 요즘 Bitcoin 시장이 위의 내용과 비슷합니다. 일주일 단위로 급등락을 하고 있습니다. 보통 주말을 주기로 급락을 하고 주 후반에 급등을 했다가 또 어느샌가 급락을 합니다. 폭은 대략 이더리움 기준으로 약 40만 원 선을 왔다 갔다 하지만 어느새 이더리움 가격은 최고점 대비 반토막이 났고 비트코인도 맞찬가지 입니다. 현재 시점 기준 비트코인은 반등했지만 4천이 안 되는 선에서 박스권을 만들고 있습니다. 다시 파도타기 이야기로 돌아간다면 바람의 방향이 언제 바뀔지 파도의 세기는 얼마나 되는지(바람의 세기와 비례하겠죠?) 파도의 높이는 어느 정도 되는지 (미리 예측하는 것이 아니라) 현시점의 변화만 알아도 얼마든지 파도타기에 도전하는 것은 무리가 없겠죠? 다시 비트코인 세계로 돌아온다면 이 모든 것을 결정하는 것은 무엇일까요? 사람들은 이동평균선으로 대충의 추세를 예측하지만 컴퓨터(프로그램)는 불행히도 인간이 보는 이동평균선을 볼 수 없습니다. 대신에 점으로 보는 거죠. 근데 문제는 점으로는 앞의 추세를 판단할 수가 없습니다. 여기서 미적분이 필요합니다. 문과분이나 수학을 잘하지 못했던 분들은 미적분이라는 소리에도 놀라시겠지만 제가 하려는 미적분은 실제로는 미적분과 거리가 멀다고 할 수 있습니다. 그냥 대충의 미적분인 것입니다. 왜냐하면 컴퓨터는 0 또는 1만 이해할 수 있고 프로그램에서는 true와 false만 이해를 할 수 있으므로 그에 대입되는 값의 정확도는 그리 중요하지 않습니다.

2. 자동매매 분석에 미적분이 필요한 이유

먼저 미분에 대하여 여러분은 예전 고등학교 시절 무엇이라고 기억하고 계신가요? 왜 미분을 해야 하는지에 대한 내용이나 미분을 하는 방법에 대하여 배우신적이 있으신가요? 지금 우리가 하려는 미분은 이전 수학 시간에 배우던 그런 어려운 미분이 아닙니다. 인간은 모든 데이터 및 주위 환경을 이해하기 위해서 아날로그 데이터를 사용합니다. 이전에는 모니터도 아날로그 음악도 아날로그 모든 것이 아날로그였지만 현재는 모든 것이 디지털입니다. 그럼 여러분 아날로그와 디지털의 차이는 무엇일까요? 예로 제가 처음 컴퓨터 프로그래머로 일했을 때 제가 주로 했던 일이 콜센터의 지능형 ARS 즉 IVR이라는 시스템을 설치하고 프로그래밍하는 일이었습니다. 이때 아날로그인 음성을 디지털로 만드는 것을 샘플링이라고 하고 주로 1초에 8000번 샘플링한 데이터를 사용하여 프로그래밍 해었습니다. 하지만 아날로그의 절대 끝은 존재하지 않아서 인간의 가청 주파수인 20000Hz 이상의 데이터는 버리고 샘플 했습니다. 즉 주변의 소리도 1초에 8000번만 높이를 기록하면 다시 소리로 변환할 때는 정상 소리와 구분이 안 가고 사람이 알아들을 수 있습니다. 요즘은 음악 녹음할 때는 198000번 정도의 초당 샘플링을 합니다. 그러면 사람은 거의 원본 녹음과 디지털 음을 구분하지 못합니다. 

다시 비트코인의 세계로 돌아 와서 우리 사람은 쉽게 이해할 수 있는 이동평균선(MA)을 컴퓨터(프로그램)가 이해하고 학습할 수 있게 할 수 있을 까요? 위 이야기의 파도타기처럼 컴퓨터가 어떤 이동평균선이 위험한지 어떤 게 안전한지 폭락 후 멈출 때 현상은 무엇인지 폭등이 시작된 것은 어떻게 판단할지 폭등은 언제쯤 멈출 것이며 폭등 후의 행동은 무엇일지 컴퓨터가 판단할 수 있는지에 대한 이야기입니다.

다시 저의 초기 프로그래머 시절로 돌아가서 소리를 8000번 샘플링하는 것을 PCM(Pulse-code modulation)이라고 했습니다. 여기서 중요한 것은 8000번 샘플링에는 일정한 주기가 있다는 것 입니다. 컴퓨터가 우리가 사는 3차원의 세계를 이해하려면 자신이 있는 1차원의 세계의 데이터로 변환하는 일이 필요합니다. 혹시 Deep learning공부를 하신 분들이 계신가요? 딮러닝 공부를 하신다면 무슨 게이트 무슨 게이트 시그모이드니 뭐니 하는 것들을 가만히 생각해 보시면 결국 다차원의 데이터를 1차원의 데이터로 변환하는 가정이라는 것을 아시게 되실 겁니다. 결국은 컴퓨터는 1차원 데이터 외에는 이해 불가라는 거죠. 다시 고등학교 시절로 돌아가서 미분을 다시 배운다면 미분은 다차원의 데이터를 1차원 데이터로 변환하는 가정이라고 배우신다면 좋을 듯합니다.(왜 그때는 수학을 어렵게만 가르쳤는지 저는 이해가 잘 안 됩니다.) 이게 뭔 말이냐 하면 우리가 분석하려는 이동평균선은 고차원 방정식의 곡선입니다. 변화무상하고 매 순간 변화하지만 일 순간은 점(dot)이라는 사실이죠. 이 점(dot)은 바로 컴퓨터가 이해할 수 있는 1차원 데이터 입니다만 문제는 이 점(dot)이 완벽한 데이터가 되려면 방향이 필요합니다. 그래서 제가 생각한 게 바로 각도입니다. 곡선의 한 점을 미분하면 그 지점의 각도를 구할 수 있습니다. 혹시라도 고등학교 수학을 놓치지 않으셨다면 다들 알고 있는 것일 겁니다.

간단하게 미분식을 표현한다면 앞의 식처럼 표현 하겠지만 정식적인 표현은 'f(x) = 기울기*x + 절편' 이런 식의 식이 나올 겁니다. 하지만 우리가 필요한 것은 기울기만 필요합니다. 시간 단위로 움직이는 평균 이동선의 값의 변화는 pandas를 이용하면 아주 쉽게 구할 수 있습니다.  pandas DataFrame을 df라고 하고 평균 이동선을 sma120(120개 이동평균선)이라고 하면  dy = df.sma120 - df.sma120.shift()로 쉽게 구할 수 있습니다. 그렇지만 시간의 변화 값을 구하는 것은 실제로는 불가능합니다. 하지만 상관없습니다. 우리는 대충의 근접 치를 구하는 것이지 딱 이 값 이런 식의 값을 구하는 것이 아닙니다. 우리는 컴퓨터 프로그램에서 사용할 수 있는 오차가 있어도 다른 값과 커플링 되지 않으면서 비교가 가능하고 방향성을 가지는 값만 구하면 됩니다. 그래서 저는 dt값을 대충 3*60 정도로 하기로 했습니다. <제가 살다 보니 사람들 마다 문과 머리와 이과 머리가 따로 있는 것 같더군요. 제가 위처럼 dt를 대충 3*60이라고 정한다고 하면 이과 머리는 절대로 이해를 못합니다. 근데 수학을 잘 못하는 문과생들은 "그래"하고 금방 수긍하죠.> 그 다음 다시 수학입니다. 그냥 인터넷만 뒤지면 나오는 기울기로 각도 구하는 공식입니다. python으로 식을 구하면

df['sma120_inc'] = df.sma120 - df.sma120.shift()
df['sma120_radians'] = df['sma120_inc'].apply(lamda x: math.atan2(x, 3*60))

위와 같이 될 겁니다. 그럼 sma120_radians는 -π/2 ~ π/2 를 리턴할 겁니다. 이 값보다는 -90 º ~ 90 º 표현되는 값이 훨씬 이해하기가 쉬울 겁니다. 그래서 다시 python 식을 바꾸면 

df['sma120_incli'] = df.sma120 - df.sma120.shift()
df['sma120_degrees'] = df['sma120_incli'].apply(lambda x: math.degrees(math.atan2(x, 3*60)))

위와 같이 표현이 됩니다.

2장에 나오는 class Auxiliary 에 위 부분을 추가하면 이제는 각 보조 지표들에 대하여 하락 중인지 상승 중인지를 알 수 있습니다. 저 같은 경우는 이 블로그 초기에 5분 데이터를 버리고 지금은 1분 데이터를 사용하고 있으며 1200분의 이동평균선(이하 sma1200)을 주로 이용하고 있고 이 값은 현재 급변하는 비트코인 시장에 너무나도 잘 맞고 있습니다. 신기할 정도입니다. sma1200의 각도인 sma1200_degrees는 위의 식을 잘 응용하신다면 쉽게 구하셨을 거라고 생각합니다. 그리고 제 프로그램에 위의 식을 이용하여 sma1200_degrees가 30 º 를 초과하면 급등 구간으로 정의하고 40 º 를 초과하면 급급등 구간으로 정의했습니다. 그래서 30 º초과하는 경우 조건식이 뭐건 상관없이 무조건 매수하게 했습니다. 40 º 를 초과하면 절대로 매도를 안 하는 거죠. 왜냐면 계속 오를 테니까요? 하지만 여기에는 문제가 있습니다. 우리가 파도를 타는 사람이라고 생각하면 무엇보다도 바람의 방향이 중요합니다. 바람의 방향을 무시하면 생명과 직결되는 문제가 발생합니다. 바람의 방향이 바뀌면 바로 모든 것을 버리고 해변으로 나와야 하죠. 순풍이라고 신이 나서 죽음이 다가오고 있는지 모르고 즐기고 있다면 죽음의 신이 바로 옆에 있는데 춤을 추는 꼴이겠죠. 그래서 제가 생각한 값이 곡선의 밑면과 닫는 넓이입니다. 밑변은 840분 동안의 최저가를 사용했습니다. 그리고 우리는 위의 각도를 알고 있기 때문에 곡선에서 90 º로 내린 선과 곡선의 기울기를 이용한 각도인 sma1200_degrees와 840분 동안의 최저가를 이용하면 하나의 직각 삼각형을 구할 수 있다는 것을 쉽게 이해하실 겁니다.  

대충 옆의 그림처럼요 이 삼각형의 넓이를 대충 저는 minus_range라고 정했습니다. 이 값은 굉장히 큰값이 나옵니다. 이 값을 이용하면 파도의 힘과 방향을 알 수 있지만 역시나 값이 너무 큽니다. 그래서 다시 한번 이 값의 이동평균선을 구했습니다. 왜냐하면 이제는 어떤 이동평균선이 와도 컴퓨터(프로그램)는 이해할 수 있기 때문이지죠 아래 python코드를 보시면 아실 겁니다.

df['sma1200'] = talib.SMA(np.asarray(df['close']), 1200)
df['sma1200_incli'] = df.sma1200 - df.sma1200.shift()
df['sma1200_degrees'] = df['sma1200_incli'].apply(lambda x: math.degrees(math.atan2(x, 3*60))) 
df['sma1200_minus_range'] = df.apply(lambda x: 0.5* ((x['sma1200'] - x['close'])**2) * math.tan(math.radians(x['sma1200_degrees']))/1200, axis=1)
df['sma1200_mrma'] = talib.SMA(np.asarray(df['sma1200_minus_range']), 60)
df['sma1200_mrinc'] = df.sma1200_mrma - df.sma1200_mrma.shift()
df['sma1200_mrdeg'] = df['sma1200_mrinc'].apply(lambda x: math.degrees(math.atan2(x, 3*60)))

직각삼각형의 넓이의 이름은 minus_range이고 이값을 이동평균 한 값은 mrma로 정했습니다. 그리고 다시 mrma의 증감 값을 구하고 이 값의 각도를 다시 구했습니다. 이 값을 mrdeg로 했는데 사실 이 값은 그리 중요하지 않습니다. 딮러닝의 시그모이드(다차원의 값을 0과 1로만 표현하게 변환하는 모델) 같은 역할을 하는 값입니다. 이 값이 양수 이면 계속 상승 중인 거고 이 값이 음수이면 하락 반전한 것입니다. 즉 sma1200_degrees가 30을 넘어가면 가격이 얼마이던지 중요하지 않고 무조건 매수를 합니다. 다면 sma1200_mrdeg가 양수 일 때만 인 거죠. 그러다가 상승 에너지가 점점 약해지면 sma1200_mrdeg가 음수로 전환됩니다. 자 이젠 파도타기를 멈추고 해변으로 탈출해야 하는 시기입니다. 즉 가지고 있던 모든 비트코인을 매도하고 잠시 해변으로 나와 파도를 감상하는 타임을 가지는 거죠. ㅋㅋㅋㅋㅋ 저는 이 공식을 만들고 급락할 때도 이익을 내고 있습니다. 왜냐하면 급락도 위의 식의 반대가 되는 거죠. 급락 구간도 두 구간으로 나누고 -30 º 에서는 매수와 매도를 계속하게 했습니다. 그러다가 -40 º가 되면 급락이 시작되는 것이기에 무조건 매도합니다. 이때는 수익률이 약간 손해를 봅니다. 이유는 사실 이때가 최저가인지 아니며 지옥의 문 앞인지 저도 컴퓨터도 예측이 불가능하기 때문이죠. 이건 어떤 누구도 예측이 불가합니다. 저는 이번 하락 구간에서 -40 º가 하루를 넘기는 적도 보았습니다. 어쨌든 계속 폭락하던 값이 언제 멈출지는 이제 sma1200_mrdeg를 이용하면 쉽게 알 수 있습니다. sma1200_mrdeg 값이 점점 줄다가 양수로 변하면 무조건 매수를 시작합니다. 아주 빨리 올인을 해야 하죠 그리고 상승장을 기다립니다. 대부분은 급락 후는 바로 급등이고 일부는 급락 후에는 제가 이름 붙인 '렌딩 구간'이 발생합니다. 렌딩의 끝이 바로sma1200_mrdeg가 양수로 변하는 순간인 거죠. 

 

3. 마치며 

이번 회차에서는 추상적인 개념을 설명하다보니 여러 세계관을 왔다 갔다 했습니다. 저는 1970년대 사람이라 다른 시대를 사신 분들과 저의 어린 시절(고교 수학 시절)이 많이 다를 수 있을 것으로 생각됩니다. 물론 저도 파도타기 세계관을 경험하지는 못했습니다. 다만 어린시절 본 '폭풍 속으로'라는 영화를 상상해 보았습니다. 주인공 키아누 리부스의 명연기가 생각나는 영화입니다. 은행강도를 잡으려 강도단에 잠입한 잠입형사역인데 익스트림을 즐기는 보니(패트릭 스웨이지)에 완전히 반해버려 보니의 꿈인 가장 큰 파도를 타는 꿈을 위해서 체포를 포기합니다. 영화 마지막 장면은 폭풍속 집채만 한 파도 속으로 보니가 뛰어들고 자니(키아누 리부스)는 체포를 포기하는 장면으로 끝이 납니다. 보니가 죽었는지 살았는지는 아무도 알 수 없습니다. 비트코인 투자를 하시는 여러분들도 이미 가상의 익스트림을 하고 있다고 저는 생각합니다. 저는 여러분들이 보니 같은 사람이 아니었으면 합니다.

반응형
LIST

맨위로버튼이미지

1. 들어가기 전에

혹시 이전 회차 에 결과에 실망하신 분들이 있으신가요? main.py 중간 쯤에 있는 add_macd의 수치를 약간 바꾸어 보았습니다.  변경 부분  bcondition.add_macd(12309)  그랬더니 

12, 26, 9와는 완전 다른 결과를 출력 하고 있습니다. 제가 이전 회차에서 왜 f(n)에서 f(x)를 찾는 작업 이라고 말씀 드린 이유를 위 그림을 보시면 잘 알 수 있습니다. 다시 돌아가서 macd의 기본 값이 왜 12, 26, 9일까요? 예전에 거래일은 6거래일 즉 토요일도 장이 열리던 시절이 있었습니다. 그럼 12는 2주 26은 한달 그리고 9는 2번주의 중간 정도 된다는 것을 아실 겁니다. 하지만 잘 생각해 보시면 bitcoin거래는 365일 24시간 계속 되는 거래 입니다. 애초에 12, 26, 9 값이 맞을 리가 만무 합니다. 그렇다고 12, 30, 9가 꼭 정답이라고도 할 수 없습니다. 계속적으로 값을 대입하다보면 일일 거래 건수와 수익률이 최적인 어떤 상태가 나올 겁니다. 우리는 그기를 찾으면 됩니다. 지금은 12, 30, 9로 값을 셋팅 하고 한번 진행 해 보도록 하겠습니다. 

 

2. Bithumb 거래소 연동 하기

제가 시작한지 얼마 안돼서 경험이 그렇게 많은 편은 아닙니다. 하지만 Bithumb의 API나 시스템은 실명스럽기 그지 없네요. 하긴 지금 저희가 사용하려는 pybithumb lib는 Bithumb의 정식 lib는 아닌듯 합니다. 홈페이지 어디에도 이 lib를 사용하라는 말은 없습니다. 대신 rest api에 대한 가이드와 설명이 있습니다. 지금은 시간이 안돼서 rest api로 다시 lib를 제작할 수 는 없지만 실제 rest api에 있는 내용중 pybithumb에는 없는 것들이 있습니다. 이런 문제를 제치고 일단 api오류가 생각보다 많이 발생 합니다. 특히 get_balance함수(잔고 조회)의 경우 3번중 1번은 오류입니다. 또 다른 문제는 구조적인 문제 입니다. 이 문제에 직면하고는 bitcoin 자동 매매에 상당한 실망을 한 상태 입니다. 주식의 경우 풍부한 유동성과 함께 여러가지 체결방법과 가격대별로 자동으로 호가에 대한 단위와 구간이 있습니다. 하지만 bitcoin의 경우 이런 구간에 대한 설정이 상당히 엉성하고 형편 없습니다. 이런 부분까지 고려한 프로그램이 나와야 할 것으로 보입니다. 

예를 들면 주식의 경우 주가가 만원 미만인 경우 100원단위 호가로 호가를 호출 한다면 만원 이상이 되면 자동적으로 호가 단위가 500원 단위 그 이상이 되면 호가 단위는 1000원 이렇게 계속 호가 단위가 바뀌는 구간이 있습니다. 하지만 bitcoin은 그런 구간이 없어서 호가 사이 금액 차이가 엄청 납니다. 그래서 자동매매가 정상적으로 동작하려면 시장가 매매가 되어야 하는데 이 시장가 매매가 호가 사이 금액 차이 때문에 우리가 원하는 금액으로 매수가 안되는 단점을 보입니다. 이를 보완 할 방법은 차차 생각 해 보도록 하겠습니다.

소스 내용(BithumbTrade.py)

import pybithumb
import math
import time as datetime
import sys
import os

class BithumbTrade:
    def __init__(self, bithumb, logger, ticker):
        self.bithumb = bithumb
        self.logger = logger
        self.ticker = ticker

    ###잔고 조회
    def get_balance(self):
        for i in range(2):
            balance = self.bithumb.get_balance('BTC')
            self.logger.info("잔고조회:" + str(balance))
            #수익률 구하기
            if balance is not None:
                return balance
            else:
                self.logger.warn("Bithumb API오류")
                datetime.sleep(1)

    def buy_crypto_currency(self, timing_price):
        while True:
            try:
                balance = self.get_balance()   #현금 보유 금액
                krw = balance[2] - balance[3]  #현금 보유 금액
                possible_krw = krw * 0.9
                possible_krw = possible_krw - (possible_krw % 1000)  #천원 단위로 매매함.
                if possible_krw < 500:
                    self.logger.info("보유 금액이 너무 작습니다.:%.2f" % krw)
                    return

                while True:
                    orderbook = pybithumb.get_orderbook(self.ticker)   #호가창 정보
                    self.logger.info("orderbook:%s", str(orderbook))
                    sell_price = orderbook['asks'][0]['price']
                    sell_volume = orderbook['asks'][0]['quantity']  #quantity
                    if sell_price > timing_price:
                        sell_price = timing_price
                    unit = possible_krw/float(sell_price)   #호가창 가격에서 첫번재 가격으로 가능 금액을 나눈다.
                    unit = math.trunc(unit * 10000) / 10000  #소수점 네자리 이상 버림.
                    self.logger.info("%s --> unit : %.5f : sell_price: %.2f sell_volume: %.5f" % (self.ticker, unit, sell_price, sell_volume))
                    self.logger.info("%s --> unit : %.5f : sell_price: %.2f" % (self.ticker, unit, timing_price))
                    if(unit == 0):
                        self.logger.info("보유 금액이 너무 작습니다.:%.2f" % krw)
                        return
                    elif(unit < 0.001):
                        self.logger.info("unit이 최소단위보다 작습니다.: %.4f", unit)
                        return

                    sell_price = int(sell_price)
                    self.logger.info("timing_price:%d, unit:%.5f, possible_krw:%.2f" % (sell_price, unit, possible_krw))
                    order = self.bithumb.buy_limit_order(self.ticker, sell_price, unit, payment_currency="KRW")   #매수 주문
                    if order is not None and type(order) is tuple:
                        self.logger.info("order success:%s, timing_price:%d, sell_price:%d" % (str(order), timing_price, sell_price))
                        return
                    elif order is not None:
                        self.logger.warn("order `fail:" + str(order))
                        if order.get('status') == '5500':
                            possible_krw = possible_krw - 10000  #만원씩 빼서 재 도전
                            unit = possible_krw/float(sell_price)   #호가창 가격에서 첫번재 가격으로 가능 금액을 나눈다.
                            unit = math.trunc(unit * 10000) / 10000  #소수점 네자리 이상 버림.
                            self.logger.info("%s --> unit : %.4f" % (self.ticker, unit))
                            datetime.sleep(1)
                            continue
                        elif order.get('status') == '5600':
                            orderbook = pybithumb.get_orderbook(self.ticker)   #호가창 정보
                            self.logger.info("orderbook:%s", str(orderbook))
                            sell_price = orderbook['asks'][0]['price']
                            unit = possible_krw/float(sell_price)   #호가창 가격에서 첫번재 가격으로 가능 금액을 나눈다.
                            unit = math.trunc(unit * 10000) / 10000  #소수점 네자리 이상 버림.
                            self.logger.info("%s --> unit : %.4f" % (self.ticker, unit))
                            return
                    else:
                        datetime.sleep(1)
            except Exception as ex:
                exc_type, exc_obj, exc_tb = sys.exc_info()
                fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
                self.logger.error("buy_crypto_currency -> exception! %s : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))
                datetime.sleep(1)

    def sell_crypto_currency(self, timing_price):
        while True:
            try:
                unit = self.bithumb.get_balance(self.ticker)[0]
                if(unit == 0):
                    self.logger.info("보유 코인(%s)이 없습니다." % self.ticker)
                    return

                orderbook = pybithumb.get_orderbook(self.ticker)   #호가창 정보
                self.logger.info("orderbook:%s", str(orderbook))
                # order = bithumb.sell_market_order(ticker, unit, payment_currency="KRW")
                sell_price = int(timing_price)
                order = self.bithumb.sell_limit_order(self.ticker, sell_price, unit, payment_currency="KRW")   #매수 주문
                self.logger.info("sell_market: ticker: %s, unit:%.2f" % (self.ticker, unit))
                if order is not None and type(order) is tuple:
                    self.logger.info("order success:%s, timing_price:%d" % (str(order), timing_price))
                    return
                elif order is not None:
                    self.logger.warn("order fail:" + str(order))
                    return
                else:
                    datetime.sleep(1)
            except Exception as ex:
                exc_type, exc_obj, exc_tb = sys.exc_info()
                fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
                self.logger.error("sell_crypto_currency -> exception! %s : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))
                datetime.sleep(1)

위의 소스를 적용하기 전에 먼저 main.py가 있는 디렉토리에 private 라고 폴더를 하나 만듭니다. 

소스 내용(BithumbKey.py)

con_key = 'bithumb에서 발급받은 api key'
secu_key = 'bithumb에서 발급받은 secu key'

 혹시 git이나 외부 소스 관리 플랫폼에 소스를 올리더라도 이 private 폴더는 .gitignore에 추가 하여 BithumbKey.py파일이 안 올라 가게 조치 하시기 발랍니다. 

그리고 main.py를 대폭 수정 했습니다. 

소스 내용(main.py)

from getBithumbData import ScrapCurrBithum
from BithumbCondition import BithumbCondition
from BithumbTrade import BithumbTrade
import time as datetime
import sqlite3
import sys
import os

import logging
import logging.config
import json

import pybithumb
import private.BithumbKey as key

config = json.load(open('./logging.json'))
logging.config.dictConfig(config)

buy_func = lambda x, dataframe: x >= 2 and dataframe.loc[x, 'flag1'] > 0 and dataframe.loc[x-1, 'flag1'] <= 0 and dataframe.loc[x-2, 'flag1'] <= 0
sell_func = lambda x, dataframe: x >= 2 and dataframe.loc[x, 'flag1'] <= 0 and dataframe.loc[x-1, 'flag1'] > 0 and dataframe.loc[x-2, 'flag1'] > 0

BASE_BALANCE = 1000000 # 초기 투자금 100만원
# BASIC_FEES = 0.0005

if __name__ == '__main__':
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.DEBUG)

    dbname = "bithum5mMACD1.db"
    sqlcon = sqlite3.connect(dbname) 

    bcondition = BithumbCondition(logger, BASE_BALANCE, buy_func, sell_func)
    scb = ScrapCurrBithum()
    bithumb = pybithumb.Bithumb(key.con_key, key.secu_key)
    trade  = BithumbTrade(bithumb, logger, 'BTC')
    last_time = ""
    krw = 0
    while True:
        try:
            start_time = datetime.time()
            ###현재가 구하기 
            try:
                price = pybithumb.get_current_price("BTC")
                logger.info("현재가 : %.4f" % price)
            except Exception as ex:
                exc_type, exc_obj, exc_tb = sys.exc_info()
                fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
                logger.error("get_current_proce -> exception! %s : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))
                
            ###잔고 조회
            balance = trade.get_balance()
            logger.info("잔고조회:" + str(balance))

            #수익률 구하기
            if balance is not None:
                rate = ((balance[0]*price + balance[2] - BASE_BALANCE)*100)/BASE_BALANCE
                logger.info("수익률: %.2f %%" % rate)
                #현재의 원화 잔고 얻기
                krw = balance[2] - balance[3]

            orderbook = pybithumb.get_orderbook("BTC")
            logger.info("orderbook:%s", str(orderbook))
            if orderbook is not None:
                #최우선 매도 호가 구하기
                asks = orderbook['asks']
                sell_price = asks[0]['price']  #==> 최우선 매도 호가 
                sell_volume = asks[0]['quantity']  #quantity
                unit = krw/float(sell_price)          #==> 매도 수
                logger.info("최우선 매도호가: %.2f 매도수량:%.5f 수량:%.4f" % (sell_price, sell_volume, unit))

            df = scb.get_data()
            bcondition.append_dataframe(df)
            #전체 보조 지표를 포함하지 않고 개별 보조 지표를 사용하도록 하겠습니다.
            bcondition.add_macd(12, 30, 9)
            bcondition.add_moving_avg240()
            df,timing = bcondition.sell_and_buy()  #macd 계산
            savedf = df.tail(1)
            lastdata = savedf.loc[savedf.index.max()]
            lasttime = lastdata['time']
            close = lastdata['close']
            sma240 = lastdata['sma240']
            sma240_incli = lastdata['sma240_incli']
            sma240vsclose = lastdata['sma240vsclose']
            sma240clsmax =  lastdata['sma240clsmax']
            sma240clsmin =  lastdata['sma240clsmin']
            sma240clsminsig1 = lastdata['sma240clsminsig1']
            sma240clsminsig2 = lastdata['sma240clsminsig2']
            sma240clsmaxsig1 = lastdata['sma240clsmaxsig1']
            sma240clsmaxsig2 = lastdata['sma240clsmaxsig2']
            logger.info("마지막 데이터:" + str(lastdata))
            logger.info("sma240:" + str([sma240, sma240_incli, sma240vsclose, sma240clsmax, sma240clsmin]))
            logger.info("sma240:" + str([sma240clsminsig1, sma240clsminsig2, sma240clsmaxsig1, sma240clsmaxsig2]))
            if sma240_incli >= 0 :
                logger.info("sma240==> 상승 추세" )
            else:
                logger.info("sma240==> 하락 추세" )
            
            if sma240vsclose >= 0:
                logger.info("sma240 보다 상위 위치==> 상승 추세" )
            else:
                logger.info("sma240 보다 하위 위치==> 하락 추세" )

            if lasttime == timing['time'] and timing['sellbuy'] == 'sell':
                logger.info("매도 타이밍" + str(timing))
                trade.sell_crypto_currency(timing['price'])
            elif lasttime == timing['time'] and timing['sellbuy'] == 'buy':
                logger.info("매수 타이밍" + str(timing))
                trade.buy_crypto_currency(timing['price'])

            get_time= datetime.strftime("%Y%m%d%H%M%S", datetime.localtime(lasttime/1000))

            if last_time == "":
                df.to_sql(name=dbname, con=sqlcon, if_exists='replace')
            elif last_time != get_time:
                last_time = get_time
                savedf.to_sql(name=dbname, con=sqlcon, if_exists='append')

            logger.info("MACD1 최종:")
            logger.info(df)
            strdt1 = datetime.strftime("%Y%m%d", datetime.localtime(df.loc[df.index.max()-2, 'time']/1000))
            strdt2 = datetime.strftime("%Y%m%d", datetime.localtime(df.loc[df.index.max()-1, 'time']/1000))
            logger.info("마지막1전: %s, 마지막:%s", strdt1, strdt2)
            if(strdt1 != strdt2 ):  #날짜가 변경되었다면
                logger.info('자정이면 메모리를 일부 크리어한다.')
                df = df.tail(df.index.max() - 12*24)
                df.reset_index(drop=True, inplace=True)

            end_time = datetime.time()
            if (end_time - start_time) > 0 and (end_time - start_time) <= 5*60:
                datetime.sleep(5*60 - (end_time - start_time))
        except Exception as ex:
            ## NULL 데이터 인 경우 merge가 안된다.
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            logger.error("`add_closeminmax -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

일단은 이 소스를 기반으로 운영하면서 수정이 필요해 보입니다. 지금 현재 Bithumb에서는 Trade에 두가지 매매 방법이 있습니다. 시장가 매매와 지정가 매매입니다. 주식의 경우 매매 방법은 총 12가지로 정부에서 매매방법에 대한 가이드를 하고 수정한 것들입니다. 같은 시장가 매매 라고 해도 지금의 coin 거래소의 방법들(Bithumb외의 다른 곳도 거의 비슷합니다.)과는 전혀 다르다고 보시면 됩니다. 우리가 트래킹한 수익률과 실제 매매의 수익률이 일치 할려면 현재의 시스템으로는 한계가 있어 보입니다. 현재는 지정가로 하면 매매가 안되고 시장가로 하면 손해가 발생합니다. 그래서 일단 240 이동 평균선(5분 데이터의 경우 10시간임)으로 추세를 예측하는 부분을 삽입했습니다. 이 후 데이터가 싸이면 이부분을 이용해서 언제 거래를 지속 할지 그리고 언제 지정가 매매를 할지 언제 시장가 매매를 할지를 판단해 보도록 하겟습니다. 

 

3. 마치며

오늘 회차는 어떠셨나요? 제가 생각했던 것 보다  거래소의 수준이 한심한 수준이네요. 현재 1위 거래소는 upbit, 2위는 bithumb 3위는 코인빗 4위가 코인원 5위가 프로빗 익스체인지 라고 하네요. 제가 생각하는 주식과 코인의 가장 큰 차이는 주식의 경우 증권거래소를 비롯하여 중간에 많은 기관이 서로를 보조하는 기능이 있습니다. 코인의 경우는 그러한 기능은 간소하지만 그러다보니 문제점이 많이 보입니다. 어쨌든 그러한 문제는 우리 서서로 풀어야 한다는게 참 안타깝네요. 다음차에 데이터가 모이면 macd와 추세를 이용하여 현재의 문제점을 보안 해보도록 하겠습니다. 

반응형
LIST

맨위로버튼이미지

1. 들어가기 전에

혹시 예전 중고등학교 시절 배운 연역법과 귀납법에 대하여 기억을 하시나요? 수학 시간에 무언가를 증명하라는 문제가 나오면 대부분은 연역법을 사용하여 A=B, B=C 그러므로 A=C이다고 증명을 했을 것입니다. 하지만 가끔은 연역법을 사용할 수 없는 것들이 있습니다. 예를 들면 수열이 그렇습니다. f(n) = f(n-1) + 3이라는 수식이 주어 진다면 f(1)은 5라고 한다면 f(0)는 2가 되고 f(2) = f(1) + 3 = 8 .. 이런 식으로 증가하는 것은 알겠는데 그래서 f(x)는 뭐지? 수학을 잘하셨던 분들은 금방 f(x) = 3*x + 2 인 것을 눈치 채었을 듯합니다만 수학에 잰뱅이셨던 분들도 우리 주제를 읽어시는데는 아무 상관이 없습니다. 왜 장황한 이야기를 하느냐 하면 지금 우리가 하고 있는 python Bitcoin 자동매매 프로그램이 해야 하는 목표가 바로 f(n)식을 이용하여 f(x)식을 구해야 하는 일이기 때문입니다. 저는 1970년 생입니다만 우리 시절에는 미적분이 너무나도 어렵게 문제가 나오는 바람에 수열과 통계 부분은 너무나 하찮게 다루어 버려서 아마도 지금의 제 나이 때 분들은 우리가 하려는 일에 대한 이해도가 그렇게 높지 않을 수도 있겠다는 생각을 하면서 이 글을 작성하고 있습니다.

이번 회차에서는 이전에 만든 5분 데이터와 이전 회차에서 만든 보조지표 클래스를 어떻게 사용하는 지에 대한 간단한 예시를 통해서 보조지표에 대한 아이디어를 제시합니다. 여러분들이 다른 아이디어를 첨가한다면 더 완벽한 전략을 만들 수도 있을 것입니다. 

2. Condition Class 만들기

소스 내용(BithumbCondition.py)

import time as datetime
import pandas as pd
from addAuxiliaryData import Auxiliary
class BithumbTrading:
    def __init__ (self, logger, base_balance, buy_condition, sell_condition):
        self.logger = logger
        self.base_balance = base_balance
        self.buy_condition = buy_condition
        self.sell_condition = sell_condition
        self.ad = Auxiliary(self.logger)
        self.df = None

    def add_auxiliary(self):
        self.ad.add_auxiliary(self.df)
        
    def add_macd(self, short, long, signal):
        macd, macdsignal, macdhist = self.ad.add_macd(self.df, short, long, signal)
        self.df['macd1'] = macd
        self.df['signal1'] = macdsignal
        self.df['flag1'] = macd - macdsignal
        
    def append_dataframe(self, df):
        if df is None:
            return
        df=df.truncate(after=(df.index.max()-1))  #마지막 데이터는 짤라 낸다.
        
        if(self.df is None):
            self.df = df
        else:
            self.df = pd.concat([self.df, df], ignore_index=True)

        if self.df is None:
            return

        self.df.sort_values(by='time', axis = 0)
        self.df.reset_index(drop=True, inplace=True)

        self.df = self.df.drop_duplicates(['time'], keep='first', inplace=False, ignore_index=True)
        self.df.reset_index(drop=True, inplace=True)
        self.logger.debug("add new data")
        self.logger.debug(self.df)

    def buy_scenario(self, index):
        # print("buy_scenario max : %d : %d, return: %d" % (self.df.index.max(), index, self.buy_condition(index)))
        return self.buy_condition(index, self.df)

    def sell_scenario(self, index):
        # print("sell_scenario max : %d : %d, return: %d" % (self.df.index.max(), index, self.buy_condition(index)))
        return self.sell_condition(index, self.df)

    def sell_and_buy(self):
        self.origin_balance = self.base_balance    # 초기 투자금 100만원
        self.year_balance = self.origin_balance
        self.balance = self.origin_balance
        self.remains = 0
        self.sell_balance = 0
        self.unit_balance = 0

        for i in self.df.index:
            # print("수익률 계산 :%s"  % datetime.ctime(df.loc[i, 'time']/1000))
            self.close = self.df.loc[i, 'close']
            time = self.df.time.loc[i]/1000

            if (time % (365*24*60*60)) == 0:  # 1년에 한번씩 년간 수익률을 계산하기 위해서
                # print("unit : %d" % self.unit_balance)
                self.logger.debug(datetime.strftime('%Y년%m월%d일', datetime.localtime(self.df.loc[i, 'time']/1000)))
                if self.unit_balance > 0 :
                    self.sell_balance = self.unit_balance * self.close
                    self.balance = self.balance + self.sell_balance
                    self.year_balance = self.balance
                else:
                    self.year_balance = self.balance

                self.logger.info("조정 원금: %.3f" % self.year_balance)

            if self.buy_scenario(i) :  #매수
                self.sell_balance = self.balance - (self.balance % 1000)
                # print("buy condition sell_balance: %.2f balance: %.2f unit: %.2f" % (sell_balance, balance, unit_balance))

                if self.sell_balance > 0:
                    self.remains = self.balance - self.sell_balance
                    self.balance = self.remains
                    self.remains = 0
                    self.unit_balance = self.sell_balance / self.close
                self.df.loc[i, 'balance'] = self.balance
                self.df.loc[i, 'unit'] = self.unit_balance
            elif self.sell_scenario(i):   #매도
                # print("sell condition close: %.2f balance: %.2f unit: %.2f" % (close, balance, unit_balance))
                if self.unit_balance > 0:
                    self.sell_balance = self.unit_balance * self.close
                    self.balance = self.balance + self.sell_balance
                    self.unit_balance = 0
                    self.sell_balance = 0
                self.df.loc[i, 'balance'] = self.balance
                self.df.loc[i, 'unit'] =  self.unit_balance
            else:
                self.df.loc[i, 'balance'] = self.balance
                self.df.loc[i, 'unit'] = self.unit_balance

            if self.year_balance != 0:
                self.df.loc[i, 'year_rate'] = ((self.df.loc[i, 'unit'] * self.df.loc[i, 'close'] + self.df.loc[i, 'balance']) - self.year_balance) * 100 / self.year_balance
            else:
                self.df.loc[i, 'year_rate'] = 0

            self.df.loc[i, 'rate'] = ((self.df.loc[i, 'unit'] * self.df.loc[i, 'close'] + self.df.loc[i, 'balance']) - self.base_balance) * 100 / self.base_balance
        return self.df

첫줄에 time lib를 import 하면서 이름을 as로 datetime으로 변경했습니다. 이유는 pandas에서 컬럼명을 attribute형으로 사용하는 경우가 있는데 이때 우리 데이터 중에 time이 있어 혼돈이 있을 것을 대비하여 변경했습니다. 위 소스는 백테스팅을 고려해서 만든 것이라 지금은 당장 사용하지 않는 부분들도 포함이 되어 있습니다. 

자 그럼 main.py를 변경하여 데이터를 연동하도록 하겠습니다. 

 

소스내용(main.py)

from getBithumbData import ScrapCurrBithum
from BithumbCondition import BithumbTrading as BitTra
import time as datetime
import sqlite3

import logging
import logging.config
import json

config = json.load(open('./logging.json'))
logging.config.dictConfig(config)

buy_func = lambda x, dataframe: (x >= 2 and dataframe.loc[x, 'flag1'] > 0 and dataframe.loc[x-1, 'flag1'] <= 0 and dataframe.loc[x-2, 'flag1'] <= 0)
sell_func = lambda x, dataframe: (x >= 2 and dataframe.loc[x, 'flag1'] <= 0 and dataframe.loc[x-1, 'flag1'] > 0 and dataframe.loc[x-2, 'flag1'] > 0)

BASE_BALANCE = 1000000 # 초기 투자금 100만원

if __name__ == '__main__':
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.INFO)

    dbname = "bithum5mMACD1.db"
    sqlcon = sqlite3.connect(dbname) 

    bt = BitTra(logger, BASE_BALANCE, buy_func, sell_func)
    scb = ScrapCurrBithum()

    last_time = ""
    while True:
        start_time = datetime.time()
        df = scb.get_data()
        bt.append_dataframe(df)
        #전체 보조 지표를 포함하지 않고 개별 보조 지표를 사용하도록 하겠습니다.
        #bt.add_auxiliary()
        bt.add_macd(12, 26, 9)
        df = bt.sell_and_buy()
        savedf = df.tail(1)
        get_time= datetime.strftime("%Y%m%d%H%M%S", datetime.localtime(savedf.loc[savedf.index.max(), 'time']/1000))

        if last_time == "":
            df.to_sql(name=dbname, con=sqlcon, if_exists='replace')
        elif last_time != get_time:
            last_time = get_time
            savedf.to_sql(name=dbname, con=sqlcon, if_exists='append')

        logger.info("MACD1 최종:")
        logger.info(df)
        strdt1 = datetime.strftime("%Y%m%d", datetime.localtime(df.loc[df.index.max()-2, 'time']/1000))
        strdt2 = datetime.strftime("%Y%m%d", datetime.localtime(df.loc[df.index.max()-1, 'time']/1000))
        logger.info("마지막1전: %s, 마지막:%s", strdt1, strdt2)
        if(strdt1 != strdt2 ):  #날짜가 변경되었다면
            logger.info('자정이면 메모리를 일부 크리어한다.')
            df = df.tail(df.index.max() - 12*24)
            df.reset_index(drop=True, inplace=True)

        end_time = datetime.time()
        if (end_time - start_time) > 0 and (end_time - start_time) <= 5*60:
            datetime.sleep(5*60 - (end_time - start_time))

sqlite3을 이용하여 수신 데이터를 DB에 저장하므로 우리가 만든 폴더에 "bithum5mMACD1.db" 파일이 생성되었음을 확인할 수 있을 것입니다. 이 회차에서 가장 어렵고 중요한 부분은 "append_dataframe" 함수입니다. 

이전에 만든 getBithumbData.py에서 close와 volume은 float형으로 값을 변경했지만 time은 값을 변경하지 않았습니다. 이유는 time은 RDBMS의 key처럼 동작 하지만 우리가 주로 사용하는 intel cpu의 경우 부동소수점의 값은 "그때그때 달라요" 값입니다. 즉 float형을 key값으로 사용하면 여러분의 데이터는 유일성을 보장할 수 없습니다. 하지만 python은 어떤 면에서는 데이터형 검사가 엄격해서 정수형 데이터로는 어려운 수식 계산 시 오류가 발생합니다. 내용을 쭉 읽어 보면 double 형 데이터가 아니어서 계산이 불가하다는 내용입니다. python을 쉽게 봤던 사람들은 여기서 멘붕에 빠지게 됩니다. 참 그리고 우리가 받고 있는 Bithumb의 5분 데이터는 실제는 1주일치 데이터이며 마지막 한 줄은 "완전 실시간" 데이터입니다. 그러다 보니 마지막 한 줄이 보조지표의 정확성에 방해가 됩니다. 보조지표의 대원칙은 간격이 일정해야 하고 유일성이 보장되어야 합니다. 그렇지 않으면 계산은 아무런 의미가 없습니다. 

 

3. DB 파일 보기 (https://sqlitebrowser.org/dl/)

제목 옆에 링크를 클릭하시면 sqlitebrowser를 다운 받을 수 있는 사이트로 접속합니다. 거기서 자신에게 맞는 버전을 다운로드하시고 설치하시면 DB파일을 볼 준비가 완료됩니다. 

DB Browser를 실행한 다음 File--> 데이터베이스 열기-->해당 폴더의 db파일 지정하시면 위와 같이 DB파일을 보실 수 있습니다. 2021년 오늘 시점 일론머스크의 발언과 미정부의 자본세 발표 및 우리나라도 비트코인 규제 방침이 발표되면서 시장이 급락하고 있습니다. 마지막 컬럼 두 개가 일 년 수익률과 전체 수익률입니다. 현재 -16.7%를 기록하고 있네요. ^^

4. 데이터 분석

DB Browser에서 File-->내보내기-->CSV로 내보내기를 선택합니다. 같은 폴더에 bithum5mMACD1.csv로 파일을 저장합니다. Excel을 실행하여 csv파일을 엽니다. 그리고 MACD가 처음 시작하는 32행까지를 삭제하여 데이터를 정리 합니다. shift키를 눌은 상태에서 close컬럼과 flag1컬럼을 선택 합니다. 그다음 삽입에서 차트를 삽입합니다. 차트를 클릭하면 메뉴에 차트 디자인이 보일 겁니다. 차트 디자인에서 차트 종류 변경을 클릭합니다. 

그럼 아래 차트가 만들어집니다.

이번에는 close컬럼과 macd1, signal1컬럼을 선택하고 데이터 범위를 하루치(12X24=288)만 선택하여 차트를 만듭니다. 

그리고 차트 디자인에서 아래와 같이 클릭하고 확인을 클릭합니다.

아래의 차트를 보면서 데이터 분석을 해 보도록 하겠습니다.

먼저 오늘 데이터 연동을 하면서 적용한 보조지표는 MACD입니다. MACD는 단기이평선과 장기이평선이 서로 만났다가 떨어지고 다시 만나는 반복되는 상황에서 macd값이 상위에 있으면 매수 macd선이 하위로 오면 매도를 하게 프로그램하면 됩니다. 마지막 차트를 보면 거의 정확한 시점에서 매수와 매도가 발생하고 있음을 알 수 있습니다. 그럼에도 불구하고 전체 수익률은 -15~-17%사이를 움직이고 있습니다. 이는 macd의 약점 중에 하나인 허매수 신호가 발생하고 있기 때문입니다. 허매수 신호는 완전 폭락이 발생하는 구간보다는 옆으로 흔들리면서 이동하는 구간에서 많이 발생하고 있음을 차트로 알 수 있습니다. 전문가들이 macd의 가장 큰 약점이라고 하는 후시성은 위에 차트에서도 알 수 있듯이 5분 데이터에서는 큰 영향이 없습니다. 오히려 급락 구간을 잘 피해서 매수를 하고 있음을 알 수 있습니다. 그리고 급등 구간에서도 매수가 되어 큰 성과를 내고 있음을 알 수 있습니다.  그렇다면 flag값은 무엇일까요? MACD Oscillator라고도 하는 값입니다. 이 값이 양수이면 매수 이 값이 음수이면 매도를 하면 되기 때문에 프로그램에서 매수매도를 구현하기가 쉽습니다. BithumbCondition.py에 BithumbTrading class에 들어가는 lambda 함수 식의 의미가 바로 flag를 기준으로 매수 매도를 하는 공식입니다. 

 

5. 마치며

오늘 회차는 어떠셨나요? 데이터 연동 및 차트 분석과 데이터 분석에 대하여 알아보았습니다. 그런데 아무리 하락장이라고 하지만 수익률이 너무 심하지 않나 싶을 겁니다. 우리의 목표가 여러 보조 지표를 활용해서 기대 수익률을 낮추면서 하락장에서 수익률을 잃지 않는 프로그램 개발이라는 것을 잊지 마세요. 오히려 지금과 같은 과하락장이 우리 프로그램에는 좋은 기회입니다. 

반응형
LIST

맨위로버튼이미지

1. 들어가기 전에

우리가 하려는 python Bitcoin 자동 매매는 과학의 영역이 아닙니다.

치료 행위를 예를 들면 아직도 많은 양의 쪽 의사들은 한방 의술을 비과학이라고 하고 절대로 해서는 안 되는 행위라고 하시는 분들이 상당히 많습니다.

양의 쪽 의술들 중 상당수는 과학적인 검증이 완료되었지만 그래도 개개의 의사마다의 경험치가 있고 이 경험치가 환자의 생명에 지대한 영양을 미칩니다.

우리가 하려고 하는 프로그램은 이러한 경험치를 최대한 짧은 시간에 많이 늘리기 위한 것이며 따라서 아무리 잘 짠 프로그램이라고 해도 손실은 피할 수 없습니다.

모든 금융상품들은 급등락을 반복하고 일정 범위의 등락은 우리에게 큰 영향을 미치지 않지만 심한 급등락이 발생한다면 일반인뿐만 아니라 기관이라고 하는 사람들 조차 큰 피해를 보게 됩니다.

 

2020년의 코로나 사태나 2003년 동일본 대지진 2008년 리먼브라더스 사태 2001년 911 사태 1997년 한국의 IMF사태(같은 시기 아시아 다른 나라도 같은 위기를 겪었습니다.)등의

경기 급락 사건이 발생하게 되면 외국 지분의 상당수는 돈을 빌려서 우리나라의 주식에 투자하고 있기에

아무리 우량주라도 팔아서 빚을 갚는 전략을 치하게 되기 때문에

우리의 일반 논리로 저가 고가 우량주 잡주의 개념은 의미가 없게 됩니다.

기존의 방식은 말이죠 그랬다는 겁니다.

제가 궁극적으로 하고자 하는 것은 우리가 다른 일을 하고 있는 중에도 시장의 폭락을 예측이 아닌 분석을 하고

폭락장에 휘둘리지 않으면서 다시 장이 상승할 경우 가장 아래 꼭짓점을 감지하여 매수하여

최고점까지 매도하지 않고 버티는 비이성적인 프로그램을 만드는 것이 목표입니다.

 

제가 장황한 글을 쓰는 이유는 일부 사람 중 비과학을 과학적으로 사고하려는 사람들이 있는데

이런 분들은 결코 성공할 수 없다는 것을 말씀드리고자 함입니다.

 

비과학은 비이성적이고 비상식적이며 기존의 관념을 버릴 때 비로소 아이디어를 얻게 됩니다.

 

2. TA-LIB 설치하기

TA-LIB는 시리즈 데이터를 이용하여 기존 보조지표를 아주 빠르게 구해 주는 lib입니다.

그러나 불행히도 설치가 꽤 까다롭습니다. https://minjejeon.github.io/learningstock/2018/04/07/installing-and-using-talib.html 이 글을 참조해 보시기 바랍니다.

TA-LIB 홈페이지는 https://www.ta-lib.org/ 이 링크입니다. 

 

여기서 잠깐 왜 TA-LIB를 사용할까요?

아시다시피 pandas의 rolling 함수를 사용하면 쉽게 이동평균선을 구할 수 있습니다.

예를 들면 rolling(window=5).mean() 이렇게 하면 5일 이동평균이 구해집니다. 

1. 속도

TA-LIB는 어떤 pandas 모듈보다 7배 이상 빠릅니다.

이유는 내부적으로 c함수로 구현이 되어 있어서 훨씬 빠른 속도를 구현합니다.

단 그래서 설치하기가 쉽지 않습니다.

설치 파일은 python에 TA-LIB를 설치하려고 하는 순간 c compiler를 찾을 것입니다.

대부분의 분들은 당연히 설치가 안되어 있을 것이며 설치가 되어 있다고 해도 설치가 안 되는 것은 맞찬가지 입니다.

저의 경우는 ubuntu에서도 맞찬가지 였습니다.

-- 2024년 기준 지금은 잘 됩니다. 

그런데 먼저 c lib TA-LIB를 먼저 설치하고 그 다음 PIP로 TA-LIB를 설치한다면 바로 성공하실 수 있을 것 입니다. --

2. 종류

TA-LIB는 pandas로 구현하려면 알아야 하는 모든 공식을 미리 다 구현하여서 바로 사용하기만 하면 됩니다.

그래도 일부는 pandas를 사용합니다. 

3. 시간

우리에게는 많은 시간이 주어지지 않습니다.

어떤 경우 python의 for loop를 사용한다면 다음 데이터가 들어오는 5분 내에 많은 일을 해야 합니다.

1분 1초가 상당히 중요하게 다가옵니다. 

 

3. logger 설정하기

이전에 만들었던 main.py에 logger를 추가하도록 하겠습니다.

 

인터넷에 있는 자동매매 관련 프로그램들이 부족한 부분이 프로그램을 시스템화하는 부분에 상당히 부족한 부분을 보입니다.

그중 한 가지가 logger입니다.

주식이나 데이터 분석이 주업이신 분들의 특징은 python에서 이미 제공하는 기능을 새로 만들려고 한다는 것입니다.

하지만 직접 제작한 것은 이미 있는 것을 이기기가 쉽지 않습니다.

양쪽 모두 천재가 아니라면 말이죠.

 

소스 내용(main.py)

from getBithumbData import ScrapCurrBithum
import logging
import logging.config
import json

config = json.load(open('./logging.json'))
logging.config.dictConfig(config)

if __name__ == '__main__':
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.INFO)

    scb = ScrapCurrBithum()
    df = scb.get_data()
    logger.info(df)

소스내용(logging.json)

{
    "version": 1,
    "disable_existing_loggers": false,
    "formatters": {
      "basic": {
        "format": "%(asctime)s:%(module)s:%(levelname)s:%(message)s",
        "datefmt": "%Y-%m-%d %H:%M:%S"
      }
    },
    "handlers": {
      "console": {
        "class": "logging.StreamHandler",
        "level": "INFO",
        "formatter": "basic"
      },
      "file_debug": {
        "class": "logging.FileHandler",
        "level": "DEBUG",
        "formatter": "basic",
        "filename": "log/debug.log"
      },
      "file_error": {
        "class": "logging.FileHandler",
        "level": "ERROR",
        "formatter": "basic",
        "filename": "log/error.log"
      }
    },
    "loggers": {
      "__main__": {
        "level": "DEBUG",
        "handlers": ["console", "file_debug", "file_error"],
        "propagate": true,
        "filename": "macd.log"
      }
    }
  }

이전에 만들었던 bitcoin_trade1 폴더 아래 log폴더를 생성합니다. 그리고 python main.py를 실행해 봅니다. 

2021-05-23 13:05:57:main:INFO:               time       close     volume                 date
0     1621138200000  58900000.0   8.866844  2021-05-16 13:10:00
1     1621138500000  58886000.0   9.422226  2021-05-16 13:15:00
2     1621138800000  58889000.0   4.299981  2021-05-16 13:20:00
3     1621139100000  58862000.0   7.325870  2021-05-16 13:25:00
4     1621139400000  58650000.0   8.781141  2021-05-16 13:30:00
...             ...         ...        ...                  ...
1951  1621741200000  47258000.0  12.528850  2021-05-23 12:40:00
1952  1621741500000  47100000.0   8.641681  2021-05-23 12:45:00
1953  1621741800000  47116000.0   5.692299  2021-05-23 12:50:00
1954  1621742100000  47087000.0   2.980005  2021-05-23 12:55:00
1955  1621742400000  46890000.0  15.279345  2021-05-23 13:00:00

[1956 rows x 4 columns]

log폴더에 debug.log파일이 생성되고 아래와 같은 내용이 생겼다면 이제 여러분은 logger를 사용할 수 있게 되신 겁니다.

 

4. 보조지표 클래스 만들기

우리가 사용하려는 보조지표가 일반적일 수도 일반적이 아닐 수도 있습니다.

중요한 것은 이전의 관습을 버리고 오로지 우리의 아이디어 만으로 새로운 보조지표를 생성하시기 바랍니다. 

1. 기존 관습을 버려야 하는 이유

인터넷에 돌고 있는 자료 중 상당수가 '한빛미디어' 출판사의 '파이썬 증권 데이터 분석'을 토대로 한 변동성 돌파 전략에 대한 소스입니다.

 

유튜브에서도 상당수의 소스가 검증 없이 돌고 있습니다.

 

래리 윌리엄스의 변동성 돌파 전략이나 책이 잘못되었다는 게 아닙니다.

 

그런데 정말 이 변동성 돌파 전략은 안전한 투자 전략일까요?

 

여러분은 어떤 전략을 실행하기 전에 반듯이 백테스팅(과거 데이터를 이용하여 로직을 검증하는 테스트)을 실행해 보시고 데이터를 자신이 신뢰할 수 있을 때에 비로소 적용하시기 바랍니다.

 

다시 변동성 돌파 전략으로 돌아가서 변동성 돌파 전략이란 전일의 고가와 전일의 저가의 평균 또는 K상수를(일반적으로 0.5를 곱하면 평균이 되고 0.3을 곱하면 30%가 될 겁니다.)

곱하여 변동성을 구한 다음 오늘의 시가에 변동성 값을 더한 값 위로 상승하면 매수하여

종가에 모두 매도하는 전략입니다.

 

곰곰이 생각해 보시면 정말 의이 없는 미신이 따로 없습니다.

 

내가 생각하는 그 종목은 정말 저 방법 되로 움직일까요?

 

초반부터 하락한다면 매수조차 안돼서 아무런 피해가 없겠지만 대부분의 경우 장 초기에는 상승을 하다가 중반 이후에는 장이 빠지는 식의 거래가 주를 이루다 보니 이 전략의 약점은 바로 나타나기 시작했습니다.

 

매도 시점에는 이미 엄청난 손실을 기록하게 되는데 이게 끝이 아니라는 게 문제입니다. 

2. 그렇다면 다른 매매전략은?

제가 주목한 것은 MACD(Moving Average Convergence & Divergenc)를 이용한 매매 전략이었습니다.

MACD에 대한 내용은 https://md2biz.tistory.com/397 링크를 참조하시기 바랍니다. 

 

소스내용(addAuxiliaryData.py)

import time
import sys
import os
import talib
import time
import numpy as np

class Auxiliary:
    def __init__(self, logger):
        self.logger = logger

    ## NULL 데이터 인 경우 merge가 안된다.
    def add_closeminmax(self, df):
        try:
            self.logger.info("Close Min Max 구하기:%s"  % time.ctime(time.time()))
            df['clsmax'] =  df.close.rolling(window=30, min_periods=1).max()
            df['clsmin'] =  df.close.rolling(window=30, min_periods=1).min()
            df['clsminsig1'] = df.clsmin - df.clsmin.shift(2)    
            df['clsminsig2'] = df.close.shift(2) - df.clsmin    
            df['clsmaxsig1'] = df.clsmax - df.clsmax.shift(2)   
            df['clsmaxsig2'] = df.close.shift(2) - df.clsmax   

        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_closeminmax -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_moving_avg5(self, df):
        try:
            self.logger.info("5 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['sma5'] = talib.SMA(np.asarray(df.close), 5)
            df['sma5_incli'] = df.sma5 - df.sma5.shift()
            df['sma5vsclose'] = df.close - df.sma5
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_moving_avg5 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))
    
    def add_moving_avg20(self, df):
        try:
            self.logger.info("20 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['sma20'] = talib.SMA(np.asarray(df.close), 20)
            df['sma20_incli'] = df.sma20 - df.sma20.shift()
            df['sma20vsclose'] = df.close - df.sma20
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_moving_avg20 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))
    
    def add_moving_avg60(self, df):
        try:
            self.logger.info("60 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['sma60'] = talib.SMA(np.asarray(df['close']), 60)
            df['sma60_incli'] = df.sma60 - df.sma60.shift()
            df['sma60vsclose'] = df.close - df.sma60
            df['sma60clsmax'] =  df.sma60.rolling(window=30, min_periods=1).max()
            df['sma60clsmin'] =  df.sma60.rolling(window=30, min_periods=1).min()
            df['sma60clsminsig1'] = df.sma60clsmin - df.sma60clsmin.shift(2)    
            df['sma60clsminsig2'] = df.sma60.shift(2) - df.sma60clsmin    
            df['sma60clsmaxsig1'] = df.sma60clsmax - df.sma60clsmax.shift(2)   
            df['sma60clsmaxsig2'] = df.sma60.shift(2) - df.sma60clsmax   
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_moving_avg60 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_moving_avg120(self, df):
        try:
            self.logger.info("120 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['sma120'] = talib.SMA(np.asarray(df['close']), 120)
            df['sma120_incli'] = df.sma120 - df.sma120.shift()
            df['sma120vsclose'] = df.close - df.sma120
            df['sma120clsmax'] =  df.sma120.rolling(window=30, min_periods=1).max()
            df['sma120clsmin'] =  df.sma120.rolling(window=30, min_periods=1).min()
            df['sma120clsminsig1'] = df.sma120clsmin - df.sma120clsmin.shift(2)    
            df['sma120clsminsig2'] = df.sma120.shift(2) - df.sma120clsmin       
            df['sma120clsmaxsig1'] = df.sma120clsmax - df.sma120clsmax.shift(2) 
            df['sma120clsmaxsig2'] = df.sma120.shift(2) - df.sma120clsmax       
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_moving_avg120 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_moving_avg240(self, df):
        try:
            self.logger.info("240 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['sma240'] = talib.SMA(np.asarray(df['close']), 240)
            df['sma240_incli'] = df.sma240 - df.sma240.shift()
            df['sma240vsclose'] = df.close - df.sma240
            df['sma240clsmax'] =  df.sma240.rolling(window=30, min_periods=1).max()
            df['sma240clsmin'] =  df.sma240.rolling(window=30, min_periods=1).min()
            df['sma240clsminsig1'] = df.sma240clsmin - df.sma240clsmin.shift(2)   
            df['sma240clsminsig2'] = df.sma240.shift(2) - df.sma240clsmin        
            df['sma240clsmaxsig1'] = df.sma240clsmax - df.sma240clsmax.shift(2)  
            df['sma240clsmaxsig2'] = df.sma240.shift(2) - df.sma240clsmax        
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_moving_avg240 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_moving_ema12(self, df):
        try:
            self.logger.info("지수 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['ema12'] = talib.EMA(np.asarray(df['close']), 12)
            df['ema12clsmax'] =  df.ema12.rolling(window=30, min_periods=1).max()
            df['ema12clsmin'] =  df.ema12.rolling(window=30, min_periods=1).min()
            df['ema12minsig1'] = df.ema12clsmin - df.ema12clsmin.shift(2)   
            df['ema12minsig2'] = df.ema12.shift(2) - df.ema12clsmin         
            df['ema12maxsig1'] = df.ema12clsmax - df.ema12clsmax.shift(2)   
            df['ema12maxsig2'] = df.ema12.shift(2) - df.ema12clsmax         
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_moving_ema12 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_moving_ema26(self, df):
        try:
            df['ema26'] = talib.EMA(np.asarray(df['close']), 26)
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_moving_ema26 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_moving_wma(self, df):
        try:
            self.logger.info("가중 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['wma12'] = talib.WMA(np.asarray(df['close']), 12)
            df['wma26'] = talib.WMA(np.asarray(df['close']), 26)
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_moving_wma -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_bbands(self, df):
        try:
            self.logger.info("볼리져 밴드 구하기:%s"  % time.ctime(time.time()))
            upper, middle, lower = talib.BBANDS(np.asarray(df['close']), timeperiod=20, nbdevup=2, nbdevdn=2, matype=0)
            df['dn']=lower
            df['mavg']=middle
            df['up']=upper
            df['bbands_sub']=df.up - df.dn
            df['bbands_submax'] =  df.bbands_sub.rolling(window=30, min_periods=1).max()
            df['bbands_submin'] =  df.bbands_sub.rolling(window=30, min_periods=1).min()
            df['bbands_subminsig1'] = df.bbands_submin - df.bbands_submin.shift(2)  
            df['bbands_subminsig2'] = df.bbands_sub.shift(2) - df.bbands_submin   
            df['bbands_submaxsig1'] = df.bbands_submax - df.bbands_submax.shift(2) 
            df['bbands_submaxsig2'] = df.bbands_sub.shift(2) - df.bbands_submax  

            df['pct8']=(df.close - df.dn)/(df.up - df.dn)
            df['pct8vsclose']=df.close - df.pct8
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_bbands -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_relative_strength(self, df):
        try:
            self.logger.info("상대 강도 지수 구하기:%s"  % time.ctime(time.time()))
            rsi14 = talib.RSI(np.asarray(df['close']), 14)
            df['rsi14'] = rsi14
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_relative_strength -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_macd(self, df, sort, long, sig):
        try:
            self.logger.info("MACD1 구하기:%s, sort:%d, long:%d, sig:%d"  % (time.ctime(time.time()), sort, long, sig))
            return talib.MACD(np.asarray(df['close']), sort, long, sig)
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_macd -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_volume_sma5(self, df):
        try:
            self.logger.info("5 거래량 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['vol_sma5'] = talib.SMA(np.asarray(df['volume']), 5)
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_volume_sma5 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_volume_sma20(self, df):
        try:
            self.logger.info("20 거래량 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['vol_sma20'] = talib.SMA(np.asarray(df['volume']), 20)
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_volume_sma20 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_volume_sma60(self, df):
        try:
            self.logger.info("60 거래량 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['vol_sma60'] = talib.SMA(np.asarray(df['volume']), 60)
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_volume_sma60 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_volume_sma120(self, df):
        try:
            self.logger.info("120거래량 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['vol_sma120'] = talib.SMA(np.asarray(df['volume']), 120)
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_volume_sma120 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_volume_separation(self, df):
        try:
            volume_df = df[['vol_sma5', 'vol_sma20', 'vol_sma60', 'vol_sma120']]

            df['vol_sepr'] = volume_df.max(axis=1, skipna = True) - volume_df.min(axis=1, skipna = True)
            df['vol_seprmax'] =  df.vol_sepr.rolling(window=30, min_periods=1).max()
            df['vol_seprmin'] =  df.vol_sepr.rolling(window=30, min_periods=1).min()
            df['vol_seprminsig1'] = df.vol_seprmin - df.vol_seprmin.shift(2)   
            df['vol_seprminsig2'] = df.vol_sepr.shift(2) - df.vol_seprmin  
            df['vol_seprmaxsig1'] = df.vol_seprmax - df.vol_seprmax.shift(2) 
            df['vol_seprmaxsig2'] = df.vol_sepr.shift(2) - df.vol_seprmax  

        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_volume_separation -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_balance_init(self, df):
        df['balance'] = 0.0
        df['unit'] = 0.0
        df['year_rate'] = 0.0
        df['rate'] = 0.0

    def add_auxiliary(self, df):
        self.add_closeminmax(df)
        self.add_moving_avg5(df)
        self.add_moving_avg20(df)
        self.add_moving_avg60(df)
        self.add_moving_avg120(df)
        self.add_moving_avg240(df)
        self.add_moving_ema12(df)
        self.add_moving_ema26(df)
        self.add_moving_wma(df)
        self.add_bbands(df)
        self.add_relative_strength(df)

        macd, macdsignal, macdhist = self.add_macd(df, 12, 26, 9)
        df['macd1'] = macd
        df['signal1'] = macdsignal
        df['flag1'] = macd - macdsignal

        macd, macdsignal, macdhist = self.add_macd(df, 7, 15, 9)
        df['macd2'] = macd
        df['signal2'] = macdsignal
        df['flag2'] = macd - macdsignal

        self.add_volume_sma5(df)
        self.add_volume_sma20(df)
        self.add_volume_sma60(df)
        self.add_volume_sma120(df)
        self.add_volume_separation(df)
        self.add_balance_init(df)

        return df

5. 마치며

이번 회차에서는 TA-LIB를 이용한 보조지표 클래스를 만들었습니다.

 

다음 회차에서는 이 보조지표를 이용하여 Bitumb 5분 데이터와 연동해 보도록 하겠습니다.

 

연동은 각 지표별로 해 볼 예정이므로 제가 생각하는 보조지표의 의미와 함께

여러분의 백테스팅 방법을 한번 고민해 보시기 바랍니다.

 

6. 추가로

저는 윈도우 환경이 아니고 azure의 리눅스(우분투) vm을 사용하고 있습니다.

 

마찬가지로 TA-LIB가 깔리지 않습니다. 그런데 하다보니 원인을 알았습니다.

홈페이지에도 내용이 나오는데 자세히 안 읽은 것 같네요.

 

먼저 python의 버젼을 확인 합니다. python --version   저는 3.6.9버젼 입니다.

centos의 경우 sudo yum install python36-devel 을 설치 하고 python -m pip install ta-lib  하면 잘 깔립니다. 

 

우분투 또는 레미안 계열은 sudo apt-get install python-dev  설치 하고 맞찬가지로 pip install ta-lib 하면 에러 없이 잘 깔립니다.

혹시 root 권한이 없는 경우 sudo 하시는 건 기본적으로 아시죠.

windows 사용자는 사이트에서 wheel 파일 받아서 설치하면 잘 설치됩니다.

우리는 다음 데이터가 오는 5분 전에 모든 것을 해야 합니다.

 

그렇게 할려면  TA-LIB는 가장 중요한 모듈입니다.

 

반응형
LIST

맨위로버튼이미지

1. 들어가기 전에 

현재 인터넷에서 돌고 있는 거의 대부분의 소스를 분석하고 적용하고 실제 거래도 태워 보고 한 경험을 여러분과 공유하고자 이 블로그를 작성하게 되었습니다.

현재 인터넷에서 돌고 있는 대부분의 내용은 현실과 부합하지 않는 것이 많으며 특히 유튜브를 통해서 돌고 있는 내용들을 믿고 자동 매매를 실제 거래로 돌리시는 분들이 있다면 상당히 주의를 요한다는 것을 미리 말씀드리고 싶습니다. 

무엇보다 백테스팅이 중요하며 백테스팅 결과를 어떻게 적용할지와 예측 데이터를 어떻게 활용할지에 중점을 맞추고자 합니다.

 

현재 인터넷상에 있는 소스들의 특징은 대부분(상당히 잘 만든 소스도 분명히 있습니다만) 대학교 졸작으로도 부족한 내용 또는 중, 고등학생 학예회 수준의 소스가 많습니다. 

프로그램에는 분명 라이프사이클이 있습니다.

아무리 잘 짠 프로그램도 예외사항을 맞으면 다시 시작되어야 하고 특히나 자동 매매 프로그램은 여러분들의 소중한 돈을 직접 운영하는 어떻게 보면 세상에서 가장 비싼 프로그램이어야 하기에 프로그램에 꼭 필요한 기능인 로그분석, 매매 기록, 예외 처리, 자동 재시작, 버그 패치 방법 등의 기능이 필요합니다.

이 기능들이 없다면 여러분은 정말 어리석은 모험을 하고 있다고 말씀드리고 싶습니다.

분명히 말씀드리지만 저를 포함 인터넷에서 돌고 있는 어떤 프로그램도 투자손실에 대한 보상을 하지 않으며 모든 행위의 책임은 본인이 지고 있는다는 것을 명심하시기 바랍니다.

 

2. Bithumb 5분 데이터 구하기

사용하시는 브라우저에 https://api.bithumb.com/public/candlestick/BTC_KRW/5m 를 입력하시면 상당한 량의 데이터가 json타입으로 내려 옴을 보실 수 있을 것입니다.

이번회에서는 이 데이터를 수신하여 가공하는 부분까지 하고 다음에 이 데이터를 어떻게 활용할지 보조 지표는 어떻게 적용할 지에 대한 글을 이어 가겠습니다.

참고로 저는 VSCode를 사용해서 개발 중이며 python은 아무 버전이나 3.x 버전이면 상관없습니다.

( 32bit,64bit도 이번 주제에서는 상관이 없습니다.

다만 주식 매매 프로그램 작성 시에는 아직도 대부분의 증권사 API가 32bit 윈도우를 지원하고 있어서 향후에는 32bit python을 설치하셔야 할 것입니다.

32bit python의 경우 메모리 제약을 가지고 있어 많은 량의 pandas DataFrame을 사용하게 되면 프로그램이 정상 동작하지 않습니다. )

python의 기본지식(pip 사용법, 기본 문법)은 알고 계신 것으로 믿고 시작하겠습니다. 

 

1. MicroSoft의 VSCode는 Visula Studio나 Eclipse나 기타 툴과는 비교가 안될 정도의 메모리나 속도 관리가 좋습니다.

그리고 일반 Text Editor에 비해 프로젝트 프로그램에 필요한 대부분의 기능을 가지고 있습니다. 

VSCode는 프로젝트란 개념이 따로 없고 폴더 단위로 프로그램 단위를 구성합니다.

또한 Python툴로 사용할 경우 해당 폴더 단위로 실행되므로 폴더 이름이 중요합니다.

bitcoin_trade1 또는 적당한 프로젝트 이름으로 디렉토리를 만들고 VSCode-->파일-->폴더 열기로 해당 폴더를 열면 됩니다.

 

2.Bithumb의 API를 이용하여 데이터를 가져 오기 위해서는 http requests lib를 이용하여 데이터를 가져옵니다.

아시다시피 Bitcoin의 거래는 365일 24시간 이루어지고 있으며 우리나라가 전 세계 평균보다 약간 비싼 편입니다.

사실 우리가 하려는 목표와 이런 내용은 크게 의미가 없지만 Bitcoin의 거래가 365일 24시간 이루어진다는 것은 우리가 작성하려는 프로그램에는 아주 좋은 일입니다. (보조 지표 테스트에 아주 좋은 조건입니다.)

 

3. 아래 데이터에는 시간(timestamp * 1000), 시작가, 종가, 고가, 저가, 거래량을 제공 하고 있습니다.

주식에서는 의미 있는 이 값들이 사실상 Bitcoin의 경우 거의 의미가 없습니다.

그래서 시작가, 고가, 저가를 버리고 종가와 거래량만을 사용하여 추가적인 보조 지표를 다음 회차에서는 다루고자 합니다. 

 

4. 데이터는 Pandas DataFrame을 사용하여 저장되고 리턴 될 것입니다.

DataFrame의 마치 RDBMS처럼 동작하기도 하고 NoSQL DB처럼 동작하기도 합니다.

요령을 익힌다면 상당히 유용하지만 잘못 사용하면 데이터가 중복되거나 유실될 수도 있습니다.

그런 경우 보조 지표에는 아주 치명적일 수 있습니다. 

 

5. 모든 소스는 적당한 크기의 클래스 형태 또는 모듈 단위로 작성될 것입니다.

나중에는 이런 모듈들이 모여 적당한 lib가 갖쳐지게 되면  특정 디렉토리에 담고 import 절에서 [디렉토리명.모듈명]의 형식으로 호출하여 사용할 수 있습니다. 

 

소스 내용(getBithumbData.py)

import time
import requests as rq
import pandas as pd
 
#### 최근 5분 데이터 구하기 하지만 1주일치 임.
class ScrapCurrBithum:
    def get_his_5m(self):
        data=rq.get('https://api.bithumb.com/public/candlestick/BTC_KRW/5m')
        data=data.json()
        data=data.get("data")
        df=pd.DataFrame(data)
        df.rename(columns=0:'time',1:"start", 2:"close", 3:"high",4:"low",5:"volume"}, inplace=True)
        df=df.drop(['start', 'high', 'low'], axis=1)
        df.close=df.close.astype("float")
        df.volume=df.volume.astype("float")
        df["date"]=df["time"].apply(lambda x:time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(x/1000)))
        return df
 
    def get_data(self):
        return self.get_his_5m()

 

3. 마치며

이번 회차에서는 Bithumb의 5분 데이터를 받아서 DataFrame으로 저장하는 소스를 작성했습니다. 이 소스는 main함수가 없으므로 단독 실행이 되지 않습니다. 아래 소스를 같은 폴더안에 작성하여 실행하면 수집된 데이터를 확인할 수 있습니다.

 

소스 내용(main.py)

from getBithumbData import ScrapCurrBithum

if __name__ == '__main__':
    scb = ScrapCurrBithum()
    df = scb.get_data()
    print(df)
반응형
LIST

+ Recent posts