作者 karlet

feat:bybit接入

@@ -15,10 +15,11 @@ $apiInfo = new ApiInfo($key, $secret, ""); @@ -15,10 +15,11 @@ $apiInfo = new ApiInfo($key, $secret, "");
15 $wsHost = "ws://bbws.keetu.com"; 15 $wsHost = "ws://bbws.keetu.com";
16 $restHost = "http://bbapi.keetu.com"; 16 $restHost = "http://bbapi.keetu.com";
17 $broker = new CmBroker(CmBroker::PLAT_BYBIT, $apiInfo, $wsHost, $restHost); 17 $broker = new CmBroker(CmBroker::PLAT_BYBIT, $apiInfo, $wsHost, $restHost);
18 -// $broker->accListen(function ($data) {  
19 -// // var_dump($data);  
20 -// }); 18 +
21 output("开始监听"); 19 output("开始监听");
22 -$broker->klineListen("BTCUSDT", "1m", function ($data) {  
23 - output($data->toArray()); 20 +$broker->accListen(function ($data) {
  21 + var_dump($data);
24 }); 22 });
  23 +// $broker->klineListen("BTCUSDT", "1m", function ($data) {
  24 +// output($data->toArray());
  25 +// });
@@ -107,6 +107,9 @@ class CmBroker @@ -107,6 +107,9 @@ class CmBroker
107 if ($this->plat == self::PLAT_OKX) { 107 if ($this->plat == self::PLAT_OKX) {
108 $this->okxAccDataHandle($data, $onData); 108 $this->okxAccDataHandle($data, $onData);
109 } 109 }
  110 + if ($this->plat == self::PLAT_BYBIT) {
  111 + $this->bybitAccDataHandle($data, $onData);
  112 + }
110 }); 113 });
111 } 114 }
112 //处理币安相关账户数据监听 115 //处理币安相关账户数据监听
@@ -203,6 +206,41 @@ class CmBroker @@ -203,6 +206,41 @@ class CmBroker
203 } 206 }
204 $this->msg("okx 无处理ws数据实现", $data); 207 $this->msg("okx 无处理ws数据实现", $data);
205 } 208 }
  209 + //处理bybit账户相关数据监听
  210 + private function bybitAccDataHandle($data, $onData)
  211 + {
  212 + if (isset($data['topic']) && $data['topic'] == 'order') {
  213 + $wsDataTrade = WsDataTrade::TransferBybitTrade($data, $this->symbolInfos, function ($symbol) {
  214 + return $this->getSymbolSt($symbol);
  215 + });
  216 + if ($wsDataTrade != null) {
  217 + $wsData = new WsData($this->plat, 'trade', $trade = $wsDataTrade);
  218 + $onData($wsData);
  219 + }
  220 + $wsDataOrd = WsDataOrder::TransferBybitOrder($data, $this->symbolInfos, function ($symbol) {
  221 + return $this->getSymbolSt($symbol);
  222 + });
  223 + if ($wsDataOrd != null) {
  224 + $wsData = new WsData($this->plat, 'order', $trade = null, $pos = null, $order = $wsDataOrd);
  225 + $onData($wsData);
  226 + }
  227 + return;
  228 + }
  229 + if (isset($data['topic']) && $data['topic'] == 'position') {
  230 + foreach ($data['data'] as $key => $value) {
  231 + $wsDataPos = WsDataPos::TransferBybitPos($value, $this->symbolInfos, function ($symbol) {
  232 + return $this->getSymbolSt($symbol);
  233 + });
  234 + }
  235 + if ($wsDataPos != null) {
  236 + $pos = Pos::transferWsDataPos($wsDataPos);
  237 + $this->positions[$wsDataPos->symbol . "_" . $wsDataPos->posSide] = $pos;
  238 + $wsData = new WsData($this->plat, 'pos', $trade = null, $pos = $wsDataPos);
  239 + $onData($wsData);
  240 + }
  241 + return;
  242 + }
  243 + }
