2022년 1월 9일 일요일

backtesting.py 나만의 전략 my Strategy 만들어보기

backtesting 이란? backtesting.py 이해와 분석

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

backtesting.py 나만의 전략 my Strategy 만들어보기

1. 익숙한 전략 만들어 보기

1.1 변동성 돌파 전략

변동성 돌파 전략은 일일 단위로 일정 수준 이상의 범위를 뛰어넘는 강한 상승세를 돌파 신호로 상승하는 추세를 따라가며 일 단위로 빠르게 수익을 실현하는 단기매매 전략입니다.

변동성 돌파에 대해서는 아래 링크로 부터 자세한 내용을 공부 해보시기 바랍니다.

https://tvextbot.github.io/post/indicator_vbi/

① 전날의 일봉 기준 range(= 전일 고가 – 전일 저가)를 계산합니다.

② 당일 장중 가격이 당일시가 + (전일 range 값 * K)을 넘을 경우 매수 합니다. (K = 노이즈비율)

③ 익일 시가 기준으로 지정가 매도를 합니다.


1.2 변동성 돌파 전략 코드 구현해보기

변동성 돌파 전략의 핵심은 변동성의 돌파되는 시점에 구매를 하는것인데 backtesting.py에서 buy 신호를 내면 다음날 구매가 일어나거나 오늘 종가로 구매가 일어나게 됩니다.(옵션 조절가능) 또한 판매하는 시점도 다음날에 매도를 하더라도 종가나 그 다음날의 시가로 매도가 되기 때문에 backtesting.py에서 변동성 돌파 전략은 어울리지 않습니다.

그러나 여기에서는 적절한 이론을 실제 구현하는것에 의미를 뒤기 때문에 그런부분들은 중요하게 보지 말고 구현하는것에 촛점을 두었으면 합니다.

 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
28
29
30
31
32
33
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import SMA, GOOG

K = 0.6
class SmaCross(Strategy):
	
	def init(self):
		self.does_have_stock = False
		
	def next(self):
		if self.does_have_stock == True:
			# 전일 산 주식이 있으면 팔다
			self.position.close()
			self.does_have_stock = False
		else:
			# 전일 고가-전일 저가
			delta_price = self.data.High[-2] - self.data.Low[-2]
			if delta_price < 0 :
				delta_price *= -1
			# 당일 시가 + ( 전일 range 값 * K ) < 당일 고가 : 당일 고가가 변동성 보다 높아짐 구매한다
			if ( self.data.Open[-1] + (delta_price * K)) < self.data.High[-1]:
				self.buy()
				self.does_have_stock = True


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

bt.plot()

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

전날의 변동성 부분은 [-2] 인덱스를 사용하여 계산합니다.

라인 18 참고 delta_price = self.data.High[-2] - self.data.Low[-2]

그럴일은 없지만 라인 19/20에서 음수가 발생하면 곱해서 양수를 만들어 주도록 합니다.

② 당일 장중 가격이 당일시가 + (전일 range 값 * K)을 넘을 경우 매수 합니다. (K = 노이즈비율)

이 부분은 라인 22에서 구현하였습니다. 변동성을 돌파하면 buy()신호를 발생시키고 self.does_have_stock = True로 만들면 다음날 self.position.close()에 의해 자동으로 팔게 됩니다.


마지막 수익률은 147%가 나왔습니다.


1.3 변동성 돌파 + 이동 평균선 전략 합치기

저는 이익보다는 안전 전략을 세워봤습니다. 그래서 이동 평균선이 만나는 지점에 사는것이 아니라 mark를 해놓고 변동성 돌파를 하게되면 구매하는 전략을 세웠습니다. 시너지가 있을지 확인해보겠습니다.


1.4 변동성 돌파 + 이동 평균선 전략 코드 구현해보기

기존에 넣었던  crossover 조건에 나오는 변수는 self.can_buy 라는 변수인데 해당값이 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import SMA, GOOG

K = 0.6
class SmaCross(Strategy):
	
	def init(self):
		price = self.data.Close
		self.does_have_stock = False
		self.ma1 = self.I(SMA, price, 10)
		self.ma2 = self.I(SMA, price, 20)
		self.can_buy = False
	def next(self):
		if crossover(self.ma1, self.ma2):
			self.can_buy = True
		elif crossover(self.ma2, self.ma1):
			self.can_buy = False
		if self.does_have_stock == True:
			# 전일 산 주식이 있으면 팔다
			self.position.close()
			self.does_have_stock = False
		elif self.can_buy:
			# 전일 고가-전일 저가
			delta_price = self.data.High[-2] - self.data.Low[-2]
			if delta_price < 0 :
				delta_price *= -1
			# 당일 시가 + ( 전일 range 값 * K ) < 당일 고가 : 당일 고가가 변동성 보다 높아짐 구매한다
			if ( self.data.Open[-1] + (delta_price * K)) < self.data.High[-1]:
				self.buy()
				self.does_have_stock = True


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

bt.plot()

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

Final이 186%가 나왔습니다. 이전 보다 좋은 결과이긴하지만 이동 평균선 전략 단독 보다도 떨어지는 수치입니다.


이동 평균선 단독 전략은 아래와 같습니다.

무려 Final이 749% 입니다.


이때의 소스입니다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import SMA, GOOG

class SmaCross(Strategy):
	
	def init(self):
		price = self.data.Close
		self.ma1 = self.I(SMA, price, 10)
		self.ma2 = self.I(SMA, price, 20)
	def next(self):
		if crossover(self.ma1, self.ma2):
			self.buy()
		elif crossover(self.ma2, self.ma1):
			self.position.close()

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

bt.plot()

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


2. 한계

