Compare commits
No commits in common. "031c5ff8dd8015ab0eea020508d99eb88c706fad" and "7bd60d938f69a49c8a0bd00a25fe6aab8a6cab97" have entirely different histories.
031c5ff8dd
...
7bd60d938f
5 changed files with 110 additions and 379 deletions
|
|
@ -4,4 +4,4 @@ This should be the folder:
|
||||||
|
|
||||||
~/.conky/mgconky/
|
~/.conky/mgconky/
|
||||||
|
|
||||||
Thank you!
|
Thank you.
|
||||||
|
|
|
||||||
33
conf
33
conf
|
|
@ -15,11 +15,10 @@
|
||||||
-- !!!IMPORTANT!!! THE FOLLOWING ARE REQUIREMENTS FOR THIS SCRIPT TO WORK PROPERLY:
|
-- !!!IMPORTANT!!! THE FOLLOWING ARE REQUIREMENTS FOR THIS SCRIPT TO WORK PROPERLY:
|
||||||
-- (1) DEPENDENCIES.
|
-- (1) DEPENDENCIES.
|
||||||
-- Install the following software, if not already installed:
|
-- Install the following software, if not already installed:
|
||||||
-- "Conky" sudo apt-get install conky-all
|
-- "Conky" sudo apt-get install conky-all (or use software manager)
|
||||||
-- "Jq" sudo apt-get install jq
|
-- "Jq" sudo apt-get install jq (or use software manager)
|
||||||
-- "Curl" sudo apt-get install curl
|
-- "Curl" sudo apt-get install curl (or use software manager)
|
||||||
-- "Wget" sudo apt-get install wget
|
-- "Wget" sudo apt-get install wget (or use software manager)
|
||||||
-- "python3" sudo apt-get install python3
|
|
||||||
-- (2) FONTS.
|
-- (2) FONTS.
|
||||||
-- Install the following custom fonts:
|
-- Install the following custom fonts:
|
||||||
-- "Neuropolitical" Place .ttf file in ~/.fonts/ https://www.dafont.com/font-comment.php?file=neuropolitical
|
-- "Neuropolitical" Place .ttf file in ~/.fonts/ https://www.dafont.com/font-comment.php?file=neuropolitical
|
||||||
|
|
@ -29,9 +28,8 @@
|
||||||
-- chmod +x ~/.conky/mgconky/weather/get_weather.sh
|
-- chmod +x ~/.conky/mgconky/weather/get_weather.sh
|
||||||
-- chmod +x ~/.conky/mgconky/weather/parse_weather.sh
|
-- chmod +x ~/.conky/mgconky/weather/parse_weather.sh
|
||||||
-- chmod +x ~/.conky/mgconky/weather/parse_forecast.sh
|
-- chmod +x ~/.conky/mgconky/weather/parse_forecast.sh
|
||||||
-- chmod +x ~/.conky/mgconky/stocks/get_stocks_alphavantage.py
|
-- chmod +x ~/.conky/mgconky/weather/parse_forecast.sh
|
||||||
-- chmod +x ~/.conky/mgconky/stocks/process_stocks_alphavantage.py
|
-- chmod +x ~/.conky/mgconky/stocks/get_stocks.py (Requires python3, which is usually pre-installed on your OS)
|
||||||
-- chmod +x ~/.conky/mgconky/stocks/get_stocks_finnhub.py
|
|
||||||
-- (4) WEATHER.
|
-- (4) WEATHER.
|
||||||
-- Make a free account at https://openweathermap.org/
|
-- Make a free account at https://openweathermap.org/
|
||||||
-- Write down your API key, which is found on the "API keys" tab after you log in. (https://home.openweathermap.org/api_key
|
-- Write down your API key, which is found on the "API keys" tab after you log in. (https://home.openweathermap.org/api_key
|
||||||
|
|
@ -93,16 +91,16 @@ conky.config = {
|
||||||
color7 = "#FF0000", -- Bad values (red)
|
color7 = "#FF0000", -- Bad values (red)
|
||||||
|
|
||||||
-- Weather variables
|
-- Weather variables
|
||||||
template0 = "<ENTER YOUR OPEANWEATHERMAP API KEY HERE>", -- OpenWeatherMap API key (https://home.openweathermap.org/api_keys)
|
template0 = "YOUR_OPENWEATHERMAP_API_KEY_HERE", -- OpenWeatherMap API key (https://home.openweathermap.org/api_keys)
|
||||||
template1 = "4467657", -- OpenWeatherMap City ID (the number in the URL of your city, for example: https://openweathermap.org/city/5128581)
|
template1 = "YOUR_OPENWEATHERMAP_CITY_ID_HERE", -- OpenWeatherMap City ID (the number in the URL of your city, for example: https://openweathermap.org/city/5128581)
|
||||||
template2 = "imperial", -- Temp unit ("default" for Kelvin, "metric" for Celsius, "imperial" for Fahrenheit)
|
template2 = "imperial", -- Temp unit ("default" for Kelvin, "metric" for Celsius, "imperial" for Fahrenheit)
|
||||||
template3 = "", -- Locale (e.g., "es_ES.UTF-8") # Leave empty for default
|
template3 = "", -- Locale (e.g., "es_ES.UTF-8") # Leave empty for default
|
||||||
|
|
||||||
-- Stock variables
|
-- Stock variables
|
||||||
template4 = "<ENTER YOUR FINNHUB API KEY HERE>", -- FinnHub API key (https://finnhub.io/)
|
template4 = "YOUR_FINNHUB_API_KEY_HERE", -- FinnHub API key (https://finnhub.io/)
|
||||||
template5 = "<ENTER YOUR ALPHAVANTAGE API KEY HERE", -- Alpha Vantage API key (https://www.alphavantage.co/)
|
template5 = "YOUR_ALPHAVANTAGE_API_KEY_HERE", -- Alpha Vantage API key (https://www.alphavantage.co/)
|
||||||
template6 = "spy,dia,nvda,tsla,amzn,aapl,msft,meta,pg,ibit", -- Stock symbols for FinnHub (comma separated, no spaces, i.e. goog,amzn,aapl)
|
template6 = "goog,amzn,aapl,msft,meta,tsla,avgo,tsm,brk.a,pg,nvda", -- Stock symbols for FinnHub (comma separated, no spaces, i.e. goog,amzn,aapl)
|
||||||
template7 = "tsla,spy,dia", -- Stock symbols for Alpha Vantage (keep to a minimum unless you have a paid API key)
|
template7 = "nvda", -- Stock symbols for Alpha Vantage (keep to a minimum unless you have a paid API key)
|
||||||
|
|
||||||
-- Load Lua script(s) -- If multiple files, separate each path with a space. They should all be loaded on a single lua_load command.
|
-- Load Lua script(s) -- If multiple files, separate each path with a space. They should all be loaded on a single lua_load command.
|
||||||
-- lua_load = "~/.conky/mgconky/script1.lua ~/.conky/mgconky/script2.lua",
|
-- lua_load = "~/.conky/mgconky/script1.lua ~/.conky/mgconky/script2.lua",
|
||||||
|
|
@ -179,8 +177,7 @@ ${voffset -6}${execpi 60 $HOME/.conky/mgconky/stocks/get_stocks_finnhub.py --api
|
||||||
${endif}
|
${endif}
|
||||||
# ***** Alpha Vantage API *****
|
# ***** Alpha Vantage API *****
|
||||||
${if_match "${template5}" != "YOUR_ALPHAVANTAGE_API_KEY_HERE"}
|
${if_match "${template5}" != "YOUR_ALPHAVANTAGE_API_KEY_HERE"}
|
||||||
${execpi 43200 $HOME/.conky/mgconky/stocks/get_stocks_alphavantage.py --api_key ${template5} --symbols ${template7} --range_in_days 30}
|
${voffset -12}${execpi 7200 $HOME/.conky/mgconky/stocks/get_stocks_alphavantage.py --api_key ${template5} --symbols ${template7} --range_in_days 30 --price_dec_places 0 --percent_dec_places 1}
|
||||||
${voffset -12}${execpi 60 $HOME/.conky/mgconky/stocks/process_stocks_alphavantage.py --symbols ${template7} --range_in_days 30 --price_dec_places 0 --percent_dec_places 1 --stale_seconds 43200}
|
|
||||||
${endif}
|
${endif}
|
||||||
#
|
#
|
||||||
#--------------------------
|
#--------------------------
|
||||||
|
|
@ -237,7 +234,7 @@ ${voffset -6}Total: ${color3}${totaldown ${gw_iface}}${color}${goto 140}Total: $
|
||||||
# Connections - netstat shows number of connections from your computer and application/PID making it. Kill spyware!
|
# Connections - netstat shows number of connections from your computer and application/PID making it. Kill spyware!
|
||||||
#--------------------
|
#--------------------
|
||||||
${voffset 6}${color0}${font Neuropolitical:size=8:bold}CONNECTIONS ${color1}${hr 2}${color}${font Courier:size=9}
|
${voffset 6}${color0}${font Neuropolitical:size=8:bold}CONNECTIONS ${color1}${hr 2}${color}${font Courier:size=9}
|
||||||
#${voffset 2}${color3}${execi 30 netstat -ept | grep ESTAB | awk '{print $9}' | cut -d: -f1 | sort | uniq -c | sort -nr}${color}
|
${voffset 6}Num. connections / PID / Process
|
||||||
${voffset 2}${lua_parse conky_get_connections}
|
${voffset 2}${color3}${execi 30 netstat -ept | grep ESTAB | awk '{print $9}' | cut -d: -f1 | sort | uniq -c | sort -nr}${color}
|
||||||
]]
|
]]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ function conky_shorten_cpu_name(cpu_name)
|
||||||
:gsub("CPU", "") -- Remove "CPU"
|
:gsub("CPU", "") -- Remove "CPU"
|
||||||
:gsub("Processor", "") -- Remove "Processor"
|
:gsub("Processor", "") -- Remove "Processor"
|
||||||
:gsub("@[%s%w%.]+", "") -- Remove "@ XGHz"
|
:gsub("@[%s%w%.]+", "") -- Remove "@ XGHz"
|
||||||
:gsub("with.*", "") -- Remove "with" and anything after the word "with"
|
|
||||||
:gsub("%s%s+", " ") -- Remove extra spaces
|
:gsub("%s%s+", " ") -- Remove extra spaces
|
||||||
:gsub("^%s+", "") -- Trim leading spaces
|
:gsub("^%s+", "") -- Trim leading spaces
|
||||||
:gsub("%s+$", "") -- Trim trailing spaces
|
:gsub("%s+$", "") -- Trim trailing spaces
|
||||||
|
|
@ -52,7 +51,7 @@ function conky_get_cpu_info()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ########################################################## GET GPU MAKE AND MODEL ##########################################################
|
-- ########################################################## GET GU MAKE AND MODEL ###########################################################
|
||||||
|
|
||||||
function conky_shorten_gpu_name(gpu_name)
|
function conky_shorten_gpu_name(gpu_name)
|
||||||
return (gpu_name
|
return (gpu_name
|
||||||
|
|
@ -74,15 +73,11 @@ function conky_shorten_gpu_name(gpu_name)
|
||||||
:gsub("Raspberry Pi Foundation", "Raspberry Pi")
|
:gsub("Raspberry Pi Foundation", "Raspberry Pi")
|
||||||
:gsub("Broadcom Inc%.", "Broadcom")
|
:gsub("Broadcom Inc%.", "Broadcom")
|
||||||
:gsub("Lite Hash Rate", "LHR")
|
:gsub("Lite Hash Rate", "LHR")
|
||||||
:gsub("Renoir", "") -- Remove "Renoir"
|
:gsub("GA%d+%s%[", "") -- Remove chip identifier like "GA106 ["
|
||||||
:gsub("Ryzen %d+/%d+ Mobile Series", "") -- Remove Ryzen details (for APUs)
|
:gsub("%].*", "") -- Remove everything after "]"
|
||||||
:gsub("%(Ryzen.-%)", "") -- Remove Ryzen-related info in parentheses
|
:gsub("%(.*%)", "") -- Remove revision info like "(rev a1)"
|
||||||
:gsub("GA%d+%s%[", "") -- Remove chip identifier like "GA106 ["
|
:gsub("%s+", " ") -- Normalize spaces
|
||||||
:gsub("%[.*%]%s*", "") -- Remove chip code in brackets like "[1002:1636]"
|
:match("^%s*(.-)%s*$") -- Trim leading/trailing spaces
|
||||||
:gsub("%((rev .-)%)", "") -- Remove revision info like "(rev a1)"
|
|
||||||
:gsub("%s+", " ") -- Normalize spaces
|
|
||||||
:gsub("%]%s*$", "") -- Remove trailing square bracket "]" if it exists
|
|
||||||
:match("^%s*(.-)%s*$") -- Trim leading/trailing spaces
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -171,23 +166,6 @@ function conky_get_drives_and_volumes()
|
||||||
if #data.mountpoints > 0 then
|
if #data.mountpoints > 0 then
|
||||||
output = output .. '${voffset 0}${color0}${font Neuropolitical:size=8:bold}DRIVE${font Courier:size=9} /dev/' .. drive .. '${color} ${color1}${hr 2}${color}\n'
|
output = output .. '${voffset 0}${color0}${font Neuropolitical:size=8:bold}DRIVE${font Courier:size=9} /dev/' .. drive .. '${color} ${color1}${hr 2}${color}\n'
|
||||||
for _, mount in ipairs(data.mountpoints) do
|
for _, mount in ipairs(data.mountpoints) do
|
||||||
|
|
||||||
-- Skip entries that are not real mountpoints like [SWAP]
|
|
||||||
if mount:sub(1, 1) ~= "/" then
|
|
||||||
goto continue
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Skip common temporary mounts
|
|
||||||
if mount:match("^/sys/") then goto continue end
|
|
||||||
if mount:match("^/proc/") then goto continue end
|
|
||||||
if mount:match("^/dev/") then goto continue end
|
|
||||||
|
|
||||||
-- Skip mounts under /var/, but allow a mount at /var itself
|
|
||||||
if mount ~= "/var" and mount:match("^/var/") then goto continue end
|
|
||||||
|
|
||||||
-- Skip runtime mounts except removable media
|
|
||||||
if mount:match("^/run/") and not mount:match("^/run/media/") then goto continue end
|
|
||||||
|
|
||||||
-- Use df to get the size and used space for the mount
|
-- Use df to get the size and used space for the mount
|
||||||
local handle = io.popen("df -h --output=target,size,used " .. mount .. " | tail -n 1")
|
local handle = io.popen("df -h --output=target,size,used " .. mount .. " | tail -n 1")
|
||||||
local df_result = handle:read("*a")
|
local df_result = handle:read("*a")
|
||||||
|
|
@ -195,19 +173,10 @@ function conky_get_drives_and_volumes()
|
||||||
|
|
||||||
-- Parse df output
|
-- Parse df output
|
||||||
local target, size, used = df_result:match("(%S+)%s+(%S+)%s+(%S+)")
|
local target, size, used = df_result:match("(%S+)%s+(%S+)%s+(%S+)")
|
||||||
|
|
||||||
-- Ensure mount still exists before emitting fs_bar
|
|
||||||
local test = io.open(target, "r")
|
|
||||||
if not test then goto continue end
|
|
||||||
test:close()
|
|
||||||
|
|
||||||
-- Create output lines for this mountpoint.
|
|
||||||
if target and size and used then
|
if target and size and used then
|
||||||
output = output .. '${voffset 2}' .. target .. ': ${alignr}${color3}' .. used .. 'B${color} of ${color3}' .. size .. 'B${color}\n'
|
output = output .. '${voffset 2}' .. target .. ': ${alignr}${color3}' .. used .. 'B${color} of ${color3}' .. size .. 'B${color}\n'
|
||||||
output = output .. '${voffset -2}${color5}${fs_bar ' .. target .. '}${color}\n'
|
output = output .. '${voffset -2}${color5}${fs_bar ' .. target .. '}${color}\n'
|
||||||
end
|
end
|
||||||
|
|
||||||
::continue::
|
|
||||||
end
|
end
|
||||||
output = output .. '\n' -- Add a blank line after the last mount point of the current drive
|
output = output .. '\n' -- Add a blank line after the last mount point of the current drive
|
||||||
end
|
end
|
||||||
|
|
@ -387,64 +356,3 @@ print(" CHECK SWAP STATUS = " .. conky_check_swap_status())
|
||||||
print(" GET MEMORY USAGE = " .. conky_get_memory_usage("mem"))
|
print(" GET MEMORY USAGE = " .. conky_get_memory_usage("mem"))
|
||||||
print(" GET SWAP USAGE = " .. conky_get_memory_usage("swap"))
|
print(" GET SWAP USAGE = " .. conky_get_memory_usage("swap"))
|
||||||
|
|
||||||
|
|
||||||
-- ####################################################### GET ACTIVE CONNECTIONS ##############################################################
|
|
||||||
|
|
||||||
function conky_get_key_with_highest_value(t)
|
|
||||||
local max_key = nil
|
|
||||||
local max_value = -math.huge -- Initialize with the smallest possible number
|
|
||||||
|
|
||||||
for key, value in pairs(t) do
|
|
||||||
if type(value) == "number" and value > max_value then
|
|
||||||
max_value = value
|
|
||||||
max_key = key
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return max_key
|
|
||||||
end
|
|
||||||
|
|
||||||
function conky_get_connections()
|
|
||||||
local handle = io.popen("ss -eptH | grep ESTAB 2>/dev/null")
|
|
||||||
if not handle then return "Error: Unable to execute ss command" end
|
|
||||||
|
|
||||||
local process_names = {}
|
|
||||||
local process_counts = {}
|
|
||||||
|
|
||||||
-- Read each line of output
|
|
||||||
for line in handle:lines() do
|
|
||||||
-- Extract process name and PID using pattern matching
|
|
||||||
local pname, pid = line:match('users:%(%("([^"]+)",pid=(%d+)')
|
|
||||||
pid = tonumber(pid) -- Ensure pid is a number
|
|
||||||
if pname and pid then
|
|
||||||
-- Store process name by PID and count occurrences
|
|
||||||
process_names[pid] = pname
|
|
||||||
process_counts[pid] = (process_counts[pid] or 0) + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
handle:close()
|
|
||||||
|
|
||||||
-- Header line
|
|
||||||
local tab1_offset = "${goto 145}"
|
|
||||||
local line_prefix = "${voffset 0}${color}${font StyleBats:size=10}h${font Courier:size=9}${voffset -1}${color3}"
|
|
||||||
local result = string.format("${color}Process%sPID${alignr}Num \n", tab1_offset)
|
|
||||||
|
|
||||||
-- Sort and format output
|
|
||||||
while true do
|
|
||||||
local max_key = conky_get_key_with_highest_value(process_counts)
|
|
||||||
|
|
||||||
if max_key == nil then
|
|
||||||
break -- Stop when the table is empty
|
|
||||||
end
|
|
||||||
|
|
||||||
result = result .. string.format("%s %s%s%d${alignr}%d \n", line_prefix, string.sub(process_names[max_key], 1, 16), tab1_offset, max_key, process_counts[max_key])
|
|
||||||
|
|
||||||
-- Remove the highest key from both tables
|
|
||||||
process_counts[max_key] = nil
|
|
||||||
process_names[max_key] = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,11 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import json
|
|
||||||
import argparse
|
|
||||||
import requests
|
import requests
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
import argparse
|
||||||
# Directory where per-symbol cache JSON files are stored
|
|
||||||
CACHE_DIR = os.path.expanduser("~/.cache/mgconky/stocks_alphavantage/")
|
|
||||||
|
|
||||||
# Delay between API calls to respect AlphaVantage rate limits
|
|
||||||
API_DELAY_SECONDS = 1
|
|
||||||
|
|
||||||
# Minimum age before re-querying a symbol to avoid API thrashing
|
|
||||||
NO_THRASH_SECONDS = 60 * 60 # 1 hour
|
|
||||||
|
|
||||||
|
|
||||||
def log_error(message):
|
|
||||||
"""Print timestamped error message to stderr and flush immediately."""
|
|
||||||
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
print(f"[{ts}] {message}", file=sys.stderr, flush=True)
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_intraday_data(api_key, symbol, interval="1min"):
|
def fetch_intraday_data(api_key, symbol, interval="1min"):
|
||||||
# Query AlphaVantage intraday endpoint for the given symbol
|
"""Fetch current and historical intraday price using TIME_SERIES_INTRADAY."""
|
||||||
url = "https://www.alphavantage.co/query"
|
url = "https://www.alphavantage.co/query"
|
||||||
params = {
|
params = {
|
||||||
"function": "TIME_SERIES_INTRADAY",
|
"function": "TIME_SERIES_INTRADAY",
|
||||||
|
|
@ -39,31 +19,36 @@ def fetch_intraday_data(api_key, symbol, interval="1min"):
|
||||||
data = response.json()
|
data = response.json()
|
||||||
time_series = data.get(f"Time Series ({interval})")
|
time_series = data.get(f"Time Series ({interval})")
|
||||||
|
|
||||||
|
# Debug: Log the raw response
|
||||||
|
#print(f"DEBUG: Response data for {symbol}: {data}")
|
||||||
|
|
||||||
if time_series:
|
if time_series:
|
||||||
# Use earliest bar as open price and latest bar as current price
|
# Sort the timestamps to get the first (open) and latest price
|
||||||
sorted_timestamps = sorted(time_series.keys())
|
sorted_timestamps = sorted(time_series.keys())
|
||||||
open_time = sorted_timestamps[0]
|
open_time = sorted_timestamps[0] # Earliest timestamp of the day
|
||||||
latest_time = sorted_timestamps[-1]
|
latest_time = sorted_timestamps[-1] # Most recent timestamp
|
||||||
|
|
||||||
|
# Extract open price and latest price
|
||||||
|
open_price = float(time_series[open_time]["1. open"])
|
||||||
|
latest_price = float(time_series[latest_time]["4. close"])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"current_price": float(time_series[latest_time]["4. close"]),
|
"current_price": latest_price,
|
||||||
"compare_price": float(time_series[open_time]["1. open"]),
|
"compare_price": open_price
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
# API responded but did not include expected time series data
|
print(f"Error: No 'Time Series ({interval})' data found for {symbol}.")
|
||||||
log_error(f"No 'Time Series ({interval})' data found for {symbol}.")
|
return None
|
||||||
else:
|
else:
|
||||||
# HTTP error from AlphaVantage
|
print(f"Error: Failed to fetch intraday data for {symbol} (HTTP {response.status_code})")
|
||||||
log_error(f"Failed to fetch intraday data for {symbol} (HTTP {response.status_code})")
|
return None
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
# Network or request failure
|
print(f"Error: Failed to fetch intraday data for {symbol} - {e}")
|
||||||
log_error(f"Failed to fetch intraday data for {symbol} - {e}")
|
return None
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_historical_data(api_key, symbol, range_in_days):
|
def fetch_historical_data(api_key, symbol, range_in_days):
|
||||||
# Query AlphaVantage daily endpoint for historical comparison
|
"""Fetch current and historical price using TIME_SERIES_DAILY, allowing fallback to nearby dates."""
|
||||||
url = "https://www.alphavantage.co/query"
|
url = "https://www.alphavantage.co/query"
|
||||||
params = {
|
params = {
|
||||||
"function": "TIME_SERIES_DAILY",
|
"function": "TIME_SERIES_DAILY",
|
||||||
|
|
@ -76,109 +61,96 @@ def fetch_historical_data(api_key, symbol, range_in_days):
|
||||||
data = response.json()
|
data = response.json()
|
||||||
time_series = data.get("Time Series (Daily)")
|
time_series = data.get("Time Series (Daily)")
|
||||||
|
|
||||||
|
# Debug: Log the raw response
|
||||||
|
#print(f"DEBUG: Response data for {symbol}: {data}")
|
||||||
|
|
||||||
if time_series:
|
if time_series:
|
||||||
# Most recent trading day's close is the current price
|
# Current price is the latest closing price
|
||||||
latest_date = max(time_series.keys())
|
latest_date = max(time_series.keys())
|
||||||
current_price = float(time_series[latest_date]["4. close"])
|
latest_data = time_series[latest_date]
|
||||||
|
current_price = float(latest_data["4. close"])
|
||||||
|
|
||||||
# Target calendar date for comparison
|
# Dates to check: exact, one day before, one day after
|
||||||
target = datetime.now() - timedelta(days=range_in_days)
|
target_date = (datetime.now() - timedelta(days=range_in_days)).strftime("%Y-%m-%d")
|
||||||
|
fallback_dates = [
|
||||||
|
target_date,
|
||||||
|
(datetime.now() - timedelta(days=range_in_days + 1)).strftime("%Y-%m-%d"),
|
||||||
|
(datetime.now() - timedelta(days=range_in_days - 1)).strftime("%Y-%m-%d")
|
||||||
|
]
|
||||||
|
|
||||||
# Walk backward up to 10 days to find a valid trading day
|
# Find the first available date in the fallback list
|
||||||
historical_price = None
|
historical_price = None
|
||||||
for offset in range(10):
|
for date in fallback_dates:
|
||||||
d = (target - timedelta(days=offset)).strftime("%Y-%m-%d")
|
if date in time_series:
|
||||||
if d in time_series:
|
historical_price = float(time_series[date]["4. close"])
|
||||||
historical_price = float(time_series[d]["4. close"])
|
|
||||||
break
|
break
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"current_price": current_price,
|
"current_price": current_price,
|
||||||
"compare_price": historical_price,
|
"compare_price": historical_price
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
# API responded but did not include expected daily series data
|
print(f"Error: No 'Time Series (Daily)' data found for {symbol}.")
|
||||||
log_error(f"No 'Time Series (Daily)' data found for {symbol}.")
|
return None
|
||||||
else:
|
else:
|
||||||
# HTTP error from AlphaVantage
|
print(f"Error: Failed to fetch historical data for {symbol} (HTTP {response.status_code})")
|
||||||
log_error(f"Failed to fetch historical data for {symbol} (HTTP {response.status_code})")
|
return None
|
||||||
|
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
# Network or request failure
|
print(f"Error: Failed to fetch historical data for {symbol} - {e}")
|
||||||
log_error(f"Failed to fetch historical data for {symbol} - {e}")
|
return None
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Parse command-line arguments
|
# Parse command-line arguments
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser(description="Fetch stock data from Alpha Vantage.")
|
||||||
parser.add_argument("--api_key", required=True)
|
parser.add_argument("--api_key", required=True, help="Your Alpha Vantage API key")
|
||||||
parser.add_argument("--symbols", required=True)
|
parser.add_argument("--symbols", required=True, help="Comma-separated list of stock symbols")
|
||||||
parser.add_argument("--range_in_days", type=int, default=0)
|
parser.add_argument("--range_in_days", type=int, default=0, help="Number of days for historical comparison (0 for none)")
|
||||||
|
parser.add_argument("--price_dec_places", type=int, default=0, help="Decimal places for prices")
|
||||||
|
parser.add_argument("--percent_dec_places", type=int, default=1, help="Decimal places for percentages")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Normalize and split ticker symbols
|
# Split symbols
|
||||||
symbols = args.symbols.strip().upper().split(",")
|
symbols = args.symbols.strip().upper().split(",")
|
||||||
|
|
||||||
# Ensure cache directory exists
|
# Use a list to build the final output string
|
||||||
os.makedirs(CACHE_DIR, exist_ok=True)
|
output = []
|
||||||
|
|
||||||
for i, symbol in enumerate(symbols):
|
# Prepare for printing output
|
||||||
cache_path = os.path.join(CACHE_DIR, f"{symbol}.json")
|
color_header = "${color}"
|
||||||
|
color_label = "${color3}"
|
||||||
|
color_value = "${color3}"
|
||||||
|
color_good = "${color6}"
|
||||||
|
color_bad = "${color7}"
|
||||||
|
line_tab1_offset = "${goto 25}"
|
||||||
|
line_tab2_offset = "${goto 90}"
|
||||||
|
line_tab3_offset = "${alignr}" # Could also replace this with goto 120 if you don't like the right alignment
|
||||||
|
|
||||||
# --- No-thrash protection: skip API call if cache is still fresh ---
|
# Iterate symbols
|
||||||
if os.path.exists(cache_path):
|
for symbol in symbols:
|
||||||
try:
|
fetched_data = fetch_intraday_data(args.api_key, symbol) if args.range_in_days < 1 else fetch_historical_data(args.api_key, symbol, args.range_in_days)
|
||||||
with open(cache_path, "r") as f:
|
if fetched_data:
|
||||||
payload = json.load(f)
|
current_price = fetched_data["current_price"]
|
||||||
|
compare_price = fetched_data["compare_price"]
|
||||||
if (
|
price_difference = current_price - compare_price
|
||||||
isinstance(payload, dict)
|
percent_change = (price_difference / current_price) * 100
|
||||||
and "timestamp" in payload
|
color_dynamic = (
|
||||||
and isinstance(payload["timestamp"], (int, float))
|
color_good if round(price_difference, args.price_dec_places) > 0
|
||||||
and (time.time() - payload["timestamp"]) < NO_THRASH_SECONDS
|
else color_bad if round(price_difference, args.price_dec_places) < 0
|
||||||
):
|
else color_value
|
||||||
# Cached data is recent enough -> skip querying AlphaVantage
|
)
|
||||||
continue
|
output.append(
|
||||||
except Exception:
|
f"{line_tab1_offset}{color_label}{symbol}: {line_tab2_offset}{color_value}{round(current_price, args.price_dec_places):.{args.price_dec_places}f} "
|
||||||
# Any read/parse error -> ignore cache and refetch
|
f"{line_tab3_offset}{color_dynamic}{round(price_difference, args.price_dec_places):+.{args.price_dec_places}f} "
|
||||||
pass
|
f"({round(percent_change, args.percent_dec_places):+.{args.percent_dec_places}f}%)"
|
||||||
|
)
|
||||||
# Fetch either intraday or historical data depending on range
|
else:
|
||||||
fetched_data = (
|
output.append(f"{symbol}: Error fetching data")
|
||||||
fetch_intraday_data(args.api_key, symbol)
|
|
||||||
if args.range_in_days < 1
|
|
||||||
else fetch_historical_data(args.api_key, symbol, args.range_in_days)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Only write cache if fetched data is structurally valid
|
|
||||||
if (
|
|
||||||
isinstance(fetched_data, dict)
|
|
||||||
and "current_price" in fetched_data
|
|
||||||
and isinstance(fetched_data["current_price"], (int, float))
|
|
||||||
):
|
|
||||||
tmp = os.path.join(CACHE_DIR, f"{symbol}.json.tmp")
|
|
||||||
final = os.path.join(CACHE_DIR, f"{symbol}.json")
|
|
||||||
|
|
||||||
# Payload includes metadata for staleness and debugging
|
|
||||||
payload = {
|
|
||||||
"symbol": symbol,
|
|
||||||
"range_in_days": args.range_in_days,
|
|
||||||
"timestamp": int(time.time()),
|
|
||||||
"data": fetched_data,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Atomic write to avoid corrupting existing cache
|
|
||||||
with open(tmp, "w") as f:
|
|
||||||
json.dump(payload, f)
|
|
||||||
os.replace(tmp, final)
|
|
||||||
|
|
||||||
# Rate-limit delay between symbols
|
|
||||||
if i < len(symbols) - 1:
|
|
||||||
time.sleep(API_DELAY_SECONDS)
|
|
||||||
|
|
||||||
|
# Join all parts of the output and print it
|
||||||
|
header_label = f"Intraday" if args.range_in_days < 1 else f"{args.range_in_days} Day"
|
||||||
|
header_line = f"{line_tab1_offset}{color_header}Ticker{line_tab2_offset}Price ($$){line_tab3_offset}{header_label}{color_label}"
|
||||||
|
return header_line + "\n" + f"{line_tab1_offset}{color_header}${{voffset -5}}${{hr 1}}" + "\n" + "\n".join(output)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
print(main())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import json
|
|
||||||
import argparse
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
# Directory containing cached per-symbol JSON files
|
|
||||||
CACHE_DIR = os.path.expanduser("~/.cache/mgconky/stocks_alphavantage/")
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Parse command-line arguments (reader does not need api_key)
|
|
||||||
parser = argparse.ArgumentParser(description="Process cached Alpha Vantage stock data.")
|
|
||||||
parser.add_argument("--symbols", required=True, help="Comma-separated list of stock symbols")
|
|
||||||
parser.add_argument("--range_in_days", type=int, default=0, help="Number of days for historical comparison")
|
|
||||||
parser.add_argument("--price_dec_places", type=int, default=0, help="Decimal places for prices")
|
|
||||||
parser.add_argument("--percent_dec_places", type=int, default=1, help="Decimal places for percentages")
|
|
||||||
parser.add_argument("--stale_seconds", type=int, default=13 * 3600, help="Seconds before cached data is considered stale")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# Split ticker symbols by comma
|
|
||||||
symbols = args.symbols.strip().upper().split(",")
|
|
||||||
|
|
||||||
# Collect formatted output lines for Conky
|
|
||||||
output = []
|
|
||||||
|
|
||||||
# Conky color and layout formatting
|
|
||||||
color_header = "${color}"
|
|
||||||
color_label = "${color3}"
|
|
||||||
color_value = "${color3}"
|
|
||||||
color_good = "${color6}"
|
|
||||||
color_bad = "${color7}"
|
|
||||||
line_tab1_offset = "${goto 25}"
|
|
||||||
line_tab2_offset = "${goto 90}"
|
|
||||||
line_tab3_offset = "${alignr}"
|
|
||||||
|
|
||||||
# Process each symbol
|
|
||||||
for symbol in symbols:
|
|
||||||
cache_path = os.path.join(CACHE_DIR, f"{symbol}.json")
|
|
||||||
|
|
||||||
# Missing cache file -- display formatted placeholder
|
|
||||||
if not os.path.exists(cache_path):
|
|
||||||
output.append(
|
|
||||||
f"{line_tab1_offset}{color_bad}{symbol}: "
|
|
||||||
f"{line_tab2_offset}{color_value}-- "
|
|
||||||
f"{line_tab3_offset}{color_value}-- (--%)"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Determine cache age using file modification time
|
|
||||||
age_seconds = time.time() - os.stat(cache_path).st_mtime
|
|
||||||
|
|
||||||
# Symbol turns red (color_bad) if cached data is stale
|
|
||||||
symbol_color = color_bad if age_seconds > args.stale_seconds else color_label
|
|
||||||
|
|
||||||
# Load cached JSON payload
|
|
||||||
try:
|
|
||||||
with open(cache_path, "r") as f:
|
|
||||||
payload = json.load(f)
|
|
||||||
|
|
||||||
# Validate payload structure before use
|
|
||||||
if not isinstance(payload, dict) or "data" not in payload:
|
|
||||||
raise ValueError("Invalid cache payload")
|
|
||||||
|
|
||||||
fetched_data = payload["data"]
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
# Corrupt or unreadable cache file -- display formatted placeholder
|
|
||||||
output.append(
|
|
||||||
f"{line_tab1_offset}{color_bad}{symbol}: "
|
|
||||||
f"{line_tab2_offset}{color_value}-- "
|
|
||||||
f"{line_tab3_offset}{color_value}-- (--%)"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# ------------------------------
|
|
||||||
# VALIDATION
|
|
||||||
# ------------------------------
|
|
||||||
if (
|
|
||||||
isinstance(fetched_data, dict)
|
|
||||||
and "current_price" in fetched_data
|
|
||||||
and isinstance(fetched_data["current_price"], (int, float))
|
|
||||||
):
|
|
||||||
current_price = fetched_data["current_price"]
|
|
||||||
compare_price = fetched_data["compare_price"]
|
|
||||||
|
|
||||||
# Comparison price may be missing if no trading day was found
|
|
||||||
if not isinstance(compare_price, (int, float)):
|
|
||||||
symbol_color = color_bad
|
|
||||||
output.append(
|
|
||||||
f"{line_tab1_offset}{symbol_color}{symbol}: "
|
|
||||||
f"{line_tab2_offset}{color_value}-- "
|
|
||||||
f"{line_tab3_offset}{color_value}-- (--%)"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Compute absolute and percentage change
|
|
||||||
price_difference = current_price - compare_price
|
|
||||||
percent_change = (price_difference / current_price) * 100
|
|
||||||
|
|
||||||
# Color change based on price movement direction
|
|
||||||
color_dynamic = (
|
|
||||||
color_good if round(price_difference, args.price_dec_places) > 0
|
|
||||||
else color_bad if round(price_difference, args.price_dec_places) < 0
|
|
||||||
else color_value
|
|
||||||
)
|
|
||||||
|
|
||||||
# The actual formatted line with valid stock data
|
|
||||||
output.append(
|
|
||||||
f"{line_tab1_offset}{symbol_color}{symbol}: "
|
|
||||||
f"{line_tab2_offset}{color_value}{round(current_price, args.price_dec_places):.{args.price_dec_places}f} "
|
|
||||||
f"{line_tab3_offset}{color_dynamic}{round(price_difference, args.price_dec_places):+.{args.price_dec_places}f} "
|
|
||||||
f"({round(percent_change, args.percent_dec_places):+.{args.percent_dec_places}f}%)"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Cached data missing required fields -- display formatted placeholder
|
|
||||||
output.append(
|
|
||||||
f"{line_tab1_offset}{color_bad}{symbol}: "
|
|
||||||
f"{line_tab2_offset}{color_value}-- "
|
|
||||||
f"{line_tab3_offset}{color_value}-- (--%)"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Header label depends on intraday vs historical mode
|
|
||||||
header_label = "Intraday" if args.range_in_days < 1 else f"{args.range_in_days} Day"
|
|
||||||
|
|
||||||
# Formatted header line
|
|
||||||
header_line = (
|
|
||||||
f"{line_tab1_offset}{color_header}Ticker"
|
|
||||||
f"{line_tab2_offset}Price ($$)"
|
|
||||||
f"{line_tab3_offset}{header_label}{color_label}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Print final output for Conky to render
|
|
||||||
print(
|
|
||||||
header_line
|
|
||||||
+ "\n"
|
|
||||||
+ f"{line_tab1_offset}{color_header}${{voffset -5}}${{hr 1}}"
|
|
||||||
+ "\n"
|
|
||||||
+ "\n".join(output)
|
|
||||||
)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue