Connector Open-Source Code
Browse connector files locally. Exchange API keys stay on your device.
exchanges/simulation.php
<?php
// /opt/nuxvision_connector/exchanges/simulation.php
declare(strict_types=1);
/* =========================================================
Simulation exchange adapter
- No real exchange calls
- Uses NuxVision API tickers.php to get last price
- exchange_order_id encodes order params so order_info() can work stateless
LIMITATION: cancel is stateless (order_info can't "remember" cancel)
========================================================= */
function _sim_clean_float(float $v, int $decimals = 8, string $mode = 'round'): float
{
if ($decimals < 0) $decimals = 0;
$factor = 10 ** $decimals;
if ($mode === 'floor') return floor(($v + 1e-15) * $factor) / $factor;
if ($mode === 'ceil') return ceil(($v - 1e-15) * $factor) / $factor;
return round($v, $decimals);
}
function _sim_cfg(array $cfg): array
{
// NV API base (where /api/v1/tickers.php lives)
if (empty($cfg['nv_base_url'])) $cfg['nv_base_url'] = 'https://nuxvision.com/api/v1';
// NV API key (required to call tickers.php)
if (empty($cfg['nv_api_key']) && !empty($cfg['api_key'])) {
// fallback only if you intentionally pass NV key as api_key
$cfg['nv_api_key'] = $cfg['api_key'];
}
// Exchange used by NV tickers.php (bitkub|bitstamp|binance)
if (empty($cfg['exchange'])) $cfg['exchange'] = 'bitkub';
// timeouts
if (!isset($cfg['timeout']) || !is_numeric($cfg['timeout'])) $cfg['timeout'] = 8;
$cfg['timeout'] = max(2, min(30, (int)$cfg['timeout']));
return $cfg;
}
function _sim_nv_tickers(array $cfg, array $symbols): array
{
$cfg = _sim_cfg($cfg);
$exchange = strtolower(trim((string)$cfg['exchange']));
$symbols = array_values(array_unique(array_filter($symbols, fn($s) => is_string($s) && trim($s) !== '')));
if (empty($symbols)) {
return [
'ok' => true,
'code' => 200,
'err' => null,
'raw' => '',
'json' => [
'ok' => true,
'data' => [],
],
];
}
$baseUrl = rtrim((string)$cfg['nv_base_url'], '/');
$headers = ['Accept: application/json'];
$apiKey = (string)($cfg['nv_api_key'] ?? '');
if ($apiKey !== '') {
$headers[] = 'X-API-KEY: ' . $apiKey;
}
$timeout = (int)$cfg['timeout'];
$chunkSize = 500;
$chunks = array_chunk($symbols, $chunkSize);
$mergedData = [];
$rawParts = [];
$lastCode = 200;
$firstErr = null;
$okChunks = 0;
foreach ($chunks as $chunk) {
$url = $baseUrl . '/tickers.php?' . http_build_query([
'exchange' => $exchange,
'symbols' => implode(',', $chunk),
]);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, min(3, $timeout));
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
$body = curl_exec($ch);
$err = curl_error($ch);
$code = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$lastCode = $code;
if (is_string($body) && $body !== '') $rawParts[] = $body;
$json = null;
if (is_string($body) && $body !== '') {
$tmp = json_decode($body, true);
if (is_array($tmp)) $json = $tmp;
}
$chunkOk = (
$err === '' &&
$code >= 200 && $code < 300 &&
is_array($json) &&
!empty($json['ok']) &&
is_array($json['data'] ?? null)
);
if ($chunkOk) {
$okChunks++;
$mergedData = array_replace($mergedData, (array)$json['data']);
continue;
}
if ($firstErr === null) {
if ($err !== '') $firstErr = $err;
elseif (is_array($json) && isset($json['message']) && is_string($json['message'])) $firstErr = $json['message'];
elseif ($code > 0) $firstErr = 'http_' . $code;
else $firstErr = 'tickers_failed';
}
}
$allOk = ($okChunks === count($chunks));
$someOk = ($okChunks > 0);
return [
'ok' => $someOk,
'code' => $someOk ? 200 : $lastCode,
'err' => $allOk ? null : ($firstErr ?? 'tickers_partial_failed'),
'raw' => implode("\n", $rawParts),
'json' => [
'ok' => $someOk,
'data' => $mergedData,
],
];
}
function _sim_get_last(array $cfg, string $symbol): ?float
{
$symbol = trim($symbol);
if ($symbol === '') return null;
$r = _sim_nv_tickers($cfg, [$symbol]);
if (!$r['ok']) return null;
$j = $r['json'] ?? null;
if (!is_array($j) || empty($j['ok'])) return null;
$data = $j['data'] ?? null;
if (!is_array($data)) return null;
$row = $data[$symbol] ?? null;
if (!is_array($row)) return null;
$last = $row['last'] ?? null;
if ($last === null || !is_numeric($last)) return null;
$f = (float)$last;
return ($f > 0) ? $f : null;
}
/**
* Stateless SIM order id that encodes order params.
* Format:
* SIM|<side>|<symbol>|<qty>|<limit>|<ms>|<rand>
*/
function _sim_make_order_id(string $side, string $symbol, float $qty, float $limit): string
{
$side = strtolower(trim($side));
if ($side !== 'buy' && $side !== 'sell') $side = 'buy';
$symbol = trim((string)$symbol);
if ($symbol === '') $symbol = 'UNKNOWN';
// keep symbol as-is because bitstamp symbols can be lowercase like "xrpeur"
$qty = _sim_clean_float($qty, 8, 'floor');
$limit = _sim_clean_float($limit, 8, 'round');
$ms = (int) round(microtime(true) * 1000);
$rand = bin2hex(random_bytes(4));
$qtyS = rtrim(rtrim(sprintf('%.8f', $qty), '0'), '.');
$limS = rtrim(rtrim(sprintf('%.8f', $limit), '0'), '.');
return 'SIM|' . $side . '|' . $symbol . '|' . $qtyS . '|' . $limS . '|' . $ms . '|' . $rand;
}
function _sim_parse_order_id(string $orderId): ?array
{
$parts = explode('|', $orderId);
if (count($parts) < 7) return null;
if ($parts[0] !== 'SIM') return null;
$side = strtolower(trim((string)$parts[1]));
$symbol = trim((string)$parts[2]);
$qty = (float)($parts[3] ?? 0);
$limit = (float)($parts[4] ?? 0);
if ($side !== 'buy' && $side !== 'sell') return null;
if ($symbol === '') return null;
if ($qty <= 0 || $limit <= 0) return null;
return [
'side' => $side,
'symbol' => $symbol,
'qty' => $qty,
'limit' => $limit,
];
}
function _sim_wallet(array $cfg): array
{
return [
'ok' => true,
'code' => 200,
'err' => null,
'raw' => '',
'json' => [
'error' => 0,
'result' => [],
],
];
}
function _sim_place_buy(array $cfg, string $symbol, float $amountQuote, float $price): array
{
$price = _sim_clean_float($price, 8, 'round');
$amountQuote = _sim_clean_float($amountQuote, 8, 'round');
$qty = ($price > 0) ? ($amountQuote / $price) : 0.0;
$qty = _sim_clean_float($qty, 8, 'floor');
if ($qty <= 0) $qty = 0.00000001;
$oid = _sim_make_order_id('buy', $symbol, $qty, $price);
return [
'ok' => true,
'code' => 200,
'err' => null,
'raw' => '',
'json' => [
'error' => 0,
'result' => [
'id' => $oid,
'side' => 'buy',
'sym' => $symbol,
'amt' => $amountQuote, // quote amount (like Bitkub 'amt' in quote)
'rat' => $price,
'typ' => 'limit',
],
],
];
}
function _sim_place_sell(array $cfg, string $symbol, float $qtyBase, float $price): array
{
$qtyBase = _sim_clean_float($qtyBase, 8, 'floor');
$price = _sim_clean_float($price, 8, 'round');
if ($qtyBase <= 0) $qtyBase = 0.00000001;
$oid = _sim_make_order_id('sell', $symbol, $qtyBase, $price);
return [
'ok' => true,
'code' => 200,
'err' => null,
'raw' => '',
'json' => [
'error' => 0,
'result' => [
'id' => $oid,
'side' => 'sell',
'sym' => $symbol,
'amt' => $qtyBase, // base qty
'rat' => $price,
'typ' => 'limit',
],
],
];
}
/**
* Simulation order-info:
* - BUY filled if last <= limit
* - SELL filled if last >= limit
*/
function _sim_order_info(array $cfg, string $symbol, string $orderId, string $side): array
{
$symbol = trim((string)$symbol);
$side = strtolower(trim((string)$side));
$parsed = _sim_parse_order_id($orderId);
$pSide = $parsed['side'] ?? (($side === 'sell') ? 'sell' : 'buy');
$pSym = $parsed['symbol'] ?? $symbol;
$pQty = isset($parsed['qty']) ? (float)$parsed['qty'] : 0.0;
$pLim = isset($parsed['limit']) ? (float)$parsed['limit'] : 0.0;
if ($pSym === '') $pSym = $symbol;
if ($pQty <= 0) $pQty = 0.00000001;
if ($pLim <= 0) $pLim = 0.00000001;
$last = _sim_get_last($cfg, $pSym);
$filled = 0.0;
$remaining = $pQty;
$status = 'open';
if ($last !== null && $last > 0) {
if ($pSide === 'buy') {
if ($last <= $pLim) {
$filled = $pQty;
$remaining = 0.0;
$status = 'filled';
}
} else {
if ($last >= $pLim) {
$filled = $pQty;
$remaining = 0.0;
$status = 'filled';
}
}
}
$filled = _sim_clean_float($filled, 8, 'floor');
$remaining = _sim_clean_float($remaining, 8, 'floor');
$history = [];
if ($status === 'filled') {
// Minimal fill record compatible with calc_*_fill
if ($pSide === 'buy') {
$quoteSpent = _sim_clean_float($filled * $pLim, 8, 'round');
$history[] = [
'amount' => $quoteSpent, // quote spent
'rate' => _sim_clean_float($pLim, 8, 'round'),
'fee' => 0.0,
];
} else {
$history[] = [
'amount' => _sim_clean_float($filled, 8, 'floor'), // base sold
'rate' => _sim_clean_float($pLim, 8, 'round'),
'fee' => 0.0,
];
}
}
return [
'ok' => true,
'code' => 200,
'err' => null,
'raw' => '',
'json' => [
'error' => 0,
'result' => [
'id' => $orderId,
'sym' => $pSym,
'side' => $pSide,
'status' => $status,
'filled' => $filled,
'remaining' => $remaining,
'last' => $last,
'history' => $history,
],
],
];
}
function _sim_open_orders(array $cfg, array $orders = []): array
{
$rows = [];
$symbols = [];
foreach ($orders as $o) {
if (!is_array($o)) continue;
$oid = (string)($o['exchange_order_id'] ?? '');
if ($oid === '') continue;
$parsed = _sim_parse_order_id($oid);
if (!is_array($parsed)) continue;
$pSym = trim((string)($parsed['symbol'] ?? ''));
$pSide = strtolower(trim((string)($parsed['side'] ?? 'buy')));
$pQty = isset($parsed['qty']) ? (float)$parsed['qty'] : 0.0;
$pLim = isset($parsed['limit']) ? (float)$parsed['limit'] : 0.0;
if ($pSym === '' || $pQty <= 0.0 || $pLim <= 0.0) continue;
if ($pSide !== 'buy' && $pSide !== 'sell') $pSide = 'buy';
$symbols[$pSym] = true;
$rows[] = [
'id' => $oid,
'sym' => $pSym,
'side' => $pSide,
'qty' => $pQty,
'limit' => $pLim,
];
}
$symbolList = array_values(array_keys($symbols));
if (empty($symbolList)) {
return [
'ok' => true,
'code' => 200,
'err' => null,
'raw' => '',
'json' => [
'error' => 0,
'result' => ['orders_by_id' => []],
],
];
}
$tick = _sim_nv_tickers($cfg, $symbolList);
$j = is_array($tick['json'] ?? null) ? $tick['json'] : [];
if (empty($tick['ok']) || empty($j['ok']) || !is_array($j['data'] ?? null)) {
return [
'ok' => false,
'code' => (int)($tick['code'] ?? 0),
'err' => $tick['err'] ?? 'open_orders_failed',
'raw' => (string)($tick['raw'] ?? ''),
'json' => [
'error' => -1,
'code' => -1,
'message' => 'open_orders_failed',
'result' => ['orders_by_id' => []],
],
];
}
$lastBySymbol = [];
foreach ((array)$j['data'] as $sym => $row) {
if (!is_array($row)) continue;
$last = $row['last'] ?? null;
if (!is_numeric($last)) continue;
$last = (float)$last;
if ($last <= 0) continue;
$lastBySymbol[(string)$sym] = $last;
}
$byId = [];
foreach ($rows as $r0) {
$sym = (string)$r0['sym'];
$lastRaw = $lastBySymbol[$sym] ?? null;
$hasLast = is_numeric($lastRaw);
$last = $hasLast ? (float)$lastRaw : null;
$isFilled = false;
if ($hasLast) {
if ($r0['side'] === 'buy') {
$isFilled = ($last <= (float)$r0['limit']);
} else {
$isFilled = ($last >= (float)$r0['limit']);
}
}
$qty = _sim_clean_float((float)$r0['qty'], 8, 'floor');
$history = [];
$status = 'open';
$filled = 0.0;
$remaining = $qty;
if ($isFilled) {
$status = 'filled';
$filled = $qty;
$remaining = 0.0;
// Keep history format aligned with _sim_order_info so tracker calc_*_fill stays consistent.
if ($r0['side'] === 'buy') {
$quoteSpent = _sim_clean_float($qty * (float)$r0['limit'], 8, 'round');
$history[] = [
'amount' => $quoteSpent, // quote spent
'rate' => _sim_clean_float((float)$r0['limit'], 8, 'round'),
'fee' => 0.0,
];
} else {
$history[] = [
'amount' => $qty, // base sold
'rate' => _sim_clean_float((float)$r0['limit'], 8, 'round'),
'fee' => 0.0,
];
}
}
$byId[(string)$r0['id']] = [
'id' => (string)$r0['id'],
'sym' => $sym,
'side' => (string)$r0['side'],
'status' => $status,
'filled' => $filled,
'remaining' => $remaining,
'last' => $last,
'history' => $history,
];
}
return [
'ok' => true,
'code' => 200,
'err' => null,
'raw' => (string)($tick['raw'] ?? ''),
'json' => [
'error' => 0,
'result' => ['orders_by_id' => $byId],
],
];
}
function _sim_cancel_order(array $cfg, string $symbol, string $orderId, string $side): array
{
return [
'ok' => true,
'code' => 200,
'err' => null,
'raw' => '',
'json' => [
'error' => 0,
'result' => [
'id' => $orderId,
'sym' => $symbol,
'side' => (strtolower(trim((string)$side)) === 'sell' ? 'sell' : 'buy'),
'status' => 'cancelled',
],
],
];
}
return [
'normalize_symbol' => function (string $sym): ?string {
$sym = trim($sym);
if ($sym === '') return null;
// keep underscores format (bitkub/binance usually)
if (strpos($sym, '_') !== false) {
$up = strtoupper($sym);
$parts = explode('_', $up);
if (count($parts) !== 2) return null;
if ($parts[0] === '' || $parts[1] === '') return null;
return $up;
}
// bitstamp-like: "xrpeur"
if (preg_match('/^[a-z0-9]{6,16}$/', $sym)) {
return strtolower($sym);
}
return null;
},
'wallet' => fn(array $cfg) => _sim_wallet($cfg),
'place_buy' => fn(array $cfg, string $symbol, float $amountQuote, float $price) =>
_sim_place_buy($cfg, $symbol, $amountQuote, $price),
'place_sell' => fn(array $cfg, string $symbol, float $qtyBase, float $price) =>
_sim_place_sell($cfg, $symbol, $qtyBase, $price),
'order_info' => fn(array $cfg, string $symbol, string $exchangeOrderId, string $side) =>
_sim_order_info($cfg, $symbol, $exchangeOrderId, $side),
'open_orders' => fn(array $cfg, array $orders = []) =>
_sim_open_orders($cfg, $orders),
'cancel_order' => fn(array $cfg, string $symbol, string $exchangeOrderId, string $side) =>
_sim_cancel_order($cfg, $symbol, $exchangeOrderId, $side),
'map_status' => function (array $result): string {
$status = strtolower(trim((string)($result['status'] ?? '')));
$filled = isset($result['filled']) ? (float)$result['filled'] : 0.0;
$remaining = isset($result['remaining']) ? (float)$result['remaining'] : 0.0;
if (in_array($status, ['filled','finished','completed'], true)) return 'completed';
if (in_array($status, ['canceled','cancelled','rejected'], true)) {
return ($filled > 0) ? 'partial' : 'cancelled';
}
if (in_array($status, ['open','unfilled','pending'], true)) {
return ($filled > 0 && $remaining > 0) ? 'partial' : 'pending';
}
if (in_array($status, ['partial','partially_filled','partiallyfilled'], true)) return 'partial';
return 'pending';
},
'calc_buy_fill' => function (array $result): array {
$fee = 0.0; $quoteNet = 0.0; $crypto = 0.0;
if (!empty($result['history']) && is_array($result['history'])) {
foreach ($result['history'] as $h) {
if (!is_array($h)) continue;
$amt = isset($h['amount']) ? (float)$h['amount'] : 0.0;
$rate = isset($h['rate']) ? (float)$h['rate'] : 0.0;
$f = isset($h['fee']) ? (float)$h['fee'] : 0.0;
$fee += $f;
$net = max($amt - $f, 0.0);
$quoteNet += $net;
if ($rate > 0) $crypto += ($net / $rate);
}
}
$avgRate = ($crypto > 0) ? ($quoteNet / $crypto) : 0.0;
return [
'fee' => $fee,
'quantity' => $crypto,
'buy_price' => $avgRate,
'purchase_value' => $quoteNet + $fee,
];
},
'calc_sell_fill' => function (array $result): array {
$fee = 0.0; $qtyBase = 0.0; $quoteGross = 0.0;
if (!empty($result['history']) && is_array($result['history'])) {
foreach ($result['history'] as $h) {
if (!is_array($h)) continue;
$amtBase = isset($h['amount']) ? (float)$h['amount'] : 0.0;
$rate = isset($h['rate']) ? (float)$h['rate'] : 0.0;
$f = isset($h['fee']) ? (float)$h['fee'] : 0.0;
$qtyBase += $amtBase;
if ($rate > 0) $quoteGross += ($amtBase * $rate);
$fee += $f;
}
}
$avgPrice = ($qtyBase > 0) ? ($quoteGross / $qtyBase) : 0.0;
return [
'fee' => $fee,
'quantity_sold' => $qtyBase,
'sell_price' => $avgPrice,
'price' => $avgPrice,
];
},
];