Admin Reference

End-to-End Trading Workflow

Complete pipeline from hourly scanner → signal review → trade entry → copy execution → position exit. Includes all four automated flowcharts, every API endpoint, and the full launchd schedule.

launchd
:35 past each
market hour
1
Scanner
scan_dip.py
→ history + email
2
Perf Sync
sync_perf.py
→ perf_trades
3
RH Copy
copy_rh_orders
→ detect & mirror
4
Health Check
check_customer_health
→ alert on cred failure
5
Buy
Admin → RH
→ copy to customers
6
Close
RH page or
RH App → copied
7
Report
Friday EOD
P&L emails (admin + customers)
1
Hourly Pipeline — dip_scan.sh
Runs automatically every market hour via macOS launchd · all output captured to logs/dip_scan.log
Scanner Criteria (scan_dip.py)
  • Price down ≥ 8% intraday
  • Market cap ≥ $2B
  • Avg volume ≥ 500K shares
These three gate the scan. Support-level proximity (±3%) is computed for every candidate and used to sort results — it does not filter them, so a dip with zero support hits still appears in dip_signals.json. The "At support only" checkbox on Live/Past Signals applies that filter client-side (default off).
11 Support Levels Checked
  • MMonthly EMA20 · Monthly EMA50
  • WWeekly EMA20 · Weekly EMA50 · Weekly SMA200
  • DDaily EMA20 · Daily EMA50 · Daily SMA200
  • HHourly EMA20 · Hourly EMA50 · Hourly SMA200
Scanner Output
dip_signals.json — latest scan, served by Live Signals page

signals_history.json — per-date log; same-day entry is overwritten on each scan

Note: H Breakout is not stored — it is computed live from /api/confirmed on page load.
launchd Schedule
8:35 CDTPre-market scan (first of day)Auto
9:35–14:35 CDTHourly scans (every full market hour)Auto
15:05 CDTEOD scan + Friday P&L reportCond.
Mon–Fri only · 8 runs/day · RunAtLoad: false
Flowchart — Hourly Pipeline (dip_scan.sh)
⏱ launchd fires at :35 (Mon–Fri, 8:35–14:35 CDT + 15:05)
① scan_dip.py
TradingView screen · 8 support levels · write dip_signals.json
New symbols since last scan?
YES
Email notification
via Gmail SMTP
NO
② sync_perf.py
Sync RH positions → update perf_trades.json
③ copy_rh_orders.run()
Poll RH last 48h · skip platform & seen orders
New direct RH trades found?
YES
copy_buy() or
copy_close() → customers
NO
④ check_customer_health.py
Ping every active customer's broker account (read-only)
Failing set changed since last run?
YES
Email admin
new failure(s) and/or recovery(ies)
NO — same as last run
Friday & time ≥ 15:05 CDT?
YES
send_perf_report.py
Admin weekly P&L email
send_customer_reports.py
Per-customer weekly email
NO
Done · launchd captures stdout+stderr → logs/dip_scan.log
2
Signal Review — Live Signals Page
Admin reads the table, applies filters, and decides which signals to trade
What to look for
  • Multiple support levels hit (more = stronger)
  • H Breakout — 1HR price closing above hourly EMA20 after the dip
  • Daily EMA20 aligned (price above)
  • !Avoid extended downtrends — check TradingView chart
  • !Check news — avoid earnings, FDA decisions, macro events
Support Level Colors
  • MPurple — Monthly support (strongest)
  • WGreen — Weekly support
  • DBlue — Daily support
  • HAmber — Hourly support (weakest)
H Breakout — how it works
Checked live via GET /api/confirmed on every page load and every 5 minutes during market hours.

✓ confirmed — last complete 1HR candle closed above the prior candle's high
✗ not confirmed — dip is at support but buyers haven't stepped in yet
Pages involved
Live Signals (admin + client) — current week, filterable by support & H Breakout

Past Signals (admin + client) — prior weeks only, same filters

