안녕하세요. 요번 시간에는 앞선 글에 사용한 코드에 몇 가지 매끄럽지 않은 부분을 수정하고, 가격이 떨어지면 수익이 나는 숏 포지션(Short Position) 또한 추가하여 자동매매 코드를 만들었습니다.
내용이 중복되는 부분의 설명은 앞선 글을 보시길 바랍니다.
<볼린저 밴드를 사용한 롱 포지션 자동매매 ver.1>
(https://dongkyue-python.tistory.com/9)
파이썬(Python) - 바이낸스(Binance) 볼린저 밴드를 사용한 자동매매(Auto Trade)
안녕하세요 요번시간은 ccxt api를 사용하여 자동매매 프로그램을 만들어 보겠습니다. 거래소는 바이낸스 선물 거래를 사용하겠습니다. 1. 볼린저 밴드 (Bollinger Bands) 볼린저 밴드는 아래와 같이
dongkyue-python.tistory.com
볼린저 밴드를 사용한 롱, 숏 포지션 자동매매 (최종ver)
from datetime import datetime
import time
import ccxt
import pandas as pd
import requests
'''볼린저 밴드를 1시간 봉 사용한 롱-숏 포지션 자동매매'''
with open('경로//binance_api_key.txt') as f:
lines = f.readlines()
api_key = lines[0].strip()
secret = lines[1].strip()
binance = ccxt.binance(config={
'apiKey': api_key,
'secret': secret,
'enableRateLimit': True,
'options': {
'defaultType': 'future'
}
})
myToken = "본인 token 주소"
def dbgout(message):
"""인자로 받은 문자열을 파이썬 셸과 슬랙으로 동시에 출력한다."""
print(datetime.now().strftime('[%m/%d %H:%M:%S]'), message)
strbuf = datetime.now().strftime('[%m/%d %H:%M:%S] ') + message
post_message(myToken,"#stock", strbuf)
def post_message(token, channel, text):
"""슬랙 메시지 전송"""
response = requests.post("https://slack.com/api/chat.postMessage",
headers={"Authorization": "Bearer "+token},
data={"channel": channel,"text": text}
)
def sma(data,window):
return(data.rolling(window=window).mean())
def bollinger_bands(data,sma,window,ntsd):
std = data.rolling(window=window).std()
upper_band = sma + std * ntsd
lower_band = sma - std * ntsd
return upper_band, lower_band
def gather_data(symbols, n_hours):
merge = False
for symbol in symbols:
ohlcv = binance.fetch_ohlcv(symbol=symbol, timeframe='1h', limit=n_hours)
cols = ['datetime', f'{symbol}-open', f'{symbol}-high', f'{symbol}-low', f'{symbol}-close', f'{symbol}-volume']
df = pd.DataFrame(ohlcv, columns=cols)
if merge == True:
dfs = pd.concat([df, dfs], join = 'outer', axis=1)
else:
dfs = df
merge=True
for symbol in symbols:
dfs[f'{symbol}-sma'] = sma(dfs[f'{symbol}-close'],20)
dfs[f'{symbol}-upper_band'] , dfs[f'{symbol}-lower_band'] = bollinger_bands(data = dfs[f'{symbol}-close'],
sma = dfs[f'{symbol}-sma'],
window = 20,
ntsd = 3)
dfs.dropna(inplace=True)
return dfs
def get_states(df,symbol):
states = {}
for symbol in symbols:
if df[f'{symbol}-close'].iloc[-1] < df[f'{symbol}-lower_band'].iloc[-1]:
states[symbol] = 'below'
elif df[f'{symbol}-close'].iloc[-1] > df[f'{symbol}-upper_band'].iloc[-1]:
states[symbol] = 'above'
else:
states[symbol] = 'inside'
return states
def set_leverage(symbol,leverage):
global binance
markets = binance.load_markets()
symbol = symbol
market = binance.market(symbol)
leverage = leverage
resp = binance.fapiPrivate_post_leverage({'symbol': market['id'],'leverage': leverage})
def long_buy(symbol,amount):
order = binance.create_market_buy_order(symbol=symbol,amount=amount)
def long_sell(symbol,amount):
order = binance.create_market_sell_order(symbol=symbol,amount=amount)
def short_buy(symbol,amount):
order = binance.create_market_sell_order(symbol=symbol,amount=amount)
def short_sell(symbol,amount):
order = binance.create_market_buy_order(symbol=symbol,amount=amount)
def get_current_fucture_cash(): #free, used, total 순서
global binance
balance = binance.fetch_balance()
dic = balance['USDT']
return dic['free'], dic['used'], dic['total']
#Long-Take Profit_Stop Limit
def long_tp_sl(symbol,loss,profit):
global binance
tp_sl_symbol = symbol.replace('/','')
balance = binance.fetch_balance()
positions = balance['info']['positions']
for position in positions:
if position["symbol"] == tp_sl_symbol:
amt = float(position['positionAmt'])
ep = float(position['entryPrice'])
leverage = float(position['leverage'])
stop_loss = ep*(1-loss/leverage)
take_profit = ep*(1+profit/leverage)
binance.create_order(symbol="BTC/USDT",type="STOP_MARKET",side="sell",amount=amt,params={'stopPrice': stop_loss})
binance.create_order(symbol="BTC/USDT",type="TAKE_PROFIT_MARKET",side="sell",amount=amt,params={'stopPrice': take_profit})
#Short-Take Profit_Stop Limit
def short_tp_sl(symbol,loss,profit):
global binance
tp_sl_symbol = symbol.replace('/','')
balance = binance.fetch_balance()
positions = balance['info']['positions']
for position in positions:
if position["symbol"] == tp_sl_symbol:
amt = -float(position['positionAmt'])
ep = float(position['entryPrice'])
leverage = float(position['leverage'])
take_profit = ep*(1+loss/leverage)
stop_loss = ep*(1-profit/leverage)
binance.create_order(symbol="BTC/USDT",type="STOP_MARKET",side="buy",amount=amt,params={'stopPrice': take_profit})
binance.create_order(symbol="BTC/USDT",type="TAKE_PROFIT_MARKET",side="buy",amount=amt,params={'stopPrice': stop_loss})
symbols = ['BTC/USDT','ETH/USDT']
balance_unit = 'USDT'
signal = True
while True:
try:
if (datetime.now().second % 10 == 0) or (signal == True):
if ((datetime.now().minute % 3 == 0) and (datetime.now().second == 10)) or (signal == True):
df = gather_data(symbols,22)
states = get_states(df,symbols)
signal = False
print(states)
#BUY
#Looking for Long_Position
if balance_unit == 'USDT':
for symbol in symbols:
ask_price = float(binance.fetch_order_book(symbol)['asks'][0][0])
lower_band = df[f'{symbol}-lower_band'].iloc[-1]
if ask_price < lower_band and states[symbol] == 'inside': # buy signal
set_leverage(symbol,5)
balance_unit = symbol
USDT_free = get_current_fucture_cash()[0] #USDT-free
amount = USDT_free/ask_price * 5 * 0.95
long_tp_sl(symbol,0.25,0.25)
dbgout(f'''
---Long-BUY : {symbol}---
Leverage(Isolated) : x5
Entry_price : {ask_price}
USDT Size : {round(amount,5)}''')
long_buy(symbol,amount)
#BUY
#Looking to Short_Position
if balance_unit == 'USDT':
for symbol in symbols:
bid_price = float(binance.fetch_order_book(symbol)['bids'][0][0])
upper_band = df[f'{symbol}-upper_band'].iloc[-1]
if bid_price > upper_band and states[symbol] == 'inside': # buy signal
set_leverage(symbol,5)
balance_unit = symbol
USDT_free = get_current_fucture_cash()[0] #USDT-free
amount = USDT_free/ask_price * 5 * 0.95
short_tp_sl(symbol,0.25,0.25)
dbgout(f'''
---Short-BUY : {symbol}---
Leverage(Isolated) : x5
Entry_price : {bid_price}
USDT Size : {round(amount,5)}''')
short_buy(symbol,amount)
#Sell
#Looking to Long_position
if balance_unit != 'USDT': #looking to sell
bid_price = float(binance.fetch_order_book(balance_unit)['bids'][0][0])
upper_band = df['{balance_unit}-upper_band'].iloc[-1]
if bid_price > upper_band:
dbgout(f'''
---Long-SELL : {balance_unit}---
sell_price : {bid_price}
Roe:{100*ask_price/bid_price}''')
long_sell(balance_unit,amount)
if get_current_fucture_cash()[0] > get_current_fucture_cash()[1]:
balance_unit = 'USDT'
#Sell
#Looking to short_position
if balance_unit != 'USDT': #looking to sell
ask_price = float(binance.fetch_order_book(balance_unit)['asks'][0][0])
lower_band = df['{balance_unit}-lower_band'].iloc[-1]
if lower_band < ask_price:
dbgout(f'''
---Long-SELL : {balance_unit}---
sell_price : {bid_price}
Roe:{100*ask_price/bid_price}''')
short_sell(balance_unit,amount)
if get_current_fucture_cash()[0] > get_current_fucture_cash()[1]:
balance_unit = 'USDT'
time.sleep(1)
except Exception as e:
print(e)
time.sleep(1)
- dbgout과 postmessage 함수는 포지션, 진입가, 레버리지 등과 같은 결과 정보를 Slack 어플을 통해 보게 해주는 함수로, 조코딩님의 소스를 사용하였습니다. (코드 25줄, 31줄)
- 기존의 get_current_future_cash() 함수는 free+used = total USDT인 total 값이었으므로, 포지션 진입에 들어가 있는 USDT를 제외한, 현재 갖고 있는 USDT인 free USDT로 변경하였습니다. (코드 100줄)
- long_tp_sl, short_tp_sl 함수는 각각 롱 포지션, 숏포지션에 대한 Take Profit(이익 실현)과 Stop Loss(손실 제한)입니다.
앞선 글에선, While문 안의 symbol은 'BTC/USDT', 'ETH/USDT' 와 같이 코인이름과 거래소를 '/'로 구분을 주어 각각의 함수들에 arguement값 중 하나로 들어갔지만, tp_sl 함수의 symbol은 'BTCUSDT', 'ETH/USDT'와 같이 '/'가 들어가지 않기 때문에, replace함수를 이용하여 string을 수정하였습니다. (코드 107, 123줄)
또한 tp_sl 함수의 loss, profit argument 값은 레버리지를 포함한 roe로 각각 양수로 들어갑니다.
ex) short_tp_sl('BTCUSDT',0.2,0.3) >>> 레버리지를 포함하여 -20%, +30%일 때, 각각 Stop Loss, Take Profit 입니다. (코드 162,180줄)
- 숏 포지션의 매수는 롱 포지션과 반대로, 호가창의(Order Book) 가장 첫번째 bid 가격이 볼린저 밴드의 Upper Band(UB)보다 높아질 때 숏 포지션에 들어가게 됩니다. (코드 171줄)
- 숏 포지션의 매도는 롱 포지션과 반대로, 호가창의(Order Book) 가장 첫번째 ask 가격이 볼린저 밴드의 Lower Band(LB)보다 낮아질 때 롱 포지션에 들어가게 됩니다.(코드 203줄)
- 롱, 숏포지션의 매수, 매도 함수(코드 88, 91, 94, 97줄)를 사용 후, 같은 줄의 코드가 회색으로 표시(vsc editor)되는 것을 확인하여, 매수할때는 필요한 코드들을 매수 함수 위에 모두 작성하는 방향으로 해결 하였고, 매도의 경우 balance_unit 객체를 다시 'USDT' 로 바꿔야 되기 때문에, 매도하는 함수와 다른 줄에 get_current_future_cash 함수를 사용하여 만약 매도가 진행된다면 Free USDT > Used USDT 가 되므로,(필자는 포지션 매수에 들어가는 수량을 갖고있는 USDT의 95%을 사용) 이런 조건문을 추가하여 balance_unit을 'USDT'로 리셋하였습니다. (코드 199줄, 212줄)
질문있으시면 댓글 부탁드립니다.