Real-time signal engine for the City Traders Imperium (CTI) prop firm strategy, with a live dashboard for monitoring, configuration, and trade management. Uses Oanda v20 REST API for market data and demo execution. Discord alerts for all signals. Designed to swap to MatchTrader for live prop firm execution with zero signal logic changes.
- 4-Layer Signal Stack β StochRSI + MACD + Keltner Channel + Candlestick confirmation
- Live Dashboard β Real-time prices, watchlist rankings, open trades, trade history, account metrics
- Dashboard Config β Switch mode (alert_only/demo/live), program (challenge/instant), phase, trigger re-scans β all from the browser
- Weekend Throttling β Dashboard auto-throttles polling from 2s β 60s when markets are closed
- Webhook Callbacks β TradeGumi sends structured events to DockeGumi for automated orchestration
- 30-Min Re-Ranking β Watchlist re-scans every 30 minutes during market hours to track range evolution
- Discord Alerts β Every signal, blocked signal, and watchlist update posted to Discord
TradeGumi records diagnostics for every evaluated opportunity so no-signal periods can be reviewed without changing signal behavior. The dashboard route /strategy-metrics shows date-range summaries, near-misses, criterion pass/fail rates, blocker rankings, period comparison, and JSON export.
Diagnostic history is stored in Postgres (the durable source of truth), with a compact latest summary written to src/tradegumi/data/strategy_metrics.json for dashboard observability. By default, diagnostic retention is 90 days and can be adjusted with STRATEGY_METRICS_RETENTION_DAYS.
Signal journal exports separate emitted signal outcomes from tradable setup outcomes. A signal only counts as a strategy-stat trade opportunity when usable_for_strategy_stats is true; duplicates, missed entries, late signals, stale signals, and manual invalidations are excluded. Tune setup grouping and entry validity with SIGNAL_SETUP_GROUP_WINDOW_MINUTES, SIGNAL_ENTRY_TOLERANCE_ATR, and SIGNAL_STALE_BARS.
Strategies are self-contained, executable plugin folders under strategies/.
TradeGumi core is the runtime framework (market data, trend/chop/shock filters,
cooldown, diagnostics, risk sizing); each strategy folder owns its decision
logic and is discovered and loaded at runtime without any change to core code.
Two reference strategies are tracked: strategies/example-strategy/ (the full
CTI pullback strategy with continuation management) and
strategies/macd-momentum/ (a minimal strategy proving the contract with only
the two required files). Backtesting and strategy research are not part of
TradeGumi β they live in QuantPipe.
See docs/strategy-plugins.md for the full folder
contract and interface reference.
strategies/
βββ example-strategy/ # Tracked β full reference implementation
β βββ strategy.json # metadata (id, label, description, signal_type, entrypoint)
β βββ strategy.py # get_strategy() -> BaseStrategy; owns the signal decision
β βββ indicators.py # strategy-specific indicators / structure helpers
β βββ management.py # trade-management (manage_open_trade), risk/exit, confidence
β βββ README.md # per-strategy notes
βββ macd-momentum/ # Tracked β minimal reference (two required files only)
β βββ strategy.json
β βββ strategy.py
β βββ README.md
βββ my-strategy/ # Gitignored β your own strategy (local only)
βββ strategy.py
Only strategy.json and strategy.py are required; indicators.py,
management.py, config.py, tests/, and any other files are optional and up to
the strategy.
| Field | Required | Description |
|---|---|---|
id |
Yes | Unique strategy identifier (used in dropdown values) |
label |
No | Display name in the dashboard dropdown |
description |
No | Short description shown in tooltips/metadata |
signal_type |
No | Strategy variant tag (e.g. pullback, continuation) |
entrypoint |
No | Strategy module filename (informational; defaults to strategy.py) |
tradegumi.strategy_loader.load_strategy() resolves a folder under the configured
strategies directory (STRATEGIES_DIR, default ./strategies) by folder name or by
strategy.json id, imports its strategy.py as an isolated synthetic package
(so from .indicators import β¦ works even with a hyphenated folder name), and
calls the module-level get_strategy() factory, which must return a
tradegumi.strategy_loader.BaseStrategy. SignalEngine loads its strategy once at
construction β the bundled example-strategy by default, overridable with the
TRADEGUMI_STRATEGY env var (folder name or id) β and calls
strategy.evaluate(engine, ctx) for each evaluation, plus the optional
strategy.bridge_trend(...) and strategy.manage_open_trade(ctx) hooks. The last
owns continuation / open-trade management: core detects a continuation against an
active trade and persists the outcome, but the strategy decides the SL/TP changes
(the default declines, so management is opt-in). The separate strategy registry
scans strategies/ for strategy.json to populate the dashboard dropdown, and
strategy_loader.discover_strategies() validates that each folder actually
implements the interface β the worker logs the result at startup.
- Copy the example folder:
cp -r strategies/example-strategy strategies/my-strategy - Edit
strategies/my-strategy/strategy.json(uniqueid) and the logic instrategy.py/indicators.py/management.py. - Point the runtime at it with
TRADEGUMI_STRATEGY=my-strategy(or itsid) and restart the API server / worker.
Your new folder is not tracked by git β only the bundled reference strategies
(example-strategy, macd-momentum) are committed. All other sibling folders are
gitignored so your private strategies stay local. The built-in CTI-v1 entry is
always available in the dropdown regardless of folder contents.
If a folder is missing strategy.json or contains malformed JSON, it still appears
in the dropdown with a warning badge (
# Strategy folders β only the bundled reference strategies are tracked.
# Sibling folders (your own strategies) are ignored so they stay local.
strategies/*
!strategies/example-strategy/
!strategies/macd-momentum/cd src && poetry installOr without Poetry:
pip install -r requirements.txtcp .env.example .envEdit .env with your values:
# Oanda API β get these from https://www.oanda.com/account/tpa/personal_token
OANDA_API_KEY=your_oanda_api_key_here
OANDA_ACCOUNT_ID=your_account_id_here
# Oanda URLs β defaults to practice (demo) account
# Change to https://api-fxtrade.oanda.com for live
OANDA_BASE_URL=https://api-fxpractice.oanda.com
# Discord webhook β create one in your Discord server settings
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/your_webhook_here
# Run mode: alert_only | demo | live
TRADEGUMI_MODE=alert_only
# CTI program and phase
CTI_PROGRAM=challenge # challenge (2-step) or instant (instant funding)
CTI_PHASE=1 # 1 = Phase 1, 2 = Phase 2, 3 = Funded
# Webhook callback URL (optional β for DockeGumi orchestration)
# Set to http://your-nuc-ip:8198/api/tradegumi/webhook when running remotely
TRADEGUMI_CALLBACK_URL=
.envis gitignored. Your keys will never be committed to the repo.
The runtime is split into three independent processes β worker (trading
loop, no HTTP port), api (analytics + control on :8199), and dashboard
(Next.js on :3000). See specs/019-split-runtime-containers.
For local development, run each in its own terminal:
# Worker β trading loop
cd src && poetry run python -m tradegumi.main --mode alert_only
# API β analytics + control server
cd src && poetry run python -m tradegumi.api_main
# Dashboard
cd dashboard && npm install && npm run build && NEXT_PUBLIC_API_URL=http://localhost:8199 npm startFor production, Docker Compose runs all three as separate services (plus Postgres and Redis):
docker compose up -d --buildDashboard: http://localhost:3000 Β· API: http://localhost:8199.
The runtime is split into three independently deployable services that share state through Redis (hot state, operator commands, worker heartbeat) and Postgres (durable analytics). A failure or restart of any one service does not affect the other two.
ββββββββββββββββββββ ββββββββββββββββββββ
β tradegumi-worker β β tradegumi-api β
β 4-layer loop βββ snapshot/heartbeat βββΆ β HTTP :8199 β
β (no public port) ββββββ commands βββββββββ β analytics+ctrl β
ββββββββββ¬ββββββββββ (Redis) ββββββββββ¬ββββββββββ
β durable writes β read-only broker
βΌ βΌ client (no orders)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Redis (hot state Β· commands Β· heartbeat) β
β Postgres (durable analytics Β· journal) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β²
ββββββββββββ΄ββββββββββββ
β tradegumi-dashboard β
β Next.js :3000 β
β β calls the API only β
ββββββββββββββββββββββββ
Data flow:
- Worker runs the 4-layer signal engine, writes durable analytics to Postgres, and publishes a hot-state snapshot + liveness heartbeat to Redis. No public HTTP port.
- API serves the dashboard: hot state from the worker's Redis snapshot, durable analytics from Postgres, and account/positions/trade-history from its own read-only broker client. It never places orders (Risk-First).
- Dashboard (Next.js) reads and writes exclusively through the API.
- Config changes (mode/program/phase/challenge_type, re-scan) flow API β worker over the Redis command channel; the worker applies them without a restart and reflects them in its snapshot. A command issued while the worker is down is applied on its next startup (durable desired-config).
- Callback webhook sends structured events (signals, worker start/stop, command applied) to DockeGumi.
| Component | Interval | Purpose |
|---|---|---|
| Price ticker | 1 second | Live bid/ask for all watchlist symbols (1 API call) |
| Signal engine | 5 seconds | Indicators, LR slopes, signal detection |
| Loop state write | 1 second | Dashboard data (prices, trends, LR values) |
| Watchlist re-rank | 30 minutes | Re-evaluate tiers during market hours |
| Pre-session scan | 02:00 ET daily | Full re-scan + instrument availability check |
API budget: ~4 calls/sec (well within Oanda's 120/sec limit).
The dashboard is a Next.js app that reads and writes exclusively through the API service (tradegumi-api); it holds no direct connection to the worker, Redis, or Postgres.
| Section | Column | Polls | Data Source |
|---|---|---|---|
| π³ Account Metrics | Left | 30s | watchlist.json |
| βοΈ Settings | Left | 5s | GET /api/status, POST /api/config/* |
| β‘ Active Signals | Left | 30s | signals.json |
| π Open Trades | Left | 5s (open) / 60s (closed) | GET /api/positions |
| π Watchlist | Right | 30s (open) / 60s (closed) | watchlist.json + loop_state.json |
| π Trade History | Right | 30s (open) / 60s (closed) | GET /api/trades |
When all symbols are closed (weekend/after-hours), the dashboard:
- Throttles all polling from 2-5s β 60s
- Shows a yellow banner: "π Markets closed β polling throttled to 60s"
- API status still polls at 5s (config changes take effect immediately)
- Auto-resumes fast polling when any market opens
Switch between alert_only β demo β live, change program (challenge/instant), phase (1/2/Funded), and trigger manual re-scans β all from the dashboard. Changes are delivered to the worker over the Redis command channel and applied within a loop iteration (no restart). They are also recorded as durable "desired config" in Redis, which the worker reconciles on startup β so a change made while the worker is down is applied when it returns. Note: while set, desired-config overrides the matching .env value across restarts (clear the Redis desired_config key to fall back to .env).
When program is set to "Instant", the phase selector dynamically hides (instant accounts don't have phases).
| Endpoint | Description |
|---|---|
GET /api/status |
Current mode, program, phase, runtime state |
GET /api/data/loop_state |
Live prices, trends, LR values |
GET /api/data/watchlist |
Ranked watchlist with tiers |
GET /api/data/signals |
Active signals |
GET /api/positions |
Open positions from Oanda |
GET /api/trades?count=N |
Closed trade history |
| Endpoint | Body | Description |
|---|---|---|
POST /api/config/mode |
{"mode": "alert_only"|"demo"|"live"} |
Switch trading mode |
POST /api/config/program |
{"program": "challenge"|"instant"} |
Switch CTI program |
POST /api/config/phase |
{"phase": 1|2|3} |
Switch CTI phase |
POST /api/action/rescan |
{} |
Trigger immediate watchlist re-scan |
Config/action POSTs are delivered to the worker over the Redis command channel.
They return {"status": "accepted", "command_id": ...} (or 400 for an invalid
value, 503 if the command channel is unavailable β never a silent success).
The worker applies the command within a loop iteration; poll GET /api/status to
observe the change.
All endpoints include CORS headers for dashboard access.
When TRADEGUMI_CALLBACK_URL is set, the bot POSTs structured events to that URL:
| Event | When | Payload |
|---|---|---|
signal |
Every trade signal | symbol, direction, confidence, strategy, LR values, mode |
mode_change |
Mode switched via API | new mode, previous mode |
rescan |
Watchlist re-scan completes | trigger type (full/periodic) |
closed_market |
All markets close | day name, message |
trade_open |
Position opened (future) | trade details |
trade_close |
Position closed (future) | trade details |
Webhook receiver runs on DockeGumi at :8198 for automated orchestration.
| Mode | Signals | Execution | Use When |
|---|---|---|---|
alert_only |
β Discord only | β None | Initial testing β see what fires without risk |
demo |
β Discord | β Oanda demo account | Forward testing β real market, fake money |
live |
β Discord | β MatchTrader prop account | Stage 2 β live prop firm trading |
Start with alert_only. Run it for 1-2 weeks. Review signal quality and Layer 2 confidence scores. Then switch to demo from the dashboard.
| Variable | Default | Description |
|---|---|---|
OANDA_API_KEY |
(required) | Oanda v20 API token |
OANDA_ACCOUNT_ID |
(required) | Oanda account ID |
OANDA_BASE_URL |
https://api-fxpractice.oanda.com |
Practice URL (change for live) |
DISCORD_WEBHOOK_URL |
(required) | Discord webhook for trade alerts |
TRADEGUMI_MODE |
alert_only |
alert_only / demo / live |
TRADEGUMI_CALLBACK_URL |
(optional) | DockeGumi webhook URL for orchestration |
TRADEGUMI_API_PORT |
8199 |
API server port |
MAX_OPEN_POSITIONS |
5 |
Max simultaneous positions |
CTI_PROGRAM |
challenge |
challenge (2-step) or instant (instant funding) |
CTI_PHASE |
1 |
1 = Phase 1 (10%), 2 = Phase 2 (5%), 3 = Funded (10%) |
CTI_DAILY_LOSS_PCT |
0.05 |
5% daily loss limit |
CTI_MAX_DD_PCT |
0.10 |
10% max drawdown |
RISK_PER_TRADE |
0.0025 |
0.25% account risk per trade |
SL_ATR_MULTIPLIER |
3 |
Stop loss = 3Γ ATR |
TP_ATR_MULTIPLIER |
12 |
Take profit = 12Γ ATR (1:4 R:R) |
Every signal passes through all four layers. All must agree for a signal to fire:
- Linear Regression slope on 15m (length=50) AND 5m (length=14)
- Both timeframes must agree on direction. No counter-trend trades.
Runs at startup, every morning at 06:30 ET, and every 30 minutes during market hours. Ranks all symbols by:
- ADR consumption β how much daily range is already used
- Volatility regime β 5d ATR vs 20d ATR (expanding vs contracting)
- Breakout probability β % of recent sessions with >1Γ ATR moves during London/NY overlap
- Day-of-week bias β historical directional tendency
Output: Tier 1 (trade), Tier 2 (watch/alert), Below Threshold (skip). No hardcoded symbol restrictions β the data decides daily.
| Indicator | Parameters | BUY Condition | SELL Condition |
|---|---|---|---|
| StochRSI | 14,14,3,3 | k prev-3 min < 30, k > d | k prev-3 max > 70, k < d |
| MACD | 12,26,9 | Histogram > prev-5 min | Histogram < prev-5 max |
| Keltner Channel | 20, 1.5Γ EMA | Last-5 low β€ KC middle min | Last-5 high β₯ KC middle max |
| Candlestick (optional) | β | Engulfing, Hammer | Shooting Star, Engulfing |
Each indicator also produces a 0-1 strength score (Layer 2 quality scoring). The weighted sum becomes the signal confidence percentage.
- Position size = 0.25% account risk Γ· (ATR Γ SL multiplier Γ lot size)
- SL = 3Γ ATR from entry
- TP = 12Γ ATR from entry (1:4 risk:reward)
- Trailing SL: 4-tier system that tightens as profit grows (3Γ β 2Γ β 1.5Γ β 1Γ ATR at 0R β 1.5R β 3R β 5R)
- Max 5 open positions
| Asset Class | Trading Hours (ET) | Notes |
|---|---|---|
| Forex | MonβFri 00:00β16:30 | 16:30β19:00 break (swap blackout) |
| Indices | MonβFri 00:00β16:30 | 16:30β18:30 break |
| Crypto | 24/7 | Low priority β doesn't fit session model |
| All | β | No new entries during swap blackout |
CTI_Scripts/
βββ .env.example # Template for API keys
βββ .env # Your config (gitignored)
βββ dashboard/ # Next.js dashboard
β βββ src/
β β βββ app/page.tsx # Main layout
β β βββ components/
β β β βββ AccountCard.tsx # π³ Account metrics + progress bars
β β β βββ SettingsPanel.tsx # βοΈ Mode/program/phase + re-scan
β β β βββ SignalsSection.tsx # β‘ Active signals cards
β β β βββ OpenTrades.tsx # π Open positions table
β β β βββ WatchlistSection.tsx # π Ranked symbols with live prices
β β β βββ TradeHistory.tsx # π Closed trades (filterable, sortable)
β β β βββ Header.tsx # Live clock, mode badge, provider
β β β βββ Footer.tsx # CTI program info
β β βββ hooks/useData.ts # Data hooks with weekend throttling
β β βββ lib/api.ts # API client (config, positions, trades)
β β βββ types/index.ts # TypeScript interfaces
β βββ public/data/ # Symlink to bot data directory
βββ src/
β βββ pyproject.toml # Poetry project
β βββ tradegumi/
β βββ main.py # Entry point, dual-cadence loop
β βββ config.py # .env loader + all configuration
β βββ api_server.py # HTTP API (:8199) for dashboard
β βββ callback.py # Webhook sender for DockeGumi
β βββ webhook_receiver.py # Webhook receiver (:8198) for orchestration
β βββ api/
β β βββ base_client.py # Abstract ExecutionClient interface
β β βββ oanda_client.py # Oanda v20 REST implementation
β βββ indicators.py # pandas_ta wrappers + Layer 2 scoring
β βββ signal_engine.py # Trend filter + 4-layer stack + confidence
β βββ pre_session_scanner.py # Morning ranked watchlist (Layer 1)
β βββ alerts.py # Discord webhook alerter + signal persistence
β βββ risk.py # Position sizing, daily drawdown checks
β βββ session_rules.py # Trading hours, swap blackout, DOW bias
β βββ trailing_sl.py # 4-tier ATR trailing stop manager
βββ src/tradegumi/data/ # Runtime JSON data (loop_state, watchlist, signals)TradeGumi runs as a single docker-compose stack with Postgres (durable analytics) + Redis (hot runtime cache).
services:
tradegumi:
image: ghcr.io/gitchegumi/cti-scripts:latest
build:
context: .
dockerfile: Dockerfile
ports:
- "8199:8199" # API server
- "3000:3000" # Dashboard
env_file:
- .env
volumes:
- tradegumi-data:/app/src/tradegumi/data
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8199/api/status"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_USER: tradegumi
POSTGRES_PASSWORD: ${TRADEGUMI_POSTGRES_PASSWORD:-tradegumi}
POSTGRES_DB: tradegumi
volumes:
- postgres-data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U tradegumi -d tradegumi"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis-data:/data
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
start_period: 5s
volumes:
tradegumi-data:
postgres-data:
redis-data:TRADEGUMI_DATABASE_URL=postgresql://tradegumi:your_password@postgres:5432/tradegumi
TRADEGUMI_REDIS_URL=redis://redis:6379/0Postgres is required β it is the durable source of truth for strategy metrics (evaluated opportunities and criterion results) and the signal journal (journal_entries). There is no SQLite fallback: if TRADEGUMI_DATABASE_URL is unset or Postgres is unreachable, the bot fails fast at startup rather than silently writing elsewhere. The application reads and writes the journal through Postgres; the append-only signal_journal.jsonl file is legacy β kept as a best-effort export/backup snapshot and importable into a fresh database via journal.backfill_from_jsonl, but never read for queries. Redis caches hot runtime state (latest loop state / dashboard view) and short-lived strategy-summary responses, and is safe to lose.
By default the dashboard is gated by the JOURNAL_TOKEN cookie. When you deploy behind an Authentik outpost, the proxy injects forwarded identity headers (x-authentik-*) after it authenticates the request. To trust those headers instead of the cookie, set:
AUTHENTIK_TRUST_PROXY_HEADERS=trueOnly enable this when both conditions hold:
- The dashboard is unreachable except through the Authentik proxy (e.g. it is not exposed directly on
:3000). - The proxy is configured to strip any client-supplied
x-authentik-*headers before forwarding.
Without those guarantees a caller could forge x-authentik-username and bypass authentication entirely. When the flag is false or unset, the x-authentik-* headers are ignored and the JOURNAL_TOKEN cookie check is used.
- Create a demo account: Oanda fxTrade Practice
- Generate API token: Log in β Account Management Portal β "Manage API Access" β Generate token
- Find your account ID: Shown in the Oanda dashboard or API response
- Add to
.env:OANDA_API_KEYandOANDA_ACCOUNT_ID
Oanda API reference: https://developer.oanda.com/rest-live-v20/introduction/
- MatchTrader REST client implementation
-
TRADEGUMI_MODE=liveunlock - Docker Compose deployment with Postgres + Redis
- Tiered risk table (from Risk_Management_Rules.docx) replacing flat 0.25%
- Dynamic position sizing (Layer 3 β after positive expectancy validated)
- Automated daily performance reporting to Discord