Skip to content

Simplify and fix: shared HTTP session, command table, Yahoo error handling, USD market caps, min_analysts, consolidation sweep - #37

Merged
robdevops merged 26 commits into
mainfrom
claude/kind-newton-ad0z2q
Oct 14, 2026
Merged

robdevops merged 26 commits into
mainfrom
claude/kind-newton-ad0z2q

Conversation

@robdevops

Copy link
Copy Markdown
Owner

A simplification pass over bot.py and lib/, the fixes from the review list, a consolidation sweep, and the report changes decided along the way: 25 commits.

Changes you will notice in chat

  • .price 3 is 3%, as the README documents and .session, .premarket and .shorts already did. A period carries its unit (.price 3 7d). Decimals work everywhere (.session 2.5), and thresholds print as 5%, not 5.0%.
  • No interim "Fetching…" notices. Every command that fetches shows typing instead.
  • .marketcap ranks in US dollars, converted at Yahoo's exchange rates, so a won market cap no longer tops the list. .marketcap SYMBOL shows the stock's own currency and USD. .beta's "under US$2B" and demote_volatile's small-cap thresholds are US dollars too; for ASX stocks those thresholds move by the exchange rate.
  • Analyst filter: .buy/.sell and the .forward pe lists take only stocks covered by min_analysts analysts (new preference, default 7). Ratings list the strongest first, and more analysts win a tie.
  • .forward pe sends a bar chart. The Telegram keyboard has one .buy & sell button (it sends both reports) and a .forward pe button, in alphabetical order.
  • Milestone records are all-time. Old 52-week records are replaced from each ticker's price history on the next run.
  • Commands:
    • .top is removed; use .price top [period].
    • .history and .compare alone give their usage.
    • An unknown portfolio gets the same answer from .trades, .performance, .holdings and .pie.
    • .trades with no buys or sells says so.
  • Markets:
    • Sharesight's TSE is Tokyo (.T) and TSX is Toronto (.TO).
    • NZX (.NZ) and SGX (.SI, SGD, 🇸🇬) are mapped.
    • The Danish krone is DKK.
    • The Ireland, Belgium and Saudi suffixes for ADR primary listings are fixed.

Fixes

  • Security: one cookie-less HTTP session. Tokens, webhook paths and crumbs are kept out of the logs, and Yahoo's cookie request has a timeout.
  • Yahoo:
    • A failed chart fetch raises.
    • A missing, null or garbled field is left out.
    • A 52-week low of zero or below is dropped.
    • The stale currency in quotes is fixed.
  • Crashes fixed:
    • The .SYMBOL profile when Yahoo leaves out any of several fields, or equity is zero.
    • The earnings calendar on an ADR whose primary listing Yahoo doesn't know.
    • podcasts.py on feeds mixing dated and undated items.
    • Pinning on a network error.
    • Scheduled reports through a Slack webhook without a bot token.
  • State:
    • milestone.py no longer records 0/0, which caused fake record highs.
    • rating.py keeps the last rating of a ticker missing from a run.
    • An interactive rating writes no state.
    • The podcast seen-list is written atomically.
  • Small fixes:
    • .hello no longer grows the shared word list.
    • A blank first name falls back to the handle.
    • Slack's debug line goes to stderr.
    • transform_title's suffix rules are fixed.
    • Company descriptions keep their last sentences.
    • The PS-ratio ⚠️ no longer carries over from another line.

Consolidation

  • Worker: one function per command in an ordered COMMANDS table, built around a Request object.
  • Reports: price and performance are split into gather, report and delivery. Destinations come from one place (webhook.destinations/default_chats/send).
  • Shared helpers:
    • yahoo.fetch_details, which replaces six loops
    • yahoo.add_usd_market_caps
    • telegram.call/try_call
    • config.data_json
    • config.print_errors
    • sharesight.pick_portfolios and PortfolioNotFound
    • util.security_line
    • chart _style_time_axes/_heading (the images are byte for byte as before)
    • transform_title tables
    • ratings, shorts and market caps each sort one list once
  • cal.lambda_handler(event='earnings' | 'ex-dividend') replaces the two flags. Schedule lines (cal.py earnings, cal.py ex-dividend) are unchanged.
  • Removed: dead code, commented-out code and unused imports.

Config and docs

  • New preference min_analysts (default 7, reloadable): documented in the README table and in defaults/preferences.conf. Existing preferences.conf files need no edit.
  • README: corrected .session and @botname price, and documented USD market caps and the analyst filter.
  • CLAUDE.md: an open PR's title and description are kept in step with every push.

Testing

  • 293 offline tests pass (.venv/bin/python -m unittest discover -s tests). The regression test for each fixed bug fails on the old code.
  • Not checked against live Yahoo, which was blocked in the development environment. The exchange-rate symbols and pence handling follow Yahoo's conventions.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX


Generated by Claude Code

claude added 26 commits October 14, 2026 06:28
… push

#35 was opened for a CI tweak, took in the 14 review fixes pushed to the work branch afterwards, and merged under its old title.
The new section: before each push, check for an open PR from the work branch and update its title and description to cover
every commit it holds; never make such a PR mergeable while they are out of date.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…ers, the ylim shim, unused imports

- lib/discord.py (a pasted discord.py sample that could not even be imported) and lib/slack.py (getUser,
  never called) are gone; outbound Discord and Slack go through lib/webhook.py as before
- util.categorise_tickers and util.get_currency_symbol had no callers; rows_chart no longer renames a
  ylim argument that nothing passes; webhook.strikethrough had no users (italics stays: reminder.py uses it)
- shortman.fetch drops the locals it never read, and still parses every row's percentage
- unused imports: json in reports, sharesight and shortman; io, json and lib.telegram in yahoo; util in
  webhook; shlex in jobs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…lder; the chart setup written once

- flag_from_market and currency_from_market read one MARKETS table (flag, currency) instead of two
  50-line if-chains walking the same groups; flag_from_ticker, transform_to_yahoo and the shared
  aliases of transform_to_google and exchange_human are tables too. Results are unchanged, checked
  against the old functions for every code either version knows (8,100 calls), quirks included
  (SGX still maps to the China flag and CNY, as the old chains' first match did)
- yahoo_link and gfinance_link build their markup with link(); quote_link is the "Yahoo for what
  Google Finance has no page for" rule that charts.py and price.py each spelled out (identical links
  on 18,700 combinations of service, hyperlink setting and provider)
- the chart palette, the 1280x800 figure and the PNG ending are written once (_figure, _png); the
  nine renderers give byte-identical PNGs
- hbar_chart loses axis_fmt, which nothing could see (the value axis is hidden), and its never-read
  span, grid and inside-label state; the three chart specs that passed axis_fmt drop it
- cache_path is the one place that says where cache and state files live
- days_from_human_days tries each suffix in a loop (same results and errors on 300 inputs)
- numpy and matplotlib.dates load inside the chart functions: a job process that draws no chart
  (trades.py every 20 minutes, reminder, cal, rating, shorts' text runs) no longer imports them.
  test_datadir imports matplotlib itself, so it still proves matplotlib starts without a home dir

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…s one thread at a time; fetch_each

- fetch_detail reads its ~40 plain fields from DETAIL_FIELDS (path, conversion, and the extra error
  that also means "not there" where the old block caught one) instead of 55 copy-paste try blocks;
  the irregular ones (52-week low, net income, recommendations, trend, quarterly, insider) stay code.
  The duplicate currency block is gone. Checked against the old parser on the doc/yahoo samples and
  6,000 single-point mutations of them (fields and modules deleted, nulled, given bad types): the
  same dict or the same exception every time
- fetch() reads its optional quote fields from QUOTE_FIELDS (1,600 mutations, identical)
- price_history drops a caption it built and never used, filters the days range once, and looks up
  the date-based periods (10Y to 1M, 7D) in one branch (identical tables and charts on 129 calls)
- fetch_chart_json parses the response once and no longer fetches a cookie it never sends (getCrumb
  still renews it when due); getCookie measures the cache file's age with util.cache_age; rmCrumb
  moves the stale crumb aside with Path.replace
- getCookie and getCrumb hold one lock: two threads renewing at once could cache a crumb for the
  cookie the other just replaced, for 14 days. prefetch_history already fetched 8 at a time
- fetch_each(get, tickers) runs per-ticker fetches 8 at a time and returns results or exceptions in
  order; prefetch_history uses it
- pandas loads in the functions that parse a history, not when the module is imported

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
- webhook: bold, italic and strike read one MARKUP table; payload_wrapper picks the size limit once
  (same "over 2,000/4,096 bytes" lines, same sends: checked on 180 calls); sendPhoto asks
  takes_charts once; pleaseHold loses its non-Telegram branch, which nothing could reach
  (TypingIndicator only runs on Telegram) and whose own Telegram arm was unreachable twice over
- sharesight: _request and _check replace four copies of the request / status / error-field block in
  get_token, get_portfolios, get_trades and get_performance (same messages, same order: get_trades
  still prints the trade count before the error check); find_portfolio is the case-insensitive
  portfolio lookup that reports.py writes out twice
- charts: the price chart's link is util.quote_link (price.py's copy of that rule too) and its marker
  util.get_emoji; the .history default view parses its 10-year history once instead of twice
  (history_payload hands back the chart it fetched); the history highlight comes from PERIODS rather
  than a second table of the same labels; one price.lambda_handler call for the price-list buttons
- bot: the Telegram branch reads the message through two locals; the misspelt userRealname, never
  read, is gone (userRealName is the first name, as before: the fallback it was meant to set never
  ran); the worker thread runs worker.process_request directly; git_version defaults to REPO_DIR
- jobs: the job process runs in REPO_DIR; commands() builds the hidden-script set once

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…; one portfolio lookup and holding line

- .buy, .sell and the .peg lists fetched each ticker's Yahoo details one after another: tens of seconds
  on a cold cache for a hundred tickers, outlasting the typing indicator. They now go through
  yahoo.fetch_each, 8 at a time, and each list keeps its own error handling: ratings still skip a
  ticker that raised TypeError and fail on anything else, the PEG lists still print "value not found"
  for a KeyError, and the first failure in ticker order is the one reported
- .compare downloads its 2 to 6 full histories at the same time (the first failure is still the one
  reported)
- the .SYMBOL profile asks price_history for the percentages only: it rendered (or read) a chart it
  then dropped, up to 0.15 s of CPU on the bot's event loop for each cold ticker
- simplywallst.get_url ran twice per profile (two identical lines); now once
- holdings and .pie find the portfolio with sharesight.find_portfolio; holding_line formats a
  Sharesight holding for the holdings list (and .who, next); one module-level last_col replaces two
  nested copies; the buy and sell branches of the ratings share one condition; the four delta lines
  come from a loop
- prepare_rating_payload is indented one tab, like every other function

Checked against the previous module on 1,578 calls with canned data (every report, service, action and
argument form, with tickers that raise TypeError, KeyError, ValueError or RuntimeError): the same
payloads, chart rows and exceptions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…th block; no print-and-reraise wrappers

- the 28 command patterns were all matched on every message up front, 100-odd lines above the
  if/elif chain that decides which one wins. Each pattern now sits in its own branch condition, in
  the same order (checked: identical patterns, identical order), and matching stops at the first hit.
  .buy and .sell, two identical branches, are one
- working(service, chat_id, ...) starts the typing indicator, sends a command's "Fetching..." notice
  where there is no typing indicator, and stops the indicator however the block ends. It replaces 23
  hand-written start/stop pairs and the per-request registry that stopped the ones a failed command
  left running: the error line still goes out after the typing has stopped
