Skip to content

v1.8.0 — Public Signals, Watch List & After-Hours Pricing

Choose a tag to compare

@furic furic released this 06 Aug 15:46
· 42 commits to main since this release

What's New

Three features headline v1.8.0: Richfolio can now post its signals publicly, track tickers you don't own yet, and price everything off the after-hours quote instead of yesterday's close.

Public Signal Posting — X, Facebook, Threads & LinkedIn

Richfolio can now broadcast its STRONG BUY / BUY calls to four social platforms on the daily and intraday runs. Weekly and refresh runs never post.

Posts are deliberately generic — they carry the signal, never the portfolio.

  • buildSignalLines() is the single privacy chokepoint: it projects each recommendation onto an allowlist (ticker, tickerFullName, action, confidence, valueRating, reason, analysisUrl) and never reads suggestedBuyValue, allocation gaps, or holdings.
  • Portfolio and watch-list picks are merged uniformly, so which tickers you actually own never leaks.
  • Reason text is scrubbed of allocation/gap/dollar references and internal guard framing ("downgraded by earnings guard"-style phrasing) before it goes out.
  • #TICKER hashtags are rendered inline and clickable on Facebook, Threads and LinkedIn, plus configurable topic hashtags.

Each platform gates on its own credentials — set none and nothing posts, set one and only that one posts. Every send runs in its own try/catch, so a dead token can never block the other platforms or the email/Telegram brief that already went out.

Platform Secrets
X / Twitter X_API_KEY, X_API_SECRET, X_ACCESS_TOKEN, X_ACCESS_TOKEN_SECRET
Facebook Page FACEBOOK_PAGE_ID, FACEBOOK_PAGE_TOKEN
Threads THREADS_USER_ID, THREADS_ACCESS_TOKEN (+ optional THREADS_TOKEN_PAT)
LinkedIn Page LINKEDIN_ACCESS_TOKEN, LINKEDIN_ORG_URN
"social": {
  "enabled": true,
  "includeLinkInX": false,
  "hashtags": ["investing", "stocks", "stockmarket", "ETFs"]
}

social.enabled is a master kill-switch. includeLinkInX defaults to false because a link raises X's pay-per-use cost. X has had no free tier since Feb 2026, so that integration is built but dormant until keys are added. X uses OAuth 1.0a signed with Node's built-in crypto — no new npm dependency for any of the four.

A monthly workflow (refresh-threads-token.yml) auto-refreshes the Threads token before its ~60-day expiry, if you give it a PAT with repo Secrets write.

Setup guide: Social Posting — translated into all 6 languages. Also adds a Privacy Policy page, required for the Meta and LinkedIn app reviews.

Watch List — Research Tickers Without Allocation Pollution

An optional watching: string[] in config.json tracks tickers you're interested in but haven't committed to a target allocation.

"watching": ["MSFT", "NVDA", "AMD"]

They flow through the full fetch pipeline (prices, fundamentals, technicals, news, AI) but stay out of report.items, so your allocation percentages are untouched — no phantom 0% targets dragging every gap around. They surface in their own "Watch List" section of the email and Telegram brief.

The AI routes them through dedicated WATCH LIST CRITERIA in the prompt, and the guard pipeline adapts: allocation-based guards (overweight→HOLD, the STRONG BUY gap ≥ 2% check, the max-2-STRONG-BUY cap) skip watch tickers, while confidence and signal-presence checks still bind. suggestedBuyValue is forced to 0 — there's no target to fill.

Documented across all 6 locales.

After-Hours Pricing — Everything Rescaled, Not Just the Headline

Earlier versions showed the regular close. v1.8.0 prefers the post-market → pre-market → regular price in daily, intraday and refresh runs (weekly deliberately keeps the close), and critically now rescales every price-derived field to match:

  • Trailing and forward P/E multiplied by latest / close — no more "P/E 18" printed next to a price that implies 21.
  • 52-week position recomputed against the fresh price.
  • MA distancepriceVsSma50, priceVsSma200 and the momentum trend label now measure against the spot price, via resolveTrendPrice().

That last one has a sanity guard: if the spot/close ratio falls outside ±50% it falls back to the chart's last daily close. This catches sub-unit currencies (LSE pence, quoted ÷100 in quote.price but not in raw chart closes) and erroneous thin prints.

Oscillators (RSI, MACD, Bollinger, Stochastic), ATR%, the 90-day percentile and the 1-day change intentionally stay on completed daily closes — they're defined on the close series and can't be derived from a single spot price.

All three prices share one FX conversion, and the whole thing is a no-op when no extended-hours quote exists (overnight, weekends). It matters most on large earnings gaps.

Fixes

Intraday whipsaw alerts silenced

Intraday technicals come from Yahoo daily candles that only update at the US close, so every US-closed intraday run saw byte-identical RSI/MACD. An action flip on frozen data was AI scoring noise — a provider's confidence drifting across the 80% line — and it produced real whipsaws in the logs (BUY→STRONG BUY at 4:32pm, STRONG BUY→BUY at 6:05pm on identical indicators).

compareWithBaseline now suppresses any alert whose ticker moved less than minPriceMovePctToAlert (default 1.0%) vs the morning baseline. Fails open when either price is missing or the threshold is 0, preserving legacy behaviour. Ships with the module's first unit tests, reproducing that whipsaw from the real logs.

Intraday crons respaced across the waking window

The four intraday checks were bunched into 10am–4pm AEST, all inside the US-closed dead zone. They're now ~3.75h apart — roughly 1:15pm / 5pm / 8:45pm / 12:30am AEST — so the last two land in US pre-market and the US open where prices actually move. (GitHub's scheduler routinely fires crons 30min–2h late, so treat these as approximate.)

Telegram no longer drops the entire brief

A large universe pushed the daily brief past Telegram's 4,096-character hard limit; the API returned 400 and no message was sent at all (email was unaffected, so it was easy to miss). Two guards now: each AI reason is truncated (300 chars daily, 400 in refresh) so a few long ones can't dominate, and clampToLimit() drops whole trailing lines — each has balanced HTML tags, so output stays valid — until the message fits, with a hard-cut fallback for a pathological single huge line. Applied to the daily, weekly and intraday builders.

Other fixes

  • Overweight tickers downgrade to HOLD. A ticker already above target (gap ≤ 0) could still be recommended as a buy. Guarded now, and downgrades are annotated so the displayed reason matches the final action.
  • Claude Stage 1 no longer truncates on large universes — max_tokens raised, and Claude now emits watch-list observations so that section survives without Gemini.
  • Intraday no longer shows a misleading confidence delta when the action itself changed.

Notes

  • No config changes are required to upgrade. watching and social are both optional; omit them and v1.8.0 behaves like v1.7.0.
  • Nothing posts publicly until you add platform credentials.
  • package-lock.json had drifted to 1.6.0; it's back in sync.

Full changelog: v1.7.0...v1.8.0