변동성 돌파라는 단순한 전략이라는 개념을 구현해보았습나다만, 사실 제대로 구현되지는 않았습니다. backtesting이 과거의 자료를 가지고 구현을 하다보니 딱 맞지 않습니다. 매수 하는 타이밍이 안맞고, 판매하는 시점도 맞지 않습니다.

결론적으로 말을 하자면 backtesting은 빈번하게 거래하는 부분하고는 잘 맞지 않습니다.

여기 자료를 보면 data가 한개만 들어감을 알 수 있습니다. 종목이 하나밖에 안들어간다는 의미입니다. 이건 사고자 하는 주식이나 코인에 대한 데이터가 되고 만약 참조하는 데이터가 여러개라면 어떻게 처리해야할까요? data에 여러개의 데이터를 붙이면 됩니다. 물론 Open, High 이런 label이 중복 안되도록 해서 진행하면 됩니다. 

여러가지 예측 기법(회귀 기법, 머신러닝, 인공지능 등)을 통해서 새로운 결과를 만들어서 제공하는것입니다. 즉 여기에는 전략이란 필요하지 않습니다. 예를들어 buy 컬럼을 하나 만들어 그곳에 0.5보다 큰 값이면 구매하도록 만드는 방식입니다. 이것은 다음에 예를 들어 보도록 하겠습니다.

머신러닝을 이용한 방법은 아래 링크에 있습니다만 좀 더 이해가 필요합니다.

https://kernc.github.io/backtesting.py/doc/examples/Trading%20with%20Machine%20Learning.html


3. 오류

3.1 너무 많은 데이터로 작업시

진행하다가 data가 많아서 아래와 같은 경우가 있을 수 있습니다.
원인은 "Data contains too many candlesticks to plot" 데이터가 너무 많아서 캔들 표시하는데 downsapling을 시작하는데 그러다가 오류가 발생하는건입니다. 
이건 정확히 분석해서 해결 처리할 수도 있지만, 분석하기에는 시간도 오래걸리니.... 데이터를 줄이던가 Backtest.plot(resample=False) 부분의 resample=False를 넣도록 합니다.
캔들만드는데 시간이 오래걸리는 문제가 있긴하지만 동작은 됩니다.

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
C:\Users\USER\AppData\Local\Programs\Python\Python38\lib\site-packages\backtesting\_plotting.py:122: UserWarning: Data contains too many candlesticks to plot; downsampling to '10T'. See `Backtest.plot(resample=...)`
  warnings.warn(f"Data contains too many candlesticks to plot; downsampling to {freq!r}. "
Traceback (most recent call last):
  File "C:\Users\USER\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\core\groupby\generic.py", line 260, in aggregate
    return self._python_agg_general(
  File "C:\Users\USER\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\core\groupby\groupby.py", line 1083, in _python_agg_general
    result, counts = self.grouper.agg_series(obj, f)
  File "C:\Users\USER\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\core\groupby\ops.py", line 897, in agg_series
    return grouper.get_result()
  File "pandas\_libs\reduction.pyx", line 162, in pandas._libs.reduction.SeriesBinGrouper.get_result
  File "pandas\_libs\reduction.pyx", line 74, in pandas._libs.reduction._BaseGrouper._apply_to_group
  File "C:\Users\USER\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\core\groupby\groupby.py", line 1060, in <lambda>
    f = lambda x: func(x, *args, **kwargs)
  File "C:\Users\USER\AppData\Local\Programs\Python\Python38\lib\site-packages\backtesting\_plotting.py", line 147, in f
    mean_time = int(bars.loc[s.index].view(int).mean())
  File "C:\Users\USER\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\core\series.py", line 667, in view
    return self._constructor(
  File "C:\Users\USER\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\core\series.py", line 313, in __init__
    raise ValueError(
ValueError: Length of passed values is 2, index implies 1.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "bt_sample.py", line 30, in <module>
    bt.plot()
  File "C:\Users\USER\AppData\Local\Programs\Python\Python38\lib\site-packages\backtesting\backtesting.py", line 1592, in plot
    return plot(
  File "C:\Users\USER\AppData\Local\Programs\Python\Python38\lib\site-packages\backtesting\_plotting.py", line 203, in plot
    df, indicators, equity_data, trades = _maybe_resample_data(
  File "C:\Users\USER\AppData\Local\Programs\Python\Python38\lib\site-packages\backtesting\_plotting.py", line 153, in _maybe_resample_data
    trades = trades.assign(count=1).resample(freq, on='ExitTime', label='right').agg(dict(
  File "C:\Users\USER\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\core\resample.py", line 288, in aggregate
    result, how = self._aggregate(func, *args, **kwargs)
  File "C:\Users\USER\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\core\base.py", line 416, in _aggregate
    result = _agg(arg, _agg_1dim)
  File "C:\Users\USER\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\core\base.py", line 383, in _agg
    result[fname] = func(fname, agg_how)
  File "C:\Users\USER\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\core\base.py", line 367, in _agg_1dim
    return colg.aggregate(how)
  File "C:\Users\USER\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\core\groupby\generic.py", line 267, in aggregate
    result = self._aggregate_named(func, *args, **kwargs)
  File "C:\Users\USER\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\core\groupby\generic.py", line 480, in _aggregate_named
    output = func(group, *args, **kwargs)
  File "C:\Users\USER\AppData\Local\Programs\Python\Python38\lib\site-packages\backtesting\_plotting.py", line 147, in f
    mean_time = int(bars.loc[s.index].view(int).mean())
  File "C:\Users\USER\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\core\series.py", line 667, in view
    return self._constructor(
  File "C:\Users\USER\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\core\series.py", line 313, in __init__
    raise ValueError(
ValueError: Length of passed values is 2, index implies 1.

댓글 없음:

댓글 쓰기