Repository navigation
A bot on every network in pull or HTTP mode, jobs per chat, Docker, log levels, docs in doc/ - #40
Merged
Merged
Conversation
discord.py cannot run under gevent, and gevent's monkey patching is the one
thing that kept the bot from plain threads; with the config swapped in one
assignment, state edits locked and the scheduler locked (M1, M2, M6), it can
go.
- bot.py starts the scheduler thread, and serves HTTP with waitress only
when some network's webhook is set (bound before Telegram is given the
URL); with none set, the main thread only waits for signals. SIGHUP and
SIGUSR1 are plain signal handlers, installed last as before, and the log
says when they are in ("finbot is running").
- The repeat check gets a lock: waitress answers on several threads, and a
resent update could race its first copy.
- Charts are drawn one at a time under a re-entrant lock instead of gevent's
one-thread pool, so a chart function may call another; render.preload()
imports matplotlib at start.
- pywsgi's access log (which printed the secret webhook path) goes; with
debug on, each request is logged by network and status.
- waitress moves to requirements-optional.txt, imported only when HTTP is
served; CI installs both files. A test imports every module and starts
bot.py with the optional libraries blocked.
- Tests wait for each update's worker thread instead of sleeping, and the
hub-stall tests go: threads preempt. The fixture quotes share one
timestamp, so quotes fetched in parallel threads still tie on their dates.
util/record_outputs.py: 180 steps, identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
Each pull mode to come acknowledges a press its own way as it arrives (Slack's socket ack, Discord's interaction callback), so the rule moves out of the worker into one place, bot.take, the same for every network and every way an update arrives. - inbound.Network.acknowledge(press), by default nothing: taking the update is the acknowledgement (Slack's 200). bot.take calls it for a press after the repeat check and before the worker has it, so a resent press is not acknowledged twice. - Telegram's answers the callback query with a toast made from the button's data alone (buttons.toast): "Loading 1Y…" for a period button, "Running…" for a .jobs button. - Transport.acknowledge and the worker's three calls go, and with them the stale .jobs button's "Buttons updated, try again": the list redrawn in place already says it. - util/record_outputs.py acknowledges its presses as bot.take does, and presses a .jobs button too. util/record_outputs.py: 182 steps, 1 differs (that stale .jobs button's toast). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
Telegram's mode follows its keys: with telegramOutgoingWebhook set it posts its updates to finbot as before (http mode); without it, finbot fetches them (pull mode), so a bot token and a chat id are all a Telegram bot needs for commands and buttons. - lib/pull.py, the pull modes: a listener per network without a webhook, each in a thread of its own handing its updates to bot.take, so reading them, the repeat check and acknowledging a press are the webhook's. TelegramPoll asks getUpdates for the updates after the last one taken, held open up to 50 s (the request waits 65), and waits out failures (a 409 when a webhook is set or another finbot polls, the network down) from 5 s, doubling up to a minute. An update whose handling fails is reported to the default chats, as a failed webhook request is, and not fetched again. - telegram.register() runs as finbot starts in both modes: it deletes the webhook (keeping the updates not delivered yet), sets it again in http mode, and names the bot. Its log no longer shows the webhook URL, whose path is a secret. - Telegram charts and .jobs lists carry their buttons in both modes. - A start-up warning (and check_config, which now shows the warnings of the file it checks) for telegramOutgoingToken without the webhook. - doc/telegram.md: pull mode and HTTP mode in place of reports only and the interactive bot; the README, the finbot.env template and CLAUDE.md. util/record_outputs.py: 182 steps, identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
Slack's mode follows its keys as Telegram's does: with slackOutgoingWebhook set it posts to finbot as before (http mode); without it, and with the new slackAppToken (an app-level xapp- token), finbot opens a socket to Slack. - pull.SlackSocket: apps.connections.open gives the socket's URL; each envelope is acked at once, before anything else, and its payload (the body Slack would post to the webhook) goes to bot.take, so the reader and the event_id repeat check are the webhook's. Slack's disconnect opens another socket at once; silence for 30 s sends a ping, and as long again with no answer fails the socket, which is opened again after the back-off. - pull.WebSocket wraps websocket-client, imported only when a socket opens (python3-websocket, now in requirements-optional.txt). - Slack chats take buttons with the bot token in either mode. - transports.slack_api(as_app=True) calls with the app-level token. - Start-up warnings: slackAppToken without slackBotToken, beside slackOutgoingWebhook (unused), or not an xapp- token. Slack's tokens are redacted from logs. - doc/slack.md: Socket Mode and HTTP mode, and the steps they share; the README, the finbot.env template and CLAUDE.md. util/record_outputs.py: 182 steps, identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
Discord gets a bot (discordBotToken) that answers commands and posts the
scheduled reports to discordChannelID, charts and buttons and all. Its
mode follows its keys: with discordOutgoingWebhook (the app's Interactions
Endpoint URL) and discordPublicKey, Discord posts to finbot (http mode);
without them, finbot keeps to the gateway (pull mode).
- lib/discord.py: REST calls as the bot, 429s waited out as Discord asks,
and the slash commands registered at start in both modes, put only when
Discord's differ (it limits how many may be made in a day).
- Slash commands follow the .help lines (worker.slash_commands): /price,
/dividend and /earnings, /buy and /sell, /forwardpe, /help, and /stock and
/profile, the new names of .SYMBOL (.stock NVDA works on every network).
Each takes its arguments as one text option, in servers and in DMs. On
Discord, .help lists them as slash commands.
- inbound.Discord reads an interaction the same from the endpoint and the
gateway (a slash command is the message it stands for, a component press
a Press), and the gateway's messages as Slack's. An interaction is
acknowledged with the answer Discord must have within 3 s ("thinking…"
for a command, nothing to see for a press); the work waits in the
answer's `then` until it is out: after waitress has sent it (bot.Sent),
or after the gateway listener has posted it to the callback.
- inbound.DiscordInteractions checks each request's Ed25519 signature with
PyNaCl, imported only there, and refuses a bad one with the 401 Discord
checks for.
- pull.DiscordGateway runs discord.py's client (imported only there) in
its own thread, with the message and Message Content intents, taking the
raw MESSAGE_CREATE and INTERACTION_CREATE events off its event loop.
- transports.DiscordBot: no previews, no pings, charts attached, buttons
as components in rows of five, the typing notice for a message command;
a slash command's first message replaces its "thinking…" (deleted if it
says nothing) and the rest follow up; a pressed message is edited through
the press's interaction.
- Start-up warnings for the Discord keys; Discord's bot and interaction
tokens are redacted from logs. discord.py and PyNaCl join
requirements-optional.txt.
- util/record_outputs.py records Discord's commands and presses too.
- doc/discord.md: the bot in both modes, and the webhook for reports only;
the README, the finbot.env template and CLAUDE.md.
util/record_outputs.py: the four .help steps differ (.stock SYMBOL), and
the new Discord steps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
Each network now has a bot that listens (pull mode by default), so the outgoing-only incoming webhooks go, and with them the conditions on what a chat can carry. - slack_webhook and discord_webhook retire. RETIRED is one table for both files, each key with what to do instead: a retired key never stops a start (check_config accepts it in finbot.env too) and gets a note at start and from check_config (slack_webhook: set slackChannelID with slackBotToken). split_env drops them with the same note. - The SlackWebhook and DiscordWebhook transports and _post_json go; the scheduled reports go to each network's home chat (telegramChatID, slackChannelID, discordChannelID) as its bot, each with its own token. - One rule for buttons: every bot transport takes charts and buttons, a plain class attribute, whichever way its updates arrive. A start-up warning says when slackBotToken has neither Socket Mode nor a webhook, so that Slack's commands and buttons would get no answer. - env.webhooks becomes env.telegram_api, the Bot API's base for the token. - A test runs the six networks and modes, in a channel and in a DM: a chart and a .jobs list carry their buttons, in the chat that asked, and each command shows its network's notice (typing, Slack's ⏳, Discord's "thinking…" for a slash command). - util/record_outputs.py records the scheduled reports to the home chats as the bots, for older commits too. - The guides lose the webhooks; the README, the finbot.env template and CLAUDE.md follow. util/record_outputs.py: 247 steps, identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
Each scheduled or manual run started a Python process that imported pandas and matplotlib again; about a hundred a day. Now a command runs in the bot, in a thread of its own, sharing its caches and connections. - The Scheduler's runner calls the report's run() in its thread (jobs.run_in_process); a failure, SystemExit too, is reported as before. - A thread cannot be killed, so JOB_TIMEOUT becomes a soft deadline: a command still running after it is reported once and left to finish, marked running meanwhile so that no second copy starts. Every request goes through lib/net, with its timeouts, so a stuck job ends; a test fails if a module outside lib/net.py calls requests itself or passes timeout=None. - The scheduler is always on: the scheduler key retires (a note at start says to delete it), and a schedule without jobs runs nothing. - Gone: lib/jobrun.py, run_child, stop, exit_reason, the preferences handed to job processes (child_environment, FINBOT_PREFERENCES_PASSED), delivery.cli and guarded, and the twelve top-level scripts that ran a report from cron: running one by hand is `.jobs run`. The checkout's top holds bot.py alone; bot_send_stdin.py is util/send_stdin.py. - Tests that start the bot give it an empty schedule (no_jobs). - util/record_outputs.py loads the shipped schedule, so .jobs lists it. - The README's scheduling notes, the finbot.env template, the flush unit and CLAUDE.md follow. util/record_outputs.py: 247 steps, identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
Each chat (a Telegram chat, Slack channel or Discord channel, by its network and id, which a mode switch does not change) has its own jobs, kept in finbot_schedule.json and each posting to its own chat: - .jobs lists the chat's jobs with their next runs (UTC and the timezone preference), then the fixed system jobs (sharesight_warm, yahoo_warm, cleanup), then the subcommands; it has no buttons. - .jobs run / .jobs toggle list them with a button per job that runs it now or switches it on or off (redrawn in place); with a number they act directly. A stale or forged button redraws the list and does nothing else. - .jobs add [CRON_TZ=ZONE] TIME REPORT adds one (alone, it lists the reports and what each takes); .jobs del N deletes one. Up to 30 per chat; a time that never comes, an unknown report or a bad argument is refused, saying why. - Times are in UTC unless a job names its zone; a change counts at once. trades, rating, milestone and podcasts keep a record per chat of what they told it, so the same report scheduled in two chats tells each of them once. As finbot starts, lib/migrate.py gives each home chat, once, the jobs of the old schedule.cron (the data dir's copy, else the shipped one) in the zone they ran in, and copies the reports' shared records to each home chat. With no home chat set it waits. SIGHUP no longer reloads a schedule; report labels lose .py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
price, shorts and cal took any destination for a chat command, so a chat's job of them posted "nothing meets the threshold" on every quiet run, skipped the trigger bars and left the earnings report unpinned. They now ask delivery.interactive(dest), as trades, rating and milestone do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
Each chat now has its own watchlist (finbot_watchlist.json, one list per chat
key) and its own plans (finbot_plan.json, {user: plan} per chat):
- .watchlist lists and changes this chat's watchlist; every report in a chat,
asked for or scheduled (price, shorts, cal, rating, milestone, the rankings,
a price list's period buttons), covers that chat's watchlist plus the
Sharesight holdings, which every chat shares.
- yahoo_warm keeps every chat's watchlist warm (watchlist.all_followed).
- .plan records and lists the plans of this chat only.
The migration copies the old shared watchlist and plans to each home chat
once, as it does the reports' records.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
The README and the guides no longer describe reports without commands: every network's bot answers commands, fetching its updates itself (pull mode) unless its webhook URL is set. "Interactive trigger" becomes "Command", the pull mode names Discord's gateway beside Telegram's and Slack's, and each channel gets its own scheduled reports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
pandas did three small things with dated closes, at 0.8 s and 37 MB to load: - yahoo.price_series now gives (name, exchange, ((date, close), ...)). - .compare crops and rebases those rows with bisect: the common start, the last close on or before it, the rows after, first to last. - render draws a line's dates as datetime.date and its closes as a numpy array, both of which matplotlib takes as they are. Every chart is pixel for pixel what it was: the five goldens and a range of line and compare charts (lengths from 2 days to 10 years, every sign) hash the same at full size, and util/record_outputs.py finds all 268 steps identical. A test fails if any module imports pandas, and another draws every chart with pandas blocked. requirements.txt and the README drop it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
The command table gives every network the same slash commands (worker.slash_commands: one per .help line, a command's several lines joined in its usage, /help, and the second names /profile and /job): - Telegram: as finbot starts, in both modes, the bot's command menu is set (setMyCommands, only when Telegram's differs; a failure is only logged). The menu leaves out /profile and /job, which work typed. - Slack: util/slack_manifest.py prints the app's manifest (slash commands, scopes, events, interactivity, the messages tab, Socket Mode on or off, and in HTTP mode the Request URL), which replaces the manual steps of doc/slack.md. A slash command arrives as a form over HTTP or a slash_commands envelope over the socket, both read as the message that says the same (/price 5 7d) in the channel or DM it was sent in. - Discord: /job is registered beside /jobs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
A slash command whose arguments have words to suggest registers its option with autocomplete. While the arguments are typed, Discord's autocomplete interaction is read as a Suggest and answered at once with worker.suggest's choices (type 8, up to 25, each up to 100 characters): on the interactions endpoint as the response, over the gateway through the interaction's callback (an interaction's answer now says so, inbound.Response.interaction). worker.suggest completes the word being typed from the command table: the keywords and periods (until one is given), the portfolios, and tickers (the chat's watchlist and the holdings, read from the cache alone by sharesight.cached, never a request); for /jobs its subcommands, this chat's job numbers with their lines, the reports after add and a time, and what a report takes. A failure suggests nothing and is logged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
- Dockerfile: debian:trixie-slim with Debian's python3-* packages, every optional library included; a user of its own (10001), the code root's and compiled at build; each start copies the missing defaults into /data, checks the startup file, and only then starts the bot. - compose.yaml: ../finbot.env and ../finbot-data beside the checkout, the log to journald (FINBOT_LOG_DRIVER to change it), no port unless HTTP mode needs one, and hardened: read-only, no capabilities, no new privileges, 256 pids, 400 MB with no swap, init passing SIGHUP and SIGUSR1 on. - util/docker_setup.sh sets up the host side once (data dir, ownership, the startup file from its template); util/sync_defaults.py gets --create, and a missing destination is an error naming it. - git_version names the commit the image was built from (FINBOT_COMMIT). - CI's docker job builds the image, imports every module in it, starts it as compose does (defaults copied, reload and flush through init), and checks a bad startup file stops it. - doc/docker.md, and a quick start in the README. The runtime half was checked here on a python:3.13-slim image (this environment cannot reach deb.debian.org): compose starts it hardened, and every CI check passes against it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
README.md is now what finbot is, its main features, a quick start (Docker in five lines, then each network's keys in a line) and links. The rest moved to doc/, flat so that CI's docs-only rule still covers it: - commands.md: what each network supports, the commands, the reports and their screenshots; - configuration.md: finbot.env and preferences.conf, the data directory, Sharesight, portfolios, hyperlinks; - scheduling.md and caching.md; - install.md: a Debian host from the packages per mode to systemd, and pip or uv elsewhere (Docker is docker.md). The network guides link to the new files, and CLAUDE.md says where each topic now lives. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…ent client Each network's code is now in three modules, flat in lib/: - telegram_common.py, slack_common.py, discord_common.py: what both ways in share: the API calls, the reader of the network's updates (READER) and its bot (TelegramBot, SlackBot, DiscordBot); - telegram_webhook.py, slack_webhook.py, discord_webhook.py: the HTTP mode (the path, and how a request proves itself); - telegram_longpolling.py, slack_websocket.py, discord_websocket.py: the pull mode's persistent client. lib/inbound.py, lib/pull.py and lib/transports.py keep only what no network owns: the update types and the Network, Webhook, Listener and Transport bases. lib/telegram.py and lib/discord.py are gone. bot.py puts the networks together: WEBHOOKS and route(), LISTENERS and listeners() (each listener says whether finbot.env wants it), and each network's registration at start. A pure refactor: the code moved verbatim but for the references that the move changes. slack_api is now slack_common.call, like the other two networks' calls; Telegram's register() is split between its two mode modules, and getMe became telegram_common.bot_name(). util/record_outputs.py finds a network's reader in either layout, and records 268 steps identical to HEAD. The tests follow the code to its new modules; CLAUDE.md's code map describes the new layout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…ok secret
The docs hosts are blocked from this environment, so these come from the docs as
quoted by web search (mirrors and library docs) and from what is known of them:
- Telegram: link_preview_options {"is_disabled": true} replaces
disable_web_page_preview in sendMessage and editMessageText, which Bot API 7.0
deprecated (December 2023).
- Telegram: "typing…" shows for 5 seconds at most (sendChatAction), so it is sent
again every 4 s, not every 7 s, which left a gap of 2 s each time; Discord's
lasts 10 s, so its 7 s stay.
- Discord: every REST call sends the User-Agent that Discord asks of a bot,
DiscordBot (https://github.com/robdevops/finbot, <commit>); one without may be
blocked by Cloudflare. bot.py notes the commit as it starts.
- Telegram: a telegramOutgoingToken that is not 1 to 256 of A-Z, a-z, 0-9, _ and
- gets a startup warning (and one from check_config): Telegram refuses such a
secret, so setting the webhook would fail.
util/record_outputs.py: 56 of 268 steps differ, each only in Telegram's preview
flag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
config: startup environment (10 keys set) config: /var/lib/finbot/preferences.conf (14 keys set; SIGHUP to reload) in place of "startup environment provides 10 of 21 startup keys (values hidden)" and "preferences from ... (14 set; reload with SIGHUP)"; one key is "1 key". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
config: no preferences.conf in /var/lib/finbot, reload unavailable in place of "..., using the defaults; reload will have nothing to re-read". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
… read defaults/preferences.conf keeps its layout, with each key commented out at its built-in default (#past_days=28), and its header says so: remove the # to change one. finbot never reads the template (it never did): without <DATA_DIR>/preferences.conf it runs on the defaults hard-coded in Prefs, and util/sync_defaults.py --apply copies the template there byte for byte, layout and all (a test checks). A key set in the template itself does nothing, so the log says so at start (and check_config, and a reload): "defaults/preferences.conf sets debug, but finbot never reads it: edit <DATA_DIR>/preferences.conf instead", or, when that file does not exist, "run util/sync_defaults.py --apply, then edit ...". doc/configuration.md and CLAUDE.md describe the template. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
- yahoo: an ETF has no analysts' recommendation, so Yahoo sends no
recommendationKey, and the converter that turns strong_buy into "strong buy"
raised AttributeError on None ('NoneType' object has no attribute 'replace').
That failed .earnings for any watchlist with an ETF in it, and logged that
error for each ETF. A missing or null field is now left out before any
converter runs, and AttributeError counts as garbage like the others.
- bot.py: the bot's stderr writes a whole line at a time (WholeLines). Under
PYTHONUNBUFFERED (finbot.service, the image) each piece of a print() is its
own write, so threads logging at once mixed their lines ("TTL:cache hit:
34254 s/var/...", and lines with no file name); each thread's pieces now wait
for the end of its line.
- cache: a hit reads "cache hit: <file> TTL: 34254s".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…ed at INFO
- log_level (ERROR, WARN, INFO or DEBUG, any case; INFO by default) sets what
reaches the log: that level and those above it. debug is retired: accepted,
with a note at start to delete it.
- lib/log.py: log.error, log.warn, log.info, log.debug, with print()'s
arguments, to stderr. Every log line of the bot has a level now: ERROR for
what failed and was lost (a message not sent, a request not answered, a
start that cannot go on); WARN for what failed and was got over (a notice
not shown, a retry, a fallback, a refused request, a ticker skipped); INFO
for what finbot does; DEBUG for cache hits and writes. The lines that debug
used to show (requests answered, messages taken, cache misses) are INFO.
- Start-up warnings read "WARN config:", not "ERROR config:"; a refused
request "refused:", not "Fatal:".
- cache: "cache expired" is "cache miss" (INFO), like a file not there.
- net: each request that goes out is a line at INFO, its method and URL,
without secrets or the scheme, cut to 100 characters
("GET query1.finance.yahoo.com/v8/finance/chart/AAPL?range=1y&crumb=<redacted>").
Telegram's long polling (getUpdates) is left out.
record_outputs: 268 steps identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…rvice goes util/flush.py --before 180s the files written more than 180 seconds ago util/flush.py --after 24h the files written in the last 24 hours util/flush.py --after 7d ... 7 days (3m: 3 months, 1y: a year) util/flush.py all every file A time is a number and s, h, d, w, m (months) or y: formats.seconds_from_human, the period words of commands (days_from_human_days) plus seconds and hours. On its own the tool prints its help with these examples. It deletes only finbot's files in DATA_DIR/cache (cleanup.remove, which skips a temp file being written), never the state in DATA_DIR, and the bot can keep running; --data-dir names finbot's data dir when the shell has no DATA_DIR. SIGUSR1 still flushes everything and refills the cache at once. doc/caching.md and doc/docker.md say how. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
At INFO, the request that a miss sends is already a line of its own ("GET
query1.finance.yahoo.com/..."), so "cache miss" moves to DEBUG beside "cache
hit" and "cache write". The docs, the template's comment and CLAUDE.md say so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
GET query2.finance.yahoo.com/v10/finance/quoteSummary/ARM.L?modules=ca… The whole line, method and URL, is at most 73 bytes, the ellipsis taking 3 in UTF-8 (it was the URL at 100 characters). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
… 30 characters
- render: a bar chart keeps every row. Past 40 rows the image grows taller,
each row as tall as at 40 and the heading where it was, instead of dropping
the middle rows behind a gap: the caption is cut short ("…and 8 more in the
chart"), so the chart must show them all. Charts of 40 rows or fewer are
pixel-identical. The shorts pictogram, a scene of a fixed size, keeps its 24.
- formats.fund_name: a fund's name loses its trust's: "Global X ETF Series OFC
Global X China EV ETF" is "Global X China EV ETF", "iShares Trust iShares Core
S&P 500 ETF" is "iShares Core S&P 500 ETF"; an earlier ETF goes with the
trust's words after it, and OFC anywhere. A company's repeated word ("Johnson
& Johnson") stays.
- formats.short_name: in a report's list (price, cal, rating, shorts, holdings,
and every stock line), a name over 30 characters is cut at a word, with an
ellipsis, so that a line fits a phone. The name stays whole elsewhere: the
Simply Wall St link and the leveraged-fund check read it.
record_outputs: 268 steps identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
- cal: a ticker whose quote lacks the earnings or ex-dividend date takes it from its own details first: what is held is that listing (a US ADR such as ARM or CLBT), so its own date counts. Its home market's listing (ARM.L, CLBT.TA: "unknown to Yahoo" in the log) is asked for only when the US listing has no date at all, as are the listing in finbot_adr.json and the NZX's for an ASX ticker reporting in NZD. - rankings: a stock .marketcap cannot rank (an ETF without a market cap, or a currency without a rate) logs at DEBUG, not INFO. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
Telegram's mobile app can send a command twice (/start in a new chat), each its
own update with its own id, so the repeat check by update id lets both through
and the chat gets two answers. bot.take now skips a message whose person, chat
and text match one taken in the last second (bot.doubled), and logs it at INFO
("@rob_llama sent '/start' again within 1 s, not run again"). Discord's slash
commands are left out: each interaction must get its answer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
The calendar no longer swaps a foreign company's US ticker for its home market's when the US listing has no date: ARM and CLBT are held on US markets (Sharesight says so), so ARM.L and CLBT.TA are never asked for, and a US listing without a date has none. The country -> Yahoo suffix table that made those tickers (markets.YAHOO_COUNTRY) goes with it. The listings named in finbot_adr.json and the NZX's for an ASX ticker reporting in NZD stay, tried only when the ticker's own details have no date. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
POST from telegram: we answered 200 OK in place of "[http] request for telegram answered 200 OK". The method is the request's own (REQUEST_METHOD); a request to an unknown path is "from no network" as before. The tests' WSGI requests say POST. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Everything on
claude/kind-newton-ad0z2qsince #39: 31 commits, plus a merge ofmainthat changes no file.Chat networks: a bot on each, pull mode by default
A network's mode follows its keys. With its webhook URL set it stays in HTTP mode, as now. Without one, finbot fetches its own updates, with no server or public URL.
bot.take), not by the worker.slackAppToken).slack_webhook,discord_webhook) retire. Every network posts as its bot, charts and buttons included.util/slack_manifest.py), Discord's registered commands.<network>_common.py,<network>_webhook.pyand the pull client (telegram_longpolling.py,slack_websocket.py,discord_websocket.py).link_preview_options;/starttwice).Scheduled reports, per chat
.jobs(run,toggle,add,del).schedule.cronis migrated into the home chats once. Times are in UTC unless a job names its zone (CRON_TZ=).Reports
.earningsworks when the watchlist holds an ETF (Yahoo sends no recommendation for one).Logging, preferences and tools
log_level(ERROR, WARN, INFO or DEBUG; INFO by default) replacesdebug. Each outgoing request is a line at INFO.POST from telegram: we answered 200 OK.defaults/preferences.confsets nothing: every key is commented out at its default, and finbot never reads the file. A key set there gets a warning.util/flush.pyflushes the cache by age (--before 180s,--after 24h) orall.finbot-flush.servicegoes.Install and docs
util/docker_setup.sh;doc/.After merging, on a host that runs
mainpython3-waitressfor any network in HTTP mode;python3-websocketfor Slack Socket Mode;python3-discordfor the Discord gateway;python3-naclfor Discord in HTTP mode.python3-geventandpython3-pandasare no longer used.slack_webhook,discord_webhook,scheduler, anddebug(nowlog_level).schedule.cronmove into the home chats at the first start. Delete the data directory's copy afterwards.finbot-flush.serviceis gone: flush withutil/flush.py.Testing
util/record_outputs.pywas run at each step: the refactors are identical (268 steps), and the other commits differ only where their messages say.🤖 Generated with Claude Code
https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX