Skip to content

Latest commit

Β 

History

485 Commits

Folders and files

Repository files navigation

fintwit-web

Self-hosted dashboard for crypto, stocks, forex and options β€” enriched Twitter/X streams, sentiment analysis, and market signals.

banner


GitHub Actions Workflow Status License Code style: black

fintwit-web aggregates financial markets data from Twitter/X, Reddit, Binance, Yahoo Finance, TradingView, and more β€” enriched with ML-based sentiment analysis and chart recognition β€” and surfaces it through a self-hosted React dashboard.

It's a full-stack migration of the fintwit-bot Discord bot: same data pipelines and ML models, but without Discord's rate limits, low data density, and lack of a custom UI. The dashboard covers crypto, stocks, forex, options, NFTs, enriched tweets, and live portfolio tracking. The migration is ongoing and incremental β€” see Documentation for current coverage.

Key Features πŸ”‘

  • Enriched tweet timeline β€” streamed live from X/Twitter, classified by ticker, scored for sentiment (FinTwitBERT), and flagged for chart images (chart-recognizer)
  • Crypto markets β€” price index, trending coins, gainers/losers, funding rates, liquidations, exchange listings, TradingView ideas
  • Stock markets β€” price index, earnings calendar, halts, StockTwits, TradingView ideas, gainers/losers
  • Options, forex & NFTs β€” options overview/volume/SPACs/short interest, forex index with economic events and yield curve, NFT top/trending/upcoming/P2E collections
  • Reddit β€” WallStreetBets scraping for cross-subreddit ticker trends
  • Portfolio tracking β€” live trade tracking, with optional Interactive Brokers sync
  • Self-hosted β€” SQLite or PostgreSQL storage, runs via Docker Compose, no Discord required

Machine Learning Models πŸ€–

Multiple custom-trained models power the enrichment pipeline. All are lightweight and load automatically when the backend starts:

Screenshots πŸ“Έ

fintwit-web home β€” enriched tweet timeline

The enriched tweet timeline (above) is the flagship view β€” each tweet is ticker-classified, sentiment-scored, and matched with a live price card, alongside cross-market overview panels (mention heat, sentiment shift, sector rotation) built from Twitter, Reddit, and StockTwits data.

Crypto dashboard
Crypto β€” trending coins, Binance movers, market cap
Stocks dashboard
Stocks β€” fear/greed index, halts, StockTwits signals
Portfolio dashboard
Portfolio β€” value over time, asset context, IBKR sync
Options dashboard
Options β€” overview, put/call ratio, live options chain

Table of Contents πŸ—‚

Quick Start πŸš€