206 public function klineListen($symbol, $peroid, $onData) 244 public function klineListen($symbol, $peroid, $onData)
207 { 245 {
208 if ($this->plat == self::PLAT_BINANCE && $peroid == '1s') { 246 if ($this->plat == self::PLAT_BINANCE && $peroid == '1s') {
@@ -11,6 +11,7 @@ use trader\struct\ApiInfo; @@ -11,6 +11,7 @@ use trader\struct\ApiInfo;
11 use trader\exchange\bybit\Api as BbApi; 11 use trader\exchange\bybit\Api as BbApi;
12 use jytools\Websocket; 12 use jytools\Websocket;
13 use function jytools\output; 13 use function jytools\output;
  14 +use function jytools\getMicrotime;
14 15
15 class ExBroker 16 class ExBroker
16 { 17 {
@@ -21,6 +22,8 @@ class ExBroker @@ -21,6 +22,8 @@ class ExBroker
21 private BbApi $api; 22 private BbApi $api;
22 private ?Websocket $wsAcc; 23 private ?Websocket $wsAcc;
23 private ?Websocket $wsKline; 24 private ?Websocket $wsKline;
  25 + private $timerAccPing = 0;
  26 + private $timerKlinePing = 0;
24 27
25 public function __construct(ApiInfo $apiInfo) 28 public function __construct(ApiInfo $apiInfo)
26 { 29 {
@@ -38,10 +41,31 @@ class ExBroker @@ -38,10 +41,31 @@ class ExBroker
38 $this->api->setHost($host); 41 $this->api->setHost($host);
39 } 42 }
40 43
41 - public function accListen(callable $onData) {} 44 + public function accListen(callable $onData)
  45 + {
  46 + if (isset($this->wsAcc)) {
  47 + $this->wsAcc->close();
  48 + }
  49 + $this->wsAcc = new Websocket($this->host . $this->pathPri);
  50 + $this->wsAcc->connect(
  51 + function () {
  52 + $this->wsLogin();
  53 + $this->wsAccPing();
  54 + },
  55 + function ($data) use ($onData) {
  56 + $this->onWsDataPre($data, $onData);
  57 + },
  58 + function () {
  59 + swoole_timer_clear($this->timerAccPing);
  60 + }
  61 + );
  62 + }
42 63
43 public function klineListen($symbol, $interval, callable $onData) 64 public function klineListen($symbol, $interval, callable $onData)
44 { 65 {
  66 + if (isset($this->wsKline)) {
  67 + $this->wsKline->close();
  68 + }
45 $this->wsKline = new Websocket($this->host . $this->pathPub); 69 $this->wsKline = new Websocket($this->host . $this->pathPub);
46 $this->wsKline->connect( 70 $this->wsKline->connect(
47 function () use ($symbol, $interval) { 71 function () use ($symbol, $interval) {
@@ -51,15 +75,118 @@ class ExBroker @@ -51,15 +75,118 @@ class ExBroker
51 "kline.{$interval}.{$symbol}" 75 "kline.{$interval}.{$symbol}"
52 ] 76 ]
53 ])); 77 ]));
  78 + $this->wsKlinePing();
54 }, 79 },
55 function ($data) use ($onData) { 80 function ($data) use ($onData) {
56 $data = json_decode($data, true); 81 $data = json_decode($data, true);
  82 + if (isset($data['op']) && $data['op'] == 'pong') {
  83 + return; //pong 消息不处理
  84 + }
57 if (isset($data['data'])) { 85 if (isset($data['data'])) {
58 $onData($data); 86 $onData($data);
59 } 87 }
60 }, 88 },
  89 + function () {
  90 + swoole_timer_clear($this->timerKlinePing);
  91 + }
61 ); 92 );
62 } 93 }
  94 +
  95 + private function wsKlinePing()
  96 + {
  97 + $this->timerKlinePing = swoole_timer_tick(1000 * 20, function () {
  98 + $pingMsg = [
  99 + 'op' => 'ping',
  100 + 'req_id' => (string)time(),
  101 + ];
  102 + $this->wsKline->push(json_encode($pingMsg));
  103 + });
  104 + }
  105 +
  106 + private function wsAccPing()
  107 + {
  108 + $this->timerAccPing = swoole_timer_tick(1000 * 20, function () {
  109 + $pingMsg = [
  110 + 'op' => 'ping',
  111 + 'req_id' => (string)time(),
  112 + ];
  113 + $this->wsAcc->push(json_encode($pingMsg));
  114 + });
  115 + }
  116 +
  117 + private function createWsSign($expires)
  118 + {
  119 + $param = "GET/realtime" . $expires;
  120 + return hash_hmac('sha256', $param, $this->apiInfo->secret);
  121 + }
  122 +
  123 + private function wsLogin()
  124 + {
  125 + $expires = getMicrotime() + 10 * 1000;
  126 + $this->wsAcc->push(json_encode([
  127 + 'req_id' => (string)time(),
  128 + 'op' => 'auth',
  129 + 'args' => [
  130 + $this->apiInfo->key,
  131 + $expires,
  132 + $this->createWsSign($expires)
  133 + ]
  134 + ]));
  135 + }
  136 +
  137 + private function onWsDataPre($data, callable $onWsData)
  138 + {
  139 + $data = json_decode($data, true);
  140 + if (isset($data['op'])) {
  141 + if ($data['op'] == 'pong') {
  142 + return; //pong 消息不处理
  143 + }
  144 + if ($data['op'] == 'auth' && $data['success']) {
  145 + output('ws登录成功');
  146 + $this->wsSubscribe();
  147 + return;
  148 + }
  149 + if ($data['op'] == 'subscribe') {
  150 + return;
  151 + }
  152 + output('bybit ws 未处理数据', $data);
  153 + }
  154 + call_user_func($onWsData, $data);
  155 + }
  156 +
  157 + private function wsSubscribe()
  158 + {
  159 + $this->wsAcc->push(json_encode([
  160 + 'req_id' => (string)time(),
  161 + 'op' => 'subscribe',
  162 + 'args' => [
  163 + // 'order',
  164 + // 'execution',
  165 + 'position',
  166 + // 'wallet'
  167 + ]
  168 + ]));
  169 + }
  170 +
  171 + public function getKlineChannels()
  172 + {
  173 + return [
  174 + '1' => '1',
  175 + '3' => '3',
  176 + '5' => '5',
  177 + '15' => '15',
  178 + '30' => '30',
  179 + '60' => '60',
  180 + '120' => '120',
  181 + '240' => '240',
  182 + '360' => '360',
  183 + '720' => '720',
  184 + 'D' => 'D',
  185 + 'W' => 'W',
  186 + 'M' => 'M'
  187 + ];
  188 + }
  189 +
