#!/usr/bin/env bash set -e # MIT License # forked from https://github.com/appatalks/ticker.sh # original script https://github.com/pstadler/ticker.sh VERSION="v1.0" ### TODO # Verif stocks in $@ # Locale compliant # Localization ### API : ${TMPDIR:=/tmp} SESSION_DIR="${TMPDIR%/}/ticker.sh-$(whoami)" COOKIE_FILE="${SESSION_DIR}/cookies.txt" API_ENDPOINT="https://query1.finance.yahoo.com/v8/finance/chart/" API_SUFFIX="?interval=1d" ### Variables ScriptArgs=( "$@" ) ScriptPath="$(readlink -f "$0")" # /Users/user/Documents/Scripts/stocks/crypto.sh ScriptWorkDir="$(dirname "$ScriptPath")" # /Users/user/Documents/Scripts/stocks ####### Configurations ###### #NO_COLOR=1 LANG=C LC_NUMERIC=C # Check if NO_COLOR is set to disable colorization if [ -z "$NO_COLOR" ]; then : "${COLOR_GREEN:=$'\e[32m'}" : "${COLOR_GREEN_BOLD:=$'\e[1;32m'}" : "${COLOR_RED:=$'\e[31m'}" : "${COLOR_RED_BOLD:=$'\e[1;31m'}" : "${COLOR_YELLOW=$'\e[33m'}" : "${COLOR_YELLOW_BOLD:=$'\e[1;33m'}" : "${COLOR_SILVER=$'\e[37m'}" : "${COLOR_LIGHT_GREY=$'\e[249m'}" : "${COLOR_BRIGHT_PURPLE=$'\e[35;1m'}" : "${BOLD:=$'\e[1m'}" : "${ITALIC:=$'\e[3m'}" : "${COLOR_RESET:=$'\e[00m'}" else : "${BOLD:=$'\e[1m'}" : "${COLOR_RESET:=$'\e[00m'}" : "${ITALIC:=$'\e[3m'}" fi # Help Help() { clear echo -e "\n${COLOR_GREEN_BOLD}Bash Stocks $VERSION${COLOR_RESET}\n" echo "Syntax: stocks.sh []" echo echo "Examples: stocks.sh AAPL ORA.PA AUB.PA" echo " stocks.sh (need ~/.stocks.yaml file)" echo echo "Options:" echo "-h Print this Help." echo echo "Requires jq and yq installed" echo " -https://stedolan.github.io/jq/" echo " -https://github.com/kislyuk/yq" } ### Parse options while getopts "h" opt; do case ${opt} in h|*) Help Help exit 2 ;; esac done shift $((OPTIND -1)) ### Config file stocks_list=$HOME/.stocks.yaml #json_file="/tmp/purchased.json" json_file="$ScriptWorkDir/purchased.json" #json_file="" ####### /Configurations ###### ### Requierements if ! $(type jq >/dev/null 2>&1); then echo -e "${COLOR_RED}'jq' is not in the PATH. (See: https://stedolan.github.io/jq/)${COLOR_RESET}" exit 1 fi if ! $(type yq >/dev/null 2>&1); then echo -e "${COLOR_RED}'yq' is not in the PATH. (See: https://github.com/kislyuk/yq)${COLOR_RESET}" exit 1 fi cat < /dev/null > /dev/tcp/1.1.1.1/53 if [[ $? -ne 0 ]]; then echo -e "\n${COLOR_RED}No Internet connection !${COLOR_RESET}" echo -e "Exit !" exit 1 fi ####### Functions ###### symbol_currency() { case $currency in EUR) echo -n "€" ;; USD) echo -n "$" ;; GBP) echo -n "£" ;; JPY) echo -n "¥" ;; esac } # Check if number is positive (or 0) or negative CheckSign() { local n="$1" # Capture the number passed as a parameter. if [ $(echo "$n < 0" | bc -l) -eq 1 ] ; then #printf "%+17.13f : %s\n" "$n" "negative" p="-" else #printf "%+17.13f : %s\n" "$n" "positive or zero" p="+" fi echo "$p" } [ ! -d "$SESSION_DIR" ] && mkdir -m 700 "$SESSION_DIR" preflight () { curl --silent --output /dev/null --cookie-jar "$COOKIE_FILE" "https://finance.yahoo.com" \ -H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8" } fetch_chart () { local symbol=$1 local url="${API_ENDPOINT}${symbol}${API_SUFFIX}" curl --silent -b "$COOKIE_FILE" "$url" } [ ! -f "$COOKIE_FILE" ] && preflight ####### Start Display ###### echo -e "${COLOR_GREEN_BOLD}Bash Stocks${COLOR_RESET} $VERSION" echo -e "Using Yahoo Finance API ($API_ENDPOINT)\n" ### Read quotes from arguments or from~/.stocks.yaml config file SYMBOLS=() # from arguments if [ -n "$ScriptArgs" ]; then REGEXP='^[a-zA-Z0-9.,_ ]+$' q="$@" if [[ "$q" =~ $REGEXP ]]; then q=${q//,/\ } q=${q//_/\ } SYMBOLS+=($q) else Help exit 1 fi : <<'END_COMMENT' # Check if quotes passed as argument really exist for val in ${!SYMBOLS[@]} do k="${SYMBOLS[$val]}" #echo $val # index #echo $k # value if [[ ! "$list_coins" == *"$k"* ]]; then echo -e "${COLOR_RED}$k is not a crypto. Remove it !${COLOR_RESET}" unset SYMBOLS[$val] fi done # Recreate the array, because the gaps have to disappear SYMBOLS=("${SYMBOLS[@]}") END_COMMENT echo -e "${COLOR_GREEN}Found ${#SYMBOLS[@]} stocks to display: ${SYMBOLS[@]} ...${COLOR_RESET}" # from ~/.stocks.yaml config file else if [ -f "$stocks_list" ]; then sl=$(cat $stocks_list) [ $? -eq 0 ] && echo -e "${COLOR_GREEN}Reading $stocks_list ...${COLOR_RESET}" || echo -e "${COLOR_RED}Error while reading $stocks_list ...${COLOR_RESET}" a=$(yq '.watchlist[]' <<< $sl) SYMBOLS+=(${a}) [ $? -eq 0 ] && echo -e "${COLOR_GREEN}Found ${#SYMBOLS[@]} stocks to display: ${SYMBOLS[@]} ...${COLOR_RESET}" || echo -e "${COLOR_RED}No coins to display...${COLOR_RESET}" b=$(yq '.lot' <<< $sl) u=$((yq '.lots[] | select(.quantity != 0) | .symbol' | tr '\n' ' ') <<< $sl) purchased=(${u}) [ $? -eq 0 ] && echo -e "${COLOR_GREEN}Found ${#purchased[@]} quotes purchased: ${purchased[@]} ...${COLOR_RESET}" || echo -e "${COLOR_RED}Fail to load quotes list purchased...${COLOR_COLOR_RESET}" # Check that purchased's stocks are in display list for val in ${!purchased[@]} do x="${purchased[$val]}" if [[ " ${SYMBOLS[*]} " != *"$x"* ]]; then SYMBOLS+=(${x}) echo -e "${COLOR_RED}$x is not in stocks's list to display. We add it !${COLOR_GREEN}" fi done [ $? -eq 0 ] && echo -e "${COLOR_GREEN}Found ${#SYMBOLS[@]} stocks to display: ${SYMBOLS[@]} ...${COLOR_RESET}" || echo -e "${COLOR_RED}No coins to display...${COLOR_RESET}" else echo -e "${COLOR_RED}$stocks_list not present !${COLOR_RESET}" echo "" fi #echo "${SYMBOLS[@]}" #echo "${purchased[@]}" : <<'END_COMMENT' while IFS= read -r obj; do #echo "$obj" stock=${obj:2} #echo "$stock" SYMBOLS+=("$stock") done < <(echo "$a") [ $status -eq 0 ] && echo -e "${COLOR_GREEN}Found ${#SYMBOLS[@]} stocks to display: ${SYMBOLS[@]} ...${COLOR_RESET}" || echo -e "${COLOR_RED}No coins to display...${COLOR_RESET}" END_COMMENT if [ -z "$SYMBOLS" ]; then echo "Usage: ./crypto.sh" echo " - add quotes to ~/.stocks.yaml file" exit 1 fi fi : <<'END_COMMENT' ### Parse options while getopts "g" opt; do case ${opt} in h ) echo "Usage: ./ticker.sh [-g] AAPL GOOG ORA.PA" exit 1 ;; esac done shift $((OPTIND -1)) END_COMMENT # volume %11s echo printf "%s%-22s %-8s %9s %8s %8s %9s %9s %11s %9s %9s %18s %18s %s \n" \ "${BOLD}" "ShortName" "Symbol" \ "Current" "Change" "Percent" \ "Day High" "Day Low" "Volume" \ "52w +" "52w -" "Last Date" "Timezone" \ "${COLOR_RESET}" # Sort arrays (lists of quotes / purchased) IFS=$'\n' SYMBOLS=($(sort <<<"${SYMBOLS[*]}")) purchased=($(sort <<<"${purchased[*]}")) unset IFS ### Create JSON purchased quotes # Create an initial block jq -n '{"quotes":[]}' > "${json_file}" ii=1 # Initialize an array to hold background process IDs (avec les PID activés => affichage dans le désordre (au fil de l'eau)) pids=() echo "quotes: ${#SYMBOLS[@]} - ${SYMBOLS[@]}" echo "purchased: ${#purchased[@]} - ${purchased[@]}" for symbol in "${SYMBOLS[@]}"; do ( # Running in subshell results=$(fetch_chart "$symbol") regularMarketTime=$(echo "$results" | jq -r '.chart.result[0].meta.regularMarketTime') #rdate -d @$regularMarketTime +"%c" rmt=$(LC_ALL=fr_FR.UTF-8 date -d @$regularMarketTime +"%d/%m/%y %H:%M:%S" 2>/dev/null || LC_ALL=fr_FR.UTF-8 date -r $regularMarketTime +"%d/%m/%y %H:%M:%S") exchangeTimezoneName=$(echo "$results" | jq -r '.chart.result[0].meta.exchangeTimezoneName') fiftyTwoWeekHigh=$(echo "$results" | jq -r '.chart.result[0].meta.fiftyTwoWeekHigh') fiftyTwoWeekLow=$(echo "$results" | jq -r '.chart.result[0].meta.fiftyTwoWeekLow') regularMarketDayHigh=$(echo "$results" | jq -r '.chart.result[0].meta.regularMarketDayHigh') regularMarketDayLow=$(echo "$results" | jq -r '.chart.result[0].meta.regularMarketDayLow') regularMarketVolume=$(echo "$results" | jq -r '.chart.result[0].meta.regularMarketVolume') longName=$(echo "$results" | jq -r '.chart.result[0].meta.longName') shortName=$(echo "$results" | jq -r '.chart.result[0].meta.shortName') currentPrice=$(echo "$results" | jq -r '.chart.result[0].meta.regularMarketPrice') previousClose=$(echo "$results" | jq -r '.chart.result[0].meta.chartPreviousClose') currency=$(echo "$results" | jq -r '.chart.result[0].meta.currency') symb=$(symbol_currency $currency) symbol=$(echo "$results" | jq -r '.chart.result[0].meta.symbol') [ "$previousClose" = "null" ] && previousClose="1.0" priceChange=$(awk -v currentPrice="$currentPrice" -v previousClose="$previousClose" 'BEGIN {printf "%.2f", currentPrice - previousClose}') percentChange=$(awk -v currentPrice="$currentPrice" -v previousClose="$previousClose" 'BEGIN {printf "%.2f", ((currentPrice - previousClose) / previousClose) * 100}') open=$(echo "$results" | jq -r '.chart.result[0].indicators.quote' | jq -r '.[].open[0]') #firstTradeDate=$(echo "$results" | jq -r '.chart.result[0].meta.firstTradeDate') #ftd==$(LC_ALL=fr_FR.UTF-8 date -d @$firstTradeDate +"%c" 2>/dev/null || LC_ALL=fr_FR.UTF-8 date -r $ts +"%c") #volume=$(echo "$results" | jq -r '.chart.result[0].indicators.quote' | jq -r '.[].volume[0]') #high=$(echo "$results" | jq -r '.chart.result[0].indicators.quote' | jq -r '.[].high[0]') #close=$(echo "$results" | jq -r '.chart.result[0].indicators.quote' | jq -r '.[].close[0]') if [[ " ${purchased[*]} " == *"$symbol"* ]]; then # Create a temporary JSON file for purchased quotes because requests arrive as they come in (not in order) # https://www.codegix.com/use-jq-to-create-a-dynamic-json-data-in-bash/ json_data=$(jq \ --arg symbol "$symbol" \ --arg price "$currentPrice" \ --arg volume "$regularMarketVolume" \ --arg last "$regularMarketTime" \ --arg symb "$symb" \ --arg percent "$percentChange" \ --argjson value "$ii" '.quotes += [{ "symbol": $symbol, "price": $price, "volume": $volume, "last": $last, "symb": $symb, "percent": $percent }]' "${json_file}") echo "${json_data}" > "${json_file}" ((ii++)) : <<'END_COMMENT' # Check quotes we have (from .stocks.yaml) z=$(echo "$sl" | yq '.lots[] | select(.symbol == "'${symbol}'")') q=$(echo "$z" | yq '.quantity') uc=$(echo "$z" | yq '.unit_cost') purchase_cost=$(echo "$q * $uc" | bc -l) valuations=$(echo "$q * $currentPrice" | bc -l) profit=$(echo "$valuations - $purchase_cost" | bc -l) performance=$(echo "($valuations / $purchase_cost - 1) * 100" | bc -l) #performance=$(round $performance 2) echo "$q - $uc - $purchase_cost - $valuations - $profit - $performance" END_COMMENT fi COLOR_pri=$([ $(CheckSign "$priceChange") == "-" ] && echo ${COLOR_RED} || echo ${COLOR_GREEN}) COLOR_per=$([ $(CheckSign "$percentChange") == "-" ] && echo ${COLOR_RED} || echo ${COLOR_GREEN}) if [ -z "$NO_COLOR" ]; then printf "%s%-22s %-8s %8.2f%s%s %s%7.2f%s%s %s%7.2f%%%s %s%8.2f%s %8.2f%s %11s %8.2f%s %8.2f%s %18s %18s %s\n" \ "${COLOR_YELLOW_BOLD}" "$shortName" "$symbol" \ "$currentPrice" "$symb" "${COLOR_RESET}" "${COLOR_pri}" "$priceChange" "$symb" "${COLOR_RESET}" "${COLOR_per}" "$percentChange" "${COLOR_RESET}" \ "${COLOR_YELLOW_BOLD}" "$regularMarketDayHigh" "$symb" "$regularMarketDayLow" "$symb" "$regularMarketVolume" \ "$fiftyTwoWeekHigh" "$symb" "$fiftyTwoWeekLow" "$symb" "$rmt" "$exchangeTimezoneName" "${COLOR_RESET}"\ else printf "%-22s %-8s %8.2f%s %7.2f%s %7.2f%% %8.2f%s %8.2f%s %11s %8.2f%s %8.2f%s %18s %18s\n" \ "$shortName" "$symbol" \ "$currentPrice" "$symb" "$priceChange" "$symb" "$percentChange" \ "$regularMarketDayHigh" "$symb" "$regularMarketDayLow" "$symb" "$regularMarketVolume" \ "$fiftyTwoWeekHigh" "$symb" "$fiftyTwoWeekLow" "$symb" "$rmt" "$exchangeTimezoneName" fi ) #& # Stack PIDs pids+=($!) done # Wait for all background processes to finish for pid in "${pids[@]}"; do wait "$pid" done sleep 1 echo "quotes: ${#SYMBOLS[@]} - ${SYMBOLS[@]}" echo "purchased: ${#purchased[@]} - ${purchased[@]}" #cat $json_file if [ ${#purchased[@]} -gt 0 ]; then echo echo -e "${COLOR_GREEN}Displaying purchased quotes ...${COLOR_RESET}" echo quotes="$(cat $json_file)" counter=0 printf "%s%-8s %13s %10s %9s %11s %16s %13s %13s %13s %11s %18s %s\n" \ "${BOLD}" "Symbol" "Percent" "Current" "Quantity" "Unit cost" "Purchase cost" "Valuations" "Profit" "Performance" "Volume" "Last quote" "${COLOR_RESET}" while [ "$counter" -lt "${#purchased[@]}" ]; do purchase="${purchased[$counter]}" s=$(echo "$quotes" | jq -r '.quotes | .[] | select(.symbol == "'${purchase}'") | (.symbol)') # Symbol p=$(echo "$quotes" | jq -r '.quotes | .[] | select(.symbol == "'${purchase}'") | (.price)') # Price v=$(echo "$quotes" | jq -r '.quotes | .[] | select(.symbol == "'${purchase}'") | (.volume)') # Volume l=$(echo "$quotes" | jq -r '.quotes | .[] | select(.symbol == "'${purchase}'") | (.last)') # Last quote rmt=$(LC_ALL=fr_FR.UTF-8 date -d @$l +"%d/%m/%y %H:%M:%S" 2>/dev/null || LC_ALL=fr_FR.UTF-8 date -r $l +"%d/%m/%y %H:%M:%S") #echo $l # 1736526916 #rmt=$(LC_ALL=fr_FR.UTF-8 date -d "@$l" +"%c" 2>/dev/null || LC_ALL=fr_FR.UTF-8 date -r "$l" +"%c") sy=$(echo "$quotes" | jq -r '.quotes | .[] | select(.symbol == "'${purchase}'") | (.symb)') # Symb (€/$/£) pc=$(echo "$quotes" | jq -r '.quotes | .[] | select(.symbol == "'${purchase}'") | (.percent)') # Percent change z=$(echo "$sl" | yq '.lots[] | select(.symbol == "'${purchase}'")') q=$(echo "$z" | yq '.quantity') uc=$(echo "$z" | yq '.unit_cost') purchase_cost=$(echo "$q * $uc" | bc -l) valuations=$(echo "$q * $p" | bc -l) profit=$(echo "$valuations - $purchase_cost" | bc -l) performance=$(echo "($valuations / $purchase_cost - 1) * 100" | bc -l) #performance=$(round $performance 2) COLOR_performance=$([ $(CheckSign "$performance") == "-" ] && echo ${COLOR_RED} || echo ${COLOR_GREEN}) #echo "$s - $p - $v - $rmt - $q - $uc - $purchase_cost - $valuations - $profit - $performance" #echo "$purchase_cost - $valuations - $profit - $performance" # symbol(s) percent (pc) price(p) quantity(q) unit_cost(uc) purchase_cost valuations profit performance volume(v) last_quote(l) printf "%s%-8s %12.2f%% %9.2f%s %9d %10.2f%s %15.2f%s %12.2f%s %12.2f%s %12.2f%% %11d %18s %s\n" \ "${COLOR_performance}" "$s" "$pc" "$p" "$sy" "$q" "$uc" "$sy" "$purchase_cost" "$sy" "$valuations" "$sy" "$profit" "$sy" "$performance" "$v" "$rmt" "${COLOR_RESET}" ((++counter)) done fi echo -e "\n${ITALIC}Script with ${#SYMBOLS[@]} requests to Yahoo Finance API finished in $SECONDS seconds.${COLOR_RESET}"