├── functions.php ├── LICENSE.md ├── config.php ├── main.php ├── README.md └── php-binance-api.php /functions.php: -------------------------------------------------------------------------------- 1 | >"; 5 | $_GLOBALS['secret_key'] = "<>"; 6 | 7 | // this variable controls how close the hedge should be to the actual position / when to stop trading / creating new buy / sell orders 8 | // once the delta in btc (between the current open position (hedge) / and the wallet balance of Coin-M futures) is near this value, stop trading 9 | $_GLOBALS['delta_amount_in_btc_to_stop_trading'] = "0.01"; // in BTC terms 10 | 11 | // These variables control the amounts / sizes when new orders are placed. These are ranges - so new orders will randomized be between two values. 12 | $_GLOBALS['per_order_min'] = 0.01; 13 | $_GLOBALS['per_order_max'] = 0.02; 14 | 15 | // Symbol to trade - note, this code is designed for BTCUSD_PERP under COIN-M futures, if you try to use it for other symbols, it may break because of decimal order sizes or other issues - if you're interested in other symbols or features, please reach out. 16 | $symbol = "BTCUSD_PERP"; 17 | 18 | // initialize / connect to the Binance API with the specified keys 19 | $api = new Binance\API( 20 | $_GLOBALS['api_key'], 21 | $_GLOBALS['secret_key'] 22 | ); 23 | 24 | 25 | ?> -------------------------------------------------------------------------------- /main.php: -------------------------------------------------------------------------------- 1 | delivery_account_information(); 14 | 15 | foreach($delivery_account_informations['assets'] as $asset) { 16 | if ($asset['asset'] == 'BTC') { 17 | $user['futures_wallet_amount_BTC'] = $asset; 18 | // print_r($asset); 19 | break 1; 20 | } 21 | } 22 | 23 | foreach($delivery_account_informations['positions'] as $position) { 24 | if ($position['symbol'] == 'BTCUSD_PERP') { 25 | $user['futures_current_position'] = $position; 26 | // print_r($position); 27 | break 1; 28 | } 29 | } 30 | 31 | // print_r($user); 32 | 33 | // calculate difference / delta in the wallet vs. position 34 | $delta_wallet_position_amt = $user['futures_wallet_amount_BTC']['walletBalance'] - (abs($user['futures_current_position']['notionalValue']) - abs($user['futures_wallet_amount_BTC']['unrealizedProfit'])); 35 | $abs_delta_wallet_position_amt = abs($delta_wallet_position_amt); 36 | 37 | print "Delta btwn Wallet / Position Amt = {$delta_wallet_position_amt}\n"; 38 | 39 | // which one is larger? wallet or notionalValue (-profLoss) 40 | if ($abs_delta_wallet_position_amt < $_GLOBALS['delta_amount_in_btc_to_stop_trading']) { 41 | die("WalletBalance / OpenPosition are very close to : {$abs_delta_wallet_position_amt} BTC - stopping trading\n"); 42 | } else if ($user['futures_wallet_amount_BTC']['walletBalance'] > ((abs($user['futures_current_position']['notionalValue']) - abs($user['futures_wallet_amount_BTC']['unrealizedProfit'])))) { 43 | print "WalletBalance is larger by {$abs_delta_wallet_position_amt} BTC so I have to short more.\n"; 44 | $direction_to_go = "SELL"; 45 | } else { 46 | print "NotionalValue +/- ProfLoss is Larger by {$abs_delta_wallet_position_amt} BTC so I have to buy / cover.\n"; 47 | $direction_to_go = "BUY"; 48 | } 49 | 50 | $instrument_name = $symbol; 51 | 52 | $ticker = $api->delivery_symbol_order_book_ticker($symbol); 53 | $ticker = $ticker[0]; 54 | 55 | /* 56 | print_r($ticker); 57 | die(); 58 | */ 59 | 60 | // -- set the max / min values based on the delta btc amt 61 | $max_trade_size = $_GLOBALS['per_order_max']; 62 | $min_trade_size = $_GLOBALS['per_order_min']; 63 | 64 | $amount_in_btc = mt_rand($min_trade_size*100, $max_trade_size*100)/100; // number of COIN for buy / sell 65 | 66 | // if the open position is close to the wallet balance, the next order amount submitted doesn't cause the open position to go beyond the delta - aka it adjusts the new orders to help get close to the wallet balance and create a perfect hedge and eventually stop the script from submitting new orders 67 | if ($amount_in_btc > $abs_delta_wallet_position_amt) { 68 | $min_trade_size = 0.01; 69 | $max_trade_size = $abs_delta_wallet_position_amt; 70 | $amount_in_btc = mt_rand($min_trade_size*100, $max_trade_size*100)/100; // number of COIN for buy / sell 71 | } 72 | 73 | $amount_in_USD = $amount_in_btc*$ticker['bidPrice']; 74 | $amount = round(($amount_in_btc*$ticker['bidPrice'])/100); 75 | 76 | // convert the $amount into BTC amount 77 | print <<delivery_position_information(); 95 | 96 | 97 | $i=0; 98 | foreach($openPositions as $openPosition) { 99 | if ($openPosition['symbol'] != $symbol) { 100 | unset($openPositions[$i]); 101 | } 102 | $i++; 103 | } 104 | 105 | $openPositions = array_values($openPositions); 106 | 107 | $temp_openOrders = $api->delivery_current_all_open_orders($symbol); 108 | 109 | $openOrders = array(); 110 | foreach($temp_openOrders as $openOrder) { 111 | if ($openOrder['type'] == "LIMIT") { 112 | $openOrders[] = $openOrder; 113 | } 114 | } 115 | 116 | if (sizeof($openOrders) > 1) { 117 | $orderC = $api->delivery_cancel_all_open_orders($symbol); 118 | $cancel = true; 119 | } else if (sizeof($openOrders) == 1) { 120 | $current_openOrder_price = sprintf('%.8f', $openOrders[0]['price']); 121 | if ($direction_to_go == "BUY" && $current_openOrder_price == $ticker['bidPrice']) { 122 | $cancel = false; 123 | } else if ($direction_to_go == "SELL" && $current_openOrder_price == $ticker['askPrice']) { 124 | $cancel = false; 125 | } else { 126 | $orderC = $api->delivery_cancel_all_open_orders($symbol); 127 | $cancel = true; 128 | } 129 | } 130 | 131 | $rand_string = random_strings(17); 132 | $newClientOrderId = "HEDGER-MC-{$rand_string}"; 133 | // string | user defined label for the order (maximum 32 characters) 134 | 135 | if ($direction_to_go == "BUY") { 136 | if ($cancel == true || ($current_openOrder_price != $ticker['bidPrice'])) { 137 | print "bid: {$ticker['bidPrice']}\n"; 138 | print "Amount: {$amount}\n"; 139 | 140 | $price = $ticker['bidPrice']; 141 | while (1 == 1) { 142 | $order = $api->delivery_create_order($side = "BUY", $symbol, $amount, $price, $type = "LIMIT", array('newClientOrderId' => $newClientOrderId), false); 143 | 144 | print_r($order); 145 | // die(); 146 | 147 | if ($order['status'] != "NEW") { 148 | $price -= 0.50; 149 | 150 | $order = $api->delivery_create_order($side = "BUY", $symbol, $amount, $price, $type = "LIMIT", array('newClientOrderId' => $newClientOrderId), false); 151 | print_r($order); 152 | 153 | $price -= 0.50; 154 | } else { 155 | break 1; 156 | } 157 | die(); 158 | } 159 | } else { 160 | print "Current Order Price = Current bid Price\n"; 161 | } 162 | } else if ($direction_to_go == "SELL") { 163 | if ($cancel == true || ($current_openOrder_price != $ticker['askPrice'])) { 164 | print "ask: {$ticker['askPrice']}\n"; 165 | print "Amount: {$amount}\n"; 166 | 167 | $price = $ticker['askPrice']; 168 | while(1 == 1) { 169 | $order = $api->delivery_create_order($side = "SELL", $symbol, $amount, $price, $type = "LIMIT", array('newClientOrderId' => $newClientOrderId), false); 170 | 171 | print_r($order); 172 | if ($order['status'] != "NEW") { 173 | $price += 0.50; 174 | 175 | $order = $api->delivery_create_order($side = "SELL", $symbol, $amount, $price, $type = "LIMIT", array('newClientOrderId' => $newClientOrderId), false); 176 | print_r($order); 177 | 178 | $price += 0.50; 179 | } else { 180 | break 1; 181 | } 182 | die(); 183 | 184 | } 185 | 186 | } else { 187 | print "Current Order Price = Current ask Price\n"; 188 | } 189 | } 190 | 191 | ?> -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
 2 | 
 3 |  _     _                                        _                _              _              _           _   
 4 | | |__ (_)_ __   __ _ _ __   ___ ___    ___ ___ (_)_ __   /\/\   | |__   ___  __| | __ _  ___  | |__   ___ | |_ 
 5 | | '_ \| | '_ \ / _` | '_ \ / __/ _ \  / __/ _ \| | '_ \ /    \  | '_ \ / _ \/ _` |/ _` |/ _ \ | '_ \ / _ \| __|
 6 | | |_) | | | | | (_| | | | | (_|  __/ | (_| (_) | | | | / /\/\ \ | | | |  __/ (_| | (_| |  __/ | |_) | (_) | |_ 
 7 | |_.__/|_|_| |_|\__,_|_| |_|\___\___|  \___\___/|_|_| |_\/    \/ |_| |_|\___|\__,_|\__, |\___| |_.__/ \___/ \__|
 8 |                                                                                   |___/                        
 9 | 
10 | 
11 | 12 | Developed by [![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/Convexical.svg?style=social&label=Convexical)](https://twitter.com/Convexical) - https://twitter.com/Convexical 13 | 14 | `Binance Futures Coin-M Hedger` 15 | 16 | ## Other Languages / Ported code 17 | This code has been currently ported to 1 different language -
18 | https://github.com/Xpipe/Binance_Coin-M_Futures_Hedger - python
19 | If you port this code to another language (NodeJS/Javascript anyone?), please contact me on Twitter. 20 | 21 | ## What is this project? 22 | This project contains code that automates / places various trades (long [buy] & short [sell]) to create a hedge (synthetic USD) of your COIN-M Futures Wallet on Binance. For example, if you deposit 1 BTC on Binance under your spot wallet and transfer it into your Binance COIN-M futures account, it will automatically place trades until you are hedged (aka it will create short limit orders). You can also set a min/max amount per order that the bot will randomize with to get your current open position to be hedged to your Coin-M Futures wallet balance. If you choose to run the code with a cronjob or scheduler, it will continuously monitor your wallet balance and appropriately buy or sell to get the current open position to match your wallet balance. 23 | 24 | ## Why would I use this? 25 | Creating a hedge for your BTC can be important in certain situations. If you want more info on why you'd do this, please look at the resources section below. This code allows you to hedge off your BTC using (1) limit orders which save you money [because you're not submiting market orders which have higher fees than limit orders], (2) manage your hedge by consistently checking your Coin-M wallet balance and buying or selling more of the derivative to match your futures wallet. The reason I created this script is that in certain situations, you want to hedge your position and protect the FIAT value. If your wallet balance is a lower amount, it's likely easy to create a hedged position, but when you have larger BTC amounts the ability to hedge your position can be difficult because creating large market orders to hedge your wallet balance would (1) result in higher fees (which is money you're just giving to the exchange willingly) and (2) bad pricing of the hedge since it's possible you'd affect the market. 26 | 27 | ## How do I use this and run it? 28 | This code is written in PHP. I am a versatile developer / engineer but I still love PHP. While I could code it in Python, C, C++, etc. - PHP is something that I can move faster in and for me it just works. If you're a coder and want to port it, I'd love to work with you. 29 | 30 | In order to get this code to work correctly, you have to do the following: 31 | 32 | * Setup a Linux VPS or have a Linux server available - the below assumes a new version of Linux 33 | * Install PHP 7 - in the terminal: `apt install php` 34 | * Install PHP Curl - in the terminal: `apt install php-curl` 35 | 36 | 1. have an understanding of how to use a linux server and have PHP 7.0+ installed with the required modules of curl. 37 | 2. create an API key on Binance with trade permissions for Futures 38 | 3. update the config.php file with your API keys and set the various variables to your own preferences - order sizes for buy / sell side orders and the BTC delta amount distance when you want the bot to stop executing 39 | 4. setup a cronjob that executes the main.php at your specified interval or run `php main.php` to test and review the output 40 | 5. enjoy the magic / simplicitiy of the program running to consistently keep your hedge optimized correctly 41 | 42 | ## Example Cronjob 43 | This executes every minute. Please change to your desired time interval. 44 | 45 | `* * * * * /usr/bin/php //main.php > /dev/null 2>&1` 46 | 47 | ## Can you explain in detail what this code does 48 | At a high level - this code creates synthetic USD or hedges your BTC Coin-M futures wallet. When the code is executed (main.php) it connects to your Binance wallet and gets the Coin-M wallet balance and open positions. Once it has that information, it checks what the delta / difference is between the two. The script then does the following: 49 | 50 | 1) The script then checks if there are existing orders open - if there's an order open that is at the top of the book (either the bid or ask side depending on the delta of the current position against the wallet size), then it will stop / exit. If the current open limit order isn't at the top of the order book, it will cancel it and place a new limit order to hopefully get filled. 51 | 2) If the current open position is less than the wallet balance, it will create orders in the order book (short / sell) until the position is hedged / matches closely with the wallet balance 52 | 3) If the current open position is greater than the wallet balance, (because you moved coins out of the Coin-M futures wallet), then the bot will create orders (buy / long) in the order book to get close to the wallet balance. 53 | 54 | It's important to note that in order to create a hedge - you are creating a position that is opposite your wallet balance - so if you have 1 BTC in your Binance.com Futures Coin-M wallet, this code will create / place trades until the open position is close to -1 BTC. 55 | 56 | ## Are there any online resources where I can learn more about Synthetic USD / hedging / delta neutral trading? 57 | * https://medium.com/@zoomerjd/how-to-bitmex-synthetic-usd-cb6c89990a7a 58 | * https://blog.bitmex.com/in-depth-creating-synthetic-usd/ 59 | * https://www.reddit.com/r/BitMEX/comments/eqed2d/guide_how_to_keep_your_balance_in_synthetic_usd/ 60 | * https://www.binance.com/en/blog/421499824684900458/exploring-marketneutral-strategies-in-cryptoderivatives- 61 | 62 | ## Does this work for other coins / tokens under Binance? 63 | As of the release of this code, I have not tested it fully on other coins / tokens. However, it wouldn't be that difficult to modify this code for other coins / tokens. If you're interested in getting this code to work with other coins / tokens, please reach out to me. 64 | 65 | ## How do I get support for this program? 66 | If you're looking for support on how to set this up or you just want to reach out, please contact me on Twitter - https://twitter.com/Convexical. 67 | 68 | ## Important notes 69 | This code utilizes the JaggedSoft PHP Binance API library (https://github.com/jaggedsoft/php-binance-api). I've added various pieces of code to bring that library up to date to interact with the Binance Coin-M futures. 70 | 71 | ## Disclaimer 72 | Please use this code at your own risk. If you modify certain variables to be beyond certain ranges, you may end up with bad results. Always test first and optimize after. 73 | 74 |
75 |                                              .__                 .__   
76 |   ____   ____    ____ ___  __  ____  ___  ___|__|  ____  _____   |  |  
77 | _/ ___\ /  _ \  /    \\  \/ /_/ __ \ \  \/  /|  |_/ ___\ \__  \  |  |  
78 | \  \___(  <_> )|   |  \\   / \  ___/  >    < |  |\  \___  / __ \_|  |__
79 |  \___  >\____/ |___|  / \_/   \___  >/__/\_ \|__| \___  >(____  /|____/
80 |      \/             \/            \/       \/         \/      \/       
81 | 		 
82 | 
83 | -------------------------------------------------------------------------------- /php-binance-api.php: -------------------------------------------------------------------------------- 1 | 0, 48 | ]; // /< Additional connection options 49 | protected $proxyConf = null; // /< Used for story the proxy configuration 50 | protected $caOverride = false; // /< set this if you donnot wish to use CA bundle auto download feature 51 | protected $transfered = 0; // /< This stores the amount of bytes transfered 52 | protected $requestCount = 0; // /< This stores the amount of API requests 53 | protected $httpDebug = false; // /< If you enable this, curl will output debugging information 54 | protected $subscriptions = []; // /< View all websocket subscriptions 55 | protected $btc_value = 0.00; // /< value of available assets 56 | protected $btc_total = 0.00; 57 | 58 | // /< value of available onOrder assets 59 | 60 | protected $exchangeInfo = null; 61 | protected $lastRequest = []; 62 | 63 | protected $xMbxUsedWeight = 0; 64 | protected $xMbxUsedWeight1m = 0; 65 | 66 | /** 67 | * Constructor for the class, 68 | * send as many argument as you want. 69 | * 70 | * No arguments - use file setup 71 | * 1 argument - file to load config from 72 | * 2 arguments - api key and api secret 73 | * 3 arguments - api key, api secret and use testnet flag 74 | * 75 | * @return null 76 | */ 77 | public function __construct() 78 | { 79 | $param = func_get_args(); 80 | switch (count($param)) { 81 | case 0: 82 | $this->setupApiConfigFromFile(); 83 | $this->setupProxyConfigFromFile(); 84 | $this->setupCurlOptsFromFile(); 85 | break; 86 | case 1: 87 | $this->setupApiConfigFromFile($param[0]); 88 | $this->setupProxyConfigFromFile($param[0]); 89 | $this->setupCurlOptsFromFile($param[0]); 90 | break; 91 | case 2: 92 | $this->api_key = $param[0]; 93 | $this->api_secret = $param[1]; 94 | break; 95 | case 3: 96 | $this->api_key = $param[0]; 97 | $this->api_secret = $param[1]; 98 | $this->useTestnet = (bool)$param[2]; 99 | break; 100 | default: 101 | echo 'Please see valid constructors here: https://github.com/jaggedsoft/php-binance-api/blob/master/examples/constructor.php'; 102 | } 103 | } 104 | 105 | /** 106 | * magic get for protected and protected members 107 | * 108 | * @param $file string the name of the property to return 109 | * @return null 110 | */ 111 | public function __get(string $member) 112 | { 113 | if (property_exists($this, $member)) { 114 | return $this->$member; 115 | } 116 | return null; 117 | } 118 | 119 | /** 120 | * magic set for protected and protected members 121 | * 122 | * @param $member string the name of the member property 123 | * @param $value the value of the member property 124 | */ 125 | public function __set(string $member, $value) 126 | { 127 | $this->$member = $value; 128 | } 129 | 130 | /** 131 | * If no paramaters are supplied in the constructor, this function will attempt 132 | * to load the api_key and api_secret from the users home directory in the file 133 | * ~/jaggedsoft/php-binance-api.json 134 | * 135 | * @param $file string file location 136 | * @return null 137 | */ 138 | protected function setupApiConfigFromFile(string $file = null) 139 | { 140 | $file = is_null($file) ? getenv("HOME") . "/.config/jaggedsoft/php-binance-api.json" : $file; 141 | 142 | if (empty($this->api_key) === false || empty($this->api_secret) === false) { 143 | return; 144 | } 145 | if (file_exists($file) === false) { 146 | echo "Unable to load config from: " . $file . PHP_EOL; 147 | echo "Detected no API KEY or SECRET, all signed requests will fail" . PHP_EOL; 148 | return; 149 | } 150 | $contents = json_decode(file_get_contents($file), true); 151 | $this->api_key = isset($contents['api-key']) ? $contents['api-key'] : ""; 152 | $this->api_secret = isset($contents['api-secret']) ? $contents['api-secret'] : ""; 153 | $this->useTestnet = isset($contents['use-testnet']) ? (bool)$contents['use-testnet'] : false; 154 | } 155 | 156 | /** 157 | * If no paramaters are supplied in the constructor, this function will attempt 158 | * to load the acurlopts from the users home directory in the file 159 | * ~/jaggedsoft/php-binance-api.json 160 | * 161 | * @param $file string file location 162 | * @return null 163 | */ 164 | protected function setupCurlOptsFromFile(string $file = null) 165 | { 166 | $file = is_null($file) ? getenv("HOME") . "/.config/jaggedsoft/php-binance-api.json" : $file; 167 | 168 | if (count($this->curlOpts) > 0) { 169 | return; 170 | } 171 | if (file_exists($file) === false) { 172 | echo "Unable to load config from: " . $file . PHP_EOL; 173 | echo "No curl options will be set" . PHP_EOL; 174 | return; 175 | } 176 | $contents = json_decode(file_get_contents($file), true); 177 | $this->curlOpts = isset($contents['curlOpts']) && is_array($contents['curlOpts']) ? $contents['curlOpts'] : []; 178 | } 179 | 180 | /** 181 | * If no paramaters are supplied in the constructor for the proxy confguration, 182 | * this function will attempt to load the proxy info from the users home directory 183 | * ~/jaggedsoft/php-binance-api.json 184 | * 185 | * @return null 186 | */ 187 | protected function setupProxyConfigFromFile(string $file = null) 188 | { 189 | $file = is_null($file) ? getenv("HOME") . "/.config/jaggedsoft/php-binance-api.json" : $file; 190 | 191 | if (is_null($this->proxyConf) === false) { 192 | return; 193 | } 194 | if (file_exists($file) === false) { 195 | echo "Unable to load config from: " . $file . PHP_EOL; 196 | echo "No proxies will be used " . PHP_EOL; 197 | return; 198 | } 199 | $contents = json_decode(file_get_contents($file), true); 200 | if (isset($contents['proto']) === false) { 201 | return; 202 | } 203 | if (isset($contents['address']) === false) { 204 | return; 205 | } 206 | if (isset($contents['port']) === false) { 207 | return; 208 | } 209 | $this->proxyConf['proto'] = $contents['proto']; 210 | $this->proxyConf['address'] = $contents['address']; 211 | $this->proxyConf['port'] = $contents['port']; 212 | if (isset($contents['user'])) { 213 | $this->proxyConf['user'] = isset($contents['user']) ? $contents['user'] : ""; 214 | } 215 | if (isset($contents['pass'])) { 216 | $this->proxyConf['pass'] = isset($contents['pass']) ? $contents['pass'] : ""; 217 | } 218 | } 219 | 220 | /* THIS IS CODE I'VE ADDED TO USE THE VARIOUS BINANCE APIS THAT THE ORIGINAL LIBRARY / CODE DOESN'T COVER */ 221 | 222 | protected $dapi = 'https://dapi.binance.com/dapi/'; // /< REST endpoint for the futures - coin M system 223 | 224 | 225 | private function httpRequest_delivery(string $url, string $method = "GET", array $params = [], bool $signed = false) 226 | { 227 | if (function_exists('curl_init') === false) { 228 | throw new \Exception("Sorry cURL is not installed!"); 229 | } 230 | 231 | if ($this->caOverride === false) { 232 | if (file_exists(getcwd() . '/ca.pem') === false) { 233 | $this->downloadCurlCaBundle(); 234 | } 235 | } 236 | 237 | $curl = curl_init(); 238 | curl_setopt($curl, CURLOPT_VERBOSE, $this->httpDebug); 239 | $query = http_build_query($params, '', '&'); 240 | 241 | // signed with params 242 | if ($signed === true) { 243 | if (empty($this->api_key)) { 244 | throw new \Exception("signedRequest error: API Key not set!"); 245 | } 246 | 247 | if (empty($this->api_secret)) { 248 | throw new \Exception("signedRequest error: API Secret not set!"); 249 | } 250 | 251 | $base = $this->dapi; 252 | $ts = (microtime(true) * 1000) + $this->info['timeOffset']; 253 | $params['timestamp'] = number_format($ts, 0, '.', ''); 254 | if (isset($params['dapi'])) { 255 | unset($params['dapi']); 256 | $base = $this->dapi; 257 | } 258 | 259 | $query = http_build_query($params, '', '&'); 260 | $signature = hash_hmac('sha256', $query, $this->api_secret); 261 | if ($method === "POST") { 262 | $endpoint = $base . $url; 263 | $params['signature'] = $signature; // signature needs to be inside BODY 264 | $query = http_build_query($params, '', '&'); // rebuilding query 265 | } else { 266 | $endpoint = $base . $url . '?' . $query . '&signature=' . $signature; 267 | } 268 | 269 | curl_setopt($curl, CURLOPT_URL, $endpoint); 270 | curl_setopt($curl, CURLOPT_HTTPHEADER, array( 271 | 'X-MBX-APIKEY: ' . $this->api_key, 272 | )); 273 | } 274 | // params so buildquery string and append to url 275 | else if (count($params) > 0) { 276 | curl_setopt($curl, CURLOPT_URL, $this->dapi . $url . '?' . $query); 277 | } 278 | // no params so just the base url 279 | else { 280 | curl_setopt($curl, CURLOPT_URL, $this->dapi . $url); 281 | curl_setopt($curl, CURLOPT_HTTPHEADER, array( 282 | 'X-MBX-APIKEY: ' . $this->api_key, 283 | )); 284 | } 285 | curl_setopt($curl, CURLOPT_USERAGENT, "User-Agent: Mozilla/4.0 (compatible; PHP Binance API)"); 286 | // Post and postfields 287 | if ($method === "POST") { 288 | curl_setopt($curl, CURLOPT_POST, true); 289 | curl_setopt($curl, CURLOPT_POSTFIELDS, $query); 290 | } 291 | // Delete Method 292 | if ($method === "DELETE") { 293 | curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); 294 | } 295 | 296 | // PUT Method 297 | if ($method === "PUT") { 298 | curl_setopt($curl, CURLOPT_PUT, true); 299 | } 300 | 301 | // proxy settings 302 | if (is_array($this->proxyConf)) { 303 | curl_setopt($curl, CURLOPT_PROXY, $this->getProxyUriString()); 304 | if (isset($this->proxyConf['user']) && isset($this->proxyConf['pass'])) { 305 | curl_setopt($curl, CURLOPT_PROXYUSERPWD, $this->proxyConf['user'] . ':' . $this->proxyConf['pass']); 306 | } 307 | } 308 | curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); 309 | curl_setopt($curl, CURLOPT_HEADER, true); 310 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); 311 | curl_setopt($curl, CURLOPT_TIMEOUT, 60); 312 | 313 | // set user defined curl opts last for overriding 314 | foreach ($this->curlOpts as $key => $value) { 315 | curl_setopt($curl, constant($key), $value); 316 | } 317 | 318 | if ($this->caOverride === false) { 319 | if (file_exists(getcwd() . '/ca.pem') === false) { 320 | $this->downloadCurlCaBundle(); 321 | } 322 | } 323 | 324 | $output = curl_exec($curl); 325 | // Check if any error occurred 326 | if (curl_errno($curl) > 0) { 327 | // should always output error, not only on httpdebug 328 | // not outputing errors, hides it from users and ends up with tickets on github 329 | echo 'Curl error: ' . curl_error($curl) . "\n"; 330 | return []; 331 | } 332 | 333 | $header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE); 334 | $header = substr($output, 0, $header_size); 335 | $output = substr($output, $header_size); 336 | 337 | curl_close($curl); 338 | 339 | $json = json_decode($output, true); 340 | 341 | /* 342 | $this->query_logs[] = [ 343 | 'url' => $url, 344 | 'method' => $method, 345 | 'params' => $params, 346 | 'header' => $header, 347 | 'json' => $json 348 | ]; 349 | */ 350 | 351 | if(isset($json['msg'])){ 352 | // should always output error, not only on httpdebug 353 | // not outputing errors, hides it from users and ends up with tickets on github 354 | echo "signedRequest error: {$output}" . PHP_EOL; 355 | } 356 | $this->transfered += strlen($output); 357 | $this->requestCount++; 358 | return $json; 359 | } 360 | 361 | 362 | // https://binance-docs.github.io/apidocs/delivery/en/#position-information-user_data 363 | public function delivery_position_information() { 364 | return $this->httpRequest_delivery("v1/positionRisk", "GET", [], true); 365 | } 366 | 367 | // https://binance-docs.github.io/apidocs/delivery/en/#account-information-user_data 368 | public function delivery_account_information() { 369 | return $this->httpRequest_delivery("v1/account", "GET", [], true); 370 | } 371 | 372 | public function delivery_symbol_order_book_ticker($symbol) { 373 | $additionalData = []; 374 | if (is_null($symbol) === false) { 375 | $additionalData = [ 376 | 'symbol' => $symbol, 377 | ]; 378 | } 379 | return $this->httpRequest_delivery("v1/ticker/bookTicker", "GET", $additionalData); 380 | } 381 | 382 | // https://binance-docs.github.io/apidocs/futures/en/#current-all-open-orders-user_data 383 | public function delivery_current_all_open_orders($symbol) { 384 | $params = [ 385 | "symbol" => $symbol 386 | ]; 387 | return $this->httpRequest_delivery("v1/openOrders", "GET", $params, true); 388 | } 389 | 390 | // https://binance-docs.github.io/apidocs/delivery/en/#new-order-trade 391 | public function delivery_create_order(string $side, string $symbol, $quantity, $price, string $type = "LIMIT", array $flags = [], bool $test = false) { 392 | $opt = [ 393 | "symbol" => $symbol, 394 | "side" => $side, 395 | "type" => $type, 396 | "quantity" => $quantity, 397 | "recvWindow" => 60000, 398 | ]; 399 | 400 | // someone has preformated there 8 decimal point double already 401 | // dont do anything, leave them do whatever they want 402 | if (gettype($price) !== "string") { 403 | // for every other type, lets format it appropriately 404 | $price = number_format($price, 8, '.', ''); 405 | } 406 | 407 | if (is_numeric($quantity) === false) { 408 | // WPCS: XSS OK. 409 | echo "warning: quantity expected numeric got " . gettype($quantity) . PHP_EOL; 410 | } 411 | 412 | if (is_string($price) === false) { 413 | // WPCS: XSS OK. 414 | echo "warning: price expected string got " . gettype($price) . PHP_EOL; 415 | } 416 | 417 | if ($type === "STOP_LOSS_LIMIT" || $type === "TAKE_PROFIT_LIMIT") { 418 | $opt["price"] = $price; 419 | $opt["timeInForce"] = "GTC"; 420 | } 421 | 422 | if ($type === "LIMIT") { 423 | $opt["price"] = $price; 424 | $opt["timeInForce"] = "GTX"; 425 | } 426 | 427 | if ($type === "STOP") { 428 | $opt["price"] = $price; 429 | } 430 | 431 | if (isset($flags['stopPrice'])) { 432 | $opt['stopPrice'] = $flags['stopPrice']; 433 | } 434 | 435 | if (isset($flags['icebergQty'])) { 436 | $opt['icebergQty'] = $flags['icebergQty']; 437 | } 438 | 439 | if (isset($flags['newOrderRespType'])) { 440 | $opt['newOrderRespType'] = $flags['newOrderRespType']; 441 | } 442 | 443 | if (isset($flags['newClientOrderId'])) { 444 | $opt['newClientOrderId'] = $flags['newClientOrderId']; 445 | } 446 | 447 | if (isset($flags['reduceOnly'])) { 448 | $opt['reduceOnly'] = $flags['reduceOnly']; 449 | } 450 | 451 | if (isset($flags['activationPrice'])) { 452 | $opt['activationPrice'] = $flags['activationPrice']; 453 | } 454 | 455 | if (isset($flags['callbackRate'])) { 456 | $opt['callbackRate'] = $flags['callbackRate']; 457 | } 458 | 459 | if (isset($flags['workingType'])) { 460 | $opt['workingType'] = $flags['workingType']; 461 | } 462 | 463 | /* 464 | print_r($qstring); 465 | die(); 466 | */ 467 | 468 | $qstring = ($test === false) ? "v1/order" : "v1/order/test"; 469 | return $this->httpRequest_delivery($qstring, "POST", $opt, true); 470 | } 471 | 472 | 473 | // https://binance-docs.github.io/apidocs/futures/en/#cancel-all-open-orders-trade 474 | public function delivery_cancel_all_open_orders(string $symbol) { 475 | $params = [ 476 | "symbol" => $symbol 477 | ]; 478 | return $this->httpRequest_delivery("v1/allOpenOrders", "DELETE", $params, true); 479 | } 480 | 481 | 482 | 483 | // end of my additions 484 | 485 | /** 486 | * buy attempts to create a currency order 487 | * each currency supports a number of order types, such as 488 | * -LIMIT 489 | * -MARKET 490 | * -STOP_LOSS 491 | * -STOP_LOSS_LIMIT 492 | * -TAKE_PROFIT 493 | * -TAKE_PROFIT_LIMIT 494 | * -LIMIT_MAKER 495 | * 496 | * You should check the @see exchangeInfo for each currency to determine 497 | * what types of orders can be placed against specific pairs 498 | * 499 | * $quantity = 1; 500 | * $price = 0.0005; 501 | * $order = $api->buy("BNBBTC", $quantity, $price); 502 | * 503 | * @param $symbol string the currency symbol 504 | * @param $quantity string the quantity required 505 | * @param $price string price per unit you want to spend 506 | * @param $type string type of order 507 | * @param $flags array addtional options for order type 508 | * @return array with error message or the order details 509 | */ 510 | public function buy(string $symbol, $quantity, $price, string $type = "LIMIT", array $flags = []) 511 | { 512 | return $this->order("BUY", $symbol, $quantity, $price, $type, $flags); 513 | } 514 | 515 | /** 516 | * buyTest attempts to create a TEST currency order 517 | * 518 | * @see buy() 519 | * 520 | * @param $symbol string the currency symbol 521 | * @param $quantity string the quantity required 522 | * @param $price string price per unit you want to spend 523 | * @param $type string config 524 | * @param $flags array config 525 | * @return array with error message or empty or the order details 526 | */ 527 | public function buyTest(string $symbol, $quantity, $price, string $type = "LIMIT", array $flags = []) 528 | { 529 | return $this->order("BUY", $symbol, $quantity, $price, $type, $flags, true); 530 | } 531 | 532 | /** 533 | * sell attempts to create a currency order 534 | * each currency supports a number of order types, such as 535 | * -LIMIT 536 | * -MARKET 537 | * -STOP_LOSS 538 | * -STOP_LOSS_LIMIT 539 | * -TAKE_PROFIT 540 | * -TAKE_PROFIT_LIMIT 541 | * -LIMIT_MAKER 542 | * 543 | * You should check the @see exchangeInfo for each currency to determine 544 | * what types of orders can be placed against specific pairs 545 | * 546 | * $quantity = 1; 547 | * $price = 0.0005; 548 | * $order = $api->sell("BNBBTC", $quantity, $price); 549 | * 550 | * @param $symbol string the currency symbol 551 | * @param $quantity string the quantity required 552 | * @param $price string price per unit you want to spend 553 | * @param $type string type of order 554 | * @param $flags array addtional options for order type 555 | * @return array with error message or the order details 556 | */ 557 | public function sell(string $symbol, $quantity, $price, string $type = "LIMIT", array $flags = []) 558 | { 559 | return $this->order("SELL", $symbol, $quantity, $price, $type, $flags); 560 | } 561 | 562 | /** 563 | * sellTest attempts to create a TEST currency order 564 | * 565 | * @see sell() 566 | * 567 | * @param $symbol string the currency symbol 568 | * @param $quantity string the quantity required 569 | * @param $price string price per unit you want to spend 570 | * @param $type array config 571 | * @param $flags array config 572 | * @return array with error message or empty or the order details 573 | */ 574 | public function sellTest(string $symbol, $quantity, $price, string $type = "LIMIT", array $flags = []) 575 | { 576 | return $this->order("SELL", $symbol, $quantity, $price, $type, $flags, true); 577 | } 578 | 579 | /** 580 | * marketQuoteBuy attempts to create a currency order at given market price 581 | * 582 | * $quantity = 1; 583 | * $order = $api->marketQuoteBuy("BNBBTC", $quantity); 584 | * 585 | * @param $symbol string the currency symbol 586 | * @param $quantity string the quantity of the quote to use 587 | * @param $flags array additional options for order type 588 | * @return array with error message or the order details 589 | */ 590 | public function marketQuoteBuy(string $symbol, $quantity, array $flags = []) 591 | { 592 | $flags['isQuoteOrder'] = true; 593 | 594 | return $this->order("BUY", $symbol, $quantity, 0, "MARKET", $flags); 595 | } 596 | 597 | /** 598 | * marketQuoteBuyTest attempts to create a TEST currency order at given market price 599 | * 600 | * @see marketBuy() 601 | * 602 | * @param $symbol string the currency symbol 603 | * @param $quantity string the quantity of the quote to use 604 | * @param $flags array additional options for order type 605 | * @return array with error message or the order details 606 | */ 607 | public function marketQuoteBuyTest(string $symbol, $quantity, array $flags = []) 608 | { 609 | $flags['isQuoteOrder'] = true; 610 | 611 | return $this->order("BUY", $symbol, $quantity, 0, "MARKET", $flags, true); 612 | } 613 | 614 | /** 615 | * marketBuy attempts to create a currency order at given market price 616 | * 617 | * $quantity = 1; 618 | * $order = $api->marketBuy("BNBBTC", $quantity); 619 | * 620 | * @param $symbol string the currency symbol 621 | * @param $quantity string the quantity required 622 | * @param $flags array addtional options for order type 623 | * @return array with error message or the order details 624 | */ 625 | public function marketBuy(string $symbol, $quantity, array $flags = []) 626 | { 627 | return $this->order("BUY", $symbol, $quantity, 0, "MARKET", $flags); 628 | } 629 | 630 | /** 631 | * marketBuyTest attempts to create a TEST currency order at given market price 632 | * 633 | * @see marketBuy() 634 | * 635 | * @param $symbol string the currency symbol 636 | * @param $quantity string the quantity required 637 | * @param $flags array addtional options for order type 638 | * @return array with error message or the order details 639 | */ 640 | public function marketBuyTest(string $symbol, $quantity, array $flags = []) 641 | { 642 | return $this->order("BUY", $symbol, $quantity, 0, "MARKET", $flags, true); 643 | } 644 | 645 | 646 | /** 647 | * numberOfDecimals() returns the signifcant digits level based on the minimum order amount. 648 | * 649 | * $dec = numberOfDecimals(0.00001); // Returns 5 650 | * 651 | * @param $val float the minimum order amount for the pair 652 | * @return integer (signifcant digits) based on the minimum order amount 653 | */ 654 | public function numberOfDecimals($val = 0.00000001) 655 | { 656 | $val = sprintf("%.14f", $val); 657 | $parts = explode('.', $val); 658 | $parts[1] = rtrim($parts[1], "0"); 659 | return strlen($parts[1]); 660 | } 661 | 662 | /** 663 | * marketQuoteSell attempts to create a currency order at given market price 664 | * 665 | * $quantity = 1; 666 | * $order = $api->marketQuoteSell("BNBBTC", $quantity); 667 | * 668 | * @param $symbol string the currency symbol 669 | * @param $quantity string the quantity of the quote you want to obtain 670 | * @param $flags array additional options for order type 671 | * @return array with error message or the order details 672 | */ 673 | public function marketQuoteSell(string $symbol, $quantity, array $flags = []) 674 | { 675 | $flags['isQuoteOrder'] = true; 676 | $c = $this->numberOfDecimals($this->exchangeInfo()['symbols'][$symbol]['filters'][2]['minQty']); 677 | $quantity = $this->floorDecimal($quantity, $c); 678 | 679 | return $this->order("SELL", $symbol, $quantity, 0, "MARKET", $flags); 680 | } 681 | 682 | /** 683 | * marketQuoteSellTest attempts to create a TEST currency order at given market price 684 | * 685 | * @see marketSellTest() 686 | * 687 | * @param $symbol string the currency symbol 688 | * @param $quantity string the quantity of the quote you want to obtain 689 | * @param $flags array additional options for order type 690 | * @return array with error message or the order details 691 | */ 692 | public function marketQuoteSellTest(string $symbol, $quantity, array $flags = []) 693 | { 694 | $flags['isQuoteOrder'] = true; 695 | 696 | return $this->order("SELL", $symbol, $quantity, 0, "MARKET", $flags, true); 697 | } 698 | 699 | /** 700 | * marketSell attempts to create a currency order at given market price 701 | * 702 | * $quantity = 1; 703 | * $order = $api->marketSell("BNBBTC", $quantity); 704 | * 705 | * @param $symbol string the currency symbol 706 | * @param $quantity string the quantity required 707 | * @param $flags array addtional options for order type 708 | * @return array with error message or the order details 709 | */ 710 | public function marketSell(string $symbol, $quantity, array $flags = []) 711 | { 712 | $c = $this->numberOfDecimals($this->exchangeInfo()['symbols'][$symbol]['filters'][2]['minQty']); 713 | $quantity = $this->floorDecimal($quantity, $c); 714 | 715 | return $this->order("SELL", $symbol, $quantity, 0, "MARKET", $flags); 716 | } 717 | 718 | /** 719 | * marketSellTest attempts to create a TEST currency order at given market price 720 | * 721 | * @see marketSellTest() 722 | * 723 | * @param $symbol string the currency symbol 724 | * @param $quantity string the quantity required 725 | * @param $flags array addtional options for order type 726 | * @return array with error message or the order details 727 | */ 728 | public function marketSellTest(string $symbol, $quantity, array $flags = []) 729 | { 730 | return $this->order("SELL", $symbol, $quantity, 0, "MARKET", $flags, true); 731 | } 732 | 733 | /** 734 | * cancel attempts to cancel a currency order 735 | * 736 | * $orderid = "123456789"; 737 | * $order = $api->cancel("BNBBTC", $orderid); 738 | * 739 | * @param $symbol string the currency symbol 740 | * @param $orderid string the orderid to cancel 741 | * @param $flags array of optional options like ["side"=>"sell"] 742 | * @return array with error message or the order details 743 | * @throws \Exception 744 | */ 745 | public function cancel(string $symbol, $orderid, $flags = []) 746 | { 747 | $params = [ 748 | "symbol" => $symbol, 749 | "orderId" => $orderid, 750 | ]; 751 | return $this->httpRequest("v3/order", "DELETE", array_merge($params, $flags), true); 752 | } 753 | 754 | /** 755 | * orderStatus attempts to get orders status 756 | * 757 | * $orderid = "123456789"; 758 | * $order = $api->orderStatus("BNBBTC", $orderid); 759 | * 760 | * @param $symbol string the currency symbol 761 | * @param $orderid string the orderid to cancel 762 | * @return array with error message or the order details 763 | * @throws \Exception 764 | */ 765 | public function orderStatus(string $symbol, $orderid) 766 | { 767 | return $this->httpRequest("v3/order", "GET", [ 768 | "symbol" => $symbol, 769 | "orderId" => $orderid, 770 | ], true); 771 | } 772 | 773 | /** 774 | * openOrders attempts to get open orders for all currencies or a specific currency 775 | * 776 | * $allOpenOrders = $api->openOrders(); 777 | * $allBNBOrders = $api->openOrders( "BNBBTC" ); 778 | * 779 | * @param $symbol string the currency symbol 780 | * @return array with error message or the order details 781 | * @throws \Exception 782 | */ 783 | public function openOrders(string $symbol = null) 784 | { 785 | $params = []; 786 | if (is_null($symbol) != true) { 787 | $params = [ 788 | "symbol" => $symbol, 789 | ]; 790 | } 791 | return $this->httpRequest("v3/openOrders", "GET", $params, true); 792 | } 793 | 794 | /** 795 | * Cancel all open orders method 796 | * $api->cancelOpenOrders( "BNBBTC" ); 797 | * @param $symbol string the currency symbol 798 | * @return array with error message or the order details 799 | * @throws \Exception 800 | */ 801 | public function cancelOpenOrders(string $symbol = null) 802 | { 803 | $params = []; 804 | if (is_null($symbol) != true) { 805 | $params = [ 806 | "symbol" => $symbol, 807 | ]; 808 | } 809 | return $this->httpRequest("v3/openOrders", "DELETE", $params, true); 810 | } 811 | 812 | /** 813 | * orders attempts to get the orders for all or a specific currency 814 | * 815 | * $allBNBOrders = $api->orders( "BNBBTC" ); 816 | * 817 | * @param $symbol string the currency symbol 818 | * @param $limit int the amount of orders returned 819 | * @param $fromOrderId string return the orders from this order onwards 820 | * @param $params array optional startTime, endTime parameters 821 | * @return array with error message or array of orderDetails array 822 | * @throws \Exception 823 | */ 824 | public function orders(string $symbol, int $limit = 500, int $fromOrderId = 0, array $params = []) 825 | { 826 | $params["symbol"] = $symbol; 827 | $params["limit"] = $limit; 828 | if ($fromOrderId) { 829 | $params["orderId"] = $fromOrderId; 830 | } 831 | return $this->httpRequest("v3/allOrders", "GET", $params, true); 832 | } 833 | 834 | /** 835 | * history Get the complete account trade history for all or a specific currency 836 | * 837 | * $BNBHistory = $api->history("BNBBTC"); 838 | * $limitBNBHistory = $api->history("BNBBTC",5); 839 | * $limitBNBHistoryFromId = $api->history("BNBBTC",5,3); 840 | * 841 | * @param $symbol string the currency symbol 842 | * @param $limit int the amount of orders returned 843 | * @param $fromTradeId int (optional) return the orders from this order onwards. negative for all 844 | * @param $startTime int (optional) return the orders from this time onwards. null to ignore 845 | * @param $endTime int (optional) return the orders from this time backwards. null to ignore 846 | * @return array with error message or array of orderDetails array 847 | * @throws \Exception 848 | */ 849 | public function history(string $symbol, int $limit = 500, int $fromTradeId = -1, int $startTime = null, int $endTime = null) 850 | { 851 | $parameters = [ 852 | "symbol" => $symbol, 853 | "limit" => $limit, 854 | ]; 855 | if ($fromTradeId > 0) { 856 | $parameters["fromId"] = $fromTradeId; 857 | } 858 | if (isset($startTime)) { 859 | $parameters["startTime"] = $startTime; 860 | } 861 | if (isset($endTime)) { 862 | $parameters["endTime"] = $endTime; 863 | } 864 | 865 | return $this->httpRequest("v3/myTrades", "GET", $parameters, true); 866 | } 867 | 868 | /** 869 | * useServerTime adds the 'useServerTime'=>true to the API request to avoid time errors 870 | * 871 | * $api->useServerTime(); 872 | * 873 | * @return null 874 | * @throws \Exception 875 | */ 876 | public function useServerTime() 877 | { 878 | $request = $this->httpRequest("v3/time"); 879 | if (isset($request['serverTime'])) { 880 | $this->info['timeOffset'] = $request['serverTime'] - (microtime(true) * 1000); 881 | } 882 | } 883 | 884 | /** 885 | * time Gets the server time 886 | * 887 | * $time = $api->time(); 888 | * 889 | * @return array with error message or array with server time key 890 | * @throws \Exception 891 | */ 892 | public function time() 893 | { 894 | return $this->httpRequest("v3/time"); 895 | } 896 | 897 | /** 898 | * exchangeInfo Gets the complete exchange info, including limits, currency options etc. 899 | * 900 | * $info = $api->exchangeInfo(); 901 | * 902 | * @return array with error message or exchange info array 903 | * @throws \Exception 904 | */ 905 | public function exchangeInfo() 906 | { 907 | if (!$this->exchangeInfo) { 908 | $arr = $this->httpRequest("v3/exchangeInfo"); 909 | 910 | $this->exchangeInfo = $arr; 911 | $this->exchangeInfo['symbols'] = null; 912 | 913 | foreach ($arr['symbols'] as $key => $value) { 914 | $this->exchangeInfo['symbols'][$value['symbol']] = $value; 915 | } 916 | } 917 | 918 | return $this->exchangeInfo; 919 | } 920 | 921 | /** 922 | * assetDetail - Fetch details of assets supported on Binance 923 | * 924 | * @link https://binance-docs.github.io/apidocs/spot/en/#asset-detail-user_data 925 | * 926 | * @property int $weight 1 927 | * 928 | * @return array containing the response 929 | */ 930 | public function assetDetail() 931 | { 932 | $params["sapi"] = true; 933 | $arr = $this->httpRequest("v1/asset/assetDetail", 'GET', $params, true); 934 | // wrap into another array for backwards compatibility with the old wapi one 935 | if (!empty($arr['BTC']['withdrawFee'])) { 936 | return array( 937 | 'success' => 1, 938 | 'assetDetail' => $arr, 939 | ); 940 | } else { 941 | return array( 942 | 'success' => 0, 943 | 'assetDetail' => array(), 944 | ); 945 | 946 | } 947 | } 948 | 949 | /** 950 | * userAssetDribbletLog - Log of the conversion of the dust assets to BNB 951 | * @deprecated 952 | */ 953 | public function userAssetDribbletLog() 954 | { 955 | $params["wapi"] = true; 956 | trigger_error('Deprecated - function will disappear on 2021-08-01 from Binance. Please switch to $api->dustLog().', E_USER_DEPRECATED); 957 | return $this->httpRequest("v3/userAssetDribbletLog.html", 'GET', $params, true); 958 | } 959 | 960 | /** 961 | * dustLog - Log of the conversion of the dust assets to BNB 962 | * 963 | * @link https://binance-docs.github.io/apidocs/spot/en/#dustlog-user_data 964 | * 965 | * @property int $weight 1 966 | * 967 | * @param long $startTime (optional) Start time, e.g. 1617580799000 968 | * @param long $endTime (optional) End time, e.g. 1617580799000. Endtime is mandatory if startTime is set. 969 | * 970 | * @return array containing the response 971 | * @throws \Exception 972 | */ 973 | public function dustLog($startTime = NULL, $endTime = NULL) 974 | { 975 | $params["sapi"] = true; 976 | if (!empty($startTime) && !empty($endTime)) { 977 | $params['startTime'] = $startTime; 978 | $params['endTime'] = $endTime; 979 | } 980 | 981 | return $this->httpRequest("v1/asset/dribblet", 'GET', $params, true); 982 | } 983 | 984 | /** 985 | * @deprecated 986 | * 987 | * Fetch current(daily) trade fee of symbol, values in percentage. 988 | * for more info visit binance official api document 989 | * 990 | * $symbol = "BNBBTC"; or any other symbol or even a set of symbols in an array 991 | * @param string $symbol 992 | * @return mixed 993 | */ 994 | public function tradeFee(string $symbol) 995 | { 996 | $params = [ 997 | "symbol" => $symbol, 998 | "wapi" => true, 999 | ]; 1000 | trigger_error('Function tradeFee is deprecated and will be removed from Binance on Aug 1, 2021. Please use $api->commissionFee', E_USER_DEPRECATED); 1001 | 1002 | return $this->httpRequest("v3/tradeFee.html", 'GET', $params, true); 1003 | } 1004 | 1005 | /** 1006 | * commissionFee - Fetch commission trade fee 1007 | * 1008 | * @link https://binance-docs.github.io/apidocs/spot/en/#trade-fee-user_data 1009 | * 1010 | * @property int $weight 1 1011 | * 1012 | * @param string $symbol (optional) Should be a symbol, e.g. BNBUSDT or empty to get the full list 1013 | * 1014 | * @return array containing the response 1015 | * @throws \Exception 1016 | */ 1017 | public function commissionFee($symbol = '') 1018 | { 1019 | $params = array('sapi' => true); 1020 | if ($symbol != '' && gettype($symbol) == 'string') 1021 | $params['symbol'] = $symbol; 1022 | 1023 | return $this->httpRequest("v1/asset/tradeFee", 'GET', $params, true); 1024 | } 1025 | 1026 | /** 1027 | * withdraw - Submit a withdraw request to move an asset to another wallet 1028 | * 1029 | * @link https://binance-docs.github.io/apidocs/spot/en/#withdraw-sapi 1030 | * 1031 | * @example https://github.com/jaggedsoft/php-binance-api#withdraw Standard withdraw 1032 | * @example https://github.com/jaggedsoft/php-binance-api#withdraw-with-addresstag Withdraw with addressTag for e.g. XRP 1033 | * 1034 | * @property int $weight 1 1035 | * 1036 | * @param string $asset (mandatory) An asset, e.g. BTC 1037 | * @param string $address (mandatory) The address where to send, e.g. 1C5gqLRs96Xq4V2ZZAR1347yUCpHie7sa or 44tLjmXrQNrWJ5NBsEj2R77ZBEgDa3fEe9GLpSf2FRmhexPvfYDUAB7EXX1Hdb3aMQ9FLqdJ56yaAhiXoRsceGJCRS3Jxkn 1038 | * @param string $amount (mandatory) The amount, e.g. 0.2 1039 | * @param string $addressTag (optional) Mandatory secondary address for some assets (XRP,XMR,etc), e.g. 0e5e38a01058dbf64e53a4333a5acf98e0d5feb8e523d32e3186c664a9c762c1 1040 | * @param string $addressName (optional) Description of the address 1041 | * @param string $transactionFeeFlag (optional) When making internal transfer, true for returning the fee to the destination account; false for returning the fee back to the departure account. 1042 | * @param string $network (optional) 1043 | * @param string $orderId (optional) Client id for withdraw 1044 | * 1045 | * @return array containing the response 1046 | * @throws \Exception 1047 | */ 1048 | public function withdraw(string $asset, string $address, $amount, $addressTag = null, $addressName = "", bool $transactionFeeFlag = false, $network = null, $orderId = null) 1049 | { 1050 | $options = [ 1051 | "coin" => $asset, 1052 | "address" => $address, 1053 | "amount" => $amount, 1054 | "transactionFeeFlag" => $transactionFeeFlag, 1055 | "sapi" => true, 1056 | ]; 1057 | if (is_null($addressName) === false && empty($addressName) === false) { 1058 | $options['name'] = str_replace(' ', '%20', $addressName); 1059 | } 1060 | if (is_null($addressTag) === false && empty($addressTag) === false) { 1061 | $options['addressTag'] = $addressTag; 1062 | } 1063 | if (is_null($network) === false && empty($network) === false) { 1064 | $options['network'] = $network; 1065 | } 1066 | if (is_null($orderId) === false && empty($orderId) === false) { 1067 | $options['withdrawOrderId'] = $orderId; 1068 | } 1069 | return $this->httpRequest("v1/capital/withdraw/apply", "POST", $options, true); 1070 | } 1071 | 1072 | /** 1073 | * depositAddress - Get the deposit address for an asset 1074 | * 1075 | * @link https://binance-docs.github.io/apidocs/spot/en/#deposit-address-supporting-network-user_data 1076 | * 1077 | * @property int $weight 1 1078 | * 1079 | * @param string $asset (mandatory) An asset, e.g. BTC 1080 | * @param string $network (optional) You can get network in networkList from /sapi/v1/capital/config/getall 1081 | * 1082 | * @return array containing the response 1083 | * @throws \Exception 1084 | */ 1085 | public function depositAddress(string $asset, $network = null) 1086 | { 1087 | $params = [ 1088 | "sapi" => true, 1089 | "coin" => $asset, 1090 | ]; 1091 | if (is_null($network) === false && empty($network) === false) { 1092 | $params['network'] = $network; 1093 | } 1094 | 1095 | $return = $this->httpRequest("v1/capital/deposit/address", "GET", $params, true); 1096 | 1097 | // Adding for backwards compatibility with wapi 1098 | $return['asset'] = $return['coin']; 1099 | $return['addressTag'] = $return['tag']; 1100 | 1101 | if (!empty($return['address'])) { 1102 | $return['success'] = 1; 1103 | } else { 1104 | $return['success'] = 0; 1105 | } 1106 | 1107 | return $return; 1108 | } 1109 | 1110 | /** 1111 | * depositHistory - Get the deposit history for one or all assets 1112 | * 1113 | * @link https://binance-docs.github.io/apidocs/spot/en/#deposit-history-supporting-network-user_data 1114 | * 1115 | * @property int $weight 1 1116 | * 1117 | * @param string $asset (optional) An asset, e.g. BTC - or leave empty for all 1118 | * @param array $params (optional) An array of additional parameters that the API endpoint allows 1119 | * 1120 | * @return array containing the response 1121 | * @throws \Exception 1122 | */ 1123 | public function depositHistory(string $asset = null, array $params = []) 1124 | { 1125 | $params["sapi"] = true; 1126 | if (is_null($asset) === false) { 1127 | $params['coin'] = $asset; 1128 | } 1129 | $return = $this->httpRequest("v1/capital/deposit/hisrec", "GET", $params, true); 1130 | 1131 | // Adding for backwards compatibility with wapi 1132 | foreach ($return as $key->$item) { 1133 | $return[$key]['asset'] = $item['coin']; 1134 | } 1135 | 1136 | return $return; 1137 | 1138 | } 1139 | 1140 | /** 1141 | * withdrawHistory - Get the withdraw history for one or all assets 1142 | * 1143 | * @link https://binance-docs.github.io/apidocs/spot/en/#withdraw-history-supporting-network-user_data 1144 | * 1145 | * @property int $weight 1 1146 | * 1147 | * @param string $asset (optional) An asset, e.g. BTC - or leave empty for all 1148 | * @param array $params (optional) An array of additional parameters that the API endpoint allows: status, offset, limit, startTime, endTime 1149 | * 1150 | * @return array containing the response 1151 | * @throws \Exception 1152 | */ 1153 | public function withdrawHistory(string $asset = null, array $params = []) 1154 | { 1155 | $params["sapi"] = true; 1156 | if (is_null($asset) === false) { 1157 | $params['coin'] = $asset; 1158 | } 1159 | // Wrapping in array for backwards compatibility with wapi 1160 | $return = array( 1161 | 'withdrawList' => $this->httpRequest("v1/capital/withdraw/history", "GET", $params, true) 1162 | ); 1163 | 1164 | // Adding for backwards compatibility with wapi 1165 | $return['success'] = 1; 1166 | 1167 | return $return; 1168 | } 1169 | 1170 | /** 1171 | * withdrawFee - Get the withdrawal fee for an asset 1172 | * 1173 | * @property int $weight 1 1174 | * 1175 | * @param string $asset (mandatory) An asset, e.g. BTC 1176 | * 1177 | * @return array containing the response 1178 | * @throws \Exception 1179 | */ 1180 | public function withdrawFee(string $asset) 1181 | { 1182 | $return = $this->assetDetail(); 1183 | 1184 | if (isset($return['success'], $return['assetDetail'], $return['assetDetail'][$asset]) && $return['success']) { 1185 | return $return['assetDetail'][$asset]; 1186 | } else { 1187 | return array(); 1188 | } 1189 | } 1190 | 1191 | /** 1192 | * prices get all the current prices 1193 | * 1194 | * $ticker = $api->prices(); 1195 | * 1196 | * @return array with error message or array of all the currencies prices 1197 | * @throws \Exception 1198 | */ 1199 | public function prices() 1200 | { 1201 | return $this->priceData($this->httpRequest("v3/ticker/price")); 1202 | } 1203 | 1204 | /** 1205 | * price get the latest price of a symbol 1206 | * 1207 | * $price = $api->price( "ETHBTC" ); 1208 | * 1209 | * @return array with error message or array with symbol price 1210 | * @throws \Exception 1211 | */ 1212 | public function price(string $symbol) 1213 | { 1214 | $ticker = $this->httpRequest("v3/ticker/price", "GET", ["symbol" => $symbol]); 1215 | 1216 | return $ticker['price']; 1217 | } 1218 | 1219 | /** 1220 | * bookPrices get all bid/asks prices 1221 | * 1222 | * $ticker = $api->bookPrices(); 1223 | * 1224 | * @return array with error message or array of all the book prices 1225 | * @throws \Exception 1226 | */ 1227 | public function bookPrices() 1228 | { 1229 | return $this->bookPriceData($this->httpRequest("v3/ticker/bookTicker")); 1230 | } 1231 | 1232 | /** 1233 | * account get all information about the api account 1234 | * 1235 | * $account = $api->account(); 1236 | * 1237 | * @return array with error message or array of all the account information 1238 | * @throws \Exception 1239 | */ 1240 | public function account() 1241 | { 1242 | return $this->httpRequest("v3/account", "GET", [], true); 1243 | } 1244 | 1245 | /** 1246 | * prevDay get 24hr ticker price change statistics for symbols 1247 | * 1248 | * $prevDay = $api->prevDay("BNBBTC"); 1249 | * 1250 | * @param $symbol (optional) symbol to get the previous day change for 1251 | * @return array with error message or array of prevDay change 1252 | * @throws \Exception 1253 | */ 1254 | public function prevDay(string $symbol = null) 1255 | { 1256 | $additionalData = []; 1257 | if (is_null($symbol) === false) { 1258 | $additionalData = [ 1259 | 'symbol' => $symbol, 1260 | ]; 1261 | } 1262 | return $this->httpRequest("v1/ticker/24hr", "GET", $additionalData); 1263 | } 1264 | 1265 | /** 1266 | * aggTrades get Market History / Aggregate Trades 1267 | * 1268 | * $trades = $api->aggTrades("BNBBTC"); 1269 | * 1270 | * @param $symbol string the symbol to get the trade information for 1271 | * @return array with error message or array of market history 1272 | * @throws \Exception 1273 | */ 1274 | public function aggTrades(string $symbol) 1275 | { 1276 | return $this->tradesData($this->httpRequest("v1/aggTrades", "GET", [ 1277 | "symbol" => $symbol, 1278 | ])); 1279 | } 1280 | 1281 | /** 1282 | * historicalTrades - Get historical trades for a specific currency 1283 | * 1284 | * @link https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md#old-trade-lookup-market_data 1285 | * @link https://binance-docs.github.io/apidocs/spot/en/#old-trade-lookup 1286 | * 1287 | * @property int $weight 5 1288 | * Standard weight is 5 but if no tradeId is given, weight is 1 1289 | * 1290 | * @param string $symbol (mandatory) to query, e.g. BNBBTC 1291 | * @param int $limit (optional) limit the amount of trades, default 500, max 1000 1292 | * @param int $tradeId (optional) return the orders from this orderId onwards, negative to get recent ones 1293 | * 1294 | * @return array containing the response 1295 | * @throws \Exception 1296 | */ 1297 | public function historicalTrades(string $symbol, int $limit = 500, int $tradeId = -1) 1298 | { 1299 | $parameters = [ 1300 | "symbol" => $symbol, 1301 | "limit" => $limit, 1302 | ]; 1303 | if ($tradeId > 0) { 1304 | $parameters["fromId"] = $tradeId; 1305 | } else { 1306 | // if there is no tradeId given, we can use v3/trades, weight is 1 and not 5 1307 | return $this->httpRequest("v3/trades", "GET", $parameters); 1308 | } 1309 | 1310 | // The endpoint cannot handle extra parameters like 'timestamp' or 'signature', 1311 | // but it needs the http header with the key so we need to construct it here 1312 | $query = http_build_query($parameters, '', '&'); 1313 | return $this->httpRequest("v3/historicalTrades?$query"); 1314 | } 1315 | 1316 | /** 1317 | * depth get Market depth 1318 | * 1319 | * $depth = $api->depth("ETHBTC"); 1320 | * 1321 | * @param $symbol string the symbol to get the depth information for 1322 | * @param $limit int set limition for number of market depth data 1323 | * @return array with error message or array of market depth 1324 | * @throws \Exception 1325 | */ 1326 | public function depth(string $symbol, int $limit = 100) 1327 | { 1328 | if (is_int($limit) === false) { 1329 | $limit = 100; 1330 | } 1331 | 1332 | if (isset($symbol) === false || is_string($symbol) === false) { 1333 | // WPCS: XSS OK. 1334 | echo "asset: expected bool false, " . gettype($symbol) . " given" . PHP_EOL; 1335 | } 1336 | $json = $this->httpRequest("v1/depth", "GET", [ 1337 | "symbol" => $symbol, 1338 | "limit" => $limit, 1339 | ]); 1340 | if (isset($this->info[$symbol]) === false) { 1341 | $this->info[$symbol] = []; 1342 | } 1343 | $this->info[$symbol]['firstUpdate'] = $json['lastUpdateId']; 1344 | return $this->depthData($symbol, $json); 1345 | } 1346 | 1347 | /** 1348 | * balances get balances for the account assets 1349 | * 1350 | * $balances = $api->balances($ticker); 1351 | * 1352 | * @param bool $priceData array of the symbols balances are required for 1353 | * @return array with error message or array of balances 1354 | * @throws \Exception 1355 | */ 1356 | public function balances($priceData = false) 1357 | { 1358 | if (is_array($priceData) === false) { 1359 | $priceData = false; 1360 | } 1361 | 1362 | $account = $this->httpRequest("v3/account", "GET", [], true); 1363 | 1364 | if (is_array($account) === false) { 1365 | echo "Error: unable to fetch your account details" . PHP_EOL; 1366 | } 1367 | 1368 | if (isset($account['balances']) === false || empty($account['balances'])) { 1369 | echo "Error: your balances were empty or unset" . PHP_EOL; 1370 | return []; 1371 | } 1372 | 1373 | return $this->balanceData($account, $priceData); 1374 | } 1375 | 1376 | /** 1377 | * coins get list coins 1378 | * 1379 | * $coins = $api->coins(); 1380 | * @return array with error message or array containing coins 1381 | * @throws \Exception 1382 | */ 1383 | public function coins() 1384 | { 1385 | return $this->httpRequest('v1/capital/config/getall', 'GET', [ 'sapi' => true ], true); 1386 | } 1387 | 1388 | /** 1389 | * getProxyUriString get Uniform Resource Identifier string assocaited with proxy config 1390 | * 1391 | * $balances = $api->getProxyUriString(); 1392 | * 1393 | * @return string uri 1394 | */ 1395 | public function getProxyUriString() 1396 | { 1397 | $uri = isset($this->proxyConf['proto']) ? $this->proxyConf['proto'] : "http"; 1398 | // https://curl.haxx.se/libcurl/c/CURLOPT_PROXY.html 1399 | $supportedProtocols = array( 1400 | 'http', 1401 | 'https', 1402 | 'socks4', 1403 | 'socks4a', 1404 | 'socks5', 1405 | 'socks5h', 1406 | ); 1407 | 1408 | if (in_array($uri, $supportedProtocols) === false) { 1409 | // WPCS: XSS OK. 1410 | echo "Unknown proxy protocol '" . $this->proxyConf['proto'] . "', supported protocols are " . implode(", ", $supportedProtocols) . PHP_EOL; 1411 | } 1412 | 1413 | $uri .= "://"; 1414 | $uri .= isset($this->proxyConf['address']) ? $this->proxyConf['address'] : "localhost"; 1415 | 1416 | if (isset($this->proxyConf['address']) === false) { 1417 | // WPCS: XSS OK. 1418 | echo "warning: proxy address not set defaulting to localhost" . PHP_EOL; 1419 | } 1420 | 1421 | $uri .= ":"; 1422 | $uri .= isset($this->proxyConf['port']) ? $this->proxyConf['port'] : "1080"; 1423 | 1424 | if (isset($this->proxyConf['address']) === false) { 1425 | // WPCS: XSS OK. 1426 | echo "warning: proxy port not set defaulting to 1080" . PHP_EOL; 1427 | } 1428 | 1429 | return $uri; 1430 | } 1431 | 1432 | /** 1433 | * setProxy set proxy config by passing in an array of the proxy configuration 1434 | * 1435 | * $proxyConf = [ 1436 | * 'proto' => 'tcp', 1437 | * 'address' => '192.168.1.1', 1438 | * 'port' => '8080', 1439 | * 'user' => 'dude', 1440 | * 'pass' => 'd00d' 1441 | * ]; 1442 | * 1443 | * $api->setProxy( $proxyconf ); 1444 | * 1445 | * @return null 1446 | */ 1447 | public function setProxy(array $proxyconf) 1448 | { 1449 | $this->proxyConf = $proxyconf; 1450 | } 1451 | 1452 | /** 1453 | * httpRequest curl wrapper for all http api requests. 1454 | * You can't call this function directly, use the helper functions 1455 | * 1456 | * @see buy() 1457 | * @see sell() 1458 | * @see marketBuy() 1459 | * @see marketSell() $this->httpRequest( "https://api.binance.com/api/v1/ticker/24hr"); 1460 | * 1461 | * @param $url string the endpoint to query, typically includes query string 1462 | * @param $method string this should be typically GET, POST or DELETE 1463 | * @param $params array addtional options for the request 1464 | * @param $signed bool true or false sign the request with api secret 1465 | * @return array containing the response 1466 | * @throws \Exception 1467 | */ 1468 | protected function httpRequest(string $url, string $method = "GET", array $params = [], bool $signed = false) 1469 | { 1470 | if (function_exists('curl_init') === false) { 1471 | throw new \Exception("Sorry cURL is not installed!"); 1472 | } 1473 | 1474 | if ($this->caOverride === false) { 1475 | if (file_exists(getcwd() . '/ca.pem') === false) { 1476 | $this->downloadCurlCaBundle(); 1477 | } 1478 | } 1479 | 1480 | $curl = curl_init(); 1481 | curl_setopt($curl, CURLOPT_VERBOSE, $this->httpDebug); 1482 | $query = http_build_query($params, '', '&'); 1483 | 1484 | // signed with params 1485 | if ($signed === true) { 1486 | if (empty($this->api_key)) { 1487 | throw new \Exception("signedRequest error: API Key not set!"); 1488 | } 1489 | 1490 | if (empty($this->api_secret)) { 1491 | throw new \Exception("signedRequest error: API Secret not set!"); 1492 | } 1493 | 1494 | $base = $this->getRestEndpoint(); 1495 | $ts = (microtime(true) * 1000) + $this->info['timeOffset']; 1496 | $params['timestamp'] = number_format($ts, 0, '.', ''); 1497 | if (isset($params['wapi'])) { 1498 | if ($this->useTestnet) { 1499 | throw new \Exception("wapi endpoints are not available in testnet"); 1500 | } 1501 | unset($params['wapi']); 1502 | $base = $this->wapi; 1503 | } 1504 | 1505 | if (isset($params['sapi'])) { 1506 | if ($this->useTestnet) { 1507 | throw new \Exception("sapi endpoints are not available in testnet"); 1508 | } 1509 | unset($params['sapi']); 1510 | $base = $this->sapi; 1511 | } 1512 | 1513 | $query = http_build_query($params, '', '&'); 1514 | $query = str_replace([ '%40' ], [ '@' ], $query);//if send data type "e-mail" then binance return: [Signature for this request is not valid.] 1515 | $signature = hash_hmac('sha256', $query, $this->api_secret); 1516 | if ($method === "POST") { 1517 | $endpoint = $base . $url; 1518 | $params['signature'] = $signature; // signature needs to be inside BODY 1519 | $query = http_build_query($params, '', '&'); // rebuilding query 1520 | } else { 1521 | $endpoint = $base . $url . '?' . $query . '&signature=' . $signature; 1522 | } 1523 | 1524 | curl_setopt($curl, CURLOPT_URL, $endpoint); 1525 | curl_setopt($curl, CURLOPT_HTTPHEADER, array( 1526 | 'X-MBX-APIKEY: ' . $this->api_key, 1527 | )); 1528 | } 1529 | // params so buildquery string and append to url 1530 | elseif (count($params) > 0) { 1531 | curl_setopt($curl, CURLOPT_URL, $this->getRestEndpoint() . $url . '?' . $query); 1532 | } 1533 | // no params so just the base url 1534 | else { 1535 | curl_setopt($curl, CURLOPT_URL, $this->getRestEndpoint() . $url); 1536 | curl_setopt($curl, CURLOPT_HTTPHEADER, array( 1537 | 'X-MBX-APIKEY: ' . $this->api_key, 1538 | )); 1539 | } 1540 | curl_setopt($curl, CURLOPT_USERAGENT, "User-Agent: Mozilla/4.0 (compatible; PHP Binance API)"); 1541 | // Post and postfields 1542 | if ($method === "POST") { 1543 | curl_setopt($curl, CURLOPT_POST, true); 1544 | curl_setopt($curl, CURLOPT_POSTFIELDS, $query); 1545 | } 1546 | // Delete Method 1547 | if ($method === "DELETE") { 1548 | curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); 1549 | } 1550 | 1551 | // PUT Method 1552 | if ($method === "PUT") { 1553 | curl_setopt($curl, CURLOPT_PUT, true); 1554 | } 1555 | 1556 | // proxy settings 1557 | if (is_array($this->proxyConf)) { 1558 | curl_setopt($curl, CURLOPT_PROXY, $this->getProxyUriString()); 1559 | if (isset($this->proxyConf['user']) && isset($this->proxyConf['pass'])) { 1560 | curl_setopt($curl, CURLOPT_PROXYUSERPWD, $this->proxyConf['user'] . ':' . $this->proxyConf['pass']); 1561 | } 1562 | } 1563 | curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); 1564 | curl_setopt($curl, CURLOPT_HEADER, true); 1565 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); 1566 | curl_setopt($curl, CURLOPT_TIMEOUT, 60); 1567 | 1568 | // set user defined curl opts last for overriding 1569 | foreach ($this->curlOpts as $key => $value) { 1570 | curl_setopt($curl, constant($key), $value); 1571 | } 1572 | 1573 | if ($this->caOverride === false) { 1574 | if (file_exists(getcwd() . '/ca.pem') === false) { 1575 | $this->downloadCurlCaBundle(); 1576 | } 1577 | } 1578 | 1579 | $output = curl_exec($curl); 1580 | // Check if any error occurred 1581 | if (curl_errno($curl) > 0) { 1582 | // should always output error, not only on httpdebug 1583 | // not outputing errors, hides it from users and ends up with tickets on github 1584 | throw new \Exception('Curl error: ' . curl_error($curl)); 1585 | } 1586 | 1587 | $header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE); 1588 | $header = $this->get_headers_from_curl_response($output); 1589 | $output = substr($output, $header_size); 1590 | 1591 | curl_close($curl); 1592 | 1593 | $json = json_decode($output, true); 1594 | 1595 | $this->lastRequest = [ 1596 | 'url' => $url, 1597 | 'method' => $method, 1598 | 'params' => $params, 1599 | 'header' => $header, 1600 | 'json' => $json 1601 | ]; 1602 | 1603 | if (isset($header['x-mbx-used-weight'])) { 1604 | $this->setXMbxUsedWeight($header['x-mbx-used-weight']); 1605 | } 1606 | 1607 | if (isset($header['x-mbx-used-weight-1m'])) { 1608 | $this->setXMbxUsedWeight1m($header['x-mbx-used-weight-1m']); 1609 | } 1610 | 1611 | if (isset($json['msg']) && !empty($json['msg'])) { 1612 | if ( $url != 'v1/system/status' && $url != 'v3/systemStatus.html' && $url != 'v3/accountStatus.html') { 1613 | // should always output error, not only on httpdebug 1614 | // not outputing errors, hides it from users and ends up with tickets on github 1615 | throw new \Exception('signedRequest error: '.print_r($output, true)); 1616 | } 1617 | } 1618 | $this->transfered += strlen($output); 1619 | $this->requestCount++; 1620 | return $json; 1621 | } 1622 | 1623 | /** 1624 | * Converts the output of the CURL header to an array 1625 | * 1626 | * @param $header string containing the response 1627 | * @return array headers converted to an array 1628 | */ 1629 | public function get_headers_from_curl_response(string $header) 1630 | { 1631 | $headers = array(); 1632 | $header_text = substr($header, 0, strpos($header, "\r\n\r\n")); 1633 | 1634 | foreach (explode("\r\n", $header_text) as $i => $line) 1635 | if ($i === 0) 1636 | $headers['http_code'] = $line; 1637 | else { 1638 | list ($key, $value) = explode(': ', $line); 1639 | $headers[$key] = $value; 1640 | } 1641 | 1642 | return $headers; 1643 | } 1644 | 1645 | /** 1646 | * order formats the orders before sending them to the curl wrapper function 1647 | * You can call this function directly or use the helper functions 1648 | * 1649 | * @see buy() 1650 | * @see sell() 1651 | * @see marketBuy() 1652 | * @see marketSell() $this->httpRequest( "https://api.binance.com/api/v1/ticker/24hr"); 1653 | * 1654 | * @param $side string typically "BUY" or "SELL" 1655 | * @param $symbol string to buy or sell 1656 | * @param $quantity string in the order 1657 | * @param $price string for the order 1658 | * @param $type string is determined by the symbol bu typicall LIMIT, STOP_LOSS_LIMIT etc. 1659 | * @param $flags array additional transaction options 1660 | * @param $test bool whether to test or not, test only validates the query 1661 | * @return array containing the response 1662 | * @throws \Exception 1663 | */ 1664 | public function order(string $side, string $symbol, $quantity, $price, string $type = "LIMIT", array $flags = [], bool $test = false) 1665 | { 1666 | $opt = [ 1667 | "symbol" => $symbol, 1668 | "side" => $side, 1669 | "type" => $type, 1670 | "quantity" => $quantity, 1671 | "recvWindow" => 60000, 1672 | ]; 1673 | 1674 | // someone has preformated there 8 decimal point double already 1675 | // dont do anything, leave them do whatever they want 1676 | if (gettype($price) !== "string") { 1677 | // for every other type, lets format it appropriately 1678 | $price = number_format($price, 8, '.', ''); 1679 | } 1680 | 1681 | if (is_numeric($quantity) === false) { 1682 | // WPCS: XSS OK. 1683 | echo "warning: quantity expected numeric got " . gettype($quantity) . PHP_EOL; 1684 | } 1685 | 1686 | if (is_string($price) === false) { 1687 | // WPCS: XSS OK. 1688 | echo "warning: price expected string got " . gettype($price) . PHP_EOL; 1689 | } 1690 | 1691 | if ($type === "LIMIT" || $type === "STOP_LOSS_LIMIT" || $type === "TAKE_PROFIT_LIMIT") { 1692 | $opt["price"] = $price; 1693 | $opt["timeInForce"] = "GTC"; 1694 | } 1695 | 1696 | if ($type === "MARKET" && isset($flags['isQuoteOrder']) && $flags['isQuoteOrder']) { 1697 | unset($opt['quantity']); 1698 | $opt['quoteOrderQty'] = $quantity; 1699 | } 1700 | 1701 | if (isset($flags['stopPrice'])) { 1702 | $opt['stopPrice'] = $flags['stopPrice']; 1703 | } 1704 | 1705 | if (isset($flags['icebergQty'])) { 1706 | $opt['icebergQty'] = $flags['icebergQty']; 1707 | } 1708 | 1709 | if (isset($flags['newOrderRespType'])) { 1710 | $opt['newOrderRespType'] = $flags['newOrderRespType']; 1711 | } 1712 | 1713 | $qstring = ($test === false) ? "v3/order" : "v3/order/test"; 1714 | return $this->httpRequest($qstring, "POST", $opt, true); 1715 | } 1716 | 1717 | /** 1718 | * candlesticks get the candles for the given intervals 1719 | * 1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M 1720 | * 1721 | * $candles = $api->candlesticks("BNBBTC", "5m"); 1722 | * 1723 | * @param $symbol string to query 1724 | * @param $interval string to request 1725 | * @param $limit int limit the amount of candles 1726 | * @param $startTime string request candle information starting from here 1727 | * @param $endTime string request candle information ending here 1728 | * @return array containing the response 1729 | * @throws \Exception 1730 | */ 1731 | public function candlesticks(string $symbol, string $interval = "5m", int $limit = null, $startTime = null, $endTime = null) 1732 | { 1733 | if (!isset($this->charts[$symbol])) { 1734 | $this->charts[$symbol] = []; 1735 | } 1736 | 1737 | $opt = [ 1738 | "symbol" => $symbol, 1739 | "interval" => $interval, 1740 | ]; 1741 | 1742 | if ($limit) { 1743 | $opt["limit"] = $limit; 1744 | } 1745 | 1746 | if ($startTime) { 1747 | $opt["startTime"] = $startTime; 1748 | } 1749 | 1750 | if ($endTime) { 1751 | $opt["endTime"] = $endTime; 1752 | } 1753 | 1754 | $response = $this->httpRequest("v1/klines", "GET", $opt); 1755 | 1756 | if (is_array($response) === false) { 1757 | return []; 1758 | } 1759 | 1760 | if (count($response) === 0) { 1761 | echo "warning: v1/klines returned empty array, usually a blip in the connection or server" . PHP_EOL; 1762 | return []; 1763 | } 1764 | 1765 | $ticks = $this->chartData($symbol, $interval, $response); 1766 | $this->charts[$symbol][$interval] = $ticks; 1767 | return $ticks; 1768 | } 1769 | 1770 | /** 1771 | * balanceData Converts all your balances into a nice array 1772 | * If priceData is passed from $api->prices() it will add btcValue & btcTotal to each symbol 1773 | * This function sets $btc_value which is your estimated BTC value of all assets combined and $btc_total which includes amount on order 1774 | * 1775 | * $candles = $api->candlesticks("BNBBTC", "5m"); 1776 | * 1777 | * @param $array array of your balances 1778 | * @param $priceData array of prices 1779 | * @return array containing the response 1780 | */ 1781 | protected function balanceData(array $array, $priceData) 1782 | { 1783 | $balances = []; 1784 | 1785 | if (is_array($priceData)) { 1786 | $btc_value = $btc_total = 0.00; 1787 | } 1788 | 1789 | if (empty($array) || empty($array['balances'])) { 1790 | // WPCS: XSS OK. 1791 | echo "balanceData error: Please make sure your system time is synchronized: call \$api->useServerTime() before this function" . PHP_EOL; 1792 | echo "ERROR: Invalid request. Please double check your API keys and permissions." . PHP_EOL; 1793 | return []; 1794 | } 1795 | 1796 | foreach ($array['balances'] as $obj) { 1797 | $asset = $obj['asset']; 1798 | $balances[$asset] = [ 1799 | "available" => $obj['free'], 1800 | "onOrder" => $obj['locked'], 1801 | "btcValue" => 0.00000000, 1802 | "btcTotal" => 0.00000000, 1803 | ]; 1804 | 1805 | if (is_array($priceData) === false) { 1806 | continue; 1807 | } 1808 | 1809 | if ($obj['free'] + $obj['locked'] < 0.00000001) { 1810 | continue; 1811 | } 1812 | 1813 | if ($asset === 'BTC') { 1814 | $balances[$asset]['btcValue'] = $obj['free']; 1815 | $balances[$asset]['btcTotal'] = $obj['free'] + $obj['locked']; 1816 | $btc_value += $obj['free']; 1817 | $btc_total += $obj['free'] + $obj['locked']; 1818 | continue; 1819 | } elseif ($asset === 'USDT' || $asset === 'USDC' || $asset === 'PAX' || $asset === 'BUSD') { 1820 | $btcValue = $obj['free'] / $priceData['BTCUSDT']; 1821 | $btcTotal = ($obj['free'] + $obj['locked']) / $priceData['BTCUSDT']; 1822 | $balances[$asset]['btcValue'] = $btcValue; 1823 | $balances[$asset]['btcTotal'] = $btcTotal; 1824 | $btc_value += $btcValue; 1825 | $btc_total += $btcTotal; 1826 | continue; 1827 | } 1828 | 1829 | $symbol = $asset . 'BTC'; 1830 | 1831 | if ($symbol === 'BTCUSDT') { 1832 | $btcValue = number_format($obj['free'] / $priceData['BTCUSDT'], 8, '.', ''); 1833 | $btcTotal = number_format(($obj['free'] + $obj['locked']) / $priceData['BTCUSDT'], 8, '.', ''); 1834 | } elseif (isset($priceData[$symbol]) === false) { 1835 | $btcValue = $btcTotal = 0; 1836 | } else { 1837 | $btcValue = number_format($obj['free'] * $priceData[$symbol], 8, '.', ''); 1838 | $btcTotal = number_format(($obj['free'] + $obj['locked']) * $priceData[$symbol], 8, '.', ''); 1839 | } 1840 | 1841 | $balances[$asset]['btcValue'] = $btcValue; 1842 | $balances[$asset]['btcTotal'] = $btcTotal; 1843 | $btc_value += $btcValue; 1844 | $btc_total += $btcTotal; 1845 | } 1846 | if (is_array($priceData)) { 1847 | uasort($balances, function ($opA, $opB) { 1848 | if ($opA == $opB) 1849 | return 0; 1850 | return ($opA['btcValue'] < $opB['btcValue']) ? 1 : -1; 1851 | }); 1852 | $this->btc_value = $btc_value; 1853 | $this->btc_total = $btc_total; 1854 | } 1855 | return $balances; 1856 | } 1857 | 1858 | /** 1859 | * balanceHandler Convert balance WebSocket data into array 1860 | * 1861 | * $data = $this->balanceHandler( $json ); 1862 | * 1863 | * @param $json array data to convert 1864 | * @return array 1865 | */ 1866 | protected function balanceHandler(array $json) 1867 | { 1868 | $balances = []; 1869 | foreach ($json as $item) { 1870 | $asset = $item->a; 1871 | $available = $item->f; 1872 | $onOrder = $item->l; 1873 | $balances[$asset] = [ 1874 | "available" => $available, 1875 | "onOrder" => $onOrder, 1876 | ]; 1877 | } 1878 | return $balances; 1879 | } 1880 | 1881 | /** 1882 | * tickerStreamHandler Convert WebSocket ticker data into array 1883 | * 1884 | * $data = $this->tickerStreamHandler( $json ); 1885 | * 1886 | * @param $json object data to convert 1887 | * @return array 1888 | */ 1889 | protected function tickerStreamHandler(\stdClass $json) 1890 | { 1891 | return [ 1892 | "eventType" => $json->e, 1893 | "eventTime" => $json->E, 1894 | "symbol" => $json->s, 1895 | "priceChange" => $json->p, 1896 | "percentChange" => $json->P, 1897 | "averagePrice" => $json->w, 1898 | "prevClose" => $json->x, 1899 | "close" => $json->c, 1900 | "closeQty" => $json->Q, 1901 | "bestBid" => $json->b, 1902 | "bestBidQty" => $json->B, 1903 | "bestAsk" => $json->a, 1904 | "bestAskQty" => $json->A, 1905 | "open" => $json->o, 1906 | "high" => $json->h, 1907 | "low" => $json->l, 1908 | "volume" => $json->v, 1909 | "quoteVolume" => $json->q, 1910 | "openTime" => $json->O, 1911 | "closeTime" => $json->C, 1912 | "firstTradeId" => $json->F, 1913 | "lastTradeId" => $json->L, 1914 | "numTrades" => $json->n, 1915 | ]; 1916 | } 1917 | 1918 | /** 1919 | * tickerStreamHandler Convert WebSocket trade execution into array 1920 | * 1921 | * $data = $this->executionHandler( $json ); 1922 | * 1923 | * @param \stdClass $json object data to convert 1924 | * @return array 1925 | */ 1926 | protected function executionHandler(\stdClass $json) 1927 | { 1928 | return [ 1929 | "symbol" => $json->s, 1930 | "side" => $json->S, 1931 | "orderType" => $json->o, 1932 | "quantity" => $json->q, 1933 | "price" => $json->p, 1934 | "executionType" => $json->x, 1935 | "orderStatus" => $json->X, 1936 | "rejectReason" => $json->r, 1937 | "orderId" => $json->i, 1938 | "clientOrderId" => $json->c, 1939 | "orderTime" => $json->T, 1940 | "eventTime" => $json->E, 1941 | ]; 1942 | } 1943 | 1944 | /** 1945 | * chartData Convert kline data into object 1946 | * 1947 | * $object = $this->chartData($symbol, $interval, $ticks); 1948 | * 1949 | * @param $symbol string of your currency 1950 | * @param $interval string the time interval 1951 | * @param $ticks array of the canbles array 1952 | * @return array object of the chartdata 1953 | */ 1954 | protected function chartData(string $symbol, string $interval, array $ticks) 1955 | { 1956 | if (!isset($this->info[$symbol])) { 1957 | $this->info[$symbol] = []; 1958 | } 1959 | 1960 | if (!isset($this->info[$symbol][$interval])) { 1961 | $this->info[$symbol][$interval] = []; 1962 | } 1963 | 1964 | $output = []; 1965 | foreach ($ticks as $tick) { 1966 | list($openTime, $open, $high, $low, $close, $assetVolume, $closeTime, $baseVolume, $trades, $assetBuyVolume, $takerBuyVolume, $ignored) = $tick; 1967 | $output[$openTime] = [ 1968 | "open" => $open, 1969 | "high" => $high, 1970 | "low" => $low, 1971 | "close" => $close, 1972 | "volume" => $baseVolume, 1973 | "openTime" => $openTime, 1974 | "closeTime" => $closeTime, 1975 | "assetVolume" => $assetVolume, 1976 | "baseVolume" => $baseVolume, 1977 | "trades" => $trades, 1978 | "assetBuyVolume" => $assetBuyVolume, 1979 | "takerBuyVolume" => $takerBuyVolume, 1980 | "ignored" => $ignored, 1981 | ]; 1982 | } 1983 | 1984 | if (isset($openTime)) { 1985 | $this->info[$symbol][$interval]['firstOpen'] = $openTime; 1986 | } 1987 | 1988 | return $output; 1989 | } 1990 | 1991 | /** 1992 | * tradesData Convert aggTrades data into easier format 1993 | * 1994 | * $tradesData = $this->tradesData($trades); 1995 | * 1996 | * @param $trades array of trade information 1997 | * @return array easier format for trade information 1998 | */ 1999 | protected function tradesData(array $trades) 2000 | { 2001 | $output = []; 2002 | foreach ($trades as $trade) { 2003 | $price = $trade['p']; 2004 | $quantity = $trade['q']; 2005 | $timestamp = $trade['T']; 2006 | $maker = $trade['m'] ? 'true' : 'false'; 2007 | $output[] = [ 2008 | "price" => $price, 2009 | "quantity" => $quantity, 2010 | "timestamp" => $timestamp, 2011 | "maker" => $maker, 2012 | ]; 2013 | } 2014 | return $output; 2015 | } 2016 | 2017 | /** 2018 | * bookPriceData Consolidates Book Prices into an easy to use object 2019 | * 2020 | * $bookPriceData = $this->bookPriceData($array); 2021 | * 2022 | * @param $array array book prices 2023 | * @return array easier format for book prices information 2024 | */ 2025 | protected function bookPriceData(array $array) 2026 | { 2027 | $bookprices = []; 2028 | foreach ($array as $obj) { 2029 | $bookprices[$obj['symbol']] = [ 2030 | "bid" => $obj['bidPrice'], 2031 | "bids" => $obj['bidQty'], 2032 | "ask" => $obj['askPrice'], 2033 | "asks" => $obj['askQty'], 2034 | ]; 2035 | } 2036 | return $bookprices; 2037 | } 2038 | 2039 | /** 2040 | * priceData Converts Price Data into an easy key/value array 2041 | * 2042 | * $array = $this->priceData($array); 2043 | * 2044 | * @param $array array of prices 2045 | * @return array of key/value pairs 2046 | */ 2047 | protected function priceData(array $array) 2048 | { 2049 | $prices = []; 2050 | foreach ($array as $obj) { 2051 | $prices[$obj['symbol']] = $obj['price']; 2052 | } 2053 | return $prices; 2054 | } 2055 | 2056 | /** 2057 | * cumulative Converts depth cache into a cumulative array 2058 | * 2059 | * $cumulative = $api->cumulative($depth); 2060 | * 2061 | * @param $depth array cache array 2062 | * @return array cumulative depth cache 2063 | */ 2064 | public function cumulative(array $depth) 2065 | { 2066 | $bids = []; 2067 | $asks = []; 2068 | $cumulative = 0; 2069 | foreach ($depth['bids'] as $price => $quantity) { 2070 | $cumulative += $quantity; 2071 | $bids[] = [ 2072 | $price, 2073 | $cumulative, 2074 | ]; 2075 | } 2076 | $cumulative = 0; 2077 | foreach ($depth['asks'] as $price => $quantity) { 2078 | $cumulative += $quantity; 2079 | $asks[] = [ 2080 | $price, 2081 | $cumulative, 2082 | ]; 2083 | } 2084 | return [ 2085 | "bids" => $bids, 2086 | "asks" => array_reverse($asks), 2087 | ]; 2088 | } 2089 | 2090 | /** 2091 | * highstock Converts Chart Data into array for highstock & kline charts 2092 | * 2093 | * $highstock = $api->highstock($chart, $include_volume); 2094 | * 2095 | * @param $chart array 2096 | * @param $include_volume bool for inclusion of volume 2097 | * @return array highchart data 2098 | */ 2099 | public function highstock(array $chart, bool $include_volume = false) 2100 | { 2101 | $array = []; 2102 | foreach ($chart as $timestamp => $obj) { 2103 | $line = [ 2104 | $timestamp, 2105 | floatval($obj['open']), 2106 | floatval($obj['high']), 2107 | floatval($obj['low']), 2108 | floatval($obj['close']), 2109 | ]; 2110 | if ($include_volume) { 2111 | $line[] = floatval($obj['volume']); 2112 | } 2113 | 2114 | $array[] = $line; 2115 | } 2116 | return $array; 2117 | } 2118 | 2119 | /** 2120 | * first Gets first key of an array 2121 | * 2122 | * $first = $api->first($array); 2123 | * 2124 | * @param $array array 2125 | * @return string key or null 2126 | */ 2127 | public function first(array $array) 2128 | { 2129 | if (count($array) > 0) { 2130 | return array_keys($array)[0]; 2131 | } 2132 | return null; 2133 | } 2134 | 2135 | /** 2136 | * last Gets last key of an array 2137 | * 2138 | * $last = $api->last($array); 2139 | * 2140 | * @param $array array 2141 | * @return string key or null 2142 | */ 2143 | public function last(array $array) 2144 | { 2145 | if (count($array) > 0) { 2146 | return array_keys(array_slice($array, -1))[0]; 2147 | } 2148 | return null; 2149 | } 2150 | 2151 | /** 2152 | * displayDepth Formats nicely for console output 2153 | * 2154 | * $outputString = $api->displayDepth($array); 2155 | * 2156 | * @param $array array 2157 | * @return string of the depth information 2158 | */ 2159 | public function displayDepth(array $array) 2160 | { 2161 | $output = ''; 2162 | foreach ([ 2163 | 'asks', 2164 | 'bids', 2165 | ] as $type) { 2166 | $entries = $array[$type]; 2167 | if ($type === 'asks') { 2168 | $entries = array_reverse($entries); 2169 | } 2170 | 2171 | $output .= "{$type}:" . PHP_EOL; 2172 | foreach ($entries as $price => $quantity) { 2173 | $total = number_format($price * $quantity, 8, '.', ''); 2174 | $quantity = str_pad(str_pad(number_format(rtrim($quantity, '.0')), 10, ' ', STR_PAD_LEFT), 15); 2175 | $output .= "{$price} {$quantity} {$total}" . PHP_EOL; 2176 | } 2177 | // echo str_repeat('-', 32).PHP_EOL; 2178 | } 2179 | return $output; 2180 | } 2181 | 2182 | /** 2183 | * depthData Formats depth data for nice display 2184 | * 2185 | * $array = $this->depthData($symbol, $json); 2186 | * 2187 | * @param $symbol string to display 2188 | * @param $json array of the depth infomration 2189 | * @return array of the depth information 2190 | */ 2191 | protected function depthData(string $symbol, array $json) 2192 | { 2193 | $bids = $asks = []; 2194 | foreach ($json['bids'] as $obj) { 2195 | $bids[$obj[0]] = $obj[1]; 2196 | } 2197 | foreach ($json['asks'] as $obj) { 2198 | $asks[$obj[0]] = $obj[1]; 2199 | } 2200 | return $this->depthCache[$symbol] = [ 2201 | "bids" => $bids, 2202 | "asks" => $asks, 2203 | ]; 2204 | } 2205 | 2206 | /** 2207 | * roundStep rounds quantity with stepSize 2208 | * @param $qty quantity 2209 | * @param $stepSize parameter from exchangeInfo 2210 | * @return rounded value. example: roundStep(1.2345, 0.1) = 1.2 2211 | * 2212 | */ 2213 | public function roundStep($qty, $stepSize = 0.1) 2214 | { 2215 | $precision = strlen(substr(strrchr(rtrim($stepSize, '0'), '.'), 1)); 2216 | return round((($qty / $stepSize) | 0) * $stepSize, $precision); 2217 | } 2218 | 2219 | /** 2220 | * roundTicks rounds price with tickSize 2221 | * @param $value price 2222 | * @param $tickSize parameter from exchangeInfo 2223 | * @return rounded value. example: roundStep(1.2345, 0.1) = 1.2 2224 | * 2225 | */ 2226 | public function roundTicks($price, $tickSize) 2227 | { 2228 | $precision = strlen(rtrim(substr($tickSize, strpos($tickSize, '.', 1) + 1), '0')); 2229 | return number_format($price, $precision, '.', ''); 2230 | } 2231 | 2232 | /** 2233 | * getTransfered gets the total transfered in b,Kb,Mb,Gb 2234 | * 2235 | * $transfered = $api->getTransfered(); 2236 | * 2237 | * @return string showing the total transfered 2238 | */ 2239 | public function getTransfered() 2240 | { 2241 | $base = log($this->transfered, 1024); 2242 | $suffixes = array( 2243 | '', 2244 | 'K', 2245 | 'M', 2246 | 'G', 2247 | 'T', 2248 | ); 2249 | return round(pow(1024, $base - floor($base)), 2) . ' ' . $suffixes[floor($base)]; 2250 | } 2251 | 2252 | /** 2253 | * getRequestCount gets the total number of API calls 2254 | * 2255 | * $apiCount = $api->getRequestCount(); 2256 | * 2257 | * @return int get the total number of api calls 2258 | */ 2259 | public function getRequestCount() 2260 | { 2261 | return $this->requestCount; 2262 | } 2263 | 2264 | /** 2265 | * addToTransfered add interger bytes to the total transfered 2266 | * also incrementes the api counter 2267 | * 2268 | * $apiCount = $api->addToTransfered( $int ); 2269 | * 2270 | * @return null 2271 | */ 2272 | public function addToTransfered(int $int) 2273 | { 2274 | $this->transfered += $int; 2275 | $this->requestCount++; 2276 | } 2277 | 2278 | /* 2279 | * WebSockets 2280 | */ 2281 | 2282 | /** 2283 | * depthHandler For WebSocket Depth Cache 2284 | * 2285 | * $this->depthHandler($json); 2286 | * 2287 | * @param $json array of depth bids and asks 2288 | * @return null 2289 | */ 2290 | protected function depthHandler(array $json) 2291 | { 2292 | $symbol = $json['s']; 2293 | if ($json['u'] <= $this->info[$symbol]['firstUpdate']) { 2294 | return; 2295 | } 2296 | 2297 | foreach ($json['b'] as $bid) { 2298 | $this->depthCache[$symbol]['bids'][$bid[0]] = $bid[1]; 2299 | if ($bid[1] == "0.00000000") { 2300 | unset($this->depthCache[$symbol]['bids'][$bid[0]]); 2301 | } 2302 | } 2303 | foreach ($json['a'] as $ask) { 2304 | $this->depthCache[$symbol]['asks'][$ask[0]] = $ask[1]; 2305 | if ($ask[1] == "0.00000000") { 2306 | unset($this->depthCache[$symbol]['asks'][$ask[0]]); 2307 | } 2308 | } 2309 | } 2310 | 2311 | /** 2312 | * chartHandler For WebSocket Chart Cache 2313 | * 2314 | * $this->chartHandler($symbol, $interval, $json); 2315 | * 2316 | * @param $symbol string to sort 2317 | * @param $interval string time 2318 | * @param \stdClass $json object time 2319 | * @return null 2320 | */ 2321 | protected function chartHandler(string $symbol, string $interval, \stdClass $json) 2322 | { 2323 | if (!$this->info[$symbol][$interval]['firstOpen']) { // Wait for /kline to finish loading 2324 | $this->chartQueue[$symbol][$interval][] = $json; 2325 | return; 2326 | } 2327 | $chart = $json->k; 2328 | $symbol = $json->s; 2329 | $interval = $chart->i; 2330 | $tick = $chart->t; 2331 | if ($tick < $this->info[$symbol][$interval]['firstOpen']) { 2332 | return; 2333 | } 2334 | // Filter out of sync data 2335 | $open = $chart->o; 2336 | $high = $chart->h; 2337 | $low = $chart->l; 2338 | $close = $chart->c; 2339 | $volume = $chart->q; // +trades buyVolume assetVolume makerVolume 2340 | $this->charts[$symbol][$interval][$tick] = [ 2341 | "open" => $open, 2342 | "high" => $high, 2343 | "low" => $low, 2344 | "close" => $close, 2345 | "volume" => $volume, 2346 | ]; 2347 | } 2348 | 2349 | /** 2350 | * sortDepth Sorts depth data for display & getting highest bid and lowest ask 2351 | * 2352 | * $sorted = $api->sortDepth($symbol, $limit); 2353 | * 2354 | * @param $symbol string to sort 2355 | * @param $limit int depth 2356 | * @return null 2357 | */ 2358 | public function sortDepth(string $symbol, int $limit = 11) 2359 | { 2360 | $bids = $this->depthCache[$symbol]['bids']; 2361 | $asks = $this->depthCache[$symbol]['asks']; 2362 | krsort($bids); 2363 | ksort($asks); 2364 | return [ 2365 | "asks" => array_slice($asks, 0, $limit, true), 2366 | "bids" => array_slice($bids, 0, $limit, true), 2367 | ]; 2368 | } 2369 | 2370 | /** 2371 | * depthCache Pulls /depth data and subscribes to @depth WebSocket endpoint 2372 | * Maintains a local Depth Cache in sync via lastUpdateId. 2373 | * See depth() and depthHandler() 2374 | * 2375 | * $api->depthCache(["BNBBTC"], function($api, $symbol, $depth) { 2376 | * echo "{$symbol} depth cache update".PHP_EOL; 2377 | * //print_r($depth); // Print all depth data 2378 | * $limit = 11; // Show only the closest asks/bids 2379 | * $sorted = $api->sortDepth($symbol, $limit); 2380 | * $bid = $api->first($sorted['bids']); 2381 | * $ask = $api->first($sorted['asks']); 2382 | * echo $api->displayDepth($sorted); 2383 | * echo "ask: {$ask}".PHP_EOL; 2384 | * echo "bid: {$bid}".PHP_EOL; 2385 | * }); 2386 | * 2387 | * @param $symbol string optional array of symbols 2388 | * @param $callback callable closure 2389 | * @return null 2390 | */ 2391 | public function depthCache($symbols, callable $callback) 2392 | { 2393 | if (!is_array($symbols)) { 2394 | $symbols = [ 2395 | $symbols, 2396 | ]; 2397 | } 2398 | 2399 | $loop = \React\EventLoop\Factory::create(); 2400 | $react = new \React\Socket\Connector($loop); 2401 | $connector = new \Ratchet\Client\Connector($loop, $react); 2402 | foreach ($symbols as $symbol) { 2403 | if (!isset($this->info[$symbol])) { 2404 | $this->info[$symbol] = []; 2405 | } 2406 | 2407 | if (!isset($this->depthQueue[$symbol])) { 2408 | $this->depthQueue[$symbol] = []; 2409 | } 2410 | 2411 | if (!isset($this->depthCache[$symbol])) { 2412 | $this->depthCache[$symbol] = [ 2413 | "bids" => [], 2414 | "asks" => [], 2415 | ]; 2416 | } 2417 | 2418 | $this->info[$symbol]['firstUpdate'] = 0; 2419 | $endpoint = strtolower($symbol) . '@depthCache'; 2420 | $this->subscriptions[$endpoint] = true; 2421 | 2422 | $connector($this->getWsEndpoint() . strtolower($symbol) . '@depth')->then(function ($ws) use ($callback, $symbol, $loop, $endpoint) { 2423 | $ws->on('message', function ($data) use ($ws, $callback, $loop, $endpoint) { 2424 | if ($this->subscriptions[$endpoint] === false) { 2425 | //$this->subscriptions[$endpoint] = null; 2426 | $loop->stop(); 2427 | return; //return $ws->close(); 2428 | } 2429 | $json = json_decode($data, true); 2430 | $symbol = $json['s']; 2431 | if (intval($this->info[$symbol]['firstUpdate']) === 0) { 2432 | $this->depthQueue[$symbol][] = $json; 2433 | return; 2434 | } 2435 | $this->depthHandler($json); 2436 | call_user_func($callback, $this, $symbol, $this->depthCache[$symbol]); 2437 | }); 2438 | $ws->on('close', function ($code = null, $reason = null) use ($symbol, $loop) { 2439 | // WPCS: XSS OK. 2440 | echo "depthCache({$symbol}) WebSocket Connection closed! ({$code} - {$reason})" . PHP_EOL; 2441 | $loop->stop(); 2442 | }); 2443 | }, function ($e) use ($loop, $symbol) { 2444 | // WPCS: XSS OK. 2445 | echo "depthCache({$symbol})) Could not connect: {$e->getMessage()}" . PHP_EOL; 2446 | $loop->stop(); 2447 | }); 2448 | $this->depth($symbol, 100); 2449 | foreach ($this->depthQueue[$symbol] as $data) { 2450 | //TODO:: WTF ??? where is json and what should be in it ?? 2451 | $this->depthHandler($json); 2452 | } 2453 | $this->depthQueue[$symbol] = []; 2454 | call_user_func($callback, $this, $symbol, $this->depthCache[$symbol]); 2455 | } 2456 | $loop->run(); 2457 | } 2458 | 2459 | /** 2460 | * trades Trades WebSocket Endpoint 2461 | * 2462 | * $api->trades(["BNBBTC"], function($api, $symbol, $trades) { 2463 | * echo "{$symbol} trades update".PHP_EOL; 2464 | * print_r($trades); 2465 | * }); 2466 | * 2467 | * @param $symbols 2468 | * @param $callback callable closure 2469 | * @return null 2470 | */ 2471 | public function trades($symbols, callable $callback) 2472 | { 2473 | if (!is_array($symbols)) { 2474 | $symbols = [ 2475 | $symbols, 2476 | ]; 2477 | } 2478 | 2479 | $loop = \React\EventLoop\Factory::create(); 2480 | $react = new \React\Socket\Connector($loop); 2481 | $connector = new \Ratchet\Client\Connector($loop, $react); 2482 | foreach ($symbols as $symbol) { 2483 | if (!isset($this->info[$symbol])) { 2484 | $this->info[$symbol] = []; 2485 | } 2486 | 2487 | // $this->info[$symbol]['tradesCallback'] = $callback; 2488 | 2489 | $endpoint = strtolower($symbol) . '@trades'; 2490 | $this->subscriptions[$endpoint] = true; 2491 | 2492 | $connector($this->getWsEndpoint() . strtolower($symbol) . '@aggTrade')->then(function ($ws) use ($callback, $symbol, $loop, $endpoint) { 2493 | $ws->on('message', function ($data) use ($ws, $callback, $loop, $endpoint) { 2494 | if ($this->subscriptions[$endpoint] === false) { 2495 | //$this->subscriptions[$endpoint] = null; 2496 | $loop->stop(); 2497 | return; //return $ws->close(); 2498 | } 2499 | $json = json_decode($data, true); 2500 | $symbol = $json['s']; 2501 | $price = $json['p']; 2502 | $quantity = $json['q']; 2503 | $timestamp = $json['T']; 2504 | $maker = $json['m'] ? 'true' : 'false'; 2505 | $trades = [ 2506 | "price" => $price, 2507 | "quantity" => $quantity, 2508 | "timestamp" => $timestamp, 2509 | "maker" => $maker, 2510 | ]; 2511 | // $this->info[$symbol]['tradesCallback']($this, $symbol, $trades); 2512 | call_user_func($callback, $this, $symbol, $trades); 2513 | }); 2514 | $ws->on('close', function ($code = null, $reason = null) use ($symbol, $loop) { 2515 | // WPCS: XSS OK. 2516 | echo "trades({$symbol}) WebSocket Connection closed! ({$code} - {$reason})" . PHP_EOL; 2517 | $loop->stop(); 2518 | }); 2519 | }, function ($e) use ($loop, $symbol) { 2520 | // WPCS: XSS OK. 2521 | echo "trades({$symbol}) Could not connect: {$e->getMessage()}" . PHP_EOL; 2522 | $loop->stop(); 2523 | }); 2524 | } 2525 | $loop->run(); 2526 | } 2527 | 2528 | /** 2529 | * ticker pulls 24h price change statistics via WebSocket 2530 | * 2531 | * $api->ticker(false, function($api, $symbol, $ticker) { 2532 | * print_r($ticker); 2533 | * }); 2534 | * 2535 | * @param $symbol string optional symbol or false 2536 | * @param $callback callable closure 2537 | * @return null 2538 | */ 2539 | public function ticker($symbol, callable $callback) 2540 | { 2541 | $endpoint = $symbol ? strtolower($symbol) . '@ticker' : '!ticker@arr'; 2542 | $this->subscriptions[$endpoint] = true; 2543 | 2544 | // @codeCoverageIgnoreStart 2545 | // phpunit can't cover async function 2546 | \Ratchet\Client\connect($this->getWsEndpoint() . $endpoint)->then(function ($ws) use ($callback, $symbol, $endpoint) { 2547 | $ws->on('message', function ($data) use ($ws, $callback, $symbol, $endpoint) { 2548 | if ($this->subscriptions[$endpoint] === false) { 2549 | //$this->subscriptions[$endpoint] = null; 2550 | $ws->close(); 2551 | return; //return $ws->close(); 2552 | } 2553 | $json = json_decode($data); 2554 | if ($symbol) { 2555 | call_user_func($callback, $this, $symbol, $this->tickerStreamHandler($json)); 2556 | } else { 2557 | foreach ($json as $obj) { 2558 | $return = $this->tickerStreamHandler($obj); 2559 | $symbol = $return['symbol']; 2560 | call_user_func($callback, $this, $symbol, $return); 2561 | } 2562 | } 2563 | }); 2564 | $ws->on('close', function ($code = null, $reason = null) { 2565 | // WPCS: XSS OK. 2566 | echo "ticker: WebSocket Connection closed! ({$code} - {$reason})" . PHP_EOL; 2567 | }); 2568 | }, function ($e) { 2569 | // WPCS: XSS OK. 2570 | echo "ticker: Could not connect: {$e->getMessage()}" . PHP_EOL; 2571 | }); 2572 | // @codeCoverageIgnoreEnd 2573 | } 2574 | 2575 | /** 2576 | * chart Pulls /kline data and subscribes to @klines WebSocket endpoint 2577 | * 2578 | * $api->chart(["BNBBTC"], "15m", function($api, $symbol, $chart) { 2579 | * echo "{$symbol} chart update\n"; 2580 | * print_r($chart); 2581 | * }); 2582 | * 2583 | * @param $symbols string required symbols 2584 | * @param $interval string time inteval 2585 | * @param $callback callable closure 2586 | * @param $limit int default 500, maximum 1000 2587 | * @return null 2588 | * @throws \Exception 2589 | */ 2590 | public function chart($symbols, string $interval = "30m", callable $callback = null, $limit = 500) 2591 | { 2592 | if (is_null($callback)) { 2593 | throw new Exception("You must provide a valid callback"); 2594 | } 2595 | if (!is_array($symbols)) { 2596 | $symbols = [ 2597 | $symbols, 2598 | ]; 2599 | } 2600 | 2601 | $loop = \React\EventLoop\Factory::create(); 2602 | $react = new \React\Socket\Connector($loop); 2603 | $connector = new \Ratchet\Client\Connector($loop, $react); 2604 | foreach ($symbols as $symbol) { 2605 | if (!isset($this->charts[$symbol])) { 2606 | $this->charts[$symbol] = []; 2607 | } 2608 | 2609 | $this->charts[$symbol][$interval] = []; 2610 | if (!isset($this->info[$symbol])) { 2611 | $this->info[$symbol] = []; 2612 | } 2613 | 2614 | if (!isset($this->info[$symbol][$interval])) { 2615 | $this->info[$symbol][$interval] = []; 2616 | } 2617 | 2618 | if (!isset($this->chartQueue[$symbol])) { 2619 | $this->chartQueue[$symbol] = []; 2620 | } 2621 | 2622 | $this->chartQueue[$symbol][$interval] = []; 2623 | $this->info[$symbol][$interval]['firstOpen'] = 0; 2624 | $endpoint = strtolower($symbol) . '@kline_' . $interval; 2625 | $this->subscriptions[$endpoint] = true; 2626 | $connector($this->getWsEndpoint() . $endpoint)->then(function ($ws) use ($callback, $symbol, $loop, $endpoint, $interval) { 2627 | $ws->on('message', function ($data) use ($ws, $loop, $callback, $endpoint) { 2628 | if ($this->subscriptions[$endpoint] === false) { 2629 | //$this->subscriptions[$endpoint] = null; 2630 | $loop->stop(); 2631 | return; //return $ws->close(); 2632 | } 2633 | $json = json_decode($data); 2634 | $chart = $json->k; 2635 | $symbol = $json->s; 2636 | $interval = $chart->i; 2637 | $this->chartHandler($symbol, $interval, $json); 2638 | call_user_func($callback, $this, $symbol, $this->charts[$symbol][$interval]); 2639 | }); 2640 | $ws->on('close', function ($code = null, $reason = null) use ($symbol, $loop, $interval) { 2641 | // WPCS: XSS OK. 2642 | echo "chart({$symbol},{$interval}) WebSocket Connection closed! ({$code} - {$reason})" . PHP_EOL; 2643 | $loop->stop(); 2644 | }); 2645 | }, function ($e) use ($loop, $symbol, $interval) { 2646 | // WPCS: XSS OK. 2647 | echo "chart({$symbol},{$interval})) Could not connect: {$e->getMessage()}" . PHP_EOL; 2648 | $loop->stop(); 2649 | }); 2650 | $this->candlesticks($symbol, $interval, $limit); 2651 | foreach ($this->chartQueue[$symbol][$interval] as $json) { 2652 | $this->chartHandler($symbol, $interval, $json); 2653 | } 2654 | $this->chartQueue[$symbol][$interval] = []; 2655 | call_user_func($callback, $this, $symbol, $this->charts[$symbol][$interval]); 2656 | } 2657 | $loop->run(); 2658 | } 2659 | 2660 | /** 2661 | * kline Subscribes to @klines WebSocket endpoint for latest chart data only 2662 | * 2663 | * $api->kline(["BNBBTC"], "15m", function($api, $symbol, $chart) { 2664 | * echo "{$symbol} chart update\n"; 2665 | * print_r($chart); 2666 | * }); 2667 | * 2668 | * @param $symbols string required symbols 2669 | * @param $interval string time inteval 2670 | * @param $callback callable closure 2671 | * @return null 2672 | * @throws \Exception 2673 | */ 2674 | public function kline($symbols, string $interval = "30m", callable $callback = null) 2675 | { 2676 | if (is_null($callback)) { 2677 | throw new Exception("You must provide a valid callback"); 2678 | } 2679 | if (!is_array($symbols)) { 2680 | $symbols = [ 2681 | $symbols, 2682 | ]; 2683 | } 2684 | 2685 | $loop = \React\EventLoop\Factory::create(); 2686 | $react = new \React\Socket\Connector($loop); 2687 | $connector = new \Ratchet\Client\Connector($loop, $react); 2688 | foreach ($symbols as $symbol) { 2689 | $endpoint = strtolower($symbol) . '@kline_' . $interval; 2690 | $this->subscriptions[$endpoint] = true; 2691 | $connector($this->getWsEndpoint() . $endpoint)->then(function ($ws) use ($callback, $symbol, $loop, $endpoint, $interval) { 2692 | $ws->on('message', function ($data) use ($ws, $loop, $callback, $endpoint) { 2693 | if ($this->subscriptions[$endpoint] === false) { 2694 | $loop->stop(); 2695 | return; 2696 | } 2697 | $json = json_decode($data); 2698 | $chart = $json->k; 2699 | $symbol = $json->s; 2700 | $interval = $chart->i; 2701 | call_user_func($callback, $this, $symbol, $chart); 2702 | }); 2703 | $ws->on('close', function ($code = null, $reason = null) use ($symbol, $loop, $interval) { 2704 | // WPCS: XSS OK. 2705 | echo "kline({$symbol},{$interval}) WebSocket Connection closed! ({$code} - {$reason})" . PHP_EOL; 2706 | $loop->stop(); 2707 | }); 2708 | }, function ($e) use ($loop, $symbol, $interval) { 2709 | // WPCS: XSS OK. 2710 | echo "kline({$symbol},{$interval})) Could not connect: {$e->getMessage()}" . PHP_EOL; 2711 | $loop->stop(); 2712 | }); 2713 | } 2714 | $loop->run(); 2715 | } 2716 | 2717 | /** 2718 | * terminate Terminates websocket endpoints. View endpoints first: print_r($api->subscriptions) 2719 | * 2720 | * $api->terminate('ethbtc_kline@5m'); 2721 | * 2722 | * @return null 2723 | */ 2724 | public function terminate($endpoint) 2725 | { 2726 | // check if $this->subscriptions[$endpoint] is true otherwise error 2727 | $this->subscriptions[$endpoint] = false; 2728 | } 2729 | 2730 | /** 2731 | * keepAlive Keep-alive function for userDataStream 2732 | * 2733 | * $api->keepAlive(); 2734 | * 2735 | * @return null 2736 | */ 2737 | public function keepAlive() 2738 | { 2739 | $loop = \React\EventLoop\Factory::create(); 2740 | $loop->addPeriodicTimer(30, function () { 2741 | $listenKey = $this->listenKey; 2742 | $this->httpRequest("v1/userDataStream?listenKey={$listenKey}", "PUT", []); 2743 | }); 2744 | $loop->run(); 2745 | } 2746 | 2747 | /** 2748 | * userData Issues userDataStream token and keepalive, subscribes to userData WebSocket 2749 | * 2750 | * $balance_update = function($api, $balances) { 2751 | * print_r($balances); 2752 | * echo "Balance update".PHP_EOL; 2753 | * }; 2754 | * 2755 | * $order_update = function($api, $report) { 2756 | * echo "Order update".PHP_EOL; 2757 | * print_r($report); 2758 | * $price = $report['price']; 2759 | * $quantity = $report['quantity']; 2760 | * $symbol = $report['symbol']; 2761 | * $side = $report['side']; 2762 | * $orderType = $report['orderType']; 2763 | * $orderId = $report['orderId']; 2764 | * $orderStatus = $report['orderStatus']; 2765 | * $executionType = $report['orderStatus']; 2766 | * if( $executionType == "NEW" ) { 2767 | * if( $executionType == "REJECTED" ) { 2768 | * echo "Order Failed! Reason: {$report['rejectReason']}".PHP_EOL; 2769 | * } 2770 | * echo "{$symbol} {$side} {$orderType} ORDER #{$orderId} ({$orderStatus})".PHP_EOL; 2771 | * echo "..price: {$price}, quantity: {$quantity}".PHP_EOL; 2772 | * return; 2773 | * } 2774 | * 2775 | * //NEW, CANCELED, REPLACED, REJECTED, TRADE, EXPIRED 2776 | * echo "{$symbol} {$side} {$executionType} {$orderType} ORDER #{$orderId}".PHP_EOL; 2777 | * }; 2778 | * $api->userData($balance_update, $order_update); 2779 | * 2780 | * @param $balance_callback callable function 2781 | * @param bool $execution_callback callable function 2782 | * @return null 2783 | * @throws \Exception 2784 | */ 2785 | public function userData(&$balance_callback, &$execution_callback = false) 2786 | { 2787 | $response = $this->httpRequest("v1/userDataStream", "POST", []); 2788 | $this->listenKey = $response['listenKey']; 2789 | $this->info['balanceCallback'] = $balance_callback; 2790 | $this->info['executionCallback'] = $execution_callback; 2791 | 2792 | $this->subscriptions['@userdata'] = true; 2793 | 2794 | $loop = \React\EventLoop\Factory::create(); 2795 | $loop->addPeriodicTimer(30*60, function () { 2796 | $listenKey = $this->listenKey; 2797 | $this->httpRequest("v1/userDataStream?listenKey={$listenKey}", "PUT", []); 2798 | }); 2799 | $connector = new \Ratchet\Client\Connector($loop); 2800 | 2801 | // @codeCoverageIgnoreStart 2802 | // phpunit can't cover async function 2803 | $connector($this->getWsEndpoint() . $this->listenKey)->then(function ($ws) { 2804 | $ws->on('message', function ($data) use ($ws) { 2805 | if ($this->subscriptions['@userdata'] === false) { 2806 | //$this->subscriptions[$endpoint] = null; 2807 | $ws->close(); 2808 | return; //return $ws->close(); 2809 | } 2810 | $json = json_decode($data); 2811 | $type = $json->e; 2812 | if ($type === "outboundAccountPosition") { 2813 | $balances = $this->balanceHandler($json->B); 2814 | $this->info['balanceCallback']($this, $balances); 2815 | } elseif ($type === "executionReport") { 2816 | $report = $this->executionHandler($json); 2817 | if ($this->info['executionCallback']) { 2818 | $this->info['executionCallback']($this, $report); 2819 | } 2820 | } 2821 | }); 2822 | $ws->on('close', function ($code = null, $reason = null) { 2823 | // WPCS: XSS OK. 2824 | echo "userData: WebSocket Connection closed! ({$code} - {$reason})" . PHP_EOL; 2825 | }); 2826 | }, function ($e) { 2827 | // WPCS: XSS OK. 2828 | echo "userData: Could not connect: {$e->getMessage()}" . PHP_EOL; 2829 | }); 2830 | 2831 | $loop->run(); 2832 | } 2833 | 2834 | /** 2835 | * miniTicker Get miniTicker for all symbols 2836 | * 2837 | * $api->miniTicker(function($api, $ticker) { 2838 | * print_r($ticker); 2839 | * }); 2840 | * 2841 | * @param $callback callable function closer that takes 2 arguments, $pai and $ticker data 2842 | * @return null 2843 | */ 2844 | public function miniTicker(callable $callback) 2845 | { 2846 | $endpoint = '@miniticker'; 2847 | $this->subscriptions[$endpoint] = true; 2848 | 2849 | // @codeCoverageIgnoreStart 2850 | // phpunit can't cover async function 2851 | \Ratchet\Client\connect($this->getWsEndpoint() . '!miniTicker@arr')->then(function ($ws) use ($callback, $endpoint) { 2852 | $ws->on('message', function ($data) use ($ws, $callback, $endpoint) { 2853 | if ($this->subscriptions[$endpoint] === false) { 2854 | //$this->subscriptions[$endpoint] = null; 2855 | $ws->close(); 2856 | return; //return $ws->close(); 2857 | } 2858 | $json = json_decode($data, true); 2859 | $markets = []; 2860 | foreach ($json as $obj) { 2861 | $markets[] = [ 2862 | "symbol" => $obj['s'], 2863 | "close" => $obj['c'], 2864 | "open" => $obj['o'], 2865 | "high" => $obj['h'], 2866 | "low" => $obj['l'], 2867 | "volume" => $obj['v'], 2868 | "quoteVolume" => $obj['q'], 2869 | "eventTime" => $obj['E'], 2870 | ]; 2871 | } 2872 | call_user_func($callback, $this, $markets); 2873 | }); 2874 | $ws->on('close', function ($code = null, $reason = null) { 2875 | // WPCS: XSS OK. 2876 | echo "miniticker: WebSocket Connection closed! ({$code} - {$reason})" . PHP_EOL; 2877 | }); 2878 | }, function ($e) { 2879 | // WPCS: XSS OK. 2880 | echo "miniticker: Could not connect: {$e->getMessage()}" . PHP_EOL; 2881 | }); 2882 | // @codeCoverageIgnoreEnd 2883 | } 2884 | 2885 | /** 2886 | * bookTicker Get bookTicker for all symbols 2887 | * 2888 | * $api->bookTicker(function($api, $ticker) { 2889 | * print_r($ticker); 2890 | * }); 2891 | * 2892 | * @param $callback callable function closer that takes 2 arguments, $api and $ticker data 2893 | * @return null 2894 | */ 2895 | public function bookTicker(callable $callback) 2896 | { 2897 | $endpoint = '!bookticker'; 2898 | $this->subscriptions[$endpoint] = true; 2899 | 2900 | // @codeCoverageIgnoreStart 2901 | // phpunit can't cover async function 2902 | \Ratchet\Client\connect($this->getWsEndpoint() . '!bookTicker')->then(function ($ws) use ($callback, $endpoint) { 2903 | $ws->on('message', function ($data) use ($ws, $callback, $endpoint) { 2904 | if ($this->subscriptions[$endpoint] === false) { 2905 | //$this->subscriptions[$endpoint] = null; 2906 | $ws->close(); 2907 | return; //return $ws->close(); 2908 | } 2909 | $json = json_decode($data, true); 2910 | 2911 | $markets = [ 2912 | "updateId" => $json['u'], 2913 | "symbol" => $json['s'], 2914 | "bid_price" => $json['b'], 2915 | "bid_qty" => $json['B'], 2916 | "ask_price" => $json['a'], 2917 | "ask_qty" => $json['A'], 2918 | ]; 2919 | call_user_func($callback, $this, $markets); 2920 | }); 2921 | $ws->on('close', function ($code = null, $reason = null) { 2922 | // WPCS: XSS OK. 2923 | echo "miniticker: WebSocket Connection closed! ({$code} - {$reason})" . PHP_EOL; 2924 | }); 2925 | }, function ($e) { 2926 | // WPCS: XSS OK. 2927 | echo "miniticker: Could not connect: {$e->getMessage()}" . PHP_EOL; 2928 | }); 2929 | // @codeCoverageIgnoreEnd 2930 | } 2931 | 2932 | /** 2933 | * Due to ongoing issues with out of date wamp CA bundles 2934 | * This function downloads ca bundle for curl website 2935 | * and uses it as part of the curl options 2936 | */ 2937 | protected function downloadCurlCaBundle() 2938 | { 2939 | $output_filename = getcwd() . "/ca.pem"; 2940 | 2941 | if (is_writable(getcwd()) === false) { 2942 | die(getcwd() . ' folder is not writeable, please check your permissions to download CA Certificates, or use $api->caOverride = true;'); 2943 | } 2944 | 2945 | $host = "https://curl.se/ca/cacert.pem"; 2946 | $curl = curl_init(); 2947 | curl_setopt($curl, CURLOPT_URL, $host); 2948 | curl_setopt($curl, CURLOPT_VERBOSE, 0); 2949 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); 2950 | curl_setopt($curl, CURLOPT_HEADER, 0); 2951 | curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0); 2952 | curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); 2953 | 2954 | // proxy settings 2955 | if (is_array($this->proxyConf)) { 2956 | curl_setopt($curl, CURLOPT_PROXY, $this->getProxyUriString()); 2957 | if (isset($this->proxyConf['user']) && isset($this->proxyConf['pass'])) { 2958 | curl_setopt($curl, CURLOPT_PROXYUSERPWD, $this->proxyConf['user'] . ':' . $this->proxyConf['pass']); 2959 | } 2960 | } 2961 | 2962 | $result = curl_exec($curl); 2963 | curl_close($curl); 2964 | 2965 | if ($result === false) { 2966 | echo "Unable to to download the CA bundle $host" . PHP_EOL; 2967 | return; 2968 | } 2969 | 2970 | $fp = fopen($output_filename, 'w'); 2971 | 2972 | if ($fp === false) { 2973 | echo "Unable to write $output_filename, please check permissions on folder" . PHP_EOL; 2974 | return; 2975 | } 2976 | 2977 | fwrite($fp, $result); 2978 | fclose($fp); 2979 | } 2980 | 2981 | protected function floorDecimal($n, $decimals=2) 2982 | { 2983 | return floor($n * pow(10, $decimals)) / pow(10, $decimals); 2984 | } 2985 | 2986 | 2987 | protected function setXMbxUsedWeight(int $usedWeight) : void 2988 | { 2989 | $this->xMbxUsedWeight = $usedWeight; 2990 | } 2991 | 2992 | protected function setXMbxUsedWeight1m(int $usedWeight1m) : void 2993 | { 2994 | $this->xMbxUsedWeight1m = $usedWeight1m; 2995 | } 2996 | 2997 | public function getXMbxUsedWeight() : int 2998 | { 2999 | $this->xMbxUsedWeight; 3000 | } 3001 | 3002 | public function getXMbxUsedWeight1m() : int 3003 | { 3004 | $this->xMbxUsedWeight1m; 3005 | } 3006 | 3007 | private function getRestEndpoint() : string 3008 | { 3009 | return $this->useTestnet ? $this->baseTestnet : $this->base; 3010 | } 3011 | 3012 | private function getWsEndpoint() : string 3013 | { 3014 | return $this->useTestnet ? $this->streamTestnet : $this->stream; 3015 | } 3016 | 3017 | public function isOnTestnet() : bool 3018 | { 3019 | return $this->useTestnet; 3020 | } 3021 | 3022 | /** 3023 | * systemStatus - Status indicator for api sapi 3024 | * 3025 | * @link https://binance-docs.github.io/apidocs/spot/en/#test-connectivity 3026 | * @link https://binance-docs.github.io/apidocs/spot/en/#system-status-system 3027 | * 3028 | * @property int $weight 2 3029 | * 3030 | * @return array containing the response 3031 | * @throws \Exception 3032 | */ 3033 | public function systemStatus() 3034 | { 3035 | $arr = array(); 3036 | $api_status = $this->httpRequest("v3/ping", 'GET'); 3037 | if ( empty($api_status) ) { 3038 | $arr['api']['status'] = 'ping ok'; 3039 | } else { 3040 | $arr['api']['status'] = $api_status; 3041 | } 3042 | 3043 | $arr['sapi'] = $this->httpRequest("v1/system/status", 'GET', [ 'sapi' => true ], true); 3044 | return $arr; 3045 | } 3046 | 3047 | /** 3048 | * accountSnapshot - Daily Account Snapshot at 00:00:00 UTC 3049 | * 3050 | * @link https://binance-docs.github.io/apidocs/spot/en/#daily-account-snapshot-user_data 3051 | * 3052 | * @property int $weight 1 3053 | * 3054 | * @param string $type (mandatory) Should be SPOT, MARGIN or FUTURES 3055 | * @param int $nbrDays (optional) Number of days. Default 5, min 5, max 30 3056 | * @param long $startTime (optional) Start time, e.g. 1617580799000 3057 | * @param long $endTime (optional) End time, e.g. 1617667199000 3058 | * 3059 | * @return array containing the response 3060 | * @throws \Exception 3061 | */ 3062 | public function accountSnapshot($type, $nbrDays = 5, $startTime = 0, $endTime = 0) 3063 | { 3064 | if ($nbrDays < 5 || $nbrDays > 30) 3065 | $nbrDays = 5; 3066 | 3067 | $params = [ 3068 | 'sapi' => true, 3069 | 'type' => $type, 3070 | ]; 3071 | 3072 | if ($startTime > 0) 3073 | $params['startTime'] = $startTime; 3074 | if ($endTime > 0) 3075 | $params['endTime'] = $startTime; 3076 | if ($nbrDays != 5) 3077 | $params['limit'] = $nbrDays; 3078 | 3079 | return $this->httpRequest("v1/accountSnapshot", 'GET', $params, true); 3080 | } 3081 | 3082 | /** 3083 | * accountStatus - Fetch account status detail. 3084 | * 3085 | * @link https://binance-docs.github.io/apidocs/spot/en/#account-status-user_data 3086 | * 3087 | * @property int $weight 1 3088 | * 3089 | * @return array containing the response 3090 | * @throws \Exception 3091 | */ 3092 | public function accountStatus() 3093 | { 3094 | $arr = array(); 3095 | $arr['sapi'] = $this->httpRequest("v1/account/status", 'GET', [ 'sapi' => true ], true); 3096 | return $arr; 3097 | } 3098 | 3099 | /** 3100 | * apiTradingStatus - Fetch account API trading status detail. 3101 | * 3102 | * @link https://binance-docs.github.io/apidocs/spot/en/#account-api-trading-status-user_data 3103 | * 3104 | * @property int $weight 1 3105 | * 3106 | * @return array containing the response 3107 | * @throws \Exception 3108 | */ 3109 | public function apiTradingStatus() 3110 | { 3111 | $arr = array(); 3112 | $arr['sapi'] = $this->httpRequest("v1/account/apiTradingStatus", 'GET', [ 'sapi' => true ], true); 3113 | return $arr; 3114 | } 3115 | } 3116 | --------------------------------------------------------------------------------