php: fetch: prices: support asset's blockchain(s)

- Related refactoring
- Update documentation
This commit is contained in:
2024-06-16 16:24:07 -07:00
parent bf4c9bf61a
commit ae1dc71559
2 changed files with 77 additions and 84 deletions

View File

@@ -40,15 +40,15 @@ namespace docker_finance\prices\internal
{ {
/** /**
* @brief Get network response data * @brief Get network response data
* @param string $id ID of given symbol ('bitcoin' in 'bitcoin/BTC') * @param array<string> $asset Parsed environment entries of assets
* @param string $timestamp Given year(s) to fetch * @param string $timestamp Given year(s) to fetch
* @return array<mixed> Array of [N]([timestamp][price]) for given year(s) * @return array<int<0, max>, array<string>> Entries of [N][timestamp, price] since given timestamp
*/ */
public function getter(string $id, string $timestamp): array; public function getter(array $asset, string $timestamp): array;
/** /**
* @brief Prepare price data for given symbols * @brief Prepare price data for given symbols
* @param string $symbols Symbols in 'asset/ticker,...' format ('bitcoin/BTC,ethereum/ETH') * @param string $symbols Unparsed envrionment assets (e.g., 'id/ticker,id/ticker,blockchain:id/ticker,...')
* @return array<int<0, max>, array<string>> Prices for all given symbols * @return array<int<0, max>, array<string>> Prices for all given symbols
*/ */
public function reader(string $symbols): array; public function reader(string $symbols): array;
@@ -89,8 +89,8 @@ namespace docker_finance\prices\internal
} }
/** /**
* @brief Parse given string of 'id/symbol' * @brief Parse environment assets
* @param string $symbols Expected format: bitcoin/BTC,ethereum/ETH,litecoin/LTC,etc. * @param string $symbols Expected format: 'id/ticker,id/ticker,blockchain:id/ticker,...' e.g., 'bitcoin/BTC,ethereum/ETH,avalanche:0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e/USDC,...'
* @details * @details
* *
* Caveats: * Caveats:
@@ -98,25 +98,24 @@ namespace docker_finance\prices\internal
* 1. There are multiple reasons why asset must be passed with symbol * 1. There are multiple reasons why asset must be passed with symbol
* instead of symbol alone: * instead of symbol alone:
* *
* a. CoinGecko uses same symbol for multiple ID's: * a. CoinGecko uses same symbol for multiple asset ID's:
* *
* ltc = binance-peg-litecoin * ltc = binance-peg-litecoin
* ltc = litecoin * ltc = litecoin
* eth = ethereum * eth = ethereum
* eth = ethereum-wormhole * eth = ethereum-wormhole
* *
* So, pass ID instead of symbol! * b. Mobula will often require blockchain along with asset ID
* * NOTE: with Mobula, ID can also consist of a contract address
* b. Mobula support will require an asset name along with symbol
* *
* 2. Ticker-symbol comes *AFTER* ID because hledger's prices are: * 2. Ticker-symbol comes *AFTER* ID because hledger's prices are:
* *
* a. *CASE SENSITIVE* * a. *CASE SENSITIVE*
* *
* b *WILL NOT UNDERSTAND THE DIFFERENCE BETWEEN (for example): * b. Will not understand the difference between (for example):
* aGUSD and AGUSD* (CoinGecko will return lowercase symbol) * aGUSD and AGUSD (CoinGecko will return lowercase symbol)
* *
* @return array<string> Array of symbols * @return array<array<string>> Parsed environment entries
*/ */
protected function parse_symbols(string $symbols): array protected function parse_symbols(string $symbols): array
{ {
@@ -126,13 +125,23 @@ namespace docker_finance\prices\internal
utils\CLI::throw_fatal("malformed symbols format"); utils\CLI::throw_fatal("malformed symbols format");
} }
$list = explode(',', $symbols);
$parsed = []; $parsed = [];
$csv = explode(',', $symbols);
foreach ($list as $coin) { for ($i = 0; $i < count($csv); $i++) {
list($key, $value) = explode('/', $coin); list($asset, $ticker) = explode('/', $csv[$i]);
$parsed[$key] = $value;
$id = $asset;
$blockchain = "";
if (str_contains($id, ':')) {
list($blockchain, $id) = explode(':', $asset);
}
$parsed[$i]['id'] = $id;
$parsed[$i]['ticker'] = $ticker;
$parsed[$i]['blockchain'] = $blockchain;
} }
return $parsed; return $parsed;
} }
@@ -182,8 +191,13 @@ namespace docker_finance\prices\internal
return $decoded; return $decoded;
} }
//! @brief Impl-specific REST API request /**
abstract protected function request(string $id, string $timestamp): mixed; * @brief Impl-specific REST API request generator
* @param array<mixed> $asset Parsed environment asset entries to request
* @param string $timestamp Timestamp of entries to request
* @return mixed REST API raw response price data
*/
abstract protected function request(array $asset, string $timestamp): mixed;
/** /**
* @brief Make impl-specific timestamp requirement * @brief Make impl-specific timestamp requirement
@@ -250,15 +264,27 @@ namespace docker_finance\prices\internal
/** /**
* @brief Parse fetched date and prices * @brief Parse fetched date and prices
* @param array<mixed> $prices Fetched prices [N]([timestamp][price]) * @param array<mixed> $prices Fetched prices [N][timestamp, price]
* @return array<string> Date and prices without ID or symbol * @details
* $prices Expectation:
*
* array[0] = oldest entry<br>
* array[0][0] = timestamp<br>
* array[0][1] = price<br>
*
* array[1] = next hour<br>
* array[1][0] = timestamp<br>
* array[1][1] = price<br>
*
* ...etc.
* @return array<string> Date and price single-line entries without ID or symbol
*/ */
abstract protected function parse_prices(array $prices): array; abstract protected function parse_prices(array $prices): array;
/** /**
* @brief Create data for master price journal file * @brief Create data for master price journal file
* @param string $symbol Given symbol associated with ID * @param string $symbol Given symbol associated with ID
* @param array<mixed> $prices Array of [N]([timestamp][price])for given year(s) * @param array<mixed> $prices Array of [N][timestamp, price] for given year(s)
* @return array<string> Master price journal file data * @return array<string> Master price journal file data
*/ */
protected function make_master(string $symbol, array $prices): array protected function make_master(string $symbol, array $prices): array
@@ -314,7 +340,7 @@ namespace docker_finance\prices\internal
return $stack; return $stack;
} }
public function getter(string $id, string $timestamp): array public function getter(array $asset, string $timestamp): array
{ {
$response = []; $response = [];
$timer = 60; // seconds $timer = 60; // seconds
@@ -322,14 +348,14 @@ namespace docker_finance\prices\internal
while (!$success) { while (!$success) {
try { try {
$response = $this->request($id, $timestamp); $response = $this->request($asset, $timestamp);
$success = true; // Should throw before this is assigned, alla C++ $success = true; // Should throw before this is assigned, alla C++
} catch (\Throwable $e) { } catch (\Throwable $e) {
$code = $e->getCode(); $code = $e->getCode();
$message = $e->getMessage(); $message = $e->getMessage();
utils\CLI::print_warning( utils\CLI::print_warning(
"server sent error '{$message}' with code '{$code}' for '{$id}'" "server sent error '{$message}' with code '{$code}' for '{$asset['id']}'"
. " - retrying in '{$timer}' seconds" . " - retrying in '{$timer}' seconds"
); );
@@ -364,9 +390,9 @@ namespace docker_finance\prices\internal
utils\CLI::print_normal(" ─ Symbols"); utils\CLI::print_normal(" ─ Symbols");
foreach ($this->parse_symbols($symbols) as $id => $symbol) { foreach ($this->parse_symbols($symbols) as $asset) {
$parsed = $this->parse_prices($this->getter($id, $timestamp)); $parsed = $this->parse_prices($this->getter($asset, $timestamp));
$master = $this->make_master($symbol, $this->make_average($parsed)); $master = $this->make_master($asset['ticker'], $this->make_average($parsed));
utils\CLI::print_debug($master); utils\CLI::print_debug($master);
array_push($stack, $master); array_push($stack, $master);

View File

@@ -44,13 +44,7 @@ namespace docker_finance\prices\internal\prices\crypto
parent::__construct($env); parent::__construct($env);
} }
/** protected function request(array $asset, string $timestamp): mixed
* @brief REST API request generator
* @param string $id Symbol's ID to request
* @param string $timestamp Timestamp to request
* @return mixed REST API response data
*/
protected function request(string $id, string $timestamp): mixed
{ {
// If `key` exists, use Pro API (otherwise, use Public API) // If `key` exists, use Pro API (otherwise, use Public API)
$key = $this->get_env()->get_env('API_PRICES_KEY'); $key = $this->get_env()->get_env('API_PRICES_KEY');
@@ -61,7 +55,7 @@ namespace docker_finance\prices\internal\prices\crypto
$header = ["x-cg-pro-api-key: $key"]; $header = ["x-cg-pro-api-key: $key"];
} }
$vs_currency = 'usd'; $vs_currency = 'usd';
$url = "https://{$domain}/api/v3/coins/{$id}/market_chart?vs_currency={$vs_currency}&days={$timestamp}"; $url = "https://{$domain}/api/v3/coins/{$asset['id']}/market_chart?vs_currency={$vs_currency}&days={$timestamp}";
$response = $this->request_impl($url, $header); $response = $this->request_impl($url, $header);
if (array_key_exists('status', $response)) { if (array_key_exists('status', $response)) {
@@ -76,19 +70,9 @@ namespace docker_finance\prices\internal\prices\crypto
} }
/** /**
* @brief Parse fetched date and prices
* @param array<mixed> $prices Fetched prices [N]([timestamp][price])
* @details * @details
*
* Parses historical market data include price, market cap * Parses historical market data include price, market cap
* and 24h volume (granularity auto) * and 24h volume (granularity auto)
*
* - Data granularity is automatic (cannot be adjusted)
* - 1 day from current time = 5 minute interval data
* - 1 - 90 days from current time = hourly data
* above 90 days from current time = daily data (00:00 UTC)
*
* @return array<string> Date and prices without ID or symbol
*/ */
protected function parse_prices(array $prices): array protected function parse_prices(array $prices): array
{ {
@@ -99,29 +83,19 @@ namespace docker_finance\prices\internal\prices\crypto
$stack = []; $stack = [];
for($i = 0; $i < count($prices); $i++) { for($i = 0; $i < count($prices); $i++) {
/**
* Expectation:
*
* array[0] = oldest entry
* array[0][0] = timestamp
* array[0][1] = price
*
* array[1] = next hour
* array[1][0] = timestamp
* array[1][1] = price
*
* ...etc.
*/
$timestamp = $prices[$i][0] / 1000; $timestamp = $prices[$i][0] / 1000;
$date = date('Y/m/d', $timestamp); $date = date('Y/m/d', $timestamp);
// Isolate given year. // Isolate given year.
// //
// If 'all', then all years are needed. Otherwise, for example, // NOTE:
// if the given year is for last year, and the beginning of last //
// year was 375 days ago, then upstream will send 375 entries // If 'all', then all years are needed. Otherwise, for example,
// (but only the first 365 will be needed). // if the given year is last year (and the beginning of last
// year was 375 days ago), then upstream will send 375 entries
// (but only the first 365 will be needed).
$given_year = $this->get_env()->get_env('API_FETCH_YEAR'); $given_year = $this->get_env()->get_env('API_FETCH_YEAR');
if ($given_year != 'all' && !preg_match('/^'.$given_year.'\//', $date)) { if ($given_year != 'all' && !preg_match('/^'.$given_year.'\//', $date)) {
utils\CLI::print_debug("skipping $date"); utils\CLI::print_debug("skipping $date");
@@ -130,6 +104,14 @@ namespace docker_finance\prices\internal\prices\crypto
$price = $prices[$i][1]; $price = $prices[$i][1];
// Push to stack // Push to stack
//
// NOTE:
//
// - Data granularity is automatic (cannot be adjusted)
// - 1 day from current time = 5 minute interval data
// - 1 through 90 days from current time = hourly data
// - Above 90 days from current time = daily data (00:00 UTC)
$stack += [$date => $price]; $stack += [$date => $price];
} }
@@ -182,13 +164,7 @@ namespace docker_finance\prices\internal\prices\crypto
parent::__construct($env); parent::__construct($env);
} }
/** protected function request(array $asset, string $timestamp): mixed
* @brief REST API request generator
* @param string $id Symbol's ID to request
* @param string $timestamp Timestamp to request
* @return mixed REST API response data
*/
protected function request(string $id, string $timestamp): mixed
{ {
// If `key` exists, append to header (used in all plans) // If `key` exists, append to header (used in all plans)
$key = $this->get_env()->get_env('API_PRICES_KEY'); $key = $this->get_env()->get_env('API_PRICES_KEY');
@@ -197,7 +173,11 @@ namespace docker_finance\prices\internal\prices\crypto
if ($key != 'None') { if ($key != 'None') {
$header = ["Authorization: $key"]; $header = ["Authorization: $key"];
} }
$url = "https://{$domain}/api/1/market/history?asset={$id}&from={$timestamp}";
$url = "https://{$domain}/api/1/market/history?asset={$asset['id']}&from={$timestamp}";
if (!empty($asset['blockchain'])) {
$url .= "&blockchain={$asset['blockchain']}";
}
$response = $this->request_impl($url, $header); $response = $this->request_impl($url, $header);
if (array_key_exists('error', $response)) { if (array_key_exists('error', $response)) {
@@ -220,19 +200,6 @@ namespace docker_finance\prices\internal\prices\crypto
$stack = []; $stack = [];
for($i = 0; $i < count($prices); $i++) { for($i = 0; $i < count($prices); $i++) {
/**
* Expectation:
*
* array[0] = oldest entry
* array[0][0] = timestamp
* array[0][1] = price
*
* array[1] = next entry
* array[1][0] = timestamp
* array[1][1] = price
*
* ...etc.
*/
$timestamp = $prices[$i][0] / 1000; $timestamp = $prices[$i][0] / 1000;
$date = date('Y/m/d', $timestamp); $date = date('Y/m/d', $timestamp);