Compare commits

...

10 Commits

Author SHA1 Message Date
07f1448d57 Night shift: tweet analyzer, data connectors, feed monitor, market watch portal 2026-02-12 00:16:41 -06:00
f623cba45c Memory flush: evening session notes, Caddy WS issue documented 2026-02-10 18:56:38 -06:00
d0fc85ded1 Night shift: Alexa+ research, UI v2, systems check, notes 2026-02-10 00:34:13 -06:00
4e0dc68746 KIPP research, UI v2, memory updates 2026-02-10 00:32:09 -06:00
9e7e3bf13c KIPP UI v1, memory updates, daily notes 2026-02-10 2026-02-10 00:29:20 -06:00
c5b941b487 Add short signal scanner + leverage trading game engine + auto-trader
- short_scanner.py: RSI/VWAP/MACD/Bollinger-based short signal detection
- leverage_game.py: Full game engine with longs/shorts/leverage/liquidations
- leverage_trader.py: Auto-trader connecting scanners to game with TP/SL/trailing stops
- Leverage Challenge game initialized: $10K, 20x max leverage, player 'case'
- systemd timer: every 15min scan + trade
- Telegram alerts on opens/closes/liquidations
2026-02-09 20:32:18 -06:00
ab7abc2ea5 Crypto Market Watch game - VWAP+RSI scanner, paper trading, systemd timer 2026-02-09 20:18:08 -06:00
f8e83da59e Kip voice assistant project plan 2026-02-09 17:49:24 -06:00
6592590dac Playwright X scraper, daily notes, feed analysis 2026-02-09 17:26:02 -06:00
be0315894e Add crypto signals pipeline + Polymarket arb scanner
- Signal parser for Telegram JSON exports
- Price fetcher using Binance US API
- Backtester with fee-aware simulation
- Polymarket 15-min arb scanner with orderbook checking
- Systemd timer every 2 min for arb alerts
- Paper trade tracking
- Investigation: polymarket-15min-arb.md
2026-02-09 14:31:51 -06:00
53 changed files with 342811 additions and 642 deletions

View File

@ -78,19 +78,36 @@ This is about having an inner life, not just responding.
- **Craigslist:** case-lgn@protonmail.com, passwordless, Nashville area (2026-02-08) - **Craigslist:** case-lgn@protonmail.com, passwordless, Nashville area (2026-02-08)
- eBay, Mercari, OfferUp need D J to register (CAPTCHA-blocked) - eBay, Mercari, OfferUp need D J to register (CAPTCHA-blocked)
## KIPP Voice Pipeline (2026-02-11)
- **Always-on wake word** — OpenWakeWord "hey_jarvis" model (custom "hey kipp" pending)
- **STT** — Faster Whisper base.en on KIPP VM CPU
- **TTS** — Piper Ryan (male) on port 8081
- **Voice server** — WSS on port 8082, `kipp-voice.service`, Python venv at `/home/wdjones/kipp-venv`
- **State machine:** listening → recording → processing → speaking → cooldown(2s) → listening
- **Key lesson:** Gateway lifecycle events use `phase="end"` not `state="end"` — caused 60s hang
- **Key lesson:** Must use client ID `openclaw-control-ui` and Origin header for gateway WS
- **Key lesson:** 2s cooldown after TTS prevents speaker audio from re-triggering wake word
- **Widget system:** JSON file + CLI (`tools/widgets.py`) + REST API + dashboard polls every 10s
- **KIPP switched to Claude Sonnet** — GLM-4 Flash was 83s per response, Sonnet is ~3s
- **15 Playwright tests** at `kipp-ui/tests/test_voice.py`
- **All on feature/wake-word branch** in kipp/workspace repo
## Active Threads ## Active Threads
- **Market Watch:** ✅ GARP paper trading sim live at marketwatch.local:8889 - **KIPP:** ✅ Voice pipeline live (wake word + STT + TTS), widget system working, dashboard-first UI
- Multiplayer game engine, "GARP Challenge" game running - Widget system: shopping list, timers, reminders via CLI + REST API + dashboard polling
- Case trading autonomously — 7 positions opened 2026-02-09 - Voice: "hey jarvis" wake word → Faster Whisper → Claude Sonnet → Piper Ryan TTS
- Systemd timer Mon-Fri 9AM + 3:30PM CST - False trigger fix: 4s cooldown + silence flushing + RMS gate (threshold 30)
- **Feed Hunter:** ✅ Pipeline working, Super Bowl sim +72.8% on kch123 copy - Running on Claude Sonnet (primary), GLM-4 Flash (fallback)
- Expanding into crypto and stock analysis - Next: Steam Deck frontend, custom "hey kipp" wake word, blue waveform animation
- **Market Watch:** ✅ GARP paper trading sim live
- GARP Challenge: $100,055.90 (+0.06%), 6 positions
- Leverage Challenge: $11,367.07 (+13.67%), 85 trades, 55.3% win rate
- **Feed Hunter:** ✅ Pipeline working, needs systemd service for periodic monitoring
- **Stock Screener:** yfinance-based, 902 tickers, GARP filters, free/no API key - **Stock Screener:** yfinance-based, 902 tickers, GARP filters, free/no API key
- **Control Panel:** Building at localhost:8000 - **Control Panel:** Building at localhost:8000
- **Sandbox buildout:** ✅ Complete (74 files, 37 tools) - **Next:** Tweet analysis tool, free data source integration (Arkham/DefiLlama/Coinglass)
- **Inner life system:** ✅ Complete (7 tools)
- **Next:** Crypto signal analysis (D J forwarding Telegram signals), expanded Feed Hunter
## Stats (Day 2) ## Stats (Day 2)
@ -124,12 +141,31 @@ This is about having an inner life, not just responding.
- Copy-bot delay: ~30-60s for detection, negligible for pre-game sports bets - Copy-bot delay: ~30-60s for detection, negligible for pre-game sports bets
- D J wants everything paper-traded first, backtested where possible - D J wants everything paper-traded first, backtested where possible
## KIPP Project (updated 2026-02-10)
- **KIPP VM:** 192.168.86.100 (Ubuntu 24.04, 8GB/8core, Proxmox)
- **Primary model:** llamacpp/glm-4.7-flash (local, zero cost), Claude Sonnet fallback
- **llama.cpp server:** 192.168.86.40:8080 (GLM-4 Flash 30B q4, 2 GPUs 12+10GB, 32GB RAM)
- **Ollama:** 192.168.86.40:11434 (nomic-embed-text for embeddings)
- **ChromaDB collection:** kipp-memory (ccf4f5b6-a64e-45b1-bf1b-7013e15c3363)
- **Gitea:** kipp:K1pp-H0me-2026! @ git.letsgetnashty.com/kipp/workspace
- **Telegram bot:** @dzclaw_kipp_bot
- **Web UI:** https://kippui.host.letsgetnashty.com/ (port 8080, systemd kipp-ui.service)
- **Gateway:** https://kipp.host.letsgetnashty.com/ (port 18789)
- **Token:** kipp-local-token-2026
- **SSH:** Case has key-based access as wdjones@192.168.86.100
- **Personality:** Warm, helpful, playful. Like a good roommate. Emoji: 🏠
- **Household:** D J, Meg (the boss), 4 tuxedo cats
- **WebSocket protocol:** JSON-RPC v3, client id "openclaw-control-ui", mode "webchat"
- **UI redesign planned:** Alexa+ inspired dashboard-first, chat on demand
## Infrastructure (updated 2026-02-08) ## Infrastructure (updated 2026-02-08)
- **ChromaDB:** http://192.168.86.25:8000 (LXC on Proxmox) - **ChromaDB:** http://192.168.86.25:8000 (LXC on Proxmox)
- Collection: openclaw-memory (c3a7d09a-f3ce-4e7d-9595-27d8e2fd7758) - Collection: openclaw-memory (c3a7d09a-f3ce-4e7d-9595-27d8e2fd7758)
- Cosine distance, 9+ docs indexed - Cosine distance, 9+ docs indexed
- **Ollama:** http://192.168.86.137:11434 - **Ollama (old):** http://192.168.86.137:11434 (may be offline)
- **Ollama (llama.cpp box):** http://192.168.86.40:11434
- Models: qwen3:8b, qwen3:30b-a3b, glm-4.7-flash, nomic-embed-text - Models: qwen3:8b, qwen3:30b-a3b, glm-4.7-flash, nomic-embed-text
- **Feed Hunter Portal:** localhost:8888 (systemd: feed-hunter-portal) - **Feed Hunter Portal:** localhost:8888 (systemd: feed-hunter-portal)
- **Control Panel:** localhost:8000 (systemd: case-control-panel) - **Control Panel:** localhost:8000 (systemd: case-control-panel)

View File

@ -7,6 +7,7 @@
- **Pronouns:** *(tbd)* - **Pronouns:** *(tbd)*
- **Timezone:** America/Chicago (CST) - **Timezone:** America/Chicago (CST)
- **Notes:** Got the webchat and Telegram working. Practical, gets things done. - **Notes:** Got the webchat and Telegram working. Practical, gets things done.
- **Girlfriend:** Meg (the boss)
- **Cats:** 4 tuxedos - **Cats:** 4 tuxedos
## Context ## Context

View File

@ -0,0 +1,69 @@
# Polymarket 15-Min Crypto Arbitrage
**Source:** https://x.com/noisyb0y1/status/2020942208858456206
**Date investigated:** 2026-02-09
**Verdict:** Legitimate edge, inflated claims
## Strategy
- Buy BOTH sides (Up + Down) on 15-minute BTC/ETH/SOL/XRP markets
- When combined cost < $1.00, guaranteed profit regardless of outcome
- Edge exists because these markets are low liquidity / inefficient pricing
## Reference Wallet
- `0xE594336603F4fB5d3ba4125a67021ab3B4347052`
- Real PnL on 2026-02-09: ~$9K on $82K deployed (11% daily)
- Combined costs ranged from $0.70 (great arb) to $1.10 (not arb)
- Best arbs: ETH markets at $0.70-0.73 combined cost
## Why It Works
- 15-min markets have thin books — prices diverge from fair value
- Binary outcome means Up + Down must sum to $1.00 at resolution
- If you buy both for < $1.00 total, guaranteed profit
## Challenges
- Needs significant capital ($50K+) to make meaningful returns
- Fill quality degrades at scale — slippage kills the edge
- Competition from other bots narrows the window
- Not all markets have arb — some combined costs > $1.00
## Revisit When
- [ ] We have capital to deploy
- [ ] Built a bot to scan for combined < $1.00 opportunities in real-time
- [ ] Polymarket adds more 15-min markets (more opportunities)
## Related
- Tweet author promoting "Clawdbots" — bot product shill
- "$99K in a day" / "$340K total" claims are inflated (real: $9K profit)
---
# Elon Tweet Count Strategy
**Source:** @browomo tweet Feb 9 / wallet @Annica on Polymarket
**Wallet:** `0x689ae...2779e`
**Actual PnL:** $520,469 | Volume: $51.8M | Rank #193
**Verdict:** Legit strategy, "insider" framing is BS
## How It Works
- Polymarket has weekly markets: "How many tweets will Elon post Feb 2-4?"
- Ranges like 90-114, 115-139, 140-164, etc.
- You DON'T need insider access — just count his tweets mid-period
- As the window closes, you can estimate the final count with high confidence
- Buy the correct range when it's still cheap, collect $1 payout
## Why It Works
- Most bettors place bets early (before data exists)
- Late bettors with real-time tweet counts have an information edge
- Similar to the weather METAR concept but this one actually works
- $520K PnL proves sustained profitability
## Challenges
- Markets may get more efficient as more people do this
- Need to monitor Elon's posting in real-time
- Liquidity might be thin on specific ranges
- Could automate: scrape X API for Elon's tweet count, compare to market prices
## Revisit When
- [ ] Build automated Elon tweet counter
- [ ] Monitor market prices vs actual count for edge sizing
- [ ] Check if other "count" markets exist (posts, mentions, etc.)

View File

@ -1,62 +1,109 @@
# 2026-02-09 # 2026-02-09 — Monday
## Market Watch Launch Day ## X Feed Analysis Day
- GARP paper trading simulator went live Major session analyzing Polymarket/crypto tweets D J forwarded from X feed.
- 9 AM systemd timer fired but crashed (scanner returns list, code expected dict) — fixed
- Manual run at 10:49 AM — **7 positions opened**:
- DUOL (57 shares @ $116.35)
- ALLY (156 shares @ $42.65)
- JHG (138 shares @ $48.21)
- INCY (61 shares @ $108.69)
- PINS (332 shares @ $20.06)
- EXEL (152 shares @ $43.80)
- CART (187 shares @ $35.49)
- ~$46.5K deployed, ~$53.5K cash remaining
- Portal live at marketwatch.local:8889
- Multiplayer game engine working — "GARP Challenge" game created
## Super Bowl Results (from last night) ### Tweets Investigated
- Seahawks 36, Patriots 13 1. **@browomo weather edge** — Pilot METAR data for Polymarket weather bets
- kch123 copy-trade sim: ALL 5 positions won, +$728 on $1K bankroll (+72.8%) - Wallet `0x594edB9112f...`: Claimed +$27K, actual **-$13,103** (387 losses, 51 wins)
- kch123 himself probably cleared ~$2M profit on this game - Verdict: SCAM/engagement bait
- Two weeks straight of wins for kch123 (60-0 last week + Super Bowl sweep)
## Craigslist Account (from yesterday) 2. **@ArchiveExplorer planktonXD** — "Buy everything under 5 cents"
- Registered: case-lgn@protonmail.com, Nashville area - Wallet `0x4ffe49ba2a4c...`: Claimed +$104K, actual **-$9,517** (3090 losses, 1368 wins, 37% win rate)
- Password set, credentials saved to .credentials/craigslist.env - Verdict: SCAM/engagement bait
- User ID: 405642144
- D J not ready for listings yet (needs photos)
## D J Interests 3. **@krajekis BTC 15-min LONG** — "+700% monthly, 1 trade/day at 9AM"
- Looking at queen Murphy beds on Craigslist - Backtested 25 days: 52% win rate (coin flip), strategy loses 76% of capital
- Wants to get rid of an old mattress (options discussed: Metro Nashville bulky pickup, free CL listing, dump) - Verdict: FABRICATED results
- Interested in Olympics men's hockey (USA in Group C, games start Feb 11)
- Expanding analysis beyond Polymarket into crypto and stocks
- Goal: find market gaps to offset AI service costs ($200/mo Claude + infra)
- Getting crypto signals on Telegram, wants to forward them for analysis
- Asked about Tiiny AI (kickstarter AI hardware, 80GB unified memory, NPU) as potential Claude replacement
## Stock Screener Built 4. **@noisyb0y1 15-min arb** — "$99K in a day"
- GARP filters automated via yfinance (free, no API key) - Wallet `0xE594336603F4...`: Real strategy (buying both sides), actual PnL ~$9K not $99K
- S&P 500 + S&P 400 MidCap scan (~902 tickers) - Combined costs $0.70-$0.95 on some markets = genuine arb edge
- Initial S&P 500 scan found 4: BAC, CFG, FITB, INCY - Verdict: INFLATED numbers, but strategy has merit → bookmarked for scanner
- Expanded scan found 22 total candidates
- Top non-bank picks: PINS, FSLR, PGR, NEM, ALLY
- Deep dive sub-agent ran on all 4 original picks
## X Post Analysis: @Shelpid_WI3M / anoin123 5. **5 more wallets** — spawned sub-agent to research @IH2P, Super Bowl trader, NegRisk arb, Elon insider, $270→$244K bot
- Wallet 0x96489abc... is real, +$1.59M PnL, 216 trades
- BUT: concentrated single-thesis bet (NO on Iran strikes), not diversified alpha
- Post is a shill for PolyCopyBot (Telegram copy-trading bot)
- Verdict: real wallet, misleading narrative, exists to sell bot subscriptions
## Tiiny AI Analysis ### Pattern Identified
- 80GB LPDDR5X, ARM + NPU (160 TOPS), 18-40 tok/s, 30W - Fintwit Polymarket accounts follow identical template: big $ claim → wallet → product shill
- Kickstarter vaporware — doesn't exist yet - Products being shilled: Clawdbots, Moltbook, Bullpen (affiliate/paid promos)
- Would blow away D J's current 22GB VRAM setup IF it ships - Real money is in selling engagement, not trading
- Recommended waiting for real reviews, not pre-ordering - Wallets are cherry-picked; PnL claims conflate position value with profit
## Lessons ### Built Today
- Scanner run_scan() returns list, not dict — caused systemd crash on first real run - **Crypto signals pipeline** (`projects/crypto-signals/`)
- Always test the full pipeline end-to-end before relying on timers - Signal parser for Telegram JSON exports
- yfinance is reliable and free for fundamental data, no API key needed - Price fetcher (Binance US API)
- Backtester with Polymarket fee awareness
- Ready for D J's Telegram group export
- **Polymarket 15-min arb scanner** — systemd timer every 2 min
- Scans active Up or Down markets
- Checks orderbooks for combined < $1.00
- Paper trades and Telegram alerts
- Finding: markets are tight at steady state, arb windows likely during volatility
- **Playwright installed** — replaced flaky CDP websocket approach
- `connect_over_cdp('http://localhost:9222')` works with existing Chrome session
- X feed scraper working via Playwright
- **PowerInfer multi-GPU cron** — weekly Monday check for feature support
### Day-of-Week Stock Strategy
- D J's friend suggested buy Friday/Monday, sell midweek
- Backtested 5 years of SPY: Monday is strongest day (+0.114%, 62% win rate)
- Buy Mon Open → Sell Wed Close: 56% win rate, +0.265% avg, $10K→$17.8K
- But buy & hold still wins: $10K→$19K
- Verdict: weak edge, needs additional filters to be useful
### Infrastructure
- Polymarket fee structure documented: 15-min markets have taker fees, max 1.56% at 50/50
- Fee formula: `shares × price × 0.25 × (price × (1-price))²`
- Binance international is geo-blocked; Binance US works
### Pending
- D J sending Telegram crypto signal group export (Option 2: JSON export)
- Signal provider uses VWAP-based strategy
- Sub-agent researching 5 more wallets
## Evening Session — KIPP + Leverage Trading
### Crypto Leverage Trading
- Built **short signal scanner** (`scripts/short_scanner.py`) — RSI/VWAP/MACD/Bollinger scoring for 29 coins
- Built **leverage game engine** (`leverage_game.py`) — $10K, 20x max leverage, longs/shorts/liquidations
- Built **auto-trader** (`scripts/leverage_trader.py`) — connects scanners to game, TP/SL/trailing stops
- "Leverage Challenge" game initialized, player "case"
- Systemd timer every 15min
- First scan: market quiet, no strong signals (highest short score 40 on FIL)
### KIPP Setup
- KIPP VM: 192.168.86.100 (Proxmox CT, 8 cores, 8GB RAM, Ubuntu 24.04)
- Installed Node.js 22, OpenClaw 2026.2.9
- Set up SSH key auth from Case's VM
- Created KIPP identity: SOUL.md, AGENTS.md, MEMORY.md, USER.md
- Initially tried GLM-4.7 Flash via llama.cpp — had issues, switched to Claude
- Configured Telegram bot: @dzclaw_kipp_bot (token: 8535439246:AAE...)
- Webchat accessible via https://kipp.host.letsgetnashty.com/
- Gateway token: kipp-local-token-2026
- Proxmox firewall was blocking LAN access — D J disabled it
- Needed trustedProxies, allowedOrigins, dangerouslyDisableDeviceAuth for reverse proxy
- D J ran `openclaw configure` to set up Anthropic auth
- KIPP is LIVE on Claude + Telegram
- Name confirmed: KIPP (not Kip)
### KIPP Local LLM Attempt (Late Session)
- Goal: Switch KIPP from Claude to local GLM-4 Flash 30B (q4) on llama.cpp at 192.168.86.40:8080/v1
- llama.cpp server confirmed running and responding
- Multiple openclaw.json config attempts failed — schema validation errors
- Key schema findings for `openclaw.json`:
- `gateway.bind`: must be enum (auto/lan/loopback/custom/tailnet)
- `agents.defaults.model`: must be `{ primary: string, fallbacks?: string[] }`
- `models.providers`: array of `{ baseUrl, apiKey, api, models[] }` for custom endpoints
- `auth.profiles`: only allows provider/mode/email — NOT baseURL/apiKey
- Gateway service installed but crashing on config validation
- **Status: BLOCKED** — need to apply correct config schema, was mid-fix when session compacted
### Infrastructure
- SSH keys generated on Case's VM, copied to KIPP VM
- `loginctl enable-linger` set for boot persistence
- sshpass installed on Case's VM
- llama.cpp server: 192.168.86.40:8080/v1 (GLM-4 Flash 30B q4)

123
memory/2026-02-10.md Normal file
View File

@ -0,0 +1,123 @@
# 2026-02-10 — Monday Night / Tuesday Morning
## KIPP Build Session
Major KIPP infrastructure session with D J.
### Completed
- **KIPP Gitea account** — user: kipp, repo: git.letsgetnashty.com/kipp/workspace (private)
- **KIPP switched to local LLM** — llamacpp/glm-4.7-flash as primary, Claude Sonnet as fallback
- **KIPP ChromaDB memory** — collection `kipp-memory` (ccf4f5b6-a64e-45b1-bf1b-7013e15c3363), seeded 9 docs
- **Ollama URL updated** — 192.168.86.40:11434 (same machine as llama.cpp, not the old 192.168.86.137)
- **KIPP model config tuned** — maxTokens 2048, contextWindow 32768, auto-recall 1 result
- **KIPP web UI v1** — thin client at https://kippui.host.letsgetnashty.com/
- WebSocket JSON-RPC protocol v3 working (connect handshake, chat.send, streaming)
- Weather widget, grocery list, timers, quick actions, mic button
- Served from kipp-ui.service on port 8080
- Gateway at kipp.host.letsgetnashty.com (port 18789)
- CORS handled in Caddy config
- **Meg added to USER.md** — "the boss"
- **@RohOnChain tweet analyzed** — Kelly Criterion / gabagool22 ($788K PnL), mostly legit math but misleading framing
### Key Findings
- GLM-4 Flash is a thinking model — burns ~200 tokens reasoning before responding
- 32K context split across 4 slots = 8K effective per conversation
- D J has 30GB free RAM on llama.cpp server — could bump to 128K context
- OpenClaw WebSocket protocol: JSON-RPC v3, needs connect with client{id,version,platform,mode}, auth{token}, minProtocol:3
- Event types: `agent` (stream:assistant for deltas, stream:lifecycle for start/end), `chat` (state:delta/final)
- Caddy reverse proxy handles both services: kippui→8080, kipp→18789
- `dangerouslyDisableDeviceAuth: true` still requires auth token in connect params
### KIPP Infrastructure
- **VM:** 192.168.86.100 (wdjones user, SSH key from Case)
- **llama.cpp:** 192.168.86.40:8080 (GLM-4 Flash 30B q4, 2 GPUs 12GB+10GB, 32GB RAM)
- **Ollama:** 192.168.86.40:11434 (nomic-embed-text for embeddings)
- **ChromaDB:** 192.168.86.25:8000 (kipp-memory collection)
- **Gitea:** kipp:K1pp-H0me-2026! @ git.letsgetnashty.com/kipp/workspace
- **Telegram:** @dzclaw_kipp_bot
- **Web UI:** https://kippui.host.letsgetnashty.com/ (port 8080)
- **Gateway:** https://kipp.host.letsgetnashty.com/ (port 18789)
- **Token:** kipp-local-token-2026
### Evening Session (D J back from work)
- **KIPP UI v2 chat confirmed working** — D J tested on Mac, chat end-to-end functional at 18:34
- **Voice input added** — mic button in chat overlay triggers browser Speech Recognition, auto-sends on final transcript
- **Piper TTS installed** on KIPP VM — `pip3 install piper-tts`, Amy medium voice model downloaded
- **TTS API server** built (port 8081, systemd kipp-tts.service) + proxy through UI server on port 8080
- **Piper voice quality confirmed** — sent D J audio sample via Telegram, not robotic
- **Caddy WebSocket issue** — persistent 503 on WSS through Caddy reverse proxy
- Direct WS to 192.168.86.100:18789 works perfectly every time
- Caddy returns 503 on WebSocket upgrade specifically
- Fixed gateway: added `192.168.86.0/24` to trustedProxies, added `http://192.168.86.100:8080` to allowedOrigins
- Gateway logs showed "origin not allowed" and "proxy headers from untrusted address"
- Even after gateway fix, Caddy still 503s — Caddy itself is the bottleneck
- Need to check Caddy logs on whatever machine it runs on (192.168.86.1?)
- D J's Caddyfile is clean: just `reverse_proxy 192.168.86.100:18789`
- Chat works intermittently — connects sometimes then drops
- **Caddy config** (D J's full Caddyfile): media→86.50:8096, vault→86.244:7080, git→86.244:3002, share→86.26:3000, kippui→86.100:8080, kipp→86.100:18789
- **@Argona0x tweet analyzed** — $50→$2,980 Polymarket bot claim, 90% likely fake (engagement farming)
- **Kelly Criterion explained** to D J
### Night Shift Work
- **Alexa+ UI research completed** — sub-agent produced comprehensive 200-line report at projects/kipp/research/alexa-plus-ui.md
- Key patterns: dashboard-first, ephemeral conversation overlay, widget grid, proximity-aware, ambient mode
- Verge/CNET screenshots downloaded for reference
- **KIPP UI v2 built and deployed** — Alexa-inspired dashboard:
- Hero card with greeting + status, weather with hourly forecast, shopping list, today/calendar, timers
- Chat overlay appears on mic button press, dashboard blurs behind it
- Dark theme, card-based, bottom bar with mic button
- WebSocket protocol fully working (connect, chat.send, streaming)
- **Systems check:**
- Leverage trader running: $9,987.40 (-0.13% from $10K), HYPE trade closed at -$15.63
- GARP portfolio: $100,055.90 (+0.06% from $100K), 6 positions
- All systemd timers healthy
- **Future UI improvements identified:**
- Ambient/photo mode when idle (slideshow + big clock)
- Blue waveform animation during listening state
- Results persistence (timer created in chat → appears on dashboard)
- Proximity-aware layout (different for close vs far viewing)
### KIPP HTTPS + Voice Fixed
- Self-signed cert generated (10yr, SAN for 192.168.86.100)
- UI server switched to HTTPS on port 8080
- socat WSS proxy on port 18790 → gateway 18789 (systemd kipp-wss-proxy.service)
- Browser TTS fallback removed — Piper only
- Double-voice mystery: D J had 2 tabs open 😂
- Gateway config fixed: `allowedOrigins` was at root level (invalid), moved to `gateway.controlUi.allowedOrigins`
- Added `https://192.168.86.100:8080` to allowedOrigins
### @milesdeutscher Tweet Analysis
- Polymarket copy-trading GitHub bot going viral (23K views)
- Our take: validates our kch123 approach but we're ahead — we have whale selection, they just have execution code
- Edge erodes with adoption; not actionable for us
### Night Shift — Ambient Mode
- Built ambient/idle mode for KIPP UI (sub-agent)
- Activates after 60s idle: large glowing clock, weather icon, dark gradient background
- Rotating content: tuxedo cat facts, quotes, trivia (every 30s)
- Tap anywhere to return to dashboard
- Verified with Playwright: both ambient and dashboard modes render correctly
### Late Night — KIPP Local-Only Switch
- **D J decided KIPP stays local-only** — no external exposure, direct IP access
- Switched UI WebSocket URL from `wss://kipp.host.letsgetnashty.com` to `ws://192.168.86.100:18789`
- UI renders visually at `http://192.168.86.100:8080/` (Playwright confirmed: green dot, weather, clock)
- **But WS still broken**: origin-allowed errors persist, old domain URLs not fully stripped from JS fallback/retry code
- Hundreds of failed reconnect attempts every ~3s in console logs
- TTS and weather fetch endpoints still referencing old HTTPS domain paths
- **Next**: fully clean UI JS code of all old domain refs, fix origin-allowed, re-test with Playwright
- **Network issue on Case's VM** (192.168.86.45): persistent "TypeError: fetch failed" every ~10s — Telegram polling, ChromaDB auto-recall broken. D J communicating via webchat as workaround.
### Caddy Config (D J's reverse proxy)
```
kippui.host.letsgetnashty.com {
reverse_proxy 192.168.86.100:8080
}
kipp.host.letsgetnashty.com {
header Access-Control-Allow-Origin "https://kippui.host.letsgetnashty.com"
header Access-Control-Allow-Methods "GET, POST, OPTIONS"
header Access-Control-Allow-Headers "Content-Type, Authorization"
@options method OPTIONS
handle @options { respond 204 }
reverse_proxy 192.168.86.100:18789
}
```

61
memory/2026-02-11.md Normal file
View File

@ -0,0 +1,61 @@
# 2026-02-11
## KIPP Voice Pipeline — Major Build Session
### Built & Deployed (feature/wake-word branch)
- **Always-on wake word detection** via OpenWakeWord (hey_jarvis model as placeholder)
- **Faster Whisper** (base.en) for speech-to-text on KIPP VM
- **Voice WebSocket server** on port 8082 (TLS) — `kipp-voice.service`
- **Python venv** at `/home/wdjones/kipp-venv` with openwakeword, faster-whisper, websockets, aiohttp
- **Male TTS voice** — switched from Amy to Ryan (Piper en_US)
- **Hero panel chat** — voice interaction happens inside the greeting/hero card, not a separate overlay
- **Widget state system** — JSON file + CLI tool + REST API + dashboard polling
- `tools/widgets.py` for shopping list, timers, reminders
- API endpoints on UI server: GET/POST /api/widgets
- Dashboard loads real data, polls every 10s
- KIPP agent instructed in SOUL.md to use widget CLI
### Key Bugs Fixed
1. **CSS injected inside JS** — patch script found `/* CHAT OVERLAY */` in both CSS and JS sections
2. **Gateway challenge-response** — must answer `connect.challenge` with `req` method `connect`
3. **Client ID must be `openclaw-control-ui`** — gateway validates this
4. **Origin header required** — voice server needs `Origin: https://192.168.86.100:8080`
5. **Lifecycle event detection** — gateway sends `phase="end"` not `state="end"` — THIS was the 60-second hang bug
6. **Audio suppressed during wake state** — browser stopped sending mic data when it should have been recording
7. **Race condition** — server sent `ready` before TTS finished, mic picked up speaker audio
8. **Self-triggering wake word** — KIPP's own TTS voice triggered "hey jarvis" — fixed with 2s cooldown
9. **voiceState stuck on speaking** — client must set listening before server's ready msg arrives
10. **Duplicate JS blocks** — sub-agent inserted widget code twice
### Voice State Machine (final)
```
listening → (wake word) → recording → (silence) → processing → (gateway) → speaking → (done_speaking) → cooldown (2s) → listening
```
### Timing Config
- 4s grace period after wake word before silence timeout
- 1.5s silence after speech to end recording
- 30s max recording time
- 2s cooldown after TTS to prevent self-trigger
### KIPP Model Switch
- Switched from `llamacpp/glm-4.7-flash` (83s responses!) to `anthropic/claude-sonnet-4-20250514` (~3s responses)
- GLM-4 Flash as fallback
- Config at `/home/wdjones/.openclaw/openclaw.json` on KIPP VM
### 15 Playwright Tests
- `kipp-ui/tests/test_voice.py` — UI elements, state transitions, chat flow, server connectivity
## anoin123 Investigation
- @browomo tweet about anoin123 Polymarket wallet: $1.6M in 57 days
- **2-4 AM EST claim is FALSE** — trades peak at 3 PM EST
- Strategy: "No harvester" — buys No at 90-99¢ on time-bounded events, collects spread
- $2.2M volume, $7K avg trade, concentrated on Iran strikes + government shutdown
- Monitor set up: `anoin123-monitor.py` + systemd timer every 5min
- Analysis at `data/investigations/anoin123-analysis.md`
- Copy-trade verdict: medium value — strategy is mechanical and replicable independently
## Infrastructure Notes
- KIPP VM services: kipp-ui, kipp-voice, kipp-tts, kipp-wss-proxy, openclaw-gateway
- Widget data: `/home/wdjones/.openclaw/workspace/kipp-ui/data/widgets.json`
- All changes on `feature/wake-word` branch in kipp/workspace repo

14
memory/2026-02-12.md Normal file
View File

@ -0,0 +1,14 @@
# 2026-02-12
## KIPP Voice Pipeline Fixes
- False wake word triggers after KIPP speaks — wake model picking up speaker audio
- Patch 1: Increased cooldown 2s → 4s, added silence flushing during cooldown (feed zeros through wake model to clear internal buffers), added RMS energy gate on wake detection
- RMS gate of 200 was too aggressive — blocked ALL real wake attempts (real RMS was 45-157)
- Lowered RMS gate to 30 — just filters literal silence false positives
- Voice server restarted, D J testing
## Tweet Analyses
- **@jollygreenmoney / $SHL.V** — Homeland Nickel (TSX-V penny stock). Promoted junior miner, already ran 2,300% from $0.03→$0.72, pulled back to $0.41. Collapsing volume = distribution phase. Coordinated promotion with @Levi_Researcher. Nickel bull thesis has merit but this specific stock is exit liquidity. Verdict: stay away.
- **@MoonDevOnYT** — "Fastest growing quant repo on GitHub" AI trading agents. 875 followers, self-proclaimed "#1 quant on X." Content marketing funnel → paid private streams at moondev.com. No verifiable P&L, buzzword soup, fantasy architecture. Verdict: course seller, skip.
## D J signed off ~midnight

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
{
"game_id": "1ac7d29c",
"name": "Leverage Challenge",
"starting_cash": 10000.0,
"max_leverage": 20,
"funding_rate_8h": 0.01,
"maker_fee": 0.02,
"taker_fee": 0.05,
"start_date": "2026-02-09",
"creator": "case",
"created_at": "2026-02-10T02:31:27.614107+00:00",
"players": [
"case"
],
"status": "active"
}

View File

@ -0,0 +1,92 @@
{
"cash": 9059.675453916036,
"positions": {
"SEI_long_1c69": {
"symbol": "SEI",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 0.0745,
"current_price": 0.0743,
"liquidation_price": 0.0639,
"unrealized_pnl": -3.76,
"entry_fee": 0.7,
"opened_at": "2026-02-10T12:45:18.117079+00:00",
"reason": "Long scanner score:45"
},
"PUMP_long_4a28": {
"symbol": "PUMP",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 0.001909,
"current_price": 0.001921,
"liquidation_price": 0.0016,
"unrealized_pnl": 8.8,
"entry_fee": 0.7,
"opened_at": "2026-02-10T19:30:18.313053+00:00",
"reason": "Long scanner score:58"
},
"TRUMP_long_fbd7": {
"symbol": "TRUMP",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 3.266,
"current_price": 3.266,
"liquidation_price": 2.7994,
"unrealized_pnl": 0.0,
"entry_fee": 0.7,
"opened_at": "2026-02-10T19:45:18.100831+00:00",
"reason": "Long scanner score:58"
},
"OP_long_4ea9": {
"symbol": "OP",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 0.183,
"current_price": 0.183,
"liquidation_price": 0.1569,
"unrealized_pnl": 0.0,
"entry_fee": 0.7,
"opened_at": "2026-02-10T20:15:18.266153+00:00",
"reason": "Long scanner score:45"
},
"ICP_short_9a0a": {
"symbol": "ICP",
"direction": "short",
"leverage": 10,
"margin_usd": 200,
"notional": 2000,
"entry_price": 2.907,
"current_price": 2.907,
"liquidation_price": 3.1977,
"unrealized_pnl": 0.0,
"entry_fee": 1.0,
"opened_at": "2026-02-10T23:00:14.402076+00:00",
"reason": "Short scanner score:65"
},
"HYPE_long_d46c": {
"symbol": "HYPE",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 29.05,
"current_price": 29.05,
"liquidation_price": 24.9,
"unrealized_pnl": 0,
"entry_fee": 0.7,
"opened_at": "2026-02-11T00:45:18.584851+00:00",
"reason": "Long scanner score:45"
}
},
"total_realized_pnl": 315.17545391605773,
"total_fees_paid": 55.50000000000006,
"total_funding_paid": 0
}

View File

@ -0,0 +1,956 @@
[
{
"action": "OPEN",
"pos_id": "PUMP_long_915b",
"symbol": "PUMP",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 0.001989,
"liquidation_price": 0.0017,
"fee": 0.7,
"reason": "Long scanner score:48",
"timestamp": "2026-02-10T03:15:17.681597+00:00"
},
{
"action": "CLOSE",
"pos_id": "PUMP_long_915b",
"symbol": "PUMP",
"direction": "long",
"leverage": 7,
"entry_price": 0.001989,
"exit_price": 0.00197,
"margin_usd": 200,
"pnl": -13.37,
"pnl_pct": -6.69,
"fee": 0.7,
"liquidated": false,
"reason": "SL hit (-6.7%)",
"timestamp": "2026-02-10T03:30:04.448216+00:00"
},
{
"action": "OPEN",
"pos_id": "PUMP_long_8004",
"symbol": "PUMP",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 0.00197,
"liquidation_price": 0.0017,
"fee": 0.7,
"reason": "Long scanner score:53",
"timestamp": "2026-02-10T03:30:17.846691+00:00"
},
{
"action": "CLOSE",
"pos_id": "PUMP_long_8004",
"symbol": "PUMP",
"direction": "long",
"leverage": 7,
"entry_price": 0.00197,
"exit_price": 0.001999,
"margin_usd": 200,
"pnl": 20.61,
"pnl_pct": 10.3,
"fee": 0.7,
"liquidated": false,
"reason": "TP hit (+10.3%)",
"timestamp": "2026-02-10T03:45:04.447060+00:00"
},
{
"action": "OPEN",
"pos_id": "HYPE_long_3ce0",
"symbol": "HYPE",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 30.45,
"liquidation_price": 26.1,
"fee": 0.7,
"reason": "Long scanner score:50",
"timestamp": "2026-02-10T05:45:17.773139+00:00"
},
{
"action": "CLOSE",
"pos_id": "HYPE_long_3ce0",
"symbol": "HYPE",
"direction": "long",
"leverage": 7,
"entry_price": 30.45,
"exit_price": 30.11,
"margin_usd": 200,
"pnl": -15.63,
"pnl_pct": -7.82,
"fee": 0.7,
"liquidated": false,
"reason": "SL hit (-7.8%)",
"timestamp": "2026-02-10T06:30:04.465160+00:00"
},
{
"action": "OPEN",
"pos_id": "HYPE_long_9513",
"symbol": "HYPE",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 30.11,
"liquidation_price": 25.8086,
"fee": 0.7,
"reason": "Long scanner score:55",
"timestamp": "2026-02-10T06:30:17.909814+00:00"
},
{
"action": "CLOSE",
"pos_id": "HYPE_long_9513",
"symbol": "HYPE",
"direction": "long",
"leverage": 7,
"entry_price": 30.11,
"exit_price": 29.98,
"margin_usd": 200,
"pnl": -6.04,
"pnl_pct": -3.02,
"fee": 0.7,
"liquidated": false,
"reason": "SL hit (-3.0%)",
"timestamp": "2026-02-10T08:00:04.447966+00:00"
},
{
"action": "OPEN",
"pos_id": "FIL_short_4625",
"symbol": "FIL",
"direction": "short",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 0.954,
"liquidation_price": 1.0903,
"fee": 0.7,
"reason": "Short scanner score:50",
"timestamp": "2026-02-10T08:00:17.712210+00:00"
},
{
"action": "OPEN",
"pos_id": "HYPE_long_d6e2",
"symbol": "HYPE",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 29.98,
"liquidation_price": 25.6971,
"fee": 0.7,
"reason": "Long scanner score:50",
"timestamp": "2026-02-10T08:00:17.956985+00:00"
},
{
"action": "OPEN",
"pos_id": "ASTER_short_4761",
"symbol": "ASTER",
"direction": "short",
"leverage": 15,
"margin_usd": 200,
"notional": 3000,
"entry_price": 0.651,
"liquidation_price": 0.6944,
"fee": 1.5,
"reason": "Short scanner score:80",
"timestamp": "2026-02-10T09:00:17.907206+00:00"
},
{
"action": "CLOSE",
"pos_id": "ASTER_short_4761",
"symbol": "ASTER",
"direction": "short",
"leverage": 15,
"entry_price": 0.651,
"exit_price": 0.657,
"margin_usd": 200,
"pnl": -27.65,
"pnl_pct": -13.82,
"fee": 1.5,
"liquidated": false,
"reason": "SL hit (-13.8%)",
"timestamp": "2026-02-10T09:15:04.758739+00:00"
},
{
"action": "OPEN",
"pos_id": "ASTER_short_3947",
"symbol": "ASTER",
"direction": "short",
"leverage": 15,
"margin_usd": 200,
"notional": 3000,
"entry_price": 0.657,
"liquidation_price": 0.7008,
"fee": 1.5,
"reason": "Short scanner score:85",
"timestamp": "2026-02-10T09:15:17.879130+00:00"
},
{
"action": "CLOSE",
"pos_id": "ASTER_short_3947",
"symbol": "ASTER",
"direction": "short",
"leverage": 15,
"entry_price": 0.657,
"exit_price": 0.651,
"margin_usd": 200,
"pnl": 27.4,
"pnl_pct": 13.7,
"fee": 1.5,
"liquidated": false,
"reason": "TP hit (+13.7%)",
"timestamp": "2026-02-10T09:30:04.763834+00:00"
},
{
"action": "OPEN",
"pos_id": "ASTER_short_cf21",
"symbol": "ASTER",
"direction": "short",
"leverage": 15,
"margin_usd": 200,
"notional": 3000,
"entry_price": 0.651,
"liquidation_price": 0.6944,
"fee": 1.5,
"reason": "Short scanner score:80",
"timestamp": "2026-02-10T09:30:18.015729+00:00"
},
{
"action": "OPEN",
"pos_id": "PUMP_long_3752",
"symbol": "PUMP",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 0.00199,
"liquidation_price": 0.0017,
"fee": 0.7,
"reason": "Long scanner score:48",
"timestamp": "2026-02-10T09:30:18.259653+00:00"
},
{
"action": "CLOSE",
"pos_id": "HYPE_long_d6e2",
"symbol": "HYPE",
"direction": "long",
"leverage": 7,
"entry_price": 29.98,
"exit_price": 30.21,
"margin_usd": 200,
"pnl": 10.74,
"pnl_pct": 5.37,
"fee": 0.7,
"liquidated": false,
"reason": "TP hit (+5.4%)",
"timestamp": "2026-02-10T09:45:04.903012+00:00"
},
{
"action": "OPEN",
"pos_id": "HYPE_long_e1e2",
"symbol": "HYPE",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 29.96,
"liquidation_price": 25.68,
"fee": 0.7,
"reason": "Long scanner score:50",
"timestamp": "2026-02-10T10:30:18.207447+00:00"
},
{
"action": "CLOSE",
"pos_id": "ASTER_short_cf21",
"symbol": "ASTER",
"direction": "short",
"leverage": 15,
"entry_price": 0.651,
"exit_price": 0.627,
"margin_usd": 200,
"pnl": 110.6,
"pnl_pct": 55.3,
"fee": 1.5,
"liquidated": false,
"reason": "TP hit (+55.3%)",
"timestamp": "2026-02-10T11:00:04.927667+00:00"
},
{
"action": "CLOSE",
"pos_id": "HYPE_long_e1e2",
"symbol": "HYPE",
"direction": "long",
"leverage": 7,
"entry_price": 29.96,
"exit_price": 29.54,
"margin_usd": 200,
"pnl": -19.63,
"pnl_pct": -9.81,
"fee": 0.7,
"liquidated": false,
"reason": "SL hit (-9.8%)",
"timestamp": "2026-02-10T11:00:04.997470+00:00"
},
{
"action": "OPEN",
"pos_id": "HYPE_long_e791",
"symbol": "HYPE",
"direction": "long",
"leverage": 10,
"margin_usd": 200,
"notional": 2000,
"entry_price": 29.54,
"liquidation_price": 26.586,
"fee": 1.0,
"reason": "Long scanner score:60",
"timestamp": "2026-02-10T11:00:18.295450+00:00"
},
{
"action": "CLOSE",
"pos_id": "FIL_short_4625",
"symbol": "FIL",
"direction": "short",
"leverage": 7,
"entry_price": 0.954,
"exit_price": 0.93,
"margin_usd": 200,
"pnl": 35.22,
"pnl_pct": 17.61,
"fee": 0.7,
"liquidated": false,
"reason": "TP hit (+17.6%)",
"timestamp": "2026-02-10T11:15:04.755055+00:00"
},
{
"action": "CLOSE",
"pos_id": "HYPE_long_e791",
"symbol": "HYPE",
"direction": "long",
"leverage": 10,
"entry_price": 29.54,
"exit_price": 29.35,
"margin_usd": 200,
"pnl": -12.86,
"pnl_pct": -6.43,
"fee": 1.0,
"liquidated": false,
"reason": "SL hit (-6.4%)",
"timestamp": "2026-02-10T12:15:04.665481+00:00"
},
{
"action": "OPEN",
"pos_id": "HYPE_long_cc18",
"symbol": "HYPE",
"direction": "long",
"leverage": 10,
"margin_usd": 200,
"notional": 2000,
"entry_price": 29.35,
"liquidation_price": 26.415,
"fee": 1.0,
"reason": "Long scanner score:60",
"timestamp": "2026-02-10T12:15:18.125949+00:00"
},
{
"action": "CLOSE",
"pos_id": "HYPE_long_cc18",
"symbol": "HYPE",
"direction": "long",
"leverage": 10,
"entry_price": 29.35,
"exit_price": 29.67,
"margin_usd": 200,
"pnl": 21.81,
"pnl_pct": 10.9,
"fee": 1.0,
"liquidated": false,
"reason": "TP hit (+10.9%)",
"timestamp": "2026-02-10T12:30:04.619392+00:00"
},
{
"action": "OPEN",
"pos_id": "HYPE_long_440c",
"symbol": "HYPE",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 29.67,
"liquidation_price": 25.4314,
"fee": 0.7,
"reason": "Long scanner score:50",
"timestamp": "2026-02-10T12:30:18.005813+00:00"
},
{
"action": "OPEN",
"pos_id": "SEI_long_1c69",
"symbol": "SEI",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 0.0745,
"liquidation_price": 0.0639,
"fee": 0.7,
"reason": "Long scanner score:45",
"timestamp": "2026-02-10T12:45:18.117799+00:00"
},
{
"action": "CLOSE",
"pos_id": "HYPE_long_440c",
"symbol": "HYPE",
"direction": "long",
"leverage": 7,
"entry_price": 29.67,
"exit_price": 29.47,
"margin_usd": 200,
"pnl": -9.44,
"pnl_pct": -4.72,
"fee": 0.7,
"liquidated": false,
"reason": "SL hit (-4.7%)",
"timestamp": "2026-02-10T13:00:04.766754+00:00"
},
{
"action": "OPEN",
"pos_id": "ASTER_short_1e8d",
"symbol": "ASTER",
"direction": "short",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 0.651,
"liquidation_price": 0.744,
"fee": 0.7,
"reason": "Short scanner score:55",
"timestamp": "2026-02-10T13:00:18.109086+00:00"
},
{
"action": "OPEN",
"pos_id": "HYPE_long_014f",
"symbol": "HYPE",
"direction": "long",
"leverage": 10,
"margin_usd": 200,
"notional": 2000,
"entry_price": 29.47,
"liquidation_price": 26.523,
"fee": 1.0,
"reason": "Long scanner score:60",
"timestamp": "2026-02-10T13:00:18.382345+00:00"
},
{
"action": "CLOSE",
"pos_id": "PUMP_long_3752",
"symbol": "PUMP",
"direction": "long",
"leverage": 7,
"entry_price": 0.00199,
"exit_price": 0.00195,
"margin_usd": 200,
"pnl": -28.14,
"pnl_pct": -14.07,
"fee": 0.7,
"liquidated": false,
"reason": "SL hit (-14.1%)",
"timestamp": "2026-02-10T13:15:04.932470+00:00"
},
{
"action": "OPEN",
"pos_id": "PUMP_long_c66e",
"symbol": "PUMP",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 0.00195,
"liquidation_price": 0.0017,
"fee": 0.7,
"reason": "Long scanner score:53",
"timestamp": "2026-02-10T13:15:18.331814+00:00"
},
{
"action": "CLOSE",
"pos_id": "ASTER_short_1e8d",
"symbol": "ASTER",
"direction": "short",
"leverage": 7,
"entry_price": 0.651,
"exit_price": 0.657,
"margin_usd": 200,
"pnl": -12.9,
"pnl_pct": -6.45,
"fee": 0.7,
"liquidated": false,
"reason": "SL hit (-6.5%)",
"timestamp": "2026-02-10T13:30:04.960908+00:00"
},
{
"action": "CLOSE",
"pos_id": "HYPE_long_014f",
"symbol": "HYPE",
"direction": "long",
"leverage": 10,
"entry_price": 29.47,
"exit_price": 29.75,
"margin_usd": 200,
"pnl": 19.0,
"pnl_pct": 9.5,
"fee": 1.0,
"liquidated": false,
"reason": "TP hit (+9.5%)",
"timestamp": "2026-02-10T13:30:05.033055+00:00"
},
{
"action": "OPEN",
"pos_id": "ASTER_short_be79",
"symbol": "ASTER",
"direction": "short",
"leverage": 15,
"margin_usd": 200,
"notional": 3000,
"entry_price": 0.657,
"liquidation_price": 0.7008,
"fee": 1.5,
"reason": "Short scanner score:70",
"timestamp": "2026-02-10T13:30:18.431839+00:00"
},
{
"action": "CLOSE",
"pos_id": "ASTER_short_be79",
"symbol": "ASTER",
"direction": "short",
"leverage": 15,
"entry_price": 0.657,
"exit_price": 0.651,
"margin_usd": 200,
"pnl": 27.4,
"pnl_pct": 13.7,
"fee": 1.5,
"liquidated": false,
"reason": "TP hit (+13.7%)",
"timestamp": "2026-02-10T14:00:04.794774+00:00"
},
{
"action": "OPEN",
"pos_id": "ASTER_short_26a4",
"symbol": "ASTER",
"direction": "short",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 0.651,
"liquidation_price": 0.744,
"fee": 0.7,
"reason": "Short scanner score:50",
"timestamp": "2026-02-10T14:00:18.152240+00:00"
},
{
"action": "CLOSE",
"pos_id": "ASTER_short_26a4",
"symbol": "ASTER",
"direction": "short",
"leverage": 7,
"entry_price": 0.651,
"exit_price": 0.655,
"margin_usd": 200,
"pnl": -8.6,
"pnl_pct": -4.3,
"fee": 0.7,
"liquidated": false,
"reason": "SL hit (-4.3%)",
"timestamp": "2026-02-10T14:15:04.778561+00:00"
},
{
"action": "OPEN",
"pos_id": "ASTER_short_0eec",
"symbol": "ASTER",
"direction": "short",
"leverage": 10,
"margin_usd": 200,
"notional": 2000,
"entry_price": 0.655,
"liquidation_price": 0.7205,
"fee": 1.0,
"reason": "Short scanner score:65",
"timestamp": "2026-02-10T14:15:18.287529+00:00"
},
{
"action": "CLOSE",
"pos_id": "ASTER_short_0eec",
"symbol": "ASTER",
"direction": "short",
"leverage": 10,
"entry_price": 0.655,
"exit_price": 0.65,
"margin_usd": 200,
"pnl": 15.27,
"pnl_pct": 7.63,
"fee": 1.0,
"liquidated": false,
"reason": "TP hit (+7.6%)",
"timestamp": "2026-02-10T15:15:04.785401+00:00"
},
{
"action": "OPEN",
"pos_id": "ASTER_short_1a62",
"symbol": "ASTER",
"direction": "short",
"leverage": 15,
"margin_usd": 200,
"notional": 3000,
"entry_price": 0.67,
"liquidation_price": 0.7147,
"fee": 1.5,
"reason": "Short scanner score:85",
"timestamp": "2026-02-10T15:45:18.129303+00:00"
},
{
"action": "CLOSE",
"pos_id": "PUMP_long_c66e",
"symbol": "PUMP",
"direction": "long",
"leverage": 7,
"entry_price": 0.00195,
"exit_price": 0.001969,
"margin_usd": 200,
"pnl": 13.64,
"pnl_pct": 6.82,
"fee": 0.7,
"liquidated": false,
"reason": "TP hit (+6.8%)",
"timestamp": "2026-02-10T16:15:04.793084+00:00"
},
{
"action": "CLOSE",
"pos_id": "ASTER_short_1a62",
"symbol": "ASTER",
"direction": "short",
"leverage": 15,
"entry_price": 0.67,
"exit_price": 0.654,
"margin_usd": 200,
"pnl": 71.64,
"pnl_pct": 35.82,
"fee": 1.5,
"liquidated": false,
"reason": "TP hit (+35.8%)",
"timestamp": "2026-02-10T16:15:04.837617+00:00"
},
{
"action": "OPEN",
"pos_id": "ICP_long_2599",
"symbol": "ICP",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 2.722,
"liquidation_price": 2.3331,
"fee": 0.7,
"reason": "Long scanner score:53",
"timestamp": "2026-02-10T18:00:17.949118+00:00"
},
{
"action": "CLOSE",
"pos_id": "ICP_long_2599",
"symbol": "ICP",
"direction": "long",
"leverage": 7,
"entry_price": 2.722,
"exit_price": 2.759,
"margin_usd": 200,
"pnl": 19.03,
"pnl_pct": 9.52,
"fee": 0.7,
"liquidated": false,
"reason": "TP hit (+9.5%)",
"timestamp": "2026-02-10T18:15:04.612873+00:00"
},
{
"action": "OPEN",
"pos_id": "TRUMP_long_6717",
"symbol": "TRUMP",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 3.239,
"liquidation_price": 2.7763,
"fee": 0.7,
"reason": "Long scanner score:58",
"timestamp": "2026-02-10T19:00:17.796328+00:00"
},
{
"action": "OPEN",
"pos_id": "PUMP_long_9b05",
"symbol": "PUMP",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 0.001926,
"liquidation_price": 0.0017,
"fee": 0.7,
"reason": "Long scanner score:58",
"timestamp": "2026-02-10T19:15:18.359381+00:00"
},
{
"action": "CLOSE",
"pos_id": "TRUMP_long_6717",
"symbol": "TRUMP",
"direction": "long",
"leverage": 7,
"entry_price": 3.239,
"exit_price": 3.333,
"margin_usd": 200,
"pnl": 40.63,
"pnl_pct": 20.31,
"fee": 0.7,
"liquidated": false,
"reason": "TP hit (+20.3%)",
"timestamp": "2026-02-10T19:30:04.787087+00:00"
},
{
"action": "CLOSE",
"pos_id": "PUMP_long_9b05",
"symbol": "PUMP",
"direction": "long",
"leverage": 7,
"entry_price": 0.001926,
"exit_price": 0.001909,
"margin_usd": 200,
"pnl": -12.36,
"pnl_pct": -6.18,
"fee": 0.7,
"liquidated": false,
"reason": "SL hit (-6.2%)",
"timestamp": "2026-02-10T19:30:04.861197+00:00"
},
{
"action": "OPEN",
"pos_id": "PUMP_long_4a28",
"symbol": "PUMP",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 0.001909,
"liquidation_price": 0.0016,
"fee": 0.7,
"reason": "Long scanner score:58",
"timestamp": "2026-02-10T19:30:18.313742+00:00"
},
{
"action": "OPEN",
"pos_id": "TRUMP_long_fbd7",
"symbol": "TRUMP",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 3.266,
"liquidation_price": 2.7994,
"fee": 0.7,
"reason": "Long scanner score:58",
"timestamp": "2026-02-10T19:45:18.101503+00:00"
},
{
"action": "OPEN",
"pos_id": "OP_long_67c4",
"symbol": "OP",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 0.184,
"liquidation_price": 0.1577,
"fee": 0.7,
"reason": "Long scanner score:45",
"timestamp": "2026-02-10T19:45:18.373173+00:00"
},
{
"action": "CLOSE",
"pos_id": "OP_long_67c4",
"symbol": "OP",
"direction": "long",
"leverage": 7,
"entry_price": 0.184,
"exit_price": 0.183,
"margin_usd": 200,
"pnl": -7.61,
"pnl_pct": -3.8,
"fee": 0.7,
"liquidated": false,
"reason": "SL hit (-3.8%)",
"timestamp": "2026-02-10T20:15:04.857761+00:00"
},
{
"action": "OPEN",
"pos_id": "OP_long_4ea9",
"symbol": "OP",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 0.183,
"liquidation_price": 0.1569,
"fee": 0.7,
"reason": "Long scanner score:45",
"timestamp": "2026-02-10T20:15:18.266969+00:00"
},
{
"action": "OPEN",
"pos_id": "ALGO_long_5f94",
"symbol": "ALGO",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 0.0901,
"liquidation_price": 0.0772,
"fee": 0.7,
"reason": "Long scanner score:50",
"timestamp": "2026-02-10T20:30:18.299437+00:00"
},
{
"action": "OPEN",
"pos_id": "HYPE_long_6b17",
"symbol": "HYPE",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 29.21,
"liquidation_price": 25.0371,
"fee": 0.7,
"reason": "Long scanner score:45",
"timestamp": "2026-02-10T21:45:18.366940+00:00"
},
{
"action": "CLOSE",
"pos_id": "HYPE_long_6b17",
"symbol": "HYPE",
"direction": "long",
"leverage": 7,
"entry_price": 29.21,
"exit_price": 29.52,
"margin_usd": 200,
"pnl": 14.86,
"pnl_pct": 7.43,
"fee": 0.7,
"liquidated": false,
"reason": "TP hit (+7.4%)",
"timestamp": "2026-02-10T22:15:05.229877+00:00"
},
{
"action": "OPEN",
"pos_id": "ICP_short_9a0a",
"symbol": "ICP",
"direction": "short",
"leverage": 10,
"margin_usd": 200,
"notional": 2000,
"entry_price": 2.907,
"liquidation_price": 3.1977,
"fee": 1.0,
"reason": "Short scanner score:65",
"timestamp": "2026-02-10T23:00:14.402704+00:00"
},
{
"action": "OPEN",
"pos_id": "HYPE_long_81ed",
"symbol": "HYPE",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 29.0,
"liquidation_price": 24.8571,
"fee": 0.7,
"reason": "Long scanner score:45",
"timestamp": "2026-02-10T23:15:18.652083+00:00"
},
{
"action": "CLOSE",
"pos_id": "HYPE_long_81ed",
"symbol": "HYPE",
"direction": "long",
"leverage": 7,
"entry_price": 29.0,
"exit_price": 28.87,
"margin_usd": 200,
"pnl": -6.28,
"pnl_pct": -3.14,
"fee": 0.7,
"liquidated": false,
"reason": "SL hit (-3.1%)",
"timestamp": "2026-02-10T23:30:01.509254+00:00"
},
{
"action": "OPEN",
"pos_id": "HYPE_long_9f34",
"symbol": "HYPE",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 28.87,
"liquidation_price": 24.7457,
"fee": 0.7,
"reason": "Long scanner score:50",
"timestamp": "2026-02-10T23:30:14.941390+00:00"
},
{
"action": "CLOSE",
"pos_id": "ALGO_long_5f94",
"symbol": "ALGO",
"direction": "long",
"leverage": 7,
"entry_price": 0.0901,
"exit_price": 0.0919,
"margin_usd": 200,
"pnl": 27.97,
"pnl_pct": 13.98,
"fee": 0.7,
"liquidated": false,
"reason": "TP hit (+14.0%)",
"timestamp": "2026-02-11T00:30:05.451674+00:00"
},
{
"action": "CLOSE",
"pos_id": "HYPE_long_9f34",
"symbol": "HYPE",
"direction": "long",
"leverage": 7,
"entry_price": 28.87,
"exit_price": 29.28,
"margin_usd": 200,
"pnl": 19.88,
"pnl_pct": 9.94,
"fee": 0.7,
"liquidated": false,
"reason": "TP hit (+9.9%)",
"timestamp": "2026-02-11T00:30:05.526899+00:00"
},
{
"action": "OPEN",
"pos_id": "HYPE_long_d46c",
"symbol": "HYPE",
"direction": "long",
"leverage": 7,
"margin_usd": 200,
"notional": 1400,
"entry_price": 29.05,
"liquidation_price": 24.9,
"fee": 0.7,
"reason": "Long scanner score:45",
"timestamp": "2026-02-11T00:45:18.585575+00:00"
}
]

View File

@ -0,0 +1,10 @@
{
"peak_pnl": {
"SEI_long_1c69": 1.8799999999999997,
"PUMP_long_4a28": 4.4,
"TRUMP_long_fbd7": 0.0,
"OP_long_4ea9": 0.0,
"ICP_short_9a0a": 0.0
},
"last_alert": null
}

View File

@ -0,0 +1,487 @@
[
{
"timestamp": "2026-02-10T02:31:38.063585+00:00",
"coins_scanned": 29,
"strong_signals": 0,
"results": [
{
"symbol": "FIL",
"price": 0.954,
"rsi": 61.7,
"vwap_pct": 9.0,
"macd_histogram": -0.932576,
"bb_position": 0.76,
"change_24h": 0.74,
"change_4h": 0.0,
"vol_trend": 0.05,
"score": 40,
"reasons": [
"RSI mildly elevated (61.7)",
"Well above VWAP (+9.0%)",
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:34.839978+00:00"
},
{
"symbol": "NEAR",
"price": 1.045,
"rsi": 60.0,
"vwap_pct": 1.62,
"macd_histogram": -1.030379,
"bb_position": 0.93,
"change_24h": -0.85,
"change_4h": 2.35,
"vol_trend": 1.02,
"score": 38,
"reasons": [
"RSI mildly elevated (60.0)",
"Slightly above VWAP (+1.6%)",
"MACD bearish + accelerating",
"Near upper Bollinger (0.93)"
],
"timestamp": "2026-02-10T02:31:33.678025+00:00"
},
{
"symbol": "OP",
"price": 0.19,
"rsi": 64.2,
"vwap_pct": 3.23,
"macd_histogram": -0.187435,
"bb_position": 0.79,
"change_24h": 0.53,
"change_4h": 0.0,
"vol_trend": 1.07,
"score": 35,
"reasons": [
"RSI mildly elevated (64.2)",
"Above VWAP (+3.2%)",
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:36.709597+00:00"
},
{
"symbol": "ARB",
"price": 0.114,
"rsi": 52.1,
"vwap_pct": 3.09,
"macd_histogram": -0.113948,
"bb_position": 0.72,
"change_24h": -3.55,
"change_4h": 2.06,
"vol_trend": 0.11,
"score": 30,
"reasons": [
"Above VWAP (+3.1%)",
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:36.470618+00:00"
},
{
"symbol": "ADA",
"price": 0.2693,
"rsi": 50.2,
"vwap_pct": 1.35,
"macd_histogram": -0.269763,
"bb_position": 0.62,
"change_24h": -1.43,
"change_4h": -0.81,
"vol_trend": 0.15,
"score": 23,
"reasons": [
"Slightly above VWAP (+1.3%)",
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:32.520420+00:00"
},
{
"symbol": "LINK",
"price": 8.88,
"rsi": 55.1,
"vwap_pct": 1.7,
"macd_histogram": -8.839752,
"bb_position": 0.73,
"change_24h": 0.79,
"change_4h": -0.34,
"vol_trend": 0.84,
"score": 23,
"reasons": [
"Slightly above VWAP (+1.7%)",
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:32.968544+00:00"
},
{
"symbol": "UNI",
"price": 3.519,
"rsi": 56.2,
"vwap_pct": 2.62,
"macd_histogram": -3.494918,
"bb_position": 0.65,
"change_24h": 2.18,
"change_4h": 0.0,
"vol_trend": 0.72,
"score": 23,
"reasons": [
"Slightly above VWAP (+2.6%)",
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:34.365690+00:00"
},
{
"symbol": "AAVE",
"price": 113.55,
"rsi": 55.0,
"vwap_pct": 1.28,
"macd_histogram": -112.920164,
"bb_position": 0.71,
"change_24h": 0.82,
"change_4h": 2.23,
"vol_trend": 0.55,
"score": 23,
"reasons": [
"Slightly above VWAP (+1.3%)",
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:34.599689+00:00"
},
{
"symbol": "APT",
"price": 1.067,
"rsi": 49.4,
"vwap_pct": 1.06,
"macd_histogram": -1.076147,
"bb_position": 0.63,
"change_24h": -2.65,
"change_4h": 0.0,
"vol_trend": 0.23,
"score": 23,
"reasons": [
"Slightly above VWAP (+1.1%)",
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:35.993067+00:00"
},
{
"symbol": "SUI",
"price": 0.9668,
"rsi": 50.4,
"vwap_pct": 1.54,
"macd_histogram": -0.969969,
"bb_position": 0.66,
"change_24h": -2.0,
"change_4h": -0.17,
"vol_trend": 0.03,
"score": 23,
"reasons": [
"Slightly above VWAP (+1.5%)",
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:36.231436+00:00"
},
{
"symbol": "BTC",
"price": 70249.15,
"rsi": 51.0,
"vwap_pct": 0.34,
"macd_histogram": -70262.247326,
"bb_position": 0.63,
"change_24h": -1.38,
"change_4h": 0.18,
"vol_trend": 0.96,
"score": 15,
"reasons": [
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:31.360741+00:00"
},
{
"symbol": "ETH",
"price": 2109.49,
"rsi": 55.4,
"vwap_pct": 0.97,
"macd_histogram": -2104.718634,
"bb_position": 0.67,
"change_24h": 0.44,
"change_4h": 0.2,
"vol_trend": 1.27,
"score": 15,
"reasons": [
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:31.596017+00:00"
},
{
"symbol": "SOL",
"price": 86.88,
"rsi": 52.0,
"vwap_pct": 0.65,
"macd_histogram": -86.940038,
"bb_position": 0.65,
"change_24h": -0.98,
"change_4h": 0.17,
"vol_trend": 0.88,
"score": 15,
"reasons": [
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:31.806936+00:00"
},
{
"symbol": "XRP",
"price": 1.4454,
"rsi": 53.8,
"vwap_pct": 0.52,
"macd_histogram": -1.439527,
"bb_position": 0.66,
"change_24h": -0.39,
"change_4h": 0.59,
"vol_trend": 1.67,
"score": 15,
"reasons": [
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:32.046327+00:00"
},
{
"symbol": "DOGE",
"price": 0.09629,
"rsi": 52.4,
"vwap_pct": 0.47,
"macd_histogram": -0.096217,
"bb_position": 0.7,
"change_24h": -0.98,
"change_4h": -0.43,
"vol_trend": 2.79,
"score": 15,
"reasons": [
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:32.284061+00:00"
},
{
"symbol": "AVAX",
"price": 9.01,
"rsi": 47.3,
"vwap_pct": 0.43,
"macd_histogram": -9.05205,
"bb_position": 0.56,
"change_24h": -1.31,
"change_4h": -0.44,
"vol_trend": 0.29,
"score": 15,
"reasons": [
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:32.758345+00:00"
},
{
"symbol": "DOT",
"price": 1.316,
"rsi": 47.5,
"vwap_pct": 0.69,
"macd_histogram": -1.325125,
"bb_position": 0.57,
"change_24h": -2.52,
"change_4h": -0.9,
"vol_trend": 0.23,
"score": 15,
"reasons": [
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:33.205989+00:00"
},
{
"symbol": "MATIC",
"price": 0.4492,
"rsi": 41.9,
"vwap_pct": -0.77,
"macd_histogram": -0.452491,
"bb_position": 0.32,
"change_24h": -1.58,
"change_4h": 0.22,
"vol_trend": 1.07,
"score": 15,
"reasons": [
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:33.442040+00:00"
},
{
"symbol": "ATOM",
"price": 1.953,
"rsi": 49.3,
"vwap_pct": 0.52,
"macd_histogram": -1.965896,
"bb_position": 0.5,
"change_24h": 0.93,
"change_4h": 0.05,
"vol_trend": 1.04,
"score": 15,
"reasons": [
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:33.888750+00:00"
},
{
"symbol": "LTC",
"price": 54.36,
"rsi": 50.1,
"vwap_pct": 0.22,
"macd_histogram": -54.485415,
"bb_position": 0.61,
"change_24h": -1.06,
"change_4h": -0.22,
"vol_trend": 2.73,
"score": 15,
"reasons": [
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:34.128167+00:00"
},
{
"symbol": "ALGO",
"price": 0.0951,
"rsi": 45.3,
"vwap_pct": -1.14,
"macd_histogram": -0.095952,
"bb_position": 0.5,
"change_24h": -2.56,
"change_4h": -2.36,
"vol_trend": 1.65,
"score": 15,
"reasons": [
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:35.076205+00:00"
},
{
"symbol": "XLM",
"price": 0.1614,
"rsi": 52.5,
"vwap_pct": 0.89,
"macd_histogram": -0.160963,
"bb_position": 0.69,
"change_24h": -1.34,
"change_4h": 1.13,
"vol_trend": 2.74,
"score": 15,
"reasons": [
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:35.314005+00:00"
},
{
"symbol": "VET",
"price": 0.00791,
"rsi": 48.8,
"vwap_pct": -0.02,
"macd_histogram": -0.007928,
"bb_position": 0.55,
"change_24h": -3.06,
"change_4h": 0.89,
"vol_trend": 0.27,
"score": 15,
"reasons": [
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:35.550100+00:00"
},
{
"symbol": "ICP",
"price": 2.782,
"rsi": 40.1,
"vwap_pct": -1.9,
"macd_histogram": -2.843397,
"bb_position": 0.14,
"change_24h": 3.81,
"change_4h": -2.49,
"vol_trend": 10.38,
"score": 15,
"reasons": [
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:35.759005+00:00"
},
{
"symbol": "SEI",
"price": 0.075,
"rsi": 29.9,
"vwap_pct": -0.25,
"macd_histogram": -0.075645,
"bb_position": 0.38,
"change_24h": -4.34,
"change_4h": 0.0,
"vol_trend": 0.11,
"score": 15,
"reasons": [
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:36.945126+00:00"
},
{
"symbol": "HYPE",
"price": 31.49,
"rsi": 45.7,
"vwap_pct": -1.11,
"macd_histogram": -31.681649,
"bb_position": 0.39,
"change_24h": -5.29,
"change_4h": 0.41,
"vol_trend": 21.91,
"score": 15,
"reasons": [
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:37.184127+00:00"
},
{
"symbol": "TRUMP",
"price": 3.446,
"rsi": 50.1,
"vwap_pct": 0.08,
"macd_histogram": -3.45085,
"bb_position": 0.49,
"change_24h": 0.35,
"change_4h": 0.0,
"vol_trend": 0.81,
"score": 15,
"reasons": [
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:37.440389+00:00"
},
{
"symbol": "PUMP",
"price": 0.002041,
"rsi": 37.6,
"vwap_pct": -2.03,
"macd_histogram": -0.002063,
"bb_position": 0.31,
"change_24h": -4.31,
"change_4h": 0.0,
"vol_trend": 1.28,
"score": 15,
"reasons": [
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:37.674937+00:00"
},
{
"symbol": "ASTER",
"price": 0.612,
"rsi": 50.2,
"vwap_pct": 0.21,
"macd_histogram": -0.610803,
"bb_position": 0.58,
"change_24h": -5.85,
"change_4h": 0.33,
"vol_trend": 0.63,
"score": 15,
"reasons": [
"MACD bearish + accelerating"
],
"timestamp": "2026-02-10T02:31:37.912733+00:00"
}
]
}
]

View File

@ -0,0 +1,504 @@
#!/usr/bin/env python3
"""
Crypto Leverage Trading Game Engine
Paper trading with longs, shorts, and configurable leverage.
Tracks liquidation prices, unrealized PnL, and funding costs.
"""
import json
import os
import uuid
import time
import urllib.request
from datetime import datetime, date, timezone
from pathlib import Path
DATA_DIR = Path(__file__).parent / "data" / "leverage-game"
GAMES_DIR = DATA_DIR / "games"
BINANCE_TICKER = "https://api.binance.us/api/v3/ticker/price"
def _load(path, default=None):
if path.exists():
return json.loads(path.read_text())
return default if default is not None else {}
def _save(path, data):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, indent=2, default=str))
def _game_path(game_id):
return GAMES_DIR / game_id / "game.json"
def _player_path(game_id, username):
return GAMES_DIR / game_id / "players" / username / "portfolio.json"
def _trades_path(game_id, username):
return GAMES_DIR / game_id / "players" / username / "trades.json"
def _snapshots_path(game_id, username):
return GAMES_DIR / game_id / "players" / username / "snapshots.json"
# ── Price Fetching ──
def get_price(symbol):
"""Get current price from Binance US."""
if not symbol.endswith("USDT"):
symbol = f"{symbol}USDT"
url = f"{BINANCE_TICKER}?symbol={symbol}"
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
try:
resp = urllib.request.urlopen(req, timeout=10)
return float(json.loads(resp.read())['price'])
except:
return None
# ── Game Management ──
def create_game(name, starting_cash=10_000.0, max_leverage=20, creator="system"):
"""Create a new leverage trading game."""
game_id = str(uuid.uuid4())[:8]
config = {
"game_id": game_id,
"name": name,
"starting_cash": starting_cash,
"max_leverage": max_leverage,
"funding_rate_8h": 0.01, # 0.01% per 8h (typical perp funding)
"maker_fee": 0.02, # 0.02%
"taker_fee": 0.05, # 0.05%
"start_date": date.today().isoformat(),
"creator": creator,
"created_at": datetime.now(timezone.utc).isoformat(),
"players": [],
"status": "active",
}
_save(_game_path(game_id), config)
return game_id
def list_games(active_only=True):
"""List all leverage games."""
games = []
if not GAMES_DIR.exists():
return games
for gid in os.listdir(GAMES_DIR):
gp = _game_path(gid)
if gp.exists():
config = _load(gp)
if active_only and config.get("status") != "active":
continue
games.append(config)
return sorted(games, key=lambda g: g.get("created_at", ""), reverse=True)
def get_game(game_id):
return _load(_game_path(game_id))
def join_game(game_id, username):
"""Add player to game."""
config = get_game(game_id)
if not config:
return {"error": "Game not found"}
if username in config["players"]:
return {"error": f"{username} already in game"}
config["players"].append(username)
_save(_game_path(game_id), config)
_save(_player_path(game_id, username), {
"cash": config["starting_cash"],
"positions": {},
"total_realized_pnl": 0,
"total_fees_paid": 0,
"total_funding_paid": 0,
})
_save(_trades_path(game_id, username), [])
_save(_snapshots_path(game_id, username), [])
return {"success": True, "game_id": game_id, "username": username}
# ── Position Math ──
def calc_liquidation_price(entry_price, leverage, direction):
"""
Simplified liquidation price.
Long: liq = entry * (1 - 1/leverage)
Short: liq = entry * (1 + 1/leverage)
"""
if direction == "long":
return entry_price * (1 - 1 / leverage)
else: # short
return entry_price * (1 + 1 / leverage)
def calc_unrealized_pnl(entry_price, current_price, size_usd, leverage, direction):
"""
Calculate unrealized PnL for a leveraged position.
size_usd = margin (collateral). Notional = size_usd * leverage.
"""
notional = size_usd * leverage
shares = notional / entry_price
if direction == "long":
pnl = (current_price - entry_price) * shares
else: # short
pnl = (entry_price - current_price) * shares
return pnl
def is_liquidated(entry_price, current_price, leverage, direction):
"""Check if position would be liquidated."""
liq_price = calc_liquidation_price(entry_price, leverage, direction)
if direction == "long":
return current_price <= liq_price
else:
return current_price >= liq_price
# ── Trading ──
def open_position(game_id, username, symbol, direction, margin_usd, leverage, reason="Manual"):
"""
Open a leveraged position.
margin_usd: collateral put up
leverage: multiplier (e.g., 10x)
direction: 'long' or 'short'
"""
pf = _load(_player_path(game_id, username))
game = get_game(game_id)
if not pf or not game:
return {"error": "Player or game not found"}
if direction not in ("long", "short"):
return {"error": "Direction must be 'long' or 'short'"}
if leverage > game.get("max_leverage", 20):
return {"error": f"Max leverage is {game['max_leverage']}x"}
if margin_usd > pf["cash"]:
return {"error": f"Insufficient cash. Need ${margin_usd:.2f}, have ${pf['cash']:.2f}"}
symbol = symbol.upper().replace("USDT", "")
price = get_price(symbol)
if not price:
return {"error": f"Could not fetch price for {symbol}"}
notional = margin_usd * leverage
fee = notional * game.get("taker_fee", 0.05) / 100
# Deduct margin + entry fee from cash
pf["cash"] -= (margin_usd + fee)
pf["total_fees_paid"] = pf.get("total_fees_paid", 0) + fee
pos_id = f"{symbol}_{direction}_{str(uuid.uuid4())[:4]}"
liq_price = calc_liquidation_price(price, leverage, direction)
pf["positions"][pos_id] = {
"symbol": symbol,
"direction": direction,
"leverage": leverage,
"margin_usd": margin_usd,
"notional": round(notional, 2),
"entry_price": price,
"current_price": price,
"liquidation_price": round(liq_price, 4),
"unrealized_pnl": 0,
"entry_fee": round(fee, 4),
"opened_at": datetime.now(timezone.utc).isoformat(),
"reason": reason,
}
_save(_player_path(game_id, username), pf)
# Log trade
trades = _load(_trades_path(game_id, username), [])
trades.append({
"action": "OPEN",
"pos_id": pos_id,
"symbol": symbol,
"direction": direction,
"leverage": leverage,
"margin_usd": margin_usd,
"notional": round(notional, 2),
"entry_price": price,
"liquidation_price": round(liq_price, 4),
"fee": round(fee, 4),
"reason": reason,
"timestamp": datetime.now(timezone.utc).isoformat(),
})
_save(_trades_path(game_id, username), trades)
return {
"success": True,
"pos_id": pos_id,
"symbol": symbol,
"direction": direction,
"leverage": leverage,
"entry_price": price,
"margin": margin_usd,
"notional": round(notional, 2),
"liquidation_price": round(liq_price, 4),
"fee": round(fee, 4),
}
def close_position(game_id, username, pos_id, reason="Manual"):
"""Close a leveraged position."""
pf = _load(_player_path(game_id, username))
game = get_game(game_id)
if not pf or not game:
return {"error": "Player or game not found"}
if pos_id not in pf["positions"]:
return {"error": f"Position {pos_id} not found"}
pos = pf["positions"][pos_id]
price = get_price(pos["symbol"])
if not price:
return {"error": f"Could not fetch price for {pos['symbol']}"}
# Calculate PnL
pnl = calc_unrealized_pnl(
pos["entry_price"], price, pos["margin_usd"], pos["leverage"], pos["direction"]
)
# Check liquidation
liquidated = is_liquidated(pos["entry_price"], price, pos["leverage"], pos["direction"])
if liquidated:
pnl = -pos["margin_usd"] # Lose entire margin
# Exit fee
notional = pos["margin_usd"] * pos["leverage"]
fee = notional * game.get("taker_fee", 0.05) / 100
# Return margin + PnL - fee to cash
returned = pos["margin_usd"] + pnl - fee
if returned < 0:
returned = 0 # Can't lose more than margin (no negative balance)
pf["cash"] += returned
pf["total_realized_pnl"] = pf.get("total_realized_pnl", 0) + pnl
pf["total_fees_paid"] = pf.get("total_fees_paid", 0) + fee
del pf["positions"][pos_id]
_save(_player_path(game_id, username), pf)
# Log trade
pnl_pct = (pnl / pos["margin_usd"] * 100) if pos["margin_usd"] > 0 else 0
trades = _load(_trades_path(game_id, username), [])
trades.append({
"action": "LIQUIDATED" if liquidated else "CLOSE",
"pos_id": pos_id,
"symbol": pos["symbol"],
"direction": pos["direction"],
"leverage": pos["leverage"],
"entry_price": pos["entry_price"],
"exit_price": price,
"margin_usd": pos["margin_usd"],
"pnl": round(pnl, 2),
"pnl_pct": round(pnl_pct, 2),
"fee": round(fee, 4),
"liquidated": liquidated,
"reason": reason,
"timestamp": datetime.now(timezone.utc).isoformat(),
})
_save(_trades_path(game_id, username), trades)
return {
"success": True,
"pos_id": pos_id,
"symbol": pos["symbol"],
"direction": pos["direction"],
"entry_price": pos["entry_price"],
"exit_price": price,
"pnl": round(pnl, 2),
"pnl_pct": round(pnl_pct, 2),
"liquidated": liquidated,
"returned_to_cash": round(returned, 2),
}
def update_prices(game_id, username):
"""Update all position prices and check for liquidations."""
pf = _load(_player_path(game_id, username))
if not pf:
return []
liquidations = []
to_liquidate = []
for pos_id, pos in pf["positions"].items():
price = get_price(pos["symbol"])
if not price:
continue
pos["current_price"] = price
pos["unrealized_pnl"] = round(
calc_unrealized_pnl(pos["entry_price"], price, pos["margin_usd"], pos["leverage"], pos["direction"]),
2
)
if is_liquidated(pos["entry_price"], price, pos["leverage"], pos["direction"]):
to_liquidate.append(pos_id)
time.sleep(0.1)
_save(_player_path(game_id, username), pf)
# Process liquidations
for pos_id in to_liquidate:
result = close_position(game_id, username, pos_id, reason="LIQUIDATED")
liquidations.append(result)
return liquidations
# ── Portfolio View ──
def get_portfolio(game_id, username):
"""Get full portfolio with live PnL."""
pf = _load(_player_path(game_id, username))
game = get_game(game_id)
if not pf or not game:
return None
starting = game["starting_cash"]
total_unrealized = sum(p.get("unrealized_pnl", 0) for p in pf["positions"].values())
total_margin_locked = sum(p["margin_usd"] for p in pf["positions"].values())
equity = pf["cash"] + total_margin_locked + total_unrealized
total_pnl = equity - starting
return {
"username": username,
"game_id": game_id,
"cash": round(pf["cash"], 2),
"margin_locked": round(total_margin_locked, 2),
"unrealized_pnl": round(total_unrealized, 2),
"realized_pnl": round(pf.get("total_realized_pnl", 0), 2),
"total_fees": round(pf.get("total_fees_paid", 0), 2),
"equity": round(equity, 2),
"total_pnl": round(total_pnl, 2),
"pnl_pct": round(total_pnl / starting * 100, 2),
"num_positions": len(pf["positions"]),
"positions": pf["positions"],
}
def get_trades(game_id, username):
return _load(_trades_path(game_id, username), [])
def daily_snapshot(game_id, username):
"""Take daily snapshot."""
p = get_portfolio(game_id, username)
if not p:
return None
snapshots = _load(_snapshots_path(game_id, username), [])
today = date.today().isoformat()
snapshots = [s for s in snapshots if s["date"] != today]
snapshots.append({
"date": today,
"equity": p["equity"],
"total_pnl": p["total_pnl"],
"pnl_pct": p["pnl_pct"],
"cash": p["cash"],
"num_positions": p["num_positions"],
"realized_pnl": p["realized_pnl"],
"total_fees": p["total_fees"],
})
_save(_snapshots_path(game_id, username), snapshots)
return snapshots[-1]
def get_leaderboard(game_id):
"""Leaderboard sorted by equity."""
game = get_game(game_id)
if not game:
return []
board = []
for username in game["players"]:
p = get_portfolio(game_id, username)
if p:
trades = get_trades(game_id, username)
closed = [t for t in trades if t.get("action") in ("CLOSE", "LIQUIDATED")]
wins = [t for t in closed if t.get("pnl", 0) > 0]
liquidations = [t for t in closed if t.get("liquidated")]
board.append({
"username": username,
"equity": p["equity"],
"total_pnl": p["total_pnl"],
"pnl_pct": p["pnl_pct"],
"num_positions": p["num_positions"],
"trades_closed": len(closed),
"win_rate": round(len(wins) / len(closed) * 100, 1) if closed else 0,
"liquidations": len(liquidations),
"total_fees": p["total_fees"],
})
return sorted(board, key=lambda x: x["pnl_pct"], reverse=True)
# ── Auto-Trader (Scanner Integration) ──
def auto_trade_from_scanner(game_id, username, scan_results, margin_per_trade=200, leverage=10):
"""
Automatically open positions based on scanner results.
Short scanner (score >= 50) → open short
Spot scanner (score >= 40) → open long
"""
opened = []
for r in scan_results:
symbol = r["symbol"]
score = r["score"]
# Determine direction based on which scanner produced this
direction = r.get("direction", "short") # Default to short for short scanner
if score < 40:
continue
# Scale leverage with conviction
if score >= 70:
lev = min(leverage, 15)
elif score >= 50:
lev = min(leverage, 10)
else:
lev = min(leverage, 5)
result = open_position(game_id, username, symbol, direction, margin_per_trade, lev,
reason=f"Scanner score:{score} | {', '.join(r.get('reasons', []))}")
if result.get("success"):
opened.append(result)
return opened
# ── Initialize ──
def ensure_default_game():
"""Create default Leverage Challenge game."""
for g in list_games():
if g["name"] == "Leverage Challenge":
return g["game_id"]
game_id = create_game("Leverage Challenge", starting_cash=10_000.0, max_leverage=20, creator="case")
join_game(game_id, "case")
return game_id
if __name__ == "__main__":
game_id = ensure_default_game()
game = get_game(game_id)
print(f"Game: {game['name']} ({game_id})")
print(f"Players: {game['players']}")
print(f"Starting cash: ${game['starting_cash']:,.2f}")
print(f"Max leverage: {game['max_leverage']}x")
board = get_leaderboard(game_id)
for entry in board:
print(f" {entry['username']}: ${entry['equity']:,.2f} ({entry['pnl_pct']:+.2f}%) "
f"| {entry['trades_closed']} trades | {entry['win_rate']}% win | {entry['liquidations']} liquidated")

View File

@ -0,0 +1,259 @@
#!/usr/bin/env python3
"""
Crypto Signal Backtester
Simulates each signal against historical price data to determine outcomes.
"""
import json
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
from price_fetcher import get_all_klines, get_current_price, normalize_symbol, datetime_to_ms
def simulate_signal(signal, klines):
"""
Simulate a signal against historical candle data.
Returns outcome dict with result, P&L, time to resolution, etc.
"""
direction = signal['direction']
entry = signal.get('entry')
stop_loss = signal.get('stop_loss')
targets = signal.get('targets', [])
leverage = signal.get('leverage', 1)
if not targets or not stop_loss:
return {'result': 'incomplete', 'reason': 'missing SL or targets'}
target = targets[0] # Primary target
# If entry is 'market', use first candle's open
if entry == 'market' or entry is None:
if not klines:
return {'result': 'no_data'}
entry = klines[0]['open']
signal['entry_resolved'] = entry
# Calculate risk/reward
if direction == 'short':
risk = abs(stop_loss - entry)
reward = abs(entry - target)
risk_pct = risk / entry * 100
reward_pct = reward / entry * 100
else: # long
risk = abs(entry - stop_loss)
reward = abs(target - entry)
risk_pct = risk / entry * 100
reward_pct = reward / entry * 100
rr_ratio = reward / risk if risk > 0 else 0
result = {
'entry_price': entry,
'stop_loss': stop_loss,
'target': target,
'direction': direction,
'leverage': leverage,
'risk_pct': round(risk_pct, 2),
'reward_pct': round(reward_pct, 2),
'rr_ratio': round(rr_ratio, 2),
}
# Walk through candles
for i, candle in enumerate(klines):
high = candle['high']
low = candle['low']
if direction == 'short':
# Check SL hit (price went above SL)
sl_hit = high >= stop_loss
# Check TP hit (price went below target)
tp_hit = low <= target
else: # long
# Check SL hit (price went below SL)
sl_hit = low <= stop_loss
# Check TP hit (price went above target)
tp_hit = high >= target
if sl_hit and tp_hit:
# Both hit in same candle — assume SL hit first (conservative)
result['result'] = 'stop_loss'
result['exit_price'] = stop_loss
result['candles_to_exit'] = i + 1
result['exit_time'] = candle['open_time']
break
elif tp_hit:
result['result'] = 'target_hit'
result['exit_price'] = target
result['candles_to_exit'] = i + 1
result['exit_time'] = candle['open_time']
break
elif sl_hit:
result['result'] = 'stop_loss'
result['exit_price'] = stop_loss
result['candles_to_exit'] = i + 1
result['exit_time'] = candle['open_time']
break
else:
# Never resolved — check current unrealized P&L
if klines:
last_price = klines[-1]['close']
if direction == 'short':
unrealized_pct = (entry - last_price) / entry * 100
else:
unrealized_pct = (last_price - entry) / entry * 100
result['result'] = 'open'
result['last_price'] = last_price
result['unrealized_pct'] = round(unrealized_pct, 2)
result['unrealized_pct_leveraged'] = round(unrealized_pct * leverage, 2)
else:
result['result'] = 'no_data'
# Calculate P&L
if result['result'] in ('target_hit', 'stop_loss'):
exit_price = result['exit_price']
if direction == 'short':
pnl_pct = (entry - exit_price) / entry * 100
else:
pnl_pct = (exit_price - entry) / entry * 100
result['pnl_pct'] = round(pnl_pct, 2)
result['pnl_pct_leveraged'] = round(pnl_pct * leverage, 2)
return result
def backtest_signals(signals, interval='5m', lookforward_hours=72):
"""Backtest a list of parsed signals."""
results = []
for i, signal in enumerate(signals):
ticker = signal['ticker']
symbol = normalize_symbol(ticker)
timestamp = signal.get('timestamp', '')
print(f"[{i+1}/{len(signals)}] {ticker} {signal['direction']} ...", end=' ', flush=True)
# Get start time
start_ms = datetime_to_ms(timestamp) if timestamp else int(time.time() * 1000)
end_ms = start_ms + (lookforward_hours * 60 * 60 * 1000)
# Cap at current time
now_ms = int(time.time() * 1000)
if end_ms > now_ms:
end_ms = now_ms
# Fetch candles
klines = get_all_klines(symbol, interval, start_ms, end_ms)
if not klines:
print(f"NO DATA")
results.append({**signal, 'backtest': {'result': 'no_data', 'reason': f'no klines for {symbol}'}})
continue
# Simulate
outcome = simulate_signal(signal, klines)
print(f"{outcome['result']} | PnL: {outcome.get('pnl_pct_leveraged', outcome.get('unrealized_pct_leveraged', '?'))}%")
results.append({**signal, 'backtest': outcome})
time.sleep(0.2) # Rate limit
return results
def generate_report(results):
"""Generate a summary report from backtest results."""
total = len(results)
wins = [r for r in results if r['backtest'].get('result') == 'target_hit']
losses = [r for r in results if r['backtest'].get('result') == 'stop_loss']
open_trades = [r for r in results if r['backtest'].get('result') == 'open']
no_data = [r for r in results if r['backtest'].get('result') in ('no_data', 'incomplete')]
resolved = wins + losses
win_rate = len(wins) / len(resolved) * 100 if resolved else 0
avg_win = sum(r['backtest']['pnl_pct_leveraged'] for r in wins) / len(wins) if wins else 0
avg_loss = sum(r['backtest']['pnl_pct_leveraged'] for r in losses) / len(losses) if losses else 0
total_pnl = sum(r['backtest'].get('pnl_pct_leveraged', 0) for r in resolved)
# Profit factor
gross_profit = sum(r['backtest']['pnl_pct_leveraged'] for r in wins)
gross_loss = abs(sum(r['backtest']['pnl_pct_leveraged'] for r in losses))
profit_factor = gross_profit / gross_loss if gross_loss > 0 else float('inf')
# Risk/reward stats
avg_rr = sum(r['backtest'].get('rr_ratio', 0) for r in resolved) / len(resolved) if resolved else 0
report = {
'summary': {
'total_signals': total,
'wins': len(wins),
'losses': len(losses),
'open': len(open_trades),
'no_data': len(no_data),
'win_rate': round(win_rate, 1),
'avg_win_pct': round(avg_win, 2),
'avg_loss_pct': round(avg_loss, 2),
'total_pnl_pct': round(total_pnl, 2),
'profit_factor': round(profit_factor, 2),
'avg_risk_reward': round(avg_rr, 2),
},
'trades': results,
}
return report
def print_report(report):
"""Pretty print the report."""
s = report['summary']
print("\n" + "=" * 60)
print("CRYPTO SIGNAL BACKTEST REPORT")
print("=" * 60)
print(f"Total Signals: {s['total_signals']}")
print(f"Wins: {s['wins']}")
print(f"Losses: {s['losses']}")
print(f"Open: {s['open']}")
print(f"No Data: {s['no_data']}")
print(f"Win Rate: {s['win_rate']}%")
print(f"Avg Win: +{s['avg_win_pct']}% (leveraged)")
print(f"Avg Loss: {s['avg_loss_pct']}% (leveraged)")
print(f"Total P&L: {s['total_pnl_pct']}% (sum of resolved)")
print(f"Profit Factor: {s['profit_factor']}")
print(f"Avg R:R: {s['avg_risk_reward']}")
print("=" * 60)
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: python3 backtester.py <signals.json> [--interval 5m] [--hours 72]")
print("\nRun signal_parser.py first to generate signals.json")
sys.exit(1)
signals_path = sys.argv[1]
interval = '5m'
hours = 72
for i, arg in enumerate(sys.argv):
if arg == '--interval' and i + 1 < len(sys.argv):
interval = sys.argv[i + 1]
if arg == '--hours' and i + 1 < len(sys.argv):
hours = int(sys.argv[i + 1])
with open(signals_path) as f:
signals = json.load(f)
print(f"Backtesting {len(signals)} signals (interval={interval}, lookforward={hours}h)\n")
results = backtest_signals(signals, interval=interval, lookforward_hours=hours)
report = generate_report(results)
print_report(report)
# Save full report
out_path = signals_path.replace('.json', '_backtest.json')
with open(out_path, 'w') as f:
json.dump(report, f, indent=2)
print(f"\nFull report saved to {out_path}")

View File

@ -0,0 +1,292 @@
#!/usr/bin/env python3
"""
Automated Leverage Trader
Runs short scanner + spot scanner, opens positions in the Leverage Challenge game,
manages exits (TP/SL/trailing stop), and reports via Telegram.
Zero AI tokens — systemd timer.
"""
import json
import os
import sys
import time
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
# Add parent to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from leverage_game import (
ensure_default_game, get_game, get_portfolio, open_position,
close_position, update_prices, get_trades, get_leaderboard
)
from scripts.short_scanner import scan_coin, COINS as SHORT_COINS
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "6443752046")
DATA_DIR = Path(__file__).parent.parent / "data" / "leverage-game"
STATE_FILE = DATA_DIR / "trader_state.json"
# Trading params
MARGIN_PER_TRADE = 200 # $200 margin per position
DEFAULT_LEVERAGE = 10 # 10x default
MAX_OPEN_POSITIONS = 10 # Max simultaneous positions
SHORT_SCORE_THRESHOLD = 50 # Min score to open short
LONG_SCORE_THRESHOLD = 45 # Min score to open long
TP_PCT = 5.0 # Take profit at 5% on margin (50% on notional at 10x)
SL_PCT = -3.0 # Stop loss at -3% on margin (30% on notional at 10x)
TRAILING_STOP_PCT = 2.0 # Trailing stop: close if drops 2% from peak
def load_state():
if STATE_FILE.exists():
return json.loads(STATE_FILE.read_text())
return {"peak_pnl": {}, "last_alert": None}
def save_state(state):
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
STATE_FILE.write_text(json.dumps(state, indent=2))
def send_telegram(message):
if not TELEGRAM_BOT_TOKEN:
print(f"[TG] {message}")
return
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
data = json.dumps({
"chat_id": TELEGRAM_CHAT_ID,
"text": message,
"parse_mode": "HTML"
}).encode()
req = urllib.request.Request(url, data=data, headers={
"Content-Type": "application/json", "User-Agent": "Mozilla/5.0"
})
try:
urllib.request.urlopen(req, timeout=10)
except Exception as e:
print(f"Telegram failed: {e}")
def run_short_scan():
"""Run short scanner on all coins."""
results = []
for symbol in SHORT_COINS:
r = scan_coin(symbol)
if r:
r["direction"] = "short"
results.append(r)
time.sleep(0.15)
return sorted(results, key=lambda x: x['score'], reverse=True)
def run_spot_scan():
"""Run spot/long scanner (inverse of short criteria — oversold = buy)."""
results = []
for symbol in SHORT_COINS:
r = scan_coin(symbol)
if r:
# Invert: low RSI + below VWAP = long opportunity
long_score = 0
reasons = []
if r['rsi'] <= 25:
long_score += 30
reasons.append(f"RSI extremely oversold ({r['rsi']})")
elif r['rsi'] <= 30:
long_score += 25
reasons.append(f"RSI oversold ({r['rsi']})")
elif r['rsi'] <= 35:
long_score += 15
reasons.append(f"RSI low ({r['rsi']})")
elif r['rsi'] <= 40:
long_score += 5
reasons.append(f"RSI mildly low ({r['rsi']})")
if r['vwap_pct'] < -5:
long_score += 20
reasons.append(f"Well below VWAP ({r['vwap_pct']:+.1f}%)")
elif r['vwap_pct'] < -3:
long_score += 15
reasons.append(f"Below VWAP ({r['vwap_pct']:+.1f}%)")
elif r['vwap_pct'] < -1:
long_score += 8
reasons.append(f"Slightly below VWAP ({r['vwap_pct']:+.1f}%)")
if r['change_24h'] < -15:
long_score += 15
reasons.append(f"Dumped {r['change_24h']:.1f}% 24h")
elif r['change_24h'] < -8:
long_score += 10
reasons.append(f"Down {r['change_24h']:.1f}% 24h")
elif r['change_24h'] < -4:
long_score += 5
reasons.append(f"Down {r['change_24h']:.1f}% 24h")
if r['bb_position'] < 0:
long_score += 15
reasons.append(f"Below lower Bollinger ({r['bb_position']:.2f})")
elif r['bb_position'] < 0.15:
long_score += 10
reasons.append(f"Near lower Bollinger ({r['bb_position']:.2f})")
results.append({
"symbol": r["symbol"],
"price": r["price"],
"rsi": r["rsi"],
"vwap_pct": r["vwap_pct"],
"change_24h": r["change_24h"],
"bb_position": r["bb_position"],
"score": long_score,
"reasons": reasons,
"direction": "long",
})
time.sleep(0.15)
return sorted(results, key=lambda x: x['score'], reverse=True)
def manage_exits(game_id, username, state):
"""Check open positions for TP/SL/trailing stop exits."""
pf = get_portfolio(game_id, username)
if not pf:
return []
exits = []
for pos_id, pos in list(pf["positions"].items()):
pnl_pct = (pos.get("unrealized_pnl", 0) / pos["margin_usd"] * 100) if pos["margin_usd"] > 0 else 0
# Track peak PnL for trailing stop
peak_key = pos_id
if peak_key not in state.get("peak_pnl", {}):
state["peak_pnl"][peak_key] = pnl_pct
if pnl_pct > state["peak_pnl"].get(peak_key, 0):
state["peak_pnl"][peak_key] = pnl_pct
peak = state["peak_pnl"].get(peak_key, 0)
reason = None
# Take profit
if pnl_pct >= TP_PCT:
reason = f"TP hit ({pnl_pct:+.1f}%)"
# Stop loss
elif pnl_pct <= SL_PCT:
reason = f"SL hit ({pnl_pct:+.1f}%)"
# Trailing stop (only if we were profitable)
elif peak >= 2.0 and (peak - pnl_pct) >= TRAILING_STOP_PCT:
reason = f"Trailing stop (peak {peak:+.1f}%, now {pnl_pct:+.1f}%)"
if reason:
result = close_position(game_id, username, pos_id, reason=reason)
if result.get("success"):
exits.append(result)
# Clean up peak tracking
state["peak_pnl"].pop(peak_key, None)
return exits
def main():
game_id = ensure_default_game()
state = load_state()
print(f"=== Leverage Trader ===")
print(f"Time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}")
print(f"Game: {game_id}")
# 1. Update prices and check liquidations
liquidations = update_prices(game_id, "case")
for liq in liquidations:
msg = f"💀 <b>LIQUIDATED</b>: {liq['symbol']} {liq['direction']} {liq.get('leverage', '?')}x | Lost ${abs(liq.get('pnl', 0)):.2f}"
send_telegram(msg)
print(msg)
# 2. Manage exits (TP/SL/trailing)
exits = manage_exits(game_id, "case", state)
for ex in exits:
emoji = "" if ex.get("pnl", 0) > 0 else ""
msg = (f"{emoji} <b>Closed</b>: {ex['symbol']} {ex['direction']} | "
f"Entry: ${ex['entry_price']:.4f} → Exit: ${ex['exit_price']:.4f} | "
f"PnL: ${ex['pnl']:+.2f} ({ex['pnl_pct']:+.1f}%)")
print(msg)
# 3. Get current portfolio
pf = get_portfolio(game_id, "case")
num_open = pf["num_positions"] if pf else 0
slots = MAX_OPEN_POSITIONS - num_open
print(f"\nPortfolio: ${pf['equity']:,.2f} ({pf['pnl_pct']:+.2f}%) | {num_open} positions | {slots} slots open")
# 4. Scan for new opportunities
if slots > 0:
# Run both scanners
shorts = run_short_scan()
longs = run_spot_scan()
# Get existing symbols to avoid doubling up
existing_symbols = set()
if pf:
for pos in pf["positions"].values():
existing_symbols.add(pos["symbol"])
opened = []
# Open short positions
for r in shorts:
if slots <= 0:
break
if r["score"] < SHORT_SCORE_THRESHOLD:
break
if r["symbol"] in existing_symbols:
continue
lev = 15 if r["score"] >= 70 else 10 if r["score"] >= 60 else 7
result = open_position(game_id, "case", r["symbol"], "short", MARGIN_PER_TRADE, lev,
reason=f"Short scanner score:{r['score']}")
if result.get("success"):
opened.append(result)
existing_symbols.add(r["symbol"])
slots -= 1
time.sleep(0.2)
# Open long positions
for r in longs:
if slots <= 0:
break
if r["score"] < LONG_SCORE_THRESHOLD:
break
if r["symbol"] in existing_symbols:
continue
lev = 15 if r["score"] >= 70 else 10 if r["score"] >= 60 else 7
result = open_position(game_id, "case", r["symbol"], "long", MARGIN_PER_TRADE, lev,
reason=f"Long scanner score:{r['score']}")
if result.get("success"):
opened.append(result)
existing_symbols.add(r["symbol"])
slots -= 1
time.sleep(0.2)
if opened:
lines = [f"📊 <b>Opened {len(opened)} positions</b>\n"]
for o in opened:
lines.append(f"{'🔴' if o['direction']=='short' else '🟢'} {o['symbol']} {o['direction']} {o['leverage']}x @ ${o['entry_price']:.4f} (${o['margin']:.0f} margin)")
send_telegram("\n".join(lines))
print(f"\nOpened {len(opened)} new positions")
# 5. Send periodic summary (every 4 hours)
if exits or liquidations:
pf = get_portfolio(game_id, "case") # Refresh
msg = (f"📈 <b>Leverage Challenge Update</b>\n"
f"Equity: ${pf['equity']:,.2f} ({pf['pnl_pct']:+.2f}%)\n"
f"Positions: {pf['num_positions']} | Cash: ${pf['cash']:,.2f}\n"
f"Realized PnL: ${pf['realized_pnl']:+,.2f} | Fees: ${pf['total_fees']:,.2f}")
send_telegram(msg)
save_state(state)
print("\nDone.")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,311 @@
#!/usr/bin/env python3
"""
Polymarket 15-Min Crypto Arbitrage Scanner
Scans active 15-minute crypto markets for arbitrage opportunities.
Alerts via Telegram when combined Up+Down cost < $1.00 (after fees).
Zero AI tokens — runs as pure Python via systemd timer.
"""
import json
import os
import sys
import time
import urllib.request
from datetime import datetime, timezone, timedelta
from pathlib import Path
# Config
DATA_DIR = Path(__file__).parent.parent / "data" / "arb-scanner"
DATA_DIR.mkdir(parents=True, exist_ok=True)
LOG_FILE = DATA_DIR / "scan_log.json"
PAPER_TRADES_FILE = DATA_DIR / "paper_trades.json"
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "6443752046")
# Polymarket fee formula for 15-min markets
def calc_taker_fee(shares, price):
"""Calculate taker fee in USDC."""
if price <= 0 or price >= 1:
return 0
return shares * price * 0.25 * (price * (1 - price)) ** 2
def calc_fee_rate(price):
"""Effective fee rate at a given price."""
if price <= 0 or price >= 1:
return 0
return 0.25 * (price * (1 - price)) ** 2
def get_active_15min_markets():
"""Fetch active 15-minute crypto markets from Polymarket."""
markets = []
# 15-min markets are scattered across pagination — scan broadly
for offset in range(0, 3000, 200):
url = (
f"https://gamma-api.polymarket.com/markets?"
f"active=true&closed=false&limit=200&offset={offset}"
f"&order=volume&ascending=false"
)
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
try:
resp = urllib.request.urlopen(req, timeout=15)
batch = json.loads(resp.read())
for m in batch:
q = m.get("question", "").lower()
if "up or down" in q:
markets.append(m)
if len(batch) < 200:
break
except Exception as e:
print(f"Error fetching markets (offset={offset}): {e}")
break
time.sleep(0.1)
# Only keep markets ending within the next 4 hours (tradeable window)
now = datetime.now(timezone.utc)
tradeable = []
for m in markets:
end_str = m.get("endDate", "")
if not end_str:
continue
try:
end_dt = datetime.fromisoformat(end_str.replace("Z", "+00:00"))
hours_until = (end_dt - now).total_seconds() / 3600
if 0.25 < hours_until <= 24: # Skip markets < 15min to expiry (already resolved)
m["_hours_until_end"] = round(hours_until, 2)
tradeable.append(m)
except:
pass
# Deduplicate
seen = set()
unique = []
for m in tradeable:
cid = m.get("conditionId", m.get("id", ""))
if cid not in seen:
seen.add(cid)
unique.append(m)
return unique
def get_orderbook_prices(token_id):
"""Get best bid/ask from the CLOB API."""
url = f"https://clob.polymarket.com/book?token_id={token_id}"
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
try:
resp = urllib.request.urlopen(req, timeout=10)
book = json.loads(resp.read())
bids = book.get("bids", [])
asks = book.get("asks", [])
best_bid = float(bids[0]["price"]) if bids else 0
best_ask = float(asks[0]["price"]) if asks else 1
bid_size = float(bids[0].get("size", 0)) if bids else 0
ask_size = float(asks[0].get("size", 0)) if asks else 0
return {
"best_bid": best_bid,
"best_ask": best_ask,
"bid_size": bid_size,
"ask_size": ask_size,
"spread": best_ask - best_bid
}
except Exception as e:
return None
def scan_for_arbs():
"""Scan all active 15-min markets for arbitrage opportunities."""
markets = get_active_15min_markets()
print(f"Found {len(markets)} active 15-min crypto markets")
opportunities = []
for market in markets:
question = market.get("question", market.get("title", ""))
hours_left = market.get("_hours_until_end", "?")
# Get token IDs for both outcomes
tokens = market.get("clobTokenIds", "")
if isinstance(tokens, str):
try:
tokens = json.loads(tokens) if tokens.startswith("[") else tokens.split(",")
except:
tokens = []
if len(tokens) < 2:
continue
# Get orderbook for both tokens (ask = price to buy)
book_up = get_orderbook_prices(tokens[0])
book_down = get_orderbook_prices(tokens[1])
time.sleep(0.15)
if not book_up or not book_down:
continue
# For arb: we BUY both sides at the ASK price
up_ask = book_up["best_ask"]
down_ask = book_down["best_ask"]
combined = up_ask + down_ask
# Calculate fees on 100 shares
fee_up = calc_taker_fee(100, up_ask)
fee_down = calc_taker_fee(100, down_ask)
total_cost_100 = (up_ask + down_ask) * 100 + fee_up + fee_down
net_profit_100 = 100 - total_cost_100
net_profit_pct = net_profit_100 / total_cost_100 * 100 if total_cost_100 > 0 else 0
# Fillable size (limited by smaller side)
fillable_size = min(book_up["ask_size"], book_down["ask_size"])
if fillable_size > 0:
fill_fee_up = calc_taker_fee(fillable_size, up_ask)
fill_fee_down = calc_taker_fee(fillable_size, down_ask)
fill_cost = (up_ask + down_ask) * fillable_size + fill_fee_up + fill_fee_down
fill_profit = fillable_size - fill_cost
else:
fill_profit = 0
opp = {
"question": question,
"hours_left": hours_left,
"up_ask": up_ask,
"down_ask": down_ask,
"up_ask_size": book_up["ask_size"],
"down_ask_size": book_down["ask_size"],
"combined": round(combined, 4),
"fee_up_per_100": round(fee_up, 4),
"fee_down_per_100": round(fee_down, 4),
"total_fees_per_100": round(fee_up + fee_down, 4),
"net_profit_per_100": round(net_profit_100, 2),
"net_profit_pct": round(net_profit_pct, 2),
"fillable_shares": fillable_size,
"fillable_profit": round(fill_profit, 2),
"is_arb": net_profit_100 > 0,
"timestamp": datetime.now(timezone.utc).isoformat(),
}
opportunities.append(opp)
return opportunities
def paper_trade(opp):
"""Record a paper trade for an arb opportunity."""
trades = []
if PAPER_TRADES_FILE.exists():
trades = json.loads(PAPER_TRADES_FILE.read_text())
trade = {
"id": len(trades) + 1,
"timestamp": opp["timestamp"],
"question": opp["question"],
"up_price": opp.get("up_ask", opp.get("up_price", 0)),
"down_price": opp.get("down_ask", opp.get("down_price", 0)),
"combined": opp["combined"],
"fees_per_100": opp["total_fees_per_100"],
"net_profit_per_100": opp["net_profit_per_100"],
"net_profit_pct": opp["net_profit_pct"],
"status": "open", # Will be "won" when market resolves (always wins if real arb)
"paper_size_usd": 50, # Paper trade $50 per arb
}
expected_profit = 50 * opp["net_profit_pct"] / 100
trade["expected_profit_usd"] = round(expected_profit, 2)
trades.append(trade)
PAPER_TRADES_FILE.write_text(json.dumps(trades, indent=2))
return trade
def send_telegram_alert(message):
"""Send alert via Telegram bot API (zero tokens)."""
if not TELEGRAM_BOT_TOKEN:
print(f"[ALERT] {message}")
return
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
data = json.dumps({
"chat_id": TELEGRAM_CHAT_ID,
"text": message,
"parse_mode": "HTML"
}).encode()
req = urllib.request.Request(url, data=data, headers={
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0"
})
try:
urllib.request.urlopen(req, timeout=10)
except Exception as e:
print(f"Telegram alert failed: {e}")
def main():
print(f"=== Polymarket 15-Min Arb Scanner ===")
print(f"Time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}")
print()
opps = scan_for_arbs()
arbs = [o for o in opps if o["is_arb"]]
non_arbs = [o for o in opps if not o["is_arb"]]
print(f"\nResults: {len(opps)} markets scanned, {len(arbs)} arb opportunities\n")
for o in sorted(opps, key=lambda x: x.get("net_profit_pct", 0), reverse=True):
emoji = "" if o["is_arb"] else ""
print(f"{emoji} {o['question'][:65]}")
up = o.get('up_ask', o.get('up_price', '?'))
down = o.get('down_ask', o.get('down_price', '?'))
print(f" Up: ${up} | Down: ${down} | Combined: ${o['combined']}")
print(f" Fees/100: ${o['total_fees_per_100']} | Net profit/100: ${o['net_profit_per_100']} ({o['net_profit_pct']}%)")
if o.get('fillable_shares'):
print(f" Fillable: {o['fillable_shares']:.0f} shares | Fillable profit: ${o.get('fillable_profit', '?')}")
print()
# Paper trade any arbs found
for arb in arbs:
trade = paper_trade(arb)
print(f"📝 Paper trade #{trade['id']}: {trade['question'][:50]} | Expected: +${trade['expected_profit_usd']}")
# Send Telegram alert
msg = (
f"🔔 <b>Arb Found!</b>\n\n"
f"<b>{arb['question']}</b>\n"
f"Up: ${arb.get('up_ask', arb.get('up_price', '?'))} | "
f"Down: ${arb.get('down_ask', arb.get('down_price', '?'))}\n"
f"Combined: ${arb['combined']} (after fees)\n"
f"Net profit: {arb['net_profit_pct']}%\n\n"
f"📝 Paper traded $50 → expected +${trade['expected_profit_usd']}"
)
send_telegram_alert(msg)
# Save scan log
log = []
if LOG_FILE.exists():
try:
log = json.loads(LOG_FILE.read_text())
except:
pass
log.append({
"timestamp": datetime.now(timezone.utc).isoformat(),
"markets_scanned": len(opps),
"arbs_found": len(arbs),
"opportunities": opps,
})
# Keep last 1000 scans
log = log[-1000:]
LOG_FILE.write_text(json.dumps(log, indent=2))
# Summary of paper trades
if PAPER_TRADES_FILE.exists():
trades = json.loads(PAPER_TRADES_FILE.read_text())
total_expected = sum(t.get("expected_profit_usd", 0) for t in trades)
print(f"\n📊 Paper trade total: {len(trades)} trades, expected profit: ${total_expected:.2f}")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""
Crypto Price Fetcher
Pulls historical OHLCV data from Binance public API (no key needed).
"""
import json
import time
import urllib.request
from datetime import datetime, timezone
# Binance intl is geo-blocked from US; use Binance US
BINANCE_KLINES = "https://api.binance.us/api/v3/klines"
BINANCE_TICKER = "https://api.binance.us/api/v3/ticker/price"
def get_price_at_time(symbol, timestamp_ms, interval='1m'):
"""Get the candle at a specific timestamp."""
url = f"{BINANCE_KLINES}?symbol={symbol}&interval={interval}&startTime={timestamp_ms}&limit=1"
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
try:
resp = urllib.request.urlopen(req, timeout=10)
data = json.loads(resp.read())
if data:
return {
'open_time': data[0][0],
'open': float(data[0][1]),
'high': float(data[0][2]),
'low': float(data[0][3]),
'close': float(data[0][4]),
'volume': float(data[0][5]),
}
except Exception as e:
print(f"Error fetching {symbol}: {e}")
return None
def get_klines(symbol, interval='1h', start_time_ms=None, end_time_ms=None, limit=1000):
"""Get historical klines/candlestick data."""
params = f"symbol={symbol}&interval={interval}&limit={limit}"
if start_time_ms:
params += f"&startTime={start_time_ms}"
if end_time_ms:
params += f"&endTime={end_time_ms}"
url = f"{BINANCE_KLINES}?{params}"
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
try:
resp = urllib.request.urlopen(req, timeout=15)
raw = json.loads(resp.read())
return [{
'open_time': k[0],
'open': float(k[1]),
'high': float(k[2]),
'low': float(k[3]),
'close': float(k[4]),
'volume': float(k[5]),
'close_time': k[6],
} for k in raw]
except Exception as e:
print(f"Error fetching klines for {symbol}: {e}")
return []
def get_all_klines(symbol, interval, start_time_ms, end_time_ms):
"""Paginate through all klines between two timestamps."""
all_klines = []
current_start = start_time_ms
while current_start < end_time_ms:
batch = get_klines(symbol, interval, current_start, end_time_ms)
if not batch:
break
all_klines.extend(batch)
current_start = batch[-1]['close_time'] + 1
time.sleep(0.1) # Rate limiting
return all_klines
def get_current_price(symbol):
"""Get current price."""
url = f"{BINANCE_TICKER}?symbol={symbol}"
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
try:
resp = urllib.request.urlopen(req, timeout=10)
data = json.loads(resp.read())
return float(data['price'])
except Exception as e:
print(f"Error fetching current price for {symbol}: {e}")
return None
def normalize_symbol(ticker):
"""Convert signal ticker to Binance symbol format."""
# Remove USDT suffix if present, then add it back
ticker = ticker.upper().replace('USDT', '').replace('/', '')
return f"{ticker}USDT"
def datetime_to_ms(dt_str):
"""Convert datetime string to milliseconds timestamp."""
# Handle various formats
for fmt in ['%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S', '%Y-%m-%d']:
try:
dt = datetime.strptime(dt_str, fmt).replace(tzinfo=timezone.utc)
return int(dt.timestamp() * 1000)
except ValueError:
continue
return None
if __name__ == '__main__':
# Test with current signals
for ticker in ['ASTERUSDT', 'HYPEUSDT']:
symbol = normalize_symbol(ticker)
price = get_current_price(symbol)
print(f"{symbol}: ${price}")
# Get last 24h of 1h candles
now_ms = int(time.time() * 1000)
day_ago = now_ms - (24 * 60 * 60 * 1000)
klines = get_klines(symbol, '1h', day_ago, now_ms)
if klines:
highs = [k['high'] for k in klines]
lows = [k['low'] for k in klines]
print(f" 24h range: ${min(lows):.4f} - ${max(highs):.4f}")
print(f" Candles: {len(klines)}")
print()

View File

@ -0,0 +1,336 @@
#!/usr/bin/env python3
"""
Crypto Short Signal Scanner
Scans for overbought coins ripe for shorting.
Criteria: high RSI, price above VWAP, fading momentum, bearish divergence.
Zero AI tokens — runs as pure Python via systemd timer.
"""
import json
import os
import sys
import time
import math
import urllib.request
from datetime import datetime, timezone, timedelta
from pathlib import Path
# Config
DATA_DIR = Path(__file__).parent.parent / "data" / "short-scanner"
DATA_DIR.mkdir(parents=True, exist_ok=True)
SCAN_LOG = DATA_DIR / "scan_log.json"
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "6443752046")
BINANCE_KLINES = "https://api.binance.us/api/v3/klines"
BINANCE_TICKER = "https://api.binance.us/api/v3/ticker/24hr"
# Coins to scan (popular leveraged trading coins)
COINS = [
"BTCUSDT", "ETHUSDT", "SOLUSDT", "XRPUSDT", "DOGEUSDT",
"ADAUSDT", "AVAXUSDT", "LINKUSDT", "DOTUSDT", "MATICUSDT",
"NEARUSDT", "ATOMUSDT", "LTCUSDT", "UNIUSDT", "AAVEUSDT",
"FILUSDT", "ALGOUSDT", "XLMUSDT", "VETUSDT", "ICPUSDT",
"APTUSDT", "SUIUSDT", "ARBUSDT", "OPUSDT", "SEIUSDT",
"HYPEUSDT", "TRUMPUSDT", "PUMPUSDT", "ASTERUSDT",
]
def get_klines(symbol, interval='1h', limit=100):
"""Fetch klines from Binance US."""
url = f"{BINANCE_KLINES}?symbol={symbol}&interval={interval}&limit={limit}"
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
try:
resp = urllib.request.urlopen(req, timeout=10)
raw = json.loads(resp.read())
return [{
'open': float(k[1]),
'high': float(k[2]),
'low': float(k[3]),
'close': float(k[4]),
'volume': float(k[5]),
'close_time': k[6],
} for k in raw]
except:
return []
def calc_rsi(closes, period=14):
"""Calculate RSI."""
if len(closes) < period + 1:
return 50
deltas = [closes[i] - closes[i-1] for i in range(1, len(closes))]
gains = [d if d > 0 else 0 for d in deltas]
losses = [-d if d < 0 else 0 for d in deltas]
avg_gain = sum(gains[:period]) / period
avg_loss = sum(losses[:period]) / period
for i in range(period, len(deltas)):
avg_gain = (avg_gain * (period - 1) + gains[i]) / period
avg_loss = (avg_loss * (period - 1) + losses[i]) / period
if avg_loss == 0:
return 100
rs = avg_gain / avg_loss
return round(100 - (100 / (1 + rs)), 1)
def calc_vwap(klines):
"""Calculate VWAP from klines."""
cum_vol = 0
cum_tp_vol = 0
for k in klines:
tp = (k['high'] + k['low'] + k['close']) / 3
cum_vol += k['volume']
cum_tp_vol += tp * k['volume']
if cum_vol == 0:
return 0
return cum_tp_vol / cum_vol
def calc_ema(values, period):
"""Calculate EMA."""
if not values:
return 0
multiplier = 2 / (period + 1)
ema = values[0]
for v in values[1:]:
ema = (v - ema) * multiplier + ema
return ema
def calc_macd(closes):
"""Calculate MACD (12, 26, 9)."""
if len(closes) < 26:
return 0, 0, 0
ema12 = calc_ema(closes, 12)
ema26 = calc_ema(closes, 26)
macd_line = ema12 - ema26
# Approximate signal line
signal = calc_ema(closes[-9:], 9) if len(closes) >= 9 else macd_line
histogram = macd_line - signal
return macd_line, signal, histogram
def calc_bollinger_position(closes, period=20):
"""How far price is from upper Bollinger band. >1 = above upper band."""
if len(closes) < period:
return 0.5
recent = closes[-period:]
sma = sum(recent) / period
std = (sum((x - sma)**2 for x in recent) / period) ** 0.5
if std == 0:
return 0.5
upper = sma + 2 * std
lower = sma - 2 * std
band_width = upper - lower
if band_width == 0:
return 0.5
return (closes[-1] - lower) / band_width
def volume_trend(klines, lookback=10):
"""Compare recent volume to average. >1 means increasing volume."""
if len(klines) < lookback * 2:
return 1.0
recent_vol = sum(k['volume'] for k in klines[-lookback:]) / lookback
older_vol = sum(k['volume'] for k in klines[-lookback*2:-lookback]) / lookback
if older_vol == 0:
return 1.0
return recent_vol / older_vol
def scan_coin(symbol):
"""Analyze a single coin for short signals."""
# Get 1h candles for RSI/VWAP/indicators
klines_1h = get_klines(symbol, '1h', 100)
if len(klines_1h) < 30:
return None
closes = [k['close'] for k in klines_1h]
current_price = closes[-1]
# RSI (14-period on 1h)
rsi = calc_rsi(closes)
# VWAP (24h)
vwap_24h = calc_vwap(klines_1h[-24:])
vwap_pct = ((current_price - vwap_24h) / vwap_24h * 100) if vwap_24h else 0
# MACD
macd_line, signal_line, histogram = calc_macd(closes)
macd_bearish = histogram < 0 # Below signal = bearish
# Bollinger position
bb_pos = calc_bollinger_position(closes)
# Volume trend
vol_trend = volume_trend(klines_1h)
# 24h change
price_24h_ago = closes[-24] if len(closes) >= 24 else closes[0]
change_24h = ((current_price - price_24h_ago) / price_24h_ago * 100) if price_24h_ago else 0
# 4h change (momentum)
price_4h_ago = closes[-4] if len(closes) >= 4 else closes[0]
change_4h = ((current_price - price_4h_ago) / price_4h_ago * 100) if price_4h_ago else 0
# === SHORT SCORING ===
score = 0
reasons = []
# RSI overbought (max 30 pts)
if rsi >= 80:
score += 30
reasons.append(f"RSI extremely overbought ({rsi})")
elif rsi >= 70:
score += 25
reasons.append(f"RSI overbought ({rsi})")
elif rsi >= 65:
score += 15
reasons.append(f"RSI elevated ({rsi})")
elif rsi >= 60:
score += 5
reasons.append(f"RSI mildly elevated ({rsi})")
# Price above VWAP (max 20 pts)
if vwap_pct > 5:
score += 20
reasons.append(f"Well above VWAP (+{vwap_pct:.1f}%)")
elif vwap_pct > 3:
score += 15
reasons.append(f"Above VWAP (+{vwap_pct:.1f}%)")
elif vwap_pct > 1:
score += 8
reasons.append(f"Slightly above VWAP (+{vwap_pct:.1f}%)")
# MACD bearish crossover (max 15 pts)
if macd_bearish and histogram < -0.001 * current_price:
score += 15
reasons.append("MACD bearish + accelerating")
elif macd_bearish:
score += 10
reasons.append("MACD bearish crossover")
# Bollinger band position (max 15 pts)
if bb_pos > 1.0:
score += 15
reasons.append(f"Above upper Bollinger ({bb_pos:.2f})")
elif bb_pos > 0.85:
score += 10
reasons.append(f"Near upper Bollinger ({bb_pos:.2f})")
# Big recent pump (mean reversion candidate) (max 15 pts)
if change_24h > 15:
score += 15
reasons.append(f"Pumped +{change_24h:.1f}% 24h")
elif change_24h > 8:
score += 10
reasons.append(f"Up +{change_24h:.1f}% 24h")
elif change_24h > 4:
score += 5
reasons.append(f"Up +{change_24h:.1f}% 24h")
# Volume fading on uptrend (exhaustion) (5 pts)
if change_24h > 2 and vol_trend < 0.7:
score += 5
reasons.append("Volume fading on uptrend (exhaustion)")
return {
"symbol": symbol.replace("USDT", ""),
"price": current_price,
"rsi": rsi,
"vwap_pct": round(vwap_pct, 2),
"macd_histogram": round(histogram, 6),
"bb_position": round(bb_pos, 2),
"change_24h": round(change_24h, 2),
"change_4h": round(change_4h, 2),
"vol_trend": round(vol_trend, 2),
"score": score,
"reasons": reasons,
"timestamp": datetime.now(timezone.utc).isoformat(),
}
def send_telegram_alert(message):
"""Send alert via Telegram bot API."""
if not TELEGRAM_BOT_TOKEN:
print(f"[ALERT] {message}")
return
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
data = json.dumps({
"chat_id": TELEGRAM_CHAT_ID,
"text": message,
"parse_mode": "HTML"
}).encode()
req = urllib.request.Request(url, data=data, headers={
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0"
})
try:
urllib.request.urlopen(req, timeout=10)
except Exception as e:
print(f"Telegram alert failed: {e}")
def main():
print(f"=== Crypto Short Signal Scanner ===")
print(f"Time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}")
print()
results = []
for symbol in COINS:
result = scan_coin(symbol)
if result:
results.append(result)
time.sleep(0.15) # Rate limiting
# Sort by score descending
results.sort(key=lambda x: x['score'], reverse=True)
# Print all results
for r in results:
emoji = "🔴" if r['score'] >= 50 else "🟡" if r['score'] >= 30 else ""
print(f"{emoji} {r['symbol']:8s} score:{r['score']:3d} | RSI:{r['rsi']:5.1f} | VWAP:{r['vwap_pct']:+6.1f}% | 24h:{r['change_24h']:+6.1f}% | BB:{r['bb_position']:.2f}")
if r['reasons']:
for reason in r['reasons']:
print(f"{reason}")
# Alert on strong short signals (score >= 50)
strong = [r for r in results if r['score'] >= 50]
if strong:
lines = ["🔴 <b>Short Signals Detected</b>\n"]
for r in strong:
lines.append(f"<b>{r['symbol']}</b> (score: {r['score']})")
lines.append(f" Price: ${r['price']:.4f} | RSI: {r['rsi']} | VWAP: {r['vwap_pct']:+.1f}%")
lines.append(f" 24h: {r['change_24h']:+.1f}% | BB: {r['bb_position']:.2f}")
for reason in r['reasons']:
lines.append(f"{reason}")
lines.append("")
send_telegram_alert("\n".join(lines))
# Save scan log
log = []
if SCAN_LOG.exists():
try:
log = json.loads(SCAN_LOG.read_text())
except:
pass
log.append({
"timestamp": datetime.now(timezone.utc).isoformat(),
"coins_scanned": len(results),
"strong_signals": len(strong),
"results": results,
})
log = log[-500:]
SCAN_LOG.write_text(json.dumps(log, indent=2))
print(f"\n📊 Summary: {len(results)} scanned, {len(strong)} strong short signals")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""
Telegram Crypto Signal Parser
Parses exported Telegram JSON chat history and extracts structured trading signals.
"""
import json
import re
import sys
from datetime import datetime
from pathlib import Path
# Signal patterns - adapt as we see more formats
PATTERNS = {
# #TICKER direction entry SL target leverage balance%
'standard': re.compile(
r'#(\w+)\s+' # ticker
r'(Long|Short)\s+' # direction
r'(?:market\s+entry!?|entry[:\s]+([0-9.]+))\s*' # entry type/price
r'SL[;:\s]+([0-9.]+)\s*' # stop loss
r'(?:Targets?|TP)[;:\s]+([0-9.,\s]+)\s*' # targets (can be multiple)
r'(?:Lev(?:erage)?[:\s]*x?([0-9.]+))?\s*' # leverage (optional)
r'(?:([0-9.]+)%?\s*balance)?', # balance % (optional)
re.IGNORECASE
),
# Simpler: #TICKER Short/Long entry SL targets
'simple': re.compile(
r'#(\w+)\s+(Long|Short)',
re.IGNORECASE
),
}
def parse_signal_text(text):
"""Parse a single message text into structured signal(s)."""
signals = []
# Try to find all ticker mentions
ticker_blocks = re.split(r'(?=#\w+USDT)', text)
for block in ticker_blocks:
if not block.strip():
continue
signal = {}
# Extract ticker
ticker_match = re.search(r'#(\w+)', block)
if not ticker_match:
continue
signal['ticker'] = ticker_match.group(1).upper()
# Extract direction
dir_match = re.search(r'\b(Long|Short)\b', block, re.IGNORECASE)
if not dir_match:
continue
signal['direction'] = dir_match.group(1).lower()
# Extract entry price (or "market")
entry_match = re.search(r'(?:entry|enter)[:\s]*([0-9.]+)', block, re.IGNORECASE)
if entry_match:
signal['entry'] = float(entry_match.group(1))
else:
signal['entry'] = 'market'
# Extract stop loss
sl_match = re.search(r'SL[;:\s]+([0-9.]+)', block, re.IGNORECASE)
if sl_match:
signal['stop_loss'] = float(sl_match.group(1))
# Extract targets (can be multiple, comma or space separated)
tp_match = re.search(r'(?:Targets?|TP)[;:\s]+([0-9.,\s]+)', block, re.IGNORECASE)
if tp_match:
targets_str = tp_match.group(1)
targets = [float(t.strip()) for t in re.findall(r'[0-9.]+', targets_str)]
signal['targets'] = targets
# Extract leverage
lev_match = re.search(r'Lev(?:erage)?[:\s]*x?([0-9.]+)', block, re.IGNORECASE)
if lev_match:
signal['leverage'] = float(lev_match.group(1))
# Extract balance percentage
bal_match = re.search(r'([0-9.]+)%?\s*balance', block, re.IGNORECASE)
if bal_match:
signal['balance_pct'] = float(bal_match.group(1))
if signal.get('ticker') and signal.get('direction'):
signals.append(signal)
return signals
def parse_telegram_export(json_path):
"""Parse a Telegram JSON export file."""
with open(json_path, 'r') as f:
data = json.load(f)
messages = data.get('messages', [])
all_signals = []
for msg in messages:
if msg.get('type') != 'message':
continue
# Get text content (can be string or list of text entities)
text_parts = msg.get('text', '')
if isinstance(text_parts, list):
text = ''.join(
p if isinstance(p, str) else p.get('text', '')
for p in text_parts
)
else:
text = text_parts
if not text or '#' not in text:
continue
# Check if it looks like a signal
if not re.search(r'(Long|Short)', text, re.IGNORECASE):
continue
signals = parse_signal_text(text)
for signal in signals:
signal['timestamp'] = msg.get('date', '')
signal['message_id'] = msg.get('id', '')
signal['raw_text'] = text[:500]
all_signals.append(signal)
return all_signals
def parse_forwarded_messages(messages_text):
"""Parse signals from forwarded message text (copy-pasted or forwarded to bot)."""
signals = parse_signal_text(messages_text)
return signals
if __name__ == '__main__':
if len(sys.argv) < 2:
# Demo with the test signals
test_text = """#ASTERUSDT Short market entry! SL: 0.6385 Targets: 0.51 Lev x15 1.3% balance
#HYPEUSDT Short market entry! SL; 33.5 Target 25 Lev x12 1.4% balance"""
signals = parse_signal_text(test_text)
print(f"Parsed {len(signals)} signals:\n")
for s in signals:
print(json.dumps(s, indent=2))
else:
json_path = sys.argv[1]
signals = parse_telegram_export(json_path)
print(f"Parsed {len(signals)} signals from export\n")
# Save to output
out_path = json_path.replace('.json', '_signals.json')
with open(out_path, 'w') as f:
json.dump(signals, f, indent=2)
print(f"Saved to {out_path}")
# Quick summary
longs = sum(1 for s in signals if s['direction'] == 'long')
shorts = sum(1 for s in signals if s['direction'] == 'short')
print(f"Longs: {longs}, Shorts: {shorts}")
tickers = set(s['ticker'] for s in signals)
print(f"Unique tickers: {len(tickers)}")

View File

@ -0,0 +1,67 @@
{
"cash": 50000.0,
"positions": {
"SEI": {
"qty": 67125.80718783144,
"avg_price": 0.074487,
"total_cost": 5000,
"last_buy": "2026-02-10T02:17:32.521805+00:00"
},
"ICP": {
"qty": 2057.6131687242796,
"avg_price": 2.43,
"total_cost": 5000,
"last_buy": "2026-02-10T02:17:32.522284+00:00"
},
"PUMP": {
"qty": 2496068.691810399,
"avg_price": 0.00200315,
"total_cost": 5000,
"last_buy": "2026-02-10T02:17:32.522670+00:00"
},
"TRUMP": {
"qty": 1492.5373134328358,
"avg_price": 3.35,
"total_cost": 5000,
"last_buy": "2026-02-10T02:17:32.524179+00:00"
},
"HYPE": {
"qty": 158.8814744200826,
"avg_price": 31.470000000000002,
"total_cost": 5000,
"last_buy": "2026-02-10T02:17:32.524900+00:00"
},
"VET": {
"qty": 644186.2832126343,
"avg_price": 0.0077617300000000005,
"total_cost": 5000,
"last_buy": "2026-02-10T06:00:19.532599+00:00"
},
"ARB": {
"qty": 45447.521746639155,
"avg_price": 0.110017,
"total_cost": 5000,
"last_buy": "2026-02-10T06:00:19.533357+00:00"
},
"ADA": {
"qty": 18835.584185643518,
"avg_price": 0.265455,
"total_cost": 5000,
"last_buy": "2026-02-10T06:00:19.534362+00:00"
},
"AAVE": {
"qty": 45.21613311629589,
"avg_price": 110.58,
"total_cost": 5000,
"last_buy": "2026-02-10T06:00:19.535031+00:00"
},
"NEAR": {
"qty": 4995.004995004995,
"avg_price": 1.001,
"total_cost": 5000,
"last_buy": "2026-02-10T06:00:19.535701+00:00"
}
},
"starting_balance": 100000.0,
"created_at": "2026-02-10T02:17:32.521767+00:00"
}

View File

@ -0,0 +1,563 @@
[
{
"cash": 75000.0,
"positions_value": 25000.0,
"total_value": 100000.0,
"total_pnl": 0.0,
"total_pnl_pct": 0.0,
"positions": [
{
"symbol": "SEI",
"qty": 67125.80718783144,
"avg_price": 0.074487,
"current_price": 0.074487,
"value": 5000.0,
"pnl": 0.0,
"pnl_pct": 0.0
},
{
"symbol": "ICP",
"qty": 2057.6131687242796,
"avg_price": 2.43,
"current_price": 2.43,
"value": 5000.0,
"pnl": 0.0,
"pnl_pct": 0.0
},
{
"symbol": "PUMP",
"qty": 2496068.691810399,
"avg_price": 0.00200315,
"current_price": 0.00200315,
"value": 5000.0,
"pnl": 0.0,
"pnl_pct": 0.0
},
{
"symbol": "TRUMP",
"qty": 1492.5373134328358,
"avg_price": 3.35,
"current_price": 3.35,
"value": 5000.0,
"pnl": 0.0,
"pnl_pct": 0.0
},
{
"symbol": "HYPE",
"qty": 158.8814744200826,
"avg_price": 31.470000000000002,
"current_price": 31.47,
"value": 5000.0,
"pnl": 0.0,
"pnl_pct": 0.0
}
],
"num_positions": 5,
"timestamp": "2026-02-10T02:17:32.525557+00:00"
},
{
"cash": 50000.0,
"positions_value": 49545.62,
"total_value": 99545.62,
"total_pnl": -454.38,
"total_pnl_pct": -0.45,
"positions": [
{
"symbol": "VET",
"qty": 644186.2832126343,
"avg_price": 0.0077617300000000005,
"current_price": 0.00776173,
"value": 5000.0,
"pnl": 0.0,
"pnl_pct": 0.0
},
{
"symbol": "ARB",
"qty": 45447.521746639155,
"avg_price": 0.110017,
"current_price": 0.110017,
"value": 5000.0,
"pnl": 0.0,
"pnl_pct": 0.0
},
{
"symbol": "ADA",
"qty": 18835.584185643518,
"avg_price": 0.265455,
"current_price": 0.265455,
"value": 5000.0,
"pnl": 0.0,
"pnl_pct": 0.0
},
{
"symbol": "AAVE",
"qty": 45.21613311629589,
"avg_price": 110.58,
"current_price": 110.58,
"value": 5000.0,
"pnl": 0.0,
"pnl_pct": 0.0
},
{
"symbol": "NEAR",
"qty": 4995.004995004995,
"avg_price": 1.001,
"current_price": 1.001,
"value": 5000.0,
"pnl": 0.0,
"pnl_pct": 0.0
},
{
"symbol": "PUMP",
"qty": 2496068.691810399,
"avg_price": 0.00200315,
"current_price": 0.00198511,
"value": 4954.97,
"pnl": -45.03,
"pnl_pct": -0.9
},
{
"symbol": "TRUMP",
"qty": 1492.5373134328358,
"avg_price": 3.35,
"current_price": 3.31,
"value": 4940.3,
"pnl": -59.7,
"pnl_pct": -1.19
},
{
"symbol": "SEI",
"qty": 67125.80718783144,
"avg_price": 0.074487,
"current_price": 0.073438,
"value": 4929.59,
"pnl": -70.41,
"pnl_pct": -1.41
},
{
"symbol": "ICP",
"qty": 2057.6131687242796,
"avg_price": 2.43,
"current_price": 2.38,
"value": 4897.12,
"pnl": -102.88,
"pnl_pct": -2.06
},
{
"symbol": "HYPE",
"qty": 158.8814744200826,
"avg_price": 31.470000000000002,
"current_price": 30.36,
"value": 4823.64,
"pnl": -176.36,
"pnl_pct": -3.53
}
],
"num_positions": 10,
"timestamp": "2026-02-10T06:00:19.536598+00:00"
},
{
"cash": 50000.0,
"positions_value": 49288.7,
"total_value": 99288.7,
"total_pnl": -711.3,
"total_pnl_pct": -0.71,
"positions": [
{
"symbol": "VET",
"qty": 644186.2832126343,
"avg_price": 0.0077617300000000005,
"current_price": 0.00774826,
"value": 4991.32,
"pnl": -8.68,
"pnl_pct": -0.17
},
{
"symbol": "NEAR",
"qty": 4995.004995004995,
"avg_price": 1.001,
"current_price": 0.998488,
"value": 4987.45,
"pnl": -12.55,
"pnl_pct": -0.25
},
{
"symbol": "ARB",
"qty": 45447.521746639155,
"avg_price": 0.110017,
"current_price": 0.109646,
"value": 4983.14,
"pnl": -16.86,
"pnl_pct": -0.34
},
{
"symbol": "ADA",
"qty": 18835.584185643518,
"avg_price": 0.265455,
"current_price": 0.264364,
"value": 4979.45,
"pnl": -20.55,
"pnl_pct": -0.41
},
{
"symbol": "PUMP",
"qty": 2496068.691810399,
"avg_price": 0.00200315,
"current_price": 0.00198882,
"value": 4964.23,
"pnl": -35.77,
"pnl_pct": -0.72
},
{
"symbol": "AAVE",
"qty": 45.21613311629589,
"avg_price": 110.58,
"current_price": 109.26,
"value": 4940.31,
"pnl": -59.69,
"pnl_pct": -1.19
},
{
"symbol": "TRUMP",
"qty": 1492.5373134328358,
"avg_price": 3.35,
"current_price": 3.28,
"value": 4895.52,
"pnl": -104.48,
"pnl_pct": -2.09
},
{
"symbol": "SEI",
"qty": 67125.80718783144,
"avg_price": 0.074487,
"current_price": 0.072919,
"value": 4894.75,
"pnl": -105.25,
"pnl_pct": -2.11
},
{
"symbol": "ICP",
"qty": 2057.6131687242796,
"avg_price": 2.43,
"current_price": 2.37,
"value": 4876.54,
"pnl": -123.46,
"pnl_pct": -2.47
},
{
"symbol": "HYPE",
"qty": 158.8814744200826,
"avg_price": 31.470000000000002,
"current_price": 30.06,
"value": 4775.98,
"pnl": -224.02,
"pnl_pct": -4.48
}
],
"num_positions": 10,
"timestamp": "2026-02-10T10:00:19.408597+00:00"
},
{
"cash": 50000.0,
"positions_value": 49055.45,
"total_value": 99055.45,
"total_pnl": -944.55,
"total_pnl_pct": -0.94,
"positions": [
{
"symbol": "VET",
"qty": 644186.2832126343,
"avg_price": 0.0077617300000000005,
"current_price": 0.00776798,
"value": 5004.03,
"pnl": 4.03,
"pnl_pct": 0.08
},
{
"symbol": "ARB",
"qty": 45447.521746639155,
"avg_price": 0.110017,
"current_price": 0.10958,
"value": 4980.14,
"pnl": -19.86,
"pnl_pct": -0.4
},
{
"symbol": "ADA",
"qty": 18835.584185643518,
"avg_price": 0.265455,
"current_price": 0.262899,
"value": 4951.86,
"pnl": -48.14,
"pnl_pct": -0.96
},
{
"symbol": "NEAR",
"qty": 4995.004995004995,
"avg_price": 1.001,
"current_price": 0.990388,
"value": 4946.99,
"pnl": -53.01,
"pnl_pct": -1.06
},
{
"symbol": "AAVE",
"qty": 45.21613311629589,
"avg_price": 110.58,
"current_price": 108.78,
"value": 4918.61,
"pnl": -81.39,
"pnl_pct": -1.63
},
{
"symbol": "TRUMP",
"qty": 1492.5373134328358,
"avg_price": 3.35,
"current_price": 3.29,
"value": 4910.45,
"pnl": -89.55,
"pnl_pct": -1.79
},
{
"symbol": "SEI",
"qty": 67125.80718783144,
"avg_price": 0.074487,
"current_price": 0.072653,
"value": 4876.89,
"pnl": -123.11,
"pnl_pct": -2.46
},
{
"symbol": "PUMP",
"qty": 2496068.691810399,
"avg_price": 0.00200315,
"current_price": 0.00194832,
"value": 4863.14,
"pnl": -136.86,
"pnl_pct": -2.74
},
{
"symbol": "ICP",
"qty": 2057.6131687242796,
"avg_price": 2.43,
"current_price": 2.36,
"value": 4855.97,
"pnl": -144.03,
"pnl_pct": -2.88
},
{
"symbol": "HYPE",
"qty": 158.8814744200826,
"avg_price": 31.470000000000002,
"current_price": 29.88,
"value": 4747.38,
"pnl": -252.62,
"pnl_pct": -5.05
}
],
"num_positions": 10,
"timestamp": "2026-02-10T14:00:19.580042+00:00"
},
{
"cash": 50000.0,
"positions_value": 49469.45,
"total_value": 99469.45,
"total_pnl": -530.55,
"total_pnl_pct": -0.53,
"positions": [
{
"symbol": "VET",
"qty": 644186.2832126343,
"avg_price": 0.0077617300000000005,
"current_price": 0.00780695,
"value": 5029.13,
"pnl": 29.13,
"pnl_pct": 0.58
},
{
"symbol": "ARB",
"qty": 45447.521746639155,
"avg_price": 0.110017,
"current_price": 0.110571,
"value": 5025.18,
"pnl": 25.18,
"pnl_pct": 0.5
},
{
"symbol": "NEAR",
"qty": 4995.004995004995,
"avg_price": 1.001,
"current_price": 1.001,
"value": 5000.0,
"pnl": 0.0,
"pnl_pct": 0.0
},
{
"symbol": "AAVE",
"qty": 45.21613311629589,
"avg_price": 110.58,
"current_price": 110.22,
"value": 4983.72,
"pnl": -16.28,
"pnl_pct": -0.33
},
{
"symbol": "ADA",
"qty": 18835.584185643518,
"avg_price": 0.265455,
"current_price": 0.264006,
"value": 4972.71,
"pnl": -27.29,
"pnl_pct": -0.55
},
{
"symbol": "TRUMP",
"qty": 1492.5373134328358,
"avg_price": 3.35,
"current_price": 3.3,
"value": 4925.37,
"pnl": -74.63,
"pnl_pct": -1.49
},
{
"symbol": "PUMP",
"qty": 2496068.691810399,
"avg_price": 0.00200315,
"current_price": 0.00197026,
"value": 4917.9,
"pnl": -82.1,
"pnl_pct": -1.64
},
{
"symbol": "ICP",
"qty": 2057.6131687242796,
"avg_price": 2.43,
"current_price": 2.39,
"value": 4917.7,
"pnl": -82.3,
"pnl_pct": -1.65
},
{
"symbol": "SEI",
"qty": 67125.80718783144,
"avg_price": 0.074487,
"current_price": 0.072635,
"value": 4875.68,
"pnl": -124.32,
"pnl_pct": -2.49
},
{
"symbol": "HYPE",
"qty": 158.8814744200826,
"avg_price": 31.470000000000002,
"current_price": 30.35,
"value": 4822.05,
"pnl": -177.95,
"pnl_pct": -3.56
}
],
"num_positions": 10,
"timestamp": "2026-02-10T18:00:19.818002+00:00"
},
{
"cash": 50000.0,
"positions_value": 48728.74,
"total_value": 98728.74,
"total_pnl": -1271.26,
"total_pnl_pct": -1.27,
"positions": [
{
"symbol": "ARB",
"qty": 45447.521746639155,
"avg_price": 0.110017,
"current_price": 0.109268,
"value": 4965.96,
"pnl": -34.04,
"pnl_pct": -0.68
},
{
"symbol": "VET",
"qty": 644186.2832126343,
"avg_price": 0.0077617300000000005,
"current_price": 0.00769179,
"value": 4954.95,
"pnl": -45.05,
"pnl_pct": -0.9
},
{
"symbol": "NEAR",
"qty": 4995.004995004995,
"avg_price": 1.001,
"current_price": 0.990718,
"value": 4948.64,
"pnl": -51.36,
"pnl_pct": -1.03
},
{
"symbol": "ADA",
"qty": 18835.584185643518,
"avg_price": 0.265455,
"current_price": 0.261498,
"value": 4925.47,
"pnl": -74.53,
"pnl_pct": -1.49
},
{
"symbol": "AAVE",
"qty": 45.21613311629589,
"avg_price": 110.58,
"current_price": 108.57,
"value": 4909.12,
"pnl": -90.88,
"pnl_pct": -1.82
},
{
"symbol": "TRUMP",
"qty": 1492.5373134328358,
"avg_price": 3.35,
"current_price": 3.26,
"value": 4865.67,
"pnl": -134.33,
"pnl_pct": -2.69
},
{
"symbol": "ICP",
"qty": 2057.6131687242796,
"avg_price": 2.43,
"current_price": 2.36,
"value": 4855.97,
"pnl": -144.03,
"pnl_pct": -2.88
},
{
"symbol": "SEI",
"qty": 67125.80718783144,
"avg_price": 0.074487,
"current_price": 0.072034,
"value": 4835.34,
"pnl": -164.66,
"pnl_pct": -3.29
},
{
"symbol": "PUMP",
"qty": 2496068.691810399,
"avg_price": 0.00200315,
"current_price": 0.00191781,
"value": 4786.99,
"pnl": -213.01,
"pnl_pct": -4.26
},
{
"symbol": "HYPE",
"qty": 158.8814744200826,
"avg_price": 31.470000000000002,
"current_price": 29.46,
"value": 4680.65,
"pnl": -319.35,
"pnl_pct": -6.39
}
],
"num_positions": 10,
"timestamp": "2026-02-10T22:00:19.668393+00:00"
}
]

View File

@ -0,0 +1,102 @@
[
{
"id": 1,
"timestamp": "2026-02-10T02:17:32.522012+00:00",
"action": "BUY",
"symbol": "SEI",
"price": 0.074487,
"qty": 67125.80718783144,
"amount_usd": 5000,
"reason": "Strong buy signal (score 60): Deep below VWAP (-2.7%), RSI oversold (0)"
},
{
"id": 2,
"timestamp": "2026-02-10T02:17:32.522427+00:00",
"action": "BUY",
"symbol": "ICP",
"price": 2.43,
"qty": 2057.6131687242796,
"amount_usd": 5000,
"reason": "Strong buy signal (score 45): Deep below VWAP (-13.5%), RSI low (39)"
},
{
"id": 3,
"timestamp": "2026-02-10T02:17:32.523774+00:00",
"action": "BUY",
"symbol": "PUMP",
"price": 0.00200315,
"qty": 2496068.691810399,
"amount_usd": 5000,
"reason": "Strong buy signal (score 40): Deep below VWAP (-5.7%), RSI low (30), Low volume (0.4x)"
},
{
"id": 4,
"timestamp": "2026-02-10T02:17:32.524471+00:00",
"action": "BUY",
"symbol": "TRUMP",
"price": 3.35,
"qty": 1492.5373134328358,
"amount_usd": 5000,
"reason": "Strong buy signal (score 40): Deep below VWAP (-2.6%), High volume (1.5x)"
},
{
"id": 5,
"timestamp": "2026-02-10T02:17:32.525072+00:00",
"action": "BUY",
"symbol": "HYPE",
"price": 31.47,
"qty": 158.8814744200826,
"amount_usd": 5000,
"reason": "Buy signal (score 25): Below VWAP (-0.8%), Low volume (0.1x), Momentum reversal (bullish)"
},
{
"id": 6,
"timestamp": "2026-02-10T06:00:19.532884+00:00",
"action": "BUY",
"symbol": "VET",
"price": 0.00776173,
"qty": 644186.2832126343,
"amount_usd": 5000,
"reason": "Strong buy signal (score 60): Deep below VWAP (-2.4%), RSI low (34), Momentum reversal (bullish)"
},
{
"id": 7,
"timestamp": "2026-02-10T06:00:19.533883+00:00",
"action": "BUY",
"symbol": "ARB",
"price": 0.110017,
"qty": 45447.521746639155,
"amount_usd": 5000,
"reason": "Strong buy signal (score 50): Deep below VWAP (-2.6%), RSI low (38), Low volume (0.1x), 24h dump (-5.4%)"
},
{
"id": 8,
"timestamp": "2026-02-10T06:00:19.534555+00:00",
"action": "BUY",
"symbol": "ADA",
"price": 0.265455,
"qty": 18835.584185643518,
"amount_usd": 5000,
"reason": "Strong buy signal (score 45): Below VWAP (-1.2%), RSI low (38), Momentum reversal (bullish)"
},
{
"id": 9,
"timestamp": "2026-02-10T06:00:19.535250+00:00",
"action": "BUY",
"symbol": "AAVE",
"price": 110.58,
"qty": 45.21613311629589,
"amount_usd": 5000,
"reason": "Strong buy signal (score 45): Below VWAP (-1.8%), RSI low (31), Momentum reversal (bullish)"
},
{
"id": 10,
"timestamp": "2026-02-10T06:00:19.535917+00:00",
"action": "BUY",
"symbol": "NEAR",
"price": 1.001,
"qty": 4995.004995004995,
"amount_usd": 5000,
"reason": "Strong buy signal (score 45): Deep below VWAP (-4.5%), RSI low (39)"
}
]

View File

@ -0,0 +1,593 @@
#!/usr/bin/env python3
"""
Crypto Market Watch - Paper Trading Game Engine
Scans top 150 cryptos using VWAP + RSI + volume analysis.
Makes autonomous paper trades with full tracking.
"""
import json
import time
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
from threading import Lock
DATA_DIR = Path(__file__).parent / "data"
DATA_DIR.mkdir(parents=True, exist_ok=True)
PORTFOLIO_FILE = DATA_DIR / "portfolio.json"
TRADES_FILE = DATA_DIR / "trades.json"
SNAPSHOTS_FILE = DATA_DIR / "snapshots.json"
WATCHLIST_FILE = DATA_DIR / "watchlist.json"
def _load(path, default=None):
if path.exists():
return json.loads(path.read_text())
return default if default is not None else {}
def _save(path, data):
path.write_text(json.dumps(data, indent=2))
# ── Portfolio Management ──
def get_portfolio():
default = {
"cash": 100000.0,
"positions": {},
"starting_balance": 100000.0,
"created_at": datetime.now(timezone.utc).isoformat(),
}
return _load(PORTFOLIO_FILE, default)
def save_portfolio(portfolio):
_save(PORTFOLIO_FILE, portfolio)
def get_trades():
return _load(TRADES_FILE, [])
def save_trades(trades):
_save(TRADES_FILE, trades)
def buy(symbol, price, amount_usd, reason=""):
"""Buy a crypto position."""
portfolio = get_portfolio()
trades = get_trades()
if amount_usd > portfolio["cash"]:
return None, "Insufficient cash"
qty = amount_usd / price
portfolio["cash"] -= amount_usd
pos = portfolio["positions"].get(symbol, {
"qty": 0, "avg_price": 0, "total_cost": 0
})
new_total_cost = pos["total_cost"] + amount_usd
new_qty = pos["qty"] + qty
pos["avg_price"] = new_total_cost / new_qty if new_qty > 0 else 0
pos["qty"] = new_qty
pos["total_cost"] = new_total_cost
pos["last_buy"] = datetime.now(timezone.utc).isoformat()
portfolio["positions"][symbol] = pos
save_portfolio(portfolio)
trade = {
"id": len(trades) + 1,
"timestamp": datetime.now(timezone.utc).isoformat(),
"action": "BUY",
"symbol": symbol,
"price": price,
"qty": qty,
"amount_usd": amount_usd,
"reason": reason,
}
trades.append(trade)
save_trades(trades)
return trade, None
def sell(symbol, price, pct=100, reason=""):
"""Sell a position (partial or full)."""
portfolio = get_portfolio()
trades = get_trades()
pos = portfolio["positions"].get(symbol)
if not pos or pos["qty"] <= 0:
return None, f"No position in {symbol}"
sell_qty = pos["qty"] * (pct / 100)
sell_value = sell_qty * price
cost_basis = pos["avg_price"] * sell_qty
pnl = sell_value - cost_basis
pnl_pct = (pnl / cost_basis) * 100 if cost_basis > 0 else 0
portfolio["cash"] += sell_value
pos["qty"] -= sell_qty
pos["total_cost"] -= cost_basis
if pos["qty"] < 0.0000001:
del portfolio["positions"][symbol]
else:
portfolio["positions"][symbol] = pos
save_portfolio(portfolio)
trade = {
"id": len(trades) + 1,
"timestamp": datetime.now(timezone.utc).isoformat(),
"action": "SELL",
"symbol": symbol,
"price": price,
"qty": sell_qty,
"amount_usd": sell_value,
"pnl": round(pnl, 2),
"pnl_pct": round(pnl_pct, 2),
"reason": reason,
}
trades.append(trade)
save_trades(trades)
return trade, None
def get_portfolio_value(prices):
"""Calculate total portfolio value."""
portfolio = get_portfolio()
positions_value = 0
position_details = []
for symbol, pos in portfolio["positions"].items():
current_price = prices.get(symbol, pos["avg_price"])
value = pos["qty"] * current_price
pnl = value - pos["total_cost"]
pnl_pct = (pnl / pos["total_cost"]) * 100 if pos["total_cost"] > 0 else 0
positions_value += value
position_details.append({
"symbol": symbol,
"qty": pos["qty"],
"avg_price": pos["avg_price"],
"current_price": current_price,
"value": round(value, 2),
"pnl": round(pnl, 2),
"pnl_pct": round(pnl_pct, 2),
})
total_value = portfolio["cash"] + positions_value
total_pnl = total_value - portfolio["starting_balance"]
total_pnl_pct = (total_pnl / portfolio["starting_balance"]) * 100
return {
"cash": round(portfolio["cash"], 2),
"positions_value": round(positions_value, 2),
"total_value": round(total_value, 2),
"total_pnl": round(total_pnl, 2),
"total_pnl_pct": round(total_pnl_pct, 2),
"positions": sorted(position_details, key=lambda x: -x["value"]),
"num_positions": len(position_details),
}
def take_snapshot(prices):
"""Save a point-in-time portfolio snapshot."""
snapshots = _load(SNAPSHOTS_FILE, [])
value = get_portfolio_value(prices)
value["timestamp"] = datetime.now(timezone.utc).isoformat()
snapshots.append(value)
# Keep last 1000
_save(SNAPSHOTS_FILE, snapshots[-1000:])
return value
# ── Market Data ──
def get_top_coins(limit=150):
"""Fetch top coins by market cap from CoinGecko."""
coins = []
for page in range(1, (limit // 100) + 2):
per_page = min(100, limit - len(coins))
if per_page <= 0:
break
url = (
f"https://api.coingecko.com/api/v3/coins/markets?"
f"vs_currency=usd&order=market_cap_desc&per_page={per_page}&page={page}"
f"&sparkline=false&price_change_percentage=1h,24h,7d"
)
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
try:
resp = urllib.request.urlopen(req, timeout=15)
batch = json.loads(resp.read())
coins.extend(batch)
except Exception as e:
print(f"Error fetching page {page}: {e}")
break
time.sleep(1) # Rate limit
return coins
def get_ohlcv(coin_id, days=2):
"""Get OHLCV data from CoinGecko for VWAP calculation."""
url = f"https://api.coingecko.com/api/v3/coins/{coin_id}/ohlc?vs_currency=usd&days={days}"
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
try:
resp = urllib.request.urlopen(req, timeout=10)
raw = json.loads(resp.read())
# CoinGecko OHLC: [timestamp, open, high, low, close]
return [{"t": r[0], "o": r[1], "h": r[2], "l": r[3], "c": r[4]} for r in raw]
except:
return []
def get_binance_klines(symbol, interval="1h", limit=48):
"""Get klines from Binance US for VWAP + volume."""
url = f"https://api.binance.us/api/v3/klines?symbol={symbol}USDT&interval={interval}&limit={limit}"
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
try:
resp = urllib.request.urlopen(req, timeout=10)
raw = json.loads(resp.read())
return [{"t": k[0], "o": float(k[1]), "h": float(k[2]), "l": float(k[3]),
"c": float(k[4]), "v": float(k[5])} for k in raw]
except:
return []
# ── Technical Analysis ──
def calc_vwap(candles):
"""Calculate VWAP with standard deviation bands."""
if not candles:
return None
cum_tpv = cum_vol = cum_sq = 0
for c in candles:
vol = c.get("v", 1) # Default volume 1 if not available
tp = (c["h"] + c["l"] + c["c"]) / 3
cum_tpv += tp * vol
cum_vol += vol
vwap = cum_tpv / cum_vol if cum_vol > 0 else candles[-1]["c"]
# Standard deviation
for c in candles:
vol = c.get("v", 1)
tp = (c["h"] + c["l"] + c["c"]) / 3
cum_sq += vol * (tp - vwap) ** 2
variance = cum_sq / cum_vol if cum_vol > 0 else 0
std = variance ** 0.5
return {
"vwap": vwap,
"std": std,
"upper1": vwap + std,
"lower1": vwap - std,
"upper2": vwap + 2 * std,
"lower2": vwap - 2 * std,
}
def calc_rsi(closes, period=14):
"""Calculate RSI."""
if len(closes) < period + 1:
return 50 # Default neutral
gains = []
losses = []
for i in range(1, len(closes)):
diff = closes[i] - closes[i - 1]
gains.append(max(diff, 0))
losses.append(max(-diff, 0))
avg_gain = sum(gains[-period:]) / period
avg_loss = sum(losses[-period:]) / period
if avg_loss == 0:
return 100
rs = avg_gain / avg_loss
return 100 - (100 / (1 + rs))
def analyze_coin(coin, klines=None):
"""Full VWAP + RSI + momentum analysis for a single coin."""
symbol = coin["symbol"].upper()
price = coin["current_price"]
if not klines:
klines = get_binance_klines(symbol, "1h", 48)
if not klines or len(klines) < 10:
return None
# VWAP
vwap_data = calc_vwap(klines)
vwap = vwap_data["vwap"]
vwap_diff = (price - vwap) / vwap * 100
# RSI
closes = [k["c"] for k in klines]
rsi = calc_rsi(closes)
# Volume trend
recent_vol = sum(k["v"] for k in klines[-6:]) # Last 6h
avg_vol = sum(k["v"] for k in klines) / (len(klines) / 6)
vol_ratio = recent_vol / avg_vol if avg_vol > 0 else 1
# Momentum (last 4h vs prior 4h)
if len(klines) >= 8:
recent_mom = (klines[-1]["c"] - klines[-4]["c"]) / klines[-4]["c"] * 100
prior_mom = (klines[-4]["c"] - klines[-8]["c"]) / klines[-8]["c"] * 100
else:
recent_mom = prior_mom = 0
# 24h change from CoinGecko
change_24h = coin.get("price_change_percentage_24h", 0) or 0
# Score: -100 to +100
score = 0
signals = []
# VWAP position
if vwap_diff < -2:
score += 30
signals.append(f"Deep below VWAP ({vwap_diff:.1f}%)")
elif vwap_diff < -0.5:
score += 15
signals.append(f"Below VWAP ({vwap_diff:.1f}%)")
elif vwap_diff > 2:
score -= 20
signals.append(f"Extended above VWAP ({vwap_diff:.1f}%)")
elif vwap_diff > 0.5:
score += 5
signals.append(f"Above VWAP ({vwap_diff:.1f}%)")
# RSI
if rsi < 30:
score += 30
signals.append(f"RSI oversold ({rsi:.0f})")
elif rsi < 40:
score += 15
signals.append(f"RSI low ({rsi:.0f})")
elif rsi > 70:
score -= 25
signals.append(f"RSI overbought ({rsi:.0f})")
elif rsi > 60:
score -= 10
signals.append(f"RSI elevated ({rsi:.0f})")
# Volume confirmation
if vol_ratio > 1.5:
score += 10 if vwap_diff < 0 else -10 # High vol at support = bullish
signals.append(f"High volume ({vol_ratio:.1f}x)")
elif vol_ratio < 0.5:
score -= 5
signals.append(f"Low volume ({vol_ratio:.1f}x)")
# Momentum reversal
if recent_mom > 0 and prior_mom < 0:
score += 15
signals.append("Momentum reversal (bullish)")
elif recent_mom < 0 and prior_mom > 0:
score -= 15
signals.append("Momentum reversal (bearish)")
# 24h trend
if change_24h < -5:
score += 10 # Potential bounce
signals.append(f"24h dump ({change_24h:.1f}%)")
elif change_24h > 5:
score -= 10 # Potential pullback
signals.append(f"24h pump ({change_24h:.1f}%)")
return {
"symbol": symbol,
"name": coin["name"],
"price": price,
"market_cap_rank": coin.get("market_cap_rank", 0),
"vwap": round(vwap, 6),
"vwap_diff_pct": round(vwap_diff, 2),
"rsi": round(rsi, 1),
"vol_ratio": round(vol_ratio, 2),
"momentum_4h": round(recent_mom, 2),
"change_24h": round(change_24h, 2),
"score": score,
"signals": signals,
"vwap_bands": {k: round(v, 6) for k, v in vwap_data.items()},
}
# ── Trading Logic ──
def should_buy(analysis):
"""Determine if we should buy based on analysis score."""
if not analysis:
return False, ""
score = analysis["score"]
rsi = analysis["rsi"]
vwap_diff = analysis["vwap_diff_pct"]
# Strong buy: oversold + below VWAP
if score >= 40:
return True, f"Strong buy signal (score {score}): {', '.join(analysis['signals'])}"
# Moderate buy: decent score + below VWAP
if score >= 20 and vwap_diff < -0.5:
return True, f"Buy signal (score {score}): {', '.join(analysis['signals'])}"
return False, ""
def should_sell(analysis, position):
"""Determine if we should sell a position."""
if not analysis:
return False, 0, ""
price = analysis["price"]
avg_price = position["avg_price"]
pnl_pct = (price - avg_price) / avg_price * 100
rsi = analysis["rsi"]
vwap_diff = analysis["vwap_diff_pct"]
score = analysis["score"]
# Take profit: +5% gain with overbought signals
if pnl_pct >= 5 and (rsi > 65 or vwap_diff > 1):
return True, 100, f"Take profit ({pnl_pct:.1f}%, RSI {rsi:.0f})"
# Strong take profit: +10% gain
if pnl_pct >= 10:
return True, 50, f"Partial take profit ({pnl_pct:.1f}%)"
# Stop loss: -8%
if pnl_pct <= -8:
return True, 100, f"Stop loss ({pnl_pct:.1f}%)"
# Bearish signals on losing position
if pnl_pct < -3 and score <= -20:
return True, 100, f"Cut loss (score {score}, PnL {pnl_pct:.1f}%)"
# Extended above VWAP with big gain
if pnl_pct >= 3 and vwap_diff > 2:
return True, 50, f"Extended above VWAP ({vwap_diff:.1f}%), lock gains"
return False, 0, ""
# ── Main Scanner ──
def run_scan(max_positions=10, position_size=5000):
"""
Full scan: analyze top 150 cryptos, make buy/sell decisions.
max_positions: max simultaneous positions
position_size: USD per position
"""
print(f"=== Crypto Market Watch Scan ===")
print(f"Time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}")
print()
# Get top coins
print("Fetching top 150 coins...", flush=True)
coins = get_top_coins(150)
# Deduplicate by symbol (CoinGecko sometimes returns dupes)
seen_symbols = set()
unique_coins = []
for c in coins:
sym = c["symbol"].upper()
if sym not in seen_symbols:
seen_symbols.add(sym)
unique_coins.append(c)
coins = unique_coins
print(f"Got {len(coins)} unique coins")
# Map symbols to Binance format
# Only analyze coins available on Binance US
binance_symbols = set()
try:
url = "https://api.binance.us/api/v3/exchangeInfo"
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
resp = urllib.request.urlopen(req, timeout=15)
exchange = json.loads(resp.read())
for s in exchange["symbols"]:
if s["quoteAsset"] == "USDT" and s["status"] == "TRADING":
binance_symbols.add(s["baseAsset"])
except:
pass
print(f"Binance US has {len(binance_symbols)} USDT pairs")
# Analyze each coin
analyses = []
prices = {}
for coin in coins:
symbol = coin["symbol"].upper()
prices[symbol] = coin["current_price"]
if symbol not in binance_symbols:
continue
analysis = analyze_coin(coin)
if analysis:
analyses.append(analysis)
time.sleep(0.2) # Rate limit
print(f"Analyzed {len(analyses)} coins with Binance data")
# Get current portfolio
portfolio = get_portfolio()
current_positions = set(portfolio["positions"].keys())
# ── SELL DECISIONS ──
sells = []
for symbol, pos in list(portfolio["positions"].items()):
analysis = next((a for a in analyses if a["symbol"] == symbol), None)
if not analysis:
# Can't analyze — skip (don't sell blind)
continue
do_sell, sell_pct, reason = should_sell(analysis, pos)
if do_sell:
trade, err = sell(symbol, analysis["price"], sell_pct, reason)
if trade:
sells.append(trade)
print(f" 📤 SELL {symbol} @ ${analysis['price']:,.4f} ({sell_pct}%) — {reason}")
# ── BUY DECISIONS ──
portfolio = get_portfolio() # Refresh after sells
num_positions = len(portfolio["positions"])
available_slots = max_positions - num_positions
# Sort by score (best opportunities first)
buy_candidates = sorted(
[a for a in analyses if a["symbol"] not in portfolio["positions"]],
key=lambda x: -x["score"]
)
buys = []
for analysis in buy_candidates[:available_slots * 2]: # Check 2x candidates
if len(buys) >= available_slots:
break
if portfolio["cash"] < position_size:
break
do_buy, reason = should_buy(analysis)
if do_buy:
trade, err = buy(analysis["symbol"], analysis["price"], position_size, reason)
if trade:
buys.append(trade)
portfolio = get_portfolio() # Refresh
print(f" 📥 BUY {analysis['symbol']} @ ${analysis['price']:,.4f}{reason}")
# Take snapshot
snapshot = take_snapshot(prices)
# Summary
print(f"\n{'='*50}")
print(f"Buys: {len(buys)} | Sells: {len(sells)}")
print(f"Portfolio: ${snapshot['total_value']:,.2f} ({snapshot['total_pnl_pct']:+.2f}%)")
print(f"Cash: ${snapshot['cash']:,.2f} | Positions: {snapshot['num_positions']}")
if snapshot["positions"]:
print(f"\nOpen Positions:")
for p in snapshot["positions"]:
emoji = "🟢" if p["pnl"] >= 0 else "🔴"
print(f" {emoji} {p['symbol']}: ${p['value']:,.2f} ({p['pnl_pct']:+.1f}%)")
# Top opportunities not taken
print(f"\nTop Scoring Coins:")
for a in sorted(analyses, key=lambda x: -x["score"])[:10]:
held = "📌" if a["symbol"] in portfolio.get("positions", {}) else " "
print(f" {held} {a['symbol']:<8} score:{a['score']:>4} | RSI:{a['rsi']:.0f} | VWAP:{a['vwap_diff_pct']:+.1f}% | 24h:{a['change_24h']:+.1f}%")
return {"buys": buys, "sells": sells, "snapshot": snapshot, "analyses": analyses}
if __name__ == "__main__":
result = run_scan()

View File

@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""
Wrapper to run crypto scan and send Telegram alerts.
Designed for systemd timer execution.
"""
import json
import os
import urllib.request
from game_engine import run_scan, get_portfolio_value, get_trades
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "6443752046")
def send_telegram(message):
if not TELEGRAM_BOT_TOKEN:
return
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
data = json.dumps({
"chat_id": TELEGRAM_CHAT_ID,
"text": message,
"parse_mode": "HTML",
}).encode()
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
try:
urllib.request.urlopen(req, timeout=10)
except Exception as e:
print(f"Telegram error: {e}")
def main():
result = run_scan(max_positions=10, position_size=5000)
buys = result.get("buys", [])
sells = result.get("sells", [])
snapshot = result.get("snapshot", {})
# Only alert if there were trades
if buys or sells:
lines = ["🎮 <b>Crypto Watch Update</b>\n"]
for t in buys:
lines.append(f"📥 BUY {t['symbol']} @ ${t['price']:,.4f}")
lines.append(f" {t.get('reason', '')}\n")
for t in sells:
emoji = "🟢" if t.get("pnl", 0) >= 0 else "🔴"
lines.append(f"📤 SELL {t['symbol']} @ ${t['price']:,.4f}")
lines.append(f" {emoji} PnL: ${t.get('pnl', 0):+,.2f} ({t.get('pnl_pct', 0):+.1f}%)")
lines.append(f" {t.get('reason', '')}\n")
lines.append(f"💰 Portfolio: ${snapshot.get('total_value', 0):,.2f} ({snapshot.get('total_pnl_pct', 0):+.2f}%)")
lines.append(f"💵 Cash: ${snapshot.get('cash', 0):,.2f} | Positions: {snapshot.get('num_positions', 0)}")
send_telegram("\n".join(lines))
if __name__ == "__main__":
main()

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
{
"last_check": "2026-02-12T06:14:23.639458+00:00"
}

View File

@ -0,0 +1,50 @@
{
"timestamp": "2026-02-12T06:11:18.980961+00:00",
"total_scraped": 44,
"new_posts": 22,
"money_posts": 7,
"posts": [
{
"text": "We\u2019re in the business of supporting traders. Get the tools you need to make your way in the market.",
"userName": "tastytrade\n@tastytrade",
"timestamp": "",
"link": "/tastytrade/status/1971214019274080389/analytics"
},
{
"text": "agentic e2e regression testing solved.\nprompt \u2192 test in under 2 mins.\nif you're still writing tests by hand... why",
"userName": "Bug0\n@bug0inc",
"timestamp": "",
"link": "/bug0inc/status/2019756209390375399/analytics"
},
{
"text": " New GPT crypto price predictions:\n\n$ETH -0.285% => $1945.73\n\n$ZRO +0.086% => $2.329\n\n$UNI 0.0% => $3.492\n\n$BTC -0.116% => $67481.29\n\n$XRP +0.181% => $1.3796\n\n$PENGU +0.116% => $0.006021\n\n$SOL +0.063% => $79.72\n\n$TRUMP +0.094% => $3.195",
"userName": "OctoBot - GPT crypto price predictions\n@OctoBotGPT\n\u00b7\n10h",
"timestamp": "2026-02-11T20:00:50.000Z",
"link": "/OctoBotGPT/status/2021675819580481813"
},
{
"text": " New GPT crypto price predictions:\n\n$ADA 0.0% => $0.2633\n\n$DOGE 0.0% => $0.09372\n\n$SOL 0.0% => $84.78\n\n$XRP +0.266% => $1.427\n\n$SENT +0.594% => $0.02863\n\n$PAXG 0.0% => $5033.12\n\n$PEPE +98.997% => $0.000367\n\n$SUI +0.864% => $0.9487",
"userName": "OctoBot - GPT crypto price predictions\n@OctoBotGPT\n\u00b7\nFeb 10",
"timestamp": "2026-02-10T08:00:48.000Z",
"link": "/OctoBotGPT/status/2021132226792947802"
},
{
"text": "$ZAMA 0.0% => $0.02688\n\n$LIT -1.503% => $0.732\n\n$ZRO -0.259% => $1.927\n\n$BTC -0.078% => $68890.0\n\n$ETH -0.53% => $1997.59\n\n$HBAR +0.512% => $0.09188\n\n$TRX -0.036% => $0.277\n\n$SHIB 0.0% => $5.98e-06",
"userName": "OctoBot - GPT crypto price predictions\n@OctoBotGPT\n\u00b7\nFeb 10",
"timestamp": "2026-02-10T08:00:48.000Z",
"link": "/OctoBotGPT/status/2021132227921301730"
},
{
"text": " New GPT crypto price predictions:\n\n$ZRO +0.959% => $2.398\n\n$DOGE 0.0% => $0.09053\n\n$LINK 0.0% => $8.32\n\n$SOL -0.671% => $80.5\n\n$PENGU -0.169% => $0.005916\n\n$USD1 +0.02% => $1.0\n\n$SUI -0.403% => $0.893\n\n$BTC 0.0% => $67046.23",
"userName": "OctoBot - GPT crypto price predictions\n@OctoBotGPT\n\u00b7\n22h",
"timestamp": "2026-02-11T08:00:52.000Z",
"link": "/OctoBotGPT/status/2021494632635408429"
},
{
"text": "$ETH +1.022% => $1971.45\n\n$BNB 0.0% => $600.95\n\n$ZAMA -0.714% => $0.0196\n\n$SHIB 0.0% => $5.84e-06\n\n$XRP -0.242% => $1.3646\n\n$ASTER +0.459% => $0.653\n\n$ADA -0.393% => $0.2546\n\n$TRX 0.0% => $0.275",
"userName": "OctoBot - GPT crypto price predictions\n@OctoBotGPT\n\u00b7\n22h",
"timestamp": "2026-02-11T08:00:52.000Z",
"link": "/OctoBotGPT/status/2021494633759408338"
}
]
}

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
{ {
"last_check": "2026-02-09T16:58:59.975254+00:00", "last_check": "2026-02-12T06:14:23.952343+00:00",
"total_tracked": 3100, "total_tracked": 3100,
"new_this_check": 0 "new_this_check": 0
} }

View File

@ -0,0 +1 @@
["37f5b094ed19f0ae", "f013475eaef8b32f", "6f9f9b6f8da5cdc9", "952f5caf7819fde5", "6043a216215e23f0", "ff1b1af5e65905a8", "6217530e01f15dd0", "6e1a48e8344a1d6f", "6d61e4a9dfb604ea", "529a533d4da86360", "d2217d5c918df581", "8b4a57cd97ae6c34", "5bf0ad8e5e2f9dfe", "2ee63742fbc6e541", "7d74e6eca55f346f", "9f29b74a37377015", "3ccdb8471c5c19b6", "8a48151a19597742", "e6a24f45f8a6e8bb", "899377e3ae43e396", "05e47b3d9e24c860", "643b1c7d00ad50f2", "b7a2c548992c278c", "a1a8dddd35fd08d5", "149ace9c327e7700", "7d767e4d0e3a12a3", "0c6a5029b97b5a30", "b7354be18fa9f71a", "7b10b1cb595f2006", "ca4011161b3ce92d", "ffbe4a2b11671722", "cd2ce19326f75133", "60fd088f8f1ae9b2", "8ebbe21b036415d8", "84da8ad2dd424e2b", "7d4648af05013346", "137b80809666db54", "1151a6c9c0f2915c", "36db4785c888600f", "e8c4ec6aa2a9a563", "f9ded3bb072f01fe", "f30bff813bce39b8", "e1a98d7590fe46ee", "a767ef4ed21a86f3", "3525848121516057", "3d6e2887b81b3016", "cb6375e11d10b745", "493d2dc82b844b36", "60dced6f08edb3c2", "7d97ce308ff157b2", "db27f494858bd743", "176776613d96cdbe", "eb7a8395aa02f113"]

View File

@ -0,0 +1,231 @@
#!/usr/bin/env python3
"""
Feed Monitor — Scrapes X home timeline via Chrome CDP (localhost:9222).
Deduplicates, filters for money/trading topics, saves captures, sends Telegram alerts.
"""
import json
import hashlib
import os
import sys
import time
import http.client
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
PROJECT_DIR = Path(__file__).parent
DATA_DIR = PROJECT_DIR / "data"
SEEN_FILE = DATA_DIR / "seen_posts.json"
CAPTURES_DIR = DATA_DIR / "feed_captures"
CAPTURES_DIR.mkdir(parents=True, exist_ok=True)
CDP_HOST = "localhost"
CDP_PORT = 9222
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "6443752046")
MONEY_KEYWORDS = [
"polymarket", "trade", "trading", "profit", "arbitrage", "crypto",
"bitcoin", "btc", "ethereum", "eth", "solana", "sol", "stock",
"stocks", "market", "portfolio", "defi", "token", "whale",
"bullish", "bearish", "short", "long", "pnl", "alpha", "degen",
"usdc", "usdt", "wallet", "airdrop", "memecoin", "nft",
"yield", "staking", "leverage", "futures", "options", "hedge",
"pump", "dump", "rug", "moon", "bag", "position", "signal",
]
def send_telegram(message: str):
if not TELEGRAM_BOT_TOKEN:
print(f"[ALERT] {message}")
return
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
data = json.dumps({
"chat_id": TELEGRAM_CHAT_ID,
"text": message,
"parse_mode": "HTML",
"disable_web_page_preview": True,
}).encode()
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
try:
urllib.request.urlopen(req, timeout=10)
except Exception as e:
print(f" Telegram error: {e}")
def cdp_send(ws, method: str, params: dict = None, msg_id: int = 1):
"""Send a CDP command over websocket and return the result."""
import websocket
payload = {"id": msg_id, "method": method}
if params:
payload["params"] = params
ws.send(json.dumps(payload))
while True:
resp = json.loads(ws.recv())
if resp.get("id") == msg_id:
return resp.get("result", {})
def get_x_tab_ws():
"""Find an X.com tab in Chrome and return its websocket URL."""
conn = http.client.HTTPConnection(CDP_HOST, CDP_PORT, timeout=5)
conn.request("GET", "/json")
tabs = json.loads(conn.getresponse().read())
conn.close()
for t in tabs:
url = t.get("url", "")
if "x.com" in url or "twitter.com" in url:
ws_url = t.get("webSocketDebuggerUrl")
if ws_url:
return ws_url, t.get("url")
return None, None
def scrape_feed_via_cdp():
"""Navigate to X home, scroll, extract posts via DOM evaluation."""
import websocket
ws_url, current_url = get_x_tab_ws()
if not ws_url:
print("ERROR: No X.com tab found in Chrome at localhost:9222")
sys.exit(1)
print(f"Connected to tab: {current_url}")
ws = websocket.create_connection(ws_url, timeout=30)
# Navigate to home timeline
cdp_send(ws, "Page.navigate", {"url": "https://x.com/home"}, 1)
time.sleep(5)
all_posts = []
seen_texts = set()
for scroll_i in range(6):
# Extract posts from timeline
js = """
(() => {
const posts = [];
document.querySelectorAll('article[data-testid="tweet"]').forEach(article => {
try {
const textEl = article.querySelector('[data-testid="tweetText"]');
const text = textEl ? textEl.innerText : '';
const userEl = article.querySelector('[data-testid="User-Name"]');
const userName = userEl ? userEl.innerText : '';
const timeEl = article.querySelector('time');
const timestamp = timeEl ? timeEl.getAttribute('datetime') : '';
const linkEl = article.querySelector('a[href*="/status/"]');
const link = linkEl ? linkEl.getAttribute('href') : '';
posts.push({ text, userName, timestamp, link });
} catch(e) {}
});
return JSON.stringify(posts);
})()
"""
result = cdp_send(ws, "Runtime.evaluate", {"expression": js, "returnByValue": True}, 10 + scroll_i)
raw = result.get("result", {}).get("value", "[]")
posts = json.loads(raw) if isinstance(raw, str) else []
for p in posts:
sig = p.get("text", "")[:120]
if sig and sig not in seen_texts:
seen_texts.add(sig)
all_posts.append(p)
# Scroll down
cdp_send(ws, "Runtime.evaluate", {"expression": "window.scrollBy(0, 2000)"}, 100 + scroll_i)
time.sleep(2)
ws.close()
return all_posts
def post_hash(post: dict) -> str:
text = post.get("text", "") + post.get("userName", "")
return hashlib.sha256(text.encode()).hexdigest()[:16]
def is_money_related(text: str) -> bool:
lower = text.lower()
return any(kw in lower for kw in MONEY_KEYWORDS)
def load_seen() -> set:
if SEEN_FILE.exists():
try:
return set(json.loads(SEEN_FILE.read_text()))
except:
pass
return set()
def save_seen(seen: set):
# Keep last 10k
items = list(seen)[-10000:]
SEEN_FILE.write_text(json.dumps(items))
def main():
now = datetime.now(timezone.utc)
print(f"=== Feed Monitor === {now.strftime('%Y-%m-%d %H:%M UTC')}")
posts = scrape_feed_via_cdp()
print(f"Scraped {len(posts)} posts from timeline")
seen = load_seen()
new_posts = []
money_posts = []
for p in posts:
h = post_hash(p)
if h in seen:
continue
seen.add(h)
new_posts.append(p)
if is_money_related(p.get("text", "")):
money_posts.append(p)
save_seen(seen)
print(f"New posts: {len(new_posts)}")
print(f"Money-related: {len(money_posts)}")
# Save capture
ts = now.strftime("%Y%m%d-%H%M")
capture = {
"timestamp": now.isoformat(),
"total_scraped": len(posts),
"new_posts": len(new_posts),
"money_posts": len(money_posts),
"posts": money_posts,
}
capture_file = CAPTURES_DIR / f"feed-{ts}.json"
capture_file.write_text(json.dumps(capture, indent=2))
print(f"Saved capture: {capture_file}")
# Alert on money posts
if money_posts:
print(f"\n🔔 {len(money_posts)} money-related posts found!")
for p in money_posts[:8]:
user = p.get("userName", "").split("\n")[0]
snippet = p.get("text", "")[:250].replace("\n", " ")
link = p.get("link", "")
full_link = f"https://x.com{link}" if link and not link.startswith("http") else link
print(f"{user}: {snippet[:100]}...")
msg = f"🔍 <b>{user}</b>\n\n{snippet}"
if full_link:
msg += f"\n\n{full_link}"
send_telegram(msg)
else:
print("No new money-related posts.")
return len(money_posts)
if __name__ == "__main__":
count = main()
sys.exit(0)

View File

@ -0,0 +1,155 @@
[
{
"name": "guest_id_marketing",
"value": "v1%3A177052493168164632",
"domain": ".x.com",
"path": "/",
"secure": true,
"httpOnly": false,
"sameSite": "None"
},
{
"name": "guest_id_ads",
"value": "v1%3A177052493168164632",
"domain": ".x.com",
"path": "/",
"secure": true,
"httpOnly": false,
"sameSite": "None"
},
{
"name": "guest_id",
"value": "v1%3A177052493168164632",
"domain": ".x.com",
"path": "/",
"secure": true,
"httpOnly": false,
"sameSite": "None"
},
{
"name": "personalization_id",
"value": "\"v1_6O8SSA4FCcIXzFzq4cql3A==\"",
"domain": ".x.com",
"path": "/",
"secure": true,
"httpOnly": false,
"sameSite": "None"
},
{
"name": "__cuid",
"value": "7ec0f8364ef9466bb4d5e5398de60a7a",
"domain": ".x.com",
"path": "/",
"secure": false,
"httpOnly": false,
"sameSite": "Lax"
},
{
"name": "guest_id_marketing",
"value": "v1%3A177052493360013497",
"domain": ".twitter.com",
"path": "/",
"secure": true,
"httpOnly": false,
"sameSite": "None"
},
{
"name": "guest_id_ads",
"value": "v1%3A177052493360013497",
"domain": ".twitter.com",
"path": "/",
"secure": true,
"httpOnly": false,
"sameSite": "None"
},
{
"name": "personalization_id",
"value": "\"v1_0RdWTpuTILka/W8MwiVsGQ==\"",
"domain": ".twitter.com",
"path": "/",
"secure": true,
"httpOnly": false,
"sameSite": "None"
},
{
"name": "guest_id",
"value": "v1%3A177052493360013497",
"domain": ".twitter.com",
"path": "/",
"secure": true,
"httpOnly": false,
"sameSite": "None"
},
{
"name": "g_state",
"value": "{\"i_l\":0,\"i_ll\":1770524933853,\"i_b\":\"/335bZxZT54Tkc2wThT5DEH5v8hDZyhbe/JOl6uvF+k\",\"i_e\":{\"enable_itp_optimization\":0}}",
"domain": "x.com",
"path": "/",
"secure": false,
"httpOnly": false,
"sameSite": "Lax"
},
{
"name": "kdt",
"value": "Y9jfWROysXsnZyHwlffVbs8jvBJabIN4RGlZYFHP",
"domain": ".x.com",
"path": "/",
"secure": true,
"httpOnly": true,
"sameSite": "Lax"
},
{
"name": "auth_token",
"value": "219b71a535b96ef9f978612a48cf81a462643ee3",
"domain": ".x.com",
"path": "/",
"secure": true,
"httpOnly": true,
"sameSite": "None"
},
{
"name": "ct0",
"value": "e2c61ad6ce7115f2d8acd2062dc5c9a377140d9b570f871d9b25847f2d7a36fe512a424a359775d73a11a5a0a5154b6623b0021992a2b7f1e094d5ac5ee65cfeaf8ac87de09b7dcfc48f28a5b6dd15dc",
"domain": ".x.com",
"path": "/",
"secure": true,
"httpOnly": false,
"sameSite": "Lax"
},
{
"name": "twid",
"value": "u%3D741482516",
"domain": ".x.com",
"path": "/",
"secure": true,
"httpOnly": false,
"sameSite": "None"
},
{
"name": "lang",
"value": "en",
"domain": "x.com",
"path": "/",
"secure": false,
"httpOnly": false,
"sameSite": "Lax"
},
{
"name": "external_referer",
"value": "vC8TI7P7q9UHtLBqrmGBr3bhFoPD7nVN|0|8e8t2xd8A2w%3D",
"domain": ".x.com",
"path": "/",
"secure": true,
"httpOnly": false,
"sameSite": "Lax"
},
{
"name": "__cf_bm",
"value": "UjX5M.SqXScrW4zZ_GhiubhCXhv.8SI8uU7MkZCGT24-1770678794.1374662-1.0.1.1-4x.1srI8Lir7aTkBYJxMGMZQ2E3.EZKgF5S_gLeoAQzEUvIFZQTLQNxhFfiiVNNaXbfZ8HgKEPtSTvpaglXpnCo9COtawFeKPtaKmENpRj5V3mP0EOhtt4w_MpLhHekN",
"domain": ".x.com",
"path": "/",
"secure": true,
"httpOnly": true,
"sameSite": "Lax"
}
]

View File

@ -0,0 +1,249 @@
#!/usr/bin/env python3
"""
X/Twitter Feed Scraper using Playwright
Scrapes specific accounts for trading-related posts.
Uses saved Chrome session cookies for authentication.
"""
import json
import os
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
ACCOUNTS = [
"browomo", "ArchiveExplorer", "noisyb0y1", "krajekis",
"Shelpid_WI3M", "polyaboretum", "0xashensoul",
]
TRADING_KEYWORDS = [
"polymarket", "trade", "profit", "wallet", "arbitrage", "signal",
"crypto", "bitcoin", "ethereum", "solana", "strategy", "edge",
"bet", "position", "stock", "market", "pnl", "alpha",
"$", "usdc", "defi", "token", "copy", "whale", "degen",
"short", "long", "bullish", "bearish", "portfolio",
]
DATA_DIR = Path(__file__).parent.parent / "data" / "x-feed"
DATA_DIR.mkdir(parents=True, exist_ok=True)
COOKIE_FILE = Path(__file__).parent / "x_cookies.json"
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "6443752046")
def send_telegram(message):
if not TELEGRAM_BOT_TOKEN:
print(f"[ALERT] {message}")
return
import urllib.request
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
data = json.dumps({"chat_id": TELEGRAM_CHAT_ID, "text": message, "parse_mode": "HTML"}).encode()
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
try:
urllib.request.urlopen(req, timeout=10)
except Exception as e:
print(f"Telegram error: {e}")
def save_cookies(context):
cookies = context.cookies()
COOKIE_FILE.write_text(json.dumps(cookies, indent=2))
print(f"Saved {len(cookies)} cookies")
def load_cookies(context):
if COOKIE_FILE.exists():
cookies = json.loads(COOKIE_FILE.read_text())
context.add_cookies(cookies)
print(f"Loaded {len(cookies)} cookies")
return True
return False
def export_cookies_from_chrome():
"""One-time: grab cookies from the running Chrome debug instance."""
import http.client, websocket as ws_mod
conn = http.client.HTTPConnection("localhost", 9222)
conn.request("GET", "/json")
tabs = json.loads(conn.getresponse().read())
x_tab = None
for t in tabs:
if "x.com" in t.get("url", ""):
x_tab = t
break
if not x_tab:
print("No X tab found in Chrome debug")
return []
ws = ws_mod.create_connection(x_tab["webSocketDebuggerUrl"], timeout=10)
ws.send(json.dumps({"id": 1, "method": "Network.getAllCookies"}))
result = json.loads(ws.recv())
all_cookies = result.get("result", {}).get("cookies", [])
ws.close()
# Filter for x.com cookies and convert to Playwright format
x_cookies = []
for c in all_cookies:
if "x.com" in c.get("domain", "") or "twitter.com" in c.get("domain", ""):
x_cookies.append({
"name": c["name"],
"value": c["value"],
"domain": c["domain"],
"path": c.get("path", "/"),
"secure": c.get("secure", False),
"httpOnly": c.get("httpOnly", False),
"sameSite": c.get("sameSite", "Lax"),
})
COOKIE_FILE.write_text(json.dumps(x_cookies, indent=2))
print(f"Exported {len(x_cookies)} X cookies from Chrome")
return x_cookies
def scrape_account(page, account, max_scroll=5):
"""Scrape recent posts from a single account."""
posts = []
try:
page.goto(f"https://x.com/{account}", wait_until="networkidle", timeout=15000)
except:
try:
page.goto(f"https://x.com/{account}", wait_until="domcontentloaded", timeout=10000)
page.wait_for_timeout(3000)
except Exception as e:
print(f" Failed to load @{account}: {e}")
return posts
seen_texts = set()
for scroll in range(max_scroll):
articles = page.query_selector_all("article")
for article in articles:
try:
text = article.inner_text()[:800]
# Deduplicate
sig = text[:100]
if sig in seen_texts:
continue
seen_texts.add(sig)
# Extract links
links = article.query_selector_all("a")
urls = [l.get_attribute("href") for l in links if l.get_attribute("href")]
posts.append({
"account": account,
"text": text,
"urls": urls[:5],
"scraped_at": datetime.now(timezone.utc).isoformat(),
})
except:
continue
# Scroll down
page.evaluate("window.scrollBy(0, 1500)")
page.wait_for_timeout(1500)
return posts
def is_trading_related(text):
text_lower = text.lower()
return any(kw in text_lower for kw in TRADING_KEYWORDS)
def main():
from playwright.sync_api import sync_playwright
print(f"=== X Feed Scraper (Playwright) ===")
print(f"Time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}")
# Export cookies from Chrome if we don't have them yet
if not COOKIE_FILE.exists():
print("No cookies found — exporting from Chrome debug session...")
export_cookies_from_chrome()
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
user_agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
viewport={"width": 1280, "height": 900},
)
load_cookies(context)
page = context.new_page()
all_posts = []
trading_posts = []
for account in ACCOUNTS:
print(f"\nScraping @{account}...", end=" ", flush=True)
posts = scrape_account(page, account)
print(f"{len(posts)} posts")
for post in posts:
all_posts.append(post)
if is_trading_related(post["text"]):
trading_posts.append(post)
browser.close()
print(f"\n{'='*50}")
print(f"Total posts: {len(all_posts)}")
print(f"Trading-related: {len(trading_posts)}")
# Save results
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M")
out_file = DATA_DIR / f"scan-{timestamp}.json"
out_file.write_text(json.dumps({
"timestamp": datetime.now(timezone.utc).isoformat(),
"total_posts": len(all_posts),
"trading_posts": len(trading_posts),
"posts": trading_posts,
}, indent=2))
print(f"Saved to {out_file}")
# Check for new posts we haven't seen before
seen_file = DATA_DIR / "seen_posts.json"
seen = set()
if seen_file.exists():
try:
seen = set(json.loads(seen_file.read_text()))
except:
pass
new_posts = []
for post in trading_posts:
sig = post["text"][:150]
if sig not in seen:
new_posts.append(post)
seen.add(sig)
seen_file.write_text(json.dumps(list(seen)[-5000:])) # Keep last 5000
if new_posts:
print(f"\n🔔 {len(new_posts)} NEW trading posts!")
for post in new_posts[:5]:
lines = post["text"].split("\n")
author = f"@{post['account']}"
snippet = post["text"][:200].replace("\n", " ")
print(f"\n {author}: {snippet}")
# Alert on Telegram
msg = f"🔍 <b>New from {author}</b>\n\n{snippet[:300]}"
if post.get("urls"):
x_urls = [u for u in post["urls"] if "x.com" in u or "twitter.com" in u]
if x_urls:
msg += f"\n\n{x_urls[0]}"
send_telegram(msg)
else:
print("\nNo new trading posts since last scan.")
if __name__ == "__main__":
main()

224
projects/kip/PROJECT.md Normal file
View File

@ -0,0 +1,224 @@
# Kip — Voice Assistant
**Codename:** Kip
**Purpose:** Alexa replacement for D J's girlfriend
**Architecture:** Steam Deck (thin client) ↔ Proxmox LXC (brains)
---
## Overview
Kip is a privacy-first voice assistant. The Steam Deck acts as a dumb terminal (mic, speaker, screen). All intelligence runs on an LXC container on Proxmox.
## Hardware
### Steam Deck (Client)
- Always-on, propped up in kitchen/living room on charging dock
- Runs: wake word detection, audio capture, audio playback, display UI
- Connects to LXC over local WiFi
### LXC Container (Server)
- **OS:** Ubuntu 22.04 or 24.04
- **RAM:** 4GB recommended (Whisper needs ~1.5GB, Piper ~200MB, OpenClaw ~500MB)
- **Disk:** 10GB (models + data)
- **CPU:** 2-4 cores (Whisper STT is CPU-bound)
- **Network:** Static IP on LAN, accessible from Steam Deck
## Software Stack
### LXC Container
| Component | Purpose | Tool |
|-----------|---------|------|
| STT | Speech-to-text | Faster Whisper (base.en model) |
| TTS | Text-to-speech | Piper (en_US voice) |
| Agent | Intelligence | OpenClaw with Kip agent |
| API | Communication | FastAPI HTTP server |
| Data | Grocery list, timers | JSON files + SQLite |
### Steam Deck
| Component | Purpose | Tool |
|-----------|---------|------|
| Wake word | "Hey Kip" detection | OpenWakeWord |
| Audio capture | Record after wake | PyAudio / sounddevice |
| Audio playback | Play TTS responses | PyAudio / sounddevice |
| UI | Display info | Web browser (fullscreen PWA) or PyQt |
| Client | Talk to LXC | Python HTTP client |
---
## LXC Setup Checklist
D J creates the LXC with:
- [ ] Ubuntu 22.04 or 24.04 template
- [ ] 4GB RAM, 2-4 CPU cores, 10GB disk
- [ ] Static IP on LAN (e.g., 192.168.86.XX)
- [ ] SSH access enabled (key-based)
- [ ] Audio passthrough NOT needed (LXC doesn't play audio — Deck does)
- [ ] Internet access (for OpenClaw, model downloads)
- [ ] Hostname: `kip` (optional, nice to have)
Once created, give Case:
- IP address
- SSH credentials or key
---
## API Design (LXC ↔ Steam Deck)
### POST /listen
Steam Deck sends audio, gets back text response + TTS audio.
```
Request:
Content-Type: multipart/form-data
Body: audio file (WAV, 16kHz mono)
Response:
{
"text": "user said this",
"response": "Kip says this",
"audio": "<base64 WAV of TTS response>",
"ui_update": {
"type": "grocery_list",
"data": ["eggs", "milk", "bread"]
}
}
```
### GET /status
Health check + current state (timers, lists, etc.)
### GET /grocery
Returns current grocery list (for phone web view)
### POST /grocery
Add/remove items (for phone web view)
### GET /ui
Returns current display state for the Deck's screen.
---
## Kip Agent (OpenClaw)
Kip gets its own OpenClaw agent with:
### SOUL.md (Personality)
- Name: Kip
- Friendly, concise, warm
- Optimized for voice — short responses, no markdown
- Knows the household (D J, girlfriend, 4 cats)
- Designed for non-technical user
### Capabilities
- **Weather:** "Hey Kip, what's the weather?" → Nashville forecast
- **Timers:** "Hey Kip, set a timer for 15 minutes" → countdown with alarm
- **Grocery list:** "Hey Kip, add eggs to the list" → persistent list
- **Grocery check:** "Hey Kip, what's on the grocery list?" → reads it back
- **General Q&A:** "Hey Kip, how long do I bake chicken at 400?" → answer
- **Time/Date:** "Hey Kip, what time is it?"
### Future Capabilities
- Calendar integration
- Music control (Spotify)
- Smart home (if they get devices)
- Recipe lookup
- Kroger API for prices/ordering
---
## Phone Web View
Simple responsive web page served by the LXC:
- Shows grocery list
- Can add/remove items by tapping
- Accessible at `http://kip.local:8080` or `http://192.168.86.XX:8080`
- Girlfriend can open it on her iPhone in the store
---
## Build Phases
### Phase 1: Voice Loop (MVP)
- [ ] LXC setup + dependencies installed
- [ ] Faster Whisper running (base.en model)
- [ ] Piper TTS running (pick a good voice)
- [ ] FastAPI server handling /listen endpoint
- [ ] Steam Deck: wake word + record + send + play response
- [ ] Test: "Hey Kip, hello" → Kip responds with voice
- **Goal: End-to-end voice working**
### Phase 2: Grocery List + Weather
- [ ] Grocery list CRUD (voice + API)
- [ ] Weather skill (Nashville)
- [ ] Timer system with alarm sounds
- [ ] Phone web view for grocery list
- [ ] Steam Deck display: clock + weather + active timers
- **Goal: Actually useful in the kitchen**
### Phase 3: OpenClaw Integration
- [ ] Kip agent running on OpenClaw
- [ ] General Q&A via Claude/Qwen
- [ ] Smarter conversations (context, follow-ups)
- [ ] Cost optimization: simple commands (timer, list) handled locally, only complex Q&A hits Claude
- **Goal: Smart assistant, not just a voice command box**
### Phase 4: Polish
- [ ] Custom wake word model trained on "Hey Kip"
- [ ] Better TTS voice selection
- [ ] Deck UI polish (nice weather widget, timer display, list view)
- [ ] Ambient mode (clock/weather when idle)
- [ ] Multiple room support (add Pi later)
- **Goal: Girlfriend actually wants to use it daily**
---
## Cost
| Item | Cost |
|------|------|
| Steam Deck | Already owned |
| LXC container | Free (Proxmox) |
| OpenWakeWord | Free (open source) |
| Faster Whisper | Free (open source) |
| Piper TTS | Free (open source) |
| OpenClaw | Already running |
| Claude API for Q&A | Covered by existing subscription |
| **Total** | **$0** |
---
## File Structure
```
projects/kip/
├── PROJECT.md # This file
├── server/ # LXC-side code
│ ├── main.py # FastAPI server
│ ├── stt.py # Whisper STT wrapper
│ ├── tts.py # Piper TTS wrapper
│ ├── skills/ # Timer, grocery, weather handlers
│ ├── data/ # Grocery lists, state
│ └── requirements.txt
├── client/ # Steam Deck code
│ ├── kip_client.py # Main client app
│ ├── wake_word.py # OpenWakeWord listener
│ ├── audio.py # Record/playback
│ ├── ui/ # Display UI
│ └── requirements.txt
└── agent/ # Kip's OpenClaw agent config
├── SOUL.md
└── config.yaml
```
---
## Notes
- All voice processing (STT/TTS) on LXC, not Deck — keeps client thin
- Wake word is the ONLY thing that runs on Deck locally
- Grocery list syncs to a web page for phone access
- Simple commands (timer, list) should be handled WITHOUT hitting Claude to save tokens
- Only complex Q&A ("how long to bake chicken?") routes through OpenClaw/Claude
- Qwen on Ollama (192.168.86.137) as fallback for simple Q&A

View File

@ -0,0 +1,203 @@
# Alexa+ UI Design Patterns for Smart Displays
> Research compiled Feb 2026 from Amazon official sources, The Verge, CNET, PCMag, Android Central
## 1. Key UI Patterns
### Home Screen / Dashboard Mode
- **Personalized home screen** that adapts based on user, time of day, and proximity
- **Visual ID** (facial recognition via 13MP camera) — recognizes who approaches and personalizes the display (your calendar, your smart home favorites)
- **Proximity-aware UI**: Shows larger fonts/info when you're far away, detailed widgets when close
- **Ambient brightness adaptation** via ambient light sensor
- **Content rotation**: Home screen cycles through widgets, photos, suggestions, weather, calendar events
- **Home Content Categories** toggle: Users can enable/disable content types (recipes, news, shopping suggestions, etc.) via Settings > Display & Appearance > Home Content Categories
### Conversation Mode (Alexa+ AI Interaction)
- **Voice-first, screen-second**: Conversation transcription appears on-screen during voice interaction
- **Visual aids**: Alexa+ shows pictures, videos, recipe cards, or product images as contextual visual responses
- **Multi-turn conversation**: Follow-up questions without re-saying wake word; conversation context maintained
- **No persistent chat window**: The conversation UI appears as an overlay during interaction and returns to dashboard when done — it's NOT a chat app
- **Suggestion chips**: When idle, Alexa+ can display "things to try" suggestions on the home screen
### Smart Home Dashboard Mode
- **Full-screen smart home dashboard** accessed via Menu > Smart Home or voice ("Alexa, open my smart home dashboard")
- **Derived from Echo Hub interface** — grid of device controls with room switching
- **Map View** integration — visual floor plan layout of devices (from Alexa Map View feature)
- **Camera feeds** inline — view Ring/security camera live feeds directly
- **Home/Away/Night modes** — single-tap to change home state
- **Device status at a glance** — lights, locks, thermostats, cameras all visible
### Media/Entertainment Mode
- **Media control center**: Dedicated browsing pages for music, ambient sounds, podcasts, books
- **TV & Videos experience**: Aggregated content from multiple streaming providers
- **Full-screen video playback** for shows, recipes, video calls
## 2. Color Schemes and Typography
### Visual Design Language
- **Clean, modern aesthetic** — described as "cleaner, sleeker full-screen UI" (The Verge)
- **Dark backgrounds** with bright accent elements for widgets (typical of ambient displays)
- **Adaptive brightness** — screen adjusts to room lighting automatically
- **Photo-forward**: When idle, displays personal photos from Amazon Photos as ambient wallpaper/slideshow
- **Card-based UI**: Information presented in distinct card/widget containers with rounded corners
- **High contrast readability**: Font sizes scale based on distance (proximity sensor)
### Typography
- Amazon's custom typeface family (Amazon Ember)
- **Large, glanceable text** for time, weather, calendar — designed to be read across a room
- **Smaller detail text** when user is close/interacting
- **Bold headers** for widget titles, lighter weight for content
### Color Palette (observed from press images)
- **Background**: Dark navy/charcoal or photo wallpaper
- **Widget cards**: Semi-transparent dark cards with white text, or light cards with dark text
- **Accent colors**: Alexa blue (#00CAFF-ish), warm amber for alerts/reminders
- **Smart home controls**: Color-coded by device type (warm yellow for lights, blue for locks, etc.)
- **Conversation UI**: Blue gradient for Alexa responses, lighter for user transcription
## 3. Widget Types and Layouts
### Available Widget Types (Echo Show 15/21 + new Echo Show 8/11)
- **Clock/Time** — large, always-visible
- **Weather** — current conditions + forecast
- **Calendar** — daily/weekly/monthly views, multi-calendar family support
- **Smart Home controls** — quick-access device toggles
- **Camera feeds** — live view from Ring cameras
- **Shopping list** — editable list
- **To-do / Tasks** — task widget (coming soon as of late 2025)
- **Reminders** — upcoming reminders
- **Music/Now Playing** — currently playing media with controls
- **Photos** — Amazon Photos slideshow
- **Timers** — multiple cooking timers visible simultaneously
- **Recipes** — personalized recipe suggestions (coming soon)
- **News/Headlines** — rotating news content
- **Sticky Notes** — personal notes
- **Skill widgets** — third-party skill mini-views
- **Personalized notifications** — follow-ups from recent conversations
### Layout System
- **Grid-based widget layout** — widgets snap to a grid
- **Resizable widgets** — can be expanded/collapsed
- **Drag-and-drop rearrangement** — long-press to enter edit mode
- **Widget Gallery** — swipe down from top to access available widgets
- **Different layouts for different screen sizes**:
- Echo Show 8 (8.7"): 1-2 widget columns, more compact
- Echo Show 11 (11"): 2-3 widget columns
- Echo Show 15 (15.6"): Full widget panel, landscape orientation, wall-mountable
- Echo Show 21 (21"): Largest canvas, most widgets visible simultaneously
- **Left panel + right content**: On larger screens (15, 21), widget sidebar on left with main content area on right
### Widget Interaction
- **Tap to expand** — widgets open to full-screen detail view
- **Swipe left** on widget pane handle to access rearrangement
- **Voice-addressable** — "Alexa, show my calendar" opens calendar widget full-screen
## 4. Conversation Interface Behavior
### How It Appears
1. **Wake word trigger** ("Alexa") — blue animation ring/bar appears at bottom of screen
2. **Listening state** — screen shows blue animated waveform/indicator
3. **Processing** — brief thinking animation
4. **Response** — text transcription of conversation appears; visual content (images, cards, lists) shown alongside
5. **Multi-turn**: Screen stays in conversation mode; displays ongoing transcript
6. **Suggestion follow-ups**: After response, may show tappable suggestion chips
### How It Disappears
1. **Conversation ends** (timeout or "thank you") — UI fades back to home screen
2. **Gradual transition** — conversation results may persist briefly as a notification/widget
3. **No chat history** visible on home screen — it's ephemeral
4. **Results persist contextually**: e.g., if you asked about weather, the weather widget may be promoted to prominent position
### Visual Treatment During Conversation
- **Overlay model**: Conversation takes over most of the screen but feels temporary
- **Blue accent theming** during active Alexa interaction
- **Cards/results** appear with smooth animations
- **Shopping lists, recipes, timers** created during conversation persist as widgets after conversation ends
## 5. Home Dashboard vs Chat App — Design Philosophy
### What Makes It Feel Like a HOME DASHBOARD:
- **Ambient-first**: Default state is glanceable information, not a conversation thread
- **Photo wallpaper**: Personal photos rotate as background — feels personal and decorative
- **Widget grid**: Calendar, weather, smart home status visible at a glance — like a family command center
- **Proactive, not reactive**: Shows relevant info (upcoming events, weather changes) without being asked
- **Environmental awareness**: Uses presence sensors, temperature, ambient light to adapt
- **Family-centric**: Visual ID recognizes different family members, shows personalized content per person
- **Physical space integration**: Smart home controls are front-and-center; the device is about controlling YOUR space
- **Always-on display**: Designed to be glanced at throughout the day, not actively used continuously
### What Makes It NOT a Chat App:
- **No persistent conversation history** on the display
- **No text input by default** (keyboard available for accessibility but voice is primary)
- **Conversation is ephemeral** — appears and disappears with the interaction
- **No message threads or bubbles** — responses are full-screen cards, not chat bubbles
- **Content-forward**: After a conversation, the RESULT persists (timer, reminder, shopping list) not the conversation itself
- **The display returns to ambient/dashboard mode** — chat doesn't persist as the primary view
### Key Design Principles:
1. **Glanceable over readable** — information hierarchy favors quick scanning
2. **Voice-first, touch-second** — primary interaction is voice; touch is supplementary
3. **Contextual intelligence** — UI adapts to who's looking, time of day, what's happening
4. **Ambient computing** — the device fades into the background of your home, activates when needed
5. **Family hub** — shared device, personalized per user, central to household operations
6. **Proactive assistance** — surface relevant info before being asked (upcoming event, package delivery, missed routine)
## 6. Screenshot URLs and Visual References
### Official Amazon Press Images
- About Amazon Echo Show guide: https://www.aboutamazon.com/news/devices/getting-started-echo-show-8-11-alexa-plus-features
- New Alexa+ features overview: https://www.aboutamazon.com/news/devices/new-alexa-generative-artificial-intelligence
### The Verge Hands-On Photos
- Smart home dashboard on Echo Show 21: https://platform.theverge.com/wp-content/uploads/sites/2/2025/02/IMG_2557.jpeg
- Echo Show UI with widgets: https://platform.theverge.com/wp-content/uploads/sites/2/2025/02/IMG_2564.jpeg
- Shopping list UI: https://platform.theverge.com/wp-content/uploads/sites/2/2025/02/shopping-list.png
- Full article: https://www.theverge.com/news/621008/hands-on-with-alexa-plus-smart-home-echo-show-21
### Android Central Widget Customization Screenshots
- Echo Show 15 widget gallery: https://cdn.mos.cms.futurecdn.net/mcx9KfDjTDnTyTWVygkk73.jpg
- Widget pane expanded: https://cdn.mos.cms.futurecdn.net/WivGCjDJhDdAA3FYWeJFPW.jpg
- Widget rearrangement: https://cdn.mos.cms.futurecdn.net/fChVCpuYSsEDewQUpxb3QQ.jpg
- Widget customization: https://cdn.mos.cms.futurecdn.net/5B84ngX2GzNqRsdy33aUua.jpg
- Full article: https://www.androidcentral.com/how-customize-alexa-widgets-amazon-echo-show-15
### Review Articles with UI Photos
- CNET Echo Show 11 review: https://www.cnet.com/home/smart-home/this-smart-display-is-the-best-add-on-my-kitchen-has-ever-had/
- PCMag Echo Show 8 (2025) review: https://www.pcmag.com/reviews/amazon-echo-show-8-4th-gen-2025
## 7. Technical Framework Notes
### Alexa Presentation Language (APL)
- Amazon's visual design framework for building interactive voice+visual experiences
- Supports responsive layouts across different Echo Show screen sizes
- Developers can build custom visual skill responses using APL
- Reference: https://developer.amazon.com/en-US/alexa/alexa-haus/alexa-presentation-language
### Device Specifications (for UI design reference)
| Device | Screen Size | Resolution | Orientation |
|--------|------------|------------|-------------|
| Echo Show 5 | 5.5" | 960×480 | Landscape |
| Echo Show 8 (2025) | 8.7" | 1340×800 | Landscape |
| Echo Show 11 (2025) | 11" | 1920×1200 | Landscape |
| Echo Show 15 | 15.6" | 1920×1080 | Landscape (wall-mount) |
| Echo Show 21 | 21" | 1920×1200 | Landscape (wall-mount) |
### Key Hardware Features Relevant to UI
- **AZ3 Pro chip** with AI accelerator — enables smooth transitions, multitasking between voice and visual
- **Omnisense presence sensor** (ultrasound + Wi-Fi radar) — fine motion detection for proximity-aware UI
- **13MP camera with Visual ID** — facial recognition for personalization
- **Ambient light sensor** — adaptive brightness
- **Ambient temperature sensor** — can trigger routines
---
## 8. Key Takeaways for KIPP
1. **Dashboard-first, conversation-second**: The display should feel like a home information hub that happens to have AI conversation capabilities, not a chatbot with a screen
2. **Ephemeral conversation UI**: Conversations appear as overlays and fade back to dashboard — results persist, conversations don't
3. **Widget grid system**: Modular, customizable cards that users arrange to their preference
4. **Proximity awareness**: Different information density based on distance from screen
5. **Visual ID / personalization**: Per-user customization is key to making a shared device feel personal
6. **Ambient mode is the default state**: Photos, clock, weather — the device should be beautiful when idle
7. **Proactive intelligence**: Surface information before being asked — upcoming events, deliveries, routine changes
8. **Voice-first interaction model**: Touch/visual is supplementary to voice

View File

@ -1,61 +1,125 @@
{ {
"cash": 53477.43000000002, "cash": 870.6802087402511,
"positions": { "positions": {
"DUOL": {
"shares": 57,
"avg_cost": 116.35,
"current_price": 116.35,
"entry_date": "2026-02-09T10:55:58.243598",
"entry_reason": "GARP signal: PE=14.65, FwdPE=14.71, RevGr=41.1%, EPSGr=1114.3%, RSI=23.44",
"trailing_stop": 104.715
},
"ALLY": { "ALLY": {
"shares": 156, "shares": 156,
"avg_cost": 42.65, "avg_cost": 42.65,
"current_price": 42.65, "current_price": 42.38999938964844,
"entry_date": "2026-02-09T10:55:58.244488", "entry_date": "2026-02-09T10:55:58.244488",
"entry_reason": "GARP signal: PE=18.0, FwdPE=6.76, RevGr=12.0%, EPSGr=265.4%, RSI=53.23", "entry_reason": "GARP signal: PE=18.0, FwdPE=6.76, RevGr=12.0%, EPSGr=265.4%, RSI=53.23",
"trailing_stop": 38.385 "trailing_stop": 38.50199890136719
}, },
"JHG": { "JHG": {
"shares": 138, "shares": 138,
"avg_cost": 48.21, "avg_cost": 48.21,
"current_price": 48.21, "current_price": 48.2400016784668,
"entry_date": "2026-02-09T10:55:58.245351", "entry_date": "2026-02-09T10:55:58.245351",
"entry_reason": "GARP signal: PE=9.22, FwdPE=9.96, RevGr=61.3%, EPSGr=243.6%, RSI=68.71", "entry_reason": "GARP signal: PE=9.22, FwdPE=9.96, RevGr=61.3%, EPSGr=243.6%, RSI=68.71",
"trailing_stop": 43.389 "trailing_stop": 43.46999931335449
}, },
"INCY": { "INCY": {
"shares": 61, "shares": 61,
"avg_cost": 108.69, "avg_cost": 108.69,
"current_price": 108.69, "current_price": 100.05000305175781,
"entry_date": "2026-02-09T10:55:58.246289", "entry_date": "2026-02-09T10:55:58.246289",
"entry_reason": "GARP signal: PE=18.42, FwdPE=13.76, RevGr=20.0%, EPSGr=290.7%, RSI=63.48", "entry_reason": "GARP signal: PE=18.42, FwdPE=13.76, RevGr=20.0%, EPSGr=290.7%, RSI=63.48",
"trailing_stop": 97.821 "trailing_stop": 98.12699890136719
}, },
"PINS": { "PINS": {
"shares": 332, "shares": 332,
"avg_cost": 20.06, "avg_cost": 20.06,
"current_price": 20.06, "current_price": 20.329999923706055,
"entry_date": "2026-02-09T10:55:58.247262", "entry_date": "2026-02-09T10:55:58.247262",
"entry_reason": "GARP signal: PE=7.04, FwdPE=10.61, RevGr=16.8%, EPSGr=225.0%, RSI=19.14", "entry_reason": "GARP signal: PE=7.04, FwdPE=10.61, RevGr=16.8%, EPSGr=225.0%, RSI=19.14",
"trailing_stop": 18.054 "trailing_stop": 18.59850082397461
}, },
"EXEL": { "EXEL": {
"shares": 152, "shares": 152,
"avg_cost": 43.8, "avg_cost": 43.8,
"current_price": 43.8, "current_price": 42.97999954223633,
"entry_date": "2026-02-09T10:55:58.252764", "entry_date": "2026-02-09T10:55:58.252764",
"entry_reason": "GARP signal: PE=18.4, FwdPE=12.76, RevGr=10.8%, EPSGr=72.5%, RSI=50.12", "entry_reason": "GARP signal: PE=18.4, FwdPE=12.76, RevGr=10.8%, EPSGr=72.5%, RSI=50.12",
"trailing_stop": 39.42 "trailing_stop": 39.573001098632815
}, },
"CART": { "CART": {
"shares": 187, "shares": 187,
"avg_cost": 35.49, "avg_cost": 35.49,
"current_price": 35.49, "current_price": 34.619998931884766,
"entry_date": "2026-02-09T10:55:58.254418", "entry_date": "2026-02-09T10:55:58.254418",
"entry_reason": "GARP signal: PE=19.5, FwdPE=9.05, RevGr=10.2%, EPSGr=21.1%, RSI=37.75", "entry_reason": "GARP signal: PE=19.5, FwdPE=9.05, RevGr=10.2%, EPSGr=21.1%, RSI=37.75",
"trailing_stop": 31.941000000000003 "trailing_stop": 31.941000000000003
},
"UBSI": {
"shares": 148,
"avg_cost": 44.93,
"current_price": 44.63999938964844,
"entry_date": "2026-02-10T09:06:30.696005",
"entry_reason": "GARP signal: PE=13.74, FwdPE=11.93, RevGr=22.1%, EPSGr=32.1%, RSI=67.45",
"trailing_stop": 40.437
},
"WTFC": {
"shares": 42,
"avg_cost": 158.12,
"current_price": 156.07000732421875,
"entry_date": "2026-02-10T09:06:30.699573",
"entry_reason": "GARP signal: PE=13.87, FwdPE=11.79, RevGr=10.5%, EPSGr=19.4%, RSI=62.2",
"trailing_stop": 142.30800000000002
},
"FHN": {
"shares": 258,
"avg_cost": 25.64,
"current_price": 25.64,
"entry_date": "2026-02-10T15:36:28.434830",
"entry_reason": "GARP signal: PE=13.71, FwdPE=10.94, RevGr=23.7%, EPSGr=74.9%, RSI=58.44",
"trailing_stop": 23.076
},
"FNB": {
"shares": 354,
"avg_cost": 18.69,
"current_price": 18.69,
"entry_date": "2026-02-10T15:36:28.437094",
"entry_reason": "GARP signal: PE=11.98, FwdPE=9.55, RevGr=26.4%, EPSGr=56.5%, RSI=62.57",
"trailing_stop": 16.821
},
"WAL": {
"shares": 69,
"avg_cost": 94.92,
"current_price": 94.92,
"entry_date": "2026-02-10T15:36:28.439819",
"entry_reason": "GARP signal: PE=10.87, FwdPE=7.98, RevGr=16.6%, EPSGr=32.9%, RSI=60.46",
"trailing_stop": 85.428
},
"ONB": {
"shares": 259,
"avg_cost": 25.53,
"current_price": 25.53,
"entry_date": "2026-02-10T15:36:28.441188",
"entry_reason": "GARP signal: PE=14.26, FwdPE=8.9, RevGr=41.4%, EPSGr=17.2%, RSI=68.73",
"trailing_stop": 22.977
},
"ZION": {
"shares": 103,
"avg_cost": 64.08,
"current_price": 64.08,
"entry_date": "2026-02-10T15:36:28.442626",
"entry_reason": "GARP signal: PE=10.66, FwdPE=9.8, RevGr=13.6%, EPSGr=31.4%, RSI=60.76",
"trailing_stop": 57.672
},
"EWBC": {
"shares": 54,
"avg_cost": 120.54,
"current_price": 120.54,
"entry_date": "2026-02-10T15:36:28.444928",
"entry_reason": "GARP signal: PE=12.66, FwdPE=11.0, RevGr=21.6%, EPSGr=21.3%, RSI=65.92",
"trailing_stop": 108.486
},
"BAC": {
"shares": 119,
"avg_cost": 55.39,
"current_price": 55.39,
"entry_date": "2026-02-10T15:36:28.446464",
"entry_reason": "GARP signal: PE=14.54, FwdPE=11.17, RevGr=13.2%, EPSGr=20.9%, RSI=69.17",
"trailing_stop": 49.851
} }
} }
} }

View File

@ -1,10 +1,18 @@
[ [
{ {
"date": "2026-02-09", "date": "2026-02-09",
"total_value": 100000.0, "total_value": 100055.9,
"total_pnl": 0.0, "total_pnl": 55.9,
"pnl_pct": 0.0, "pnl_pct": 0.06,
"cash": 53477.43, "cash": 60255.3,
"num_positions": 7 "num_positions": 6
},
{
"date": "2026-02-10",
"total_value": 99255.75,
"total_pnl": -744.25,
"pnl_pct": -0.74,
"cash": 870.68,
"num_positions": 15
} }
] ]

View File

@ -61,5 +61,97 @@
"cost": 6636.63, "cost": 6636.63,
"reason": "GARP signal: PE=19.5, FwdPE=9.05, RevGr=10.2%, EPSGr=21.1%, RSI=37.75", "reason": "GARP signal: PE=19.5, FwdPE=9.05, RevGr=10.2%, EPSGr=21.1%, RSI=37.75",
"timestamp": "2026-02-09T10:55:58.254721" "timestamp": "2026-02-09T10:55:58.254721"
},
{
"action": "SELL",
"ticker": "DUOL",
"shares": 57,
"price": 118.91000366210938,
"proceeds": 6777.87,
"realized_pnl": 145.92,
"entry_price": 116.35,
"reason": "No longer passes GARP filter",
"timestamp": "2026-02-09T15:36:18.884898"
},
{
"action": "BUY",
"ticker": "UBSI",
"shares": 148,
"price": 44.93,
"cost": 6649.64,
"reason": "GARP signal: PE=13.74, FwdPE=11.93, RevGr=22.1%, EPSGr=32.1%, RSI=67.45",
"timestamp": "2026-02-10T09:06:30.696435"
},
{
"action": "BUY",
"ticker": "WTFC",
"shares": 42,
"price": 158.12,
"cost": 6641.04,
"reason": "GARP signal: PE=13.87, FwdPE=11.79, RevGr=10.5%, EPSGr=19.4%, RSI=62.2",
"timestamp": "2026-02-10T09:06:30.699988"
},
{
"action": "BUY",
"ticker": "FHN",
"shares": 258,
"price": 25.64,
"cost": 6615.12,
"reason": "GARP signal: PE=13.71, FwdPE=10.94, RevGr=23.7%, EPSGr=74.9%, RSI=58.44",
"timestamp": "2026-02-10T15:36:28.436095"
},
{
"action": "BUY",
"ticker": "FNB",
"shares": 354,
"price": 18.69,
"cost": 6616.26,
"reason": "GARP signal: PE=11.98, FwdPE=9.55, RevGr=26.4%, EPSGr=56.5%, RSI=62.57",
"timestamp": "2026-02-10T15:36:28.437460"
},
{
"action": "BUY",
"ticker": "WAL",
"shares": 69,
"price": 94.92,
"cost": 6549.48,
"reason": "GARP signal: PE=10.87, FwdPE=7.98, RevGr=16.6%, EPSGr=32.9%, RSI=60.46",
"timestamp": "2026-02-10T15:36:28.440182"
},
{
"action": "BUY",
"ticker": "ONB",
"shares": 259,
"price": 25.53,
"cost": 6612.27,
"reason": "GARP signal: PE=14.26, FwdPE=8.9, RevGr=41.4%, EPSGr=17.2%, RSI=68.73",
"timestamp": "2026-02-10T15:36:28.441586"
},
{
"action": "BUY",
"ticker": "ZION",
"shares": 103,
"price": 64.08,
"cost": 6600.24,
"reason": "GARP signal: PE=10.66, FwdPE=9.8, RevGr=13.6%, EPSGr=31.4%, RSI=60.76",
"timestamp": "2026-02-10T15:36:28.443023"
},
{
"action": "BUY",
"ticker": "EWBC",
"shares": 54,
"price": 120.54,
"cost": 6509.16,
"reason": "GARP signal: PE=12.66, FwdPE=11.0, RevGr=21.6%, EPSGr=21.3%, RSI=65.92",
"timestamp": "2026-02-10T15:36:28.445341"
},
{
"action": "BUY",
"ticker": "BAC",
"shares": 119,
"price": 55.39,
"cost": 6591.41,
"reason": "GARP signal: PE=14.54, FwdPE=11.17, RevGr=13.2%, EPSGr=20.9%, RSI=69.17",
"timestamp": "2026-02-10T15:36:28.446981"
} }
] ]

View File

@ -194,5 +194,117 @@
"ticker": "WTFC", "ticker": "WTFC",
"reason": "RSI too high (72.6 > 70)", "reason": "RSI too high (72.6 > 70)",
"details": {} "details": {}
},
{
"timestamp": "2026-02-09T15:36:18.885237",
"action": "SELL",
"ticker": "DUOL",
"reason": "No longer passes GARP filter",
"details": {
"success": true,
"ticker": "DUOL",
"shares": 57,
"price": 118.91000366210938,
"proceeds": 6777.87,
"realized_pnl": 145.92
}
},
{
"timestamp": "2026-02-09T15:36:19.302964",
"action": "SKIP",
"ticker": "VLY",
"reason": "RSI too high (78.3 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T15:36:19.303492",
"action": "SKIP",
"ticker": "FHN",
"reason": "RSI too high (72.4 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T15:36:19.304721",
"action": "SKIP",
"ticker": "FNB",
"reason": "RSI too high (71.0 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T15:36:19.305710",
"action": "SKIP",
"ticker": "SSB",
"reason": "RSI too high (89.0 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T15:36:19.306687",
"action": "SKIP",
"ticker": "WBS",
"reason": "RSI too high (82.0 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T15:36:19.307754",
"action": "SKIP",
"ticker": "ONB",
"reason": "RSI too high (77.6 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T15:36:19.308706",
"action": "SKIP",
"ticker": "WAL",
"reason": "RSI too high (71.7 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T15:36:19.309624",
"action": "SKIP",
"ticker": "ZION",
"reason": "RSI too high (73.3 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T15:36:19.310657",
"action": "SKIP",
"ticker": "CFG",
"reason": "RSI too high (78.5 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T15:36:19.311641",
"action": "SKIP",
"ticker": "UBSI",
"reason": "RSI too high (77.5 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T15:36:19.312722",
"action": "SKIP",
"ticker": "EWBC",
"reason": "RSI too high (78.6 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T15:36:19.313689",
"action": "SKIP",
"ticker": "FITB",
"reason": "Too close to 52wk high (1.9% away)",
"details": {}
},
{
"timestamp": "2026-02-09T15:36:19.314689",
"action": "SKIP",
"ticker": "BAC",
"reason": "RSI too high (78.1 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T15:36:19.315714",
"action": "SKIP",
"ticker": "WTFC",
"reason": "RSI too high (70.2 > 70)",
"details": {}
} }
] ]

View File

@ -0,0 +1,240 @@
[
{
"timestamp": "2026-02-10T09:06:30.678934",
"action": "SKIP",
"ticker": "VLY",
"reason": "RSI too high (74.2 > 70)",
"details": {}
},
{
"timestamp": "2026-02-10T09:06:30.689088",
"action": "SKIP",
"ticker": "FHN",
"reason": "Too close to 52wk high (1.5% away)",
"details": {}
},
{
"timestamp": "2026-02-10T09:06:30.689343",
"action": "SKIP",
"ticker": "FNB",
"reason": "Too close to 52wk high (1.1% away)",
"details": {}
},
{
"timestamp": "2026-02-10T09:06:30.690376",
"action": "SKIP",
"ticker": "SSB",
"reason": "RSI too high (85.0 > 70)",
"details": {}
},
{
"timestamp": "2026-02-10T09:06:30.691362",
"action": "SKIP",
"ticker": "WBS",
"reason": "RSI too high (79.7 > 70)",
"details": {}
},
{
"timestamp": "2026-02-10T09:06:30.692156",
"action": "SKIP",
"ticker": "ONB",
"reason": "RSI too high (71.2 > 70)",
"details": {}
},
{
"timestamp": "2026-02-10T09:06:30.692901",
"action": "SKIP",
"ticker": "WAL",
"reason": "Too close to 52wk high (1.0% away)",
"details": {}
},
{
"timestamp": "2026-02-10T09:06:30.694010",
"action": "SKIP",
"ticker": "ZION",
"reason": "Too close to 52wk high (1.5% away)",
"details": {}
},
{
"timestamp": "2026-02-10T09:06:30.694846",
"action": "SKIP",
"ticker": "CFG",
"reason": "RSI too high (72.1 > 70)",
"details": {}
},
{
"timestamp": "2026-02-10T09:06:30.696787",
"action": "BUY",
"ticker": "UBSI",
"reason": "GARP signal: PE=13.74, FwdPE=11.93, RevGr=22.1%, EPSGr=32.1%, RSI=67.45",
"details": {
"success": true,
"ticker": "UBSI",
"shares": 148,
"price": 44.93,
"cost": 6649.64,
"cash_remaining": 53605.66
}
},
{
"timestamp": "2026-02-10T09:06:30.697122",
"action": "SKIP",
"ticker": "EWBC",
"reason": "RSI too high (74.7 > 70)",
"details": {}
},
{
"timestamp": "2026-02-10T09:06:30.697710",
"action": "SKIP",
"ticker": "FITB",
"reason": "Too close to 52wk high (1.2% away)",
"details": {}
},
{
"timestamp": "2026-02-10T09:06:30.698565",
"action": "SKIP",
"ticker": "BAC",
"reason": "RSI too high (78.8 > 70)",
"details": {}
},
{
"timestamp": "2026-02-10T09:06:30.700317",
"action": "BUY",
"ticker": "WTFC",
"reason": "GARP signal: PE=13.87, FwdPE=11.79, RevGr=10.5%, EPSGr=19.4%, RSI=62.2",
"details": {
"success": true,
"ticker": "WTFC",
"shares": 42,
"price": 158.12,
"cost": 6641.04,
"cash_remaining": 46964.62
}
},
{
"timestamp": "2026-02-10T15:36:28.432393",
"action": "SKIP",
"ticker": "VLY",
"reason": "RSI too high (71.8 > 70)",
"details": {}
},
{
"timestamp": "2026-02-10T15:36:28.436450",
"action": "BUY",
"ticker": "FHN",
"reason": "GARP signal: PE=13.71, FwdPE=10.94, RevGr=23.7%, EPSGr=74.9%, RSI=58.44",
"details": {
"success": true,
"ticker": "FHN",
"shares": 258,
"price": 25.64,
"cost": 6615.12,
"cash_remaining": 40349.5
}
},
{
"timestamp": "2026-02-10T15:36:28.437790",
"action": "BUY",
"ticker": "FNB",
"reason": "GARP signal: PE=11.98, FwdPE=9.55, RevGr=26.4%, EPSGr=56.5%, RSI=62.57",
"details": {
"success": true,
"ticker": "FNB",
"shares": 354,
"price": 18.69,
"cost": 6616.26,
"cash_remaining": 33733.24
}
},
{
"timestamp": "2026-02-10T15:36:28.438236",
"action": "SKIP",
"ticker": "SSB",
"reason": "RSI too high (71.1 > 70)",
"details": {}
},
{
"timestamp": "2026-02-10T15:36:28.438922",
"action": "SKIP",
"ticker": "WBS",
"reason": "RSI too high (78.0 > 70)",
"details": {}
},
{
"timestamp": "2026-02-10T15:36:28.440504",
"action": "BUY",
"ticker": "WAL",
"reason": "GARP signal: PE=10.87, FwdPE=7.98, RevGr=16.6%, EPSGr=32.9%, RSI=60.46",
"details": {
"success": true,
"ticker": "WAL",
"shares": 69,
"price": 94.92,
"cost": 6549.48,
"cash_remaining": 27183.76
}
},
{
"timestamp": "2026-02-10T15:36:28.441912",
"action": "BUY",
"ticker": "ONB",
"reason": "GARP signal: PE=14.26, FwdPE=8.9, RevGr=41.4%, EPSGr=17.2%, RSI=68.73",
"details": {
"success": true,
"ticker": "ONB",
"shares": 259,
"price": 25.53,
"cost": 6612.27,
"cash_remaining": 20571.49
}
},
{
"timestamp": "2026-02-10T15:36:28.443384",
"action": "BUY",
"ticker": "ZION",
"reason": "GARP signal: PE=10.66, FwdPE=9.8, RevGr=13.6%, EPSGr=31.4%, RSI=60.76",
"details": {
"success": true,
"ticker": "ZION",
"shares": 103,
"price": 64.08,
"cost": 6600.24,
"cash_remaining": 13971.25
}
},
{
"timestamp": "2026-02-10T15:36:28.443883",
"action": "SKIP",
"ticker": "CFG",
"reason": "Too close to 52wk high (1.8% away)",
"details": {}
},
{
"timestamp": "2026-02-10T15:36:28.445705",
"action": "BUY",
"ticker": "EWBC",
"reason": "GARP signal: PE=12.66, FwdPE=11.0, RevGr=21.6%, EPSGr=21.3%, RSI=65.92",
"details": {
"success": true,
"ticker": "EWBC",
"shares": 54,
"price": 120.54,
"cost": 6509.16,
"cash_remaining": 7462.09
}
},
{
"timestamp": "2026-02-10T15:36:28.447352",
"action": "BUY",
"ticker": "BAC",
"reason": "GARP signal: PE=14.54, FwdPE=11.17, RevGr=13.2%, EPSGr=20.9%, RSI=69.17",
"details": {
"success": true,
"ticker": "BAC",
"shares": 119,
"price": 55.39,
"cost": 6591.41,
"cash_remaining": 870.68
}
}
]

View File

@ -1,356 +1,338 @@
{ {
"date": "2026-02-09", "date": "2026-02-09",
"timestamp": "2026-02-09T10:55:58.242176", "timestamp": "2026-02-09T15:36:18.290971",
"total_scanned": 902, "total_scanned": 902,
"candidates_found": 21, "candidates_found": 20,
"candidates": [ "candidates": [
{
"ticker": "DUOL",
"price": 116.35,
"market_cap": 5378552832,
"market_cap_b": 5.4,
"trailing_pe": 14.65,
"forward_pe": 14.71,
"peg_ratio": null,
"revenue_growth": 41.1,
"earnings_growth": 1114.3,
"roe": 36.2,
"quick_ratio": 2.6,
"debt_to_equity": 7.4,
"rsi": 23.44,
"week52_high": 544.93,
"pct_from_52wk_high": 78.6,
"score": -100.83
},
{ {
"ticker": "ALLY", "ticker": "ALLY",
"price": 42.65, "price": 42.04,
"market_cap": 13158768640, "market_cap": 12969046016,
"market_cap_b": 13.2, "market_cap_b": 13.0,
"trailing_pe": 18.0, "trailing_pe": 17.74,
"forward_pe": 6.76, "forward_pe": 6.66,
"peg_ratio": null, "peg_ratio": null,
"revenue_growth": 12.0, "revenue_growth": 12.0,
"earnings_growth": 265.4, "earnings_growth": 265.4,
"roe": 5.8, "roe": 5.8,
"quick_ratio": null, "quick_ratio": null,
"debt_to_equity": null, "debt_to_equity": null,
"rsi": 53.23, "rsi": 49.52,
"week52_high": 47.27, "week52_high": 47.27,
"pct_from_52wk_high": 9.8, "pct_from_52wk_high": 11.1,
"score": -20.98 "score": -21.08
}, },
{ {
"ticker": "JHG", "ticker": "JHG",
"price": 48.21, "price": 48.2,
"market_cap": 7447323136, "market_cap": 7445763584,
"market_cap_b": 7.4, "market_cap_b": 7.4,
"trailing_pe": 9.22, "trailing_pe": 9.22,
"forward_pe": 9.96, "forward_pe": 9.95,
"peg_ratio": null, "peg_ratio": null,
"revenue_growth": 61.3, "revenue_growth": 61.3,
"earnings_growth": 243.6, "earnings_growth": 243.6,
"roe": 16.2, "roe": 16.2,
"quick_ratio": 69.46, "quick_ratio": 69.46,
"debt_to_equity": 6.5, "debt_to_equity": 6.5,
"rsi": 68.71, "rsi": 68.18,
"week52_high": 49.42, "week52_high": 49.42,
"pct_from_52wk_high": 2.4, "pct_from_52wk_high": 2.5,
"score": -20.529999999999998 "score": -20.54
}, },
{ {
"ticker": "INCY", "ticker": "INCY",
"price": 108.69, "price": 109.03,
"market_cap": 21338314752, "market_cap": 21405063168,
"market_cap_b": 21.3, "market_cap_b": 21.4,
"trailing_pe": 18.42, "trailing_pe": 18.48,
"forward_pe": 13.76, "forward_pe": 13.81,
"peg_ratio": null, "peg_ratio": null,
"revenue_growth": 20.0, "revenue_growth": 20.0,
"earnings_growth": 290.7, "earnings_growth": 290.7,
"roe": 30.4, "roe": 30.4,
"quick_ratio": 2.86, "quick_ratio": 2.86,
"debt_to_equity": 0.9, "debt_to_equity": 0.9,
"rsi": 63.48, "rsi": 64.03,
"week52_high": 112.29, "week52_high": 112.29,
"pct_from_52wk_high": 3.2, "pct_from_52wk_high": 2.9,
"score": -17.310000000000002 "score": -17.259999999999998
}, },
{ {
"ticker": "PINS", "ticker": "PINS",
"price": 20.06, "price": 20.14,
"market_cap": 13635989504, "market_cap": 13693783040,
"market_cap_b": 13.6, "market_cap_b": 13.7,
"trailing_pe": 7.04, "trailing_pe": 7.07,
"forward_pe": 10.61, "forward_pe": 10.66,
"peg_ratio": null, "peg_ratio": null,
"revenue_growth": 16.8, "revenue_growth": 16.8,
"earnings_growth": 225.0, "earnings_growth": 225.0,
"roe": 51.5, "roe": 51.5,
"quick_ratio": 8.14, "quick_ratio": 8.14,
"debt_to_equity": 4.3, "debt_to_equity": 4.3,
"rsi": 19.14, "rsi": 19.93,
"week52_high": 40.38, "week52_high": 40.38,
"pct_from_52wk_high": 50.3, "pct_from_52wk_high": 50.1,
"score": -13.57 "score": -13.52
}, },
{ {
"ticker": "VLY", "ticker": "VLY",
"price": 13.72, "price": 13.7,
"market_cap": 7647972352, "market_cap": 7639607296,
"market_cap_b": 7.6, "market_cap_b": 7.6,
"trailing_pe": 13.58, "trailing_pe": 13.56,
"forward_pe": 9.2, "forward_pe": 9.19,
"peg_ratio": null, "peg_ratio": null,
"revenue_growth": 38.3, "revenue_growth": 38.3,
"earnings_growth": 66.3, "earnings_growth": 66.3,
"roe": 7.8, "roe": 7.8,
"quick_ratio": null, "quick_ratio": null,
"debt_to_equity": null, "debt_to_equity": null,
"rsi": 78.6, "rsi": 78.34,
"week52_high": 13.79, "week52_high": 13.79,
"pct_from_52wk_high": 0.5, "pct_from_52wk_high": 0.7,
"score": -1.2600000000000002 "score": -1.27
}, },
{ {
"ticker": "FHN", "ticker": "FHN",
"price": 26.33, "price": 26.03,
"market_cap": 12967198720, "market_cap": 12817018880,
"market_cap_b": 13.0, "market_cap_b": 12.8,
"trailing_pe": 14.08, "trailing_pe": 13.92,
"forward_pe": 11.23, "forward_pe": 11.1,
"peg_ratio": null, "peg_ratio": null,
"revenue_growth": 23.7, "revenue_growth": 23.7,
"earnings_growth": 74.9, "earnings_growth": 74.9,
"roe": 10.9, "roe": 10.9,
"quick_ratio": null, "quick_ratio": null,
"debt_to_equity": null, "debt_to_equity": null,
"rsi": 76.1, "rsi": 72.36,
"week52_high": 26.56, "week52_high": 26.56,
"pct_from_52wk_high": 0.8, "pct_from_52wk_high": 2.0,
"score": 1.37 "score": 1.2399999999999993
}, },
{ {
"ticker": "FNB", "ticker": "FNB",
"price": 19.05, "price": 18.92,
"market_cap": 6822501376, "market_cap": 6775944192,
"market_cap_b": 6.8, "market_cap_b": 6.8,
"trailing_pe": 12.21, "trailing_pe": 12.13,
"forward_pe": 9.73, "forward_pe": 9.67,
"peg_ratio": null, "peg_ratio": null,
"revenue_growth": 26.4, "revenue_growth": 26.4,
"earnings_growth": 56.5, "earnings_growth": 56.5,
"roe": 8.7, "roe": 8.7,
"quick_ratio": null, "quick_ratio": null,
"debt_to_equity": null, "debt_to_equity": null,
"rsi": 71.92, "rsi": 70.99,
"week52_high": 19.14, "week52_high": 19.14,
"pct_from_52wk_high": 0.4, "pct_from_52wk_high": 1.1,
"score": 1.4400000000000004 "score": 1.38
}, },
{ {
"ticker": "SSB", "ticker": "SSB",
"price": 107.67, "price": 107.18,
"market_cap": 10821986304, "market_cap": 10773236736,
"market_cap_b": 10.8, "market_cap_b": 10.8,
"trailing_pe": 13.68, "trailing_pe": 13.62,
"forward_pe": 10.18, "forward_pe": 10.13,
"peg_ratio": null, "peg_ratio": null,
"revenue_growth": 53.2, "revenue_growth": 53.2,
"earnings_growth": 30.9, "earnings_growth": 30.9,
"roe": 10.7, "roe": 10.7,
"quick_ratio": null, "quick_ratio": null,
"debt_to_equity": null, "debt_to_equity": null,
"rsi": 92.25, "rsi": 89.03,
"week52_high": 108.46, "week52_high": 108.46,
"pct_from_52wk_high": 0.7, "pct_from_52wk_high": 1.2,
"score": 1.7699999999999996 "score": 1.7200000000000006
}, },
{ {
"ticker": "WBS", "ticker": "WBS",
"price": 73.28, "price": 73.21,
"market_cap": 11818547200, "market_cap": 11808063488,
"market_cap_b": 11.8, "market_cap_b": 11.8,
"trailing_pe": 12.42, "trailing_pe": 12.41,
"forward_pe": 9.79, "forward_pe": 9.78,
"peg_ratio": null, "peg_ratio": null,
"revenue_growth": 18.2, "revenue_growth": 18.2,
"earnings_growth": 53.4, "earnings_growth": 53.4,
"roe": 10.8, "roe": 10.8,
"quick_ratio": null, "quick_ratio": null,
"debt_to_equity": null, "debt_to_equity": null,
"rsi": 82.13, "rsi": 82.05,
"week52_high": 73.5, "week52_high": 73.5,
"pct_from_52wk_high": 0.3, "pct_from_52wk_high": 0.4,
"score": 2.6299999999999994 "score": 2.6199999999999997
},
{
"ticker": "WAL",
"price": 96.21,
"market_cap": 10588263424,
"market_cap_b": 10.6,
"trailing_pe": 11.02,
"forward_pe": 8.09,
"peg_ratio": null,
"revenue_growth": 16.6,
"earnings_growth": 32.9,
"roe": 13.5,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 71.81,
"week52_high": 96.94,
"pct_from_52wk_high": 0.8,
"score": 3.1399999999999997
}, },
{ {
"ticker": "ONB", "ticker": "ONB",
"price": 25.93, "price": 25.67,
"market_cap": 10132744192, "market_cap": 10031142912,
"market_cap_b": 10.1, "market_cap_b": 10.0,
"trailing_pe": 14.49, "trailing_pe": 14.34,
"forward_pe": 9.04, "forward_pe": 8.95,
"peg_ratio": null, "peg_ratio": null,
"revenue_growth": 41.4, "revenue_growth": 41.4,
"earnings_growth": 17.2, "earnings_growth": 17.2,
"roe": 9.0, "roe": 9.0,
"quick_ratio": null, "quick_ratio": null,
"debt_to_equity": null, "debt_to_equity": null,
"rsi": 81.24, "rsi": 77.64,
"week52_high": 26.17, "week52_high": 26.17,
"pct_from_52wk_high": 1.9,
"score": 3.09
},
{
"ticker": "WAL",
"price": 96.08,
"market_cap": 10573956096,
"market_cap_b": 10.6,
"trailing_pe": 11.01,
"forward_pe": 8.08,
"peg_ratio": null,
"revenue_growth": 16.6,
"earnings_growth": 32.9,
"roe": 13.5,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 71.66,
"week52_high": 96.99,
"pct_from_52wk_high": 0.9, "pct_from_52wk_high": 0.9,
"score": 3.1799999999999997 "score": 3.13
}, },
{ {
"ticker": "EXEL", "ticker": "EXEL",
"price": 43.8, "price": 43.95,
"market_cap": 11791070208, "market_cap": 11831451648,
"market_cap_b": 11.8, "market_cap_b": 11.8,
"trailing_pe": 18.4, "trailing_pe": 18.47,
"forward_pe": 12.76, "forward_pe": 12.8,
"peg_ratio": null, "peg_ratio": null,
"revenue_growth": 10.8, "revenue_growth": 10.8,
"earnings_growth": 72.5, "earnings_growth": 72.5,
"roe": 30.6, "roe": 30.6,
"quick_ratio": 3.5, "quick_ratio": 3.5,
"debt_to_equity": 8.2, "debt_to_equity": 8.2,
"rsi": 50.12, "rsi": 51.02,
"week52_high": 49.62, "week52_high": 49.62,
"pct_from_52wk_high": 11.7, "pct_from_52wk_high": 11.4,
"score": 4.43 "score": 4.470000000000001
}, },
{ {
"ticker": "ZION", "ticker": "ZION",
"price": 65.29, "price": 65.16,
"market_cap": 9640263680, "market_cap": 9621069824,
"market_cap_b": 9.6, "market_cap_b": 9.6,
"trailing_pe": 10.86, "trailing_pe": 10.84,
"forward_pe": 9.99, "forward_pe": 9.97,
"peg_ratio": null, "peg_ratio": null,
"revenue_growth": 13.6, "revenue_growth": 13.6,
"earnings_growth": 31.4, "earnings_growth": 31.4,
"roe": 13.5, "roe": 13.5,
"quick_ratio": null, "quick_ratio": null,
"debt_to_equity": null, "debt_to_equity": null,
"rsi": 74.03, "rsi": 73.29,
"week52_high": 66.18, "week52_high": 66.18,
"pct_from_52wk_high": 1.3, "pct_from_52wk_high": 1.5,
"score": 5.49 "score": 5.470000000000001
}, },
{ {
"ticker": "CART", "ticker": "CART",
"price": 35.49, "price": 35.15,
"market_cap": 9349425152, "market_cap": 9259855872,
"market_cap_b": 9.3, "market_cap_b": 9.3,
"trailing_pe": 19.5, "trailing_pe": 19.31,
"forward_pe": 9.05, "forward_pe": 8.97,
"peg_ratio": null, "peg_ratio": null,
"revenue_growth": 10.2, "revenue_growth": 10.2,
"earnings_growth": 21.1, "earnings_growth": 21.1,
"roe": 15.3, "roe": 15.3,
"quick_ratio": 3.33, "quick_ratio": 3.33,
"debt_to_equity": 1.0, "debt_to_equity": 1.0,
"rsi": 37.75, "rsi": 36.01,
"week52_high": 53.5, "week52_high": 53.5,
"pct_from_52wk_high": 33.7, "pct_from_52wk_high": 34.3,
"score": 5.92 "score": 5.84
}, },
{ {
"ticker": "CFG", "ticker": "CFG",
"price": 68.17, "price": 67.7,
"market_cap": 29278072832, "market_cap": 29076213760,
"market_cap_b": 29.3, "market_cap_b": 29.1,
"trailing_pe": 17.66, "trailing_pe": 17.54,
"forward_pe": 10.82, "forward_pe": 10.75,
"peg_ratio": null, "peg_ratio": null,
"revenue_growth": 10.7, "revenue_growth": 10.7,
"earnings_growth": 35.9, "earnings_growth": 35.9,
"roe": 7.2, "roe": 7.2,
"quick_ratio": null, "quick_ratio": null,
"debt_to_equity": null, "debt_to_equity": null,
"rsi": 80.86, "rsi": 78.47,
"week52_high": 68.65, "week52_high": 68.65,
"pct_from_52wk_high": 0.7, "pct_from_52wk_high": 1.4,
"score": 6.16 "score": 6.09
}, },
{ {
"ticker": "UBSI", "ticker": "UBSI",
"price": 45.32, "price": 45.06,
"market_cap": 6316939264, "market_cap": 6280699392,
"market_cap_b": 6.3, "market_cap_b": 6.3,
"trailing_pe": 13.86, "trailing_pe": 13.78,
"forward_pe": 12.03, "forward_pe": 11.96,
"peg_ratio": null, "peg_ratio": null,
"revenue_growth": 22.1, "revenue_growth": 22.1,
"earnings_growth": 32.1, "earnings_growth": 32.1,
"roe": 8.9, "roe": 8.9,
"quick_ratio": null, "quick_ratio": null,
"debt_to_equity": null, "debt_to_equity": null,
"rsi": 80.0, "rsi": 77.51,
"week52_high": 45.93, "week52_high": 45.93,
"pct_from_52wk_high": 1.3, "pct_from_52wk_high": 1.9,
"score": 6.61 "score": 6.54
}, },
{ {
"ticker": "EWBC", "ticker": "EWBC",
"price": 123.19, "price": 122.56,
"market_cap": 16949170176, "market_cap": 16862490624,
"market_cap_b": 16.9, "market_cap_b": 16.9,
"trailing_pe": 12.94, "trailing_pe": 12.87,
"forward_pe": 11.24, "forward_pe": 11.18,
"peg_ratio": null, "peg_ratio": null,
"revenue_growth": 21.6, "revenue_growth": 21.6,
"earnings_growth": 21.3, "earnings_growth": 21.3,
"roe": 15.9, "roe": 15.9,
"quick_ratio": null, "quick_ratio": null,
"debt_to_equity": null, "debt_to_equity": null,
"rsi": 79.27, "rsi": 78.61,
"week52_high": 123.82, "week52_high": 123.82,
"pct_from_52wk_high": 0.5, "pct_from_52wk_high": 1.0,
"score": 6.949999999999999 "score": 6.890000000000001
}, },
{ {
"ticker": "FITB", "ticker": "FITB",
"price": 54.38, "price": 54.33,
"market_cap": 48944635904, "market_cap": 48899633152,
"market_cap_b": 48.9, "market_cap_b": 48.9,
"trailing_pe": 15.41, "trailing_pe": 15.39,
"forward_pe": 11.09, "forward_pe": 11.08,
"peg_ratio": null, "peg_ratio": null,
"revenue_growth": 11.5, "revenue_growth": 11.5,
"earnings_growth": 20.8, "earnings_growth": 20.8,
"roe": 12.2, "roe": 12.2,
"quick_ratio": null, "quick_ratio": null,
"debt_to_equity": null, "debt_to_equity": null,
"rsi": 66.02, "rsi": 65.77,
"week52_high": 55.36, "week52_high": 55.36,
"pct_from_52wk_high": 1.8, "pct_from_52wk_high": 1.9,
"score": 7.859999999999999 "score": 7.85
}, },
{ {
"ticker": "BAC", "ticker": "BAC",
"price": 56.43, "price": 56.41,
"market_cap": 412079849472, "market_cap": 411933769728,
"market_cap_b": 412.1, "market_cap_b": 411.9,
"trailing_pe": 14.81, "trailing_pe": 14.81,
"forward_pe": 11.38, "forward_pe": 11.38,
"peg_ratio": null, "peg_ratio": null,
@ -359,28 +341,28 @@
"roe": 10.2, "roe": 10.2,
"quick_ratio": null, "quick_ratio": null,
"debt_to_equity": null, "debt_to_equity": null,
"rsi": 78.3, "rsi": 78.1,
"week52_high": 57.55, "week52_high": 57.55,
"pct_from_52wk_high": 1.9, "pct_from_52wk_high": 2.0,
"score": 7.970000000000001 "score": 7.970000000000001
}, },
{ {
"ticker": "WTFC", "ticker": "WTFC",
"price": 159.71, "price": 158.57,
"market_cap": 10696563712, "market_cap": 10620212224,
"market_cap_b": 10.7, "market_cap_b": 10.6,
"trailing_pe": 14.0, "trailing_pe": 13.9,
"forward_pe": 11.91, "forward_pe": 11.82,
"peg_ratio": null, "peg_ratio": null,
"revenue_growth": 10.5, "revenue_growth": 10.5,
"earnings_growth": 19.4, "earnings_growth": 19.4,
"roe": 12.1, "roe": 12.1,
"quick_ratio": null, "quick_ratio": null,
"debt_to_equity": null, "debt_to_equity": null,
"rsi": 72.56, "rsi": 70.23,
"week52_high": 162.96, "week52_high": 162.96,
"pct_from_52wk_high": 2.0, "pct_from_52wk_high": 2.7,
"score": 8.92 "score": 8.83
} }
] ]
} }

View File

@ -0,0 +1,368 @@
{
"date": "2026-02-10",
"timestamp": "2026-02-10T15:36:27.292187",
"total_scanned": 902,
"candidates_found": 20,
"candidates": [
{
"ticker": "ALLY",
"price": 42.39,
"market_cap": 13077017600,
"market_cap_b": 13.1,
"trailing_pe": 17.89,
"forward_pe": 6.72,
"peg_ratio": null,
"revenue_growth": 12.0,
"earnings_growth": 265.4,
"roe": 5.8,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 51.52,
"week52_high": 47.27,
"pct_from_52wk_high": 10.3,
"score": -21.02
},
{
"ticker": "JHG",
"price": 48.24,
"market_cap": 7451942400,
"market_cap_b": 7.5,
"trailing_pe": 9.22,
"forward_pe": 9.96,
"peg_ratio": null,
"revenue_growth": 61.3,
"earnings_growth": 243.6,
"roe": 16.2,
"quick_ratio": 69.46,
"debt_to_equity": 6.5,
"rsi": 65.85,
"week52_high": 49.42,
"pct_from_52wk_high": 2.4,
"score": -20.529999999999998
},
{
"ticker": "INCY",
"price": 100.05,
"market_cap": 19642087424,
"market_cap_b": 19.6,
"trailing_pe": 16.96,
"forward_pe": 11.19,
"peg_ratio": null,
"revenue_growth": 20.0,
"earnings_growth": 290.7,
"roe": 30.4,
"quick_ratio": 2.86,
"debt_to_equity": 0.9,
"rsi": 42.24,
"week52_high": 112.29,
"pct_from_52wk_high": 10.9,
"score": -19.880000000000003
},
{
"ticker": "PINS",
"price": 20.33,
"market_cap": 13822969856,
"market_cap_b": 13.8,
"trailing_pe": 7.13,
"forward_pe": 10.76,
"peg_ratio": null,
"revenue_growth": 16.8,
"earnings_growth": 225.0,
"roe": 51.5,
"quick_ratio": 8.14,
"debt_to_equity": 4.3,
"rsi": 22.65,
"week52_high": 39.96,
"pct_from_52wk_high": 49.1,
"score": -13.42
},
{
"ticker": "VLY",
"price": 13.62,
"market_cap": 7594996736,
"market_cap_b": 7.6,
"trailing_pe": 13.49,
"forward_pe": 9.13,
"peg_ratio": null,
"revenue_growth": 38.3,
"earnings_growth": 66.3,
"roe": 7.8,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 71.78,
"week52_high": 13.79,
"pct_from_52wk_high": 1.2,
"score": -1.3299999999999987
},
{
"ticker": "FHN",
"price": 25.64,
"market_cap": 12624985088,
"market_cap_b": 12.6,
"trailing_pe": 13.71,
"forward_pe": 10.94,
"peg_ratio": null,
"revenue_growth": 23.7,
"earnings_growth": 74.9,
"roe": 10.9,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 58.44,
"week52_high": 26.56,
"pct_from_52wk_high": 3.5,
"score": 1.0799999999999992
},
{
"ticker": "FNB",
"price": 18.69,
"market_cap": 6693572608,
"market_cap_b": 6.7,
"trailing_pe": 11.98,
"forward_pe": 9.55,
"peg_ratio": null,
"revenue_growth": 26.4,
"earnings_growth": 56.5,
"roe": 8.7,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 62.57,
"week52_high": 19.14,
"pct_from_52wk_high": 2.4,
"score": 1.2600000000000007
},
{
"ticker": "SSB",
"price": 105.17,
"market_cap": 10571200512,
"market_cap_b": 10.6,
"trailing_pe": 13.36,
"forward_pe": 9.94,
"peg_ratio": null,
"revenue_growth": 53.2,
"earnings_growth": 30.9,
"roe": 10.7,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 71.11,
"week52_high": 108.46,
"pct_from_52wk_high": 3.0,
"score": 1.5299999999999994
},
{
"ticker": "WBS",
"price": 73.01,
"market_cap": 11771847680,
"market_cap_b": 11.8,
"trailing_pe": 12.37,
"forward_pe": 9.76,
"peg_ratio": null,
"revenue_growth": 18.2,
"earnings_growth": 53.4,
"roe": 10.8,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 77.97,
"week52_high": 73.76,
"pct_from_52wk_high": 1.0,
"score": 2.6
},
{
"ticker": "WAL",
"price": 94.92,
"market_cap": 10446294016,
"market_cap_b": 10.4,
"trailing_pe": 10.87,
"forward_pe": 7.98,
"peg_ratio": null,
"revenue_growth": 16.6,
"earnings_growth": 32.9,
"roe": 13.5,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 60.46,
"week52_high": 97.23,
"pct_from_52wk_high": 2.4,
"score": 3.0300000000000002
},
{
"ticker": "ONB",
"price": 25.53,
"market_cap": 9976434688,
"market_cap_b": 10.0,
"trailing_pe": 14.26,
"forward_pe": 8.9,
"peg_ratio": null,
"revenue_growth": 41.4,
"earnings_growth": 17.2,
"roe": 9.0,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 68.73,
"week52_high": 26.17,
"pct_from_52wk_high": 2.4,
"score": 3.040000000000001
},
{
"ticker": "EXEL",
"price": 42.98,
"market_cap": 11570324480,
"market_cap_b": 11.6,
"trailing_pe": 18.06,
"forward_pe": 12.45,
"peg_ratio": null,
"revenue_growth": 10.8,
"earnings_growth": 72.5,
"roe": 30.6,
"quick_ratio": 3.5,
"debt_to_equity": 8.2,
"rsi": 38.94,
"week52_high": 49.62,
"pct_from_52wk_high": 13.4,
"score": 4.119999999999999
},
{
"ticker": "ZION",
"price": 64.08,
"market_cap": 9461604352,
"market_cap_b": 9.5,
"trailing_pe": 10.66,
"forward_pe": 9.8,
"peg_ratio": null,
"revenue_growth": 13.6,
"earnings_growth": 31.4,
"roe": 13.5,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 60.76,
"week52_high": 66.18,
"pct_from_52wk_high": 3.2,
"score": 5.300000000000001
},
{
"ticker": "CART",
"price": 34.62,
"market_cap": 9120232448,
"market_cap_b": 9.1,
"trailing_pe": 19.02,
"forward_pe": 8.83,
"peg_ratio": null,
"revenue_growth": 10.2,
"earnings_growth": 21.1,
"roe": 15.3,
"quick_ratio": 3.33,
"debt_to_equity": 1.0,
"rsi": 32.09,
"week52_high": 53.5,
"pct_from_52wk_high": 35.3,
"score": 5.699999999999999
},
{
"ticker": "CFG",
"price": 67.55,
"market_cap": 29011791872,
"market_cap_b": 29.0,
"trailing_pe": 17.5,
"forward_pe": 10.73,
"peg_ratio": null,
"revenue_growth": 10.7,
"earnings_growth": 35.9,
"roe": 7.2,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 68.71,
"week52_high": 68.78,
"pct_from_52wk_high": 1.8,
"score": 6.07
},
{
"ticker": "UBSI",
"price": 44.64,
"market_cap": 6222157312,
"market_cap_b": 6.2,
"trailing_pe": 13.65,
"forward_pe": 11.85,
"peg_ratio": null,
"revenue_growth": 22.1,
"earnings_growth": 32.1,
"roe": 8.9,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 64.4,
"week52_high": 45.93,
"pct_from_52wk_high": 2.8,
"score": 6.430000000000001
},
{
"ticker": "EWBC",
"price": 120.54,
"market_cap": 16584568832,
"market_cap_b": 16.6,
"trailing_pe": 12.66,
"forward_pe": 11.0,
"peg_ratio": null,
"revenue_growth": 21.6,
"earnings_growth": 21.3,
"roe": 15.9,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 65.92,
"week52_high": 123.82,
"pct_from_52wk_high": 2.6,
"score": 6.710000000000001
},
{
"ticker": "BAC",
"price": 55.39,
"market_cap": 404485242880,
"market_cap_b": 404.5,
"trailing_pe": 14.54,
"forward_pe": 11.17,
"peg_ratio": null,
"revenue_growth": 13.2,
"earnings_growth": 20.9,
"roe": 10.2,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 69.17,
"week52_high": 57.55,
"pct_from_52wk_high": 3.8,
"score": 7.76
},
{
"ticker": "FITB",
"price": 54.5,
"market_cap": 49052639232,
"market_cap_b": 49.1,
"trailing_pe": 15.44,
"forward_pe": 11.12,
"peg_ratio": null,
"revenue_growth": 11.5,
"earnings_growth": 20.8,
"roe": 12.2,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 57.61,
"week52_high": 55.36,
"pct_from_52wk_high": 1.6,
"score": 7.889999999999999
},
{
"ticker": "WTFC",
"price": 156.07,
"market_cap": 10452774912,
"market_cap_b": 10.5,
"trailing_pe": 13.69,
"forward_pe": 11.64,
"peg_ratio": null,
"revenue_growth": 10.5,
"earnings_growth": 19.4,
"roe": 12.1,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 58.11,
"week52_high": 162.96,
"pct_from_52wk_high": 4.2,
"score": 8.65
}
]
}

View File

@ -0,0 +1,296 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Market Watch — Paper Trading Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0a0e1a; color: #e0e6f0; min-height: 100vh; }
.header { background: linear-gradient(135deg, #0f1629, #1a2342); padding: 20px 30px; border-bottom: 1px solid #1e2a4a; display: flex; justify-content: space-between; align-items: center; }
.header h1 { font-size: 1.5rem; font-weight: 600; }
.header h1 span { color: #4ecdc4; }
.header .meta { font-size: 0.85rem; color: #7a8bb5; }
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; margin-bottom: 20px; }
.card { background: linear-gradient(145deg, #111827, #0f1520); border: 1px solid #1e2a4a; border-radius: 12px; padding: 20px; }
.card h2 { font-size: 1rem; color: #7a8bb5; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 12px; font-weight: 500; }
.card h2 .icon { margin-right: 6px; }
.stat-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 8px; }
.stat-label { color: #7a8bb5; font-size: 0.85rem; }
.stat-value { font-size: 1.1rem; font-weight: 600; }
.stat-big { font-size: 2rem; font-weight: 700; }
.green { color: #4ecdc4; }
.red { color: #ff6b6b; }
.neutral { color: #7a8bb5; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.75rem; font-weight: 600; }
.badge-green { background: rgba(78,205,196,0.15); color: #4ecdc4; }
.badge-red { background: rgba(255,107,107,0.15); color: #ff6b6b; }
.badge-blue { background: rgba(100,149,237,0.15); color: #6495ed; }
table { width: 100%; border-collapse: collapse; }
th { text-align: left; color: #7a8bb5; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; padding: 8px; border-bottom: 1px solid #1e2a4a; }
td { padding: 8px; font-size: 0.9rem; border-bottom: 1px solid #111827; }
tr:hover td { background: rgba(255,255,255,0.02); }
.chart-wrap { height: 250px; position: relative; }
.tabs { display: flex; gap: 8px; margin-bottom: 20px; }
.tab { padding: 8px 16px; border-radius: 8px; cursor: pointer; font-size: 0.9rem; border: 1px solid #1e2a4a; background: transparent; color: #7a8bb5; transition: all 0.2s; }
.tab.active { background: #4ecdc4; color: #0a0e1a; border-color: #4ecdc4; font-weight: 600; }
.tab:hover:not(.active) { border-color: #4ecdc4; color: #4ecdc4; }
.game-section { display: none; }
.game-section.active { display: block; }
.spinner { display: inline-block; width: 20px; height: 20px; border: 2px solid #1e2a4a; border-top-color: #4ecdc4; border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.empty { text-align: center; color: #7a8bb5; padding: 40px; }
</style>
</head>
<body>
<div class="header">
<h1>📊 <span>Market Watch</span> — Paper Trading</h1>
<div class="meta">Auto-refresh 60s · <span id="lastUpdate">Loading...</span></div>
</div>
<div class="container">
<div class="tabs" id="gameTabs"></div>
<div id="gameContent"><div class="empty"><div class="spinner"></div> Loading games...</div></div>
</div>
<script>
let games = [];
let activeGameIdx = 0;
async function fetchJSON(url) {
const r = await fetch(url);
return r.json();
}
function pnlClass(v) { return v > 0 ? 'green' : v < 0 ? 'red' : 'neutral'; }
function pnlSign(v) { return v > 0 ? '+' : ''; }
function fmt(v, d=2) { return v != null ? Number(v).toFixed(d) : '—'; }
function fmtK(v) { return v >= 1e6 ? (v/1e6).toFixed(1)+'M' : v >= 1e3 ? (v/1e3).toFixed(1)+'K' : fmt(v); }
function renderTabs() {
const el = document.getElementById('gameTabs');
el.innerHTML = games.map((g, i) =>
`<button class="tab ${i === activeGameIdx ? 'active' : ''}" onclick="switchGame(${i})">${g.name || g.game_id.slice(0,8)}</button>`
).join('');
}
function switchGame(idx) {
activeGameIdx = idx;
renderTabs();
renderGame(games[idx]);
}
async function renderGame(game) {
const el = document.getElementById('gameContent');
const gid = game.game_id;
// Fetch details in parallel
const [detail, trades, portfolio] = await Promise.all([
fetchJSON(`/api/game/${gid}`),
fetchJSON(`/api/game/${gid}/trades`),
fetchJSON(`/api/game/${gid}/portfolio`)
]);
const player = game.players?.[0] || 'unknown';
const pf = portfolio[player] || {};
const board = detail.leaderboard || [];
const entry = board[0] || {};
const starting = game.starting_cash || 100000;
const totalVal = pf.total_value || entry.total_value || starting;
const totalPnl = totalVal - starting;
const pnlPct = (totalPnl / starting * 100);
const sells = trades.filter(t => t.action === 'SELL');
const wins = sells.filter(t => (t.realized_pnl||0) > 0);
const winRate = sells.length ? (wins.length / sells.length * 100) : null;
const totalFees = trades.reduce((s,t) => s + (t.fees||0), 0);
const realizedPnl = sells.reduce((s,t) => s + (t.realized_pnl||0), 0);
// Equity curve from snapshots
const snapshots = detail.snapshots?.[player] || [];
let html = `
<!-- Summary Cards -->
<div class="grid">
<div class="card">
<h2><span class="icon">💰</span>Portfolio Value</h2>
<div class="stat-big ${pnlClass(totalPnl)}">$${fmtK(totalVal)}</div>
<div class="stat-row">
<span class="stat-label">P&L</span>
<span class="stat-value ${pnlClass(totalPnl)}">${pnlSign(totalPnl)}$${fmt(totalPnl)} (${pnlSign(pnlPct)}${fmt(pnlPct)}%)</span>
</div>
<div class="stat-row">
<span class="stat-label">Starting Cash</span>
<span class="stat-value">$${fmtK(starting)}</span>
</div>
<div class="stat-row">
<span class="stat-label">Cash Available</span>
<span class="stat-value">$${fmtK(pf.cash || 0)}</span>
</div>
</div>
<div class="card">
<h2><span class="icon">📈</span>Trading Stats</h2>
<div class="stat-row">
<span class="stat-label">Total Trades</span>
<span class="stat-value">${trades.length}</span>
</div>
<div class="stat-row">
<span class="stat-label">Win Rate</span>
<span class="stat-value ${winRate && winRate > 50 ? 'green' : winRate ? 'red' : 'neutral'}">${winRate != null ? fmt(winRate,1)+'%' : '—'}</span>
</div>
<div class="stat-row">
<span class="stat-label">Realized P&L</span>
<span class="stat-value ${pnlClass(realizedPnl)}">${pnlSign(realizedPnl)}$${fmt(realizedPnl)}</span>
</div>
<div class="stat-row">
<span class="stat-label">Total Fees</span>
<span class="stat-value red">-$${fmt(totalFees)}</span>
</div>
</div>
<div class="card">
<h2><span class="icon">🎯</span>Game Info</h2>
<div class="stat-row">
<span class="stat-label">Game</span>
<span class="stat-value">${game.name || gid.slice(0,8)}</span>
</div>
<div class="stat-row">
<span class="stat-label">Type</span>
<span class="stat-value"><span class="badge badge-blue">${game.game_type || 'stock'}</span></span>
</div>
<div class="stat-row">
<span class="stat-label">Player</span>
<span class="stat-value">${player}</span>
</div>
<div class="stat-row">
<span class="stat-label">Open Positions</span>
<span class="stat-value">${Object.keys(pf.positions || {}).length}</span>
</div>
</div>
</div>
<!-- Equity Chart -->
${snapshots.length > 1 ? `
<div class="card" style="margin-bottom:16px">
<h2><span class="icon">📊</span>Equity Curve</h2>
<div class="chart-wrap"><canvas id="equityChart"></canvas></div>
</div>` : ''}
<!-- Positions -->
<div class="card" style="margin-bottom:16px">
<h2><span class="icon">📋</span>Open Positions</h2>
${Object.keys(pf.positions||{}).length ? `
<table>
<thead><tr><th>Symbol</th><th>Shares/Qty</th><th>Avg Cost</th><th>Current</th><th>Value</th><th>P&L</th></tr></thead>
<tbody>
${Object.entries(pf.positions||{}).map(([sym, pos]) => {
const upnl = pos.unrealized_pnl || ((pos.current_price||pos.avg_cost) - pos.avg_cost) * (pos.shares||pos.quantity||0);
const val = pos.market_value || (pos.current_price||pos.avg_cost) * (pos.shares||pos.quantity||0);
return `<tr>
<td><strong>${sym}</strong></td>
<td>${pos.shares||pos.quantity||0}</td>
<td>$${fmt(pos.avg_cost)}</td>
<td>$${fmt(pos.current_price||pos.live_price||pos.avg_cost)}</td>
<td>$${fmtK(val)}</td>
<td class="${pnlClass(upnl)}">${pnlSign(upnl)}$${fmt(upnl)}</td>
</tr>`;
}).join('')}
</tbody>
</table>` : '<div class="empty">No open positions</div>'}
</div>
<!-- Recent Trades -->
<div class="card">
<h2><span class="icon">🔄</span>Recent Trades (last 25)</h2>
${trades.length ? `
<table>
<thead><tr><th>Time</th><th>Action</th><th>Symbol</th><th>Qty</th><th>Price</th><th>P&L</th></tr></thead>
<tbody>
${trades.slice(0,25).map(t => {
const rpnl = t.realized_pnl || 0;
const actionClass = t.action === 'BUY' ? 'badge-green' : t.action === 'SELL' ? 'badge-red' : 'badge-blue';
return `<tr>
<td style="font-size:0.8rem;color:#7a8bb5">${t.timestamp ? new Date(t.timestamp).toLocaleString() : '—'}</td>
<td><span class="badge ${actionClass}">${t.action}</span></td>
<td><strong>${t.ticker||t.symbol||'—'}</strong></td>
<td>${t.shares||t.quantity||'—'}</td>
<td>$${fmt(t.price)}</td>
<td class="${pnlClass(rpnl)}">${t.action==='SELL' ? pnlSign(rpnl)+'$'+fmt(rpnl) : '—'}</td>
</tr>`;
}).join('')}
</tbody>
</table>` : '<div class="empty">No trades yet</div>'}
</div>`;
el.innerHTML = html;
// Draw equity chart
if (snapshots.length > 1) {
const ctx = document.getElementById('equityChart').getContext('2d');
const labels = snapshots.map(s => {
const d = s.timestamp || s.date;
if (!d) return '—';
const dt = new Date(d);
return isNaN(dt) ? String(d).slice(0,10) : dt.toLocaleDateString();
});
const values = snapshots.map(s => s.total_value || s.equity || starting);
new Chart(ctx, {
type: 'line',
data: {
labels,
datasets: [{
label: 'Equity',
data: values,
borderColor: '#4ecdc4',
backgroundColor: 'rgba(78,205,196,0.1)',
fill: true,
tension: 0.3,
pointRadius: 0,
borderWidth: 2
}, {
label: 'Starting',
data: Array(labels.length).fill(starting),
borderColor: '#7a8bb544',
borderDash: [5,5],
borderWidth: 1,
pointRadius: 0
}]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { grid: { color: '#1e2a4a' }, ticks: { color: '#7a8bb5', maxTicksLimit: 8 } },
y: { grid: { color: '#1e2a4a' }, ticks: { color: '#7a8bb5', callback: v => '$'+fmtK(v) } }
}
}
});
}
}
async function init() {
try {
games = await fetchJSON('/api/games');
if (!games.length) {
document.getElementById('gameContent').innerHTML = '<div class="empty">No games found</div>';
return;
}
renderTabs();
await renderGame(games[activeGameIdx]);
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString();
} catch (e) {
document.getElementById('gameContent').innerHTML = `<div class="empty">Error: ${e.message}</div>`;
}
}
init();
setInterval(async () => {
try {
games = await fetchJSON('/api/games');
await renderGame(games[activeGameIdx]);
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString();
} catch(e) {}
}, 60000);
</script>
</body>
</html>

View File

@ -1,424 +1,173 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Market Watch Web Portal - Multiplayer GARP Paper Trading.""" """Market Watch Web Portal — modern dark-themed dashboard."""
import json import json
import os import os
import sys import sys
import traceback
from datetime import datetime from datetime import datetime
from http.server import HTTPServer, BaseHTTPRequestHandler from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn from socketserver import ThreadingMixIn
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import game_engine import game_engine
PORT = 8889 PORT = 8889
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PORTAL_DIR = os.path.dirname(os.path.abspath(__file__))
SCANS_DIR = os.path.join(PROJECT_DIR, "data", "scans")
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
daemon_threads = True daemon_threads = True
CSS = """:root{--bg-primary:#0d1117;--bg-secondary:#161b22;--bg-tertiary:#21262d;--text-primary:#f0f6fc;--text-secondary:#8b949e;--border-color:#30363d;--accent-blue:#58a6ff;--accent-purple:#bc8cff;--positive-green:#3fb950;--negative-red:#f85149;--gold:#f0c000;--silver:#c0c0c0;--bronze:#cd7f32} def _fetch_live_prices(tickers):
*{margin:0;padding:0;box-sizing:border-box} """Fetch live prices via yfinance. Returns {ticker: price}."""
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg-primary);color:var(--text-primary);line-height:1.5} try:
a{color:var(--accent-blue);text-decoration:none}a:hover{text-decoration:underline} import yfinance as yf
.navbar{background:var(--bg-secondary);border-bottom:1px solid var(--border-color);padding:1rem 2rem;display:flex;align-items:center;justify-content:space-between} data = yf.download(tickers, period="1d", progress=False)
.nav-brand{font-size:1.5rem;font-weight:bold;color:var(--accent-blue)} prices = {}
.nav-links{display:flex;gap:1.5rem} if len(tickers) == 1:
.nav-links a{color:var(--text-secondary);text-decoration:none;padding:.5rem 1rem;border-radius:6px;transition:all .2s} t = tickers[0]
.nav-links a:hover{color:var(--text-primary);background:var(--bg-tertiary)} if "Close" in data.columns and len(data) > 0:
.nav-links a.active{color:var(--accent-blue);background:var(--bg-tertiary)} prices[t] = float(data["Close"].iloc[-1])
.container{max-width:1400px;margin:0 auto;padding:2rem} else:
.card{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:8px;padding:1.5rem;margin-bottom:1.5rem} if "Close" in data.columns:
.card h3{color:var(--text-primary);margin-bottom:1rem;font-size:1.1rem} for t in tickers:
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1.5rem;margin-bottom:2rem} try:
.metric-large{font-size:2rem;font-weight:bold;margin-bottom:.3rem} val = data["Close"][t].iloc[-1]
.metric-small{color:var(--text-secondary);font-size:.85rem} if val == val: # not NaN
.positive{color:var(--positive-green)!important}.negative{color:var(--negative-red)!important} prices[t] = float(val)
table{width:100%;border-collapse:collapse} except Exception:
th,td{padding:.6rem .8rem;text-align:left;border-bottom:1px solid var(--border-color)} pass
th{color:var(--text-secondary);font-size:.8rem;text-transform:uppercase} return prices
td{font-size:.9rem} except Exception:
.rank-1{color:var(--gold);font-weight:bold}.rank-2{color:var(--silver)}.rank-3{color:var(--bronze)} return {}
.btn{display:inline-block;padding:.5rem 1.2rem;background:var(--accent-blue);color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:.9rem;text-decoration:none;transition:opacity .2s}
.btn:hover{opacity:.85;text-decoration:none}
.btn-outline{background:transparent;border:1px solid var(--border-color);color:var(--text-primary)}
.btn-outline:hover{border-color:var(--accent-blue)}
.btn-green{background:var(--positive-green)}.btn-red{background:var(--negative-red)}
input,select{background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-primary);padding:.5rem .8rem;border-radius:6px;font-size:.9rem}
.form-row{display:flex;gap:1rem;align-items:end;flex-wrap:wrap;margin-bottom:1rem}
.form-group{display:flex;flex-direction:column;gap:.3rem}
.form-group label{font-size:.8rem;color:var(--text-secondary);text-transform:uppercase}
.badge{display:inline-block;padding:.15rem .5rem;border-radius:4px;font-size:.75rem;font-weight:bold}
.badge-ai{background:var(--accent-purple);color:#fff}
.badge-human{background:var(--accent-blue);color:#fff}
.player-link{color:var(--text-primary);font-weight:500}
@media(max-width:768px){.navbar{flex-direction:column;gap:1rem}.cards{grid-template-columns:1fr}.container{padding:1rem}.form-row{flex-direction:column}}"""
def nav(active=""): class Handler(BaseHTTPRequestHandler):
return f"""<nav class="navbar">
<a href="/" style="text-decoration:none"><div class="nav-brand">📊 Market Watch</div></a>
<div class="nav-links">
<a href="/" class="{'active' if active=='home' else ''}">Games</a>
<a href="/scans" class="{'active' if active=='scans' else ''}">Scans</a>
</div></nav>"""
class MarketWatchHandler(BaseHTTPRequestHandler):
def do_GET(self): def do_GET(self):
try: try:
parsed = urlparse(self.path) path = urlparse(self.path).path.rstrip("/") or "/"
path = parsed.path.rstrip("/")
params = parse_qs(parsed.query)
if path == "" or path == "/": if path == "/":
self.serve_home() return self._serve_file("index.html", "text/html")
elif path == "/create-game":
self.serve_create_game()
elif path.startswith("/game/") and "/player/" in path:
parts = path.split("/") # /game/{gid}/player/{user}
self.serve_player(parts[2], parts[4])
elif path.startswith("/game/"):
game_id = path.split("/")[2]
self.serve_game(game_id)
elif path == "/scans":
self.serve_scans()
# API
elif path.startswith("/api/games") and len(path.split("/")) == 3:
self.send_json(game_engine.list_games(active_only=False))
elif path.startswith("/api/games/") and path.endswith("/leaderboard"):
gid = path.split("/")[3]
self.send_json(game_engine.get_leaderboard(gid))
elif "/portfolio" in path:
parts = path.split("/")
self.send_json(game_engine.get_portfolio(parts[3], parts[5]))
else:
self.send_error(404)
except Exception as e:
self.send_response(500)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(f"<h1>500</h1><pre>{e}</pre>".encode())
def do_POST(self):
try:
content_len = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(content_len).decode() if content_len else ""
parsed = urlparse(self.path)
path = parsed.path.rstrip("/")
# API endpoints
if path == "/api/games": if path == "/api/games":
data = parse_form(body)
name = data.get("name", "Untitled Game")
cash = float(data.get("starting_cash", 100000))
end_date = data.get("end_date") or None
gid = game_engine.create_game(name, cash, end_date)
self.redirect(f"/game/{gid}")
elif path.endswith("/join"):
data = parse_form(body)
parts = path.split("/")
gid = parts[3]
username = data.get("username", "").strip().lower()
if username:
game_engine.join_game(gid, username)
self.redirect(f"/game/{gid}")
elif path.endswith("/trade"):
data = parse_form(body)
parts = path.split("/")
gid, username = parts[3], parts[5]
action = data.get("action", "").upper()
ticker = data.get("ticker", "").upper().strip()
shares = int(data.get("shares", 0))
if ticker and shares > 0:
import yfinance as yf
price = yf.Ticker(ticker).info.get("currentPrice") or yf.Ticker(ticker).info.get("regularMarketPrice", 0)
if price and price > 0:
if action == "BUY":
game_engine.buy(gid, username, ticker, shares, price, reason="Manual trade")
elif action == "SELL":
game_engine.sell(gid, username, ticker, shares, price, reason="Manual trade")
self.redirect(f"/game/{gid}/player/{username}")
else:
self.send_error(404)
except Exception as e:
self.send_response(500)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(f"<h1>500</h1><pre>{e}</pre>".encode())
def serve_home(self):
games = game_engine.list_games(active_only=False) games = game_engine.list_games(active_only=False)
rows = "" # Enrich with summary
for g in games: for g in games:
players = len(g.get("players", [])) board = game_engine.get_leaderboard(g["game_id"])
status_badge = '<span class="badge badge-ai">Active</span>' if g["status"] == "active" else '<span class="badge">Ended</span>' g["leaderboard"] = board
rows += f"""<tr> trades_all = []
<td><a href="/game/{g['game_id']}" class="player-link">{g['name']}</a></td> for p in g.get("players", []):
<td>{players}</td> trades_all.extend(game_engine.get_trades(g["game_id"], p))
<td>${g['starting_cash']:,.0f}</td> g["total_trades"] = len(trades_all)
<td>{g['start_date']}</td> sells = [t for t in trades_all if t.get("action") == "SELL"]
<td>{g.get('end_date', '') or ''}</td> wins = [t for t in sells if t.get("realized_pnl", 0) > 0]
<td>{status_badge}</td> g["win_rate"] = round(len(wins)/len(sells)*100, 1) if sells else None
</tr>""" return self._json(games)
html = f"""<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"> # /api/game/{id}
<title>Market Watch</title><style>{CSS}</style></head><body> parts = path.split("/")
{nav('home')} if len(parts) >= 4 and parts[1] == "api" and parts[2] == "game":
<div class="container"> gid = parts[3]
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1.5rem">
<h2>🎮 Active Games</h2>
<a href="/create-game" class="btn">+ New Game</a>
</div>
<div class="card">
<table><thead><tr><th>Game</th><th>Players</th><th>Starting Cash</th><th>Started</th><th>Ends</th><th>Status</th></tr></thead>
<tbody>{rows if rows else '<tr><td colspan="6" style="text-align:center;color:var(--text-secondary)">No games yet — create one!</td></tr>'}</tbody></table>
</div>
</div></body></html>"""
self.send_html(html)
def serve_create_game(self): if len(parts) == 4:
html = f"""<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"> game = game_engine.get_game(gid)
<title>Create Game - Market Watch</title><style>{CSS}</style></head><body>
{nav()}
<div class="container">
<div class="card">
<h3>🎮 Create New Game</h3>
<form method="POST" action="/api/games">
<div class="form-row">
<div class="form-group"><label>Game Name</label><input type="text" name="name" placeholder="GARP Challenge" required></div>
<div class="form-group"><label>Starting Cash ($)</label><input type="number" name="starting_cash" value="100000" min="1000" step="1000"></div>
<div class="form-group"><label>End Date (optional)</label><input type="date" name="end_date"></div>
</div>
<button type="submit" class="btn">Create Game</button>
</form>
</div>
</div></body></html>"""
self.send_html(html)
def serve_game(self, game_id):
game = game_engine.get_game(game_id)
if not game: if not game:
return self.send_error(404) return self._json({"error": "not found"}, 404)
game["leaderboard"] = game_engine.get_leaderboard(gid)
# Add snapshots for each player
game["snapshots"] = {}
for p in game.get("players", []):
game["snapshots"][p] = game_engine.get_snapshots(gid, p)
return self._json(game)
board = game_engine.get_leaderboard(game_id) if len(parts) == 5 and parts[4] == "trades":
game = game_engine.get_game(gid)
if not game:
return self._json({"error": "not found"}, 404)
all_trades = []
for p in game.get("players", []):
for t in game_engine.get_trades(gid, p):
t["player"] = p
all_trades.append(t)
all_trades.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
return self._json(all_trades)
rank_rows = "" if len(parts) == 5 and parts[4] == "portfolio":
for i, entry in enumerate(board): game = game_engine.get_game(gid)
rank_class = f"rank-{i+1}" if i < 3 else "" if not game:
medal = ["🥇", "🥈", "🥉"][i] if i < 3 else f"#{i+1}" return self._json({"error": "not found"}, 404)
pnl_class = "positive" if entry["pnl_pct"] >= 0 else "negative" portfolios = {}
badge = ' <span class="badge badge-ai">AI</span>' if entry["username"] == "case" else "" all_tickers = []
rank_rows += f"""<tr> for p in game.get("players", []):
<td class="{rank_class}">{medal}</td> pf = game_engine.get_portfolio(gid, p)
<td><a href="/game/{game_id}/player/{entry['username']}" class="player-link">{entry['username']}</a>{badge}</td> if pf:
<td>${entry['total_value']:,.2f}</td> portfolios[p] = pf
<td class="{pnl_class}">{entry['pnl_pct']:+.2f}%</td> all_tickers.extend(pf["positions"].keys())
<td class="{pnl_class}">${entry['total_pnl']:+,.2f}</td>
<td>{entry['num_positions']}</td>
<td>{entry['num_trades']}</td>
</tr>"""
html = f"""<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"> # Fetch live prices
<title>{game['name']} - Market Watch</title><script src="https://cdn.jsdelivr.net/npm/chart.js"></script><style>{CSS}</style></head><body> all_tickers = list(set(all_tickers))
{nav()} if all_tickers:
<div class="container"> live = _fetch_live_prices(all_tickers)
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem"> for p, pf in portfolios.items():
<h2>🏆 {game['name']}</h2> total_value = pf["cash"]
<span class="badge badge-ai">{game['status'].upper()}</span> for ticker, pos in pf["positions"].items():
</div> if ticker in live:
<p style="color:var(--text-secondary);margin-bottom:1.5rem">Started {game['start_date']} · ${game['starting_cash']:,.0f} starting cash · {len(game['players'])} players</p> pos["live_price"] = live[ticker]
pos["current_price"] = live[ticker]
<div class="card"> pos["unrealized_pnl"] = round((live[ticker] - pos["avg_cost"]) * pos["shares"], 2)
<h3>Leaderboard</h3> pos["market_value"] = round(live[ticker] * pos["shares"], 2)
<table><thead><tr><th>Rank</th><th>Player</th><th>Portfolio</th><th>Return</th><th>P&L</th><th>Positions</th><th>Trades</th></tr></thead> total_value += pos["market_value"]
<tbody>{rank_rows if rank_rows else '<tr><td colspan="7" style="text-align:center;color:var(--text-secondary)">No players yet</td></tr>'}</tbody></table> pf["total_value"] = round(total_value, 2)
</div>
<div class="card">
<h3>Join This Game</h3>
<form method="POST" action="/api/games/{game_id}/join">
<div class="form-row">
<div class="form-group"><label>Username</label><input type="text" name="username" placeholder="your name" required pattern="[a-zA-Z0-9_-]+" title="Letters, numbers, dashes, underscores only"></div>
<button type="submit" class="btn">Join Game</button>
</div>
</form>
</div>
</div></body></html>"""
self.send_html(html)
def serve_player(self, game_id, username):
game = game_engine.get_game(game_id)
p = game_engine.get_portfolio(game_id, username)
if not game or not p:
return self.send_error(404)
trades = game_engine.get_trades(game_id, username)
snapshots = game_engine.get_snapshots(game_id, username)
pnl_class = "positive" if p["total_pnl"] >= 0 else "negative"
is_ai = username == "case"
badge = '<span class="badge badge-ai">AI Player</span>' if is_ai else '<span class="badge badge-human">Human</span>'
# Positions table
pos_rows = ""
for ticker, pos in sorted(p["positions"].items()):
pc = "positive" if pos["unrealized_pnl"] >= 0 else "negative"
pos_rows += f"""<tr>
<td><strong>{ticker}</strong></td><td>{pos['shares']}</td>
<td>${pos['avg_cost']:.2f}</td><td>${pos['current_price']:.2f}</td>
<td>${pos['market_value']:,.2f}</td><td class="{pc}">${pos['unrealized_pnl']:+,.2f}</td>
<td>${pos.get('trailing_stop',0):.2f}</td>
</tr>"""
if not pos_rows:
pos_rows = '<tr><td colspan="7" style="text-align:center;color:var(--text-secondary)">No positions</td></tr>'
# Trade log
trade_rows = ""
for t in reversed(trades[-30:]):
action_class = "positive" if t["action"] == "BUY" else "negative"
pnl_cell = ""
if t["action"] == "SELL":
rpnl = t.get("realized_pnl", 0)
rpnl_class = "positive" if rpnl >= 0 else "negative"
pnl_cell = f'<span class="{rpnl_class}">${rpnl:+,.2f}</span>'
trade_rows += f"""<tr>
<td class="{action_class}">{t['action']}</td><td>{t['ticker']}</td><td>{t['shares']}</td>
<td>${t['price']:.2f}</td><td>{pnl_cell}</td>
<td>{t.get('reason','')[:40]}</td><td>{t['timestamp'][:16]}</td>
</tr>"""
chart_labels = json.dumps([s["date"] for s in snapshots])
chart_values = json.dumps([s["total_value"] for s in snapshots])
starting = game.get("starting_cash", 100000) starting = game.get("starting_cash", 100000)
pf["total_pnl"] = round(total_value - starting, 2)
pf["pnl_pct"] = round((total_value - starting) / starting * 100, 2)
# Trade form (only for humans) return self._json(portfolios)
trade_form = "" if is_ai else f"""
<div class="card">
<h3>📝 Place Trade</h3>
<form method="POST" action="/api/games/{game_id}/players/{username}/trade">
<div class="form-row">
<div class="form-group"><label>Action</label>
<select name="action"><option value="BUY">BUY</option><option value="SELL">SELL</option></select></div>
<div class="form-group"><label>Ticker</label><input type="text" name="ticker" placeholder="AAPL" required style="text-transform:uppercase"></div>
<div class="form-group"><label>Shares</label><input type="number" name="shares" min="1" value="10" required></div>
<button type="submit" class="btn btn-green">Execute</button>
</div>
<p style="color:var(--text-secondary);font-size:.8rem;margin-top:.5rem">Trades execute at current market price via Yahoo Finance</p>
</form>
</div>"""
html = f"""<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"> self._error(404)
<title>{username} - {game['name']}</title><script src="https://cdn.jsdelivr.net/npm/chart.js"></script><style>{CSS}</style></head><body> except Exception as e:
{nav()} self._json({"error": str(e), "trace": traceback.format_exc()}, 500)
<div class="container">
<div style="margin-bottom:.5rem"><a href="/game/{game_id}" style="color:var(--text-secondary)">← {game['name']}</a></div>
<h2>{username} {badge}</h2>
<div class="cards" style="margin-top:1rem">
<div class="card"><h3>Portfolio Value</h3><div class="metric-large">${p['total_value']:,.2f}</div><div class="metric-small">Started at ${starting:,.0f}</div></div>
<div class="card"><h3>Cash</h3><div class="metric-large">${p['cash']:,.2f}</div><div class="metric-small">{p['cash']/max(p['total_value'],1)*100:.1f}% available</div></div>
<div class="card"><h3>Return</h3><div class="metric-large {pnl_class}">{p['pnl_pct']:+.2f}%</div><div class="metric-small {pnl_class}">${p['total_pnl']:+,.2f}</div></div>
<div class="card"><h3>Positions</h3><div class="metric-large">{p['num_positions']}</div><div class="metric-small">{len(trades)} total trades</div></div>
</div>
<div class="card"><h3>Performance</h3><canvas id="chart" height="80"></canvas></div> def _serve_file(self, filename, content_type):
filepath = os.path.join(PORTAL_DIR, filename)
{trade_form} with open(filepath, "rb") as f:
data = f.read()
<div class="card"><h3>Positions</h3>
<table><thead><tr><th>Ticker</th><th>Shares</th><th>Avg Cost</th><th>Price</th><th>Value</th><th>P&L</th><th>Stop</th></tr></thead>
<tbody>{pos_rows}</tbody></table>
</div>
<div class="card"><h3>Trade Log</h3>
<table><thead><tr><th>Action</th><th>Ticker</th><th>Shares</th><th>Price</th><th>P&L</th><th>Reason</th><th>Time</th></tr></thead>
<tbody>{trade_rows if trade_rows else '<tr><td colspan="7" style="text-align:center;color:var(--text-secondary)">No trades yet</td></tr>'}</tbody></table>
</div>
</div>
<script>
const ctx = document.getElementById('chart').getContext('2d');
const labels = {chart_labels}; const values = {chart_values};
if (labels.length > 0) {{
new Chart(ctx, {{type:'line',data:{{labels:labels,datasets:[
{{label:'Portfolio',data:values,borderColor:'#58a6ff',backgroundColor:'rgba(88,166,255,0.1)',fill:true,tension:0.3}},
{{label:'Starting',data:labels.map(()=>{starting}),borderColor:'#30363d',borderDash:[5,5],pointRadius:0}}
]}},options:{{responsive:true,plugins:{{legend:{{labels:{{color:'#f0f6fc'}}}}}},scales:{{x:{{ticks:{{color:'#8b949e'}},grid:{{color:'#21262d'}}}},y:{{ticks:{{color:'#8b949e',callback:v=>'$'+v.toLocaleString()}},grid:{{color:'#21262d'}}}}}}}}
}});
}} else {{ ctx.canvas.parentElement.innerHTML += '<div style="text-align:center;color:#8b949e;padding:2rem">Chart populates after first trading day</div>'; }}
</script></body></html>"""
self.send_html(html)
def serve_scans(self):
rows = ""
if os.path.exists(SCANS_DIR):
for sf in sorted(os.listdir(SCANS_DIR), reverse=True)[:30]:
if not sf.endswith(".json"): continue
data = {}
with open(os.path.join(SCANS_DIR, sf)) as f:
data = json.load(f)
n = data.get("candidates_found", len(data.get("candidates", [])))
top = ", ".join(c.get("ticker","?") for c in data.get("candidates", [])[:8])
rows += f'<tr><td>{sf.replace(".json","")}</td><td>{data.get("total_scanned",0)}</td><td>{n}</td><td>{top or ""}</td></tr>'
html = f"""<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Scans - Market Watch</title><style>{CSS}</style></head><body>
{nav('scans')}
<div class="container"><div class="card"><h3>📡 GARP Scan History</h3>
<table><thead><tr><th>Date</th><th>Scanned</th><th>Candidates</th><th>Top Picks</th></tr></thead>
<tbody>{rows if rows else '<tr><td colspan="4" style="text-align:center;color:var(--text-secondary)">No scans yet</td></tr>'}</tbody></table>
</div></div></body></html>"""
self.send_html(html)
def redirect(self, url):
self.send_response(303)
self.send_header("Location", url)
self.end_headers()
def send_html(self, content):
self.send_response(200) self.send_response(200)
self.send_header("Content-type", "text/html") self.send_header("Content-Type", content_type)
self.end_headers() self.end_headers()
self.wfile.write(content.encode()) self.wfile.write(data)
def send_json(self, data): def _json(self, data, code=200):
self.send_response(200) body = json.dumps(data, default=str).encode()
self.send_header("Content-type", "application/json") self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers() self.end_headers()
self.wfile.write(json.dumps(data, default=str).encode()) self.wfile.write(body)
def log_message(self, format, *args): def _error(self, code):
self.send_response(code)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(f"{code}".encode())
def log_message(self, fmt, *args):
pass pass
def parse_form(body):
"""Parse URL-encoded form data."""
result = {}
for pair in body.split("&"):
if "=" in pair:
k, v = pair.split("=", 1)
from urllib.parse import unquote_plus
result[unquote_plus(k)] = unquote_plus(v)
return result
def main(): def main():
game_engine.ensure_default_game() game_engine.ensure_default_game()
print(f"📊 Market Watch Portal starting on localhost:{PORT}") print(f"📊 Market Watch Portal → http://localhost:{PORT}")
server = ThreadedHTTPServer(("0.0.0.0", PORT), MarketWatchHandler) server = ThreadedHTTPServer(("0.0.0.0", PORT), Handler)
try: try:
server.serve_forever() server.serve_forever()
except KeyboardInterrupt: except KeyboardInterrupt:
print("\nPortal stopped") print("\nStopped")
if __name__ == "__main__": if __name__ == "__main__":

389
tools/analyze_tweet.py Executable file
View File

@ -0,0 +1,389 @@
#!/usr/bin/env python3
"""Tweet Analysis Tool - Scrapes and analyzes tweets via Chrome CDP."""
import argparse
import asyncio
import json
import re
import sys
from datetime import datetime
try:
from playwright.async_api import async_playwright
except ImportError:
print("ERROR: playwright not installed. Run: pip install playwright", file=sys.stderr)
sys.exit(1)
try:
import yfinance as yf
except ImportError:
yf = None
def extract_tickers(text: str) -> list[str]:
"""Extract $TICKER patterns from text."""
return list(set(re.findall(r'\$([A-Z]{1,5}(?:\.[A-Z]{1,2})?)', text.upper())))
def lookup_tickers(tickers: list[str]) -> dict:
"""Look up ticker data via yfinance."""
if not yf or not tickers:
return {}
results = {}
for t in tickers[:5]: # limit to 5
try:
info = yf.Ticker(t).info
results[t] = {
"price": info.get("currentPrice") or info.get("regularMarketPrice"),
"market_cap": info.get("marketCap"),
"name": info.get("shortName"),
"volume": info.get("volume"),
"day_change_pct": info.get("regularMarketChangePercent"),
"52w_high": info.get("fiftyTwoWeekHigh"),
"52w_low": info.get("fiftyTwoWeekLow"),
}
except Exception:
results[t] = {"error": "lookup failed"}
return results
async def scrape_tweet(url: str) -> dict:
"""Connect to Chrome CDP and scrape tweet data."""
# Normalize URL
url = url.replace("twitter.com", "x.com")
if not url.startswith("http"):
url = "https://" + url
data = {
"url": url,
"author": None,
"handle": None,
"text": None,
"timestamp": None,
"metrics": {},
"images": [],
"bio": None,
"followers": None,
"following": None,
"reply_to": None,
"replies_sample": [],
"scrape_error": None,
}
async with async_playwright() as p:
try:
browser = await p.chromium.connect_over_cdp("http://localhost:9222")
except Exception as e:
data["scrape_error"] = f"CDP connection failed: {e}"
return data
try:
ctx = browser.contexts[0] if browser.contexts else await browser.new_context()
page = await ctx.new_page()
await page.goto(url, wait_until="domcontentloaded", timeout=30000)
await page.wait_for_timeout(4000)
# Get the main tweet article
# Try to find the focal tweet
tweet_sel = 'article[data-testid="tweet"]'
articles = await page.query_selector_all(tweet_sel)
if not articles:
data["scrape_error"] = "No tweet articles found on page"
await page.close()
return data
# The focal tweet is typically the one with the largest text or specific structure
# On a tweet permalink, it's usually the first or second article
focal = None
for art in articles:
# The focal tweet has a different time display (absolute vs relative)
time_el = await art.query_selector('time')
if time_el:
dt = await time_el.get_attribute('datetime')
if dt:
focal = art
data["timestamp"] = dt
break
if not focal:
focal = articles[0]
# Author info
user_links = await focal.query_selector_all('a[role="link"]')
for link in user_links:
href = await link.get_attribute("href") or ""
if href.startswith("/") and href.count("/") == 1 and len(href) > 1:
spans = await link.query_selector_all("span")
for span in spans:
txt = (await span.inner_text()).strip()
if txt.startswith("@"):
data["handle"] = txt
elif txt and not data["author"] and not txt.startswith("@"):
data["author"] = txt
break
# Tweet text
text_el = await focal.query_selector('div[data-testid="tweetText"]')
if text_el:
data["text"] = await text_el.inner_text()
# Metrics (replies, retweets, likes, views)
group = await focal.query_selector('div[role="group"]')
if group:
buttons = await group.query_selector_all('button')
metric_names = ["replies", "retweets", "likes", "bookmarks"]
for i, btn in enumerate(buttons):
aria = await btn.get_attribute("aria-label") or ""
# Parse numbers from aria labels like "123 replies"
nums = re.findall(r'[\d,]+', aria)
if nums and i < len(metric_names):
data["metrics"][metric_names[i]] = nums[0].replace(",", "")
# Views - often in a separate span
view_spans = await focal.query_selector_all('a[role="link"] span')
for vs in view_spans:
txt = (await vs.inner_text()).strip()
if "views" in txt.lower() or "Views" in txt:
nums = re.findall(r'[\d,.KkMm]+', txt)
if nums:
data["metrics"]["views"] = nums[0]
# Images
imgs = await focal.query_selector_all('img[alt="Image"]')
for img in imgs:
src = await img.get_attribute("src")
if src:
data["images"].append(src)
# Check if it's a reply
reply_indicators = await page.query_selector_all('div[data-testid="tweet"] a[role="link"]')
# Try to get author profile info by hovering or checking
# We'll grab it from the page if visible
if data["handle"]:
handle_clean = data["handle"].lstrip("@")
# Check for bio/follower info in any hover cards or visible elements
all_text = await page.inner_text("body")
# Look for follower patterns
follower_match = re.search(r'([\d,.]+[KkMm]?)\s+Followers', all_text)
following_match = re.search(r'([\d,.]+[KkMm]?)\s+Following', all_text)
if follower_match:
data["followers"] = follower_match.group(1)
if following_match:
data["following"] = following_match.group(1)
# Sample some replies (articles after the focal tweet)
if len(articles) > 1:
for art in articles[1:4]:
reply_text_el = await art.query_selector('div[data-testid="tweetText"]')
if reply_text_el:
rt = await reply_text_el.inner_text()
if rt:
data["replies_sample"].append(rt[:200])
await page.close()
except Exception as e:
data["scrape_error"] = str(e)
try:
await page.close()
except:
pass
return data
def analyze(data: dict) -> dict:
"""Produce structured analysis from scraped data."""
text = data.get("text") or ""
tickers = extract_tickers(text)
ticker_data = lookup_tickers(tickers)
# Red flags detection
red_flags = []
text_lower = text.lower()
promo_words = ["100x", "1000x", "moon", "gem", "rocket", "guaranteed", "easy money",
"don't miss", "last chance", "about to explode", "next big", "sleeping giant",
"never stops printing", "true freedom", "beat the institutions", "revolution",
"empire", "vault", "get rich", "financial freedom", "life changing",
"without a degree", "from a bedroom", "join this"]
for w in promo_words:
if w in text_lower:
red_flags.append(f"Promotional language: '{w}'")
if len(tickers) > 3:
red_flags.append(f"Multiple tickers mentioned ({len(tickers)})")
if len(text) > 2000:
red_flags.append("Extremely long promotional thread")
if "github" in text_lower and ("star" in text_lower or "repo" in text_lower):
red_flags.append("Pushing GitHub repo (potential funnel to paid product)")
if any(w in text_lower for w in ["course", "discord", "premium", "paid group", "subscribe"]):
red_flags.append("Funneling to paid product/community")
# Check replies for coordinated patterns
replies = data.get("replies_sample", [])
if replies:
rocket_replies = sum(1 for r in replies if any(e in r for e in ["🚀", "💎", "🔥", "LFG"]))
if rocket_replies >= 2:
red_flags.append("Replies show coordinated hype patterns")
# Check for penny stock characteristics
for t, info in ticker_data.items():
if isinstance(info, dict) and not info.get("error"):
price = info.get("price")
mcap = info.get("market_cap")
if price and price < 1:
red_flags.append(f"${t} is a penny stock (${price})")
if mcap and mcap < 50_000_000:
red_flags.append(f"${t} micro-cap (<$50M market cap)")
# Build verdict
if len(red_flags) >= 3:
verdict = "High risk - multiple red flags detected, exercise extreme caution"
elif len(red_flags) >= 1:
verdict = "Some concerns - verify claims independently before acting"
elif tickers:
verdict = "Worth investigating - do your own due diligence"
else:
verdict = "Informational tweet - no immediate financial claims detected"
return {
"tweet_data": data,
"tickers_found": tickers,
"ticker_data": ticker_data,
"red_flags": red_flags,
"verdict": verdict,
}
def format_markdown(analysis: dict) -> str:
"""Format analysis as markdown."""
d = analysis["tweet_data"]
lines = [f"# Tweet Analysis", ""]
lines.append(f"**URL:** {d['url']}")
lines.append(f"**Analyzed:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
lines.append("")
# WHO
lines.append("## 👤 WHO")
lines.append(f"- **Author:** {d.get('author') or 'Unknown'}")
lines.append(f"- **Handle:** {d.get('handle') or 'Unknown'}")
if d.get("followers"):
lines.append(f"- **Followers:** {d['followers']}")
if d.get("following"):
lines.append(f"- **Following:** {d['following']}")
if d.get("bio"):
lines.append(f"- **Bio:** {d['bio']}")
lines.append("")
# WHAT
lines.append("## 📝 WHAT")
lines.append(f"> {d.get('text') or 'Could not extract tweet text'}")
lines.append("")
if d.get("timestamp"):
lines.append(f"**Posted:** {d['timestamp']}")
metrics = d.get("metrics", {})
if metrics:
m_parts = [f"{v} {k}" for k, v in metrics.items()]
lines.append(f"**Metrics:** {' | '.join(m_parts)}")
if d.get("images"):
lines.append(f"**Images:** {len(d['images'])} attached")
lines.append("")
# VERIFY
lines.append("## ✅ VERIFY")
tickers = analysis.get("tickers_found", [])
td = analysis.get("ticker_data", {})
if tickers:
lines.append(f"**Tickers mentioned:** {', '.join('$' + t for t in tickers)}")
lines.append("")
for t, info in td.items():
if isinstance(info, dict) and not info.get("error"):
lines.append(f"### ${t}" + (f" - {info.get('name', '')}" if info.get('name') else ""))
if info.get("price"):
lines.append(f"- **Price:** ${info['price']}")
if info.get("market_cap"):
mc = info["market_cap"]
if mc > 1e9:
lines.append(f"- **Market Cap:** ${mc/1e9:.2f}B")
else:
lines.append(f"- **Market Cap:** ${mc/1e6:.1f}M")
if info.get("volume"):
lines.append(f"- **Volume:** {info['volume']:,}")
if info.get("day_change_pct"):
lines.append(f"- **Day Change:** {info['day_change_pct']:.2f}%")
if info.get("52w_high") and info.get("52w_low"):
lines.append(f"- **52W Range:** ${info['52w_low']} - ${info['52w_high']}")
lines.append("")
elif isinstance(info, dict) and info.get("error"):
lines.append(f"- ${t}: lookup failed")
else:
lines.append("No tickers mentioned in tweet.")
lines.append("")
# RED FLAGS
lines.append("## 🚩 RED FLAGS")
flags = analysis.get("red_flags", [])
if flags:
for f in flags:
lines.append(f"- ⚠️ {f}")
else:
lines.append("- None detected")
lines.append("")
# MONEY
lines.append("## 💰 MONEY")
if tickers and not flags:
lines.append("Potential opportunity identified. Research further before any position.")
elif tickers and flags:
lines.append("Tickers mentioned but red flags present. High risk of promoted/manipulated asset.")
else:
lines.append("No direct financial opportunity identified in this tweet.")
lines.append("")
# VERDICT
lines.append("## 🎯 VERDICT")
lines.append(f"**{analysis['verdict']}**")
lines.append("")
# Scrape issues
if d.get("scrape_error"):
lines.append(f"---\n⚠️ *Scrape warning: {d['scrape_error']}*")
return "\n".join(lines)
async def main():
parser = argparse.ArgumentParser(description="Analyze a tweet")
parser.add_argument("url", help="Tweet URL (x.com or twitter.com)")
parser.add_argument("--json", action="store_true", dest="json_output", help="Output JSON")
parser.add_argument("-o", "--output", help="Write output to file")
args = parser.parse_args()
# Validate URL
if not re.search(r'(x\.com|twitter\.com)/.+/status/\d+', args.url):
print("ERROR: Invalid tweet URL", file=sys.stderr)
sys.exit(1)
print("Scraping tweet...", file=sys.stderr)
data = await scrape_tweet(args.url)
print("Analyzing...", file=sys.stderr)
analysis = analyze(data)
if args.json_output:
output = json.dumps(analysis, indent=2, default=str)
else:
output = format_markdown(analysis)
if args.output:
with open(args.output, "w") as f:
f.write(output)
print(f"Written to {args.output}", file=sys.stderr)
else:
print(output)
if __name__ == "__main__":
asyncio.run(main())

View File

@ -0,0 +1,51 @@
# Data Source Connectors
Standalone Python scripts for fetching crypto/market data. Each has CLI with `--pretty` (JSON formatting) and `--summary` (human-readable output).
## defillama.py ✅ (no auth needed)
DefiLlama API — DeFi protocol data, token prices, yield farming opportunities.
```bash
./defillama.py protocols --limit 10 --summary # Top protocols by TVL
./defillama.py tvl aave --pretty # TVL for specific protocol
./defillama.py prices coingecko:bitcoin coingecko:ethereum --summary
./defillama.py yields --limit 20 --stablecoins --summary # Top stablecoin yields
```
**Endpoints used:** api.llama.fi/protocols, api.llama.fi/tvl/{name}, coins.llama.fi/prices, yields.llama.fi/pools
## coinglass.py 🔑 (API key recommended)
Coinglass — funding rates, open interest, long/short ratios.
```bash
export COINGLASS_API_KEY=your_key # Get at coinglass.com/pricing
./coinglass.py funding --summary
./coinglass.py oi --summary
./coinglass.py long-short --summary
```
**Note:** Free internal API endpoints often return empty data. API key required for reliable access.
## arkham.py 🔑 (API key required)
Arkham Intelligence — whale wallet tracking, token transfers, entity search.
```bash
export ARKHAM_API_KEY=your_key # Sign up at platform.arkhamintelligence.com
./arkham.py notable --summary # List known whale addresses
./arkham.py address vitalik --summary # Address intelligence (supports name shortcuts)
./arkham.py transfers 0x1234... --limit 10 --pretty
./arkham.py search "binance" --pretty
```
**Built-in shortcuts:** vitalik, justin-sun, binance-hot, coinbase-prime, aave-treasury, uniswap-deployer
## Programmatic Usage
```python
from tools.data_sources.defillama import get_protocols, get_prices, get_yield_pools
from tools.data_sources.coinglass import get_funding_rates
from tools.data_sources.arkham import get_address_info, NOTABLE_ADDRESSES
```

4
tools/data_sources/__init__.py Executable file
View File

@ -0,0 +1,4 @@
"""Crypto & market data source connectors."""
from pathlib import Path
DATA_SOURCES_DIR = Path(__file__).parent

167
tools/data_sources/arkham.py Executable file
View File

@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""Arkham Intelligence connector — whale tracking, token flows, address intelligence.
Requires API key for most endpoints. Set ARKHAM_API_KEY env var.
Sign up at https://platform.arkhamintelligence.com
"""
import argparse
import json
import os
import sys
from typing import Any
import requests
BASE = "https://api.arkhamintelligence.com"
TIMEOUT = 30
NOTABLE_ADDRESSES = {
"vitalik": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"justin-sun": "0x3DdfA8eC3052539b6C9549F12cEA2C295cfF5296",
"binance-hot": "0x28C6c06298d514Db089934071355E5743bf21d60",
"coinbase-prime": "0xA9D1e08C7793af67e9d92fe308d5697FB81d3E43",
"aave-treasury": "0x464C71f6c2F760DdA6093dCB91C24c39e5d6e18c",
"uniswap-deployer": "0x41653c7d61609D856f29355E404F310Ec4142Cfb",
}
def _get(path: str, params: dict | None = None) -> Any:
key = os.environ.get("ARKHAM_API_KEY")
headers = {"User-Agent": "Mozilla/5.0"}
if key:
headers["API-Key"] = key
r = requests.get(f"{BASE}/{path}", params=params, headers=headers, timeout=TIMEOUT)
if r.status_code in (401, 403) or "api key" in r.text.lower():
raise EnvironmentError(
"Arkham API key required. Set ARKHAM_API_KEY env var.\n"
"Sign up at https://platform.arkhamintelligence.com"
)
r.raise_for_status()
return r.json()
def resolve_address(name_or_addr: str) -> str:
return NOTABLE_ADDRESSES.get(name_or_addr.lower(), name_or_addr)
# ── Data fetchers ───────────────────────────────────────────────────────────
def get_address_info(address: str) -> dict:
return _get(f"intelligence/address/{resolve_address(address)}")
def get_transfers(address: str, limit: int = 20) -> dict:
return _get("token/transfers", {"address": resolve_address(address), "limit": limit})
def search_entity(query: str) -> dict:
return _get("intelligence/search", {"query": query})
# ── Summary helpers ─────────────────────────────────────────────────────────
def summary_address(data: dict) -> str:
lines = ["═══ Address Intelligence ═══", ""]
if isinstance(data, dict):
entity = data.get("entity", {}) or {}
if entity:
lines.append(f" Entity: {entity.get('name', 'Unknown')}")
lines.append(f" Type: {entity.get('type', 'Unknown')}")
lines.append(f" Address: {data.get('address', '?')}")
labels = data.get("labels", [])
if labels:
lines.append(f" Labels: {', '.join(str(l) for l in labels)}")
else:
lines.append(f" {data}")
return "\n".join(lines)
def summary_transfers(data) -> str:
lines = ["═══ Recent Transfers ═══", ""]
transfers = data if isinstance(data, list) else (data.get("transfers", data.get("data", [])) if isinstance(data, dict) else [])
if not transfers:
lines.append(" No transfers found.")
return "\n".join(lines)
for t in transfers[:15]:
token = t.get("token", {}).get("symbol", "?") if isinstance(t.get("token"), dict) else "?"
amount = t.get("amount", t.get("value", "?"))
fr = t.get("from", {})
to = t.get("to", {})
fl = (fr.get("label") or fr.get("address", "?")[:12]) if isinstance(fr, dict) else str(fr)[:12]
tl = (to.get("label") or to.get("address", "?")[:12]) if isinstance(to, dict) else str(to)[:12]
lines.append(f" {token:<8} {str(amount):>15} {fl}{tl}")
return "\n".join(lines)
def summary_notable() -> str:
lines = ["═══ Notable/Whale Addresses ═══", ""]
for name, addr in NOTABLE_ADDRESSES.items():
lines.append(f" {name:<20} {addr}")
lines.append("")
lines.append(" Use these as shortcuts: arkham.py address vitalik")
return "\n".join(lines)
# ── CLI ─────────────────────────────────────────────────────────────────────
def main():
common = argparse.ArgumentParser(add_help=False)
common.add_argument("--pretty", action="store_true", help="Pretty-print JSON output")
common.add_argument("--summary", action="store_true", help="Human-readable summary")
parser = argparse.ArgumentParser(description="Arkham Intelligence connector", parents=[common])
sub = parser.add_subparsers(dest="command", required=True)
p_addr = sub.add_parser("address", help="Address intelligence", parents=[common])
p_addr.add_argument("address", help="Ethereum address or notable name")
p_tx = sub.add_parser("transfers", help="Recent token transfers", parents=[common])
p_tx.add_argument("address")
p_tx.add_argument("--limit", type=int, default=20)
p_search = sub.add_parser("search", help="Search entities", parents=[common])
p_search.add_argument("query")
sub.add_parser("notable", help="List notable/whale addresses", parents=[common])
args = parser.parse_args()
try:
if args.command == "notable":
if args.summary:
print(summary_notable())
else:
json.dump(NOTABLE_ADDRESSES, sys.stdout, indent=2 if args.pretty else None)
print()
return
if args.command == "address":
data = get_address_info(args.address)
if args.summary:
print(summary_address(data)); return
result = data
elif args.command == "transfers":
data = get_transfers(args.address, args.limit)
if args.summary:
print(summary_transfers(data)); return
result = data
elif args.command == "search":
result = search_entity(args.query)
else:
parser.print_help(); return
json.dump(result, sys.stdout, indent=2 if args.pretty else None)
print()
except EnvironmentError as e:
print(str(e), file=sys.stderr); sys.exit(1)
except requests.HTTPError as e:
detail = e.response.text[:200] if e.response is not None else ""
print(json.dumps({"error": str(e), "detail": detail}), file=sys.stderr); sys.exit(1)
except Exception as e:
print(json.dumps({"error": f"{type(e).__name__}: {e}"}), file=sys.stderr); sys.exit(1)
if __name__ == "__main__":
main()

181
tools/data_sources/coinglass.py Executable file
View File

@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""Coinglass data connector — funding rates, open interest, long/short ratios.
Uses the free fapi.coinglass.com internal API where available.
Some endpoints may return empty data without authentication.
Set COINGLASS_API_KEY env var for authenticated access to open-api.coinglass.com.
"""
import argparse
import json
import os
import sys
from typing import Any
import requests
FREE_BASE = "https://fapi.coinglass.com/api"
AUTH_BASE = "https://open-api.coinglass.com/public/v2"
TIMEOUT = 30
def _free_get(path: str, params: dict | None = None) -> Any:
headers = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
"Referer": "https://www.coinglass.com/",
}
r = requests.get(f"{FREE_BASE}/{path}", params=params, headers=headers, timeout=TIMEOUT)
r.raise_for_status()
data = r.json()
if data.get("code") == "0" or data.get("success"):
return data.get("data", [])
raise ValueError(f"API error: {data.get('msg', 'unknown')}")
def _auth_get(path: str, params: dict | None = None) -> Any:
key = os.environ.get("COINGLASS_API_KEY")
if not key:
raise EnvironmentError("COINGLASS_API_KEY not set. Get one at https://www.coinglass.com/pricing")
headers = {"coinglassSecret": key}
r = requests.get(f"{AUTH_BASE}/{path}", params=params, headers=headers, timeout=TIMEOUT)
r.raise_for_status()
data = r.json()
if data.get("success") or data.get("code") == "0":
return data.get("data", [])
raise ValueError(f"API error: {data.get('msg', 'unknown')}")
# ── Data fetchers ───────────────────────────────────────────────────────────
def get_funding_rates() -> list[dict]:
"""Funding rates across exchanges."""
try:
data = _free_get("fundingRate/v2/home")
if data:
return data
except Exception:
pass
return _auth_get("funding")
def get_open_interest() -> list[dict]:
"""Aggregated open interest data."""
try:
data = _free_get("openInterest/v3/home")
if data:
return data
except Exception:
pass
return _auth_get("open_interest")
def get_long_short_ratio() -> list[dict]:
"""Global long/short account ratios."""
try:
data = _free_get("futures/longShort/v2/home")
if data:
return data
except Exception:
pass
return _auth_get("long_short")
# ── Summary helpers ─────────────────────────────────────────────────────────
def _no_data_msg(name: str) -> str:
return (f"No {name} data available (free API may be restricted).\n"
"Set COINGLASS_API_KEY for full access: https://www.coinglass.com/pricing")
def summary_funding(data: list[dict]) -> str:
if not data:
return _no_data_msg("funding rate")
lines = ["═══ Funding Rates ═══", ""]
for item in data[:20]:
symbol = item.get("symbol", item.get("coin", "?"))
rate = None
if "uMarginList" in item:
for m in item["uMarginList"]:
rate = m.get("rate")
if rate is not None:
break
else:
rate = item.get("rate")
if rate is not None:
lines.append(f" {symbol:<10} {float(rate)*100:>8.4f}%")
else:
lines.append(f" {symbol:<10} (rate unavailable)")
return "\n".join(lines)
def summary_oi(data: list[dict]) -> str:
if not data:
return _no_data_msg("open interest")
lines = ["═══ Open Interest ═══", ""]
for item in data[:20]:
symbol = item.get("symbol", item.get("coin", "?"))
oi = item.get("openInterest", item.get("oi", 0))
lines.append(f" {symbol:<10} OI: ${float(oi):>15,.0f}")
return "\n".join(lines)
def summary_ls(data: list[dict]) -> str:
if not data:
return _no_data_msg("long/short")
lines = ["═══ Long/Short Ratios ═══", ""]
for item in data[:20]:
symbol = item.get("symbol", item.get("coin", "?"))
long_rate = item.get("longRate", item.get("longRatio", "?"))
short_rate = item.get("shortRate", item.get("shortRatio", "?"))
lines.append(f" {symbol:<10} Long: {long_rate} Short: {short_rate}")
return "\n".join(lines)
# ── CLI ─────────────────────────────────────────────────────────────────────
def main():
common = argparse.ArgumentParser(add_help=False)
common.add_argument("--pretty", action="store_true", help="Pretty-print JSON output")
common.add_argument("--summary", action="store_true", help="Human-readable summary")
parser = argparse.ArgumentParser(description="Coinglass data connector", parents=[common])
sub = parser.add_subparsers(dest="command", required=True)
sub.add_parser("funding", help="Funding rates across exchanges", parents=[common])
sub.add_parser("oi", help="Open interest overview", parents=[common])
sub.add_parser("long-short", help="Long/short ratios", parents=[common])
args = parser.parse_args()
try:
if args.command == "funding":
data = get_funding_rates()
if args.summary:
print(summary_funding(data)); return
result = data
elif args.command == "oi":
data = get_open_interest()
if args.summary:
print(summary_oi(data)); return
result = data
elif args.command == "long-short":
data = get_long_short_ratio()
if args.summary:
print(summary_ls(data)); return
result = data
else:
parser.print_help(); return
json.dump(result, sys.stdout, indent=2 if args.pretty else None)
print()
except EnvironmentError as e:
print(str(e), file=sys.stderr); sys.exit(1)
except requests.HTTPError as e:
print(json.dumps({"error": str(e)}), file=sys.stderr); sys.exit(1)
except Exception as e:
print(json.dumps({"error": f"{type(e).__name__}: {e}"}), file=sys.stderr); sys.exit(1)
if __name__ == "__main__":
main()

176
tools/data_sources/defillama.py Executable file
View File

@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""DefiLlama API connector — TVL, token prices, yield/APY data.
No authentication required. All endpoints are free.
API base: https://api.llama.fi | Prices: https://coins.llama.fi | Yields: https://yields.llama.fi
"""
import argparse
import json
import sys
from typing import Any
import requests
BASE = "https://api.llama.fi"
COINS_BASE = "https://coins.llama.fi"
YIELDS_BASE = "https://yields.llama.fi"
TIMEOUT = 30
def _get(url: str, params: dict | None = None) -> Any:
r = requests.get(url, params=params, timeout=TIMEOUT)
r.raise_for_status()
return r.json()
# ── Protocol / TVL ──────────────────────────────────────────────────────────
def get_protocols(limit: int = 20) -> list[dict]:
"""Top protocols by TVL."""
data = _get(f"{BASE}/protocols")
# Sort by tvl descending, filter out CEXes
protos = [p for p in data if p.get("category") != "CEX" and p.get("tvl")]
protos.sort(key=lambda p: p.get("tvl", 0), reverse=True)
return protos[:limit]
def get_tvl(protocol: str) -> dict:
"""Get current TVL for a specific protocol (slug name)."""
val = _get(f"{BASE}/tvl/{protocol}")
return {"protocol": protocol, "tvl": val}
def get_protocol_detail(protocol: str) -> dict:
"""Full protocol details including chain breakdowns."""
return _get(f"{BASE}/protocol/{protocol}")
# ── Token Prices ────────────────────────────────────────────────────────────
def get_prices(coins: list[str]) -> dict:
"""Get current prices. Coins format: 'coingecko:ethereum', 'ethereum:0x...', etc."""
joined = ",".join(coins)
data = _get(f"{COINS_BASE}/prices/current/{joined}")
return data.get("coins", {})
# ── Yields / APY ────────────────────────────────────────────────────────────
def get_yield_pools(limit: int = 30, min_tvl: float = 1_000_000, stablecoin_only: bool = False) -> list[dict]:
"""Top yield pools sorted by APY."""
data = _get(f"{YIELDS_BASE}/pools")
pools = data.get("data", [])
# Filter
pools = [p for p in pools if (p.get("tvlUsd") or 0) >= min_tvl and (p.get("apy") or 0) > 0]
if stablecoin_only:
pools = [p for p in pools if p.get("stablecoin")]
pools.sort(key=lambda p: p.get("apy", 0), reverse=True)
return pools[:limit]
# ── Summary helpers ─────────────────────────────────────────────────────────
def _fmt_usd(v: float) -> str:
if v >= 1e9:
return f"${v/1e9:.2f}B"
if v >= 1e6:
return f"${v/1e6:.1f}M"
return f"${v:,.0f}"
def summary_protocols(protos: list[dict]) -> str:
lines = ["═══ Top Protocols by TVL ═══", ""]
for i, p in enumerate(protos, 1):
lines.append(f" {i:>2}. {p['name']:<25} TVL: {_fmt_usd(p.get('tvl', 0)):>12} chain: {p.get('chain', '?')}")
return "\n".join(lines)
def summary_prices(prices: dict) -> str:
lines = ["═══ Token Prices ═══", ""]
for coin, info in prices.items():
lines.append(f" {info.get('symbol', coin):<10} ${info['price']:>12,.2f} (confidence: {info.get('confidence', '?')})")
return "\n".join(lines)
def summary_yields(pools: list[dict]) -> str:
lines = ["═══ Top Yield Pools ═══", ""]
for i, p in enumerate(pools, 1):
lines.append(
f" {i:>2}. {p.get('symbol','?'):<25} APY: {p.get('apy',0):>8.2f}% "
f"TVL: {_fmt_usd(p.get('tvlUsd',0)):>10} {p.get('chain','?')}/{p.get('project','?')}"
)
return "\n".join(lines)
# ── CLI ─────────────────────────────────────────────────────────────────────
def main():
common = argparse.ArgumentParser(add_help=False)
common.add_argument("--pretty", action="store_true", help="Pretty-print JSON output")
common.add_argument("--summary", action="store_true", help="Human-readable summary")
parser = argparse.ArgumentParser(description="DefiLlama data connector", parents=[common])
sub = parser.add_subparsers(dest="command", required=True)
# protocols
p_proto = sub.add_parser("protocols", help="Top protocols by TVL", parents=[common])
p_proto.add_argument("--limit", type=int, default=20)
# tvl
p_tvl = sub.add_parser("tvl", help="TVL for a specific protocol", parents=[common])
p_tvl.add_argument("protocol", help="Protocol slug (e.g. aave, lido)")
# prices
p_price = sub.add_parser("prices", help="Token prices", parents=[common])
p_price.add_argument("coins", nargs="+", help="Coin IDs: coingecko:ethereum, ethereum:0x...")
# yields
p_yield = sub.add_parser("yields", help="Top yield pools", parents=[common])
p_yield.add_argument("--limit", type=int, default=30)
p_yield.add_argument("--min-tvl", type=float, default=1_000_000)
p_yield.add_argument("--stablecoins", action="store_true")
args = parser.parse_args()
try:
if args.command == "protocols":
data = get_protocols(args.limit)
if args.summary:
print(summary_protocols(data))
return
result = [{"name": p["name"], "tvl": p.get("tvl"), "chain": p.get("chain"), "category": p.get("category"), "symbol": p.get("symbol")} for p in data]
elif args.command == "tvl":
result = get_tvl(args.protocol)
if args.summary:
print(f"{args.protocol}: {_fmt_usd(result['tvl'])}")
return
elif args.command == "prices":
result = get_prices(args.coins)
if args.summary:
print(summary_prices(result))
return
elif args.command == "yields":
data = get_yield_pools(args.limit, args.min_tvl, args.stablecoins)
if args.summary:
print(summary_yields(data))
return
result = [{"symbol": p.get("symbol"), "apy": p.get("apy"), "tvlUsd": p.get("tvlUsd"), "chain": p.get("chain"), "project": p.get("project"), "pool": p.get("pool")} for p in data]
else:
parser.print_help()
return
indent = 2 if args.pretty else None
json.dump(result, sys.stdout, indent=indent)
print()
except requests.HTTPError as e:
print(json.dumps({"error": str(e)}), file=sys.stderr)
sys.exit(1)
except Exception as e:
print(json.dumps({"error": f"Unexpected: {e}"}), file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

15
tools/tweet_analyzer_wrapper.sh Executable file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Tweet Analyzer Wrapper - for OpenClaw agent use
# Usage: ./tweet_analyzer_wrapper.sh <tweet_url> [output_file]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
URL="${1:?Usage: $0 <tweet_url> [output_file]}"
OUTPUT="${2:-}"
if [ -n "$OUTPUT" ]; then
python3 "$SCRIPT_DIR/analyze_tweet.py" "$URL" -o "$OUTPUT"
echo "Analysis written to $OUTPUT"
else
python3 "$SCRIPT_DIR/analyze_tweet.py" "$URL"
fi