作者 karlet

feat:bitget初步完成

<?php
require_once __DIR__ . '/../trader/CmBroker.php';
require_once __DIR__ . '/../trader/struct/ApiInfo.php';
require_once __DIR__ . '/../jytools/BinanceFutures.php';
require_once __DIR__ . '/../jytools/func.php';
use trader\struct\ApiInfo;
use trader\struct\WsData;
use trader\CmBroker;
use function jytools\output;
$key = "bg_21bd365eaecaaa165a6167211785e2c0";
$secret = "d850a0b1e344157d91b0d2585805e3c8d92f0f61d867a11aaf2569f3a8587991";
$passphrase = "Z1345zbnm";
$apiInfo = new ApiInfo($key, $secret, $passphrase);
$wsHost = "ws://bgws.keetu.com";
$restHost = "http://bgapi.keetu.com";
$broker = new CmBroker(CmBroker::PLAT_BITGET, $apiInfo, $wsHost, $restHost);
output("开始监听");
// $broker->accListen(function (WsData $data) {
// if ($data->dataType == "order") {
// var_dump($data);
// }
// });
$broker->setLever("BTCUSDT", 20);
// $broker->klineListen("BTCUSDT", "1m", function ($data) {
// output($data->toArray());
// });
... ...
... ... @@ -6,6 +6,7 @@ require_once __DIR__ . '/struct/ApiInfo.php';
require_once __DIR__ . '/exchange/okx/ExBroker.php';
require_once __DIR__ . '/exchange/binance/ExBroker.php';
require_once __DIR__ . '/exchange/bybit/ExBroker.php';
require_once __DIR__ . '/exchange/bitget/ExBroker.php';
require_once __DIR__ . '/struct/Kline.php';
require_once __DIR__ . '/struct/Order.php';
require_once __DIR__ . '/struct/WsData.php';
... ... @@ -23,6 +24,7 @@ use trader\struct\ApiInfo;
use trader\exchange\okx\ExBroker as OkxBroker;
use trader\exchange\binance\ExBroker as BinanceBroker;
use trader\exchange\bybit\ExBroker as BybitBroker;
use trader\exchange\bitget\ExBroker as BitgetBroker;
use trader\struct\Kline;
use trader\struct\Order;
use trader\struct\WsData;
... ... @@ -52,7 +54,7 @@ class CmBroker
* @see PLAT_BITGET
*/
public $plat;
private OkxBroker|BinanceBroker|BybitBroker $exBroker;
private OkxBroker|BinanceBroker|BybitBroker|BitgetBroker $exBroker;
/** @var SymbolInfo[] $symbolInfos */
public $symbolInfos = [];
/** @var Pos[] $positions */
... ... @@ -75,14 +77,16 @@ class CmBroker
if ($plat == self::PLAT_BYBIT) {
$exBroker = new BybitBroker($apiInfo);
}
if ($plat == self::PLAT_BITGET) {
$exBroker = new BitgetBroker($apiInfo);
}
$this->exBroker = $exBroker;
if ($wsHost) {
if ($wsHost && $wsHost != "") {
$this->exBroker->setWsHost($wsHost);
}
if ($restHost) {
if ($restHost && $restHost != "") {
$this->exBroker->setRestHost($restHost);
}
var_dump("初始化。。。。");
$this->init();
}
public function setName($name)
... ... @@ -110,6 +114,9 @@ class CmBroker
if ($this->plat == self::PLAT_BYBIT) {
$this->bybitAccDataHandle($data, $onData);
}
if ($this->plat == self::PLAT_BITGET) {
$this->bitgetAccDataHandle($data, $onData);
}
});
}
//处理币安相关账户数据监听
... ... @@ -248,6 +255,52 @@ class CmBroker
return;
}
}
//处理bitget账户相关数据监听
private function bitgetAccDataHandle($data, $onData)
{
// 处理订单和成交数据
if (isset($data['arg']) && $data['arg']['channel'] == 'orders') {
foreach ($data['data'] as $value) {
// 处理成交数据
$wsDataTrade = WsDataTrade::TransferBitgetOrder($value, $this->symbolInfos, function ($symbol) {
return $this->getSymbolSt($symbol);
});
if ($wsDataTrade) {
$wsData = new WsData($this->plat, 'trade', $wsDataTrade);
$onData($wsData);
}
// 处理订单数据
$wsDataOrder = WsDataOrder::TransferBitgetOrder($value, $this->symbolInfos, function ($symbol) {
return $this->getSymbolSt($symbol);
});
if ($wsDataOrder) {
$wsData = new WsData($this->plat, 'order', null, null, $wsDataOrder);
$onData($wsData);
}
}
return;
}
// 处理账户和持仓数据
if (isset($data['arg']) && $data['arg']['channel'] == 'account') {
foreach ($data['data'] as $value) {
if (isset($value['positions'])) {
foreach ($value['positions'] as $pos) {
$wsDataPos = WsDataPos::TransferBitgetPos($pos, $this->symbolInfos, function ($symbol) {
return $this->getSymbolSt($symbol);
});
if ($wsDataPos) {
$this->positions[$wsDataPos->symbol . "_" . $wsDataPos->posSide] = $wsDataPos;
$wsData = new WsData($this->plat, 'pos', null, $wsDataPos);
$onData($wsData);
}
}
}
}
return;
}
}
public function klineListen($symbol, $peroid, $onData)
{
if ($this->plat == self::PLAT_BINANCE && $peroid == '1s') {
... ... @@ -266,6 +319,9 @@ class CmBroker
if ($this->plat == self::PLAT_BYBIT) {
$kline = Kline::transferBybit($data);
}
if ($this->plat == self::PLAT_BITGET) {
$kline = Kline::transferBitget($data);
}
$onData($kline);
});
}
... ... @@ -455,6 +511,19 @@ class CmBroker
}
$this->symbolInfos = $infos;
}
if ($this->plat == self::PLAT_BITGET) {
$res = $this->exBroker->getSymbolInfos();
$infos = [];
foreach ($res as $value) {
if ($value['quoteCoin'] == 'USDT' && $value['symbolStatus'] == 'normal') {
$info = SymbolInfo::transferBitget($value, function ($symbol) {
return $this->getSymbolSt($symbol);
});
$infos[$info->symbol] = $info;
}
}
$this->symbolInfos = $infos;
}
if (count($this->symbolInfos) == 0) {
$this->initSymbolInfos($count + 1);
return;
... ...
... ... @@ -4,6 +4,7 @@ namespace trader\exchange\bitget;
require_once __DIR__ . '/../../struct/ApiInfo.php';
require_once __DIR__ . '/../../../jytools/func.php';
require_once __DIR__ . '/../../../jytools/Curl.php';
use trader\struct\ApiInfo;
use jytools\Curl;
... ... @@ -11,14 +12,105 @@ use function jytools\getMicrotime;
class Api
{
private $host = '';
private $host = 'https://api.bitget.com';
private ApiInfo $apiInfo;
public function __construct(ApiInfo $apiInfo)
{
$this->apiInfo = $apiInfo;
}
public function setHost($host)
{
$this->host = $host;
}
//----------- public interface ------------
// 获取交易对信息
public function instruments($param)
{
$path = '/api/v2/mix/market/contracts';
$method = 'GET';
return $this->request($path, $method, $param);
}
// 获取K线数据
public function klines($param)
{
$path = '/api/v2/spot/market/candles';
$method = 'GET';
return $this->request($path, $method, $param);
}
//----------- private interface ------------
// 下单
public function placeOrder($param)
{
$path = '/api/v2/spot/trade/place-order';
$method = 'POST';
return $this->request($path, $method, $param, true);
}
// 查询持仓
public function getPositions($param)
{
$path = '/api/v2/mix/position/positions';
$method = 'GET';
return $this->request($path, $method, $param, true);
}
// 设置杠杆
public function setLeverage($param)
{
$path = '/api/v2/mix/account/set-leverage';
$method = 'POST';
return $this->request($path, $method, $param, true);
}
// 查询账户余额
public function accountBalance($param)
{
$path = '/api/v2/spot/account/assets';
$method = 'GET';
return $this->request($path, $method, $param, true);
}
//-------------------------------------
private function createSign($timestamp, $method, $requestPath, $body = '')
{
$message = $timestamp . $method . $requestPath . $body;
return base64_encode(hash_hmac('sha256', $message, $this->apiInfo->secret, true));
}
private function request($path, $method, $param, $auth = false)
{
$header = [];
$body = '';
$fullPath = $path;
if ($method == 'GET' && !empty($param)) {
$fullPath = $path . '?' . http_build_query($param);
} else {
$header[] = 'Content-Type: application/json';
$body = json_encode($param);
}
if ($auth) {
$timestamp = (string)getMicrotime();
$header[] = 'ACCESS-KEY: ' . $this->apiInfo->key;
$header[] = 'ACCESS-TIMESTAMP: ' . $timestamp;
$header[] = 'ACCESS-PASSPHRASE: ' . $this->apiInfo->passphrase;
$header[] = 'ACCESS-SIGN: ' . $this->createSign($timestamp, $method, $fullPath, $body);
}
$url = $this->host . $path;
$result = "";
if ($method == 'POST') {
$result = Curl::httpPost($url, $param, $header);
} else {
$result = Curl::httpGet($url, $param, $header);
}
return json_decode($result, true);
}
}
... ...
<?php
namespace trader\exchange\bitget;
require_once __DIR__ . '/../../struct/ApiInfo.php';
require_once __DIR__ . '/Api.php';
require_once __DIR__ . '/../../../jytools/Websocket.php';
require_once __DIR__ . '/../../../jytools/func.php';
use trader\struct\ApiInfo;
use trader\exchange\bitget\Api as BgApi;
use jytools\Websocket;
use function jytools\output;
use function jytools\getMicrotime;
class ExBroker
{
private $host = 'wss://ws.bitget.com';
private $pathPri = '/v2/ws/private'; //账户信息path
private $pathPub = '/v2/ws/public'; //行情信息path
private ApiInfo $apiInfo;
private BgApi $api;
private ?Websocket $wsAcc;
private ?Websocket $wsKline;
private $timerAccPing = 0;
private $timerKlinePing = 0;
public function __construct(ApiInfo $apiInfo)
{
$this->apiInfo = $apiInfo;
$this->api = new BgApi($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' => [
[
'instType' => 'SPOT',
'channel' => 'candle' . $interval,
'instId' => $symbol
]
]
]));
$this->wsKlinePing();
},
function ($data) use ($onData) {
$data = json_decode($data, true);
if (isset($data['data'])) {
$onData($data);
}
},
function () {
swoole_timer_clear($this->timerKlinePing);
}
);
}
private function wsKlinePing()
{
$this->timerKlinePing = swoole_timer_tick(1000 * 20, function () {
$this->wsKline->push('ping');
});
}
private function wsAccPing()
{
$this->timerAccPing = swoole_timer_tick(1000 * 20, function () {
$this->wsAcc->push('ping');
});
}
private function createWsSign($timestamp)
{
$message = $timestamp . 'GET' . '/user/verify';
return base64_encode(hash_hmac('sha256', $message, $this->apiInfo->secret, true));
}
private function wsLogin()
{
$timestamp = (string)(getMicrotime() / 1000);
$this->wsAcc->push(json_encode([
'op' => 'login',
'args' => [
[
'apiKey' => $this->apiInfo->key,
'passphrase' => $this->apiInfo->passphrase,
'timestamp' => $timestamp,
'sign' => $this->createWsSign($timestamp)
]
]
]));
}
private function onWsDataPre($data, callable $onWsData)
{
if ($data == 'pong') {
return;
}
$data = json_decode($data, true);
if (isset($data['event'])) {
if ($data['event'] == 'login' && $data['code'] == '0') {
output('ws登录成功');
$this->wsSubscribe();
return;
}
if ($data['event'] == 'subscribe') {
return;
}
output('bitget ws 未处理数据', $data);
}
call_user_func($onWsData, $data);
}
private function wsSubscribe()
{
$this->wsAcc->push(json_encode([
'op' => 'subscribe',
'args' => [
[
'instType' => 'SPOT',
'channel' => 'account',
'instId' => 'default'
],
[
'instType' => 'SPOT',
'channel' => 'orders',
'instId' => 'default'
]
]
]));
}
public function placeOrder(array $params)
{
return $this->api->placeOrder($params);
}
public function setLever($symbol, $lever)
{
$res = $this->api->setLeverage([
'symbol' => $symbol,
'leverage' => $lever,
'productType' => 'USDT-FUTURES',
'marginCoin' => 'USDT'
]);
if (!isset($res['code']) || $res['code'] != '00000') {
output('bitget设置杠杆失败', $res);
} else {
output('bitget设置杠杆成功', $res);
}
return $res;
}
public function getAllPos()
{
return $this->api->getPositions(['productType' => 'SPOT']);
}
public function getSymbolInfos()
{
$res = $this->api->instruments([
'productType' => 'USDT-FUTURES'
]);
if (!isset($res['data'])) {
output('bitget获取所有交易对信息失败');
return [];
}
return $res['data'];
}
}
... ...
... ... @@ -85,4 +85,18 @@ class Kline
$isFinal = $kline['confirm'];
return new Kline($time, $open, $high, $low, $close, $vol, $volQuote, $uts, $isFinal);
}
public static function transferBitget($data)
{
$kline = $data['data'][0];
$time = (int)($kline['ts']);
$open = (float)$kline['open'];
$high = (float)$kline['high'];
$low = (float)$kline['low'];
$close = (float)$kline['close'];
$vol = (float)$kline['vol'];
$volQuote = (float)$kline['volCcy'];
$uts = $time;
$isFinal = $kline['confirm'] ?? true;
return new Kline($time, $open, $high, $low, $close, $vol, $volQuote, $uts, $isFinal);
}
}
... ...
... ... @@ -94,6 +94,30 @@ class SymbolInfo
$info = new SymbolInfo($symbolOri, $symbolSt, $ctVal, $pricePrec, $qtyPrec, $lotPrec, $minLot, $minQty, $mul);
return $info;
}
public static function transferBitget($data, callable $getSymbolSt): SymbolInfo
{
$symbolOri = $data['symbol'];
$symbolSt = $getSymbolSt($symbolOri); //转换为标准交易对
$ctVal = 1; // Bitget合约面值为1
$pricePrec = (int)$data['pricePlace'];
$qtyPrec = (int)$data['volumePlace'];
$lotPrec = $qtyPrec;
$minQty = $data['minTradeNum'];
$minLot = $minQty * $ctVal;
$mul = self::extractNumber($symbolOri);
return new SymbolInfo(
$symbolOri,
$symbolSt,
$ctVal,
$pricePrec,
$qtyPrec,
$lotPrec,
$minLot,
$minQty,
$mul
);
}
public static function extractNumber($str)
{
// 示例
... ...
... ... @@ -85,13 +85,13 @@ class WsDataOrder
$ordType = strtoupper($data['ordType']);
$cliOrdId = $data['clOrdId'];
$ordId = $data['ordId'];
$status = self::getOkStatus($data['state']);
$status = self::getOkxStatus($data['state']);
if ($data['cancelSource'] != '') {
$status = 'CANCELED';
}
return new WsDataOrder($platform, $posSide, $symbol, $side, $price, $avgPx, $qty, $lot, $pnl, $ts, $uts, $ordType, $cliOrdId, $ordId, $status);
}
private static function getOkStatus(string $state)
private static function getOkxStatus(string $state)
{
if ($state == 'live') {
return 'LIVE';
... ... @@ -129,9 +129,28 @@ class WsDataOrder
$ordType = strtoupper($order['o']);
$cliOrdId = $order['c'];
$ordId = $order['i'];
$status = self::getBnStatus($order['X']);
$status = self::getBinanceStatus($order['X']);
return new WsDataOrder($platform, $posSide, $symbol, $side, $price, $avgPx, $qty, $lot, $pnl, $ts, $uts, $ordType, $cliOrdId, $ordId, $status);
}
private static function getBinanceStatus(string $state)
{
if ($state == 'NEW') {
return 'LIVE';
}
if ($state == 'PARTIALLY_FILLED') {
return 'PARTIALLY_FILLED';
}
if ($state == 'FILLED') {
return 'FILLED';
}
if ($state == 'CANCELED') {
return 'CANCELED';
}
if ($state == 'EXPIRED' || $state == "EXPIRED_IN_MATCH") {
return 'CANCELED';
}
return 'UNKNOWN';
}
public static function TransferBybitOrder($data, $symbolInfos, callable $toSymbolSt): WsDataOrder|null
{
$symbol = call_user_func($toSymbolSt, $data['symbol']);
... ... @@ -166,25 +185,6 @@ class WsDataOrder
$status = self::getBybitStatus($data['orderStatus']);
return new WsDataOrder($platform, $posSide, $symbol, $side, $price, $avgPx, $qty, $lot, $pnl, $ts, $uts, $ordType, $cliOrdId, $ordId, $status);
}
private static function getBnStatus(string $state)
{
if ($state == 'NEW') {
return 'LIVE';
}
if ($state == 'PARTIALLY_FILLED') {
return 'PARTIALLY_FILLED';
}
if ($state == 'FILLED') {
return 'FILLED';
}
if ($state == 'CANCELED') {
return 'CANCELED';
}
if ($state == 'EXPIRED' || $state == "EXPIRED_IN_MATCH") {
return 'CANCELED';
}
return 'UNKNOWN';
}
private static function getBybitStatus(string $state)
{
if ($state == 'New') {
... ... @@ -201,4 +201,47 @@ class WsDataOrder
}
return 'UNKNOWN';
}
public static function TransferBitgetOrder(array $data, array $symbolInfos, callable $toSymbolSt): WsDataOrder|null
{
$symbol = call_user_func($toSymbolSt, $data['symbol']);
/** @var SymbolInfo $symbolInfo */
$symbolInfo = $symbolInfos[$symbol] ?? null;
if ($symbolInfo === null) {
return null;
}
return new WsDataOrder(
'bitget',
'BOTH',
$symbol,
strtoupper($data['side']),
(float)$data['price'],
(float)$data['fillPrice'],
(float)$data['size'],
(float)$data['size'],
0,
(int)($data['cTime']),
(int)($data['uTime']),
strtoupper($data['orderType']),
$data['clientOid'],
$data['orderId'],
self::getBitgetStatus($data['status'])
);
}
private static function getBitgetStatus($status)
{
switch ($status) {
case 'new':
return 'LIVE';
case 'partial-filled':
return 'PARTIALLY_FILLED';
case 'filled':
return 'FILLED';
case 'cancelled':
return 'CANCELED';
default:
return 'UNKNOWN';
}
}
}
... ...
... ... @@ -92,4 +92,23 @@ class WsDataPos
$pnl = (float)$data['unrealisedPnl'];
return new WsDataPos($symbol, $posSide, $qty, $lot, $avgPrice, false, $pnl);
}
public static function TransferBitgetPos(array $data, array $symbolInfos, callable $toSymbolSt): WsDataPos|null
{
$symbol = call_user_func($toSymbolSt, $data['symbol']);
/** @var SymbolInfo $symbolInfo */
$symbolInfo = $symbolInfos[$symbol] ?? null;
if ($symbolInfo === null) {
return null;
}
return new WsDataPos(
$symbol,
'BOTH',
(float)$data['total'],
(float)$data['total'],
(float)$data['averageOpenPrice'],
false,
(float)$data['unrealizedPL']
);
}
}
... ...
... ... @@ -139,4 +139,35 @@ class WsDataTrade
$lever = 0;
return new WsDataTrade($platform, $posSide, $symbol, $side, $price, $qty, $lot, $pnl, $fee, $quoteVol, $ts, $tradeId, $ordId, $cliOrdId, $lever);
}
public static function TransferBitgetOrder(array $data, array $symbolInfos, callable $toSymbolSt): WsDataTrade|null
{
if ($data['status'] != 'filled' && $data['status'] != 'partial-filled') {
return null;
}
$symbol = call_user_func($toSymbolSt, $data['symbol']);
/** @var SymbolInfo $symbolInfo */
$symbolInfo = $symbolInfos[$symbol] ?? null;
if ($symbolInfo === null) {
return null;
}
return new WsDataTrade(
'bitget',
'BOTH',
$symbol,
strtoupper($data['side']),
(float)$data['fillPrice'],
(float)$data['fillQuantity'],
(float)$data['fillQuantity'],
0,
(float)$data['fee'],
(float)$data['fillPrice'] * (float)$data['fillQuantity'],
(int)($data['cTime']),
$data['tradeId'],
$data['orderId'],
$data['clientOid'],
1
);
}
}
... ...