Repository navigation
Releases
v0.10.0 — Opt-in model panel, ICS conformance, live monitor
Four pull requests (#8–#11) and this version bump. Upgrading needs no
config, env or systemd change: everything new is either opt-in or additive.
Added
- Model panel (
[[llm.panel]], opt-in, #8). Several independent models
(any mix of providers, each with its own key variable) extract every changed
post once each; an event is published only whenpanel_quorumof them (a
majority by default) extracted the same window. A failed model abstains and
counts against every event, and with fewer answers than the quorum the sync
fails rather than reporting an empty announcement. A window too few models
saw is not published but is reported like a rejected extraction (anomaly,
notification,rejected_extractionson/healthz). Without a panel the
single-model extractor is unchanged (same prompt, same voting) /healthzextraction(#8): which models extract events and how many
must agree, or the single-model consensus.prop-firm-calendar evalalso
scores each panel member alone, from the same calls- Live monitor (#10):
scripts/check_live.pychecks an instance from
outside the way a subscriber's app would (healthz ok, a well-formed feed
with events, the API answering)..github/workflows/live-monitor.ymlruns
it against the public instance every 30 minutes and opens, updates and
closes alive-feed-downissue. The server leaves the monitor's
User-Agent out of its usage statistics schedule_notein source profiles (#11), shown per source on
/healthzand, while the firm has nothing upcoming, on the status page.
Blueberry Funded's explains why its calendar can be empty- Independent ICS conformance tests (#9): the feed is read back with
icalendar in five zones across every 2026 DST change, each time resolved
through the feed's ownVTIMEZONE
Fixed
- The hour repeated when clocks go back (#9). An instant in its second
pass was written as TZID-local time, which RFC 5545 reads as the first
pass: a London-zone window ending 01:30 GMT on 25 Oct ended an hour early.
Such instants are now written in UTC. The public instance (fixed GMT+3)
was not affected - Topstep titles name the holiday (#11), e.g. "⏳ Early Close —
Thanksgiving" instead of 13 identical "⏳ Early Close" entries. Applies to
rows extracted after Topstep next edits its article tests/test_autodeploy.pyruns on git < 2.28 (the production host has
2.25) (#9)
Changed
v0.9.0 — Multi-firm, JSON API, evidence quotes
First version string since 0.8.1. It covers the two sections below it, which
were merged and deployed in August without a version bump, plus the fixes
here. Upgrading needs no config, env or systemd change (see "Renamed").
Fixed
- Every event linked to FTMO. The feed passed
[source] url— FTMO's
index page — as the link for every event, so Topstep, Blueberry Funded and E8
Markets rows all sent subscribers to ftmo.com. Each event now links to its own
announcement (state records the post URL; older entries fall back to their
firm's page and heal on the next sync) - Events beyond
max_days_aheadwere lost for good. They were rejected,
and an unchanged post is never re-extracted, so Topstep's 2027-01-01 closure
never appeared even once it was in range. They are now held in state
(deferred) and published once within range, with no LLM call — also for
posts that have since left the index page. After upgrading, each tracked post
is re-extracted once (state written before 0.9 cannot say what it dropped) - Rejected extractions were invisible. Only a log line said an event had
been dropped (e.g. an E8 row with no stated offset). A rejection that drops a
real event now raises a per-firm anomaly (non-zero exit, notification, 503 on
/healthzfor that run), and each/healthzsource reports
rejected_extractions,events_upcomingandevents_deferred - Auto-deploy waits for CI.
scripts/autodeploy.shrebuilt production from
mainwhatever CI said; it now deploys a commit only after theCIworkflow
passed for it (scripts/ci_gate.py, stdlib only). The first deploy of this
version is still made by the old script; opt out with
AUTODEPLOY_REQUIRE_CI=0— see docs/DEPLOYMENT.md
Added
- Read-only JSON API in serve mode:
/api/v1/events?firm=&type=&from=&to=,
/api/v1/next(the current or next window per firm) and/api/v1/(index).
CORS-open,Cache-Control: public, max-age=300,ETag/304. Same state
and de-duplication as the feed - Evidence spans: each event carries the announcement's own words for it,
verified word-for-word against the scraped text (ICS/Google description, API).
An unfound quote publishes the event as (unconfirmed);
[events] require_evidence = truerejects it prop-firm-calendar eval: the production prompt over every golden fixture
N times against a real model, diffed against the expected events; also a
live_llmpytest marker and a manual/weekly workflow that needs the
LLM_API_KEYsecret- Secret scanning: a gitleaks CI job over each push/PR's new commits, an
optional pre-commit config, andSECURITY.md
Changed
- One calendar entry per window across posts (Google sync): a follow-up
post re-announcing a window shares the existing entry, which is deleted only
when the last post announcing it withdraws it. Duplicates created by earlier
versions are merged on the first run after upgrading
Renamed
- Package
ftmo_calendar→prop_firm_calendar, distribution
ftmo-calendar→prop-firm-calendar, commandprop-firm-calendar - Kept as aliases, so nothing deployed breaks: the
ftmo-calendarcommand
(same entry point), andimport ftmo_calendar[.x], which resolves to the
very same module objects and emits aDeprecationWarning - Deliberately not renamed: ICS
UIDs (…@ftmo-calendar— changing them
would duplicate every subscriber's events), the Googleaftc_keyreconcile
property, the compose serviceftmo-calendar, theftmo-autodeploysystemd
unit, theftmocontainer user, and theftmo-events.icsdefault path - PRODID is now
-//Bogzx//prop-firm-calendar//ENand descriptions credit
prop-firm-calendar; those two strings are the only bytes of the feed the
rename moved (pinned intests/test_ftmo_compatibility.py) - Change-notification heading reads "Trading calendar updated"
v0.8.0 — Granular events + stats
Granular events (grounded in FTMO history) + self-hosted stats.
Seven event types replace the coarse four: maintenance, crypto_closure, holiday_closure, early_close, late_open, symbol_event, other. Derived from sampling FTMO's announcement archive. Verified live on DeepSeek: the Memorial Day post extracts as 8 correctly-typed events:
maintenance Sat 17:00-21:00 Platform Maintenance - MT4, MT5, cTrader, DXtrade
holiday_closure Mon all day Closed All Day - UK100, HK50, Equities I, Agriculture
early_close Mon 20:00 Early Close - JP225, US30, US100, US500, US2000
early_close Mon 20:30 Early Close - UKOIL
early_close Mon 21:30 Early Close - Metals, USOIL, HEATOIL, NATGAS
early_close Mon 23:00 Early Close - GER40
late_open Tue ->16:35 Late Open - CORN, SOYBEAN, WHEAT
late_open Tue ->03:05 Late Open - UK100
- Event titles carry affected symbols via a new
affectedextraction field; consensus merges partial lists - Seven filter chips on the landing page; old state files stay compatible
- Anonymous usage stats in serve mode: page views, unique visitors (random-id first-party cookie, no third parties), feed pulls, unique feed clients. Footer summary + GET /stats JSON with 30-day history.
v0.7.0 — Per-interest feeds
Subscribe to only what you trade.
- Type-filtered feeds: /feed.ics?types=crypto_closure (any combination of maintenance, crypto_closure, holiday_hours, other). Each distinct URL behaves as its own calendar in Google/Apple/Outlook.
- Landing page filter chips: untick event types and the subscribe URL + webcal link rebuild live.
- Filtered calendars are named after their filter; unknown types return a 400 listing valid ones.
- State v3: tracked events carry their event type (older state files load transparently).
Verified live: filtered endpoint returns only matching events; UI chips rebuild the URL in a real browser.
v0.6.0 — Landing page
A real website for hosted deployments (e.g. calendar.yourdomain.com).
The served / page is now a designed, self-contained landing page - trading-terminal aesthetic, zero external requests:
- Live countdown to the next trading interruption (switches to "ends in" while one is in progress, "ALL CLEAR" when nothing is scheduled)
- Every time rendered in the visitor's own timezone
- One-click feed subscribe: copy-URL button + webcal:// open, with per-app instructions for Google/Apple/Outlook
- Upcoming / in-progress (LIVE) / past schedule, sync health in the footer
- Works without JavaScript, responsive down to phones, ~16 KB single response
v0.5.0 — Deterministic extraction
Always reports the same thing — verified live on DeepSeek via OpenRouter.
- Consensus voting (
[llm] consensus_runs, default 3): each changed post is extracted N times, majority events kept. Hosted APIs are not deterministic at temperature 0 (OpenRouter routes one model id across providers); voting makes the result stable. Live proof: 4 consecutive consensus extractions of a real FTMO post -> identical event sets, identical event keys. - Prompt rule excluding Client Area/IT maintenance — the one borderline case that flickered between runs
- Consensus identity merges timezone-attribution variants (explicit offset wins) so the vote never splits on equivalent events
seedhint on OpenAI-compatible calls
v0.4.0 — Feed-first hosting
Run a public feed for a whole trading group with just an LLM key.
- Feed-only mode (
[calendar] enabled = false): no Google account needed anywhere — one host runs a container, everyone else subscribes to/feed.ics --dry-runworks before any Google setup- Status page is now a shareable landing page: next event, sync health, per-app subscribe instructions
/healthzreportsnext_runfor overdue-sync detection; DockerHEALTHCHECKincluded- ICS feed: refresh hints (
REFRESH-INTERVAL) and per-event source links - DeepSeek/OpenRouter hardening:
<think>blocks and prose-wrapped JSON handled
Fixed: UTF-8 BOM configs (Notepad/PowerShell) parse correctly; feed serves last-good data immediately after restart; identical persistent errors notify once, not every interval.
v0.3.0 — Notifications, ICS feed, hosted mode, Docker
Reach & visibility.
- Discord webhook and Telegram bot notifications: new/removed events, run failures, optional heartbeat
- ICS feed export: subscribe from any calendar app with zero OAuth setup
ftmo-calendar serve: periodic sync + HTTP server exposing/feed.ics,/status,/healthz- Docker support:
docker compose up -druns serve mode with a./datavolume - State v2: tracked events carry display data; v1 state files load transparently
v0.2.0 — Trustworthy sync core
Complete rewrite as a professional package.
ftmo-calendarCLI:run(with--dry-run),auth(with--check),status- Provider-agnostic LLM parsing: Gemini or any OpenAI-compatible endpoint
- Service-account auth mode: no browser, no token expiry
- Reconcile sync: rescheduled/withdrawn announcements update or remove their calendar events
- Multi-post scraping matching FTMO's redesigned site
- Content-hash caching: unchanged posts cost zero LLM calls
- Event reminders, validation, timezone normalization
- Tests, ruff, mypy, GitHub Actions CI, TOML config
Fixed: expired OAuth tokens no longer hang cron with a browser prompt; documented the 7-day Testing-status token expiry.