63 public function getSymbolInfos() 190 public function getSymbolInfos()
64 { 191 {
65 $res = $this->api->instruments(["category" => "linear"]); 192 $res = $this->api->instruments(["category" => "linear"]);
@@ -69,4 +196,95 @@ class ExBroker @@ -69,4 +196,95 @@ class ExBroker
69 } 196 }
70 return $res['result']['list']; 197 return $res['result']['list'];
71 } 198 }
  199 +
  200 + public function stopListen()
  201 + {
  202 + if (isset($this->wsAcc)) {
  203 + $this->wsAcc->close();
  204 + }
  205 + if (isset($this->wsKline)) {
  206 + $this->wsKline->close();
  207 + }
  208 + }
  209 +
  210 + public function placeOrder($param)
  211 + {
  212 + return $this->api->placeOrder($param);
  213 + }
  214 +
  215 + public function getPositions($symbol = '')
  216 + {
  217 + $param = [
  218 + 'category' => 'linear'
  219 + ];
  220 + if ($symbol) {
  221 + $param['symbol'] = $symbol;
  222 + }
  223 + return $this->api->getPositions($param);
  224 + }
  225 +
  226 + public function setLeverage($symbol, $leverage)
  227 + {
  228 + $param = [
  229 + 'category' => 'linear',
  230 + 'symbol' => $symbol,
  231 + 'buyLeverage' => $leverage,
  232 + 'sellLeverage' => $leverage
  233 + ];
  234 + $res = $this->api->setLeverage($param);
  235 + if ($res['retCode'] == 0) {
  236 + output($symbol, '设置杠杆为:', $leverage);
  237 + return true;
  238 + } else {
  239 + output($symbol, '设置杠杆错误', $res['retMsg']);
  240 + return false;
  241 + }
  242 + }
  243 +
  244 + public function getKlines($symbol, $interval, $limit = 200, $start = '', $end = '')
  245 + {
  246 + $param = [
  247 + 'category' => 'linear',
  248 + 'symbol' => $symbol,
  249 + 'interval' => $interval,
  250 + 'limit' => $limit
  251 + ];
  252 + if ($start) {
  253 + $param['start'] = $start;
  254 + }
  255 + if ($end) {
  256 + $param['end'] = $end;
  257 + }
  258 + return $this->api->klines($param);
  259 + }
  260 +
  261 + public function closeAllPos()
  262 + {
  263 + $res = $this->getPositions();
  264 + if ($res['retCode'] == 0 && !empty($res['result']['list'])) {
  265 + foreach ($res['result']['list'] as $pos) {
  266 + if ($pos['size'] != '0') {
  267 + $this->closePos($pos['symbol'], $pos['side'], $pos['size']);
  268 + }
  269 + }
  270 + }
  271 + }
  272 +
  273 + private function closePos($symbol, $side, $size)
  274 + {
  275 + $param = [
  276 + 'category' => 'linear',
  277 + 'symbol' => $symbol,
  278 + 'side' => $side == 'Buy' ? 'Sell' : 'Buy',
  279 + 'orderType' => 'Market',
  280 + 'qty' => $size
  281 + ];
  282 + $res = $this->api->placeOrder($param);
  283 + if ($res['retCode'] == 0) {
  284 + return true;
  285 + } else {
  286 + output('平仓失败', $res);
  287 + return false;
  288 + }
  289 + }