Strategy (admin + client) — entry/exit rules reference
3
Trade Entry — Buy on Robinhood Page
Admin clicks Buy → limit order on admin's Robinhood account → copy trade mirrors to all active customers simultaneously
Flowchart — Buy Flow
Admin clicks Buy on Live Signals or Robinhood page
Buy modal — symbol · qty · limit price
POST /api/buy → serve_dip.py
Admin's account
RobinhoodAPI
.buy_limit()
Admin's RH account
perf_trades.json
new trade logged
+
All active customers
copy_trade.copy_buy()
qty = floor(capital ÷ max_trades ÷ price)
Alpaca creds?
→ AlpacaAPI
.limit_buy()
RH token?
→ RobinhoodAPI
.buy_limit()
copy_log.json
per-broker order_id + state logged
Sizing Formula
# Per customer, per trade qty = floor( customer.capital / customer.max_trades / limit_price ) # e.g. $50K ÷ 10 ÷ $25.00 = 200 shares
Order Types
Admin (RH): Limit order · time_in_force: day
Customer (Alpaca): Limit order · same price
Customer (RH): Limit order · time_in_force: gfd

Extended hours (pre/after-market): limit orders only — extended_hours flag set automatically.
Skipped if…
  • Customer status ≠ "active"
  • Computed qty < 1 share
  • No broker credentials set
  • Customer capital < $1,000
4
Automatic RH Order Detection — copy_rh_orders.py
Detects trades made directly in Robinhood (bypassing the platform) and copies them to all active customers
Flowchart — Copy RH Orders (runs inside sync_perf.py each hour)
Triggered hourly (via sync_perf.py) or manually via GET /api/copy_rh_orders
RobinhoodAPI.get_orders() — last 48 hours, newest-first
Order older than 48h?
YES
Stop paging
NO — continue
state == "filled"?
NO
Skip
YES
order_id in platform orders or seen set?
YES
Skip + mark seen
NO — new direct trade
side = buy
copy_trade.copy_buy()
→ all active customers
side = sell
copy_trade.copy_close()
→ all active customers
Save seen IDs → data/rh_copy_seen.json (capped at 5,000)
Deduplication — how platform orders are excluded
Platform-originated orders have their order_id stored in perf_trades.json at buy time. copy_rh_orders loads that set and skips any matching order ID, so platform trades are never double-copied. A separate rh_copy_seen.json prevents re-processing across hourly runs.
5
Position Monitoring
Robinhood page shows admin's live RH positions. Alpaca page is a read-only monitor of the first/test client account — used to verify copy trades are landing correctly.
Robinhood Page — Open Positions Panel
Source: GET /api/rh/positions — admin's live Robinhood positions

Shows: Symbol · Qty · Avg Entry · Current Price · Unrealized P&L · Move %

Close button → POST /api/close (RH market sell + copy_close to all customers).
Alpaca Page — First Client Monitor
Source: GET /api/alpaca/positionsfirst/test client's Alpaca account

Read-only — no Close button. This account is always the same permanent test client. Its positions verify that copy trades are landing. Admin never trades through Alpaca; closing on the RH page (or RH app) will copy-close this account automatically.
Performance Pages (Admin + Client)
Source: perf_trades.json (file, updated hourly by sync_perf.py)

Open positions panel shows live current price via GET /api/prices, updated every 5 minutes during market hours. Shows unrealized P&L per symbol.
Exit Signals to Watch
  • !1HR price closes below hourly EMA20 after entry
  • !Target hit (strategy-defined % gain)
  • !Adverse news / fundamental change
  • !Position at manual stop-loss level
