diff --git a/Makefile b/Makefile index dc6359d..5a8d715 100644 --- a/Makefile +++ b/Makefile @@ -9,3 +9,4 @@ test: @bats tests/*.bats shellcheck: @shellcheck src/*.sh + @shellcheck examples/*.sh diff --git a/examples/klines.sh b/examples/klines.sh index 49fc584..dacb103 100644 --- a/examples/klines.sh +++ b/examples/klines.sh @@ -7,32 +7,13 @@ SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")" . "$SCRIPT_DIR/../lib/binance.sh" main() { - local symbols start_time - local klines open high low close - local -a kline + local symbols start_time symbol symbols=$(symbols spot) start_time=$(today) for symbol in $symbols; do - klines=$(klines spot $symbol 1d $start_time | jq -c .[]) - - for kline in $klines; do - readarray -t kline < <(jq -r .[] <<< $kline) - - open_time=${kline[0]} - open=${kline[1]} - high=${kline[2]} - low=${kline[3]} - close=${kline[4]} - close_time=${kline[6]} - - if is_set $open; then - echo "Symbol: $symbol, Open: $open, High: $high" - fi - - done - + TZ=UTC klines spot "$symbol" 1d "$start_time" done } diff --git a/examples/ohlcv.sh b/examples/ohlcv.sh new file mode 100644 index 0000000..46bde89 --- /dev/null +++ b/examples/ohlcv.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")" + +. "$SCRIPT_DIR/../lib/binance.sh" + +main() { + local symbols start_time symbol + + symbols=$(symbols spot) + start_time=$(today) + + for symbol in $symbols; do + # shellcheck disable=SC2034 + klines spot "$symbol" 1d "$start_time" | json_to_tsv | \ + while IFS=$'\t' read -r open_time open high low close volume close_time quote_asset_volume; do + + if is_set "$open"; then + echo "Symbol: $symbol, Open: $open, High: $high, Low: $low, Close: $close, Volume: $quote_asset_volume" + fi + + done + done +} + +main diff --git a/examples/symbols.sh b/examples/symbols.sh new file mode 100644 index 0000000..d9f2819 --- /dev/null +++ b/examples/symbols.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -eu + +SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")" + +. "$SCRIPT_DIR/../lib/binance.sh" + +main() { + local product + + for product in "${API_URLS[@]}"; do + symbols "$product" + done +} + +main diff --git a/lib/binance.sh b/lib/binance.sh index 6af1011..5c28d6e 100644 --- a/lib/binance.sh +++ b/lib/binance.sh @@ -274,6 +274,68 @@ tomorrow() { date -d "tomorrow" +%F } +####################################### +# Converts a JSON array into newline-delimited JSON (NDJSON). +# Globals: +# None +# Arguments: +# json (string): JSON array as a string +# Outputs: +# Writes each item in the JSON array to stdout, one per line +# Returns: +# 0 on success +####################################### +json_to_ndjson() { + local json=${1:-} + + if [ -z "$json" ] && [ -p /dev/stdin ]; then + json=$(< /dev/stdin) + fi + + if [ -z "$json" ]; then + echo "Error: Empty JSON string" + return 1 + fi + + echo "$json" | jq -rc '.[]' +} + +####################################### +# Converts a JSON array into TSV format with only values. +# Supports arrays, array of arrays, and array of objects. +# Globals: +# None +# Arguments: +# json (string): JSON array as a string +# Outputs: +# Writes TSV-formatted values to stdout +# Returns: +# 0 on success, 1 on error +####################################### +json_to_tsv() { + local json=${1:-} + + if [ -z "$json" ] && [ -p /dev/stdin ]; then + json=$(< /dev/stdin) + fi + + if [ -z "$json" ]; then + echo "Error: Empty JSON string" >&2 + return 1 + fi + + echo "$json" | jq -r ' + .[] | + if type == "array" then + . + elif type == "object" then + [.[]] + else + [.] # Wrap single values into an array + end | @tsv + ' +} + ####################################### # Checks if the provided argument is set (non-empty). # Globals: @@ -333,6 +395,7 @@ fail() { exit 1 } + declare -Ag API_URLS API_URLS=( @@ -360,7 +423,7 @@ symbols() { base_url=${API_URLS[$product]:?API URL is not set} # todo: check response before passing to jq - http.get "${base_url}/exchangeInfo" | jq -r .symbols[].symbol + http.get "${base_url}/exchangeInfo" | jq -r '.symbols[].symbol' } ####################################### diff --git a/src/config.sh b/src/config.sh new file mode 100644 index 0000000..d3611fc --- /dev/null +++ b/src/config.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +# shellcheck disable=SC2034 +declare -Ag API_URLS + +API_URLS=( + [spot]=https://api.binance.com/api/v3 + [cm]=https://dapi.binance.com/dapi/v1 # COIN-M Futures + [um]=https://fapi.binance.com/fapi/v1 # USD-M Futures +) diff --git a/src/klines.sh b/src/klines.sh new file mode 100644 index 0000000..d50e7cf --- /dev/null +++ b/src/klines.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +####################################### +# Retrieves kline (candlestick) data for a specific symbol and interval. +# Globals: +# API_URLS (associative array of API URLs per product) +# Arguments: +# product (string): Product key used to select base URL. +# symbol (string): Trading symbol (e.g., BTCUSDT). +# interval (string): Kline interval (e.g., 1m, 1h, 1d). +# start_time (string, optional): Start date in format 'YYYY-MM-DD'. +# end_time (string, optional): End date in format 'YYYY-MM-DD'. +# Outputs: +# JSON array of kline data. +# Returns: +# 0 on success, non-zero on error. +####################################### +klines() { + local product symbol interval start_time end_time + local base_url query_string + local start_time_ms end_time_ms + + product=${1:?missing required argument} + symbol=${2:?missing required argument} + interval=${3:?missing required argument} + start_time=${4:-} + end_time=${5:-} + + base_url=${API_URLS[$product]:?API URL is not set} + query_string="symbol=${symbol}&interval=${interval}&limit=1000" + + if is_set "$start_time"; then + if ! is_date "$start_time"; then + fail " must be valid date" + fi + + start_time_ms=$(date_to_ms "$start_time") + query_string+="&startTime=${start_time_ms}" + fi + + if is_set "$end_time"; then + if ! is_date "$end_time"; then + fail " must be valid date" + fi + + end_time_ms=$(date_to_ms "$end_time") + query_string+="&endTime=${end_time_ms}" + fi + + # todo: check response before passing to jq + http.get "${base_url}/klines?${query_string}" | jq -r . +} diff --git a/src/main.sh b/src/main.sh index 5de1be1..012a8a6 100644 --- a/src/main.sh +++ b/src/main.sh @@ -2,83 +2,6 @@ . ../lib/utils.sh -declare -Ag API_URLS - -API_URLS=( - [spot]=https://api.binance.com/api/v3 - [cm]=https://dapi.binance.com/dapi/v1 # COIN-M Futures - [um]=https://fapi.binance.com/fapi/v1 # USD-M Futures -) - -####################################### -# Retrieves available symbols for a given product. -# Globals: -# API_URLS (associative array of API URLs per product) -# Arguments: -# product (string): Product key used to select base URL. -# Outputs: -# A list of symbols in plain text, one per line. -# Returns: -# 0 on success, non-zero on error. -####################################### -symbols() { - local product base_url - - product=${1:?missing required argument} - - base_url=${API_URLS[$product]:?API URL is not set} - - # todo: check response before passing to jq - http.get "${base_url}/exchangeInfo" | jq -r .symbols[].symbol -} - -####################################### -# Retrieves kline (candlestick) data for a specific symbol and interval. -# Globals: -# API_URLS (associative array of API URLs per product) -# Arguments: -# product (string): Product key used to select base URL. -# symbol (string): Trading symbol (e.g., BTCUSDT). -# interval (string): Kline interval (e.g., 1m, 1h, 1d). -# start_time (string, optional): Start date in format 'YYYY-MM-DD'. -# end_time (string, optional): End date in format 'YYYY-MM-DD'. -# Outputs: -# JSON array of kline data. -# Returns: -# 0 on success, non-zero on error. -####################################### -klines() { - local product symbol interval start_time end_time - local base_url query_string - local start_time_ms end_time_ms - - product=${1:?missing required argument} - symbol=${2:?missing required argument} - interval=${3:?missing required argument} - start_time=${4:-} - end_time=${5:-} - - base_url=${API_URLS[$product]:?API URL is not set} - query_string="symbol=${symbol}&interval=${interval}&limit=1000" - - if is_set "$start_time"; then - if ! is_date "$start_time"; then - fail " must be valid date" - fi - - start_time_ms=$(date_to_ms "$start_time") - query_string+="&startTime=${start_time_ms}" - fi - - if is_set "$end_time"; then - if ! is_date "$end_time"; then - fail " must be valid date" - fi - - end_time_ms=$(date_to_ms "$end_time") - query_string+="&endTime=${end_time_ms}" - fi - - # todo: check response before passing to jq - http.get "${base_url}/klines?${query_string}" | jq -r . -} +. ./config.sh +. ./symbols.sh +. ./klines.sh diff --git a/src/symbols.sh b/src/symbols.sh new file mode 100644 index 0000000..0956635 --- /dev/null +++ b/src/symbols.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +####################################### +# Retrieves available symbols for a given product. +# Globals: +# API_URLS (associative array of API URLs per product) +# Arguments: +# product (string): Product key used to select base URL. +# Outputs: +# A list of symbols in plain text, one per line. +# Returns: +# 0 on success, non-zero on error. +####################################### +symbols() { + local product base_url + + product=${1:?missing required argument} + + base_url=${API_URLS[$product]:?API URL is not set} + + # todo: check response before passing to jq + http.get "${base_url}/exchangeInfo" | jq -r '.symbols[].symbol' +} diff --git a/tests/klines.bats b/tests/klines.bats index e6a7c6c..a2844be 100644 --- a/tests/klines.bats +++ b/tests/klines.bats @@ -4,7 +4,8 @@ setup() { bats_load_library bats-support bats_load_library bats-assert load "../lib/utils.sh" - load "../src/main.sh" + load "../src/config.sh" + load "../src/klines.sh" } # Test: klines should return kline data for valid inputs diff --git a/tests/symbols.bats b/tests/symbols.bats index a47502c..84a866d 100644 --- a/tests/symbols.bats +++ b/tests/symbols.bats @@ -4,7 +4,8 @@ setup() { bats_load_library bats-support bats_load_library bats-assert load "../lib/utils.sh" - load "../src/main.sh" + load "../src/config.sh" + load "../src/symbols.sh" } # Test: symbols should return a list of symbols for a valid product