- the 22 `except Exception as e: print(e); raise` wrappers are gone: process_request's report_error
  already logs the error line and the traceback (stderr loses only the duplicate bare message)
- period_or_ticker, percent_or_ticker and period_and_portfolio parse the arguments that nine branches
  parsed by hand; the reply URL is webhook.chat_url; deliver() has one chart branch
- .beta fetches its tickers' details at the same time (yahoo.fetch_each) and sorts its lines and chart
  rows once, together; .who formats its lines with reports.holding_line
- dead locals gone: a doubled `threshold = threshold=` assignment, an unused specific_stock, a
  "DEBUG" print in .top, checks on match groups that cannot be empty

Checked against the previous worker on 276 messages (every command and argument form, on Telegram
DMs, Telegram groups and Slack) and on 1,748 more with Yahoo or Sharesight failing: the same sends,
typing starts and stops, and error lines, in the same order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
… of the logs; a timeout on Yahoo's cookie

- lib/net.py holds one requests.Session that webhook, telegram, yahoo, sharesight, shortman and
  podcasts all send through, so calls to the same host reuse a connection instead of a new TCP and
  TLS handshake each time (each typing notice and reply, each ticker of a Yahoo burst, each
  Sharesight report). The session keeps no cookies: Yahoo's cookie still travels only in the Cookie
  header finbot sets, and the request that asks fc.yahoo.com for a new cookie never carries the old
  one (a stored one would be sent back and Yahoo would set none). No retries are added: Telegram and
  Slack posts are not safe to resend, and the Yahoo calls already fall back to the second host
- net.redact strips Telegram bot tokens, Slack and Discord webhook paths, bearer tokens and token,
  crumb, key, secret and auth parameters. Applied where secrets were printed: a failed webhook.write
  (it printed the URL with the bot token and Slack's Authorization header), failed sendPhoto, chart
  upload, message edits, typing notices and button answers (requests' errors quote the token URL),
  the traceback report_error and the WSGI handler print, Yahoo URLs (the crumb) and errors. The new
  Yahoo cookie and crumb, and Sharesight's token in debug, are no longer printed at all; print_headers
  hides Telegram's secret header and print_body Slack's verification token
- getCookie's request to fc.yahoo.com has the same timeout as every other request (it had none)
- tests patch lib.net where they patched requests.get/post; new tests: secrets stay out of a failed
  send's log and a reported traceback; the session keeps no cookies

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…null or garbled field is left out

- fetch_chart_json raises RuntimeError (no secrets in it) instead of returning (error, None), and
  price_history and price_series pass it on: the nine places that checked for an error string are
  gone. Callers that skip one ticker now catch it: the price list and the profile leave that ticker's
  lines out, performance's benchmark line says "no price history", milestone skips the ticker, and
  prefetch logs it once. The ones that report it (.history, the price chart, .price TICKER, the chart
  buttons) let it reach report_error as before. historic_high raises on a history with no highs and
  lows rather than returning None for its caller to trip over. price_history raises on a history
  with no prices, uses the chart's last close when Yahoo gives no current price, and UTC when it gives
  no time zone. The "temporarily disabled" inject-latest block and its unused time are gone
- fetch_detail: one dig() reads every path; a field Yahoo leaves out, sends as null or garbles (text
  or a list where a number belongs) is left out instead of failing the whole ticker or crashing.
  This also ends the crash on a 52-week low of 0 or less (now left out, as fetch() did) and the
  half-stored analyst estimates; the recommendation is stored whole or not at all. One percentage
  converter: a Yahoo 0 is 0.0 like every other percentage. Trailing P/E is rounded like fetch()'s,
  not truncated (56.7 showed as 56). 6,000 mutated responses: none raises
- fetch(): the currency is the quote's own (a quote without one took the previous ticker's, or crashed
  on the first); a null name or price skips the ticker; the earnings date is the first announced date
  still to come among those Yahoo sends; percentages are floats
- one query2-then-query1 fallback (_get_first) for the crumb, quotes and details
- util: read_cache and read_binary_cache share one freshness check (_fresh_cache, ages from
  cache_age); its hit, miss and expired lines are all debug-only now (the PNG miss line printed
  always). getCookie reads its cache through read_cache, renewing 30 seconds early as before

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…lit into gather, report and delivery

Destinations
- webhook.default_chats() says where a scheduled report goes: Telegram's telegramChatID (left out
  without one), and Slack and Discord through their incoming webhooks, with no chat id. It raises
  "nowhere to post" when finbot.env names none. report_error, price, performance, shorts, cal,
  trades, rating, milestone, reminder, podcasts and bot_send_stdin all use it, and
  webhook.destinations(interactive, service, chat_id) adds the reply-to-the-asker case
