Kelly Watcher is a local, operator-driven Polymarket copy-trading system. It watches selected wallets, scores incoming trades, sizes positions with Kelly-style logic, executes in shadow or live mode, records everything to SQLite, and exposes a terminal dashboard for monitoring the bot in real time.
This repository is meant to be clonable by another developer without private local runtime state. kelly-config.env is committed because it contains non-secret operator settings. kelly-secrets.env, databases, logs, model artifacts, and other runtime files are intentionally excluded from git.
- A Python backend that polls watched wallets and decides whether to copy trades.
- A shadow trading path that simulates fills from captured order books.
- A live trading path guarded by readiness and risk checks.
- A terminal dashboard built with Ink + React.
- Wallet discovery and identity resolution tools.
- A training and auto-retraining pipeline for the model-based signal path.
- Tests covering runtime behavior, training/search logic, market URL handling, CLI behavior, and retrain bookkeeping.
These are intentionally local-only and should stay out of version control:
kelly-secrets.env,.env, and any other secret-bearing env filessave/runtime files, SQLite databases, logs, and model artifacts- Python caches and Node modules
- one-off local artifacts such as
nohup.outandresults.json
The bot creates its runtime directories automatically on startup.
The Python runtime intentionally lives under backend/src/kelly_watcher/. That directory is the importable Python package used by backend/pyproject.toml and all kelly_watcher.* imports. Do not flatten those files directly into backend/src/; doing so breaks packaging and console scripts.
The supported operator UI is frontend/, the React Ink terminal dashboard. The old browser dashboard is not part of the supported workflow.
At a high level, the system works like this:
backend/src/kelly_watcher/runtime/tracker.pypolls watched Polymarket wallets for recent trades and loads market metadata, order books, and price history.backend/src/kelly_watcher/engine/watchlist_manager.pykeeps wallets in hot, warm, discovery, or dropped tiers so more promising wallets are polled more aggressively.backend/src/kelly_watcher/engine/signal_engine.pyscores each buy signal using either:- the heuristic pipeline, or
- a deployed model artifact if a current compatible
save/model.joblibexists.
backend/src/kelly_watcher/engine/kelly.pysizes the trade, then wallet trust and exposure guards shrink or block it.backend/src/kelly_watcher/runtime/executor.pyeither:- simulates a shadow order from the order book, or
- posts a real Polymarket order in live mode.
backend/src/kelly_watcher/data/db.pywrites the durable record intosave/data/trading.db.backend/src/kelly_watcher/main.pyalso emits a lightweight JSON event stream, bot state file, and an HTTP API for the dashboard.backend/src/kelly_watcher/runtime/evaluator.pyresolves finished markets, computes PnL, and closes positions.backend/src/kelly_watcher/research/train.pyandbackend/src/kelly_watcher/research/auto_retrain.pyperiodically retrain and optionally deploy a new model.
Important distinction:
- PnL, readiness checks, and open-position accounting are based on executed trades.
- The training set is broader: it includes resolved executed buys and can also include a narrow set of resolved skipped buys with counterfactual returns, down-weighted during training.
You need:
- Python 3.11+
uv- Node.js and
npmfor the dashboard - network access to Polymarket APIs
Optional for live trading:
- a Polygon wallet with USDC
POLYGON_PRIVATE_KEYPOLYGON_WALLET_ADDRESS
Notes:
- The dashboard talks to the backend over HTTP, so it no longer needs direct SQLite access.
- The repository includes
backend/uv.lockandfrontend/package-lock.jsonso installs are reproducible.
Production can run split across two network-connected machines (e.g. over Tailscale):
- Machine A runs the trading backend/API.
- Machine B runs the React Ink terminal dashboard.
Use two repo-root env files per checkout:
kelly-config.env: program settings that are safe to inspect and tune, such as watchlists, thresholds, sizing, polling, and risk limits.kelly-secrets.env: private or machine-specific values, such as wallet keys, Telegram tokens, dashboard API tokens, chat IDs, and backend URLs.
On the backend machine's kelly-secrets.env, keep:
DASHBOARD_API_HOST=<backend-ip>
DASHBOARD_API_PORT=8765
KELLY_API_BASE_URL=http://<backend-ip>:8765On the frontend machine's kelly-secrets.env, keep:
KELLY_API_BASE_URL=http://<backend-ip>:8765If you set DASHBOARD_API_TOKEN on the backend, set the same value as KELLY_API_TOKEN on the frontend.
git clone https://github.com/aryan-cs/kelly-watcher.git
cd kelly-watchercd backend
uv sync
cd ..cd frontend
npm install
cd ..Use two repo-root env files. Kelly Watcher reads kelly-config.env first and kelly-secrets.env second. Do not put env files in save/; save/ is disposable runtime state and can be reset or rebuilt.
Examples:
cp kelly-config.env.example kelly-config.env # only if kelly-config.env is missing
cp kelly-secrets.env.example kelly-secrets.envCopy-Item kelly-config.env.example kelly-config.env
Copy-Item kelly-secrets.env.example kelly-secrets.envcopy kelly-config.env.example kelly-config.env
copy kelly-secrets.env.example kelly-secrets.envAt minimum for shadow mode, confirm:
WATCHED_WALLETSUSE_REAL_MONEY=false
WATCHED_WALLETS should be a comma-separated list of lowercase wallet addresses.
If you already know the wallets, paste them into kelly-config.env.
If you only know Polymarket handles or profile URLs, use:
cd backend
uv run resolve-wallet @some_user
uv run resolve-wallet https://polymarket.com/@some_user
cd ..If you want to discover candidate wallets from leaderboard data, use:
cd backend
uv run rank-copytrade-wallets --top 20
cd ..Both tools print a ready-to-paste WATCHED_WALLETS=... line.
Preferred:
cd backend
uv run main --localEquivalent:
cd backend
uv run kelly-watcher --localRun backend commands from backend/ unless noted otherwise.
The backend will automatically:
- create
save/data/andsave/logs/ - initialize or migrate the SQLite schema
- load
kelly-config.envandkelly-secrets.env - validate startup config
- start the polling loop
- emit
save/data/events.jsonlandsave/data/bot_state.json - start the dashboard API on
http://127.0.0.1:8765by default
--local is a process-only override for same-machine Mac testing. It does not edit kelly-config.env or kelly-secrets.env; it forces this run to use localhost. Omit it when running the backend on Windows for the Mac dashboard over Tailscale.
If you want to run the dashboard on another computer, set:
DASHBOARD_API_HOST=<backend-ip>on the backend machine- optionally
DASHBOARD_API_TOKEN=some-shared-secret
In a second terminal:
cd frontend
npm run dev:localTo run the dashboard on another computer, point it at the backend API:
cd frontend
KELLY_API_BASE_URL=http://<backend-ip>:8765 npm startIf the backend sets DASHBOARD_API_TOKEN, also set:
cd frontend
KELLY_API_BASE_URL=http://<backend-ip>:8765 KELLY_API_TOKEN=some-shared-secret npm startYou can also put those in the dashboard machine's repo-level kelly-secrets.env instead of exporting them every time:
KELLY_API_BASE_URL=http://<backend-ip>:8765
KELLY_API_TOKEN=some-shared-secretThe dashboard reads KELLY_API_BASE_URL and KELLY_API_TOKEN from kelly-secrets.env, with shell environment variables taking precedence if you set both.
For dashboard development from the TypeScript sources instead of the checked-in runtime JS:
cd frontend
npm run devThis is the default and the recommended starting point.
Requirements:
USE_REAL_MONEY=falseWATCHED_WALLETSconfigured
Behavior:
- no real orders are sent
- fills are simulated from the captured order book
- positions, skips, resolutions, and PnL are still logged normally
- the dashboard works the same way as in live mode
Reset helper:
cd backend
uv run shadow-resetThe default reset is a full shadow reset. It deletes the local save/ runtime state,
including old SQLite data, events, logs, and save/model.joblib.
Soft account reset, keeping the learned model and operator caches:
cd backend
uv run shadow-reset --preserve-model --preserve-identity-cache --preserve-telegram-stateForeground mode:
cd backend
uv run shadow-reset --foregroundReset only, then start the bot yourself:
cd backend
uv run shadow-reset --preserve-model --preserve-identity-cache --preserve-telegram-state --reset-onlyWhat shadow-reset does:
- refuses to run if
USE_REAL_MONEY=true - stops an existing bot process
- deletes shadow runtime state such as the SQLite DB, positions, PnL, event stream, and bot state
- preserves config and
WATCHED_WALLETS - preserves
save/model.joblib, identity cache, and Telegram state only when the explicit preserve flags are used - recreates the DB around the configured
SHADOW_BANKROLL_USDand restarts the bot
Use live mode only after you have validated shadow behavior.
Required env values:
USE_REAL_MONEY=truePOLYGON_PRIVATE_KEYPOLYGON_WALLET_ADDRESS
Recommended first-time setup:
cd backend
uv run polymarket-setupLive startup also enforces operational checks such as:
- wallet/private-key consistency
- minimum balance availability
- live allowance checks
- live position sync health
- optional shadow-history requirement if
LIVE_REQUIRE_SHADOW_HISTORY=true
Important live behavior:
- USDC collateral approval is typically a one-time step.
- Conditional token approvals are requested automatically when opening a live position.
- Entry guards can pause new entries after drawdown or daily-loss limits are hit.
tracker.py polls Polymarket data endpoints for watched-wallet activity and normalizes each incoming source trade into a structured event. It fetches:
- source trade details
- market metadata
- order books
- price history when available
It also manages cursors so old trades do not replay forever.
watchlist_manager.py separates wallets into tiers:
- hot
- warm
- discovery
- dropped
Hot wallets are polled most frequently. Lower tiers are polled less often. Wallets can be demoted or dropped when they become stale or underperform on copied trades.
trader_scorer.py builds trader-level features such as:
- win rate
- sample count
- volume
- average size
- account age
- conviction ratio
market_scorer.py scores the market itself using inputs such as:
- spread
- visible depth
- time to resolution
- momentum
- volume
- concentration
Unsafe markets are vetoed before sizing.
signal_engine.py then decides whether the trade passes.
It has two paths:
- Heuristic path: combines trader and market quality into a confidence estimate, then applies adaptive floors and belief adjustments.
- Model path: if
MODEL_PATHpoints to a compatible artifact, the engine uses the trained model instead of the pure heuristic scorer.
If the model artifact is missing, stale, or incompatible with the current training contract, the system falls back to heuristics automatically.
kelly.py computes the base Kelly-style size. That output is then adjusted by:
- the minimum confidence threshold
- the minimum bet size
- bankroll availability
- wallet trust and quality multipliers
- portfolio exposure caps
executor.py handles both entries and exits.
Shadow mode:
- simulates fills from the current order book
- rejects trades that would not have filled cleanly
Live mode:
- initializes the CLOB client
- checks live balances and allowances
- posts market orders
- reconciles live fills and positions
evaluator.py periodically checks whether copied markets resolved. It updates:
- resolved outcome
- remaining position size
- shadow or live PnL
- training labels
It also contains sports-page fallbacks for certain market types when the direct market payload is not enough.
auto_retrain.py and train.py handle scheduled and early retraining.
Current model behavior:
- the label mode is expected-return based
- the artifact is versioned by a data contract
- deployable models must pass internal search and holdout checks
- skipped-but-trainable counterfactual rows are down-weighted relative to executed fills
If a retrain passes deployment checks, the bot reloads the model automatically.
The dashboard is a terminal app, not a web app. It reads backend state through the local dashboard API:
save/data/trading.dbsave/data/events.jsonlsave/data/bot_state.jsonsave/data/identity_cache.json
If a cache-backed dashboard read fails, the header/footer will show stale sources such as queries stale, events stale, config stale, or identity stale instead of silently reusing cached rows.
In production, run it on the frontend machine and set KELLY_API_BASE_URL=http://<backend-ip>:8765 in its kelly-secrets.env.
The bot reads and writes these local files during normal operation:
save/data/trading.db: source-of-truth SQLite databasesave/data/events.jsonl: rolling event stream for the dashboardsave/data/bot_state.json: lightweight runtime statussave/data/identity_cache.json: wallet-to-username cachesave/logs/bot.log: rotating backend logsave/logs/shadow_runtime.out: background log when usingshadow-resetsave/model.joblib: optional deployed model artifact
Important note:
trade_login SQLite is the durable record.events.jsonlexists to drive the dashboard and should be treated as a convenience stream, not as the canonical ledger.
The most important tables in save/data/trading.db are:
trade_log: all copied trades, skips, fills, exits, resolutions, and featurespositions: currently open positionstrader_cache: cached trader stats used by scoringmodel_history: deployed model artifacts and metricsretrain_runs: every retrain attempt, including skipped and rejected runswallet_cursors: per-wallet polling cursorswallet_watch_state: tracked/dropped wallet state
The terminal dashboard currently has six main pages.
Shows raw incoming watched-wallet trades before the bot makes a copy decision.
Useful for checking:
- whether the feed is alive
- whether watched wallets are active
- whether prices and sizes look sane
Shows accepted, rejected, skipped, and paused decisions after scoring.
Useful for checking:
- why a trade was accepted or rejected
- whether thresholds are too strict
- whether vetoes or risk controls are dominating
Shows open positions, closed positions, and performance state.
Useful for checking:
- current exposure
- recent exits
- shadow or live PnL
- time spent in positions
Shows model and retrain health.
Useful for checking:
- whether the deployed model beats baseline
- whether retraining is succeeding
- whether the bot is on heuristics or a model-backed path
Shows wallet-level quality and tracking information.
Useful for checking:
- which wallets are helping
- which wallets are stale or downgraded
- which ones have local resolved copied history
Shows runtime state plus editable env-backed config values.
Important behavior:
- some fields apply on the next loop
- some fields require a bot restart
- toggling live trading only edits
kelly-config.env; it does not hot-switch the running bot
Global:
1through6: switch pagesr: refreshq: quit
Page-specific:
- Live Feed:
Up/Downscroll - Signals:
Up/Downscroll,Left/Rightpan long text - Performance: arrows to move,
j/kto scroll,Enterfor detail,Escto close - Models: arrows to move,
Enterfor detail - Wallets:
Up/Downselect,Enterdetail,Escclose - Settings: arrows or
j/kselect,Enteroreedit,Esccancel
Start the bot:
cd backend
uv run main --localStart the dashboard:
cd frontend
npm run dev:localRun the full test suite:
cd backend
uv run pytestResolve a handle or profile into wallets:
cd backend
uv run resolve-wallet @some_userRank candidate wallets:
cd backend
uv run rank-copytrade-wallets --top 20Run manual training:
cd backend
uv run python -m kelly_watcher.research.trainReset and restart the shadow account:
cd backend
uv run shadow-resetRun one-time live collateral setup:
cd backend
uv run polymarket-setupAll env parsing lives in config.py. Duration values typically accept forms such as 45s, 10m, 6h, 7d, or unlimited.
WATCHED_WALLETS: comma-separated watched wallet addressesUSE_REAL_MONEY:falsefor shadow,truefor liveMIN_CONFIDENCE: minimum signal confidenceMIN_BET_USD: minimum order sizeMAX_BET_FRACTION: Kelly cap as a fraction of bankrollSHADOW_BANKROLL_USD: paper bankroll in shadow modeMODEL_PATH: optional deployed model artifact path
POLYGON_PRIVATE_KEYPOLYGON_WALLET_ADDRESSMAX_LIVE_DRAWDOWN_PCTMAX_DAILY_LOSS_PCTMAX_TOTAL_OPEN_EXPOSURE_FRACTIONMAX_MARKET_EXPOSURE_FRACTIONMAX_TRADER_EXPOSURE_FRACTIONMAX_LIVE_HEALTH_FAILURESLIVE_REQUIRE_SHADOW_HISTORYLIVE_MIN_SHADOW_RESOLVED
POLL_INTERVAL_SECONDSHOT_WALLET_COUNTWARM_WALLET_COUNTWARM_POLL_INTERVAL_MULTIPLIERDISCOVERY_POLL_INTERVAL_MULTIPLIERDATA_API_REQUEST_RATE_PER_SECONDDATA_API_REQUEST_BURSTWALLET_TRADE_FETCH_WORKERSENRICHMENT_FETCH_WORKERSSOURCE_EVENT_PROCESS_BATCH_SIZEMAX_SOURCE_TRADE_AGEMAX_SOURCE_TRADE_AGE_FARSOURCE_TRADE_AGE_FAR_MARKET_HORIZONMAX_FEED_STALENESSMIN_EXECUTION_WINDOWMAX_MARKET_HORIZON
HEURISTIC_MIN_ENTRY_PRICEandHEURISTIC_MAX_ENTRY_PRICEare fallback safety bounds.- When
ADAPTIVE_HEURISTIC_ENTRY_PRICE_ENABLED=true, the heuristic entry-price band is recomputed from recent resolved shadow evidence and counterfactual outside-band rejects. The refresh is controlled byADAPTIVE_HEURISTIC_ENTRY_PRICE_REFRESH; the evidence window is controlled byADAPTIVE_HEURISTIC_ENTRY_PRICE_LOOKBACK. ALLOWED_ENTRY_PRICE_BANDSALLOWED_TIME_TO_CLOSE_BANDSHEURISTIC_MIN_ENTRY_PRICEHEURISTIC_MAX_ENTRY_PRICEHEURISTIC_ALLOWED_ENTRY_PRICE_BANDSADAPTIVE_HEURISTIC_ENTRY_PRICE_ENABLEDADAPTIVE_HEURISTIC_ENTRY_PRICE_LOOKBACKADAPTIVE_HEURISTIC_ENTRY_PRICE_REFRESHADAPTIVE_HEURISTIC_ENTRY_PRICE_MIN_SAMPLESADAPTIVE_HEURISTIC_ENTRY_PRICE_MIN_BAND_SAMPLESADAPTIVE_HEURISTIC_ENTRY_PRICE_MIN_AVG_RETURNXGBOOST_ALLOWED_ENTRY_PRICE_BANDSMODEL_MIN_TIME_TO_CLOSE
WALLET_INACTIVITY_LIMITWALLET_SLOW_DROP_MAX_TRACKING_AGEWALLET_COLD_START_MIN_OBSERVED_BUYSWALLET_DISCOVERY_MIN_OBSERVED_BUYSWALLET_DISCOVERY_MIN_RESOLVED_BUYSWALLET_DISCOVERY_SIZE_MULTIPLIERWALLET_TRUSTED_MIN_RESOLVED_COPIED_BUYSWALLET_PROBATION_SIZE_MULTIPLIERWALLET_QUALITY_SIZE_MIN_MULTIPLIERWALLET_QUALITY_SIZE_MAX_MULTIPLIER
RETRAIN_BASE_CADENCERETRAIN_HOUR_LOCALRETRAIN_EARLY_CHECK_INTERVALRETRAIN_MIN_NEW_LABELSRETRAIN_MIN_SAMPLESLOG_LEVEL
TELEGRAM_BOT_TOKENTELEGRAM_CHAT_ID
Core runtime:
backend/src/kelly_watcher/main.py: startup, polling loop, scheduling, event emission, bot-state writesbackend/src/kelly_watcher/runtime/tracker.py: Polymarket trade and market data clientbackend/src/kelly_watcher/engine/signal_engine.py: heuristic/model decision logicbackend/src/kelly_watcher/runtime/executor.py: shadow and live executionbackend/src/kelly_watcher/runtime/evaluator.py: resolution and PnL updatesbackend/src/kelly_watcher/data/db.py: SQLite schema and migrationsbackend/src/kelly_watcher/engine/dedup.py: duplicate and open-position gating
Training and model:
backend/src/kelly_watcher/research/train.py: feature loading, search, calibration, deployment decisionbackend/src/kelly_watcher/research/auto_retrain.py: scheduled and early retrain orchestrationbackend/src/kelly_watcher/engine/economic_model.py: return-target transforms and sample weightsbackend/src/kelly_watcher/engine/trade_contract.py: SQL contract for trainable and executed rowsbackend/src/kelly_watcher/engine/features.py: shared feature list
Watchlist and wallet tooling:
backend/src/kelly_watcher/engine/watchlist_manager.py: wallet tiering and auto-drop logicbackend/src/kelly_watcher/engine/wallet_trust.py: sizing and trust tiersbackend/src/kelly_watcher/tools/resolve_wallet.py: handle/profile URL to wallet resolverbackend/src/kelly_watcher/tools/rank_copytrade_wallets.py: leaderboard-based discovery and rankingbackend/src/kelly_watcher/data/identity_cache.py: wallet and username cache
Frontend:
frontend/dashboard.tsx: main terminal UIfrontend/pages/*.tsx: page viewsfrontend/configEditor.ts: env-backed editable settingsfrontend/settingsDanger.ts: live toggle and shadow reset actions
Packaging and entrypoints:
backend/pyproject.toml: project metadata and console scriptsbackend/src/kelly_watcher/cli.py: lightweight launcher souv run mainworks cleanlybackend/src/kelly_watcher/shadow_reset.py: cross-platform shadow reset and restart helper
- This is an operator system, not a fire-and-forget hosted service.
- The frontend reads runtime state through the backend HTTP API, with repo-root env files used only for connection settings and editable config.
- A fresh clone starts on heuristics unless you later train and deploy a model artifact.
- Runtime backups and scheduled jobs are driven by
backend/src/kelly_watcher/main.py, not by external infra. - The bot will refuse unsafe live startup states rather than silently continuing.
Current scheduled tasks inside the main process include:
- trade resolution checks every 2 minutes
- daily report at 08:00 local time
- DB backup at 04:00 local time
- scheduled retrain at the configured cadence/hour
- early retrain checks at
RETRAIN_EARLY_CHECK_INTERVAL - watchlist refresh and cache refresh jobs
Primary test command:
cd backend
uv run pytestAreas covered by tests include:
- runtime and startup regressions
- CLI launch behavior
- expected-return model handling
- training-data contract rules
- search/holdout planning
- retrain run bookkeeping
- market URL handling
- wallet trust and watchlist management
Check that the backend is running and writing:
save/data/trading.dbsave/data/events.jsonlsave/data/bot_state.json
Check:
WATCHED_WALLETSis populated- the watched wallets are actually active
- your poll interval is sane
- the logs are not showing repeated API failures or rate limits
That usually means one of:
save/model.joblibdoes not exist- the artifact was trained under an older data contract
- the artifact failed to load
In all of those cases the bot falls back to heuristics automatically.
Common reasons:
- wallet/private-key mismatch
- missing balance or allowance
- live position sync failure
LIVE_REQUIRE_SHADOW_HISTORY=truebut you do not have enough resolved shadow trades yet
Refresh it with:
cd backend
uv run resolve-wallet <wallet-or-handle>That updates save/data/identity_cache.json.
- Shadow mode is the default for a reason.
- Live mode should be treated as high risk and operationally supervised.
- Test and validate watchlist quality before trusting bankroll results.
- Keep secrets in
kelly-secrets.env, never in committed source files. - Do not treat the dashboard event stream as the canonical ledger; use SQLite for durable records.