2022년 1월 9일 일요일

backtesting.py custom data 적용과 buy 메소드 사용과 구매시점 이해

1. 사용자 데이터 사용하기

1.1 무작정 넣어보기

이전에는 기본으로 들어있는 sample data를 사용하였는데, 일반적인 데이터를 넣어보겠습니다.

아래와 같은 형태의 csv 파일을 넣어봤습니다.

backtest1.csv 입니다. 이 자료는 2021-03-02일자 이더리움 분단위 자료를 가져왔습니다.

실제 예제는 더 많은데 여기에서는 몇줄만 표기하였습니다.

indexdatetime,open,high,low,close,volume
2021-03-02 14:19:00,1787500.0,1790000.0,1787500.0,1790000.0,53.28931438
2021-03-02 14:20:00,1790000.0,1791000.0,1789000.0,1790500.0,181.15638692
2021-03-02 14:21:00,1790500.0,1791000.0,1787000.0,1787500.0,91.69244693
2021-03-02 14:22:00,1787500.0,1787500.0,1786500.0,1787000.0,178.84881752
...이하 생략...

csv 읽는 함수는 github 에 있는 코드를 약간 변형하였습니다.

https://github.com/kernc/backtesting.py/blob/master/backtesting/test/__init__.py

import pandas as pd

def _read_file(filename):
    from os.path import dirname, join
    return pd.read_csv(filename, index_col=0, parse_dates=True, infer_datetime_format=True)

...일부 코드 생략...
df = _read_file('../testdata/backtest1.csv')
print(df)

bt = Backtest(df, SmaCross, commission=.002,
			  exclusive_orders=True)
stats = bt.run()
bt.plot()

print(stats)

테스트해보면 아래와 같은 오류가 발생합니다.

오류를 살펴보면 columns 'Open', 'High', 'Low', 'Close', and (optionally) 'Volume' 이런식으로 되어야 한다고 되어있습니다. 순서는 관계가 없고 label 명이 대소 문자 일치하지 않으면 오류가 발생합니다.

