Files
stocks/stocks.sh
2025-01-13 20:35:04 +01:00

477 lines
15 KiB
Bash
Executable File

#!/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 [<stock>]"
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}"