From ae1dc715592bc4b2b5ab0e65fa6a81880684f869 Mon Sep 17 00:00:00 2001 From: Aaron Fiore Date: Sun, 16 Jun 2024 16:24:07 -0700 Subject: [PATCH] php: fetch: prices: support asset's blockchain(s) - Related refactoring - Update documentation --- .../internal/fetch/prices/internal/base.php | 82 ++++++++++++------- .../fetch/prices/internal/prices/crypto.php | 79 ++++++------------ 2 files changed, 77 insertions(+), 84 deletions(-) diff --git a/container/src/finance/lib/internal/fetch/prices/internal/base.php b/container/src/finance/lib/internal/fetch/prices/internal/base.php index 5039c73..5db2500 100644 --- a/container/src/finance/lib/internal/fetch/prices/internal/base.php +++ b/container/src/finance/lib/internal/fetch/prices/internal/base.php @@ -40,15 +40,15 @@ namespace docker_finance\prices\internal { /** * @brief Get network response data - * @param string $id ID of given symbol ('bitcoin' in 'bitcoin/BTC') + * @param array $asset Parsed environment entries of assets * @param string $timestamp Given year(s) to fetch - * @return array Array of [N]([timestamp][price]) for given year(s) + * @return array, array> 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 - * @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, array> Prices for all given symbols */ public function reader(string $symbols): array; @@ -89,8 +89,8 @@ namespace docker_finance\prices\internal } /** - * @brief Parse given string of 'id/symbol' - * @param string $symbols Expected format: bitcoin/BTC,ethereum/ETH,litecoin/LTC,etc. + * @brief Parse environment assets + * @param string $symbols Expected format: 'id/ticker,id/ticker,blockchain:id/ticker,...' e.g., 'bitcoin/BTC,ethereum/ETH,avalanche:0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e/USDC,...' * @details * * Caveats: @@ -98,25 +98,24 @@ namespace docker_finance\prices\internal * 1. There are multiple reasons why asset must be passed with symbol * 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 = litecoin * eth = ethereum * eth = ethereum-wormhole * - * So, pass ID instead of symbol! - * - * b. Mobula support will require an asset name along with symbol + * b. Mobula will often require blockchain along with asset ID + * NOTE: with Mobula, ID can also consist of a contract address * * 2. Ticker-symbol comes *AFTER* ID because hledger's prices are: * * a. *CASE SENSITIVE* * - * b *WILL NOT UNDERSTAND THE DIFFERENCE BETWEEN (for example): - * aGUSD and AGUSD* (CoinGecko will return lowercase symbol) + * b. Will not understand the difference between (for example): + * aGUSD and AGUSD (CoinGecko will return lowercase symbol) * - * @return array Array of symbols + * @return array> Parsed environment entries */ protected function parse_symbols(string $symbols): array { @@ -126,13 +125,23 @@ namespace docker_finance\prices\internal utils\CLI::throw_fatal("malformed symbols format"); } - $list = explode(',', $symbols); $parsed = []; + $csv = explode(',', $symbols); - foreach ($list as $coin) { - list($key, $value) = explode('/', $coin); - $parsed[$key] = $value; + for ($i = 0; $i < count($csv); $i++) { + list($asset, $ticker) = explode('/', $csv[$i]); + + $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; } @@ -182,8 +191,13 @@ namespace docker_finance\prices\internal 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 $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 @@ -250,15 +264,27 @@ namespace docker_finance\prices\internal /** * @brief Parse fetched date and prices - * @param array $prices Fetched prices [N]([timestamp][price]) - * @return array Date and prices without ID or symbol + * @param array $prices Fetched prices [N][timestamp, price] + * @details + * $prices 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. + * @return array Date and price single-line entries without ID or symbol */ abstract protected function parse_prices(array $prices): array; /** * @brief Create data for master price journal file * @param string $symbol Given symbol associated with ID - * @param array $prices Array of [N]([timestamp][price])for given year(s) + * @param array $prices Array of [N][timestamp, price] for given year(s) * @return array Master price journal file data */ protected function make_master(string $symbol, array $prices): array @@ -314,7 +340,7 @@ namespace docker_finance\prices\internal return $stack; } - public function getter(string $id, string $timestamp): array + public function getter(array $asset, string $timestamp): array { $response = []; $timer = 60; // seconds @@ -322,14 +348,14 @@ namespace docker_finance\prices\internal while (!$success) { try { - $response = $this->request($id, $timestamp); + $response = $this->request($asset, $timestamp); $success = true; // Should throw before this is assigned, alla C++ } catch (\Throwable $e) { $code = $e->getCode(); $message = $e->getMessage(); 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" ); @@ -364,9 +390,9 @@ namespace docker_finance\prices\internal utils\CLI::print_normal(" ─ Symbols"); - foreach ($this->parse_symbols($symbols) as $id => $symbol) { - $parsed = $this->parse_prices($this->getter($id, $timestamp)); - $master = $this->make_master($symbol, $this->make_average($parsed)); + foreach ($this->parse_symbols($symbols) as $asset) { + $parsed = $this->parse_prices($this->getter($asset, $timestamp)); + $master = $this->make_master($asset['ticker'], $this->make_average($parsed)); utils\CLI::print_debug($master); array_push($stack, $master); diff --git a/container/src/finance/lib/internal/fetch/prices/internal/prices/crypto.php b/container/src/finance/lib/internal/fetch/prices/internal/prices/crypto.php index 852d246..d1cfcb1 100644 --- a/container/src/finance/lib/internal/fetch/prices/internal/prices/crypto.php +++ b/container/src/finance/lib/internal/fetch/prices/internal/prices/crypto.php @@ -44,13 +44,7 @@ namespace docker_finance\prices\internal\prices\crypto parent::__construct($env); } - /** - * @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 + protected function request(array $asset, string $timestamp): mixed { // If `key` exists, use Pro API (otherwise, use Public API) $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"]; } $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); 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 $prices Fetched prices [N]([timestamp][price]) * @details - * * Parses historical market data include price, market cap * 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 Date and prices without ID or symbol */ protected function parse_prices(array $prices): array { @@ -99,29 +83,19 @@ namespace docker_finance\prices\internal\prices\crypto $stack = []; 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; $date = date('Y/m/d', $timestamp); // Isolate given year. // - // If 'all', then all years are needed. Otherwise, for example, - // if the given year is for 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). + // NOTE: + // + // If 'all', then all years are needed. Otherwise, for example, + // 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'); if ($given_year != 'all' && !preg_match('/^'.$given_year.'\//', $date)) { utils\CLI::print_debug("skipping $date"); @@ -130,6 +104,14 @@ namespace docker_finance\prices\internal\prices\crypto $price = $prices[$i][1]; // 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]; } @@ -182,13 +164,7 @@ namespace docker_finance\prices\internal\prices\crypto parent::__construct($env); } - /** - * @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 + protected function request(array $asset, string $timestamp): mixed { // If `key` exists, append to header (used in all plans) $key = $this->get_env()->get_env('API_PRICES_KEY'); @@ -197,7 +173,11 @@ namespace docker_finance\prices\internal\prices\crypto if ($key != 'None') { $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); if (array_key_exists('error', $response)) { @@ -220,19 +200,6 @@ namespace docker_finance\prices\internal\prices\crypto $stack = []; 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; $date = date('Y/m/d', $timestamp);