72 } 290 }
@@ -132,6 +132,31 @@ class WsDataOrder @@ -132,6 +132,31 @@ class WsDataOrder
132 $status = self::getBnStatus($order['X']); 132 $status = self::getBnStatus($order['X']);
133 return new WsDataOrder($platform, $posSide, $symbol, $side, $price, $avgPx, $qty, $lot, $pnl, $ts, $uts, $ordType, $cliOrdId, $ordId, $status); 133 return new WsDataOrder($platform, $posSide, $symbol, $side, $price, $avgPx, $qty, $lot, $pnl, $ts, $uts, $ordType, $cliOrdId, $ordId, $status);
134 } 134 }
  135 + public static function TransferBybitOrder($data, $symbolInfos, callable $toSymbolSt): WsDataOrder|null
  136 + {
  137 + $order = $data['data'];
  138 + $symbol = call_user_func($toSymbolSt, $order['symbol']);
  139 + /** @var SymbolInfo $symbolInfo */
  140 + $symbolInfo = $symbolInfos[$symbol] ?? null;
  141 + if ($symbolInfo == null) {
  142 + return null;
  143 + }
  144 + $platform = 'bybit';
  145 + $posSide = strtoupper($order['side']);
  146 + $side = strtoupper($order['side']);
  147 + $price = (float)$order['price'];
  148 + $avgPx = (float)$order['avg_price'];
  149 + $lot = (float)$order['qty'];
  150 + $qty = $lot;
  151 + $pnl = (float)$order['realised_pnl'];
  152 + $ts = (int)$order['transact_time'];
  153 + $uts = (int)$order['transact_time'];
  154 + $ordType = strtoupper($order['order_type']);
  155 + $cliOrdId = $order['order_link_id'];
  156 + $ordId = $order['order_id'];
  157 + $status = self::getBnStatus($order['order_status']);
  158 + return new WsDataOrder($platform, $posSide, $symbol, $side, $price, $avgPx, $qty, $lot, $pnl, $ts, $uts, $ordType, $cliOrdId, $ordId, $status);
  159 + }