Traceback (most recent call last):
  File "bt_sample.py", line 32, in <module>
    bt = Backtest(data, SmaCross, commission=.002,
  File "C:\Users\USER\AppData\Local\Programs\Python\Python38\lib\site-packages\backtesting\backtesting.py", line 1067, in __init__
    raise ValueError("`data` must be a pandas.DataFrame with columns "
ValueError: `data` must be a pandas.DataFrame with columns 'Open', 'High', 'Low', 'Close', and (optionally) 'Volume'


1.2 컬럼명 변경하기

pandas 컬럼의 label 변경 방법 pandas에 rename() 메소드를 이용합니다.

df = _read_file('../testdata/backtest1.csv')
print(df)
df = df.rename({'open': 'Open', 'high': 'High', 'low': 'Low', 'close': 'Close', 'volume': 'Volume'}, axis='columns')
print(df)

전과 후를 출력해 봤습니다. 

잘 변환 된것을 알 수 있습니다.

                          open       high        low      close      volume
indexdatetime
2021-03-02 14:19:00  1787500.0  1790000.0  1787500.0  1790000.0   53.289314
2021-03-02 14:20:00  1790000.0  1791000.0  1789000.0  1790500.0  181.156387
2021-03-02 14:21:00  1790500.0  1791000.0  1787000.0  1787500.0   91.692447
... 생략 ...
                          Open       High        Low      Close      Volume
indexdatetime
2021-03-02 14:19:00  1787500.0  1790000.0  1787500.0  1790000.0   53.289314
2021-03-02 14:20:00  1790000.0  1791000.0  1789000.0  1790500.0  181.156387
2021-03-02 14:21:00  1790500.0  1791000.0  1787000.0  1787500.0   91.692447
... 생략 ...



2. 좀 더 깊게 분석 해보기

2.1 캔들 차트

주식에서 흔희 볼 수 있는 캔들 차트입니다. 그런데 일반 주식의 색상이 다른 경우가 있어서 확인해 봤습니다.


여기 제공되는 캔들 차트는 빨간색이 Open가격보다 Close가격이 내려가는 것입니다. 녹색올랐다는 표시입니다.


2.2 한번만 구매/판매 해보기

앞에서 필요시 buy(), sell()을 호출하면 된다고 설명하였는데 막상 해보면 이상하게 동작합니다.

기존 사용하던 예제에서 self.i 를 추가하여 처음 한번만 구매하고 다음날 판매하도록 구현하였습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import SMA, GOOG

class SmaCross(Strategy):
	
	def init(self):
		self.i = 0;

	def next(self):
		print(len(self.data), self.data, self.data.Open)
		if self.i==0 :
			self.buy()
		elif self.i==1 :
			self.sell()
		self.i+=1

bt = Backtest(GOOG, SmaCross, commission=0.0, exclusive_orders=True)
stats = bt.run()

bt.plot()

print(stats)
print(stats['_trades'])
구현 의도는 buy()한것을 다음날 판매한것으로 구현했지만 실제 전체는 Final(0%) (이 의미는 최종 시점에 최초 100%의 금액대비 0%로 되어서 모든 돈이 사라졌다는 의미입니다.)에 거래거 두번 일어난것으로 되어 있습니다. 
거꾸로 유추해보자면 sell()도 하나의 거래로 동작하고 있습니다. sell()의 개념이 복잡하기 때문에 이런식으로 구현하지 않고 다른 방식을 찾아보았습니다.

트레이드 되는 내역은 라인 24: print(stats['_trades']) 에 의해서 확인이 가능합니다.


이번에는 sell() 메소드를 사용하지 않고 self.position.close() 을 사용하였습니다. 이 메소드는 거래를 닫는 함수입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import SMA, GOOG

class SmaCross(Strategy):
	
	def init(self):
		self.i = 0;

	def next(self):
		print(len(self.data), self.data, self.data.Open)
		if self.i==0 :
			self.buy()
		elif self.i==1 :
			self.position.close()
		self.i+=1


bt = Backtest(GOOG, SmaCross, commission=0.0, exclusive_orders=True)
stats = bt.run()

bt.plot()

print(stats)
print(stats['_trades'])

결과를 보면 최종 100.441% 에 한번 거래시 0.004424 리턴되는 수익률이 적절히 표시되고 있습니다.

다시 한번 정리해보자면 구매시는 self.buy(), 판매시에는 self.position.close() 함수를 이용합니다.


2.3 구매/판매 시점 이해하기

next() 메소드 내에서 buy나 sell을 하게 되면 차트에서는 어떻게 표기될지와 언제 구매하도록 구현 되었는지 살펴보겠습니다. 

다음 링크에서 설명을 발견할 수 있습니다.

https://kernc.github.io/backtesting.py/doc/examples/Quick%20Start%20User%20Guide.html

Note, backtesting.py cannot make decisions / trades within candlesticksany new orders are executed on the next candle's open (or the current candle's close if trade_on_close=True). If you find yourself wishing to trade within candlesticks (e.g. daytrading), you instead need to begin with more fine-grained (e.g. hourly) data.

위와 같은 문구가 있습니다. 즉 다음번 캔들 오픈될때 거래가 실행되는데 trade_on_close=True 설정을 하면 close 될때 거래가 실행된다고 합니다. 

each next() call so that array[-1] (e.g. self.data.Close[-1] or self.sma1[-1]) always contains the most recent value, array[-2] the previous value, etc. (ordinary Python indexing of ascending-sorted 1D arrays).

또한 자세히 읽어보면 array[-1] (e.g. self.data.Close[-1] or self.sma1[-1]) 이런값으로 최신값을 접근가능하며, 그 전 데이터는 array[-2] 이런식으로 접근이 가능하다고 합니다.

위 마지막 예제를 잠시 다시 살펴보겠습니다.

라인 12에서 if self.i==0 일때 buy가 동작하도록 구현하였습니다. 그런데, i=0이면 첫번째 캔들에서 거래가 일어나야할것 같은데... 그림을 보면 3번째 캔들에서 buy가 일어나고 있음을 알 수 있습니다.

첫번째 사유는 위에서 설명드린 다음번 캔들 오픈될때 거래가 실행되기 때문입니다. 그렇더라도 하나가 더 이해가 안되는 사유가 있습니다. 두번째도 아니고 세번째 캔들이라니...

이것의 실마리는 라인 11의 print(len(self.data), self.data, self.data.Open) 코드의 출력을 보고 유추가 가능합니다.

코드가 너무 빨리지나가서 중간에 sleep함수를 넣어서 앞부분만 확인해봤습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import SMA, GOOG
import time

class SmaCross(Strategy):
	
	def init(self):
		self.i = 0;

	def next(self):
		print(len(self.data), self.data, self.data.Open)
		time.sleep(1)
		if self.i==0 :
			self.buy()
		elif self.i==1 :
			self.position.close()
		self.i+=1


bt = Backtest(GOOG, SmaCross, commission=0.0, exclusive_orders=True)
stats = bt.run()

bt.plot()

print(stats)
print(stats['_trades'])


C:\Users\USER\Documents\GitHub\dota2trader\code>python bt_sample.py
2 <Data i=2 (2004-08-23 00:00:00) Open=110.75, High=113.48, Low=109.05, Close=109.4, Volume=9137200.0> [100.   101.01]
3 <Data i=3 (2004-08-24 00:00:00) Open=111.24, High=111.6, Low=103.57, Close=104.87, Volume=7631300.0> [100.   101.01 110.75]
4 <Data i=4 (2004-08-25 00:00:00) Open=104.96, High=108.0, Low=103.88, Close=106.0, Volume=4598900.0> [100.   101.01 110.75 111.24]
5 <Data i=5 (2004-08-26 00:00:00) Open=104.95, High=107.95, Low=104.66, Close=107.91, Volume=3551000.0> [100.   101.01 110.75 111.24 104.96]
6 <Data i=6 (2004-08-27 00:00:00) Open=108.1, High=108.62, Low=105.69, Close=106.15, Volume=3109000.0> [100.   101.01 110.75 111.24 104.96 104.95]
7 <Data i=7 (2004-08-30 00:00:00) Open=105.28, High=105.49, Low=102.01, Close=102.01, Volume=2601000.0> [100.   101.01 110.75 111.24 104.96 104.95 108.1 ]
8 <Data i=8 (2004-08-31 00:00:00) Open=102.3, High=103.71, Low=102.16, Close=102.37, Volume=2461400.0> [100.   101.01 110.75 111.24 104.96 104.95 108.1  105.28]
9 <Data i=9 (2004-09-01 00:00:00) Open=102.7, High=102.97, Low=99.67, Close=100.25, Volume=4573700.0> [100.   101.01 110.75 111.24 104.96 104.95 108.1  105.28 102.3 ]

준비된 csv 데이터는 2004-08-19일 부터입니다. 그리고 제일 처음 데이터가 self.data.Open [100.   101.01] 임에 주목해주세요. 앞쪽 self.data 의 출력은 <Data i=2 (2004-08-23 00:00:00) Open=110.75, High=113.48, Low=109.05, Close=109.4, Volume=9137200.0> 이런식으로 나오는 부분은 index라서 해당 날짜는 아닌것으로 확인 됩니다. open 값만을 가지고 확인했을때 101.01은 2004-08-20임을 알 수 있습니다. 

[100.   101.01] 이런식으로 나온다는것은 self.data.Open[-1] = 101.01 이 되고 self.data.Open[-2] = 100. 이 됩니다.

위 사실은 위에서 언급한 아래 내용과 일치합니다.

또한 자세히 읽어보면 array[-1] (e.g. self.data.Close[-1] or self.sma1[-1]) 이런값으로 최신값을 접근가능하며, 그 전 데이터는 array[-2] 이런식으로 접근이 가능하다고 합니다.

그렇다고 하더라도 왜 두번째 캔들부터 들어오게 되는지는 소스의 확인 이 필요합니다.

앞쪽 몇개 데이터는 안들어올 수 있는데 warming up 때문에 그렇습니다. 위 소스에서 start가 0이 아니라 1+어떤 값이 되기 때문에 index가 skip되기 때문입니다.

정리, next() 메소드 안에서 오늘 날짜의 데이터는 self.data.Open[-1], self.data.High[-1], self.data.Low[-1], self.data.Close[-1] 로 접근이 가능합니다. 그리고 그이전 날짜의 데이터는  self.data.Open[-2], self.data.High[-2], self.data.Low[-2], self.data.Close[-2] 로 접근이 가능합니다.


2.3 trade_on_close=True 사용해보기


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import SMA, GOOG
import time

class SmaCross(Strategy):
	
	def init(self):
		self.i = 0;

	def next(self):
		if self.i==0 :
			self.buy()
		elif self.i==1 :
			self.position.close()
		self.i+=1


bt = Backtest(GOOG, SmaCross, commission=0.0, exclusive_orders=True, trade_on_close=True)
stats = bt.run()

bt.plot()

print(stats)
print(stats['_trades'])

좌측이 기존(다음날 open 기준), 우측이 trade_on_close=True 사용한 예제 입니다. 당일 close 기준으로 변경되었습니다.



댓글 없음:

댓글 쓰기