Runs on a Raspberry Pi, a home server, or any Linux/macOS/Windows host with Docker (on Linux: curl -fsSL https://get.docker.com | sh).

git clone https://github.com/StephanAkkerman/fintwit-web.git
cd fintwit-web
cp .env.example .env        # then set X_AUTH_TOKEN and X_CT0, see below
docker compose up -d

Open http://127.0.0.1:3000. docker compose up pulls prebuilt images (amd64 and arm64) and falls back to building from source if it can't.

Only the X cookies are needed for the tweet timeline (how to get them). Skip them and everything else still works; the timeline explains how to connect. The stack is localhost-only by default: the frontend is on 127.0.0.1:3000 and the backend API on 127.0.0.1:7999, and nothing is exposed beyond your machine until you opt into the Cloudflare Tunnel.

Connect your X account

The tweet timeline streams your X Following feed as your own logged-in session, not through an official API key. It needs two cookies from that session:

  1. Log into x.com in your browser.
  2. Open DevTools (F12) β†’ Application tab (Firefox: Storage) β†’ Cookies β†’ https://x.com.
  3. Copy the values of auth_token and ct0 into .env, then restart the backend (docker compose up -d):
X_AUTH_TOKEN=...
X_CT0=...

These cookies grant full access to your X account: .env is gitignored, never commit it, and treat the values like a password. They stay valid until that browser session is logged out. When X starts rejecting them, the timeline shows an "X session expired" notice; recapture both values and restart.

Already have a curl.txt?

A captured HomeLatestTimeline request (DevTools β†’ Network β†’ right-click β†’ Copy as cURL) still works when X_AUTH_TOKEN/X_CT0 are unset: put it at the repo root for local runs, or at state/curl.txt for Docker. X_CURL_PATH overrides the location.

Configuration

Everything lives in .env. .env.example lists what a typical install touches: the X cookies, an optional API key, and commented-out blocks for each optional integration. docs/configuration.md documents every variable, including tuning knobs. In Docker, every .env value is passed to the backend. Keep the KEY=value format, with no spaces around =.

The backend logs which integrations are active when it starts:

[startup] integrations: X timeline: cookies | Reddit: unauthenticated fallback | IBKR: off | ...

Setting API_KEY protects the backend's /api with an X-API-Key header. The dashboard keeps working because the frontend's proxy (nginx in Docker, Vite in dev) attaches the key server-side, so it never reaches the browser.

Optional add-ons

Each of these is opt-in; skip the ones you don't need.

Interactive Brokers portfolio sync

Adds the ibgateway container and a backend sync worker. In .env, set:

IBKR_ENABLED=true
TRADING_MODE=live
TWS_USERID=your-ibkr-username
TWS_PASSWORD=your-ibkr-password
VNC_SERVER_PASSWORD=change-me-to-a-strong-password
COMPOSE_PROFILES=ibkr

TRADING_MODE is live or paper; the backend picks the matching gateway port. Then run docker compose up -d.

IB Gateway runs headless most days, but still asks for an occasional interactive login/2FA approval. Connect a VNC client (e.g. Remmina: sudo apt install -y remmina remmina-plugin-vnc) to 127.0.0.1:5901 with VNC_SERVER_PASSWORD, complete the login and any 2FA prompt, and leave it running.

Check sync health:

docker logs -f fintwit-ibgateway
docker logs -f fintwit-backend
curl -H "X-API-Key: YOUR_API_KEY" http://127.0.0.1:7999/api/ibkr/status   # connected: true

Notes:

  • In Docker the backend reaches the gateway at ibgateway:4003 (live) or :4004 (paper). connection refused there means IB Gateway is up but the login/2FA isn't complete yet.
  • Running the backend outside Docker? Set IBKR_HOST=127.0.0.1; it then uses 4001/4002.
  • Gateway settings persist in ./ibgateway-settings.

Public access via Cloudflare Tunnel

Adds the cloudflared container and an Access login gate, so only people on your allowlist can reach the dashboard. One-time Cloudflare setup:

  1. Create a free Cloudflare account and add your domain.
  2. At your domain registrar, switch its nameservers to the ones Cloudflare assigns.
  3. Create an API token at dash.cloudflare.com/profile/api-tokens with: Account β†’ Access: Apps and Policies β†’ Edit; Account β†’ Access: Service Tokens β†’ Edit; Account β†’ Cloudflare Tunnel β†’ Edit; Zone β†’ DNS β†’ Edit; Zone β†’ Zone β†’ Read.

Then provision the tunnel, DNS record and Access policy with Terraform, from infra/:

cp terraform.tfvars.example terraform.tfvars
# set your Cloudflare token/account values and access_allowed_emails
terraform init
terraform apply
terraform output -raw tunnel_token

Back in the repo root, add to .env and run docker compose up -d:

CLOUDFLARE_TUNNEL_TOKEN=<terraform tunnel_token output>
COMPOSE_PROFILES=tunnel

(COMPOSE_PROFILES=ibkr,tunnel if you also use IBKR.) The dashboard is then served at https://<subdomain>.<zone_name> from terraform.tfvars. Visitors first see Cloudflare's login page and verify with a one-time code emailed to them; anyone not on the allowlist is blocked at Cloudflare's edge. access_allowed_emails only seeds the list; after that, manage it from the dashboard's Admin page, which needs a few more .env values (see infra/README.md).

Give Claude access to your deployment

app/mcp_server.py is a read-only MCP server over the dashboard's API, so Claude (Code, Desktop, or a cloud session) can pull your tweets, market data, Reddit trends and portfolio and reason over them. It reads the backend's OpenAPI schema, so everything the dashboard shows is available without a tool per endpoint (list_endpoints + get). Three bundled tools answer the common questions in one call each: portfolio (positions, P&L, diversification, IBKR account), holdings_check (per holding: tweet chatter and sentiment, Reddit buzz, recent news, upcoming earnings) and ticker (everything about one symbol, including your position in it). market_snapshot and candidates cover the market and buy ideas for the scheduled market check below. It can't change anything: edits, the SSE stream and /api/admin/* are not reachable.

Claude can't complete Cloudflare's email-code login, so Terraform also creates an Access service token (access_service_tokens in terraform.tfvars, ["claude"] by default): a client id + secret that gets past Access without a login, and that you can revoke on its own. After terraform apply:

terraform output -json access_service_tokens   # {"claude": {"client_id": ..., "client_secret": ...}}

Then point the MCP server at the deployment with these environment variables:

FINTWIT_URL=https://<subdomain>.<zone_name>
CF_ACCESS_CLIENT_ID=<client_id>
CF_ACCESS_CLIENT_SECRET=<client_secret>
  • Claude Code in this repo: .mcp.json registers the server as fintwit; export the variables before starting claude (and pip install -e ".[test]" or pip install -r requirements.txt for the mcp package).
  • Claude Code on the web: add the three variables to the cloud environment's environment variables, and allow your dashboard's hostname under its network access. Sessions on that environment get the fintwit tools automatically.
  • Claude Desktop / any MCP client: run python -m app.mcp_server from the repo root over stdio, with the same variables.

Without the variables it talks to a local backend on http://127.0.0.1:7999. To rotate the token, terraform apply -replace='cloudflare_zero_trust_access_service_token.fintwit["claude"]'.

Scheduled market check (Advisor)

With the MCP server connected, Claude can check the market on a schedule and suggest buys and sells for you to act on. It is advice only: Claude can't place trades or change your rules.

  1. Open the dashboard's Advisor page (/advisor) and write your rules (position limits, take-profit and stop levels, what to avoid, risk appetite), a watchlist, and optionally the cash you have available, so Claude can size its buy ideas.
  2. Create a routine in Claude Code on the web, on the cloud environment from the section above, that runs every hour on weekdays with the prompt "Run the market-check skill." The skill (.claude/skills/market-check/SKILL.md) skips the run when US markets are closed, reviews your holdings and buy candidates against your rules, and replies "Nothing new" when nothing changed.
  3. Each suggestion is logged to the Advisor page's decision journal, priced when it was made, and graded after 7 and 30 days, so the scorecard shows whether the calls are worth following.

Before suggesting a symbol, Claude checks its valuation and analyst targets, short interest, TradingView technicals and where the price sits against its 52-week range and 200-day average (/api/research/*, part of the ticker and holdings_check tools).

Signa. .mcp.json also registers Signa's MCP server (https://app.getsigna.ai/api/mcp), authenticated with the same SIGNA_KEY the backend uses (a cmts_... key). Set SIGNA_KEY in the environment Claude runs in, and on a cloud environment allow app.getsigna.ai in its network access. The market check then weighs Signa's verdict for each symbol it suggests, and skips Signa when the key isn't set.

Reddit API credentials

Without credentials, the r/wallstreetbets service uses an unauthenticated HTTP fallback, which Reddit blocks more often. For more reliable access, create a "script" app at reddit.com/prefs/apps and set REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET in .env.

Raspberry Pi and older CPUs

If the backend crashes with a PyTorch Illegal instruction error in chart recognition (common on ARM and older CPUs), disable chart inference and/or chart OCR in .env:

CHART_ENABLED=false
CHART_EXTRACTION_ENABLED=false

To find the failing stage (PyTorch conv vs timm vs chart model), run docker exec -it fintwit-backend python -m app.runtime.probe_chart_stack. To use a different PyTorch version, set TORCH_VERSION in .env and build locally with docker compose up -d --build backend. The image uses CPU-only PyTorch wheels, so no CUDA runtime is downloaded.

Updating

make update

This runs git pull, pulls the latest prebuilt images, and restarts the stack, picking up any COMPOSE_PROFILES (e.g. ibkr, tunnel) set in .env. If the images can't be pulled, it builds them from source instead (docker compose up -d --build). No make on your host? Run the same steps directly:

git pull
docker compose pull
docker compose down
docker compose up -d

Local development βš™οΈ

pip install -r requirements.txt
cd frontend && npm install && cd ..
cp .env.example .env
npm run dev

npm run dev starts uvicorn on 127.0.0.1:7999 and the Vite dev server on http://127.0.0.1:5173, which proxies /api/* to the backend. To run them separately:

uvicorn app.api.main:app --port 7999 --reload
cd frontend && npm run dev

make check runs the same tests, linting and build checks as CI.

Documentation

Contributing πŸ› 

Contributions are welcome! If you have a feature request, bug report, or proposal for code refactoring, please feel free to open an issue on GitHub. See CONTRIBUTING.md for code style and PR guidelines. We appreciate your help in improving this project.

https://github.com/StephanAkkerman/fintwit-web/graphs/contributors

License πŸ“œ

This project is licensed under the MIT License. See the LICENSE file for details.