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'],
        ];
    },
];