ExBroker.php 7.7 KB
<?php

namespace trader\exchange\bybit;

require_once __DIR__ . '/../../struct/ApiInfo.php';
require_once __DIR__ . '/Api.php';
require_once __DIR__ . '/../../../jiaoyin/Websocket.php';
require_once __DIR__ . '/../../../jiaoyin/func.php';

use trader\struct\ApiInfo;
use trader\exchange\bybit\Api as BbApi;
use Jiaoyin\Websocket;
use function Jiaoyin\output;
use function Jiaoyin\getMicrotime;

class ExBroker
{
    private $host = 'wss://stream.bybit.com';
    private $pathPri = '/v5/private'; //账户信息path
    private $pathPub = '/v5/public/linear'; //行情信息path 永续
    private ?ApiInfo $apiInfo;
    private BbApi $api;
    private ?Websocket $wsAcc;
    private ?Websocket $wsKline;
    private $timerAccPing = 0;
    private $timerKlinePing = 0;

    public function __construct(?ApiInfo $apiInfo)
    {
        $this->apiInfo = $apiInfo;
        $this->api = new BbApi($apiInfo);
    }

    public function setWsHost($host)
    {
        $this->host = $host;
    }

    public function setRestHost($host)
    {
        $this->api->setHost($host);
    }

    public function accListen(callable $onData)
    {
        if (isset($this->wsAcc)) {
            $this->wsAcc->close();
        }
        $this->wsAcc = new Websocket($this->host . $this->pathPri);
        $this->wsAcc->connect(
            function () {
                $this->wsLogin();
                $this->wsAccPing();
            },
            function ($data) use ($onData) {
                $this->onWsDataPre($data, $onData);
            },
            function () {
                swoole_timer_clear($this->timerAccPing);
            }
        );
    }

    public function klineListen($symbol, $interval, callable $onData)
    {
        if (isset($this->wsKline)) {
            $this->wsKline->close();
        }
        $this->wsKline = new Websocket($this->host . $this->pathPub);
        $this->wsKline->connect(
            function () use ($symbol, $interval) {
                $this->wsKline->push(json_encode([
                    'op' => 'subscribe',
                    'args' => [
                        "kline.{$interval}.{$symbol}"
                    ]
                ]));
                $this->wsKlinePing();
            },
            function ($data) use ($onData) {
                $data = json_decode($data, true);
                if (isset($data['op']) && $data['op'] == 'pong') {
                    return; //pong 消息不处理
                }
                if (isset($data['data'])) {
                    $onData($data);
                }
            },
            function () {
                swoole_timer_clear($this->timerKlinePing);
            }
        );
    }

    private function wsKlinePing()
    {
        $this->timerKlinePing = swoole_timer_tick(1000 * 20, function () {
            $pingMsg = [
                'op' => 'ping',
                'req_id' => (string)time(),
            ];
            $this->wsKline->push(json_encode($pingMsg));
        });
    }

    private function wsAccPing()
    {
        $this->timerAccPing = swoole_timer_tick(1000 * 20, function () {
            $pingMsg = [
                'op' => 'ping',
                'req_id' => (string)time(),
            ];
            $this->wsAcc->push(json_encode($pingMsg));
        });
    }

    private function createWsSign($expires)
    {
        $param = "GET/realtime" . $expires;
        return hash_hmac('sha256', $param, $this->apiInfo->secret);
    }

    private function wsLogin()
    {
        $expires = getMicrotime() + 10 * 1000;
        $this->wsAcc->push(json_encode([
            'req_id' => (string)time(),
            'op' => 'auth',
            'args' => [
                $this->apiInfo->key,
                $expires,
                $this->createWsSign($expires)
            ]
        ]));
    }

    private function onWsDataPre($data, callable $onWsData)
    {
        $data = json_decode($data, true);
        if (isset($data['op'])) {
            if ($data['op'] == 'pong') {
                return; //pong 消息不处理
            }
            if ($data['op'] == 'auth' && $data['success']) {
                output('ws登录成功');
                $this->wsSubscribe();
                return;
            }
            if ($data['op'] == 'subscribe') {
                return;
            }
            output('bybit ws 未处理数据', $data);
        }
        call_user_func($onWsData, $data);
    }

    private function wsSubscribe()
    {
        $this->wsAcc->push(json_encode([
            'req_id' => (string)time(),
            'op' => 'subscribe',
            'args' => [
                'order',
                'execution',
                'position',
                // 'wallet'
            ]
        ]));
    }

    public function getKlineChannels()
    {
        return [
            '1' => '1',
            '3' => '3',
            '5' => '5',
            '15' => '15',
            '30' => '30',
            '60' => '60',
            '120' => '120',
            '240' => '240',
            '360' => '360',
            '720' => '720',
            'D' => 'D',
            'W' => 'W',
            'M' => 'M'
        ];
    }

    public function getSymbolInfos()
    {
        $res = $this->api->instruments(["category" => "linear", "limit" => "1000"]);
        if ($res['retCode'] != '0') {
            output('bybit获取所有交易对信息失败');
            return [];
        }
        return $res['result']['list'];
    }

    public function stopListen()
    {
        if (isset($this->wsAcc)) {
            $this->wsAcc->close();
        }
        if (isset($this->wsKline)) {
            $this->wsKline->close();
        }
    }

    public function placeOrder($param)
    {
        return $this->api->placeOrder($param);
    }

    public function getPositions($symbol = '')
    {
        $param = [
            'category' => 'linear'
        ];
        if ($symbol) {
            $param['symbol'] = $symbol;
        }
        return $this->api->getPositions($param);
    }

    public function setLever($symbol, $leverage)
    {
        $param = [
            'category' => 'linear',
            'symbol' => $symbol,
            'buyLeverage' => $leverage,
            'sellLeverage' => $leverage
        ];
        $res = $this->api->setLeverage($param);
        if ($res['retCode'] == 0) {
            output($symbol, '设置杠杆为:', $leverage);
            return true;
        } else {
            output($symbol, '设置杠杆错误', $res['retMsg']);
            return false;
        }
    }

    public function getKlines($symbol, $interval, $limit = 200, $start = '', $end = '')
    {
        $param = [
            'category' => 'linear',
            'symbol' => $symbol,
            'interval' => $interval,
            'limit' => $limit
        ];
        if ($start) {
            $param['start'] = $start;
        }
        if ($end) {
            $param['end'] = $end;
        }
        return $this->api->klines($param);
    }

    public function closeAllPos()
    {
        $res = $this->getPositions();
        if ($res['retCode'] == 0 && !empty($res['result']['list'])) {
            foreach ($res['result']['list'] as $pos) {
                if ($pos['size'] != '0') {
                    $this->closePos($pos['symbol'], $pos['side'], $pos['size']);
                }
            }
        }
    }

    private function closePos($symbol, $side, $size)
    {
        $param = [
            'category' => 'linear',
            'symbol' => $symbol,
            'side' => $side == 'Buy' ? 'Sell' : 'Buy',
            'orderType' => 'Market',
            'qty' => $size
        ];
        $res = $this->api->placeOrder($param);
        if ($res['retCode'] == 0) {
            return true;
        } else {
            output('平仓失败', $res);
            return false;
        }
    }
}