- this fixes the crash with slack_webhook set, no slackBotToken and telegramChatID set: price,
  shorts, rating and milestone sent Telegram's chat id to Slack's webhook, write() then needed the
  bot token and raised TypeError before Telegram got its copy. A Slack reply without slackBotToken
  now logs why and sends nothing instead of raising
- the ten `if not webhooks: sys.exit(1)` guards are gone: in a chat reply they killed the worker
  thread outright (SystemExit gets past except Exception), e.g. on a Slack bot without slack_webhook
- webhook.send(service, url, chat, payload, image, buttons) sends the chart with the text as caption,
  or the text: the scripts no longer each choose between sendPhoto and payload_wrapper
- cal's scheduled run uses its days argument (it used config_future_days, which is the default);
  shortman.fetch hands the quotes back unchanged when Short Man is down (it returned {} and the
  US short interest from Yahoo went too); shorts, cal and rating fetch Yahoo details 8 at a time and
  skip a ticker whose fetch failed; trades finds its portfolio with sharesight.find_portfolio;
  milestone loses parameters nothing passes (it never had an interactive path)

price.py and performance.py
- gather() fetches the data once; report(service, ...) builds (payload, chart) for one service;
  lambda_handler sends it. The chart period buttons call report(): return_result and _rows_out go
- price: the volatile-stock second pass recursed with the demotion still on, so it re-failed the very
  tickers it was meant to add back (and could add a second heading). Now, as its comment intended,
  a leveraged or volatile ticker needs its higher bar (trigger_bar) only to send a scheduled report
  on its own; once one goes out, every ticker past the threshold is listed, whatever their order.
  Interactive reports are unchanged (identical on 39 cases), as are scheduled ones without demotion
- performance: benchmark() and report() at module level; util.get_emoji instead of a nested copy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…ices; .hello no longer grows its word list

- COMMANDS lists (pattern, function) in match order; each command is a function taking the request and the match.
- The "Fetching..." notices are gone, searchVerb with them; every command that fetches shows typing, a ticker lookup too.
- .hello: the three struck-out verbs are local, so the shared list no longer grows and a tuple of them is never picked.
- Percent arguments are numbers (2.5 included; nan and inf are tickers); thresholds print without a trailing .0.
- .beta, .who and the one-ticker .marketcap are built in reports; .beta says so when no stock qualifies.
- .pe, .fpe and .peg read a ticker as the other commands do; .performance and .trades take a portfolio name of several words.
- .history and .compare on their own give their usage instead of the profile of a ticker named after them.
- bot.py: a blank Telegram first name falls back to the handle; Slack's debug line goes to stderr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…d SGX get their Yahoo suffixes, flags and currencies

