📈 Stock screening and portfolio management toolkit with:
- Trend-state screening (
Uptrend,Downtrend,Undecided) from a 4-line SMMA engine - Daily/weekly RSI context
- Watchlist management with per-user persistence
- Portfolio tracking with cash, positions, and daily valuation snapshots
- Chart generation (10Y + 1Y) with volume-profile S/R overlays
- Short-TTL market-data cache to reduce duplicate fetches in report/snapshot flows
Telegram is the primary interface in this repository, but the core logic is the screening/portfolio toolkit behind it.
- Fetches market data from Yahoo Finance (
yfinance) - Computes:
- Trend Indicator state (
Uptrend,Downtrend,Undecided) - Spread between fast and slow outer SMMAs
- Daily and weekly RSI
- Performance windows (
1D,WoW,1M,6M,1Y) in detail/report views
- Trend Indicator state (
- Tracks state changes ("flips") with history
- Optional daily price-alerts on large moves (single or multi-threshold ±% levels, deduplicated once per ticker/direction/threshold/day)
- Sends scheduled and on-demand status/report outputs
- User-scoped portfolios in SQLite
- Position CRUD (
ticker,quantity, category, optional display name) - Portfolio position adds auto-sync the ticker into the same user's watchlist
- Cash tracking in USD
- On-demand portfolio valuation (
positions + cash) - Portfolio summaries include an allocation pie chart
- Daily scheduled snapshots
- Daily scheduled portfolio performance report (
1D,WoW,1M,6M,1Y)- Includes full-timeline two-panel history chart (value line + stacked holdings composition)
- Includes allocation pie chart
- Portfolio history chart from snapshots (value + holdings composition)
- Snapshot failure logging + retry attempts
- Snapshot maintenance commands (backup/export, delete/redact, rebuild latest)
- Single-ticker pair charts:
- 10Y context view
- 1Y detail view
- Category-level multi-ticker report charts
- Volume profile + point-of-control highlight + S/R intensity lines
- Portfolio history chart with stacked holdings (snapshot-based)
- Portfolio allocation pie chart
src/eigensignal/engine.py: SMMA, Trend Indicator, RSIsrc/eigensignal/fetcher.py: market data fetch + retriessrc/eigensignal/charts.py: chart generationsrc/eigensignal/store.py: SQLite persistence (users, watchlists, portfolios, snapshots)src/eigensignal/state.py: JSON state + flip history (atomic writes)src/eigensignal/scheduler.py: periodic jobssrc/eigensignal/bot.py: Telegram command handlers (interface layer)src/eigensignal/dev.py: local dev CLI (no Telegram required)
- Python
>=3.11 uv(recommended package manager)- Telegram bot token (for bot interface mode)
- Authorized Telegram user IDs
./scripts/setup.shSetup is intentionally dead simple: it bootstraps dependencies, guides config creation, runs verification, and sets up a service when supported.
It also shows timezone examples during setup (for example America/New_York, Europe/Vienna, UTC).
It validates interactive numeric/time inputs, applies secure config.yaml permissions, and falls back to manual run mode when systemd user services are unavailable.
uv sync --frozen --extra dev- Create a bot with BotFather:
- Open
https://t.me/botfather - Run
/newbot - Follow prompts for bot name and username
- Copy the token you get back (this is your Telegram Bot API token)
- Put the token in config:
- Set
telegram.bot_tokeninconfig.yaml
- Get your Telegram user ID:
- Method A (
@userinfobot): openhttps://t.me/userinfobot(or search Telegram for@userinfobot), start the bot, and copy your numeric user ID from its reply. - Method B (built-in): run your bot once, message it, and if your ID is not authorized yet it replies with your user ID.
- Method C (API): message your bot (
/start), then run:
curl -s "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates"- In the JSON response, read
message.from.id.
- Add your ID to
telegram.authorized_usersinconfig.yaml.
Notes:
- Treat the bot token like a password.
- No separate market-data API key is required for default data fetching (
yfinance/ Yahoo Finance).
Copy the example and edit values:
cp config.example.yaml config.yamlMinimum required sections:
telegram.bot_tokentelegram.authorized_userstickers(can be empty)schedulepaths
Notes:
- Watchlists are user-scoped in SQLite and managed via commands.
tickersin config are best treated as seed/default symbols and are used directly by--dry-run.scheduleandprice_alertsinconfig.yamlact as global defaults.- Users can override selected settings (
timezone, daily report times, and price-alert options) via Telegram or CLI settings commands. check_interval_minutesremains a global runtime cadence for now.- Paths in
config.yamlcan be relative; they are resolved relative to the config file location. runtime.data_cache_ttl_secondscontrols in-process market data cache TTL (120by default).price_alerts.enabledplusprice_alerts.daily_change_pct_threshold(single level) orprice_alerts.daily_change_pct_thresholds(multiple levels) control automatic daily move alerts on watchlist tickers.- Restore/import safety can be tuned via:
runtime.max_restore_payload_bytesruntime.max_restore_watchlist_itemsruntime.max_restore_portfolios
schedule.daily_portfolio_report_timecontrols scheduled portfolio performance reports; if omitted, it falls back toschedule.daily_report_time.
Effective runtime settings are resolved per user:
- Start with global defaults from
config.yaml. - Apply optional per-user overrides from SQLite (
user_preferences).
This affects /config, startup info messages, and all user-scheduled jobs.
Supported per-user keys:
schedule.timezoneschedule.daily_report_timeschedule.daily_portfolio_report_timeprice_alerts.enabledprice_alerts.daily_change_pct_thresholdprice_alerts.daily_change_pct_thresholds
Price-alert thresholds:
daily_change_pct_thresholdis the legacy single-threshold field.daily_change_pct_thresholdsenables multiple levels (for example3,5,8).- The system keeps backward compatibility by mirroring the first sorted threshold into the legacy single-threshold field.
- Deduplication is per
ticker + direction + threshold + local day.
Examples (Telegram):
/settings/settings_set timezone Europe/Vienna/settings_set daily_report_time 07:30/settings_set price_alerts.enabled true/settings_set price_alert_thresholds 3,5,8/settings_reset price_alert_thresholds
Examples (CLI):
uv run eigensignal --config config.yaml settings get --user-id 123uv run eigensignal --config config.yaml settings set --user-id 123 --key timezone --value Europe/Viennauv run eigensignal --config config.yaml settings set --user-id 123 --key price_alert_thresholds --value 3,5,8uv run eigensignal --config config.yaml settings reset --user-id 123 --key schedule.timezoneuv run eigensignal --config config.yaml settings reset --user-id 123 --all
uv run eigensignal --config config.yamluv run eigensignal --config config.yaml --dry-runuv run eigensignal dev analyze AAPL
uv run eigensignal dev chart BTC-USD --no-open
uv run eigensignal dev pipeline AAPL SPY QQQ
uv run eigensignal dev pipeline --config config.yamluv run eigensignal --config config.yaml data wl-backup --user-id 123 --output watchlist.json
uv run eigensignal --config config.yaml data wl-restore --user-id 123 --input watchlist.json
uv run eigensignal --config config.yaml data pf-backup --user-id 123 --output portfolios.json
uv run eigensignal --config config.yaml data pf-restore --user-id 123 --input portfolios.jsonuv run eigensignal --config config.yaml settings get --user-id 123
uv run eigensignal --config config.yaml settings set --user-id 123 --key timezone --value Europe/Vienna
uv run eigensignal --config config.yaml settings set --user-id 123 --key daily_report_time --value 07:30
uv run eigensignal --config config.yaml settings set --user-id 123 --key price_alert_thresholds --value 3,5,8
uv run eigensignal --config config.yaml settings reset --user-id 123 --key schedule.timezone
uv run eigensignal --config config.yaml settings reset --user-id 123 --alluv run eigensignal --config config.yaml doctor
uv run eigensignal --config config.yaml doctor --user-id 123Prints a JSON diagnostics report covering runtime paths, SQLite/state readiness, and optionally user-scoped schedule, holdings, mute status, and recent snapshot failures.
Configured from schedule:
- Flip check every
check_interval_minutes - Daily watchlist price-alert checks happen inside the flip-check cycle when
price_alerts.enabled: true - Daily watchlist report at
daily_report_time - Daily portfolio snapshot at each user's effective
daily_report_time - Daily portfolio performance report at each user's effective
daily_portfolio_report_time(or their effectivedaily_report_timeif omitted) - Effective user schedule/timezone comes from global config defaults plus optional per-user overrides.
- On restart, if a user's local scheduled minute has already passed and today's run was not recorded yet, the dispatcher performs a one-time catch-up send.
- Sends full-timeline two-panel portfolio history chart (
maxrange) with1D/WoW/1M/6M/1Yannotations - Sends allocation pie chart for the latest snapshot
- Systemd watchdog heartbeat every 60s (no-op outside systemd)
/start/user_name <name>/help/ping/doctor
/wl_add <ticker>(alias:/watchlist_add)/wl_remove <ticker>(alias:/watchlist_remove)/wl_list(alias:/watchlist_list)/wl_status(alias:/watchlist_status)/wl_detail <ticker>(alias:/watchlist_detail)/wl_chart <ticker>(alias:/watchlist_chart)/wl_report(alias:/watchlist_report)/wl_backup(alias:/watchlist_backup)/wl_restore [replace](alias:/watchlist_restore) then upload backup JSON document/wl_flips [count](alias:/watchlist_flips)/wl_check(alias:/watchlist_check)
/pf_create <name>(alias:/portfolio_create)/pf_delete <name>(alias:/portfolio_delete)/pf_list(alias:/portfolio_list)/pf_set <name> <ticker> <quantity>(alias:/portfolio_set) - syncs added tickers to watchlist and confirms in chat/pf_remove <name> <ticker>(alias:/portfolio_remove)/pf_positions <name>(alias:/portfolio_positions)/pf_cash_set <name> <amount>(alias:/portfolio_cash_set)/pf_cash_add <name> <amount>(alias:/portfolio_cash_add)/pf_cash_sub <name> <amount>(alias:/portfolio_cash_sub)/pf_summary <name>(alias:/portfolio_summary) - sends valuation text plus allocation pie chart/pf_report <name>(alias:/portfolio_report) - refreshes live valuation first, then sends history chart and allocation pie chart/pf_chart <name> [30d|90d|1y|max](alias:/portfolio_chart) - two-panel history chart (value + stacked holdings)/pf_backup [name](alias:/portfolio_backup)/pf_restore [replace](alias:/portfolio_restore) then upload backup JSON document/pf_snapshot_backup <name>(alias:/portfolio_snapshot_backup)/pf_snapshot_delete <name> <YYYY-MM-DD|all>(alias:/portfolio_snapshot_delete)/pf_snapshot_rebuild <name>(alias:/portfolio_snapshot_rebuild)
Portfolio name parsing:
<name>supports spaces (for example:/portfolio_set Long Term AAPL 2).
/config/doctor/settings/settings_set <key> <value>/settings_reset <key|all>/auth_add <user_id>/auth_revoke <user_id>(alias:/auth_remove)/mute <minutes>/unmute/export/import [replace]then upload scopedstate.jsondocument
Command menu autocomplete:
- On startup, the bot automatically calls Telegram
setMyCommands(default scope) so typing/shows the registered command list.
Export/import behavior:
/exportreturns only the requesting user's scoped state entries./importaccepts only the requesting user's scoped entries and rejects mismatchedtelegram_user_idpayloads.[replace]mode clears the requesting user's existing scoped state keys before import.
Backup restore safety:
- Watchlist and portfolio restore payloads enforce
telegram_user_idmatching the target user when present in the JSON payload.
Configured paths.data_dir:
data.db: SQLite primary storestate.json: latest computed screening state peruser_id:tickerflips.json: append-style flip history
Configured paths.logs_dir:
bot.log: rotating file log
Configured paths.charts_dir:
- temporary/generated chart PNG files
scripts/setup.sh: interactive setup + dependency install + config generation + verificationscripts/start.sh,scripts/stop.sh,scripts/restart.sh,scripts/status.sh: systemd user service helpersscripts/uninstall.sh: remove service and optionally data
On non-systemd environments, run directly with:
uv run eigensignal --config config.yamlRun all tests:
uv run pytestRun only unit tests:
uv run pytest -m unitRun only integration tests:
uv run pytest -m integrationQuick command examples:
uv run eigensignal --config config.yaml --dry-run
uv run eigensignal --config config.yaml data wl-backup --user-id 123 --output watchlist.json
uv run eigensignal dev chart AAPL --no-open- First-time authorized users must set a display name (onboarding prompt).
- Unauthorized users receive a message containing their Telegram user ID.
- Category auto-detection supports
stocks,crypto, andfunds; uncertain tickers prompt for category selection. - Portfolio values use latest fetched close prices from Yahoo Finance.
/muteand/unmuteare user-scoped.- This project does not execute trades or connect to brokerage APIs.