6
Trade Exit — Robinhood Page (or Robinhood App)
Admin closes on the Robinhood page (or directly in the Robinhood app). Copy close mirrors the sell to all customers — including their Alpaca accounts. The Alpaca page is read-only monitoring only.
Close Flow — Robinhood Page (or Robinhood App via copy_rh_orders)
Admin clicks Close on Robinhood page
Modal: symbol · total qty · avg entry · live P&L
POST /api/close {symbol, qty}
RobinhoodAPI.sell_market()
Admin's Robinhood account
Update perf_trades.json
Mark open lots closed · fill price · P&L
copy_trade.copy_close()
Market sell on each customer's broker (Alpaca + RH)
Copy log updated
Customer close logic (copy_trade.copy_close)
# For each active customer: if alpaca_key + alpaca_secret: qty = GET /v2/positions/{symbol} → qty if qty > 0: AlpacaAPI.sell_market(qty) if rh_token: qty = RobinhoodAPI.get_positions() → qty for symbol if qty > 0: RobinhoodAPI.sell_market(qty) # Customers with no position are silently skipped
API
API Endpoints — serve_dip.py (port 8081)
All endpoints at http://localhost:8081 · static files served from website-dip/
MethodPathDescriptionUsed by
GET /api/prices?symbols=… Live last prices via yfinance for comma-separated symbols, including pre/post-market Live Signals, Past Signals, Performance pages
GET /api/confirmed?symbols=… H Breakout status — last complete 1HR candle closed above prior high Live Signals, Past Signals pages
GET /api/account Admin Robinhood account — equity, buying power, margin, day P&L Robinhood page account strip
GET /api/alpaca/account First/test client's Alpaca account — equity, cash, buying power, margin Alpaca page account strip
GET /api/alpaca/positions First/test client's Alpaca positions — used to verify copy trades are landing Alpaca page (read-only monitor)
GET /api/rh/positions Admin's Robinhood positions with live quotes Robinhood page open positions panel
GET /api/refresh Triggers sync_perf.py — syncs RH positions → perf_trades.json Robinhood page Sync button
GET /api/customers Load all customers from data/customers.json Customers page
GET /api/copy_log Full copy trade log — all buy/close mirror records Customers page copy log
GET /api/copy_rh_orders Manually trigger copy_rh_orders.run() — detect & mirror direct RH trades Robinhood page "Copy RH Trades" button
GET /api/customer_account?id=… Load a single customer's broker account data (equity, positions) Customers page per-customer panel
GET /api/client/resolve?token=… Resolve a client's personal access token to {customer_id, name, joined} — no broker data Client portal login (auth.js requireClient)
GET /api/client/activity?id=… That one customer's copy_log entries only — never the full multi-customer log client/account.html Recent Activity
POST /api/buy Limit buy on admin's Robinhood + copy to all active customers Buy modal (Live Signals / Robinhood page)
POST /api/close Market sell on admin's Robinhood + copy_close to all customers · updates perf_trades.json Close button on Robinhood page positions panel
POST /api/onboard Create new customer record from onboarding form landing.html onboarding flow
POST /api/customers Update existing customer — status, capital, credentials Customers page edit modal
POST /api/admin/login Verify admin password → sets signed, HttpOnly session cookie login.html
POST /api/logout Clears the session cookie Admin nav logout
All admin-only routes above (buy, close, refresh, customers, copy_log, copy_rh_orders, account/positions endpoints) require a valid admin session cookie — see the new Deployment Workflow section below for how auth fits into the deployed setup.
System Architecture
How all components connect — automated flows and manual flows
Automated — launchd (Mon–Fri, 8:35–15:05 CDT)
launchd :35
dip_scan.sh
scan_dip.py
dip_signals.json
+
signals_history.json
Email (new signals)
sync_perf.py
perf_trades.json
copy_rh_orders.run()
copy_buy / copy_close → customers
check_customer_health.py
customer_health_state.json
diffed vs last run
Email admin (only on change)
Friday ≥ 15:05
send_perf_report.py → admin weekly P&L email
+
send_customer_reports.py → per-customer weekly email
All output → logs/dip_scan.log (via launchd StandardOutPath)
Manual — Admin UI → serve_dip.py :8081
Admin (Robinhood page)
→ POST /api/buy →
RobinhoodAPI → Admin RH account
+
copy_trade → customers
Admin (Robinhood page)
→ POST /api/close →
RobinhoodAPI.sell_market
+
copy_close → customers
Alpaca page — first/test client monitor
read-only · verifies copy trades landed · same account always · never admin's trade broker
Customer Broker Routing (copy_trade.py)
data/customers.json
→ active customers →
Alpaca creds → AlpacaAPI (live or paper)
+
RH token → RobinhoodAPI
(legacy / dual-broker)
data/copy_log.json
← per-broker order_id + state, viewed on Customers page
Client Portal (website-dip/client/) — auth: personal access link, one per customer
?t=<access_token>
GET /api/client/resolve
signed session cookie + sessionStorage (id/name/joined for the UI)
no shared password — each customer's link is generated from the Customers page (Copy Link). The cookie is what the server actually trusts; sessionStorage is just a UI convenience.
client/signals.html
current week signals · live prices + H Breakout
client/history.html
prior weeks · same filters as admin past_signals.html
client/performance.html
perf_trades.json, filtered to trades entered on/after this client's join date · open positions + live unrealized P&L · weekly table
client/account.html
the client's real broker account, not the strategy log — GET /api/customer_account + GET /api/client/activity (that client's copy_log entries only, including failures)
client/strategy.html
entry/exit rules · support levels · H Breakout checklist
D
Deployment Workflow — Local → Repo → Production
Local dev on the Mac (launchd) mirrors production on AWS (systemd). Three steps ship a change: edit & test locally, commit to GitHub, then run one script to deploy.
Flowchart — Shipping a Change
① Local — edit code, test against local serve_dip.py (localhost:8081)
② Repo — git commit && git push (origin/main on GitHub)
③ Deploy — bash deploy/deploy.sh
Pull prod's live data down first
customers.json, copy_log.json, rh_copy_seen.json, dip_signals.json, signals_history.json, perf_trades.json — backed up locally
rsync local tree → /opt/dip-swing
push_to_ec2.sh — bypasses git; scan/trade files are excluded outright, never pushed back
systemctl restart dip-swing.service
only needed for Python code changes
Live at https://dip-swing.com
Local vs Production Scheduler
Local (Mac): launchd — only com.dipswing.dip_server (the dev web server) runs. The hourly scan job (com.dipswing.dip.hourly) is disabled — its plist was moved out of ~/Library/LaunchAgents after it was found running redundantly alongside production and clobbering live scan data on every deploy.

