Connector Open-Source Code
Browse connector files locally. Exchange API keys stay on your device.
exchanges/bitstamp.php
<?php
// /opt/nuxvision_connector/exchanges/bitstamp.php
declare(strict_types=1);
/* =========================================================
Bitstamp Spot adapter (REST v2)
- Wallet: POST /api/v2/balance/
- Orders:
POST /api/v2/buy/{pair}/
POST /api/v2/sell/{pair}/
POST /api/v2/order_status/
POST /api/v2/cancel_order/
- Contract normalized to connector internal shape:
{ ok, code, err, raw, json:{error,result,...} }
========================================================= */
function _bs_with_base_url(array $cfg): array {
$base = trim((string)($cfg['base_url'] ?? ''));
if ($base === '') $base = 'https://www.bitstamp.net';
$cfg['base_url'] = rtrim($base, '/');
return $cfg;
}
function _bs_uuid4(): string {
$b = random_bytes(16);
$b[6] = chr((ord($b[6]) & 0x0f) | 0x40);
$b[8] = chr((ord($b[8]) & 0x3f) | 0x80);
$h = bin2hex($b);
return substr($h, 0, 8) . '-' . substr($h, 8, 4) . '-' . substr($h, 12, 4) . '-' . substr($h, 16, 4) . '-' . substr($h, 20);
}
function _bs_symbol_to_exchange(string $sym): ?string {
$s = strtoupper(trim($sym));
if ($s === '') return null;
if (str_contains($s, '_')) {
$p = explode('_', $s);
if (count($p) !== 2 || $p[0] === '' || $p[1] === '') return null;
return strtolower($p[0] . $p[1]);
}
return strtolower($s);
}
function _bs_symbol_to_internal(string $sym): ?string {
$s = strtoupper(trim($sym));
if ($s === '') return null;
if (str_contains($s, '_')) {
$p = explode('_', $s);
if (count($p) !== 2 || $p[0] === '' || $p[1] === '') return null;
return $p[0] . '_' . $p[1];
}
$quotes = ['USDT','USDC','USD','EUR','GBP','BTC','ETH'];
foreach ($quotes as $q) {
if (str_ends_with($s, $q) && strlen($s) > strlen($q)) {
$base = substr($s, 0, -strlen($q));
if ($base !== '') return $base . '_' . $q;
}
}
return null;
}
function _bs_to_num_str(float $v, int $decimals = 8, string $mode = 'round'): string {
if ($decimals < 0) $decimals = 0;
$factor = 10 ** $decimals;
if ($mode === 'floor') $v = floor(($v + 1e-15) * $factor) / $factor;
elseif ($mode === 'ceil') $v = ceil(($v - 1e-15) * $factor) / $factor;
else $v = round($v, $decimals);
$s = rtrim(rtrim(sprintf('%.16F', $v), '0'), '.');
return ($s === '' ? '0' : $s);
}
function _bs_request(array $cfg, string $method, string $path, array $payload = []): array {
$cfg = _bs_with_base_url($cfg);
$methodUp = strtoupper($method);
$baseUrl = (string)$cfg['base_url'];
$apiKey = trim((string)($cfg['api_key'] ?? ''));
$apiSecret = trim((string)($cfg['api_secret'] ?? ''));
$host = parse_url($baseUrl, PHP_URL_HOST);
if (!is_string($host) || $host === '') $host = 'www.bitstamp.net';
$contentType = 'application/x-www-form-urlencoded';
$nonce = _bs_uuid4();
$timestamp = (string)((int)round(microtime(true) * 1000));
$version = 'v2';
$body = http_build_query($payload, '', '&', PHP_QUERY_RFC3986);
$message = 'BITSTAMP ' . $apiKey . $methodUp . $host . $path . '' . $contentType . $nonce . $timestamp . $version . $body;
$signature = strtoupper(hash_hmac('sha256', $message, $apiSecret));
$headers = [
'Accept: application/json',
'Content-Type: ' . $contentType,
'X-Auth: BITSTAMP ' . $apiKey,
'X-Auth-Signature: ' . $signature,
'X-Auth-Nonce: ' . $nonce,
'X-Auth-Timestamp: ' . $timestamp,
'X-Auth-Version: ' . $version,
];
$url = $baseUrl . $path;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
curl_setopt($ch, CURLOPT_TIMEOUT, 12);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
if ($methodUp === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} else {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $methodUp);
}
$respBody = curl_exec($ch);
$err = curl_error($ch);
$code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$json = null;
if (is_string($respBody) && $respBody !== '') {
$tmp = json_decode($respBody, true);
if (is_array($tmp) || is_bool($tmp)) $json = $tmp;
}
return [
'ok' => ($err === '' && $code >= 200 && $code < 300),
'code' => $code,
'err' => ($err !== '' ? $err : null),
'raw' => $respBody,
'json' => $json,
];
}
function _bs_normalize_error_payload(array $r, string $fallbackMsg): array {
$j = is_array($r['json'] ?? null) ? $r['json'] : [];
$code = -1;
if (isset($j['code']) && is_numeric($j['code'])) $code = (int)$j['code'];
elseif (isset($r['code']) && is_numeric($r['code']) && (int)$r['code'] > 0) $code = (int)$r['code'];
$msg = (string)($j['reason'] ?? $j['message'] ?? $j['error'] ?? $r['err'] ?? $fallbackMsg);
$r['json'] = [
'error' => $code,
'code' => $code,
'message' => $msg,
'result' => [],
];
return $r;
}
function _bs_wallet(array $cfg): array {
$r = _bs_request($cfg, 'POST', '/api/v2/balance/', []);
$j = is_array($r['json'] ?? null) ? $r['json'] : [];
if (empty($r['ok']) || empty($j) || isset($j['status']) || isset($j['error'])) {
return _bs_normalize_error_payload($r, 'wallet_failed');
}
$out = [];
foreach ($j as $k => $v) {
if (!is_string($k) || !is_numeric($v)) continue;
if (!preg_match('/^([a-z0-9]+)_(available|balance|reserved)$/i', $k, $m)) continue;
$asset = strtoupper((string)$m[1]);
$field = strtolower((string)$m[2]);
if (!isset($out[$asset])) $out[$asset] = ['available' => 0.0, 'reserved' => 0.0];
if ($field === 'available') $out[$asset]['available'] = (float)$v;
elseif ($field === 'reserved') $out[$asset]['reserved'] = (float)$v;
elseif ($field === 'balance' && !isset($j[strtolower($asset) . '_reserved'])) {
$out[$asset]['available'] = (float)$v;
}
}
$r['json'] = [
'error' => 0,
'result' => $out,
];
return $r;
}
function _bs_place_buy(array $cfg, string $symbolEx, float $amountQuote, float $price): array {
if ($amountQuote <= 0 || $price <= 0) {
return [
'ok' => false, 'code' => 0, 'err' => 'invalid_order_inputs', 'raw' => null,
'json' => ['error' => -1, 'code' => -1, 'message' => 'invalid_order_inputs', 'result' => []],
];
}
$qtyBase = $amountQuote / $price;
$payload = [
'amount' => _bs_to_num_str($qtyBase, 8, 'floor'),
'price' => _bs_to_num_str($price, 8, 'round'),
];
$r = _bs_request($cfg, 'POST', '/api/v2/buy/' . $symbolEx . '/', $payload);
$j = is_array($r['json'] ?? null) ? $r['json'] : [];
if (!empty($r['ok']) && isset($j['id'])) {
$r['json'] = [
'error' => 0,
'result' => [
'id' => (string)$j['id'],
'order' => $j,
],
];
return $r;
}
return _bs_normalize_error_payload($r, 'place_buy_failed');
}
function _bs_place_buy_market(array $cfg, string $symbolEx, float $amountQuote): array {
if ($amountQuote <= 0) {
return [
'ok' => false, 'code' => 0, 'err' => 'invalid_order_inputs', 'raw' => null,
'json' => ['error' => -1, 'code' => -1, 'message' => 'invalid_order_inputs', 'result' => []],
];
}
$payload = [
// Bitstamp market buy endpoint accepts amount payload.
'amount' => _bs_to_num_str($amountQuote, 8, 'round'),
];
$r = _bs_request($cfg, 'POST', '/api/v2/buy/market/' . $symbolEx . '/', $payload);
$j = is_array($r['json'] ?? null) ? $r['json'] : [];
if (!empty($r['ok']) && isset($j['id'])) {
$r['json'] = [
'error' => 0,
'result' => [
'id' => (string)$j['id'],
'order' => $j,
],
];
return $r;
}
return _bs_normalize_error_payload($r, 'place_market_buy_failed');
}
function _bs_place_sell(array $cfg, string $symbolEx, float $qtyBase, float $price): array {
if ($qtyBase <= 0 || $price <= 0) {
return [
'ok' => false, 'code' => 0, 'err' => 'invalid_order_inputs', 'raw' => null,
'json' => ['error' => -1, 'code' => -1, 'message' => 'invalid_order_inputs', 'result' => []],
];
}
$payload = [
'amount' => _bs_to_num_str($qtyBase, 8, 'floor'),
'price' => _bs_to_num_str($price, 8, 'round'),
];
$r = _bs_request($cfg, 'POST', '/api/v2/sell/' . $symbolEx . '/', $payload);
$j = is_array($r['json'] ?? null) ? $r['json'] : [];
if (!empty($r['ok']) && isset($j['id'])) {
$r['json'] = [
'error' => 0,
'result' => [
'id' => (string)$j['id'],
'order' => $j,
],
];
return $r;
}
return _bs_normalize_error_payload($r, 'place_sell_failed');
}
function _bs_order_info(array $cfg, string $symbolEx, string $orderId, string $side): array {
$payload = [
'id' => $orderId,
'omit_transactions' => 'false',
];
$r = _bs_request($cfg, 'POST', '/api/v2/order_status/', $payload);
$j = is_array($r['json'] ?? null) ? $r['json'] : [];
if (!empty($r['ok']) && !empty($j) && !isset($j['status']) && !isset($j['error'])) {
$j['_symbol'] = $symbolEx;
$j['_side'] = strtolower($side);
$r['json'] = [
'error' => 0,
'result' => $j,
];
return $r;
}
return _bs_normalize_error_payload($r, 'order_info_failed');
}
function _bs_open_orders(array $cfg, array $orders = []): array {
$r = _bs_request($cfg, 'POST', '/api/v2/open_orders/all/', []);
$j = $r['json'] ?? null;
if (!empty($r['ok']) && is_array($j)) {
$isList = ($j === [] || array_keys($j) === range(0, count($j) - 1));
if ($isList) {
$byId = [];
foreach ($j as $row) {
if (!is_array($row)) continue;
$id = $row['id'] ?? null;
if ($id === null || $id === '') continue;
$id = (string)$id;
$amount = is_numeric($row['amount'] ?? null) ? (float)$row['amount'] : 0.0;
$remaining = is_numeric($row['amount_remaining'] ?? null) ? (float)$row['amount_remaining'] : $amount;
if ($remaining < 0) $remaining = 0.0;
$row['status'] = (string)($row['status'] ?? 'open');
$row['amount'] = $amount;
$row['amount_remaining'] = $remaining;
$byId[$id] = $row;
}
$r['json'] = [
'error' => 0,
'result' => [
'orders_by_id' => $byId,
],
];
return $r;
}
}
return _bs_normalize_error_payload($r, 'open_orders_failed');
}
function _bs_cancel_order(array $cfg, string $orderId): array {
$r = _bs_request($cfg, 'POST', '/api/v2/cancel_order/', ['id' => $orderId]);
$j = $r['json'] ?? null;
if (!empty($r['ok']) && (is_bool($j) || is_array($j))) {
// Bitstamp may return boolean true on success.
$r['json'] = [
'error' => 0,
'result' => is_array($j) ? $j : ['cancelled' => (bool)$j],
];
return $r;
}
return _bs_normalize_error_payload($r, 'cancel_order_failed');
}
function _bs_status_to_nv(array $result): string {
$status = strtolower(trim((string)($result['status'] ?? '')));
$amount = is_numeric($result['amount'] ?? null) ? (float)$result['amount'] : 0.0;
$remaining = is_numeric($result['amount_remaining'] ?? null) ? (float)$result['amount_remaining'] : 0.0;
$filled = max(0.0, $amount - $remaining);
if ($status === 'finished' || $status === 'filled') return 'completed';
if ($status === 'canceled' || $status === 'cancelled') {
return ($filled > 0.0) ? 'partial' : 'cancelled';
}
if ($status === 'open' || $status === 'in_queue' || $status === 'in queue') {
return ($filled > 0.0 && $remaining > 0.0) ? 'partial' : 'pending';
}
if ($filled > 0.0 && $remaining > 0.0) return 'partial';
if ($filled > 0.0 && $remaining <= 0.0) return 'completed';
return 'pending';
}
function _bs_extract_fill(array $result): array {
$symInternal = _bs_symbol_to_internal((string)($result['_symbol'] ?? ''));
$base = '';
$quote = '';
if ($symInternal !== null) {
$p = explode('_', $symInternal);
$base = strtolower((string)($p[0] ?? ''));
$quote = strtolower((string)($p[1] ?? ''));
}
$qty = 0.0;
$quoteAbs = 0.0;
$fee = 0.0;
$txs = $result['transactions'] ?? [];
if (is_array($txs) && !empty($txs)) {
foreach ($txs as $tx) {
if (!is_array($tx)) continue;
$baseAmt = ($base !== '' && isset($tx[$base]) && is_numeric($tx[$base])) ? (float)$tx[$base] : null;
$quoteAmt = ($quote !== '' && isset($tx[$quote]) && is_numeric($tx[$quote])) ? (float)$tx[$quote] : null;
$price = isset($tx['price']) && is_numeric($tx['price']) ? (float)$tx['price'] : 0.0;
$feeTx = isset($tx['fee']) && is_numeric($tx['fee']) ? (float)$tx['fee'] : 0.0;
if ($baseAmt !== null) $qty += abs($baseAmt);
if ($quoteAmt !== null) $quoteAbs += abs($quoteAmt);
elseif ($baseAmt !== null && $price > 0) $quoteAbs += abs($baseAmt) * $price;
$fee += abs($feeTx);
}
} else {
$amount = is_numeric($result['amount'] ?? null) ? (float)$result['amount'] : 0.0;
$remaining = is_numeric($result['amount_remaining'] ?? null) ? (float)$result['amount_remaining'] : 0.0;
$price = is_numeric($result['price'] ?? null) ? (float)$result['price'] : 0.0;
$qty = max(0.0, $amount - $remaining);
$quoteAbs = ($price > 0 && $qty > 0) ? ($qty * $price) : 0.0;
if (isset($result['fee']) && is_numeric($result['fee'])) $fee = abs((float)$result['fee']);
}
$avg = ($qty > 0.0) ? ($quoteAbs / $qty) : 0.0;
return [
'qty' => $qty,
'quote_abs' => $quoteAbs,
'fee' => $fee,
'avg' => $avg,
];
}
return [
'normalize_symbol' => function (string $sym): ?string {
return _bs_symbol_to_exchange($sym);
},
'wallet' => fn(array $cfg) => _bs_wallet($cfg),
'place_buy' => function (array $cfg, string $symbol, float $amountQuote, float $price): array {
$symbolEx = _bs_symbol_to_exchange($symbol);
if ($symbolEx === null) {
return [
'ok' => false, 'code' => 0, 'err' => 'invalid_symbol', 'raw' => null,
'json' => ['error' => -1, 'code' => -1, 'message' => 'invalid_symbol', 'result' => []],
];
}
return _bs_place_buy($cfg, $symbolEx, $amountQuote, $price);
},
'place_buy_market' => function (array $cfg, string $symbol, float $amountQuote): array {
$symbolEx = _bs_symbol_to_exchange($symbol);
if ($symbolEx === null) {
return [
'ok' => false, 'code' => 0, 'err' => 'invalid_symbol', 'raw' => null,
'json' => ['error' => -1, 'code' => -1, 'message' => 'invalid_symbol', 'result' => []],
];
}
return _bs_place_buy_market($cfg, $symbolEx, $amountQuote);
},
'place_sell' => function (array $cfg, string $symbol, float $qtyBase, float $price): array {
$symbolEx = _bs_symbol_to_exchange($symbol);
if ($symbolEx === null) {
return [
'ok' => false, 'code' => 0, 'err' => 'invalid_symbol', 'raw' => null,
'json' => ['error' => -1, 'code' => -1, 'message' => 'invalid_symbol', 'result' => []],
];
}
return _bs_place_sell($cfg, $symbolEx, $qtyBase, $price);
},
'order_info' => function (array $cfg, string $symbol, string $exchangeOrderId, string $side): array {
$symbolEx = _bs_symbol_to_exchange($symbol);
if ($symbolEx === null) $symbolEx = strtolower(trim($symbol));
return _bs_order_info($cfg, $symbolEx, $exchangeOrderId, $side);
},
'open_orders' => fn(array $cfg, array $orders = []) =>
_bs_open_orders($cfg, $orders),
'cancel_order' => fn(array $cfg, string $symbol, string $exchangeOrderId, string $side) =>
_bs_cancel_order($cfg, $exchangeOrderId),
'map_status' => function (array $result): string {
return _bs_status_to_nv($result);
},
'calc_buy_fill' => function (array $result): array {
$f = _bs_extract_fill($result);
return [
'fee' => $f['fee'],
'quantity' => $f['qty'],
'buy_price' => $f['avg'],
'price' => $f['avg'],
'purchase_value' => $f['quote_abs'] + $f['fee'],
];
},
'calc_sell_fill' => function (array $result): array {
$f = _bs_extract_fill($result);
return [
'fee' => $f['fee'],
'quantity_sold' => $f['qty'],
'sell_price' => $f['avg'],
'price' => $f['avg'],
];
},
];