Connector Open-Source Code
Browse connector files locally. Exchange API keys stay on your device.
lib/nuxvision.php
<?php
// /opt/nuxvision_connector/lib/nuxvision.php
declare(strict_types=1);
/**
* NuxVision API client helpers (HTTP + endpoint wrappers).
* Used by runner.php / tracker.php.
*
* Requires: lib/core.php (for to_int/to_float etc. optional, but not required here)
*/
/* =========================================================
NV client: cross-process lock + pacing + small file cache
========================================================= */
function nv_extract_instance_id_from_url(string $url): int {
$q = parse_url($url, PHP_URL_QUERY);
if (!is_string($q) || $q === '') return 0;
parse_str($q, $params);
$id = isset($params['instance_id']) ? (int)$params['instance_id'] : 0;
return $id > 0 ? $id : 0;
}
/** lock scope: per instance_id when possible, else global; includes api key hash */
function nv_lock_path_for_url(string $url, array $headers): string {
$iid = nv_extract_instance_id_from_url($url);
$key = '';
foreach ($headers as $h) {
if (stripos($h, 'X-API-KEY:') === 0) {
$key = trim(substr($h, 10));
break;
}
}
$kh = $key !== '' ? substr(sha1($key), 0, 10) : 'nokey';
if ($iid > 0) return "/tmp/nuxvision_nvapi_i{$iid}_k{$kh}.lock";
return "/tmp/nuxvision_nvapi_global_k{$kh}.lock";
}
/**
* Ensures that 2 processes (runner/tracker) never fire NV calls at the same time
* and that calls are spaced by >= $minGapMs.
*/
function nv_with_lock_and_pacing(string $lockPath, int $minGapMs, callable $fn) {
$fp = @fopen($lockPath, 'c+');
if (!$fp) {
// best effort: run without lock if filesystem is not writable
return $fn();
}
flock($fp, LOCK_EX);
// Read last ms from lock file
$lastMs = 0;
rewind($fp);
$raw = stream_get_contents($fp);
if (is_string($raw) && $raw !== '') {
$lastMs = (int)trim($raw);
}
$nowMs = (int)floor(microtime(true) * 1000);
// jitter prevents periodic sync between processes
$jitterMs = random_int(0, 25);
$waitMs = 0;
if ($lastMs > 0) {
$diff = $nowMs - $lastMs;
if ($diff < $minGapMs) {
$waitMs = ($minGapMs - $diff);
}
}
$waitMs += $jitterMs;
if ($waitMs > 0) {
usleep($waitMs * 1000);
}
$out = $fn();
// Write new last ms
$afterMs = (int)floor(microtime(true) * 1000);
ftruncate($fp, 0);
rewind($fp);
fwrite($fp, (string)$afterMs);
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);
return $out;
}
/** tiny file cache helpers (JSON) */
function nv_cache_get(string $path, int $ttlSec): ?array {
if ($ttlSec <= 0) return null;
if (!is_file($path)) return null;
$age = time() - (int)@filemtime($path);
if ($age < 0 || $age > $ttlSec) return null;
$raw = @file_get_contents($path);
if (!is_string($raw) || $raw === '') return null;
$j = json_decode($raw, true);
return is_array($j) ? $j : null;
}
function nv_cache_set(string $path, array $data): void {
@file_put_contents($path, json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
}
/* =========================================================
HTTP (curl) helpers
========================================================= */
function nv_http_get_json(string $url, array $headers, int $timeout): array {
$lockPath = nv_lock_path_for_url($url, $headers);
$doCall = function() use ($url, $headers, $timeout) {
$respHeaders = [];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_USERAGENT, 'NuxVisionConnector/1.0');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
// capture response headers (Retry-After, etc.)
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($ch, $line) use (&$respHeaders) {
$len = strlen($line);
$line = trim($line);
if ($line === '' || strpos($line, ':') === false) return $len;
[$k, $v] = explode(':', $line, 2);
$respHeaders[strtolower(trim($k))] = trim($v);
return $len;
});
$body = curl_exec($ch);
$err = curl_error($ch);
$code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$json = null;
if (is_string($body) && $body !== '') {
$tmp = json_decode($body, true);
if (is_array($tmp)) $json = $tmp;
}
return [
'ok' => ($err === '' && $code >= 200 && $code < 300),
'code' => $code,
'err' => $err ?: null,
'json' => $json,
'raw' => $body,
'headers' => $respHeaders,
];
};
// 20ms gap => effectively <= 50 req/s per (instance,key) across processes
$minGapMs = 20;
$r = nv_with_lock_and_pacing($lockPath, $minGapMs, $doCall);
// If rate-limited, honor Retry-After and retry once
if (is_array($r) && (int)($r['code'] ?? 0) === 429) {
$ra = 0;
if (!empty($r['headers']['retry-after'])) {
$ra = (int)$r['headers']['retry-after'];
}
if ($ra <= 0) $ra = 1;
sleep($ra);
$r2 = nv_with_lock_and_pacing($lockPath, $minGapMs, $doCall);
return $r2;
}
return $r;
}
function nv_http_post_json(string $url, array $headers, int $timeout, array $payload): array {
$lockPath = nv_lock_path_for_url($url, $headers);
$doCall = function() use ($url, $headers, $timeout, $payload) {
$respHeaders = [];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_USERAGENT, 'NuxVisionConnector/1.0');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($ch, $line) use (&$respHeaders) {
$len = strlen($line);
$line = trim($line);
if ($line === '' || strpos($line, ':') === false) return $len;
[$k, $v] = explode(':', $line, 2);
$respHeaders[strtolower(trim($k))] = trim($v);
return $len;
});
$jsonPayload = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
if ($jsonPayload === false) $jsonPayload = '{}';
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonPayload);
$body = curl_exec($ch);
$err = curl_error($ch);
$code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$json = null;
if (is_string($body) && $body !== '') {
$tmp = json_decode($body, true);
if (is_array($tmp)) $json = $tmp;
}
return [
'ok' => ($err === '' && $code >= 200 && $code < 300),
'code' => $code,
'err' => $err ?: null,
'json' => $json,
'raw' => $body,
'headers' => $respHeaders,
];
};
$minGapMs = 20;
$r = nv_with_lock_and_pacing($lockPath, $minGapMs, $doCall);
if (is_array($r) && (int)($r['code'] ?? 0) === 429) {
$ra = 0;
if (!empty($r['headers']['retry-after'])) {
$ra = (int)$r['headers']['retry-after'];
}
if ($ra <= 0) $ra = 1;
sleep($ra);
$r2 = nv_with_lock_and_pacing($lockPath, $minGapMs, $doCall);
return $r2;
}
return $r;
}
function nv_url(string $base, string $path, array $query = []): string {
$base = rtrim($base, '/');
$url = $base . '/' . ltrim($path, '/');
if ($query) $url .= '?' . http_build_query($query);
return $url;
}
function nv_headers(string $apiKey, bool $json = false): array {
$h = ['X-API-KEY: ' . $apiKey];
if ($json) $h[] = 'Content-Type: application/json';
return $h;
}
/* =========================================================
Connector events helpers
========================================================= */
/**
* Normalize event type to: UPPERCASE_WITH_UNDERSCORES
* (keeps only A-Z 0-9 _ ; converts '-' and spaces to '_')
*/
function nv_event_type_normalize(string $eventType): string {
$s = strtoupper(trim($eventType));
if ($s === '') return '';
$s = str_replace(['-', ' '], '_', $s);
$s = preg_replace('/[^A-Z0-9_]/', '_', $s);
$s = preg_replace('/_+/', '_', $s);
$s = trim($s, '_');
return $s;
}
/**
* Normalize severity to one of: INFO, WARN, ERROR
*/
function nv_event_severity_normalize(string $severity): string {
$s = strtoupper(trim($severity));
return match ($s) {
'ERROR' => 'ERROR',
'WARN', 'WARNING' => 'WARN',
default => 'INFO',
};
}
/**
* POST /api/v1/connector_event_create.php
* payload: {
* instance_id: int,
* event_type: string,
* severity: string,
* source: string, // runner|tracker|...
* payload: object
* }
*
* Returns NV json (expected): { ok: true, inserted_id: ..., ... }
*/
function nv_connector_event_create(
string $nvBase,
string $nvKey,
int $timeout,
int $instanceId,
string $eventType,
string $severity,
string $source,
array $payload = []
): array {
$eventType = nv_event_type_normalize($eventType);
$severity = nv_event_severity_normalize($severity);
$source = strtolower(trim($source));
if ($instanceId <= 0 || $eventType === '' || $source === '') {
return [
'ok' => false,
'code' => 0,
'err' => 'invalid_event_inputs',
'json' => null,
'raw' => null,
];
}
$url = nv_url($nvBase, 'instance_events.php');
return nv_http_post_json(
$url,
nv_headers($nvKey, true),
$timeout,
[
'instance_id' => $instanceId,
'event_type' => $eventType,
'severity' => $severity,
'source' => $source,
'payload' => $payload,
]
);
}
/* =========================================================
Endpoint wrappers
========================================================= */
/**
* GET /api/v1/sync_instance_opportunities.php?instance_id=..&timeframe=..
*/
function nv_sync_instance_opportunities(string $nvBase, string $nvKey, int $timeout, int $instanceId, string $timeframe): array {
$url = nv_url($nvBase, 'instance_opportunities_sync.php', [
'instance_id' => $instanceId,
'timeframe' => $timeframe,
]);
return nv_http_get_json($url, nv_headers($nvKey), $timeout);
}
/**
* GET /api/v1/heartbeat.php?instance_id=..
*/
function nv_heartbeat(string $nvBase, string $nvKey, int $timeout, int $instanceId): array {
$url = nv_url($nvBase, 'heartbeat.php', ['instance_id' => $instanceId]);
return nv_http_get_json($url, nv_headers($nvKey), $timeout);
}
/**
* GET /api/v1/instance_queue.php?instance_id=..&timeframe=..
* Optional: scope=tracker (lets API return a reduced payload for tracker.php)
*
* Expected json keys (full): ok, to_place, to_track, symbols, missing_sells, last_completed_buy
*/
function nv_instance_queue(
string $nvBase,
string $nvKey,
int $timeout,
int $instanceId,
string $timeframe,
?string $scope = null
): array {
$q = [
'instance_id' => $instanceId,
'timeframe' => $timeframe,
];
if (is_string($scope) && trim($scope) !== '') {
$q['scope'] = trim($scope);
}
$url = nv_url($nvBase, 'instance_queue.php', $q);
return nv_http_get_json($url, nv_headers($nvKey), $timeout);
}
/**
* GET /api/v1/instance_settings.php?instance_id=..
*
* Return shape used by runner.php / tracker.php:
* [
* 'ok' => bool,
* 'http' => int,
* 'err' => ?string,
* 'raw' => ?string,
* 'settings' => array,
* 'updated_at' => mixed
* ]
*/
function nv_fetch_instance_settings(string $nvBase, string $nvKey, int $timeout, int $instanceId): array {
// File cache (60s)
$ttl = 60;
$cachePath = "/tmp/nuxvision_nvcache_settings_i{$instanceId}.json";
$cached = nv_cache_get($cachePath, $ttl);
if (is_array($cached) && !empty($cached['settings'])) {
return $cached;
}
$url = nv_url($nvBase, 'instance_settings.php', [
'instance_id' => $instanceId,
]);
$resp = nv_http_get_json($url, nv_headers($nvKey), $timeout);
$settings = [];
$updatedAt = null;
if (!empty($resp['ok']) && is_array($resp['json'])) {
$j = (array)$resp['json'];
// support both "settings" and legacy "data"
$rawSettings = $j['settings'] ?? ($j['data'] ?? null);
if (is_array($rawSettings)) {
$settings = $rawSettings;
}
$updatedAt = $j['updated_at'] ?? ($j['settings_updated_at'] ?? null);
}
$out = [
'ok' => (bool)($resp['ok'] && !empty($settings)),
'http' => (int)($resp['code'] ?? 0),
'err' => $resp['err'] ?? null,
'raw' => $resp['raw'] ?? null,
'settings' => $settings,
'updated_at' => $updatedAt,
];
if (!empty($out['ok'])) {
nv_cache_set($cachePath, $out);
}
return $out;
}
/**
* POST /api/v1/instance_orders_update.php
* payload: { instance_id: int, updates: [...] }
*/
function nv_orders_update(string $nvBase, string $nvKey, int $timeout, int $instanceId, array $updates): array {
$url = nv_url($nvBase, 'instance_orders_update.php');
return nv_http_post_json(
$url,
nv_headers($nvKey, true),
$timeout,
['instance_id' => $instanceId, 'updates' => $updates]
);
}
/**
* POST /api/v1/create_connector_sell.php
* payload: { instance_id: int, buy_order_id: int }
*/
function nv_create_sell_from_buy(string $nvBase, string $nvKey, int $timeout, int $instanceId, int $buyOrderId): array {
$url = nv_url($nvBase, 'create_connector_sell.php');
return nv_http_post_json(
$url,
nv_headers($nvKey, true),
$timeout,
['instance_id' => $instanceId, 'buy_order_id' => $buyOrderId]
);
}
/**
* GET /api/v1/exchange_symbol.php?exchange=...&symbols=SYM1,SYM2
* Returns: json.data (map by symbol)
*/
function nv_fetch_exchange_rules(string $nvBase, string $nvKey, int $timeout, string $exchangeName, array $symbols): array {
$symbols = array_values(array_unique(array_filter($symbols, fn($s) => is_string($s) && $s !== '')));
sort($symbols, SORT_STRING);
if (!$symbols) return [];
if (count($symbols) > 300) $symbols = array_slice($symbols, 0, 300);
$url = nv_url($nvBase, 'exchange_symbol.php', [
'exchange' => $exchangeName,
'symbols' => implode(',', $symbols),
]);
$resp = nv_http_get_json($url, nv_headers($nvKey), $timeout);
if (empty($resp['ok']) || empty($resp['json']['ok']) || !is_array($resp['json']['data'] ?? null)) {
return [];
}
return (array)$resp['json']['data'];
}
/**
* GET /api/v1/tickers.php?exchange=...&symbols=SYM1,SYM2
* Returns: json.data (map by symbol)
*/
function nv_tickers(string $nvBase, string $nvKey, int $timeout, string $exchangeName, string $symbolsKey): array {
// File cache (60s) by (exchange, symbolsKey)
$ttl = 60;
$cacheKey = substr(sha1($exchangeName . '|' . $symbolsKey), 0, 16);
$cachePath = "/tmp/nuxvision_nvcache_tickers_{$cacheKey}.json";
$cached = nv_cache_get($cachePath, $ttl);
if (is_array($cached) && array_key_exists('ok', $cached)) {
return $cached;
}
$url = nv_url($nvBase, 'tickers.php', [
'exchange' => $exchangeName,
'symbols' => $symbolsKey,
]);
$resp = nv_http_get_json($url, nv_headers($nvKey), $timeout);
// Cache even failures briefly to avoid hammering on repeated errors
nv_cache_set($cachePath, $resp);
return $resp;
}
/* =========================================================
Reserve conversions (NV: reserve_conversions.php)
========================================================= */
/**
* GET /api/v1/reserve_conversions.php?action=add_from_sell
* params: instance_id, source_order_id, reserve_percent
*/
function nv_reserve_add_from_sell(
string $nvBase,
string $nvKey,
int $timeout,
int $instanceId,
int $sourceOrderId,
string $reservePercent
): array {
$url = nv_url($nvBase, 'reserve_conversions.php', [
'action' => 'add_from_sell',
'instance_id' => $instanceId,
'source_order_id' => $sourceOrderId,
'reserve_percent' => $reservePercent,
]);
return nv_http_get_json($url, nv_headers($nvKey), $timeout);
}
/**
* GET /api/v1/reserve_conversions.php?action=get_pending
* params: instance_id, quote_asset(optional)
*/
function nv_reserve_get_pending(
string $nvBase,
string $nvKey,
int $timeout,
int $instanceId,
?string $quoteAsset = null
): array {
$q = [
'action' => 'get_pending',
'instance_id' => $instanceId,
];
if (is_string($quoteAsset) && trim($quoteAsset) !== '') {
$q['quote_asset'] = trim($quoteAsset);
}
$url = nv_url($nvBase, 'reserve_conversions.php', $q);
return nv_http_get_json($url, nv_headers($nvKey), $timeout);
}
/**
* POST /api/v1/reserve_conversions.php?action=mark_result
* params: instance_id, quote_asset, status(placed|filled|failed), from_amount,
* to_amount, price, fee, exchange_order_id, error_message, source_order_id
*/
function nv_reserve_mark_result(
string $nvBase,
string $nvKey,
int $timeout,
int $instanceId,
string $quoteAsset,
string $status,
string $fromAmount,
?string $toAmount = null,
?string $price = null,
?string $fee = null,
?string $exchangeOrderId = null,
?string $errorMessage = null,
?int $sourceOrderId = null
): array {
$payload = [
'action' => 'mark_result',
'instance_id' => $instanceId,
'quote_asset' => $quoteAsset,
'status' => $status,
'from_amount' => $fromAmount,
];
if ($toAmount !== null) $payload['to_amount'] = $toAmount;
if ($price !== null) $payload['buy_price'] = $price;
if ($fee !== null) $payload['fee'] = $fee;
if (is_string($exchangeOrderId) && trim($exchangeOrderId) !== '') {
$payload['exchange_order_id'] = trim($exchangeOrderId);
}
if (is_string($errorMessage) && trim($errorMessage) !== '') {
$payload['error_message'] = trim($errorMessage);
}
if ($sourceOrderId !== null && $sourceOrderId > 0) {
$payload['source_order_id'] = $sourceOrderId;
}
$url = nv_url($nvBase, 'reserve_conversions.php');
return nv_http_post_json(
$url,
nv_headers($nvKey, true),
$timeout,
$payload
);
}
function nv_instance_list(string $nvBase, string $nvKey, int $timeout): array {
$url = nv_url($nvBase, 'instance_list.php');
return nv_http_get_json($url, nv_headers($nvKey), $timeout);
}
function nv_fetch_instance_meta(string $nvBase, string $nvKey, int $timeout, int $instanceId): array {
if ($instanceId <= 0) return ['ok' => false];
$resp = nv_instance_list($nvBase, $nvKey, $timeout);
if (empty($resp['ok']) || empty($resp['json']['ok']) || !is_array($resp['json']['instances'] ?? null)) {
return [
'ok' => false,
'http' => (int)($resp['code'] ?? 0),
'err' => $resp['err'] ?? null,
'raw' => $resp['raw'] ?? null,
];
}
foreach ((array)$resp['json']['instances'] as $row) {
if (!is_array($row)) continue;
if ((int)($row['id'] ?? 0) !== $instanceId) continue;
return [
'ok' => true,
'instance_id' => $instanceId,
'exchange' => strtolower(trim((string)($row['exchange'] ?? ''))),
'native_quote_ccy' => strtoupper(trim((string)($row['native_quote_ccy'] ?? ''))),
'name' => (string)($row['name'] ?? ''),
];
}
return ['ok' => false];
}
function nv_opportunities_build(string $nvBase, string $nvKey, int $timeout, int $instanceId, int $debug = 0): array {
$q = ['instance_id' => $instanceId];
if ($debug === 1) $q['debug'] = 1;
$url = nv_url($nvBase, 'opportunities_build.php', $q);
return nv_http_get_json($url, nv_headers($nvKey), $timeout);
}
function nv_instance_bankroll_update(
string $nvBase,
string $nvKey,
int $timeout,
int $instanceId,
float $available
): array {
$url = nv_url($nvBase, 'instance_bankroll_update.php');
return nv_http_post_json(
$url,
nv_headers($nvKey, true),
$timeout,
[
'instance_id' => $instanceId,
'available' => $available,
]
);
}