- TSE (Sharesight's Tokyo) becomes .T; TSX becomes .TO, NZX .NZ and SGX .SI.
- SGX is Singapore (SGD), not China; NZX gets a flag and NZD; .SI, .T and .TA tickers get flags.
- The Danish krone is DKK, not DEK.
- Country suffixes for ADR primary listings: Ireland is IR (the stray dot made X..IR), Belgium BR (AS is Amsterdam), Saudi Arabia SR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…t has both new values

- Gone: milestone's profit and cash-flow block with its four unused lists, the pinning stub in bot.py, the unpinChatMessage stub,
  and single lines in bot.py, reports, util, podcasts and trades. trades keeps the reason as a comment on the live line.
- milestone wrote 0/0 for a ticker it saw for the first time before it had the ticker's 52-week values; when those were missing
  the zeros stayed, and the next run reported a record high for it. Now a ticker is recorded only with both values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…ashing; data files read in one place; unused imports gone

- The .SYMBOL profile reads one data dict with each field defaulted once. It crashed when Yahoo left out marketState,
  profile_exchange, currency, market cap (with insider data present), regularMarketPrice or sharesOutstanding, on a zero
  shareholder equity, and on a missing percent_change it never used.
- The PS ratio line carried over whatever warning an earlier line had set (forward P/E, insider selling): it now warns only above 15.
- config.data_json reads a data file (the user's copy, else the shipped default) for the ADR table, simplywall.st and reminders.
  For simplywall.st the custom list now wins over the shipped ones, which it used to lose to (they never disagree as shipped).
- Unused imports and two f-strings with nothing to fill removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…ments from two ordered tables

- Pinning, button toasts, text and chart edits and the typing notice go through telegram.call; telegram.try_call is the same
  for calls whose failure is only logged. Pinning used to crash the earnings report on a non-JSON answer or a network error.
- transform_title's ~90 replace lines are TITLE_TRIMS and TITLE_SHORTENINGS, in their old order. Fixed on the way: a title
  ending " Se" kept it (the check was for Se, the removal for SE), a trailing AG, SE, & or "and" removed every occurrence of
  it in the title rather than the one at the end, and runs of spaces left behind are collapsed.
- The test fixture points lib.telegram at the fake Telegram too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
… same way everywhere; trades loses its dead lines

- cal.lambda_handler(event='earnings' | 'ex-dividend') replaces the earnings and dividend flags (with neither set it crashed).
  An ADR whose primary listing Yahoo does not know is skipped instead of crashing the earnings report. "No events found"
  names the period as the heading does.
- sharesight.pick_portfolios is the one lookup for .trades, .performance, .holdings and .pie. A name that matches no portfolio
  raises PortfolioNotFound, which the worker answers with the name and the portfolios there are. It used to be a ValueError,
  "no Sharesight data found", or one of two wordings of that answer.
- trades: an interactive .trades whose trades are all neither buys nor sells says so instead of nothing; an unused start date,
  a second ticker and a portfolio filter that could not filter anything are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…ed items and keeps its state like the other scripts

- rating wrote only the tickers it rated in a run: one that failed to fetch lost its last rating, and its next change went
  unreported (a one-ticker run wiped all the others). The record is now added to, and an interactive run records nothing,
  as with trades.
- podcasts: a feed mixing dated and undated items crashed the sort (aware and naive times); a garbled date is now no date.
  The newest five are picked once, after reading the feed rather than at every item; a run with nothing new posts nothing
  (it used to post an empty report when the stored list was not in sorted order); the seen list is kept with util.json_write
  like the other state files (atomic); single-tab indentation like the rest of the code.
- webhook.italics, an alias of italic, is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…ops did it; the ratings list sorts once

- yahoo.fetch_details fetches the details of many tickers at once and leaves out (and logs) one whose fetch failed, so a list
  goes out without it; strict=True raises instead, for a report on the one ticker asked for. It replaces the loops in .beta,
  .buy/.sell, .peg, shorts, the calendar and rating.py. .buy/.sell used to stop at the first ticker that failed (other than with
  a TypeError), and .peg did too.
- .buy/.sell sorts (rating, analysts, line, ticker) once instead of sorting the text lines and a chart list separately to the same
  key; the report and chart are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…r axis styling and heading; config errors print in one place

- make_paragraphs dropped the words after its last paragraph break, so a short company description came out empty and a longer
  one lost its last sentences.
- The price and compare charts share _style_time_axes, and those two and the pie share _heading; every chart renders byte for
  byte as before.
- config.print_errors logs a ConfigError, where bot.py and config each had the loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
The text lines and the chart rows were two lists sorted separately to the same key; now one list of (percent, line, label) is
sorted once. The report and chart are unchanged, but for the space after 🩳 in the "nothing found" lines, which every other
line has.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…gone; a bare number on .price is a percent

As decided:
- milestone keeps the all-time record (the higher of the stored record and the new 52-week high, the lower for lows). After its
  first run it used to store the 52-week values, so a stock far below its peak posted a record high at each new 52-week high.
  Records kept before this (52-week, under 'high'/'low') are replaced by the ticker's history on the next run.
- .buy and .sell list the strongest rating first (.sell used to start with the mildest), and on equal ratings the one more
  analysts give.
- .top, undocumented and the same as .price top [period], is removed; .top alone looked up a ticker named TOP and .top10 read
  its 0 as a 0-day period.
- .price 3 is 3%, as on .session, .premarket and .shorts and as the README has it; a period carries its unit (.price 3 7d is
  3% over a week; it used to drop the 3). The README's price section names .session (not .midsession) and @botName price.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…on market cap no longer tops .marketcap

- .buy and .sell list only stocks rated by 7 or more analysts, and say so in the heading; with none, the reply says that.
  Thinly covered ASX small caps (two to six brokers) were nearly all a perfect 1.0 and filled the top of .buy.
- yahoo.add_usd_market_caps converts each market cap, which Yahoo gives in the stock's own currency, to US dollars at Yahoo's
  exchange rates (KRW=X and the like); a market quoted in pence, agorot or cents has its cap in pounds, shekels or rand.
  .marketcap ranks and shows US dollars ("(USD)" in the heading; a stock with no rate is left out and logged); .marketcap SYMBOL
  shows its own currency and the US dollar figure. .beta's "mkt cap under 2B" and demote_volatile's small-cap thresholds
  (150M, 1B) are US dollars too: unconverted, a small cap quoted in won or yen was never small. For a stock quoted in Australian
  dollars the thresholds move by the exchange rate. The profile labels a pence-quoted market cap GBP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…pe takes the spare place; buttons stay alphabetical

- The rating command takes both words (.buy & sell, .buy sell, .buy/sell, .buy and sell) and sends each report once, in the
  order asked; .buy and .sell alone are unchanged.
- The Telegram command keyboard has .buy & sell in place of .buy and .sell, and .forward pe in the place that frees, sorted
  alphabetically row by row. Its version changes, so chats get the new keyboard at startup and after their next DM reply.
- A test checks that the keyboard is in order and that every button is a command, not a ticker for the .SYMBOL catch-all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
The forward P/E list already had a chart spec; the command now delivers it, with the list as its caption, where the service can
take an image. The top, bottom and negative lists all chart; one ticker's ratios stay text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
…s (default 7), documented

- New preferences.conf key min_analysts (default 7, reloadable): .buy, .sell and the .forward pe lists (top, bottom, negative)
  take only stocks covered by at least that many analysts. A forward P/E is analysts' estimate, so with few of them it says as
  little as a perfect buy from two brokers. One ticker asked for (.fpe SYMBOL) is still shown.
- The forward P/E headings say "(7+ analysts)", as .buy's does, and an empty list says why.
- Documented in the README's preferences table (now 26 keys), the .buy/.sell and P/E sections, and the shipped
  defaults/preferences.conf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVEkZofdPjQ4jtXFKipeUX
@robdevops
robdevops merged commit 986f486 into main Oct 14, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants