477 lines
15 KiB
Bash
Executable File
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}"
|