Production (AWS EC2): systemd — dip-swing.service (the web app, always running) + dip-swing-scan.timer (hourly pipeline) — the only scheduler for scanning and trading now.
Customer data — bidirectional
Real customers sign up directly on the live site, so customers.json (+ copy_log.json / rh_copy_seen.json) on production is often ahead of the local copy. deploy/deploy.sh pulls these down first — timestamped local backup — then pushes the local tree back up, so either side can be the one that added a record and neither push order can silently erase it.
Scan/trade data — pull-only, never pushed
dip_signals.json, signals_history.json, and perf_trades.json are pulled down for local testing but push_to_ec2.sh excludes them outright — production's own scan is the sole source of truth for these, full stop. A local machine can never overwrite them, no matter what's sitting in the working tree.
What needs a restart
  • Python changes (serve_dip.py, copy_trade.py, etc.) → restart required
  • Static files (HTML/CSS/JS in website-dip/) → live immediately, no restart
  • Scan/report scripts → picked up fresh on the next scheduled run, no restart
Auth — production only
Admin pages and admin-only API routes require a signed, HttpOnly session cookie (POST /api/admin/login). The client portal uses a per-customer access token instead of a shared password — see the Client Portal row in System Architecture above.
C
Customer Schema — data/customers.json
Fields required for copy trade execution
{ "id": "cust_001", "name": "John Doe", "email": "john@example.com", // also the address for weekly reports (send_customer_reports.py) "phone": "5551234567", "state": "GA", "status": "active", // active | pending | paused — filter used: status == "active" "capital": 50000, // USD — used for sizing formula "max_trades": 10, // max simultaneous positions "notes": "", "access_token": "qeQdbXug...", // client portal login — /client/signals.html?t=<this>, no password // Alpaca credentials (new customers) "alpaca_key": "PKXXXXXXXX", "alpaca_secret": "xxxxxxxxxxxxxxxx", "alpaca_paper": true, // true = paper-api.alpaca.markets // Robinhood credentials (legacy / dual-broker) "rh_token": "Bearer xxxxxxxx" }
Broker Routing Rules
  • AHas alpaca_key + alpaca_secret → trades on Alpaca (live or paper)
  • RHas rh_token → trades on Robinhood
  • BHas both → trades on both simultaneously
  • Has neither → skipped, logged as skipped
Status Values
  • active — receives copy trades
  • !pending — onboarded, not yet active
  • paused — skipped from copy trades
Log Files
  • Llogs/dip_scan.log — hourly pipeline run log
  • Cdata/copy_log.json — all copy trade fills
  • Sdata/rh_copy_seen.json — RH order_id → {attempts, done}; failed copies retry up to 5× before giving up
  • Pwebsite-dip/perf_trades.json — trade log + P&L
  • Hdata/customer_health_state.json — last known failing customer/broker pairs; diffed each run to decide whether to email
Customers Page — Admin Tools
Edit — opens a form for name/capital/broker credentials, wired to POST /api/customers. Credential fields are always blank on open and only sent if retyped, so leaving one blank never overwrites the existing key.

Click a customer's name — opens their live performance on the fly (no page navigation): per-broker Portfolio/Buying Power/Day/Week/Month P&L, open positions with unrealized P&L, and recent copy-trade activity from copy_log.json. Source: same GET /api/customer_account?id=… call.

Copy Link — generates (if missing) the customer's access_token and copies their personal client-portal login link to the clipboard, for the admin to send directly. No shared password exists for the client portal.