Repository navigation
Overnight plan and second pass: objects and layers, one delivery path, Slack buttons, Discord webhook, warm jobs - #39
Merged
Conversation
…ans take yes/no; graph=False means no charts - Slack: link previews are turned off in the request body, where Slack reads unfurl_links/unfurl_media (they went in the headers and never worked). A reply without slackBotToken logs "ERROR slack: a reply to channel … needs slackBotToken", slack_api raises the same instead of a TypeError, and the bot warns once at startup when Slack commands are configured without the token. - Text a user wrote is shown as text: a .plan entry with a < made Telegram refuse every .plan reply after it, and a first name is escaped where it arrives. Slack's already escaped text is not escaped twice. - A true/false setting takes true/false, yes/no, on/off or 1/0; anything else is a config error (scheduler=yes and graph=Ture used to mean False). A blank value means the default, as the README says of finbot.env. - graph=False turns off every chart, not only the price lines. - performance names each portfolio from the portfolio list: one with no holdings crashed the report. - Yahoo: the chart request sends the cookie of its crumb; cookie and crumb are fetched together under the lock; price_history no longer writes its PNG cache with the cache off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…map for CLAUDE.md - Replies never quoted the asking message (the worker passed message_id=None even before the command table): the whole chain goes, from bot.py through process_request, the report scripts and write/sendPhoto, with Slack's thread_ts/reply_broadcast. So do the chat_id=telegramChatID defaults only an interactive run reads, the "make google cloud happy" returns, hbar_chart(threshold=) that only a test used, sharesight.warm_once (the tests call warm()[0]), a fontweight DejaVu Sans does not have, a dead emoji, an unreachable except, a stranded unittest.main() and a misnamed test. - A passing test run prints 0.4 KB instead of 23.7 KB: -b in CLAUDE.md and CI, bot imported before requests so gevent patches ssl first, a real job child's output sent to /dev/null, and the two chart warnings fixed at their cause (all-zero bars divided by zero; a 2-day span made the date locator complain). - README: a table of what each chat network can do, near the top; the command list as it is (.value and .rating never existed; .beta .buy .sell .compare .pe .fpe .peg .plan were missing); 12-hour cache default; no key counts to keep in step. - CLAUDE.md: a code map, a test map, conventions and token tips. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…l start finbot-flush - bot.py deletes its cache files on SIGUSR1 and keeps its state (watchlist, seen trades, ratings, schedule). Yahoo's cookie and crumb go together under their lock, and the Sharesight caches refill at once (keep_warm can now be woken). The signal is ignored until startup is done, like SIGHUP, and job processes ignore it, since systemctl kill signals every process of the unit. - finbot-flush.service, a oneshot unit, gives it a systemctl verb; finbot.service needs no change. README: how to flush under systemd, Docker or by hand, and cleanup.py 0 with the bot stopped. - cleanup leaves a temp file written in the last minute (an atomic write in flight) and still deletes a crash's old leftovers. - A cache file deleted between the age check and the read is a miss, not an error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…cripts load less; cache-first Yahoo details - The bot draws charts in one native thread (util.render_off_hub, from bot.py only). Drawn in the hub, the shorts pictogram froze every chat and incoming webhook for 1.3 s; now the hub's worst pause is about 0.1 s. One thread, because several at once deadlock matplotlib under gevent's patched locks (tested). matplotlib is imported at start instead of by the first chart. - A Telegram update or a Slack event that arrives again (Slack resends after 3 s without an answer) runs once, by update_id and event_id. - reports imports pandas and the scheduler where it uses them: a price or performance job process no longer loads pandas and every script through charts → reports → jobs (import price: 0.53 s → 0.14 s). - fetch_detail reads its cache before taking Yahoo's auth lock; data files (the simplywall.st lists, 73 KB, read on every profile) are parsed again only when they change. - A scheduled price report fetches the betas of the small caps that moved together, not one at a time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…eviews; reports split by size as well as lines - Discord reports go to the webhook's own endpoint instead of its Slack-compatible one (a /slack suffix in finbot.env is dropped): flags=4 turns link previews off (the README's "garbage link previews" limitation is gone), mentions ping nobody, and ?wait=true makes Discord answer 200 instead of an empty 204. - Scheduled reports to Discord carry their charts, uploaded with the report as the message (up to Discord's 2,000 characters). - Links on Discord are masked links, [text](<url>), with the brackets and parentheses that would end one escaped. - A long report is split into messages of at most chunk_maxlines lines and the service's character limit, whichever comes first: twenty linked lines made a 3,000-character Discord message, which Discord refuses. - README: the Discord setup and the chat-networks table say what the webhook now does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…editorconfig A whitespace-only change (git diff -w shows nothing but .editorconfig, .gitignore and CLAUDE.md): the 4-space files now indent with tabs like the rest of the code. Lines inside multi-line strings that are data were left exactly as they were. One style means an edit's quoted indentation always matches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…erences.conf), read where used; no star import - lib/config.py builds frozen Env and Prefs dataclasses, one place that lists every setting. A reload replaces config.prefs in one assignment, so a request never sees half a reload; it used to rewrite each changed name in every module that had copied it with `from lib.config import *`. The job processes' snapshot of the preferences lives in the same object. - Every module reads config.env.<name> / config.prefs.<name> when it runs: no star import, no UNSET sentinel (defaults are None, resolved at call time), no names copied into 19 modules. preferences.conf's keys come from the Prefs fields. - The greeting words moved to lib/words.py, the country suffixes to util.YAHOO_COUNTRY, and cal reads finbot_adr.json when it uses it (it was read once at import, so an edit needed a restart). - Tests change settings through self.env(...) / self.prefs(...); the tests that policed the star import and the sentinel became one guard that nothing star-imports config or freezes a preference into a default. - Checked by recording every request the bot sends for 70 chat commands on Telegram and Slack, 8 buttons and 13 scheduled reports to all three networks: byte for byte the same before and after, charts included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…e.py util.py's cache_path/read_cache/json_write/json_load family splits by what the file is for. lib/cache.py holds the copies of Yahoo and Sharesight answers and drawn charts, any of which may vanish at any time; lib/state.py holds what must last in DATA_DIR (the watchlist, plans, seen trades and so on), written whole and flushed. state.edit() is the locked read-modify-write for the next step. Output is unchanged: every command, button and scheduled report posts what it did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…hlist.py) state.edit() reads a state file, lets the block change it and writes it back only if it changed, while no other edit of that file runs in the process. Every read-modify-write uses it: the watchlist, plans, chart button refs, keyboard versions, and the seen trades, ratings, records and podcast episodes, which now add to the file as it is when the run ends instead of writing back what was read when it began. charts' keyboard lock goes. The watchlist: tickers(), find() (Yahoo, with the .AX retry), add() and remove(). A ticker is checked with Yahoo before the file is opened, so two chats adding at once both count; before, both read the list, waited on Yahoo, and the later write dropped the other's add. prepare_watchlist shrinks to phrasing the outcome: it fetches only the ticker it is changing, not the whole list it then threw away. yahoo.resolve() is the one .AX retry, shared with the company profile. Visible changes: the add/remove confirmation links to the stock's exchange (MSFT:NASDAQ, not MSFT:MSFT), and removing a ticker Yahoo no longer knows says it was removed (it was, but the reply said it could not be found). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…folio(name, id) A Portfolio has holdings(), tickers(), performance(days) and trades(days); sharesight.portfolios(name=None) picks every portfolio or the one named (in any case, else PortfolioNotFound), and sharesight.tickers() is everything held. They replace pick_portfolios, get_holdings, get_holdings_new, get_holdings_wrapper and get_performance_wrapper, so callers pass one object instead of a (name, id) pair. The cached, one-at-a-time requests stay in the module functions. A performance answer without a report is now an error naming the portfolio (it was a KeyError: 'report' further on), and is not cached. Output is otherwise unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
finbot.service sends stdout to null, and job processes inherit it, so the 30 prints there (progress dots, "Preparing outbound", "Fetching ...", skip notices, a debug payload dump) were never read. They go, and a test keeps the bot and its reports printing only to stderr; the util/ tools, run by hand, still print to stdout. The one error among them, a data or cache directory that cannot be created, moves to stderr. On the way: get_trades printed len(data['trades']) before checking the answer, so a Sharesight error surfaced as KeyError: 'trades'; get_portfolios' include/exclude filter is one expression. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
lib/util.py (807 lines) held four unrelated things; each moves unchanged into its own module: - lib/markets.py: market tables, flags, currencies, exchange names, ticker translation; - lib/formats.py: company names trimmed, units, dates and spans in words, signed percents, ordinals; - lib/links.py: chat-markup links and Yahoo/Google Finance links; - lib/render.py: every matplotlib chart and the render pool. get_holdings_and_watchlist becomes watchlist.with_holdings(). util imported sharesight, which imported util back; that cycle is gone. Callers and test patch targets follow; the profile's local `links` list is now `sites`. Output is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…/buttons.py The ten scheduled scripts move from the top level into lib/reports/ (renamed, so their history follows), and lib/reports.py (622 lines) splits by subject beside them: about (.help, .jobs), history (.history, .compare), holdings (.holdings, .who, .pie, .watchlist), profile (.SYMBOL) and rankings (.marketcap, .pe, .fpe, .peg, .beta, .buy, .sell). The period-button helpers (PERIODS, keyboard, the callback refs) leave lib/charts.py for lib/buttons.py, which the reports use; charts.py keeps the rebuild behind a button press and imports the reports at the top. That ends the second import cycle: charts imported reports, which imported jobs, which imported every script, two of which imported charts. lib/jobrun.py takes a module path (lib.reports.price) and names the run by its last part. The next commit puts a script back at the top level for each report, for schedule lines and runs by hand. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
price.py, trades.py and the other eight are back at the top level as three-line shims that run lib/reports/<name>.py with their arguments (reports.cli), so schedule lines, cron entries and runs by hand work unchanged. A test runs two of them and checks there is one per scheduled report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…service) lib/dialects.py holds each network's markup: bold, italic, strike, links, user text made safe, and its message sizes. A dialect is a str equal to the network's name (TELEGRAM == 'telegram'), so it goes wherever the name did; the bot turns a name into its dialect where a message comes in or a report goes out (worker, webhook.destinations). The 73 markup calls that passed the service name along become service.bold(text), service.link(url, text) and the like; webhook's MARKUP table and links.link go. Tests pass TELEGRAM, SLACK and DISCORD. Output is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
The 128 functions and methods that had none get one, nested functions included; a comment that said the same on the def line goes. A test now fails on any function in the bot, lib/ or util/ without a docstring, and CLAUDE.md says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
The 421 test functions without one get one: each test method says what it checks, and each helper and stub what it stands in for; a comment that said the same at the top of a test goes. The guard test now covers tests/ as well, so every function in the project has a docstring. Four one-line stubs became two-line ones to take theirs; test_config no longer imports the cal.py shim it never used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
The harness that checked each refactor so far records every HTTP request the bot posts for 159 steps (each chat command on Telegram and Slack, the period and job buttons, every scheduled report) on the offline fixtures, and compares two recordings step by step. --against COMMIT (HEAD by default) records that commit, checked out in a temporary worktree, and this tree in the same run, so dates in the output always match; --diff compares two saved recordings; --record writes one. The exit status is 1 when they differ. tests/common.py now names what its fixture replaces with recorders (REAL_SENDING), so the tool knows what to put back on either side of a change to how the tests capture output. The pre-M1 branches of the scratch version are gone. A test covers the comparison, and CLAUDE.md points to the tool instead of the by-hand method. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
lib/transports.py has one class per way of posting: TelegramBot, SlackBot, SlackWebhook and DiscordWebhook. Each one knows its dialect, what it can carry (charts, buttons, a typing notice) and the network's size limits. Transport.text() splits long reports and Transport.chart() fits the caption; each network posts one message its own way. lib/webhook.py becomes lib/delivery.py: - Report(lines, chart, buttons) and deliver(dest, report), which draws a chart spec only where it can go; - reply_to() for the chat that asked and default_chats() for scheduled reports, with destinations() choosing between them; - report_error() and the CLI entry point. Other changes: - worker: Request.reply() replaces say/send/deliver. Every command shows the typing notice after half a second, in place of 18 `with request.typing()` blocks. - Buttons are neutral, buttons.Button(label, data, active), and TelegramBot.markup() turns them into an inline keyboard. Button presses edit through the transport (edit_chart, edit_text). - cal pins its Telegram messages through the transport. - util/record_outputs.py imports lib.webhook only for a commit from before REAL_SENDING. - The CLAUDE.md code map is updated. The output comparison (util/record_outputs.py) against the previous commit is identical in all 159 steps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
lib/inbound.py has one class per network that posts to the bot, Telegram and Slack. Each class knows: - the path the network posts to; - how it proves itself: Telegram by a header, checked before the body is read; Slack by a token in the body; - how to read an update, as a Message to answer, a Press of a button, or a Response given straight back (Slack's challenge, or nothing to do). bot.py handle() shrinks from 113 lines to a route, verify, parse and dispatch sequence, and turns away repeats once an update parses to something to run. The Telegram bot's name now lives on inbound.TELEGRAM, set at startup. Behaviour changes: - Secrets are compared in constant time, and an empty configured secret matches nothing. Before, a request carrying an empty header or token was accepted when finbot.env left the token empty. - Log wording: "warning: Slack auth incorrect", the unhandled Slack type is named, and repeats are logged the same way on both networks. - Slack's unhandled-type answer has the same body as Telegram's. A scratch comparison put 49 requests through the previous bot.py and this one: statuses, bodies and the calls handed to the worker match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
The new startup key slackSigningSecret (a secret) turns on signature checks for Slack requests. With it set, every request must carry: - X-Slack-Signature, the HMAC-SHA256 under the signing secret of v0:<timestamp>:<body>; - a timestamp within five minutes of now, so a recorded request cannot be replayed. The verification token is then neither needed nor enough. Without the signing secret, slackOutgoingToken is checked as before; Slack has deprecated it because the token travels in every request. New startup warnings: - an inbound Slack webhook with neither secret; - telegramOutgoingWebhook without telegramOutgoingToken. Either way every request would be refused. The README documents the key in the finbot.env table and in the Slack setup steps. A test checks the code against Slack's documented example signature. Also fixed: test_config.py imported lib.config before tests/common.py had set the environment. Run before test_edges, it failed 10 Telegram tests (on the previous commit as well). It now imports the fixture first, and every test module passes when run alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
77 docstrings that spilled a few words onto a second line are reworded to fit on one, within 120 columns with tabs counting 4. Their meaning is kept. - 74 of them overflowed by 30 columns or less once joined. - 3 had first lines already past 120 columns and only two to four words on the second line. The other 111 two-line docstrings need their second line and stay as they are. CLAUDE.md's convention now asks for one line where it fits, and states the line limit as 120 columns with tabs counting 4. Also included: a table of cases in test_config's startup-warning test, which wraps two lines of the previous commit that passed 120 columns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
lib/answer.py adds Answer(*lines): a reply in place of the report asked for. The worker and the period buttons send its lines as an ordinary message. Every other exception is still reported as a one-line⚠️ error. - sharesight.PortfolioNotFound is now an Answer. - yahoo.NoData is both an Answer and a RuntimeError, so code that leaves a ticker out of a list on RuntimeError still does. It is raised for: - a history without prices; - no high/low history; - Yahoo's 404 whose body says "Not Found", i.e. no such symbol. Any other 404 (an endpoint that moved, say), a refusal or an unreachable Yahoo stays a RuntimeError, reported as an error. - .history and .compare of a ticker without data, compare without enough history in the period, and a period button with nothing to draw now answer plainly. Before, each was "⚠️ ...: RuntimeError: ...". The output comparison is identical in all 159 steps. Its fixtures always have data, so the no-data replies are covered by new tests instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
A Telegram bot set up for reports only (no telegramOutgoingWebhook) still put period buttons under its charts, scheduled ones included. Telegram posts a button press to the bot's webhook, and with none set up the press went nowhere: the button spun and did nothing. Charts on such a bot now come without buttons, and with the webhook they keep them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
Most installs use one of Telegram, Slack and Discord. Each now has a guide covering its whole setup, webhook part included: doc/telegram.md, doc/slack.md and doc/discord.md. Where there is a choice, a table puts the two options side by side before the steps, so it is clear to the admin that it is a choice: - Telegram: reports only, or the interactive bot. - Slack: incoming webhook, or the interactive bot (which builds on the webhook). - Discord: a webhook only, so the guide says there is no choice. The guides include the nginx front end for each network, now with only TLS 1.2 and 1.3, plus two steps the README lacked: - Telegram: finding the chat id, and the allowed characters of the webhook password. - Slack: inviting the bot to the channel. The README links the guides under its chat networks table and in its requirements. Its per-network sections and the interactive bot setup are gone, moved into the guides. Daemonize (systemd) becomes a section of its own. CI treats doc/*.md like README.md: a pull request changing only docs skips the tests. The tests read only the API samples in doc/'s subdirectories, which the allowlist does not match. CLAUDE.md says so, and points to the guides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
A change figure now gets its sign everywhere, through
formats.signed_percent. Before, the same history figure read +3% in
.history but 3% in a profile.
- The profile's history lines and its pre/post-market line now read
+3% where they read 3%.
- The forecast growth lines and .compare's caption signed theirs by
hand; they use the helper now, and thousands get separators.
Levels (dividend yield, short interest, debt ratio, pie shares) stay
unsigned.
The output comparison differs only in the profile's 1Y line
("1Y: 3%" now "1Y: +3%"), on Telegram and Slack.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
The Australian end of financial year checklist (23 June) was written in reminder.py. It is now an AU-gated entry in defaults/reminder.json, like the other reminders. reminder.json entries gain what the checklist needs: - a message may be a list of lines as well as one line; - [text](url) is a link, in each network's own markup; - a line wrapped in _underscores_ is in italics; - the rest is text, now escaped for the network (a & in a reminder used to reach Telegram's HTML raw). reminder.py splits into report(service, today, entries), a pure function, and lambda_handler(), which reads the clock and the file and delivers. Only the birthday line stays computed. Checked old against new: for six dates (the EOFY day, the birthday, gated, dated and ordinary days), in AU and US, on all three networks, the output is identical. The README documents the reminder.json format. An install that keeps its own reminder.json in the data directory takes that file instead of the default, so it no longer gets the EOFY checklist until it adds the entry; util/sync_defaults.py shows the difference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
COMMANDS becomes a table of Command(pattern, handler, usage, section, buttons). The .help text and the Telegram command keyboard are made from it, where four lists used to be kept by hand: the patterns, prepare_help's lines, the keyboard's buttons and the README. - .help lists each section's commands in table order. - The keyboard is the commands' buttons, alphabetical. It is the same fifteen buttons as before, so its version hash is unchanged and no DM keyboard is sent again. - The typo aliases (earrings, prince, hospital...) stay, as part of the patterns (D10). The table is now in the order .help shows it, with .SYMBOL still last. None of the reordered patterns can match another command's words, and the output comparison is identical in every command step but .help and /start. There .SYMBOL moves from the top of "Company info" to its end, as the catch-all must stay last in the table. The command keyboard's upkeep (keyboard_version, mark, push, ensure, refresh) moves from charts.py to worker.py, beside the table it is made from; charts.py keeps the period-button rebuild. The keyboard test no longer counts buttons: each must run its own command (R16). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
The scheduled reports' entry point, lambda_handler (a leftover from AWS Lambda), becomes run(dest=None, **kwargs), with keyword-only arguments. dest is the chat that asked; without one, the report goes to the default chats. That replaces the (chat_id, service, interactive) triple, and the argument orders that differed between reports: worker called shorts(chat, threshold, ticker, service) but price(chat, threshold, service, user, ticker). - delivery.targets(dest) replaces destinations(interactive, service, chat_id). - The worker passes request.dest; jobs, jobrun, the command line shims and the cache flush call run(**kwargs). - milestone, reminder and podcasts take a dest too, so every report can be sent to one chat. - util/record_outputs.py calls run, or lambda_handler for a commit from before this one. The output comparison is identical in all 159 steps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
Both reports did their gathering inside the per-network code, so it ran again for every destination. Each now has: - gather(), which fetches once; - report(service, ...), which only formats; - run(), which delivers to each chat. trades (R5): a Trade dataclass replaces the 9-element lists that were sorted by position. The Yahoo lookup of each trade's market moves into gather(), and the closure that collected the new trade ids goes: run() records the ids of the trades it reported. cal (R6): primary_listing() takes the lookup that reassigned market_data inside a loop over it: an ADR's listing in finbot_adr.json, the NZX for an ASX ticker reporting in NZD, the exchange suffix of a foreign company's US ticker. gather() returns the dated events once. Checks: - The output comparison is identical in all 159 steps. - An old/new comparison of cal on crafted listings is identical on Telegram and Slack. It covered an ADR, an NZD listing, an ASML-style suffix, a ticker's own details, and a title mismatch, an ETF and a far date that are left out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
- formats.sections joins blocks with a blank line between each two, for .help, the profile, .who, .plan, trades, the calendar and milestone. A trades report over days no longer starts with a blank line, nor does a milestone report of record lows only, and .plan and .who no longer end with one. - .history builds the line of the period pressed itself; it no longer returns a table its caller threw away. - Unreachable branches removed: an empty performance report (gather raises first), its rebuild's guard, the compare rebuild's; performance.report's default that repeated run's. - A .jobs button pressed while the scheduler is off is told so directly, not by catching AttributeError. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
price.report ran once per chat and refetched in that loop: Yahoo's histories for the period and the small caps' details. Now gather() works out each stock's change over the period (Sharesight's for a holding, Yahoo's for the rest) and the one stock's line chart, moves() picks the moves once (the mode's filters, the threshold, the scheduled trigger), and report() only formats them for one network. What the chats see changes in four places: - The at-close and period headings say "≥" as the others do (≥ 5% at close, Price movement ≥ 5%). - The top list's chart says "% change over the past 3 months", not "in 3 months". - With fewer than twice `top` stocks, the Bottom list no longer repeats the Top list's stocks, and with none left there is no Bottom list and no gap in the chart. - One stock with no change over a period is said to have none over that period, not "no interday price". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…anges Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…es match the caption The renderers take the Chart and are looked up by kind (render.RENDERERS), so draw() no longer unpacks each kind its own way, and the tests draw specs instead of calling renderers with positional arguments. One time-axes helper for the line and compare charts, one table of sign colours, one way to turn (date, close) rows into a series, one shrink-to-fit, and the row limits in one place (render.trimmed). What the charts show changes, as decided (D22): - A long title or subtitle is shrunk to fit, where it ran off the right edge (.fpe negative, long company names). - The line chart's title is the company and the span; beneath it, the price and the change the caption gives (164.12 USD ▲ 6.96%), where the title had Yahoo's figures and the subtitle a change of its own that disagreed. A null close no longer breaks the line or draws "nan". - Dates are unpadded (1 Oct, 5 Jul 2026), and the date ticks are muted like the value ticks, both 10 pt. - The compare chart's axes match the line chart's; crypto labels lose -USD everywhere (BTC, not BTC-USD). - The shorts chart marks the stocks it leaves out above 24 with a gap row, as the bar charts do. - .performance sorts one list for its caption and chart. The golden images of the line and compare charts are redrawn; the pie has one now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…pandas, dated where it trades
- One way to read Yahoo's answers, for quotes, details and charts: the first host's 200 is the answer, a 404 Not
Found is no such symbol, anything else an error. An outage used to read as an unknown symbol for a detail ("I
could not find TSLA", after trying TSLA.AX too). The chart now falls back to query1 as the others do, and a crumb
Yahoo refuses (401) is renewed once.
- The cookie and its crumb live in one cache file, renewed together under a plain lock; no cookie or no crumb is an
error, where a fallback pair from 2024 only turned it into a refusal later. The cache flush needs no lock.
- price_history works on the bars without pandas: each bar dated in its exchange's time zone (an ASX bar carried
the day before's date all summer, an NZX bar always, so N-day changes slipped a session), the price and today's
change from the quote where the caller has one (the chart is cached up to 12 hours). It takes 3 ms for every
period on a 10-year chart (15 ms before), 2 ms for one (5 ms); price and performance jobs no longer import pandas.
- Record highs and lows come from the whole history, and records seeded from 10 years are seeded again.
- Fields that never filled or nothing read are gone (dividend_date, 52-week change, the details' 52-week range,
price to book, quarterly earnings growth, profit margin, analysts per estimate), and so is the profile's net
debt/equity line, which Yahoo no longer sends the figures for (D30), with the balance-sheet module.
- Log lines are whole lines (fragments without a newline landed in front of other threads' lines), and the ones
every ETF set off are gone.
- An unreachable host costs 5 s to connect, not http_timeout (18 s): net sets one default for every request.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…rts; one warming pass - Every request goes through one function: one at a time, retried while Sharesight says too many are in flight (only the performance report retried before), and checked: an error answer says Sharesight's error (an OAuth refusal, which has no error_code, was a KeyError on it), an answer that is not 200 or lacks what was asked for says so (it was a KeyError later). - One cache-or-fetch, cache.cached, serves Sharesight and Yahoo. A trade's portfolio name is added after the cache, not written into it (and out of the sample, which came from that cache). - Portfolio.gain(days) and Portfolio.gains(days) give the portfolio's and each holding's capital gain, so the price and performance reports no longer dig through Sharesight's report. - The cache warmer refreshes each file at 90% of its lifetime and says when the next is due in the same pass; keep_warm lost the stop event only the tests used, and the fetchers are private. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…x for every market - TSE is the Toronto Stock Exchange in Sharesight, and TYO Tokyo (as in Sharesight's code list, and as the original ticker translation had it). Since d2b297c a TSE holding was looked up on Tokyo (SHOP.T), dropped from the reports, and shown with Japan's flag and yen; it is SHOP.TO again, with Canada's flag and dollar. - One table holds each market's flag, currency, Yahoo suffix and Google code. 13 of 40 codes had a suffix, so a holding on Xetra (Sharesight's XETR), Frankfurt, Milan, Madrid, Swiss, Nordic, Indian, Chinese or Thai markets asked Yahoo for SAP.XETR and dropped out; OTC holdings (Sharesight's OTC) did too. Codes from Sharesight's list that were missing are in (XETR, CVE, CNSX, NEO, SHG, BSE, OSL, CSE, HEL, ASE, IDX, KLS, PHS); the old ones stay. - European markets get their country's flag (Amsterdam was 🇳🇱 as a Yahoo ticker but 🇪🇺 as Sharesight's AMS); EURONEXT, five exchanges, keeps 🇪🇺. - One venue table maps Yahoo's names and ticker suffixes to those codes, so the flag, Google code and country of an exchange come from the same row. The price report's heading now says AU for the ASX (it said ASX; the docstring always promised AU), US for Cboe US, and GB for London (it said UK); a Google link for an NZX stock says NZE and one for Xetra ETR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…p by its type A preference was written twice in lib/config.py (its field, then its read with the default) and twice more in the tests (a table of every default, and a count of the keys). Now the field holds the default and its type says how it is read (a tuple is a list of words; exchange_suffix and hyperlinkProvider are tidied as before), so a new preference goes in three places: the field, defaults/preferences.conf and the README's table. The tests check the template against the fields' defaults, which are the same as before (checked against the old table before it went). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…fter themselves - Cache files are written compact (a 10-year chart: 832 KB to 297 KB, and quicker to parse); state files keep their indentation, for people to read. - lib/config no longer changes directory when imported, so util/check_config.py and split_env.py take a relative path from the shell's directory, as a command-line tool should. A relative DATA_DIR (and the default, var/) is still in the checkout, wherever finbot starts. - A full test run left about 100 directories in /tmp (preferences files, schedules, job records); now none. - The scheduler's flag that skipped a whole line while its last run went on is gone: the set of commands running already skips any command still going, whoever started it. The in-process runner only the tests used moved to them. - formats: ordinal and strip_url in a line each; td_to_human went with its last caller. util/sws_gen.py runs only as a script and leaves the umask as it found it. .holdings and .pie lost their branch for no portfolio, which the command now answers with its usage and the portfolios before they are called. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…says why Telegram refused
sendMessage and sendPhoto were posted by hand, so a refusal logged "400 error outbound to telegram" (the message) or
the raw answer (the chart). They go through telegram.call now, as the edits and acknowledgements already did: a
refusal logs Telegram's reason ("Bad Request: can't parse entities"), a 200 that is not JSON counts as a failure, and a
timeout on a chart is still MAYBE_SENT. sendPhoto is no longer sent disable_web_page_preview, which it does not take.
The chat id goes in the body; the answers are Telegram's messages, which the pinning reads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…Press process_request took seven positional arguments and process_callback five, which bot.py unpacked from the Message and Press that inbound had already made. They take those objects now, so a way of receiving updates other than a webhook (a pull mode) hands over the same thing, and the .jobs button handler takes the Press too. The test recorder hands over either form, so it still compares with commits from before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
A network's class read its updates and also held its webhook: the path, the secret header or signature, the form a Slack press comes in. The readers (Telegram, Slack) now only read, and a webhook class per network (TelegramWebhook, SlackWebhook) takes the request in and hands the update to its reader. bot.take(network, update) does what follows for any way of arriving (read it, drop a repeat, hand it to the worker), so a pull mode (getUpdates, Socket Mode, a gateway) only has to fetch updates and call it. A test hands updates over without a webhook. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…s in a plain list - Every report gives its chart as a render.Chart, which deliver() draws where it can go; the path for a chart drawn already served only two tests, which give a spec now. - default_chats() lists the destinations plainly, Telegram, then Slack (the channel as the bot, else the incoming webhook), then Discord, where it worked them out from the order of the webhooks plus a Slack afterthought. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
A period button's data was made in buttons.keyboard but taken apart in worker.process_callback, and a .jobs button's was made in about.py and taken apart in worker again. buttons.parse reads both (a ChartPress or a JobPress, or None), beside keyboard and a new job_button, so the format lives in one file; malformed .jobs data is still refused by the schedule's check, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
… builds it .history's first view came from its button builder, but .compare's and .price SYMBOL PERIOD's were made by other code, so the chart a command posted and the one its own button redraws could differ. All three go through the builder now (worker.chart_reply), and the price report lost the code that drew one stock's line, which only that view used. One difference: with performance_use_sharesight on, .price SYMBOL PERIOD used to give Sharesight's figure and no chart; it gives Yahoo's figure and line now, as its buttons always did (the lists still take Sharesight's figures). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…deleted - .jobs on Slack lists the commands with a button each, two to a row of Block Kit, where presses reach finbot (the bot token and slackOutgoingWebhook), and says to tap one; a press of a stale button puts the current list in its place (SlackBot.edit_text, chat.update). Whether a chat gets the buttons is now its transport's say, not a test for Telegram. - A period button pressed under a Slack chart uploads the new chart, shows it, and then deletes the file it replaced (D28): nothing reuses an uploaded chart, since a period shown again is uploaded again, so deleting costs nothing and the bot's files no longer pile up. Each chart message's file is noted in finbot_slack_charts.json (the newest 500); a message from before this, or noted no longer, keeps its earlier file. files.delete needs the files:write scope the uploads already have. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
chunk_maxlines capped the lines per message only of a report already too long for one message, which the README did not say (it read as a split of any longer report). The size-aware split does the job, so the preference goes (D23): a long report now fills each message up to the network's limit instead of 20 lines. preferences.conf files that still set it load as before, and the log at start says to delete the line, as for alliterate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
418 lines of code, tests, shipped defaults, units and tools were over 120 columns (tabs counting 4), which every edit quotes whole. They are rewrapped: comments and docstrings re-flowed (a one-line docstring that no longer fit was reworded to fit, as the convention asks), code split after a comma inside its brackets or given a name for a part, the odd long string joined from two. Nothing runs differently: the tests pass and the recorder finds every command, button and scheduled report the same. A test in test_finbot holds every line to 120 columns from now on; README.md, CLAUDE.md and doc/ are left out, since CI runs no tests for a change to those alone, and so are data files. Along the way the tests' repeated "environment without finbot's settings" became common.bare_environment(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…ught up to date (D25, N7) test_providers.py (65 KB), test_jobs.py (44 KB) and test_edges.py (42 KB) were the biggest files a session opened. They are split by subject, as moves only (the same 400 tests pass): - test_yahoo, test_sharesight, test_sending (Telegram, Slack and Discord posting) and test_telegram_api, from test_providers; the stand-ins they share (sample, Resp, serve, ProviderCase) moved into tests/common.py; - test_schedule (the schedule file, job arguments) and test_scheduler (firing, daylight saving, runs from chat, child processes), from test_jobs; - test_inbound (webhook requests, Slack events and signatures, text escaped on arrival), from test_edges, which keeps the failures, bad arguments, secrets in logs and signals. CLAUDE.md: the new test map, the code map for buttons, charts, markets and the test helpers, the worker's entry points, the README's size (36 KB), and its own lines wrapped at 120 from the Facts down. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…e figures A list over a period (.price 7d, price.py 7) took each holding's figure from Sharesight even with performance_use_sharesight off, while one stock's view (.price BHP 7d) drew Yahoo's history and the figure that goes with it. Now both take the same source: - off (the default): Yahoo's price history for every stock, which is the change its chart draws, and covers the watchlist too; - on: each holding's capital gain from Sharesight, which counts only the time it was held, for the list and for one stock, whose view then comes without a chart: Sharesight has no price history to draw. The README's row for the preference says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…load again A press on a chart's period button uploaded the chart afresh every time and deleted the file it replaced, so switching back to a period meant building, drawing and uploading it again, with Slack's wait for a new file to become showable. Now the upload of each period is noted with the report that came with it, and a press for that period within 15 minutes, on this message or another, shows that upload again in one chat.update: nothing is built, drawn or uploaded. If Slack will not show it (a file deleted outside finbot, say), the press builds and uploads as before. An upload is deleted once no chart message shows it and it is a minute past those 15 minutes (the minute lets a press that is reusing it finish showing it); a file a message shows is never deleted. The chart messages noted the old way are forgotten, their files left alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…fresh every 8 hours A period list or a stock's chart reads each ticker's 10-year daily history from the cache, where it is kept for cache_seconds (12 hours by default); the first request after it runs out waited on Yahoo, one request per ticker. A new scheduled job, yahoo_warm.py, fetches every followed stock's history afresh (the watchlist and the holdings; the watchlist alone while Sharesight cannot be asked), and the default schedule runs it every 8 hours, so none runs out. With the cache preference off it fetches nothing, since nothing would be kept. It is housekeeping, so .jobs does not offer it. Sharesight's warmer keeps its own timing in the bot (it refreshes each cache at 90% of its lifetime, and at once after a flush); its thread is now named sharesight_warm, the name the README gives it beside yahoo_warm.py. - yahoo.prefetch_history takes the cache age to fetch past (0: all afresh) and returns the tickers that failed. - lib/jobrun.py takes a module name with an underscore. - A data directory with its own schedule.cron needs the line: 07 */8 * * * yahoo_warm.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
.jobs left the scheduled commands that are not run from chat (cleanup, podcasts, and now yahoo_warm) out of its list entirely. It now lists them after the numbered commands under a System heading, unnumbered and without buttons, marked when running like the rest: they run on their schedule only. `.jobs run cleanup.py` says it is a system job instead of that it is not in the schedule, and a schedule of system jobs alone lists them rather than saying there are no jobs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…ht retired With Yahoo's price histories kept warm (yahoo_warm.py), Yahoo answers a period list faster than Sharesight, whose report is per portfolio and per period, kept 30 minutes and fetched one at a time; and its figure is the better one: the stock's move over the whole period (Sharesight's capital gain counts only the time a holding was held), for the watchlist too, and the change its chart draws. So the Sharesight path goes: - .price 7d, price.py 7 and .price BHP 7d take every figure from Yahoo's history, and one stock over a period is always its chart; - performance_use_sharesight is retired: a preferences.conf that still sets it starts as before, with a note to delete the line; - Portfolio.gains, which only that path read, goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…, like Yahoo's The bot kept Sharesight's portfolio list and holdings reports warm from a thread of its own, which refreshed each cache at 90% of its lifetime and again at once after a flush. It is now a scheduled job like yahoo_warm.py, on a fixed interval inside a cache lifetime made a little longer: - sharesight_warm.py fetches the portfolio list and each portfolio's holdings report afresh; the default schedule runs it every 30 minutes (at :14 and :44, clear of trades.py); - performance_cache_seconds defaults to 40 minutes (was 30), so the holdings never run out between runs; - without Sharesight configured, or with the cache off, it fetches nothing; a failure is logged, not reported in the chat: the caches it would have replaced are served while fresh, and the next run tries again; - .jobs lists it under System. The thread goes, with sharesight.keep_warm and wake: after a flush, the warm jobs fill the cache again on their next runs (a request meanwhile fetches what it needs). sharesight.warm() now simply fetches both afresh. A data directory with its own schedule.cron needs the line `14,44 * * * * sharesight_warm.py`; a preferences.conf that sets performance_cache_seconds=1800 should raise it, or drop the line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
After a flush, the cache stayed empty until each warm job's next run (up to 30 minutes for Sharesight, 8 hours for Yahoo), so requests meanwhile waited on the sources. Now the flush, once the files are deleted, runs sharesight_warm.py and yahoo_warm.py at once, one after the other in a thread of their own (a reload does not wait for them): through the scheduler, which does not start one already running on its schedule, or with the scheduler off each in a child process. The log says when they start and when they are done. yahoo_warm.py asks Sharesight for the holdings only where Sharesight is configured, as sharesight_warm.py does: without it, the watchlist's histories are fetched and no request goes to Sharesight. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…0z2q 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 the work branch since #38: the overnight plan (Q2–Q5, M1–M6, M8, D8), its second pass (P2-1 to P2-8), and today's follow-ups. M7 (the Discord bot) and M9 (dropping gevent) stay deferred.
Structure
config.env(finbot.env) andconfig.prefs(preferences.conf), read where used, with no star import; each preference's default lives on itsPrefsfield.state.edit), and the watchlist and Sharesight portfolios are objects.lib/util.pysplit into markets, formats, links and render. Reports move into alib/reports/package, each withrun(dest=None, **kwargs)andjob(argv); the top-level scripts are shims, so cron lines keep working.Report(lines, chart, buttons)withdeliver(). Messages split by size on every network (chunk_maxlinesretired)..helpand the Telegram keyboard; one argument reader serves every command.render.Chart), drawn once and only where they go, cached by the spec's hash. Builders are per kind, and a chart's first view is built the way a period button builds it.Chat networks
slackChannelID..jobsbuttons.doc/, and a chat-features table in the README.Commands and reports
exchange_suffix,AXby default) applies to every ticker..helloand.thanksare gone.reminder.json. Only the scheduled earnings report is pinned..jobslists the system jobs under System, without a number or a button.Data sources and caching
systemctl kill -s USR1 finbot, orfinbot-flush.service), then runs both warm jobs straight away.sharesight_warm.pyevery 30 minutes; the holdings are cached 40 minutes.yahoo_warm.pyevery 8 hours; the price histories are cached 12 hours.Fixes
Upgrading (test environment and production)
preferences.confalliterate,chunk_maxlinesandperformance_use_sharesight. finbot still starts with them, and logs a note to delete them.performance_cache_secondsnow defaults to2400. A file that sets1800should raise it, or drop the line.exchange_suffix, defaultAX.finbot.env: new and optional,slackSigningSecretandslackChannelID.schedule.cron: a data directory with its own copy needs these lines:doc/slack.md):chat:write,files:write,files:readandreactions:write.sudo cp finbot-flush.service /etc/systemd/system/ && sudo systemctl daemon-reload.Testing
.venv/bin/python -m unittest discover -s tests -b).util/record_outputs.pycompared what every command, button and scheduled report posts against the previous commit, so only intended changes went in.🤖 Generated with Claude Code
https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
Generated by Claude Code