From 7bc37c9840ac2313296433539ba972289b7c02f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 12:21:43 +0000 Subject: [PATCH 1/8] Let darkmode take the hours the charts are dark darkmode accepts 18:00-6:00, 18-6 or 6pm-6am besides off, on and auto. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01RsqJny87LfkxT9UwQbuAsH --- defaults/settings.conf | 3 ++- doc/configuration.md | 2 +- lib/config.py | 38 +++++++++++++++++++++++++++++++++++--- lib/render.py | 13 +++++++------ tests/test_charts.py | 18 ++++++++++++++++++ 5 files changed, 63 insertions(+), 11 deletions(-) diff --git a/defaults/settings.conf b/defaults/settings.conf index 3bece61..66ffae7 100644 --- a/defaults/settings.conf +++ b/defaults/settings.conf @@ -44,7 +44,8 @@ # Display #graph=True -# darkmode: the charts' theme. off: light; on: dark; auto: dark from 18:00 to 06:00 in the timezone setting +# darkmode: the charts' theme. off: light; on: dark; auto: dark from 18:00 to 06:00 in the timezone setting; +# or the hours they are dark, in that zone: 20:00-07:00, 20-7, 8pm-7am #darkmode=off #hyperlink=True #hyperlinkFooter=True diff --git a/doc/configuration.md b/doc/configuration.md index b8bcdf5..42598e5 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -67,7 +67,7 @@ The administrators of the chat a person is in are admins there with no setting: | `include_portfolios` | | Sharesight portfolio IDs to report on, space separated (empty: all that are not excluded) | | `trades_use_yahoo` | `True` | Use Yahoo for trades; set `False` if Yahoo breaks | | `graph` | `True` | Send charts with reports | -| `darkmode` | `off` | The charts' theme. `off`: light; `on`: dark; `auto`: dark from 18:00 to 06:00 in the `timezone` setting, light by day. The shorts chart keeps its own colours | +| `darkmode` | `off` | The charts' theme. `off`: light; `on`: dark; `auto`: dark from 18:00 to 06:00 in the `timezone` setting, light by day; or the hours the charts are dark in the `timezone` setting, light otherwise: `18:00-6:00`, `18-6` or `6pm-6am`. The shorts chart keeps its own colours | | `hyperlink` | `True` | Hyperlink tickers in reports | | `hyperlinkFooter` | `True` | Add a website link footer to stock lookups | | `hyperlinkProvider` | `google` | Where ticker links go: `google` or `yahoo` (any other value stops a start or a reload, naming the key) | diff --git a/lib/config.py b/lib/config.py index aa06130..5c28c3f 100644 --- a/lib/config.py +++ b/lib/config.py @@ -36,7 +36,8 @@ } RENAMED = {} # startup keys under a new name: the old still works (the new wins where both are set), with a note SERVICE_DATA_DIR = '/var/lib/private/finbot' # systemd's state directory of finbot.service (StateDirectory=) -DARKMODES = ('off', 'on', 'auto') # darkmode's values: light charts, dark ones, or dark at night (lib/render.py) +DARKMODES = ('off', 'on', 'auto') # darkmode's words: light charts, dark ones, or dark at night (lib/render.py) +NIGHT_HOURS = '18:00-06:00' # what darkmode=auto means: dark from 18:00 to 06:00 LOG_LEVELS = ('ERROR', 'WARN', 'INFO', 'DEBUG') # log_level's values, the most severe first (lib/log.py) @@ -150,6 +151,26 @@ def read_startup_file(path): FALSE_WORDS = ('false', '0', 'f', 'no', 'n', 'off') +_HOURS = re.compile(r'(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*-\s*(\d{1,2})(?::(\d{2}))?\s*(am|pm)?', re.IGNORECASE) + +def night_hours(text): + """The minutes of the day (start, end) of "18:00-6:00", "18-6" or "6pm-6am", or None where it is no such hours + (or starts when it ends). The end may come before the start: the hours run past midnight.""" + if not (found := _HOURS.fullmatch(text.strip())): + return None + times = [] + for hours, mins, half in (found.group(1, 2, 3), found.group(4, 5, 6)): + hour, minute = int(hours), int(mins or 0) + if half: + if not 1 <= hour <= 12: + return None + hour = hour % 12 + (12 if half.lower() == 'pm' else 0) + if hour > 23 or minute > 59: + return None + times.append(hour * 60 + minute) + return tuple(times) if times[0] != times[1] else None + + class _Reader: """Typed reads from a getenv(name, default); a bad value is remembered, the default used, and all problems raised together. A blank value means unset: the default.""" @@ -206,6 +227,17 @@ def choice(self, name, default, choices): self.errors.append(f"{name}={self.getenv(name, default)!r} is not one of {', '.join(choices)}") return default + def darkmode(self, name, default): + """off, on, auto or the hours the charts are dark, "18:00-6:00", "18-6" or "6pm-6am": as off, on or HH:MM-HH:MM + (auto as 18:00-06:00 would be, kept as auto); anything else is an error, giving default.""" + value = str(self.value(name, default)).strip() + if value.lower() in DARKMODES: + return value.lower() + if hours := night_hours(value): + return '%02d:%02d-%02d:%02d' % (*divmod(hours[0], 60), *divmod(hours[1], 60)) + self.errors.append(f"{name}={value!r} is not one of {', '.join(DARKMODES)} or hours such as 6pm-6am") + return default + def zone(self, name, default): """A time zone's name as zoneinfo knows it (Australia/Sydney, UTC); anything else is an error, giving default: a zone read wrong would schedule every new job in UTC.""" @@ -305,7 +337,7 @@ class Settings: cache: bool = True cache_seconds: int = 21600 country_code: str = 'AU' - darkmode: str = 'off' # charts' theme: one of DARKMODES + darkmode: str = 'off' # charts' theme: one of DARKMODES, or the hours they are dark (HH:MM-HH:MM) demote_leveraged: bool = True demote_volatile: bool = True exchange_suffix: str = 'AX' # Yahoo's suffix for the local exchange: a bare ticker Yahoo does not know gets it @@ -342,7 +374,7 @@ def setting_values(values): 'admin_only': r.blankable_words, 'exchange_suffix': lambda name, default: r.blankable(name, default).removeprefix('.').upper(), # blank: none 'country_code': lambda name, default: r.str(name, default).strip().upper(), - 'darkmode': lambda name, default: r.choice(name, default, DARKMODES), + 'darkmode': r.darkmode, 'hyperlinkProvider': lambda name, default: r.choice(name, default, ('google', 'yahoo')), 'log_level': lambda name, default: r.choice(name, default, LOG_LEVELS), 'timezone': r.zone} diff --git a/lib/render.py b/lib/render.py index 8bd0a6c..53a8078 100644 --- a/lib/render.py +++ b/lib/render.py @@ -51,7 +51,6 @@ def drawable(self): PALETTE = ('#14181f', '#eef1f5', '#9aa4b2', '#2a313c') # the dark charts: surface, ink, muted ink, grid lines LIGHT_PALETTE = ('#ffffff', '#1a1f27', '#5f6b7a', '#e1e5ea') # the light charts' (darkmode off) LIGHT_UP, LIGHT_DOWN = '#0f9d6b', '#e11d48' # a rise and a fall on a light surface: darker, to read on white -NIGHT = (18, 6) # darkmode auto: dark from 18:00 to 06:00 in the timezone setting CATEGORIES = ('#3987e5', '#d95926', '#199e70', '#c98500', '#d55181', '#008300', '#9085e9', '#e66767') # the dark # categorical slots, in fixed order; the green and red ones mean up and down, so lines that are neither leave them out NEITHER_UP_NOR_DOWN = tuple(color for color in CATEGORIES if color not in ('#008300', '#e66767')) @@ -61,14 +60,16 @@ def drawable(self): _dark_now = contextvars.ContextVar('dark_now', default=None) # draw() settles the theme once, for its whole drawing def is_dark(): - """Whether charts are drawn dark now: the darkmode setting (on; auto: at night in the timezone setting).""" + """Whether charts are drawn dark now: the darkmode setting (on; auto or hours: within them in the timezone).""" if (settled := _dark_now.get()) is not None: return settled mode = config.settings.darkmode - if mode == 'auto': - hour = datetime.datetime.now(zoneinfo.ZoneInfo(config.settings.timezone)).hour - return hour >= NIGHT[0] or hour < NIGHT[1] - return mode == 'on' + if mode in ('on', 'off'): + return mode == 'on' + start, end = config.night_hours(config.NIGHT_HOURS if mode == 'auto' else mode) + now = datetime.datetime.now(zoneinfo.ZoneInfo(config.settings.timezone)) + minute = now.hour * 60 + now.minute + return start <= minute < end if start < end else minute >= start or minute < end def palette(): """The surface, ink, muted ink and grid colours of the theme in force.""" diff --git a/tests/test_charts.py b/tests/test_charts.py index 7ac36a1..532cdc6 100644 --- a/tests/test_charts.py +++ b/tests/test_charts.py @@ -544,6 +544,24 @@ def test_auto_follows_the_night(self): self.assertEqual(self.corner('auto', 5), BG) self.assertEqual(self.corner('auto', 6), (255, 255, 255)) + def test_hours_are_the_night(self): + """Hours in any spelling are dark within them, past midnight or not, and light outside.""" + for mode in ('20:00-7:00', '20-7', '8pm-7am'): + self.assertEqual(self.corner(mode, 19), (255, 255, 255), mode) + self.assertEqual(self.corner(mode, 20), BG, mode) + self.assertEqual(self.corner(mode, 6), BG, mode) + self.assertEqual(self.corner(mode, 7), (255, 255, 255), mode) + self.assertEqual(self.corner('9-17', 12), BG) + self.assertEqual(self.corner('9-17', 17), (255, 255, 255)) + + def test_hours_are_read(self): + """The setting reads hours, and refuses what is not.""" + for text in ('18:00-6:00', '18-6', '6pm-6am', ' 6PM - 6:00 am '): + self.assertEqual(config.setting_values({'darkmode': text}).darkmode, '18:00-06:00', text) + for text in ('18-18', '25-6', '0pm-6am', 'night', '18'): + with self.assertRaises(config.ConfigError, msg=text): + config.setting_values({'darkmode': text}) + def test_the_cache_keeps_a_chart_per_theme(self): """The same spec drawn light and dark are two images, not one read back.""" chart = line(price_rows()) From 3d912717c0831e2ab9cf5f3d4887b84903096416 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 23:21:23 +0000 Subject: [PATCH 2/8] Remove darkmode=auto Hours (18-6) say the same and more. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01RsqJny87LfkxT9UwQbuAsH --- defaults/settings.conf | 4 ++-- doc/configuration.md | 2 +- lib/config.py | 7 +++---- lib/render.py | 4 ++-- tests/test_charts.py | 9 +-------- 5 files changed, 9 insertions(+), 17 deletions(-) diff --git a/defaults/settings.conf b/defaults/settings.conf index 66ffae7..f99894e 100644 --- a/defaults/settings.conf +++ b/defaults/settings.conf @@ -44,8 +44,8 @@ # Display #graph=True -# darkmode: the charts' theme. off: light; on: dark; auto: dark from 18:00 to 06:00 in the timezone setting; -# or the hours they are dark, in that zone: 20:00-07:00, 20-7, 8pm-7am +# darkmode: the charts' theme. off: light; on: dark; or the hours they are dark, in the timezone setting: +# 18:00-6:00, 18-6, 6pm-6am #darkmode=off #hyperlink=True #hyperlinkFooter=True diff --git a/doc/configuration.md b/doc/configuration.md index 42598e5..01801d1 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -67,7 +67,7 @@ The administrators of the chat a person is in are admins there with no setting: | `include_portfolios` | | Sharesight portfolio IDs to report on, space separated (empty: all that are not excluded) | | `trades_use_yahoo` | `True` | Use Yahoo for trades; set `False` if Yahoo breaks | | `graph` | `True` | Send charts with reports | -| `darkmode` | `off` | The charts' theme. `off`: light; `on`: dark; `auto`: dark from 18:00 to 06:00 in the `timezone` setting, light by day; or the hours the charts are dark in the `timezone` setting, light otherwise: `18:00-6:00`, `18-6` or `6pm-6am`. The shorts chart keeps its own colours | +| `darkmode` | `off` | The charts' theme. `off`: light; `on`: dark; or the hours the charts are dark in the `timezone` setting, light otherwise: `18:00-6:00`, `18-6` or `6pm-6am`. The shorts chart keeps its own colours | | `hyperlink` | `True` | Hyperlink tickers in reports | | `hyperlinkFooter` | `True` | Add a website link footer to stock lookups | | `hyperlinkProvider` | `google` | Where ticker links go: `google` or `yahoo` (any other value stops a start or a reload, naming the key) | diff --git a/lib/config.py b/lib/config.py index 5c28c3f..a57160f 100644 --- a/lib/config.py +++ b/lib/config.py @@ -36,8 +36,7 @@ } RENAMED = {} # startup keys under a new name: the old still works (the new wins where both are set), with a note SERVICE_DATA_DIR = '/var/lib/private/finbot' # systemd's state directory of finbot.service (StateDirectory=) -DARKMODES = ('off', 'on', 'auto') # darkmode's words: light charts, dark ones, or dark at night (lib/render.py) -NIGHT_HOURS = '18:00-06:00' # what darkmode=auto means: dark from 18:00 to 06:00 +DARKMODES = ('off', 'on') # darkmode's words: light charts or dark ones; else the hours they are dark (lib/render.py) LOG_LEVELS = ('ERROR', 'WARN', 'INFO', 'DEBUG') # log_level's values, the most severe first (lib/log.py) @@ -228,8 +227,8 @@ def choice(self, name, default, choices): return default def darkmode(self, name, default): - """off, on, auto or the hours the charts are dark, "18:00-6:00", "18-6" or "6pm-6am": as off, on or HH:MM-HH:MM - (auto as 18:00-06:00 would be, kept as auto); anything else is an error, giving default.""" + """off, on or the hours the charts are dark, "18:00-6:00", "18-6" or "6pm-6am": as off, on or HH:MM-HH:MM; + anything else is an error, giving default.""" value = str(self.value(name, default)).strip() if value.lower() in DARKMODES: return value.lower() diff --git a/lib/render.py b/lib/render.py index 53a8078..14f8fd9 100644 --- a/lib/render.py +++ b/lib/render.py @@ -60,13 +60,13 @@ def drawable(self): _dark_now = contextvars.ContextVar('dark_now', default=None) # draw() settles the theme once, for its whole drawing def is_dark(): - """Whether charts are drawn dark now: the darkmode setting (on; auto or hours: within them in the timezone).""" + """Whether charts are drawn dark now: the darkmode setting (on; or within its hours in the timezone).""" if (settled := _dark_now.get()) is not None: return settled mode = config.settings.darkmode if mode in ('on', 'off'): return mode == 'on' - start, end = config.night_hours(config.NIGHT_HOURS if mode == 'auto' else mode) + start, end = config.night_hours(mode) now = datetime.datetime.now(zoneinfo.ZoneInfo(config.settings.timezone)) minute = now.hour * 60 + now.minute return start <= minute < end if start < end else minute >= start or minute < end diff --git a/tests/test_charts.py b/tests/test_charts.py index 532cdc6..723cabf 100644 --- a/tests/test_charts.py +++ b/tests/test_charts.py @@ -537,13 +537,6 @@ def test_off_is_light_and_on_is_dark(self): self.assertEqual(self.corner('off'), (255, 255, 255)) self.assertEqual(self.corner('on'), BG) - def test_auto_follows_the_night(self): - """Auto is dark from 18:00 to 06:00 and light between.""" - self.assertEqual(self.corner('auto', 12), (255, 255, 255)) - self.assertEqual(self.corner('auto', 23), BG) - self.assertEqual(self.corner('auto', 5), BG) - self.assertEqual(self.corner('auto', 6), (255, 255, 255)) - def test_hours_are_the_night(self): """Hours in any spelling are dark within them, past midnight or not, and light outside.""" for mode in ('20:00-7:00', '20-7', '8pm-7am'): @@ -558,7 +551,7 @@ def test_hours_are_read(self): """The setting reads hours, and refuses what is not.""" for text in ('18:00-6:00', '18-6', '6pm-6am', ' 6PM - 6:00 am '): self.assertEqual(config.setting_values({'darkmode': text}).darkmode, '18:00-06:00', text) - for text in ('18-18', '25-6', '0pm-6am', 'night', '18'): + for text in ('18-18', '25-6', '0pm-6am', 'night', '18', 'auto'): with self.assertRaises(config.ConfigError, msg=text): config.setting_values({'darkmode': text}) From e97c6c174cf2bd2a649af76e726248571ba59686 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 23:23:15 +0000 Subject: [PATCH 3/8] Lay out the darkmode values in settings.conf as rows Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01RsqJny87LfkxT9UwQbuAsH --- defaults/settings.conf | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/defaults/settings.conf b/defaults/settings.conf index f99894e..baffaef 100644 --- a/defaults/settings.conf +++ b/defaults/settings.conf @@ -44,8 +44,10 @@ # Display #graph=True -# darkmode: the charts' theme. off: light; on: dark; or the hours they are dark, in the timezone setting: -# 18:00-6:00, 18-6, 6pm-6am +# darkmode: the charts' theme +# off light +# on dark +# 18:00-6:00 dark within those hours of the timezone setting, light otherwise (18-6 and 6pm-6am are the same) #darkmode=off #hyperlink=True #hyperlinkFooter=True From dbe8d82bd68cb93497b3bdb59510e5232e2e9da0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 23:24:16 +0000 Subject: [PATCH 4/8] Revert "Lay out the darkmode values in settings.conf as rows" This reverts commit e97c6c174cf2bd2a649af76e726248571ba59686. --- defaults/settings.conf | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/defaults/settings.conf b/defaults/settings.conf index baffaef..f99894e 100644 --- a/defaults/settings.conf +++ b/defaults/settings.conf @@ -44,10 +44,8 @@ # Display #graph=True -# darkmode: the charts' theme -# off light -# on dark -# 18:00-6:00 dark within those hours of the timezone setting, light otherwise (18-6 and 6pm-6am are the same) +# darkmode: the charts' theme. off: light; on: dark; or the hours they are dark, in the timezone setting: +# 18:00-6:00, 18-6, 6pm-6am #darkmode=off #hyperlink=True #hyperlinkFooter=True From 5869d27130a11d696bf3451eff5632320e109684 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 23:24:23 +0000 Subject: [PATCH 5/8] Give each darkmode value its row in the settings table Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01RsqJny87LfkxT9UwQbuAsH --- doc/configuration.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/configuration.md b/doc/configuration.md index 01801d1..6545b8a 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -67,7 +67,9 @@ The administrators of the chat a person is in are admins there with no setting: | `include_portfolios` | | Sharesight portfolio IDs to report on, space separated (empty: all that are not excluded) | | `trades_use_yahoo` | `True` | Use Yahoo for trades; set `False` if Yahoo breaks | | `graph` | `True` | Send charts with reports | -| `darkmode` | `off` | The charts' theme. `off`: light; `on`: dark; or the hours the charts are dark in the `timezone` setting, light otherwise: `18:00-6:00`, `18-6` or `6pm-6am`. The shorts chart keeps its own colours | +| `darkmode` | `off` | The charts' theme: `off` draws them light. The shorts chart keeps its own colours | +| `darkmode=on` | | Draws the charts dark | +| `darkmode=18:00-6:00` | | Draws the charts dark within those hours of the `timezone` setting, light otherwise (`18-6` and `6pm-6am` are the same) | | `hyperlink` | `True` | Hyperlink tickers in reports | | `hyperlinkFooter` | `True` | Add a website link footer to stock lookups | | `hyperlinkProvider` | `google` | Where ticker links go: `google` or `yahoo` (any other value stops a start or a reload, naming the key) | From 6c077286952f3ccb0ed136bcea98d092c3fbdc9c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 23:25:04 +0000 Subject: [PATCH 6/8] Revert "Give each darkmode value its row in the settings table" This reverts commit 5869d27130a11d696bf3451eff5632320e109684. --- doc/configuration.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/doc/configuration.md b/doc/configuration.md index 6545b8a..01801d1 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -67,9 +67,7 @@ The administrators of the chat a person is in are admins there with no setting: | `include_portfolios` | | Sharesight portfolio IDs to report on, space separated (empty: all that are not excluded) | | `trades_use_yahoo` | `True` | Use Yahoo for trades; set `False` if Yahoo breaks | | `graph` | `True` | Send charts with reports | -| `darkmode` | `off` | The charts' theme: `off` draws them light. The shorts chart keeps its own colours | -| `darkmode=on` | | Draws the charts dark | -| `darkmode=18:00-6:00` | | Draws the charts dark within those hours of the `timezone` setting, light otherwise (`18-6` and `6pm-6am` are the same) | +| `darkmode` | `off` | The charts' theme. `off`: light; `on`: dark; or the hours the charts are dark in the `timezone` setting, light otherwise: `18:00-6:00`, `18-6` or `6pm-6am`. The shorts chart keeps its own colours | | `hyperlink` | `True` | Hyperlink tickers in reports | | `hyperlinkFooter` | `True` | Add a website link footer to stock lookups | | `hyperlinkProvider` | `google` | Where ticker links go: `google` or `yahoo` (any other value stops a start or a reload, naming the key) | From c3d9b920bf922d6833dd081d27460e1bf09b9552 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 23:26:44 +0000 Subject: [PATCH 7/8] Keep the settings table in one piece The paragraph on chat administrators sat between the admins row and the rest, so past_days to print_headers rendered outside the table. It now follows the table. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01RsqJny87LfkxT9UwQbuAsH --- doc/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/configuration.md b/doc/configuration.md index 01801d1..3c1de88 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -53,8 +53,6 @@ A change takes effect on a reload. A true/false setting takes `true`/`false`, `y |---|---|---| | `admin_only` | `jobs` | The commands only the admins may use, by name, space separated (`jobs watchlist`). Closed by default: `/jobs` is one, and only the admins may use it (see `admins`: a chat's own administrators are admins there). Blank (`admin_only=`) restricts no command. Someone else gets a refusal, and a press of one of those commands' buttons is refused with a message to them alone (an alert on Telegram, a message only they see on Slack and Discord) | | `admins` | | Who the admins are besides each chat's own administrators (below), space separated, each the network and the sender's id as the log shows them: `telegram:@username` (or the numeric user id; never a first name, which anyone may take), `slack:U0123ABCD` (`<@U0123ABCD>` in the log), `discord:123456789012345678` | - -The administrators of the chat a person is in are admins there with no setting: the creator and administrators of a Telegram group (the bot asks `getChatMember`), the owner and anyone with an Administrator role in a Discord server, and the owners and admins of a Slack workspace (`users.info`; needs the `users:read` scope, which the manifest has). In a direct message with the bot, a person is an admin if they are one in any Telegram group finbot has seen, or Discord server the bot is in (a Slack workspace's admins are admins everywhere). finbot asks again every five minutes, so a change shows soon. A question whose connection fails is asked once more at once; where it still fails (a network down, or a scope missing), the network's last answer stands, else the `admins` setting alone decides, and the next message asks again. | `past_days` | `28` | How many days back reports look | | `future_days` | `30` | How many days ahead the events calendar looks | | `price_percent` | `5` | Movers threshold, percent: the moves `/movers`, `/session` and their jobs list | @@ -85,6 +83,8 @@ The administrators of the chat a person is in are admins there with no setting: | `log_width` | `71` | The most characters of a log line that can run long: a longer one is cut there, ending in `…`. That is each request's line (its method and URL) and each chat message's (who sent what, in which network). A message of several lines is logged on one line, its line breaks spaces. `0`: no line is cut | | `print_headers` | `False` | Log inbound request headers | +The administrators of the chat a person is in are admins there with no setting: the creator and administrators of a Telegram group (the bot asks `getChatMember`), the owner and anyone with an Administrator role in a Discord server, and the owners and admins of a Slack workspace (`users.info`; needs the `users:read` scope, which the manifest has). In a direct message with the bot, a person is an admin if they are one in any Telegram group finbot has seen, or Discord server the bot is in (a Slack workspace's admins are admins everywhere). finbot asks again every five minutes, so a change shows soon. A question whose connection fails is asked once more at once; where it still fails (a network down, or a scope missing), the network's last answer stands, else the `admins` setting alone decides, and the next message asks again. + A key in the wrong file, or one that is not listed, stops finbot starting; on a reload it is reported and nothing is applied. A key finbot no longer reads (`alliterate`, `chunk_maxlines`) is ignored, and the log at start says to delete it. edit `settings.conf`, then reload if running. The service log shows what changed (`config: future_days 30 -> 45`). If the file has a mistake, the reload reports it (`ERROR config: ...`), applies nothing and leaves finbot running on its previous settings. Check an edit before reloading with `sudo util/check_config.py /etc/finbot.env` (under systemd it reads the service's `settings.conf`). Edit `finbot.env`, then restart: a running bot cannot see it change. From 3a2a1f96266ac23e07a0b3874daa4df85d1e00ca Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 23:28:18 +0000 Subject: [PATCH 8/8] Sort the settings table by key Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01RsqJny87LfkxT9UwQbuAsH --- doc/configuration.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/doc/configuration.md b/doc/configuration.md index 3c1de88..1419c84 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -53,35 +53,35 @@ A change takes effect on a reload. A true/false setting takes `true`/`false`, `y |---|---|---| | `admin_only` | `jobs` | The commands only the admins may use, by name, space separated (`jobs watchlist`). Closed by default: `/jobs` is one, and only the admins may use it (see `admins`: a chat's own administrators are admins there). Blank (`admin_only=`) restricts no command. Someone else gets a refusal, and a press of one of those commands' buttons is refused with a message to them alone (an alert on Telegram, a message only they see on Slack and Discord) | | `admins` | | Who the admins are besides each chat's own administrators (below), space separated, each the network and the sender's id as the log shows them: `telegram:@username` (or the numeric user id; never a first name, which anyone may take), `slack:U0123ABCD` (`<@U0123ABCD>` in the log), `discord:123456789012345678` | -| `past_days` | `28` | How many days back reports look | -| `future_days` | `30` | How many days ahead the events calendar looks | -| `price_percent` | `5` | Movers threshold, percent: the moves `/movers`, `/session` and their jobs list | -| `shorts_percent` | `15` | Short interest alert threshold, percent | -| `min_analysts` | `7` | `/buy`, `/sell`, and `/ratio` (the forward P/E and the PEG) list only stocks covered by at least this many analysts; with fewer, a consensus or an estimate says little (thinly covered small caps are nearly all a perfect buy). Their charts say so in the subtitle | -| `demote_volatile` | `True` | Volatile (small cap, high beta) stocks need a bigger move to alert | +| `cache` | `True` | Cache Yahoo and Sharesight responses | +| `cache_seconds` | `21600` | Cache lifetime in seconds (6 hours) | +| `country_code` | `AU` | Regional: your country, in any case, for the default jobs' messages that are for one (`AU`: the tax year's reminders) | +| `darkmode` | `off` | The charts' theme. `off`: light; `on`: dark; or the hours the charts are dark in the `timezone` setting, light otherwise: `18:00-6:00`, `18-6` or `6pm-6am`. The shorts chart keeps its own colours | | `demote_leveraged` | `True` | Leveraged (2x, 3x) funds need a bigger move to alert | -| `volatility_multiplier` | `2` | How much bigger a move volatile stocks need | +| `demote_volatile` | `True` | Volatile (small cap, high beta) stocks need a bigger move to alert | +| `exchange_suffix` | `AX` | Regional: Yahoo's suffix for your exchange (`AX` for the ASX, `NZ`, `L`, `TO` and so on). A bare symbol Yahoo does not know is tried again with it, in every command that takes a symbol (`BHP` becomes `BHP.AX`); empty, a symbol is taken as written | | `exclude_portfolios` | | Sharesight portfolio IDs to leave out, space separated | -| `include_portfolios` | | Sharesight portfolio IDs to report on, space separated (empty: all that are not excluded) | -| `trades_use_yahoo` | `True` | Use Yahoo for trades; set `False` if Yahoo breaks | +| `future_days` | `30` | How many days ahead the events calendar looks | | `graph` | `True` | Send charts with reports | -| `darkmode` | `off` | The charts' theme. `off`: light; `on`: dark; or the hours the charts are dark in the `timezone` setting, light otherwise: `18:00-6:00`, `18-6` or `6pm-6am`. The shorts chart keeps its own colours | +| `http_timeout` | `18` | Seconds to wait on outbound HTTP requests | | `hyperlink` | `True` | Hyperlink tickers in reports | | `hyperlinkFooter` | `True` | Add a website link footer to stock lookups | | `hyperlinkProvider` | `google` | Where ticker links go: `google` or `yahoo` (any other value stops a start or a reload, naming the key) | -| `exchange_suffix` | `AX` | Regional: Yahoo's suffix for your exchange (`AX` for the ASX, `NZ`, `L`, `TO` and so on). A bare symbol Yahoo does not know is tried again with it, in every command that takes a symbol (`BHP` becomes `BHP.AX`); empty, a symbol is taken as written | -| `country_code` | `AU` | Regional: your country, in any case, for the default jobs' messages that are for one (`AU`: the tax year's reminders) | -| `timezone` | `Australia/Melbourne` | Regional: your time zone, the clock of the times `/jobs` takes and shows, a job's zone where it names none, and what "today" is (ytd, reports' dates); a name that is no zone stops a start or a reload, naming the key | -| `telegram_preload_chat` | | A private Telegram channel or group of your own, by its id (`-1001234567890`, as the log shows a chat's): as a chart with period or portfolio buttons goes out, its other periods and portfolios are uploaded there, so that a press shows them without an upload (see [Telegram](telegram.md#preloading-charts)). Empty: off | -| `slack_preload_chat` | | The same on Slack: a private channel's id (`C0123ABCD`), the bot invited (see [Slack](slack.md#preloading-charts)). Empty: off | -| `preload_gap` | `0.1` | Seconds between the starts of two charts preloaded to a network (two uploads at a time, on Telegram and Slack alike, each chart sent as soon as it is drawn). A rate limit is waited out once and the upload tried again, as every request to a network is | -| `cache` | `True` | Cache Yahoo and Sharesight responses | -| `cache_seconds` | `21600` | Cache lifetime in seconds (6 hours) | -| `performance_cache_seconds` | `2400` | Cache lifetime of Sharesight performance reports in seconds: longer than the 30 minutes between `sharesight_warm` runs, so the holdings never run out | -| `http_timeout` | `18` | Seconds to wait on outbound HTTP requests | +| `include_portfolios` | | Sharesight portfolio IDs to report on, space separated (empty: all that are not excluded) | | `log_level` | `INFO` | What the log shows: that level and those above it, syslog style. `ERROR`: what failed and was lost (a message not sent); `WARN`: what failed and was got over (a retry, a fallback, a request refused); `INFO`: what finbot does (each message sent, each job run, each request sent, each request refused); `DEBUG`: the routine of a button press too (the press, its acknowledgement, the message edited in place), each request answered with success, and each cache miss, hit and write. Replaces `debug`. Under systemd each line carries its syslog priority (error, warning, info, debug), so `journalctl` colours it and `journalctl -p warning` filters by it | | `log_width` | `71` | The most characters of a log line that can run long: a longer one is cut there, ending in `…`. That is each request's line (its method and URL) and each chat message's (who sent what, in which network). A message of several lines is logged on one line, its line breaks spaces. `0`: no line is cut | +| `min_analysts` | `7` | `/buy`, `/sell`, and `/ratio` (the forward P/E and the PEG) list only stocks covered by at least this many analysts; with fewer, a consensus or an estimate says little (thinly covered small caps are nearly all a perfect buy). Their charts say so in the subtitle | +| `past_days` | `28` | How many days back reports look | +| `performance_cache_seconds` | `2400` | Cache lifetime of Sharesight performance reports in seconds: longer than the 30 minutes between `sharesight_warm` runs, so the holdings never run out | +| `preload_gap` | `0.1` | Seconds between the starts of two charts preloaded to a network (two uploads at a time, on Telegram and Slack alike, each chart sent as soon as it is drawn). A rate limit is waited out once and the upload tried again, as every request to a network is | +| `price_percent` | `5` | Movers threshold, percent: the moves `/movers`, `/session` and their jobs list | | `print_headers` | `False` | Log inbound request headers | +| `shorts_percent` | `15` | Short interest alert threshold, percent | +| `slack_preload_chat` | | The same on Slack: a private channel's id (`C0123ABCD`), the bot invited (see [Slack](slack.md#preloading-charts)). Empty: off | +| `telegram_preload_chat` | | A private Telegram channel or group of your own, by its id (`-1001234567890`, as the log shows a chat's): as a chart with period or portfolio buttons goes out, its other periods and portfolios are uploaded there, so that a press shows them without an upload (see [Telegram](telegram.md#preloading-charts)). Empty: off | +| `timezone` | `Australia/Melbourne` | Regional: your time zone, the clock of the times `/jobs` takes and shows, a job's zone where it names none, and what "today" is (ytd, reports' dates); a name that is no zone stops a start or a reload, naming the key | +| `trades_use_yahoo` | `True` | Use Yahoo for trades; set `False` if Yahoo breaks | +| `volatility_multiplier` | `2` | How much bigger a move volatile stocks need | The administrators of the chat a person is in are admins there with no setting: the creator and administrators of a Telegram group (the bot asks `getChatMember`), the owner and anyone with an Administrator role in a Discord server, and the owners and admins of a Slack workspace (`users.info`; needs the `users:read` scope, which the manifest has). In a direct message with the bot, a person is an admin if they are one in any Telegram group finbot has seen, or Discord server the bot is in (a Slack workspace's admins are admins everywhere). finbot asks again every five minutes, so a change shows soon. A question whose connection fails is asked once more at once; where it still fails (a network down, or a scope missing), the network's last answer stands, else the `admins` setting alone decides, and the next message asks again.