python-binance を使って Binance API 触ってみる

何も考えずにトレイルストップもどきを作ってみたのをちょっと改良

在庫を見つけたら価格の上昇に合わせてストップリミットオーダーを発行
リミット値は現在値の0.8％下に設定
リミット値は後戻りしないので0.8%以上上昇すれば勝ち？
使う場合は適当に変更してください

LINEで定時発報していますので動作中であれば、新たに購入して在庫ができれば動き続けるはず・・・

```py
import time
from binance.client import Client # bainanceクライアント
from binance.websockets import BinanceSocketManager # BinanceにWebsocketで接続するのに必要
from binance.enums import * # ENUM定義

import datetime
import requests
import math


TRAIL_STOP_LIMIT = 0.992 # ストップリミット(トレイルストップコントロール中の最高値の99.2%を下回ったら即売り)

# Binance API TOKEN
PUBLIC = 'バイナンスから取得してください'
SECRET = 'バイナンスから取得してください'

# LINE API TOKEN
LINE = 'LINEから取得してください'

# LINE通知
def SendLineNotify(msg):
    lineNotifyApi = 'https://notify-api.line.me/api/notify'
    headers = {'Authorization': f'Bearer {LINE}'}
    data = {'message': f'message: {msg}'}
    requests.post(lineNotifyApi, headers = headers, data = data)


class TrailStop:

    def __init__(self,dest,src):

        self.calcLimit = 0.0 # 現在値に対しての常時演算
        self.stopLimit = 0.0 # トレイルストップ値
        self.minQty = 0.0  # 最小取引数

        self.symbol = dest + src #通貨ペアのシンボルを作成 ex.BTC/USDT -> BTCUSDT
        self.target = dest
        self.src = src

        # クライアント接続開始
        self.connect()

        # 最低取引数量計算
        info = self.client.get_symbol_info(self.symbol) # 通貨ペアの情報を取得
        self.minQty = float(info['filters'][2]['minQty']) # 基軸通貨の最低取引量
        self.dp = len(str(int(1/self.minQty))) - 1   # 小数点位置を取得

        # BTCのお値段確認
        ticker = self.client.get_ticker(symbol='BTCUSDT')
        btcPrice = float(ticker['lastPrice'])

        # 今回の取引通貨ペアのお値段確認
        ticker = self.client.get_ticker(symbol=self.symbol)
        lastPrice = float(ticker['lastPrice'])

        # 現物を確認
        qty = float(self.GetQty())
        self.qtyValue = qty # 現物の量を覚えておく
        value = '{:.6g}'.format(qty)

        self.spotStatus = False

        # 通貨ベアの現在の状態調査用
        self.minTradeQty = 10.3/ lastPrice
        print('最低取引単位',self.minQty,"小数点桁数",self.dp,"最低取引数量",self.minTradeQty)        
        print("現物確認","qty:",value,"現在の価格:",lastPrice,"BTC Price：",btcPrice)

        # オーダーブックを確認
        res = self.GetOrders()
        if len(res)!=0:
            self.spotStatus = True

        else:
            # 10．3＄以上現物があるかどうか。なかったら購入待ちで待機
            if (qty * lastPrice) < self.minTradeQty:
                print("購入待ち")
                self.spotStatus = False
            else:
                msg = "トレイルストップコントロール開始しました:" + self.symbol
                SendLineNotify(msg)
                print("トレイルストップコントロール開始")
                self.spotStatus = True

        self.preValue = lastPrice # 前回の価格
        self.preStopLimit = 0   #ストップリミット
        self.nexttime005 = datetime.datetime.now() # 現物監視用タイマ
        self.nexttime3600 = datetime.datetime.now() # 現物監視用タイマ

    # バイナンスAPIサーバへ接続
    def connect(self):
        self.client = Client(api_key=PUBLIC, api_secret=SECRET)

    # Websocketからのメッセージ
    # 主にここで条件を定義して
    def handle_message(self,msg):

        self.price = float(msg['p']) # 現在の価格

        # ストップ値計算
        self.calcLimit = self.price * TRAIL_STOP_LIMIT

        # 現在時刻
        now = datetime.datetime.now()

        # 演算された現在のストップ値が
        if self.calcLimit > self.stopLimit and self.spotStatus == True:
            self.stopLimit = self.calcLimit
            print("リミット更新","現在の価格:",'{:.6g}'.format(self.price),"STOP LIMIT:",'{:.6g}'.format(self.stopLimit))

        # 1分間のアクセス数に制限があるので5秒に一回程度にしておく
        # 現物があるときは問合せを実行しない
        if now >= self.nexttime005:

            self.nexttime005 = datetime.datetime.now() + datetime.timedelta(seconds=5) # 次の判定タイミングをセット
            # オーダーブックを確認
            res = self.GetOrders()
            if len(res)!=0:
                for r in res:
                    side = r['side']
                
                if side == 'SELL' and self.spotStatus == False:
                    self.spotStatus = True
                else:
                    self.spotStatus = False
            else:
                self.spotStatus = False

            # オーダーブックにもなかったら原物の在庫を確認する
            if self.spotStatus == False:
                # 現物の状態を確認
                qty = float(self.GetQty())
                self.qtyValue = qty # 現物の量を更新
                #print("現物確認","qty:",qty,"現在の価格:",self.price)

                # 現物が取引最低数量存在するかどうか判定
                if qty  > self.minTradeQty:
                    print("現物を発見")
                    print("トレイルストップコントロール開始")
                    self.spotStatus = True
                else:
                    self.spotStatus = False

            # 5秒前のストップリミットが前回値より低かったらオーダーを更新する
            if self.spotStatus == True:
                if self.preStopLimit < self.stopLimit:
                    self.stoplimitSell(self.stopLimit)
                    self.preStopLimit = self.stopLimit

        # 前回値を保存
        self.preValue = self.price

        if now >= self.nexttime3600:
            # BTCのお値段確認
            ticker = self.client.get_ticker(symbol='BTCUSDT')
            btcPrice = float(ticker['lastPrice'])

            # 今回の取引通貨ペアのお値段確認
            ticker = self.client.get_ticker(symbol=self.symbol)
            lastPrice = float(ticker['lastPrice'])

            self.nexttime3600 = datetime.datetime.now() + datetime.timedelta(seconds=3600) # 次の判定タイミングをセット
            msg = 'トレイルコントロール中：' 
            msg += self.target 
            msg += '現在の価格:'
            msg += '{:.6g}'.format(self.price) 
            msg += self.src 
            msg += '現在のリミット値:'
            msg += '{:.6g}'.format(self.stopLimit)
            SendLineNotify(msg)

        '''未使用
        # トレイルストップ トラップ発動判定
        if self.price <= self.stopLimit and self.spotStatus ==True:
            self.spotStatus = False
            print("トレイルストップ")
            self.Cancel()
            self.marketSell()
            print("現在の価格:",self.price,"STOP LIMIT:",self.stopLimit)
            msg = "トレイルストップ:" + ""
        '''
        '''未使用
        # 前回値との差分が1%以上で下がったら
        if self.price <= self.preValue * 0.99 and self.spotStatus ==True:
            self.spotStatus = False
            print("相場急落 緊急売却")
            self.Cancel()
            self.marketSell()
            print("現在の価格:",self.price,"STOP LIMIT:",self.stopLimit)
        '''

    # Websocketスタート
    def Start(self):
        self.bm = BinanceSocketManager(self.client)
        self.conn_key = self.bm.start_trade_socket(self.symbol, self.handle_message)
        self.bm.start()

        # 開始したらループ
        try:
            while True:
                pass
        except KeyboardInterrupt:
            # Ctrl-C を捕まえた！
            print('interrupted!')
            return

    # コネクションクローズ
    def Close(self):
        if self.bm is None:
            pass
        else:
            self.bm.stop_socket(conn_key)
            self.bm.close()

    # ストップリミット注文
    def stoplimitSell(self,stopValue):
        val = float(self.GetQty())
        n = 2 # ETH/USDT通貨ペアだと小数点二桁
        value = math.floor(stopValue * 10 ** n) / (10 ** n)
        l = value + 0.01 # USDTの最小取引単位
        limitPrice = math.floor(l * 10 ** n) / (10 ** n) 
        #limitPrice = str(round(stopValue + 0.01,2))
    
        n = self.dp-1
        qty = math.floor(float(self.Cancel()) * 10 ** n) / (10 ** n)
        #qty = str(round(float(self.Cancel()),self.dp))
        print("売却 StopLimit更新：","qty",qty,"limitPrice",limitPrice)

        order = self.client.create_order(
            symbol = self.symbol,
            side = SIDE_SELL,
            type = ORDER_TYPE_STOP_LOSS_LIMIT,
            timeInForce = TIME_IN_FORCE_GTC,
            quantity = str(qty),
            price = str(value),
            stopPrice = str(limitPrice)
            )


    # 売り飛ばし
    def marketSell(self):
        value = round(float(self.qtyValue)*0.99,self.dp)
        qty = '{:6g}'.format(value)
        self.spotStatus == False
        if value * self.price < self.minTradeQty:
            print("現物が最小取引数量を満たしません:","現物",qty,"最低取引数量",self.minTradeQty)
        else:
            print("売るよ",qty)
            order = self.client.order_market_sell(symbol=self.symbol, quantity=qty)
            if Isfloat(order['cummulativeQuoteQty']):
                now_asset = float(order['cummulativeQuoteQty'])
                print("売り価格",now_asset)
                msg = "トレールストップリミット発動しました。"
                SendLineNotify(msg)
                msg="販売価格" + str(now_asset)
                SendLineNotify(msg)
            else:
                msg = "売るの失敗しているかも。すぐに確認を"
                SendLineNotify(msg)

    # 購入
    def MarketBuy(self):
        qty = round(self.GetSrcQty(),self.dp)
        # 買い注文（成行注文）
        print("買うよ",qty)
        sell_order = self.client.order_market_buy(symbol=self.symbol, quantity=qty)
        now_asset = float(sell_order['cummulativeQuoteQty'])
        print("買い価格",now_asset)
        self.calcLimit = self.price * 0.99

    # 使用可能在庫量の取得
    def GetQty(self):
        balance = self.client.get_asset_balance(asset=self.target)
        return balance['free']
    
    # 使用可能購入用通貨量の取得
    def GetSrcQty(self):
        basis = self.client.get_asset_balance(asset=self.src)
        print(basis)
        basisFree = float(basis['free'])
        ticker = self.client.get_ticker(symbol=self.symbol)
        lastPrice = float(ticker['lastPrice'])
        print(basisFree,lastPrice)
        qty = ((basisFree / lastPrice * 0.99))
        return qty

    # オーダーキャンセルして在庫量を返す。
    def Cancel(self):
        qty = "0.0"
        res = self.GetOrders()
        if len(res)!=0:
            for r in res:
                id = r['orderId']
                result = self.client.cancel_order(
                symbol=self.symbol,
                orderId=id)
                qty = r['origQty']
        else:
            qty = self.GetQty()
        return qty

    # オーダーブック確認
    def GetOrders(self):
        return self.client.get_open_orders(symbol=self.symbol)



def main():
    pair1 = "ETH"
    pair2 = "USDT"
    cl = TrailStop(pair1,pair2)
    cl.Start()
    cl.Close()
    print('終了')


if __name__ == "__main__":
    main()
```

簡単な自動売買を組んで2時間ほど動かしたら5万ほど消えていたので
自動買いはとりあえず無し・・・

handle_messageの所に買い条件も入れたらなんとなく自動売買は作れてしまう

はまるポイントとしては通貨によって桁数が違うことと
バイナンスの手数料を考慮して金額を入れる部分ですね

ETH/USDTは小数点二桁だけどDOGE/USDTの通貨ペアだと小数点4桁だったりする

## 参考

https://www.handsonplus.com/programming/binance-buy-sell/

https://note.com/bakaruda/n/n4b38e518d8ba