135 private static function getBnStatus(string $state) 160 private static function getBnStatus(string $state)
136 { 161 {
137 if ($state == 'NEW') { 162 if ($state == 'NEW') {
@@ -71,4 +71,19 @@ class WsDataPos @@ -71,4 +71,19 @@ class WsDataPos
71 $pnl = (float)$data['up']; 71 $pnl = (float)$data['up'];
72 return new WsDataPos($symbol, $posSide, $qty, $lot, $avgPrice, false, $pnl); 72 return new WsDataPos($symbol, $posSide, $qty, $lot, $avgPrice, false, $pnl);
73 } 73 }
  74 + public static function TransferBybitPos($data, $symbolInfos, callable $toSymbolSt): WsDataPos|null
  75 + {
  76 + $symbol = call_user_func($toSymbolSt, $data['symbol']);
  77 + /** @var SymbolInfo $symbolInfo */
  78 + $symbolInfo = $symbolInfos[$symbol] ?? null;
  79 + if ($symbolInfo === null) {
  80 + return null;
  81 + }
  82 + $posSide = strtoupper($data['side']);
  83 + $qty = (float)$data['size'];
  84 + $lot = $qty;
  85 + $avgPrice = (float)$data['entryPrice'];
  86 + $pnl = (float)$data['unrealisedPnl'];
  87 + return new WsDataPos($symbol, $posSide, $qty, $lot, $avgPrice, false, $pnl);
  88 + }
74 } 89 }
@@ -114,4 +114,24 @@ class WsDataTrade @@ -114,4 +114,24 @@ class WsDataTrade
114 $lever = 0; 114 $lever = 0;
115 return new WsDataTrade($platform, $posSide, $symbol, $side, $price, $qty, $lot, $pnl, $fee, $quoteVol, $ts, $tradeId, $ordId, $cliOrdId, $lever); 115 return new WsDataTrade($platform, $posSide, $symbol, $side, $price, $qty, $lot, $pnl, $fee, $quoteVol, $ts, $tradeId, $ordId, $cliOrdId, $lever);
116 } 116 }
  117 + public static function TransferBybitTrade($data, $symbolInfos, callable $toSymbolSt): WsDataTrade|null
  118 + {
  119 + $platform = 'bybit';
  120 + $order = $data['data'];
  121 + $symbol = call_user_func($toSymbolSt, $order['symbol']);
  122 + $posSide = strtoupper($order['side']);
  123 + $side = strtoupper($order['side']);
  124 + $price = (float)$order['price'];
  125 + $qty = (float)$order['qty'];
  126 + $lot = $qty;
  127 + $pnl = (float)$order['realised_pnl'];
  128 + $fee = (float)$order['fee'];
  129 + $quoteVol = $price * $qty;
  130 + $ts = $order['transact_time'];
  131 + $tradeId = $order['order_id'];
  132 + $ordId = $order['order_id'];
  133 + $cliOrdId = $order['order_link_id'];
  134 + $lever = 0;
  135 + return new WsDataTrade($platform, $posSide, $symbol, $side, $price, $qty, $lot, $pnl, $fee, $quoteVol, $ts, $tradeId, $ordId, $cliOrdId, $lever);
  136 + }
117 } 137 }