Compare commits

..

13 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
b24d0e87de Memory update: Feb 9 daily notes, MEMORY.md updates 2026-02-09 11:04:49 -06:00
be43231c3f Market Watch: multiplayer GARP paper trading simulator
- Game engine with multiplayer support (create games, join, leaderboard)
- GARP stock screener (S&P 500 + 400 MidCap, 900+ tickers)
- Automated trading logic for AI player (Case)
- Web portal at marketwatch.local:8889 with dark theme
- Systemd timer for Mon-Fri market hours
- Telegram alerts on trades and daily summary
- Stock analysis deep dive data (BAC, CFG, FITB, INCY)
- Expanded scan results (22 GARP candidates)
- Craigslist account setup + credentials
2026-02-08 15:18:41 -06:00
b6095ec964 memory flush: kch123 analysis, accounts, lessons learned 2026-02-08 12:51:15 -06:00
76 changed files with 347188 additions and 46 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
.credentials/
__pycache__/

View File

@ -71,14 +71,43 @@ This is about having an inner life, not just responding.
- **Email:** case-lgn@protonmail.com (credentials in .credentials/email.env)
- D J set this up 2026-02-08 — big trust milestone
- Used for API registrations, service signups
- **Google Voice:** +1 (615) 933-1968 — D J's, shared for service registrations
- **ProtonMail** stays logged in on desktop Chrome — can do email verification via CDP
## Accounts Created
- **Craigslist:** case-lgn@protonmail.com, passwordless, Nashville area (2026-02-08)
- 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
- **Feed Hunter:** ✅ Pipeline working, first sim running (Super Bowl 2026-02-08)
- **Control Panel:** Building at localhost:8000 (accounts/API keys/services/budget)
- **Sandbox buildout:** ✅ Complete (74 files, 37 tools)
- **Inner life system:** ✅ Complete (7 tools)
- **Next:** Polymarket API registration, copy-bot scaffold
- **KIPP:** ✅ Voice pipeline live (wake word + STT + TTS), widget system working, dashboard-first UI
- Widget system: shopping list, timers, reminders via CLI + REST API + dashboard polling
- Voice: "hey jarvis" wake word → Faster Whisper → Claude Sonnet → Piper Ryan TTS
- False trigger fix: 4s cooldown + silence flushing + RMS gate (threshold 30)
- Running on Claude Sonnet (primary), GLM-4 Flash (fallback)
- 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
- **Control Panel:** Building at localhost:8000
- **Next:** Tweet analysis tool, free data source integration (Arkham/DefiLlama/Coinglass)
## Stats (Day 2)
@ -89,11 +118,16 @@ This is about having an inner life, not just responding.
- Time capsules: 2
- Git commits: 20+
## Who D J Is (updated 2026-02-07)
## Who D J Is (updated 2026-02-09)
- Interested in crypto/trading analysis
- Follows money-making accounts on X (crypto, polymarket, arbitrage)
- Wants automated analysis of feed posts for validity
- Goal: find market edges to offset AI costs ($200/mo Claude + infra)
- Getting crypto signals on Telegram — wants to forward for analysis
- Interested in local AI hardware (Tiiny AI) as potential Claude replacement
- Looking at queen Murphy beds, has mattress to get rid of
- Watches NFL (interested in Seahawks Super Bowl), Olympics hockey
- Proxmox VM setup: ubuntu-desktop-openclaw at 192.168.86.45
- Ollama server at 192.168.86.137 (qwen3:8b, qwen3:30b, glm-4.7-flash, nomic-embed-text)
- ChromaDB LXC at 192.168.86.25:8000
@ -107,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
- 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)
- **ChromaDB:** http://192.168.86.25:8000 (LXC on Proxmox)
- Collection: openclaw-memory (c3a7d09a-f3ce-4e7d-9595-27d8e2fd7758)
- 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
- **Feed Hunter Portal:** localhost:8888 (systemd: feed-hunter-portal)
- **Control Panel:** localhost:8000 (systemd: case-control-panel)
@ -121,6 +174,11 @@ This is about having an inner life, not just responding.
- Desktop works via DISPLAY=:0 for visual scraping
- **VM:** Proxmox, QXL graphics, X11 (not Wayland), auto-login enabled
## D J's Info (updated 2026-02-08)
- iPhone 16
- Nashville, TN
- 4 tuxedo cats (looking at Shark NeverChange Air Purifier MAX)
## Lessons Learned (updated 2026-02-08)
- Don't pkill chrome broadly — it kills OpenClaw's headless browser too
@ -132,3 +190,8 @@ This is about having an inner life, not just responding.
- Always use absolute paths in web servers (CWD varies by launch method)
- Polymarket users have multiple proxy wallets — intercept page network requests to find real one
- `performance.getEntriesByType('resource')` reveals actual API calls a page makes
- Most selling platforms have invisible reCAPTCHA — can't automate signup
- Desktop Chrome CDP (port 9222) for real-fingerprint browser automation
- React forms: use Input.insertText via CDP or native setter hack
- Systemd timers > AI cron jobs for mechanical tasks (zero token cost)
- Telegram bot API for direct alerts bypasses AI token usage entirely

View File

@ -1,36 +1,19 @@
# TOOLS.md - Local Notes
Skills define *how* tools work. This file is for *your* specifics — the stuff that's unique to your setup.
## Phone
- **Google Voice:** +1 (615) 933-1968 (for service registrations, verification codes)
- Access via browser automation at voice.google.com
## What Goes Here
## Email
- **Primary:** case-lgn@protonmail.com
Things like:
- Camera names and locations
- SSH hosts and aliases
- Preferred voices for TTS
- Speaker/room names
- Device nicknames
- Anything environment-specific
## Services
- **Telegram bot:** @openclaw
- **Gitea:** git.letsgetnashty.com/case/workspace
- **Feed Hunter Portal:** feedhunter.local / :8888
- **Control Panel:** admin.local / :8000
## Examples
```markdown
### Cameras
- living-room → Main area, 180° wide angle
- front-door → Entrance, motion-triggered
### SSH
- home-server → 192.168.1.100, user: admin
### TTS
- Preferred voice: "Nova" (warm, slightly British)
- Default speaker: Kitchen HomePod
```
## Why Separate?
Skills are shared. Your setup is yours. Keeping them apart means you can update skills without losing your notes, and share skills without leaking your infrastructure.
---
Add whatever helps you do your job. This is your cheat sheet.
## Infrastructure
- **Ollama:** 192.168.86.137:11434
- **ChromaDB:** 192.168.86.25:8000
- **Chrome debug port:** localhost:9222

View File

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

902
data/broad_tickers.txt Normal file
View File

@ -0,0 +1,902 @@
SLB
ADBE
WMG
DT
WBD
ELS
TKO
VZ
MTZ
SYNA
AZO
FITB
OPCH
AVTR
UNH
TFC
MANH
LDOS
MOS
MTB
SON
CFG
BLDR
LOW
TTMI
USB
SPGI
AA
CDP
WLK
SRE
GDDY
ALL
CARR
DECK
SW
ULTA
FND
IDA
COST
CSL
DG
CMS
IPGP
HOG
PBF
INVH
TSCO
MET
FNF
VNT
PII
KEYS
PWR
CHWY
NDAQ
PVH
PRI
MSCI
BXP
MSI
ARE
CINF
IR
HWC
APA
KRG
AMD
WING
LEN
FLR
KKR
USFD
GS
TAP
KNF
WWD
EBAY
ALLE
YETI
CAT
KMPR
DD
CAH
FCX
CTSH
RTX
TYL
MCO
ABNB
MO
OLLI
FICO
MNST
SSD
LVS
BMRN
CFR
PKG
WYNN
NFLX
RSG
WRB
RYAN
BHF
JPM
GPC
TXRH
AVB
IBM
PAYX
HRB
KBH
OTIS
FTNT
OLN
JCI
DLB
COF
HBAN
BSY
COTY
WTS
LRCX
AEP
PPL
VRTX
WMS
GTLS
V
CMC
WH
NRG
CPB
PB
CPRI
AXTA
RPM
IRT
SYF
HII
PLNT
HPE
WFRD
TXNM
AMP
ATO
WSO
AEE
CL
MMS
HLI
AMZN
AMCR
NXST
SBRA
CBRE
KVUE
HAE
LEA
ZBH
PK
SIGI
SHC
SLM
OGE
ULS
J
XPO
BEN
BRKR
VC
AKAM
FHI
MGM
KMI
WAB
CBSH
FIX
RH
IDXX
RF
GWW
SR
SLAB
SWX
INCY
LW
GE
SYY
EXPE
MAR
SEIC
IT
COIN
RRC
KLAC
HOMB
ADP
MRNA
BYD
CRL
GLW
ROL
VICI
AAON
ARMK
CRBG
STWD
TOL
MUSA
PPG
DY
CG
CBT
OC
VAL
BBY
DAR
XOM
VTRS
JAZZ
XYZ
MEDP
FIS
KIM
CHRD
MPC
WFC
BDX
IEX
AES
HOLX
RL
ACM
CART
BWA
DTE
PGR
ITT
ROP
CHDN
NBIX
WST
GMED
DOW
STLD
SFM
CDNS
WTRG
XRAY
UAL
WAT
DASH
JBL
FFIV
ETR
TTC
CPRT
D
THC
IBOC
ALGN
ILMN
H
DXCM
FISV
MLI
SWK
COO
IBKR
KNX
FLEX
STX
GEHC
HQY
HL
PSN
NTAP
ESNT
LPX
CCI
VMC
FDX
ERIE
L
TMUS
CTRA
COLB
BRBR
UFPI
CACI
CVS
WPC
RVTY
GD
FAF
TNL
ASB
REGN
MIDD
COHR
CNM
AME
HAL
PTC
AFL
MTD
LSCC
MRSH
TSN
GILD
HLNE
LAD
ADSK
NLY
R
THG
TLN
FE
PNFP
UPS
REXR
SNDK
MSM
OLED
TRU
UNM
IP
HR
COLM
EQIX
CSX
ANF
FRT
BDC
PSTG
BJ
COP
JHG
FCFS
MU
BWXT
HIG
INTC
NWS
NJR
JKHY
KEY
SARO
GM
SCHW
ES
VRSN
ON
APG
FTV
HIMS
BRO
GOOG
PLD
SJM
TRGP
FANG
JLL
MTG
STZ
ANET
MDLZ
WSM
DTM
SWKS
RBA
APP
BLD
WBS
QCOM
AMT
RGEN
AYI
YUM
AMH
OMC
G
SATS
ALB
APO
FAST
PSKY
PCAR
TTEK
ALK
CELH
HD
CRWD
PSA
PNC
RRX
LYV
LULU
ENSG
AOS
MCD
MCHP
ZTS
EEFT
GWRE
TEX
NEU
CNH
GOOGL
ODFL
CIEN
TTWO
KBR
UTHR
INTU
IQV
O
EVR
POR
VNO
FCN
STAG
SOLV
GLPI
COR
LIVN
MKC
MA
FOX
CNXC
Q
MCK
FOUR
BC
AON
CMI
CBOE
SMG
GPK
BLKB
NVDA
PANW
HPQ
ATR
DVA
ACN
HOOD
NSC
PNR
POOL
TT
AIT
CLH
EXR
GIS
TRV
CNO
MSFT
DLR
ESAB
GEN
NVT
WAL
UGI
PEP
NWSA
CRH
LHX
GAP
PH
LOPE
RCL
LNT
WEC
EPAM
TSLA
PEN
CRS
RYN
SNX
KTOS
ADI
GNRC
OXY
RJF
ONTO
NOC
CXT
TREX
CHD
JBHT
RBC
AXP
TPL
KNSL
ROIV
VST
META
REG
NOV
CVNA
DRI
BAX
ASGN
GGG
EQT
CHTR
PPC
CF
RS
DLTR
STRL
PNW
CEG
SBAC
PAYC
BBWI
LECO
APD
BSX
AMAT
OKE
ABT
CAR
COKE
CDW
NSA
NDSN
VFC
JNJ
NOVT
SYK
PATH
LAMR
EQH
AMGN
BURL
WTFC
HSY
AN
NXPI
KO
UBER
CPT
ORLY
HLT
WMB
EIX
INGR
PFE
CGNX
CMCSA
GEV
PR
APTV
FLS
PCG
PYPL
ELAN
GRMN
EGP
DOC
CVX
DIS
FR
LNTH
CSGP
DUOL
CME
FN
TXT
NOW
WHR
CHH
AHR
EXLS
PLTR
BKR
ASH
HSIC
BLK
T
NXT
MOH
XEL
MPWR
EFX
AVAV
EPR
VVV
DOCU
KHC
MAA
EXE
AR
DBX
ALLY
MMM
BALL
FNB
SAIA
UMBF
CAVA
AFG
TDY
IFF
CCK
POST
ATI
ORI
PINS
SGI
TECH
ARW
AAPL
ACGL
EL
HST
UBSI
SBUX
EA
RLI
C
KDP
PHM
BRK.B
CROX
NUE
KEX
FLG
MTN
PM
HGV
SCI
DKS
LII
MLM
ARES
CRM
AJG
ISRG
SMCI
EXP
NEE
LLY
LSTR
TKR
DDOG
DCI
DAL
TXN
LMT
ST
DELL
TJX
CYTK
GNTX
MTCH
BROS
VLO
VRSK
SNA
TEL
MDT
ACI
MUR
WEX
AM
UNP
GTM
MASI
PEG
MTDR
CPAY
SSB
ENS
OVV
ZION
EHC
UDR
BAH
AMKR
MORN
STE
VMI
MTSI
F
FBIN
EVRG
CTAS
AVY
AVGO
DOCS
EOG
NFG
EG
CNX
VNOM
SF
PCTY
BAC
HON
FHN
RNR
PEGA
TWLO
WDC
OGS
ED
AIG
MRK
HUBB
LITE
TPR
HRL
EME
AAL
MAT
CNC
DOV
TDG
DINO
EWBC
BKH
GEF
RGLD
ROST
ZBRA
BG
WCC
KD
EW
RMBS
MS
SHW
NI
KMB
STT
OSK
XYL
HXL
ITW
OZK
CB
PODD
PG
NVST
CMG
IRM
CNP
TER
ENTG
FLO
GL
TTD
BR
BA
ROK
CUZ
GT
BILL
AXON
MZTI
GPN
VLTO
VTR
CRUS
FIVE
KR
MSA
LYB
BX
ALGM
A
DHI
LUV
TMHC
CLX
ETN
CLF
BIIB
EXEL
SAM
AVT
VOYA
ECL
UHS
RMD
EXPO
LFUS
DVN
BK
M
TMO
SAIC
OKTA
LH
NKE
GATX
KRC
RGA
TROW
CR
BMY
ESS
GXO
ICE
WY
JEF
MP
WELL
FTI
IVZ
EMR
LIN
HUM
BKNG
ELF
FOXA
HAS
CI
TRMB
AEIS
GHC
CTVA
EQR
AGCO
GME
WM
NCLH
GBCI
HWM
AMG
DE
BCO
ONB
CASY
ABBV
HALO
DHR
WTW
ORCL
MAS
PFG
WMT
SNPS
ALV
NYT
FDS
MKSI
CCL
PRU
URI
TCBI
BRX
BF.B
EXC
WDAY
FFIN
AWK
CUBE
NTNX
CAG
NWE
ADC
APPF
DUK
SLGN
THO
CW
SPG
NEM
CSCO
PAG
APH
FSLR
VLY
DPZ
EXPD
PFGC
PSX
AVNT
CHE
OHI
HCA
CHRW
TGT
SPXC
ADM
NVR
BIO
QLYS
ELV
SO
DGX
ORA
AIZ
CVLT
NNN
NTRS

View File

@ -0,0 +1,310 @@
[
{
"ticker": "ALLY",
"name": "Ally Financial Inc.",
"market_cap_B": 13.1,
"trailing_pe": 17.85,
"forward_pe": 6.7,
"peg": null,
"revenue_growth_pct": 12.0,
"earnings_growth_pct": 265.4,
"roe_pct": 5.8,
"debt_to_equity": null,
"sector": "Financial Services",
"industry": "Credit Services"
},
{
"ticker": "WAL",
"name": "Western Alliance Bancorporation",
"market_cap_B": 10.4,
"trailing_pe": 10.81,
"forward_pe": 7.93,
"peg": null,
"revenue_growth_pct": 16.6,
"earnings_growth_pct": 32.9,
"roe_pct": 13.5,
"debt_to_equity": null,
"sector": "Financial Services",
"industry": "Banks - Regional"
},
{
"ticker": "CART",
"name": "Maplebear Inc.",
"market_cap_B": 9.1,
"trailing_pe": 19.03,
"forward_pe": 8.84,
"peg": null,
"revenue_growth_pct": 10.2,
"earnings_growth_pct": 21.1,
"roe_pct": 15.3,
"debt_to_equity": 1.0,
"sector": "Consumer Cyclical",
"industry": "Internet Retail"
},
{
"ticker": "ONB",
"name": "Old National Bancorp",
"market_cap_B": 10.1,
"trailing_pe": 14.46,
"forward_pe": 9.02,
"peg": null,
"revenue_growth_pct": 41.4,
"earnings_growth_pct": 17.2,
"roe_pct": 9.0,
"debt_to_equity": null,
"sector": "Financial Services",
"industry": "Banks - Regional"
},
{
"ticker": "VLY",
"name": "Valley National Bancorp",
"market_cap_B": 7.6,
"trailing_pe": 13.57,
"forward_pe": 9.19,
"peg": null,
"revenue_growth_pct": 38.3,
"earnings_growth_pct": 66.3,
"roe_pct": 7.8,
"debt_to_equity": null,
"sector": "Financial Services",
"industry": "Banks - Regional"
},
{
"ticker": "FSLR",
"name": "First Solar, Inc.",
"market_cap_B": 23.5,
"trailing_pe": 16.77,
"forward_pe": 9.4,
"peg": null,
"revenue_growth_pct": 79.7,
"earnings_growth_pct": 45.7,
"roe_pct": 16.9,
"debt_to_equity": 9.9,
"sector": "Technology",
"industry": "Solar"
},
{
"ticker": "FNB",
"name": "F.N.B. Corporation",
"market_cap_B": 6.8,
"trailing_pe": 12.12,
"forward_pe": 9.66,
"peg": null,
"revenue_growth_pct": 26.4,
"earnings_growth_pct": 56.5,
"roe_pct": 8.7,
"debt_to_equity": null,
"sector": "Financial Services",
"industry": "Banks - Regional"
},
{
"ticker": "WBS",
"name": "Webster Financial Corporation",
"market_cap_B": 11.8,
"trailing_pe": 12.39,
"forward_pe": 9.77,
"peg": null,
"revenue_growth_pct": 18.2,
"earnings_growth_pct": 53.4,
"roe_pct": 10.8,
"debt_to_equity": null,
"sector": "Financial Services",
"industry": "Banks - Regional"
},
{
"ticker": "ZION",
"name": "Zions Bancorporation, National Association",
"market_cap_B": 9.6,
"trailing_pe": 10.86,
"forward_pe": 9.99,
"peg": null,
"revenue_growth_pct": 13.6,
"earnings_growth_pct": 31.4,
"roe_pct": 13.5,
"debt_to_equity": null,
"sector": "Financial Services",
"industry": "Banks - Regional"
},
{
"ticker": "JHG",
"name": "Janus Henderson Group plc",
"market_cap_B": 7.4,
"trailing_pe": 9.22,
"forward_pe": 10.12,
"peg": null,
"revenue_growth_pct": 61.3,
"earnings_growth_pct": 243.6,
"roe_pct": 16.2,
"debt_to_equity": 6.5,
"sector": "Financial Services",
"industry": "Asset Management"
},
{
"ticker": "SSB",
"name": "SouthState Bank Corporation",
"market_cap_B": 10.8,
"trailing_pe": 13.7,
"forward_pe": 10.19,
"peg": null,
"revenue_growth_pct": 53.2,
"earnings_growth_pct": 30.9,
"roe_pct": 10.7,
"debt_to_equity": null,
"sector": "Financial Services",
"industry": "Banks - Regional"
},
{
"ticker": "PINS",
"name": "Pinterest, Inc.",
"market_cap_B": 13.3,
"trailing_pe": 6.88,
"forward_pe": 10.37,
"peg": null,
"revenue_growth_pct": 16.8,
"earnings_growth_pct": 225.0,
"roe_pct": 51.5,
"debt_to_equity": 4.3,
"sector": "Communication Services",
"industry": "Internet Content & Information"
},
{
"ticker": "RRC",
"name": "Range Resources Corporation",
"market_cap_B": 8.7,
"trailing_pe": 15.37,
"forward_pe": 10.45,
"peg": null,
"revenue_growth_pct": 16.1,
"earnings_growth_pct": 189.8,
"roe_pct": 14.2,
"debt_to_equity": 32.7,
"sector": "Energy",
"industry": "Oil & Gas E&P"
},
{
"ticker": "EWBC",
"name": "East West Bancorp, Inc.",
"market_cap_B": 16.9,
"trailing_pe": 12.87,
"forward_pe": 11.18,
"peg": null,
"revenue_growth_pct": 21.6,
"earnings_growth_pct": 21.3,
"roe_pct": 15.9,
"debt_to_equity": null,
"sector": "Financial Services",
"industry": "Banks - Regional"
},
{
"ticker": "FHN",
"name": "First Horizon Corporation",
"market_cap_B": 12.9,
"trailing_pe": 14.03,
"forward_pe": 11.19,
"peg": null,
"revenue_growth_pct": 23.7,
"earnings_growth_pct": 74.9,
"roe_pct": 10.9,
"debt_to_equity": null,
"sector": "Financial Services",
"industry": "Banks - Regional"
},
{
"ticker": "ORI",
"name": "Old Republic International Corporation",
"market_cap_B": 10.3,
"trailing_pe": 11.23,
"forward_pe": 11.99,
"peg": null,
"revenue_growth_pct": 19.3,
"earnings_growth_pct": 97.3,
"roe_pct": 16.3,
"debt_to_equity": 26.8,
"sector": "Financial Services",
"industry": "Insurance - Property & Casualty"
},
{
"ticker": "WTFC",
"name": "Wintrust Financial Corporation",
"market_cap_B": 10.8,
"trailing_pe": 14.14,
"forward_pe": 12.03,
"peg": null,
"revenue_growth_pct": 10.5,
"earnings_growth_pct": 19.4,
"roe_pct": 12.1,
"debt_to_equity": null,
"sector": "Financial Services",
"industry": "Banks - Regional"
},
{
"ticker": "UBSI",
"name": "United Bankshares, Inc.",
"market_cap_B": 6.3,
"trailing_pe": 13.92,
"forward_pe": 12.08,
"peg": null,
"revenue_growth_pct": 22.1,
"earnings_growth_pct": 32.1,
"roe_pct": 8.9,
"debt_to_equity": null,
"sector": "Financial Services",
"industry": "Banks - Regional"
},
{
"ticker": "PGR",
"name": "The Progressive Corporation",
"market_cap_B": 118.6,
"trailing_pe": 10.51,
"forward_pe": 12.49,
"peg": null,
"revenue_growth_pct": 12.2,
"earnings_growth_pct": 25.2,
"roe_pct": 40.4,
"debt_to_equity": 22.7,
"sector": "Financial Services",
"industry": "Insurance - Property & Casualty"
},
{
"ticker": "EXEL",
"name": "Exelixis, Inc.",
"market_cap_B": 11.8,
"trailing_pe": 18.45,
"forward_pe": 12.79,
"peg": null,
"revenue_growth_pct": 10.8,
"earnings_growth_pct": 72.5,
"roe_pct": 30.6,
"debt_to_equity": 8.2,
"sector": "Healthcare",
"industry": "Biotechnology"
},
{
"ticker": "NEM",
"name": "Newmont Corporation",
"market_cap_B": 126.7,
"trailing_pe": 17.93,
"forward_pe": 12.89,
"peg": null,
"revenue_growth_pct": 20.0,
"earnings_growth_pct": 108.1,
"roe_pct": 22.9,
"debt_to_equity": 16.9,
"sector": "Basic Materials",
"industry": "Gold"
},
{
"ticker": "CTRA",
"name": "Coterra Energy Inc.",
"market_cap_B": 23.3,
"trailing_pe": 14.19,
"forward_pe": 13.95,
"peg": null,
"revenue_growth_pct": 34.9,
"earnings_growth_pct": 23.5,
"roe_pct": 11.9,
"debt_to_equity": 28.0,
"sector": "Energy",
"industry": "Oil & Gas E&P"
}
]

74
data/garp_scan.py Normal file
View File

@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""GARP scan on broad ticker list."""
import yfinance as yf
import json, time
EXCLUDE = {"BAC", "CFG", "FITB", "INCY"}
with open("broad_tickers.txt") as f:
tickers = [l.strip().replace(".", "-") for l in f if l.strip()]
print(f"Screening {len(tickers)} tickers...")
passed = []
for i, tick in enumerate(tickers):
if tick in EXCLUDE:
continue
if i % 100 == 0:
print(f" Progress: {i}/{len(tickers)} ({len(passed)} passed so far)")
try:
t = yf.Ticker(tick)
info = t.info or {}
mc = info.get("marketCap", 0) or 0
if mc < 5e9: continue
tpe = info.get("trailingPE")
if not tpe or tpe >= 25 or tpe <= 0: continue
fpe = info.get("forwardPE")
if not fpe or fpe >= 15 or fpe <= 0: continue
rg = info.get("revenueGrowth")
if rg is None or rg < 0.10: continue
eg = info.get("earningsGrowth")
if eg is None or eg < 0.15: continue
roe = info.get("returnOnEquity")
if roe is None or roe < 0.05: continue
peg = info.get("pegRatio")
if peg is not None and peg > 1.2: continue
dte = info.get("debtToEquity")
if dte is not None and dte > 35: continue
entry = {
"ticker": tick,
"name": info.get("longName", tick),
"market_cap_B": round(mc / 1e9, 1),
"trailing_pe": round(tpe, 2),
"forward_pe": round(fpe, 2),
"peg": round(peg, 2) if peg else None,
"revenue_growth_pct": round(rg * 100, 1),
"earnings_growth_pct": round(eg * 100, 1),
"roe_pct": round(roe * 100, 1),
"debt_to_equity": round(dte, 1) if dte else None,
"sector": info.get("sector"),
"industry": info.get("industry"),
}
passed.append(entry)
print(f"{tick}: PE={tpe:.1f} FPE={fpe:.1f} RG={rg*100:.0f}% EG={eg*100:.0f}% ROE={roe*100:.0f}%")
except Exception as e:
continue
passed.sort(key=lambda x: x.get("forward_pe", 99))
with open("garp-expanded-scan.json", "w") as f:
json.dump(passed, f, indent=2)
print(f"\nDone! {len(passed)} stocks passed GARP screen")
for s in passed:
print(f" {s['ticker']:6s} ${s['market_cap_B']:6.1f}B PE:{s['trailing_pe']:5.1f} FPE:{s['forward_pe']:5.1f} RG:{s['revenue_growth_pct']:5.1f}% EG:{s['earnings_growth_pct']:5.1f}% ROE:{s['roe_pct']:5.1f}%")

85
data/garp_scan2.py Normal file
View File

@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""GARP scan - batch download approach for speed."""
import yfinance as yf
import json, sys
sys.stdout.reconfigure(line_buffering=True)
EXCLUDE = {"BAC", "CFG", "FITB", "INCY"}
with open("broad_tickers.txt") as f:
tickers = [l.strip().replace(".", "-") for l in f if l.strip()]
print(f"Screening {len(tickers)} tickers in batches...")
passed = []
batch_size = 20
for batch_start in range(0, len(tickers), batch_size):
batch = tickers[batch_start:batch_start + batch_size]
batch = [t for t in batch if t not in EXCLUDE]
if not batch:
continue
print(f"Batch {batch_start}-{batch_start+len(batch)} / {len(tickers)}...")
try:
data = yf.Tickers(" ".join(batch))
for tick in batch:
try:
info = data.tickers[tick].info or {}
mc = info.get("marketCap", 0) or 0
if mc < 5e9: continue
tpe = info.get("trailingPE")
if not tpe or tpe >= 25 or tpe <= 0: continue
fpe = info.get("forwardPE")
if not fpe or fpe >= 15 or fpe <= 0: continue
rg = info.get("revenueGrowth")
if rg is None or rg < 0.10: continue
eg = info.get("earningsGrowth")
if eg is None or eg < 0.15: continue
roe = info.get("returnOnEquity")
if roe is None or roe < 0.05: continue
peg = info.get("pegRatio")
if peg is not None and peg > 1.2: continue
dte = info.get("debtToEquity")
if dte is not None and dte > 35: continue
entry = {
"ticker": tick,
"name": info.get("longName", tick),
"market_cap_B": round(mc / 1e9, 1),
"trailing_pe": round(tpe, 2),
"forward_pe": round(fpe, 2),
"peg": round(peg, 2) if peg else None,
"revenue_growth_pct": round(rg * 100, 1),
"earnings_growth_pct": round(eg * 100, 1),
"roe_pct": round(roe * 100, 1),
"debt_to_equity": round(dte, 1) if dte else None,
"sector": info.get("sector"),
"industry": info.get("industry"),
}
passed.append(entry)
print(f"{tick}: PE={tpe:.1f} FPE={fpe:.1f} RG={rg*100:.0f}% EG={eg*100:.0f}% ROE={roe*100:.0f}%")
except:
continue
except Exception as e:
print(f" Batch error: {e}")
continue
passed.sort(key=lambda x: x.get("forward_pe", 99))
with open("garp-expanded-scan.json", "w") as f:
json.dump(passed, f, indent=2)
print(f"\nDone! {len(passed)} stocks passed GARP screen")
for s in passed:
print(f" {s['ticker']:6s} ${s['market_cap_B']:6.1f}B PE:{s['trailing_pe']:5.1f} FPE:{s['forward_pe']:5.1f} RG:{s['revenue_growth_pct']:5.1f}% EG:{s['earnings_growth_pct']:5.1f}% ROE:{s['roe_pct']:5.1f}%")

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

@ -0,0 +1,762 @@
{
"BAC": {
"ticker": "BAC",
"name": "Bank of America Corporation",
"price_action": {
"current": 56.53,
"1yr_ago": 46.33,
"1yr_return_pct": 22.02,
"52wk_high": 57.25,
"52wk_low": 33.82,
"pct_from_52wk_high": -1.26,
"50d_ma": 54.25,
"200d_ma": 49.18,
"above_50d_ma": true,
"above_200d_ma": true
},
"insider_transactions": [
{
"date": "2026-01-15 00:00:00",
"insider": "MOYNIHAN BRIAN T",
"transaction": "",
"shares": "17891",
"value": "nan"
},
{
"date": "2025-12-15 00:00:00",
"insider": "MOYNIHAN BRIAN T",
"transaction": "",
"shares": "17892",
"value": "nan"
},
{
"date": "2025-12-11 00:00:00",
"insider": "MOYNIHAN BRIAN T",
"transaction": "",
"shares": "130000",
"value": "0.0"
},
{
"date": "2025-12-09 00:00:00",
"insider": "GREENER GEOFFREY S",
"transaction": "",
"shares": "9265",
"value": "0.0"
},
{
"date": "2025-12-03 00:00:00",
"insider": "SCRIVENER THOMAS M",
"transaction": "",
"shares": "3000",
"value": "0.0"
},
{
"date": "2025-11-28 00:00:00",
"insider": "OKPARA JOHNBULL",
"transaction": "",
"shares": "26784",
"value": "nan"
},
{
"date": "2025-11-14 00:00:00",
"insider": "MOYNIHAN BRIAN T",
"transaction": "",
"shares": "17891",
"value": "nan"
},
{
"date": "2025-11-14 00:00:00",
"insider": "SCHIMPF ERIC A",
"transaction": "",
"shares": "1234",
"value": "nan"
},
{
"date": "2025-11-14 00:00:00",
"insider": "HANS LINDSAY D",
"transaction": "",
"shares": "975",
"value": "nan"
},
{
"date": "2025-11-14 00:00:00",
"insider": "GOPALKRISHNAN HARI",
"transaction": "",
"shares": "2703",
"value": "nan"
}
],
"top_institutional_holders": [
{
"holder": "Vanguard Group Inc",
"shares": 651076825,
"pct_held": "0.0892"
},
{
"holder": "Berkshire Hathaway, Inc",
"shares": 568070012,
"pct_held": "0.077800006"
},
{
"holder": "Blackrock Inc.",
"shares": 535326028,
"pct_held": "0.0733"
},
{
"holder": "JPMORGAN CHASE & CO",
"shares": 363341282,
"pct_held": "0.0498"
},
{
"holder": "State Street Corporation",
"shares": 301312466,
"pct_held": "0.041300002"
}
],
"institutional_pct": 0.71739,
"earnings": [
{
"date": "2026-04-15 09:00:00-04:00",
"eps_estimate": 0.99,
"eps_actual": null,
"surprise_pct": null
},
{
"date": "2026-01-14 06:00:00-05:00",
"eps_estimate": 0.96,
"eps_actual": 0.98,
"surprise_pct": 2.23
},
{
"date": "2025-10-15 06:00:00-04:00",
"eps_estimate": 0.95,
"eps_actual": 1.06,
"surprise_pct": 11.43
},
{
"date": "2025-07-16 06:00:00-04:00",
"eps_estimate": 0.86,
"eps_actual": 0.89,
"surprise_pct": 3.61
},
{
"date": "2025-04-15 06:00:00-04:00",
"eps_estimate": 0.82,
"eps_actual": 0.9,
"surprise_pct": 10.29
},
{
"date": "2025-01-16 06:00:00-05:00",
"eps_estimate": 0.77,
"eps_actual": 0.82,
"surprise_pct": 6.84
},
{
"date": "2024-10-15 06:00:00-04:00",
"eps_estimate": 0.76,
"eps_actual": 0.81,
"surprise_pct": 6.34
},
{
"date": "2024-07-16 06:00:00-04:00",
"eps_estimate": 0.8,
"eps_actual": 0.83,
"surprise_pct": 3.58
}
],
"analyst": {
"target_mean": 62.20833,
"target_low": 56.0,
"target_high": 71.0,
"recommendation": "buy",
"num_analysts": 24,
"upside_pct": 10.04
},
"fundamentals": {
"trailing_pe": 14.83727,
"forward_pe": 11.407365,
"peg_ratio": null,
"market_cap": 412810084352,
"revenue_growth": 0.132,
"earnings_growth": 0.209,
"roe": 0.1019,
"debt_to_equity": null,
"dividend_yield": 1.95,
"beta": 1.273
},
"technical": {
"rsi_14": 71.14,
"trend": "bullish"
}
},
"CFG": {
"ticker": "CFG",
"name": "Citizens Financial Group, Inc.",
"price_action": {
"current": 68.12,
"1yr_ago": 46.24,
"1yr_return_pct": 47.32,
"52wk_high": 68.12,
"52wk_low": 33.06,
"pct_from_52wk_high": 0.0,
"50d_ma": 59.58,
"200d_ma": 49.69,
"above_50d_ma": true,
"above_200d_ma": true
},
"insider_transactions": [
{
"date": "2025-12-11 00:00:00",
"insider": "VAN SAUN BRUCE W",
"transaction": "",
"shares": "8000",
"value": "458558"
},
{
"date": "2025-12-03 00:00:00",
"insider": "LAMONICA SUSAN",
"transaction": "",
"shares": "20128",
"value": "1120727"
},
{
"date": "2025-11-12 00:00:00",
"insider": "KELLY EDWARD J. III",
"transaction": "",
"shares": "316",
"value": "0"
},
{
"date": "2025-11-12 00:00:00",
"insider": "HANKOWSKY WILLIAM P",
"transaction": "",
"shares": "347",
"value": "0"
},
{
"date": "2025-11-12 00:00:00",
"insider": "ZURAITIS MARITA",
"transaction": "",
"shares": "347",
"value": "0"
},
{
"date": "2025-11-12 00:00:00",
"insider": "CUMMING CHRISTINE M",
"transaction": "",
"shares": "347",
"value": "0"
},
{
"date": "2025-11-12 00:00:00",
"insider": "ATKINSON TRACY A",
"transaction": "",
"shares": "85",
"value": "0"
},
{
"date": "2025-11-12 00:00:00",
"insider": "CUMMINGS KEVIN",
"transaction": "",
"shares": "162",
"value": "0"
},
{
"date": "2025-11-12 00:00:00",
"insider": "SWIFT CHRISTOPHER J",
"transaction": "",
"shares": "200",
"value": "0"
},
{
"date": "2025-11-12 00:00:00",
"insider": "SIEKERKA MICHELE N",
"transaction": "",
"shares": "162",
"value": "0"
}
],
"top_institutional_holders": [
{
"holder": "Vanguard Group Inc",
"shares": 51303226,
"pct_held": "0.1195"
},
{
"holder": "Blackrock Inc.",
"shares": 43851199,
"pct_held": "0.1021"
},
{
"holder": "Capital World Investors",
"shares": 37289711,
"pct_held": "0.0868"
},
{
"holder": "Invesco Ltd.",
"shares": 24064513,
"pct_held": "0.055999998"
},
{
"holder": "State Street Corporation",
"shares": 22969833,
"pct_held": "0.0535"
}
],
"institutional_pct": 0.99170995,
"earnings": [
{
"date": "2026-04-16 09:00:00-04:00",
"eps_estimate": 1.09,
"eps_actual": null,
"surprise_pct": null
},
{
"date": "2026-01-21 06:00:00-05:00",
"eps_estimate": 1.11,
"eps_actual": 1.13,
"surprise_pct": 2.24
},
{
"date": "2025-10-15 06:00:00-04:00",
"eps_estimate": 1.03,
"eps_actual": 1.05,
"surprise_pct": 2.27
},
{
"date": "2025-07-17 06:00:00-04:00",
"eps_estimate": 0.88,
"eps_actual": 0.92,
"surprise_pct": 4.13
},
{
"date": "2025-04-16 06:00:00-04:00",
"eps_estimate": 0.75,
"eps_actual": 0.77,
"surprise_pct": 2.68
},
{
"date": "2025-01-17 06:00:00-05:00",
"eps_estimate": 0.83,
"eps_actual": 0.85,
"surprise_pct": 2.3
},
{
"date": "2024-10-16 06:00:00-04:00",
"eps_estimate": 0.79,
"eps_actual": 0.79,
"surprise_pct": -0.24
},
{
"date": "2024-07-17 03:00:00-04:00",
"eps_estimate": 0.79,
"eps_actual": 0.82,
"surprise_pct": 3.5
}
],
"analyst": {
"target_mean": 72.275,
"target_low": 62.5,
"target_high": 80.0,
"recommendation": "buy",
"num_analysts": 20,
"upside_pct": 6.1
},
"fundamentals": {
"trailing_pe": 17.647669,
"forward_pe": 10.848602,
"peg_ratio": null,
"market_cap": 29256599552,
"revenue_growth": 0.107,
"earnings_growth": 0.359,
"roe": 0.07241,
"debt_to_equity": null,
"dividend_yield": 2.7,
"beta": 1.071
},
"technical": {
"rsi_14": 75.46,
"trend": "bullish"
}
},
"FITB": {
"ticker": "FITB",
"name": "Fifth Third Bancorp",
"price_action": {
"current": 55.08,
"1yr_ago": 42.49,
"1yr_return_pct": 29.63,
"52wk_high": 55.08,
"52wk_low": 32.46,
"pct_from_52wk_high": 0.0,
"50d_ma": 48.27,
"200d_ma": 42.82,
"above_50d_ma": true,
"above_200d_ma": true
},
"insider_transactions": [
{
"date": "2026-02-02 00:00:00",
"insider": "SMITH BARBARA",
"transaction": "",
"shares": "40498",
"value": "0"
},
{
"date": "2026-02-02 00:00:00",
"insider": "SEFZIK PETER L",
"transaction": "",
"shares": "209382",
"value": "0"
},
{
"date": "2026-02-02 00:00:00",
"insider": "KERR DEREK J",
"transaction": "",
"shares": "14189",
"value": "0"
},
{
"date": "2026-02-02 00:00:00",
"insider": "VAN DE VEN MICHAEL G",
"transaction": "",
"shares": "47972",
"value": "0"
},
{
"date": "2026-01-07 00:00:00",
"insider": "ALMODOVAR PRISCILLA",
"transaction": "",
"shares": "848",
"value": "0"
},
{
"date": "2025-12-15 00:00:00",
"insider": "PRESTON BRYAN D",
"transaction": "",
"shares": "1100",
"value": "0"
},
{
"date": "2025-12-11 00:00:00",
"insider": "SCHRAMM JUDE",
"transaction": "",
"shares": "2250",
"value": "109125"
},
{
"date": "2025-10-20 00:00:00",
"insider": "BAYH B EVAN III",
"transaction": "",
"shares": "3000",
"value": "123650"
},
{
"date": "2025-10-09 00:00:00",
"insider": "GONZALEZ CHRISTIAN",
"transaction": "",
"shares": "5709",
"value": "0"
},
{
"date": "2025-08-28 00:00:00",
"insider": "SHAFFER ROBERT P",
"transaction": "",
"shares": "14035",
"value": "372208"
}
],
"top_institutional_holders": [
{
"holder": "Vanguard Group Inc",
"shares": 107237083,
"pct_held": "0.16219999"
},
{
"holder": "Blackrock Inc.",
"shares": 88421716,
"pct_held": "0.1338"
},
{
"holder": "JPMORGAN CHASE & CO",
"shares": 65878286,
"pct_held": "0.099700004"
},
{
"holder": "State Street Corporation",
"shares": 39653081,
"pct_held": "0.06"
},
{
"holder": "Charles Schwab Investment Management, Inc.",
"shares": 32138466,
"pct_held": "0.048600003"
}
],
"institutional_pct": 0.66008,
"earnings": [
{
"date": "2026-04-17 09:00:00-04:00",
"eps_estimate": 0.43,
"eps_actual": null,
"surprise_pct": null
},
{
"date": "2026-01-20 06:00:00-05:00",
"eps_estimate": 0.99,
"eps_actual": 1.04,
"surprise_pct": 4.93
},
{
"date": "2025-10-17 06:00:00-04:00",
"eps_estimate": 0.86,
"eps_actual": 0.91,
"surprise_pct": 5.47
},
{
"date": "2025-07-17 06:00:00-04:00",
"eps_estimate": 0.87,
"eps_actual": 0.9,
"surprise_pct": 3.85
},
{
"date": "2025-04-17 06:00:00-04:00",
"eps_estimate": 0.7,
"eps_actual": 0.71,
"surprise_pct": 1.42
},
{
"date": "2025-01-21 06:00:00-05:00",
"eps_estimate": 0.87,
"eps_actual": 0.9,
"surprise_pct": 3.01
},
{
"date": "2024-10-18 06:00:00-04:00",
"eps_estimate": 0.82,
"eps_actual": 0.85,
"surprise_pct": 3.17
},
{
"date": "2024-07-19 06:00:00-04:00",
"eps_estimate": 0.84,
"eps_actual": 0.86,
"surprise_pct": 1.79
}
],
"analyst": {
"target_mean": 57.15789,
"target_low": 49.0,
"target_high": 61.0,
"recommendation": "buy",
"num_analysts": 19,
"upside_pct": 3.77
},
"fundamentals": {
"trailing_pe": 15.6034,
"forward_pe": 11.235359,
"peg_ratio": null,
"market_cap": 49574670336,
"revenue_growth": 0.115,
"earnings_growth": 0.208,
"roe": 0.121929996,
"debt_to_equity": null,
"dividend_yield": 2.9,
"beta": 0.977
},
"technical": {
"rsi_14": 71.83,
"trend": "bullish"
}
},
"INCY": {
"ticker": "INCY",
"name": "Incyte Corporation",
"price_action": {
"current": 108.39,
"1yr_ago": 74.13,
"1yr_return_pct": 46.22,
"52wk_high": 110.57,
"52wk_low": 55.17,
"pct_from_52wk_high": -1.97,
"50d_ma": 101.9,
"200d_ma": 84.49,
"above_50d_ma": true,
"above_200d_ma": true
},
"insider_transactions": [
{
"date": "2026-01-16 00:00:00",
"insider": "MORRISSEY MICHAEL JAMES",
"transaction": "",
"shares": "7426",
"value": "0"
},
{
"date": "2026-01-16 00:00:00",
"insider": "HEESON LEE",
"transaction": "",
"shares": "8911",
"value": "0"
},
{
"date": "2026-01-07 00:00:00",
"insider": "ISSA MOHAMED KHAIRIE",
"transaction": "",
"shares": "10856",
"value": "1184064"
},
{
"date": "2026-01-05 00:00:00",
"insider": "STEIN STEVEN H.",
"transaction": "",
"shares": "15634",
"value": "1589978"
},
{
"date": "2025-12-31 00:00:00",
"insider": "BAKER BROS ADVISORS L.P.",
"transaction": "",
"shares": "656",
"value": "0"
},
{
"date": "2025-12-31 00:00:00",
"insider": "HARRIGAN EDMUND P. M.D.",
"transaction": "",
"shares": "245",
"value": "24199"
},
{
"date": "2025-12-31 00:00:00",
"insider": "CLANCY PAUL J.",
"transaction": "",
"shares": "241",
"value": "23804"
},
{
"date": "2025-12-19 00:00:00",
"insider": "TRAY THOMAS R",
"transaction": "",
"shares": "3374",
"value": "336350"
},
{
"date": "2025-12-19 00:00:00",
"insider": "TRAY THOMAS R",
"transaction": "",
"shares": "2774",
"value": "265638"
},
{
"date": "2025-12-17 00:00:00",
"insider": "MORRISSEY MICHAEL JAMES",
"transaction": "",
"shares": "58331",
"value": "5675002"
}
],
"top_institutional_holders": [
{
"holder": "Baker Bros. Advisors, LP",
"shares": 30743663,
"pct_held": "0.1566"
},
{
"holder": "Vanguard Group Inc",
"shares": 19911434,
"pct_held": "0.1014"
},
{
"holder": "Blackrock Inc.",
"shares": 17894297,
"pct_held": "0.0911"
},
{
"holder": "Dodge & Cox Inc.",
"shares": 13932416,
"pct_held": "0.071"
},
{
"holder": "State Street Corporation",
"shares": 9676796,
"pct_held": "0.0493"
}
],
"institutional_pct": 1.05931,
"earnings": [
{
"date": "2026-02-10 08:00:00-05:00",
"eps_estimate": 1.95,
"eps_actual": null,
"surprise_pct": null
},
{
"date": "2025-10-28 07:00:00-04:00",
"eps_estimate": 1.64,
"eps_actual": 2.26,
"surprise_pct": 38.04
},
{
"date": "2025-07-29 07:00:00-04:00",
"eps_estimate": 1.47,
"eps_actual": 1.57,
"surprise_pct": 6.59
},
{
"date": "2025-04-29 07:00:00-04:00",
"eps_estimate": 1.03,
"eps_actual": 1.16,
"surprise_pct": 12.32
},
{
"date": "2025-02-10 07:00:00-05:00",
"eps_estimate": 1.55,
"eps_actual": 1.43,
"surprise_pct": -8.01
},
{
"date": "2024-10-29 07:00:00-04:00",
"eps_estimate": 1.09,
"eps_actual": 1.07,
"surprise_pct": -2.03
},
{
"date": "2024-07-30 07:00:00-04:00",
"eps_estimate": 1.11,
"eps_actual": -1.82,
"surprise_pct": -264.53
},
{
"date": "2024-04-30 07:00:00-04:00",
"eps_estimate": 0.83,
"eps_actual": 0.64,
"surprise_pct": -23.09
}
],
"analyst": {
"target_mean": 104.22727,
"target_low": 70.0,
"target_high": 135.0,
"recommendation": "buy",
"num_analysts": 22,
"upside_pct": -3.84
},
"fundamentals": {
"trailing_pe": 18.371185,
"forward_pe": 13.606971,
"peg_ratio": null,
"market_cap": 21279418368,
"revenue_growth": 0.2,
"earnings_growth": 2.907,
"roe": 0.30389,
"debt_to_equity": 0.887,
"dividend_yield": null,
"beta": 0.847
},
"technical": {
"rsi_14": 54.22,
"trend": "bullish"
}
}
}

View File

@ -0,0 +1,71 @@
{
"date": "2026-02-08",
"filters": "GARP",
"results": [
{
"ticker": "BAC",
"name": "Bank of America Corporation",
"mcap_b": 412.8,
"rev_growth": 13.2,
"trail_pe": 14.8,
"fwd_pe": 11.4,
"peg": "N/A",
"eps_growth": 20.9,
"roe": 10.2,
"quick": "N/A",
"de": "N/A"
},
{
"ticker": "CFG",
"name": "Citizens Financial Group, Inc.",
"mcap_b": 29.3,
"rev_growth": 10.7,
"trail_pe": 17.6,
"fwd_pe": 10.8,
"peg": "N/A",
"eps_growth": 35.9,
"roe": 7.2,
"quick": "N/A",
"de": "N/A"
},
{
"ticker": "FITB",
"name": "Fifth Third Bancorp",
"mcap_b": 49.6,
"rev_growth": 11.5,
"trail_pe": 15.6,
"fwd_pe": 11.2,
"peg": "N/A",
"eps_growth": 20.8,
"roe": 12.2,
"quick": "N/A",
"de": "N/A"
},
{
"ticker": "INCY",
"name": "Incyte Corporation",
"mcap_b": 21.3,
"rev_growth": 20.0,
"trail_pe": 18.4,
"fwd_pe": 13.6,
"peg": "N/A",
"eps_growth": 290.7,
"roe": 30.4,
"quick": 2.86,
"de": 0.9
}
],
"stats": {
"scanned": 503,
"revenue": 316,
"pe": 121,
"fwd_pe": 32,
"peg": 0,
"eps": 14,
"roe": 1,
"quick": 11,
"de": 4,
"mcap": 0,
"err": 0
}
}

295
data/stock_deep_dive.py Normal file
View File

@ -0,0 +1,295 @@
#!/usr/bin/env python3
"""Deep dive analysis on BAC, CFG, FITB, INCY + expanded GARP scan."""
import yfinance as yf
import json
import datetime
import numpy as np
from pathlib import Path
TARGETS = ["BAC", "CFG", "FITB", "INCY"]
def safe_get(d, *keys, default=None):
for k in keys:
if isinstance(d, dict):
d = d.get(k, default)
else:
return default
return d
def analyze_stock(ticker_str):
print(f"\n{'='*60}\nAnalyzing {ticker_str}...")
t = yf.Ticker(ticker_str)
info = t.info or {}
result = {"ticker": ticker_str, "name": info.get("longName", ticker_str)}
# 1. Price action (1yr)
hist = t.history(period="1y")
if not hist.empty:
prices = hist["Close"]
result["price_action"] = {
"current": round(float(prices.iloc[-1]), 2),
"1yr_ago": round(float(prices.iloc[0]), 2),
"1yr_return_pct": round(float((prices.iloc[-1] / prices.iloc[0] - 1) * 100), 2),
"52wk_high": round(float(prices.max()), 2),
"52wk_low": round(float(prices.min()), 2),
"pct_from_52wk_high": round(float((prices.iloc[-1] / prices.max() - 1) * 100), 2),
"50d_ma": round(float(prices.tail(50).mean()), 2),
"200d_ma": round(float(prices.tail(200).mean()), 2) if len(prices) >= 200 else None,
}
cur = float(prices.iloc[-1])
ma50 = result["price_action"]["50d_ma"]
ma200 = result["price_action"]["200d_ma"]
result["price_action"]["above_50d_ma"] = cur > ma50
result["price_action"]["above_200d_ma"] = cur > ma200 if ma200 else None
# 2. Insider activity
try:
insiders = t.insider_transactions
if insiders is not None and not insiders.empty:
recent = insiders.head(10)
txns = []
for _, row in recent.iterrows():
txns.append({
"date": str(row.get("Start Date", "")),
"insider": str(row.get("Insider", "")),
"transaction": str(row.get("Transaction", "")),
"shares": str(row.get("Shares", "")),
"value": str(row.get("Value", "")),
})
result["insider_transactions"] = txns
else:
result["insider_transactions"] = []
except:
result["insider_transactions"] = []
# 3. Institutional ownership
try:
inst = t.institutional_holders
if inst is not None and not inst.empty:
top5 = []
for _, row in inst.head(5).iterrows():
top5.append({
"holder": str(row.get("Holder", "")),
"shares": int(row.get("Shares", 0)) if row.get("Shares") else 0,
"pct_held": str(row.get("pctHeld", "")),
})
result["top_institutional_holders"] = top5
result["institutional_pct"] = info.get("heldPercentInstitutions")
except:
result["top_institutional_holders"] = []
# 4. Earnings surprises
try:
earn = t.earnings_dates
if earn is not None and not earn.empty:
surprises = []
for idx, row in earn.head(8).iterrows():
s = {
"date": str(idx),
"eps_estimate": row.get("EPS Estimate"),
"eps_actual": row.get("Reported EPS"),
"surprise_pct": row.get("Surprise(%)"),
}
# clean NaN
s = {k: (None if isinstance(v, float) and np.isnan(v) else v) for k, v in s.items()}
surprises.append(s)
result["earnings"] = surprises
else:
result["earnings"] = []
except:
result["earnings"] = []
# 5. Analyst consensus
result["analyst"] = {
"target_mean": info.get("targetMeanPrice"),
"target_low": info.get("targetLowPrice"),
"target_high": info.get("targetHighPrice"),
"recommendation": info.get("recommendationKey"),
"num_analysts": info.get("numberOfAnalystOpinions"),
}
if result["price_action"].get("current") and info.get("targetMeanPrice"):
upside = (info["targetMeanPrice"] / result["price_action"]["current"] - 1) * 100
result["analyst"]["upside_pct"] = round(upside, 2)
# 6. Key fundamentals snapshot
result["fundamentals"] = {
"trailing_pe": info.get("trailingPE"),
"forward_pe": info.get("forwardPE"),
"peg_ratio": info.get("pegRatio"),
"market_cap": info.get("marketCap"),
"revenue_growth": info.get("revenueGrowth"),
"earnings_growth": info.get("earningsGrowth"),
"roe": info.get("returnOnEquity"),
"debt_to_equity": info.get("debtToEquity"),
"dividend_yield": info.get("dividendYield"),
"beta": info.get("beta"),
}
# 7. Technical - RSI approximation
if not hist.empty and len(hist) >= 14:
delta = hist["Close"].diff()
gain = delta.where(delta > 0, 0).rolling(14).mean()
loss = (-delta.where(delta < 0, 0)).rolling(14).mean()
rs = gain / loss
rsi = 100 - (100 / (1 + rs))
result["technical"] = {
"rsi_14": round(float(rsi.iloc[-1]), 2) if not np.isnan(rsi.iloc[-1]) else None,
"trend": "bullish" if cur > ma50 and (ma200 is None or cur > ma200) else "bearish" if cur < ma50 and (ma200 and cur < ma200) else "mixed",
}
print(f" Done: {result['name']} @ ${result['price_action']['current']}")
return result
# ============================================================
# EXPANDED GARP SCAN
# ============================================================
def get_broad_ticker_list():
"""Get a broad list of US tickers to screen."""
import urllib.request
# Use Wikipedia's S&P 500 + S&P 400 midcap for broader coverage
# Plus some known Russell 1000 members not in S&P 500
# Start with S&P 500
sp500 = []
try:
import pandas as pd
tables = pd.read_html("https://en.wikipedia.org/wiki/List_of_S%26P_500_companies")
sp500 = tables[0]["Symbol"].str.replace(".", "-", regex=False).tolist()
except:
pass
# S&P 400 midcap
sp400 = []
try:
import pandas as pd
tables = pd.read_html("https://en.wikipedia.org/wiki/List_of_S%26P_400_companies")
sp400 = tables[0]["Symbol"].str.replace(".", "-", regex=False).tolist()
except:
pass
all_tickers = list(set(sp500 + sp400))
print(f"Total tickers to screen: {len(all_tickers)}")
return all_tickers
def garp_screen(tickers, exclude=None):
"""Apply GARP filters to ticker list."""
exclude = set(exclude or [])
passed = []
for i, tick in enumerate(tickers):
if tick in exclude:
continue
if i % 50 == 0:
print(f" Screening {i}/{len(tickers)}...")
try:
t = yf.Ticker(tick)
info = t.info or {}
mc = info.get("marketCap", 0) or 0
if mc < 5e9:
continue
tpe = info.get("trailingPE")
if tpe is None or tpe >= 25 or tpe <= 0:
continue
fpe = info.get("forwardPE")
if fpe is None or fpe >= 15 or fpe <= 0:
continue
rg = info.get("revenueGrowth")
if rg is None or rg < 0.10:
continue
eg = info.get("earningsGrowth")
if eg is None or eg < 0.15:
continue
roe = info.get("returnOnEquity")
if roe is None or roe < 0.05:
continue
# Optional filters
peg = info.get("pegRatio")
if peg is not None and peg > 1.2:
continue
dte = info.get("debtToEquity")
if dte is not None and dte > 35:
continue
passed.append({
"ticker": tick,
"name": info.get("longName", tick),
"market_cap": mc,
"trailing_pe": round(tpe, 2),
"forward_pe": round(fpe, 2),
"peg": round(peg, 2) if peg else None,
"revenue_growth": round(rg * 100, 1),
"earnings_growth": round(eg * 100, 1),
"roe": round(roe * 100, 1),
"debt_to_equity": round(dte, 1) if dte else None,
})
print(f" ✅ PASS: {tick} (PE:{tpe:.1f} FPE:{fpe:.1f} RG:{rg*100:.0f}% EG:{eg*100:.0f}%)")
except Exception as e:
continue
return passed
# ============================================================
# MAIN
# ============================================================
if __name__ == "__main__":
output_dir = Path("/home/wdjones/.openclaw/workspace/data")
output_dir.mkdir(exist_ok=True)
# Deep dive on 4 stocks
print("=" * 60)
print("DEEP DIVE ANALYSIS")
print("=" * 60)
analyses = {}
for tick in TARGETS:
analyses[tick] = analyze_stock(tick)
# Save deep dive
with open(output_dir / "stock-analysis-deep-dive.json", "w") as f:
json.dump(analyses, f, indent=2, default=str)
print(f"\nSaved deep dive to stock-analysis-deep-dive.json")
# Expanded GARP scan
print("\n" + "=" * 60)
print("EXPANDED GARP SCAN (S&P 500 + S&P 400 MidCap)")
print("=" * 60)
tickers = get_broad_ticker_list()
new_passes = garp_screen(tickers, exclude=set(TARGETS))
with open(output_dir / "garp-expanded-scan.json", "w") as f:
json.dump(new_passes, f, indent=2, default=str)
print(f"\nExpanded scan found {len(new_passes)} additional stocks")
print("Saved to garp-expanded-scan.json")
# Print summary
print("\n" + "=" * 60)
print("SUMMARY")
print("=" * 60)
for tick, data in analyses.items():
pa = data.get("price_action", {})
an = data.get("analyst", {})
fu = data.get("fundamentals", {})
te = data.get("technical", {})
print(f"\n{tick} - {data['name']}")
print(f" Price: ${pa.get('current')} | 1yr Return: {pa.get('1yr_return_pct')}%")
print(f" From 52wk High: {pa.get('pct_from_52wk_high')}%")
print(f" PE: {fu.get('trailing_pe')} | Fwd PE: {fu.get('forward_pe')} | PEG: {fu.get('peg_ratio')}")
print(f" Target: ${an.get('target_mean')} ({an.get('upside_pct')}% upside) | Rec: {an.get('recommendation')}")
print(f" RSI: {te.get('rsi_14')} | Trend: {te.get('trend')}")
print(f" Insiders: {len(data.get('insider_transactions', []))} recent txns")
if new_passes:
print(f"\nNew GARP Candidates ({len(new_passes)}):")
for s in sorted(new_passes, key=lambda x: x.get("forward_pe", 99)):
print(f" {s['ticker']:6s} PE:{s['trailing_pe']:5.1f} FPE:{s['forward_pe']:5.1f} RG:{s['revenue_growth']:5.1f}% EG:{s['earnings_growth']:5.1f}% ROE:{s['roe']:5.1f}%")

View File

@ -37,7 +37,37 @@
- All Seahawks Super Bowl bets, game tonight ~5:30pm CST
- Will auto-resolve and report via Telegram
### Selling Stuff Project
- D J wants to sell a bunch of stuff, needs accounts on selling platforms
- **Craigslist**: ✅ Account created — case-lgn@protonmail.com, passwordless, Nashville area
- **eBay, Mercari, OfferUp**: Blocked by CAPTCHA — need D J to register manually
- Google Voice number: +1 (615) 933-1968 — D J gave this for service registrations
- ProtonMail is already logged in on desktop Chrome on the VM
- Browser automation via CDP on desktop Chrome (port 9222) works for sites without CAPTCHA
- Craigslist has no CAPTCHA — signed up fully automated
### Air Purifier Research
- D J looking at Shark NeverChange Air Purifier MAX for 4 cats
- Washable HEPA filter = no replacement costs, good for heavy pet hair
### iPhone Action Button
- D J has iPhone 16, tried to set up voice-to-Telegram shortcut
- Abandoned — Telegram doesn't have native Shortcuts integration, workarounds were clunky
### Recycling
- Nashville cardboard recycling drop-offs: 24/7 unstaffed bins are always full
- Best bet: staffed Convenience Centers (Tue-Sat 8:30am-4:30pm)
- Winter storm notice: all centers free this week
## Key Decisions
- Systemd timers > AI cron jobs for mechanical monitoring (zero token cost)
- Telegram bot API for direct alerts bypasses AI entirely
- Admin panel todos = how I request human action items
- Desktop Chrome via CDP (port 9222) for browser automation that needs real fingerprint
- Craigslist for local selling (no CAPTCHA), FB Marketplace needs D J's account
## Lessons Learned
- Most selling platforms (eBay, Mercari, OfferUp) have invisible reCAPTCHA on signup — can't automate
- React forms need native input setter (Object.getOwnPropertyDescriptor hack) or Input.insertText via CDP
- Best Buy blocks all web scraping
- Proton Mail stays logged in on desktop Chrome — can use for email verification flows

109
memory/2026-02-09.md Normal file
View File

@ -0,0 +1,109 @@
# 2026-02-09 — Monday
## X Feed Analysis Day
Major session analyzing Polymarket/crypto tweets D J forwarded from X feed.
### Tweets Investigated
1. **@browomo weather edge** — Pilot METAR data for Polymarket weather bets
- Wallet `0x594edB9112f...`: Claimed +$27K, actual **-$13,103** (387 losses, 51 wins)
- Verdict: SCAM/engagement bait
2. **@ArchiveExplorer planktonXD** — "Buy everything under 5 cents"
- Wallet `0x4ffe49ba2a4c...`: Claimed +$104K, actual **-$9,517** (3090 losses, 1368 wins, 37% win rate)
- Verdict: SCAM/engagement bait
3. **@krajekis BTC 15-min LONG** — "+700% monthly, 1 trade/day at 9AM"
- Backtested 25 days: 52% win rate (coin flip), strategy loses 76% of capital
- Verdict: FABRICATED results
4. **@noisyb0y1 15-min arb** — "$99K in a day"
- Wallet `0xE594336603F4...`: Real strategy (buying both sides), actual PnL ~$9K not $99K
- Combined costs $0.70-$0.95 on some markets = genuine arb edge
- Verdict: INFLATED numbers, but strategy has merit → bookmarked for scanner
5. **5 more wallets** — spawned sub-agent to research @IH2P, Super Bowl trader, NegRisk arb, Elon insider, $270→$244K bot
### Pattern Identified
- Fintwit Polymarket accounts follow identical template: big $ claim → wallet → product shill
- Products being shilled: Clawdbots, Moltbook, Bullpen (affiliate/paid promos)
- Real money is in selling engagement, not trading
- Wallets are cherry-picked; PnL claims conflate position value with profit
### Built Today
- **Crypto signals pipeline** (`projects/crypto-signals/`)
- Signal parser for Telegram JSON exports
- 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-08T17:06:58.270395+00:00",
"last_check": "2026-02-12T06:14:23.952343+00:00",
"total_tracked": 3100,
"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

@ -11,9 +11,9 @@
"quantity": 845,
"stop_loss": null,
"take_profit": null,
"current_price": 0.505,
"unrealized_pnl": -1.52,
"unrealized_pnl_pct": -0.36,
"current_price": 0.9995,
"unrealized_pnl": 416.33,
"unrealized_pnl_pct": 97.22,
"source_post": "https://polymarket.com/profile/kch123",
"thesis": "Copy kch123 proportional. Spread: Seahawks (-4.5) (Seahawks). Weight: 42.9%",
"notes": "kch123 has $975,120 on this (42.9% of active book)",
@ -101,9 +101,9 @@
"quantity": 186,
"stop_loss": null,
"take_profit": null,
"current_price": 0.475,
"unrealized_pnl": -0.93,
"unrealized_pnl_pct": -1.04,
"current_price": 1,
"unrealized_pnl": 96.72,
"unrealized_pnl_pct": 108.33,
"source_post": "https://polymarket.com/profile/kch123",
"thesis": "Copy kch123 proportional. Spread: Seahawks (-5.5) (Seahawks). Weight: 9.0%",
"notes": "kch123 has $203,779 on this (9.0% of active book)",

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

@ -0,0 +1,86 @@
# Market Watch - Multiplayer GARP Paper Trading Simulator
Multiplayer paper trading simulator implementing a **Growth at a Reasonable Price (GARP)** strategy. Compete against Case (AI) and other players.
## How It Works
- **Create or join a game** with configurable starting cash
- **Trade manually** via the web portal or Telegram
- **Case (AI)** trades autonomously using the GARP strategy
- **Leaderboard** tracks who's winning by % return
## GARP Filter Criteria
| Metric | Threshold |
|--------|-----------|
| Revenue Growth | ≥ 10% |
| Trailing P/E | < 25 |
| Forward P/E | < 15 |
| PEG Ratio | < 1.2 (if available) |
| EPS Growth | > 15% |
| ROE | > 5% |
| Quick Ratio | > 1.5 (if available) |
| Debt/Equity | < 35% (if available) |
| Market Cap | > $5B |
### Case's Trading Rules
- **Buy:** GARP filter pass + RSI < 70 + not within 2% of 52wk high + max 10% per position + max 15 positions
- **Sell:** Fails GARP rescan OR 10% trailing stop-loss OR RSI > 80
- **Universe:** S&P 500 + S&P 400 MidCap (~900 stocks)
## Architecture
| File | Description |
|------|-------------|
| `game_engine.py` | Multiplayer game/player/portfolio engine |
| `scanner.py` | GARP scanner across S&P 500 + 400 |
| `trader.py` | Case's autonomous trading logic |
| `run_daily.py` | Daily orchestrator (scan → trade → snapshot → alert) |
| `portfolio.py` | Backward-compatible wrapper for single-player |
| `portal/server.py` | Web dashboard with multiplayer UI |
### Data Structure
```
data/games/{game_id}/
├── game.json # Game config, players, rules
└── players/{username}/
├── portfolio.json # Current positions & cash
├── trades.json # Trade history
└── snapshots.json # Daily value snapshots
```
## Web Portal
**URL:** http://marketwatch.local (or http://localhost:8889)
- **Home:** List of games, create new game
- **Game page:** Leaderboard, join game
- **Player page:** Portfolio, trade form, performance chart, trade history
### API Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/games` | Create game (form: name, starting_cash, end_date) |
| POST | `/api/games/{id}/join` | Join game (form: username) |
| POST | `/api/games/{id}/players/{user}/trade` | Trade (form: action, ticker, shares) |
| GET | `/api/games/{id}/leaderboard` | Get leaderboard JSON |
| GET | `/api/games/{id}/players/{user}/portfolio` | Get portfolio JSON |
## Systemd Services
| Service | Schedule |
|---------|----------|
| `market-watch.timer` | Mon-Fri 9:00 AM + 3:30 PM CST |
| `market-watch-portal.service` | Always running (port 8889) |
```bash
systemctl --user status market-watch.timer
systemctl --user status market-watch-portal
journalctl --user -u market-watch -f
```
## Telegram
Players can trade via: `buy AAPL 10` or `sell BAC 50`
Daily summaries with leaderboard sent automatically.

View File

@ -0,0 +1,13 @@
{
"game_id": "7ebf65c7",
"name": "GARP Challenge",
"starting_cash": 100000.0,
"start_date": "2026-02-08",
"end_date": null,
"creator": "case",
"created_at": "2026-02-08T15:15:43.402301",
"players": [
"case"
],
"status": "active"
}

View File

@ -0,0 +1,125 @@
{
"cash": 870.6802087402511,
"positions": {
"ALLY": {
"shares": 156,
"avg_cost": 42.65,
"current_price": 42.38999938964844,
"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",
"trailing_stop": 38.50199890136719
},
"JHG": {
"shares": 138,
"avg_cost": 48.21,
"current_price": 48.2400016784668,
"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",
"trailing_stop": 43.46999931335449
},
"INCY": {
"shares": 61,
"avg_cost": 108.69,
"current_price": 100.05000305175781,
"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",
"trailing_stop": 98.12699890136719
},
"PINS": {
"shares": 332,
"avg_cost": 20.06,
"current_price": 20.329999923706055,
"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",
"trailing_stop": 18.59850082397461
},
"EXEL": {
"shares": 152,
"avg_cost": 43.8,
"current_price": 42.97999954223633,
"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",
"trailing_stop": 39.573001098632815
},
"CART": {
"shares": 187,
"avg_cost": 35.49,
"current_price": 34.619998931884766,
"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",
"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

@ -0,0 +1,18 @@
[
{
"date": "2026-02-09",
"total_value": 100055.9,
"total_pnl": 55.9,
"pnl_pct": 0.06,
"cash": 60255.3,
"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

@ -0,0 +1,157 @@
[
{
"action": "BUY",
"ticker": "DUOL",
"shares": 57,
"price": 116.35,
"cost": 6631.95,
"reason": "GARP signal: PE=14.65, FwdPE=14.71, RevGr=41.1%, EPSGr=1114.3%, RSI=23.44",
"timestamp": "2026-02-09T10:55:58.243888"
},
{
"action": "BUY",
"ticker": "ALLY",
"shares": 156,
"price": 42.65,
"cost": 6653.4,
"reason": "GARP signal: PE=18.0, FwdPE=6.76, RevGr=12.0%, EPSGr=265.4%, RSI=53.23",
"timestamp": "2026-02-09T10:55:58.244719"
},
{
"action": "BUY",
"ticker": "JHG",
"shares": 138,
"price": 48.21,
"cost": 6652.98,
"reason": "GARP signal: PE=9.22, FwdPE=9.96, RevGr=61.3%, EPSGr=243.6%, RSI=68.71",
"timestamp": "2026-02-09T10:55:58.245596"
},
{
"action": "BUY",
"ticker": "INCY",
"shares": 61,
"price": 108.69,
"cost": 6630.09,
"reason": "GARP signal: PE=18.42, FwdPE=13.76, RevGr=20.0%, EPSGr=290.7%, RSI=63.48",
"timestamp": "2026-02-09T10:55:58.246554"
},
{
"action": "BUY",
"ticker": "PINS",
"shares": 332,
"price": 20.06,
"cost": 6659.92,
"reason": "GARP signal: PE=7.04, FwdPE=10.61, RevGr=16.8%, EPSGr=225.0%, RSI=19.14",
"timestamp": "2026-02-09T10:55:58.247536"
},
{
"action": "BUY",
"ticker": "EXEL",
"shares": 152,
"price": 43.8,
"cost": 6657.6,
"reason": "GARP signal: PE=18.4, FwdPE=12.76, RevGr=10.8%, EPSGr=72.5%, RSI=50.12",
"timestamp": "2026-02-09T10:55:58.253048"
},
{
"action": "BUY",
"ticker": "CART",
"shares": 187,
"price": 35.49,
"cost": 6636.63,
"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"
},
{
"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

@ -0,0 +1 @@
[]

View File

@ -0,0 +1,310 @@
[
{
"timestamp": "2026-02-09T10:55:58.244115",
"action": "BUY",
"ticker": "DUOL",
"reason": "GARP signal: PE=14.65, FwdPE=14.71, RevGr=41.1%, EPSGr=1114.3%, RSI=23.44",
"details": {
"success": true,
"ticker": "DUOL",
"shares": 57,
"price": 116.35,
"cost": 6631.95,
"cash_remaining": 93368.05
}
},
{
"timestamp": "2026-02-09T10:55:58.244903",
"action": "BUY",
"ticker": "ALLY",
"reason": "GARP signal: PE=18.0, FwdPE=6.76, RevGr=12.0%, EPSGr=265.4%, RSI=53.23",
"details": {
"success": true,
"ticker": "ALLY",
"shares": 156,
"price": 42.65,
"cost": 6653.4,
"cash_remaining": 86714.65
}
},
{
"timestamp": "2026-02-09T10:55:58.245798",
"action": "BUY",
"ticker": "JHG",
"reason": "GARP signal: PE=9.22, FwdPE=9.96, RevGr=61.3%, EPSGr=243.6%, RSI=68.71",
"details": {
"success": true,
"ticker": "JHG",
"shares": 138,
"price": 48.21,
"cost": 6652.98,
"cash_remaining": 80061.67
}
},
{
"timestamp": "2026-02-09T10:55:58.246780",
"action": "BUY",
"ticker": "INCY",
"reason": "GARP signal: PE=18.42, FwdPE=13.76, RevGr=20.0%, EPSGr=290.7%, RSI=63.48",
"details": {
"success": true,
"ticker": "INCY",
"shares": 61,
"price": 108.69,
"cost": 6630.09,
"cash_remaining": 73431.58
}
},
{
"timestamp": "2026-02-09T10:55:58.247771",
"action": "BUY",
"ticker": "PINS",
"reason": "GARP signal: PE=7.04, FwdPE=10.61, RevGr=16.8%, EPSGr=225.0%, RSI=19.14",
"details": {
"success": true,
"ticker": "PINS",
"shares": 332,
"price": 20.06,
"cost": 6659.92,
"cash_remaining": 66771.66
}
},
{
"timestamp": "2026-02-09T10:55:58.248094",
"action": "SKIP",
"ticker": "VLY",
"reason": "RSI too high (78.6 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T10:55:58.248884",
"action": "SKIP",
"ticker": "FHN",
"reason": "RSI too high (76.1 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T10:55:58.249433",
"action": "SKIP",
"ticker": "FNB",
"reason": "RSI too high (71.9 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T10:55:58.250037",
"action": "SKIP",
"ticker": "SSB",
"reason": "RSI too high (92.2 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T10:55:58.250674",
"action": "SKIP",
"ticker": "WBS",
"reason": "RSI too high (82.1 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T10:55:58.251353",
"action": "SKIP",
"ticker": "WAL",
"reason": "RSI too high (71.8 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T10:55:58.251944",
"action": "SKIP",
"ticker": "ONB",
"reason": "RSI too high (81.2 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T10:55:58.253279",
"action": "BUY",
"ticker": "EXEL",
"reason": "GARP signal: PE=18.4, FwdPE=12.76, RevGr=10.8%, EPSGr=72.5%, RSI=50.12",
"details": {
"success": true,
"ticker": "EXEL",
"shares": 152,
"price": 43.8,
"cost": 6657.6,
"cash_remaining": 60114.06
}
},
{
"timestamp": "2026-02-09T10:55:58.253702",
"action": "SKIP",
"ticker": "ZION",
"reason": "RSI too high (74.0 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T10:55:58.254967",
"action": "BUY",
"ticker": "CART",
"reason": "GARP signal: PE=19.5, FwdPE=9.05, RevGr=10.2%, EPSGr=21.1%, RSI=37.75",
"details": {
"success": true,
"ticker": "CART",
"shares": 187,
"price": 35.49,
"cost": 6636.63,
"cash_remaining": 53477.43
}
},
{
"timestamp": "2026-02-09T10:55:58.255400",
"action": "SKIP",
"ticker": "CFG",
"reason": "RSI too high (80.9 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T10:55:58.256002",
"action": "SKIP",
"ticker": "UBSI",
"reason": "RSI too high (80.0 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T10:55:58.256804",
"action": "SKIP",
"ticker": "EWBC",
"reason": "RSI too high (79.3 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T10:55:58.257554",
"action": "SKIP",
"ticker": "FITB",
"reason": "Too close to 52wk high (1.8% away)",
"details": {}
},
{
"timestamp": "2026-02-09T10:55:58.258277",
"action": "SKIP",
"ticker": "BAC",
"reason": "RSI too high (78.3 > 70)",
"details": {}
},
{
"timestamp": "2026-02-09T10:55:58.259039",
"action": "SKIP",
"ticker": "WTFC",
"reason": "RSI too high (72.6 > 70)",
"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

@ -0,0 +1 @@
{"cash": 100000.0, "positions": {}}

View File

@ -0,0 +1,206 @@
{
"date": "2026-02-08",
"timestamp": "2026-02-08T15:18:03.800566",
"total_scanned": 902,
"candidates_found": 11,
"candidates": [
{
"ticker": "ALLY",
"price": 42.31,
"market_cap": 13052339200,
"market_cap_b": 13.1,
"trailing_pe": 17.85,
"forward_pe": 6.7,
"peg_ratio": null,
"revenue_growth": 12.0,
"earnings_growth": 265.4,
"roe": 5.8,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 44.58,
"week52_high": 47.27,
"pct_from_52wk_high": 10.5,
"score": -21.04
},
{
"ticker": "JHG",
"price": 48.22,
"market_cap": 7448852992,
"market_cap_b": 7.4,
"trailing_pe": 9.22,
"forward_pe": 10.12,
"peg_ratio": null,
"revenue_growth": 61.3,
"earnings_growth": 243.6,
"roe": 16.2,
"quick_ratio": 69.46,
"debt_to_equity": 6.5,
"rsi": 63.83,
"week52_high": 49.42,
"pct_from_52wk_high": 2.4,
"score": -20.37
},
{
"ticker": "INCY",
"price": 108.39,
"market_cap": 21279418368,
"market_cap_b": 21.3,
"trailing_pe": 18.37,
"forward_pe": 13.61,
"peg_ratio": null,
"revenue_growth": 20.0,
"earnings_growth": 290.7,
"roe": 30.4,
"quick_ratio": 2.86,
"debt_to_equity": 0.9,
"rsi": 54.22,
"week52_high": 112.29,
"pct_from_52wk_high": 3.5,
"score": -17.46
},
{
"ticker": "FHN",
"price": 26.23,
"market_cap": 12915496960,
"market_cap_b": 12.9,
"trailing_pe": 14.03,
"forward_pe": 11.19,
"peg_ratio": null,
"revenue_growth": 23.7,
"earnings_growth": 74.9,
"roe": 10.9,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 72.21,
"week52_high": 26.56,
"pct_from_52wk_high": 1.2,
"score": 1.3299999999999992
},
{
"ticker": "FNB",
"price": 18.9,
"market_cap": 6768781312,
"market_cap_b": 6.8,
"trailing_pe": 12.12,
"forward_pe": 9.66,
"peg_ratio": null,
"revenue_growth": 26.4,
"earnings_growth": 56.5,
"roe": 8.7,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 69.25,
"week52_high": 19.04,
"pct_from_52wk_high": 0.7,
"score": 1.37
},
{
"ticker": "EXEL",
"price": 43.9,
"market_cap": 11817991168,
"market_cap_b": 11.8,
"trailing_pe": 18.45,
"forward_pe": 12.79,
"peg_ratio": null,
"revenue_growth": 10.8,
"earnings_growth": 72.5,
"roe": 30.6,
"quick_ratio": 3.5,
"debt_to_equity": 8.2,
"rsi": 49.65,
"week52_high": 49.62,
"pct_from_52wk_high": 11.5,
"score": 4.459999999999999
},
{
"ticker": "CART",
"price": 34.64,
"market_cap": 9125501952,
"market_cap_b": 9.1,
"trailing_pe": 19.03,
"forward_pe": 8.84,
"peg_ratio": null,
"revenue_growth": 10.2,
"earnings_growth": 21.1,
"roe": 15.3,
"quick_ratio": 3.33,
"debt_to_equity": 1.0,
"rsi": 30.92,
"week52_high": 53.5,
"pct_from_52wk_high": 35.3,
"score": 5.709999999999999
},
{
"ticker": "CFG",
"price": 68.12,
"market_cap": 29256599552,
"market_cap_b": 29.3,
"trailing_pe": 17.65,
"forward_pe": 10.85,
"peg_ratio": null,
"revenue_growth": 10.7,
"earnings_growth": 35.9,
"roe": 7.2,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 75.46,
"week52_high": 68.36,
"pct_from_52wk_high": 0.4,
"score": 6.1899999999999995
},
{
"ticker": "EWBC",
"price": 122.5,
"market_cap": 16854236160,
"market_cap_b": 16.9,
"trailing_pe": 12.87,
"forward_pe": 11.18,
"peg_ratio": null,
"revenue_growth": 21.6,
"earnings_growth": 21.3,
"roe": 15.9,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 67.58,
"week52_high": 123.22,
"pct_from_52wk_high": 0.6,
"score": 6.890000000000001
},
{
"ticker": "BAC",
"price": 56.53,
"market_cap": 412810084352,
"market_cap_b": 412.8,
"trailing_pe": 14.84,
"forward_pe": 11.41,
"peg_ratio": null,
"revenue_growth": 13.2,
"earnings_growth": 20.9,
"roe": 10.2,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 71.14,
"week52_high": 57.55,
"pct_from_52wk_high": 1.8,
"score": 8.0
},
{
"ticker": "FITB",
"price": 55.08,
"market_cap": 49574670336,
"market_cap_b": 49.6,
"trailing_pe": 15.6,
"forward_pe": 11.24,
"peg_ratio": null,
"revenue_growth": 11.5,
"earnings_growth": 20.8,
"roe": 12.2,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 71.83,
"week52_high": 55.36,
"pct_from_52wk_high": 0.5,
"score": 8.01
}
]
}

View File

@ -0,0 +1,368 @@
{
"date": "2026-02-09",
"timestamp": "2026-02-09T15:36:18.290971",
"total_scanned": 902,
"candidates_found": 20,
"candidates": [
{
"ticker": "ALLY",
"price": 42.04,
"market_cap": 12969046016,
"market_cap_b": 13.0,
"trailing_pe": 17.74,
"forward_pe": 6.66,
"peg_ratio": null,
"revenue_growth": 12.0,
"earnings_growth": 265.4,
"roe": 5.8,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 49.52,
"week52_high": 47.27,
"pct_from_52wk_high": 11.1,
"score": -21.08
},
{
"ticker": "JHG",
"price": 48.2,
"market_cap": 7445763584,
"market_cap_b": 7.4,
"trailing_pe": 9.22,
"forward_pe": 9.95,
"peg_ratio": null,
"revenue_growth": 61.3,
"earnings_growth": 243.6,
"roe": 16.2,
"quick_ratio": 69.46,
"debt_to_equity": 6.5,
"rsi": 68.18,
"week52_high": 49.42,
"pct_from_52wk_high": 2.5,
"score": -20.54
},
{
"ticker": "INCY",
"price": 109.03,
"market_cap": 21405063168,
"market_cap_b": 21.4,
"trailing_pe": 18.48,
"forward_pe": 13.81,
"peg_ratio": null,
"revenue_growth": 20.0,
"earnings_growth": 290.7,
"roe": 30.4,
"quick_ratio": 2.86,
"debt_to_equity": 0.9,
"rsi": 64.03,
"week52_high": 112.29,
"pct_from_52wk_high": 2.9,
"score": -17.259999999999998
},
{
"ticker": "PINS",
"price": 20.14,
"market_cap": 13693783040,
"market_cap_b": 13.7,
"trailing_pe": 7.07,
"forward_pe": 10.66,
"peg_ratio": null,
"revenue_growth": 16.8,
"earnings_growth": 225.0,
"roe": 51.5,
"quick_ratio": 8.14,
"debt_to_equity": 4.3,
"rsi": 19.93,
"week52_high": 40.38,
"pct_from_52wk_high": 50.1,
"score": -13.52
},
{
"ticker": "VLY",
"price": 13.7,
"market_cap": 7639607296,
"market_cap_b": 7.6,
"trailing_pe": 13.56,
"forward_pe": 9.19,
"peg_ratio": null,
"revenue_growth": 38.3,
"earnings_growth": 66.3,
"roe": 7.8,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 78.34,
"week52_high": 13.79,
"pct_from_52wk_high": 0.7,
"score": -1.27
},
{
"ticker": "FHN",
"price": 26.03,
"market_cap": 12817018880,
"market_cap_b": 12.8,
"trailing_pe": 13.92,
"forward_pe": 11.1,
"peg_ratio": null,
"revenue_growth": 23.7,
"earnings_growth": 74.9,
"roe": 10.9,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 72.36,
"week52_high": 26.56,
"pct_from_52wk_high": 2.0,
"score": 1.2399999999999993
},
{
"ticker": "FNB",
"price": 18.92,
"market_cap": 6775944192,
"market_cap_b": 6.8,
"trailing_pe": 12.13,
"forward_pe": 9.67,
"peg_ratio": null,
"revenue_growth": 26.4,
"earnings_growth": 56.5,
"roe": 8.7,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 70.99,
"week52_high": 19.14,
"pct_from_52wk_high": 1.1,
"score": 1.38
},
{
"ticker": "SSB",
"price": 107.18,
"market_cap": 10773236736,
"market_cap_b": 10.8,
"trailing_pe": 13.62,
"forward_pe": 10.13,
"peg_ratio": null,
"revenue_growth": 53.2,
"earnings_growth": 30.9,
"roe": 10.7,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 89.03,
"week52_high": 108.46,
"pct_from_52wk_high": 1.2,
"score": 1.7200000000000006
},
{
"ticker": "WBS",
"price": 73.21,
"market_cap": 11808063488,
"market_cap_b": 11.8,
"trailing_pe": 12.41,
"forward_pe": 9.78,
"peg_ratio": null,
"revenue_growth": 18.2,
"earnings_growth": 53.4,
"roe": 10.8,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 82.05,
"week52_high": 73.5,
"pct_from_52wk_high": 0.4,
"score": 2.6199999999999997
},
{
"ticker": "ONB",
"price": 25.67,
"market_cap": 10031142912,
"market_cap_b": 10.0,
"trailing_pe": 14.34,
"forward_pe": 8.95,
"peg_ratio": null,
"revenue_growth": 41.4,
"earnings_growth": 17.2,
"roe": 9.0,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 77.64,
"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,
"score": 3.13
},
{
"ticker": "EXEL",
"price": 43.95,
"market_cap": 11831451648,
"market_cap_b": 11.8,
"trailing_pe": 18.47,
"forward_pe": 12.8,
"peg_ratio": null,
"revenue_growth": 10.8,
"earnings_growth": 72.5,
"roe": 30.6,
"quick_ratio": 3.5,
"debt_to_equity": 8.2,
"rsi": 51.02,
"week52_high": 49.62,
"pct_from_52wk_high": 11.4,
"score": 4.470000000000001
},
{
"ticker": "ZION",
"price": 65.16,
"market_cap": 9621069824,
"market_cap_b": 9.6,
"trailing_pe": 10.84,
"forward_pe": 9.97,
"peg_ratio": null,
"revenue_growth": 13.6,
"earnings_growth": 31.4,
"roe": 13.5,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 73.29,
"week52_high": 66.18,
"pct_from_52wk_high": 1.5,
"score": 5.470000000000001
},
{
"ticker": "CART",
"price": 35.15,
"market_cap": 9259855872,
"market_cap_b": 9.3,
"trailing_pe": 19.31,
"forward_pe": 8.97,
"peg_ratio": null,
"revenue_growth": 10.2,
"earnings_growth": 21.1,
"roe": 15.3,
"quick_ratio": 3.33,
"debt_to_equity": 1.0,
"rsi": 36.01,
"week52_high": 53.5,
"pct_from_52wk_high": 34.3,
"score": 5.84
},
{
"ticker": "CFG",
"price": 67.7,
"market_cap": 29076213760,
"market_cap_b": 29.1,
"trailing_pe": 17.54,
"forward_pe": 10.75,
"peg_ratio": null,
"revenue_growth": 10.7,
"earnings_growth": 35.9,
"roe": 7.2,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 78.47,
"week52_high": 68.65,
"pct_from_52wk_high": 1.4,
"score": 6.09
},
{
"ticker": "UBSI",
"price": 45.06,
"market_cap": 6280699392,
"market_cap_b": 6.3,
"trailing_pe": 13.78,
"forward_pe": 11.96,
"peg_ratio": null,
"revenue_growth": 22.1,
"earnings_growth": 32.1,
"roe": 8.9,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 77.51,
"week52_high": 45.93,
"pct_from_52wk_high": 1.9,
"score": 6.54
},
{
"ticker": "EWBC",
"price": 122.56,
"market_cap": 16862490624,
"market_cap_b": 16.9,
"trailing_pe": 12.87,
"forward_pe": 11.18,
"peg_ratio": null,
"revenue_growth": 21.6,
"earnings_growth": 21.3,
"roe": 15.9,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 78.61,
"week52_high": 123.82,
"pct_from_52wk_high": 1.0,
"score": 6.890000000000001
},
{
"ticker": "FITB",
"price": 54.33,
"market_cap": 48899633152,
"market_cap_b": 48.9,
"trailing_pe": 15.39,
"forward_pe": 11.08,
"peg_ratio": null,
"revenue_growth": 11.5,
"earnings_growth": 20.8,
"roe": 12.2,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 65.77,
"week52_high": 55.36,
"pct_from_52wk_high": 1.9,
"score": 7.85
},
{
"ticker": "BAC",
"price": 56.41,
"market_cap": 411933769728,
"market_cap_b": 411.9,
"trailing_pe": 14.81,
"forward_pe": 11.38,
"peg_ratio": null,
"revenue_growth": 13.2,
"earnings_growth": 20.9,
"roe": 10.2,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 78.1,
"week52_high": 57.55,
"pct_from_52wk_high": 2.0,
"score": 7.970000000000001
},
{
"ticker": "WTFC",
"price": 158.57,
"market_cap": 10620212224,
"market_cap_b": 10.6,
"trailing_pe": 13.9,
"forward_pe": 11.82,
"peg_ratio": null,
"revenue_growth": 10.5,
"earnings_growth": 19.4,
"roe": 12.1,
"quick_ratio": null,
"debt_to_equity": null,
"rsi": 70.23,
"week52_high": 162.96,
"pct_from_52wk_high": 2.7,
"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 @@
[]

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,344 @@
#!/usr/bin/env python3
"""Multiplayer game engine for Market Watch paper trading."""
import json
import os
import uuid
from datetime import datetime, date
DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
GAMES_DIR = os.path.join(DATA_DIR, "games")
def _load_json(path, default=None):
if os.path.exists(path):
with open(path) as f:
return json.load(f)
return default if default is not None else {}
def _save_json(path, data):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as f:
json.dump(data, f, indent=2, default=str)
def _game_dir(game_id):
return os.path.join(GAMES_DIR, game_id)
def _player_dir(game_id, username):
return os.path.join(_game_dir(game_id), "players", username)
def _portfolio_path(game_id, username):
return os.path.join(_player_dir(game_id, username), "portfolio.json")
def _trades_path(game_id, username):
return os.path.join(_player_dir(game_id, username), "trades.json")
def _snapshots_path(game_id, username):
return os.path.join(_player_dir(game_id, username), "snapshots.json")
def _game_config_path(game_id):
return os.path.join(_game_dir(game_id), "game.json")
# ── Game Management ──
def create_game(name, starting_cash=100_000.0, end_date=None, creator="system"):
"""Create a new game. Returns game_id."""
game_id = str(uuid.uuid4())[:8]
config = {
"game_id": game_id,
"name": name,
"starting_cash": starting_cash,
"start_date": date.today().isoformat(),
"end_date": end_date,
"creator": creator,
"created_at": datetime.now().isoformat(),
"players": [],
"status": "active",
}
os.makedirs(_game_dir(game_id), exist_ok=True)
_save_json(_game_config_path(game_id), config)
return game_id
def list_games(active_only=True):
"""List all games."""
games = []
if not os.path.exists(GAMES_DIR):
return games
for gid in os.listdir(GAMES_DIR):
config_path = _game_config_path(gid)
if os.path.exists(config_path):
config = _load_json(config_path)
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):
"""Get game config."""
return _load_json(_game_config_path(game_id))
def join_game(game_id, username):
"""Add a player to a game. Initializes their portfolio."""
config = get_game(game_id)
if not config:
return {"success": False, "error": "Game not found"}
if username in config["players"]:
return {"success": False, "error": f"{username} already in game"}
config["players"].append(username)
_save_json(_game_config_path(game_id), config)
# Initialize player portfolio
player_dir = _player_dir(game_id, username)
os.makedirs(player_dir, exist_ok=True)
_save_json(_portfolio_path(game_id, username), {
"cash": config["starting_cash"],
"positions": {},
})
_save_json(_trades_path(game_id, username), [])
_save_json(_snapshots_path(game_id, username), [])
return {"success": True, "game_id": game_id, "username": username, "starting_cash": config["starting_cash"]}
# ── Trading ──
def buy(game_id, username, ticker, shares, price, reason="Manual"):
"""Buy shares for a player in a game."""
pf = _load_json(_portfolio_path(game_id, username))
if not pf:
return {"success": False, "error": "Player portfolio not found"}
cost = shares * price
if cost > pf["cash"]:
return {"success": False, "error": f"Insufficient cash. Need ${cost:,.2f}, have ${pf['cash']:,.2f}"}
pf["cash"] -= cost
if ticker in pf["positions"]:
pos = pf["positions"][ticker]
total_shares = pos["shares"] + shares
pos["avg_cost"] = ((pos["avg_cost"] * pos["shares"]) + cost) / total_shares
pos["shares"] = total_shares
pos["current_price"] = price
# Update trailing stop
new_stop = price * 0.90
if new_stop > pos.get("trailing_stop", 0):
pos["trailing_stop"] = new_stop
else:
pf["positions"][ticker] = {
"shares": shares,
"avg_cost": price,
"current_price": price,
"entry_date": datetime.now().isoformat(),
"entry_reason": reason,
"trailing_stop": price * 0.90,
}
_save_json(_portfolio_path(game_id, username), pf)
# Log trade
trades = _load_json(_trades_path(game_id, username), [])
trades.append({
"action": "BUY",
"ticker": ticker,
"shares": shares,
"price": price,
"cost": round(cost, 2),
"reason": reason,
"timestamp": datetime.now().isoformat(),
})
_save_json(_trades_path(game_id, username), trades)
return {"success": True, "ticker": ticker, "shares": shares, "price": price, "cost": round(cost, 2), "cash_remaining": round(pf["cash"], 2)}
def sell(game_id, username, ticker, shares=None, price=None, reason="Manual"):
"""Sell shares for a player."""
pf = _load_json(_portfolio_path(game_id, username))
if not pf:
return {"success": False, "error": "Player portfolio not found"}
if ticker not in pf["positions"]:
return {"success": False, "error": f"No position in {ticker}"}
pos = pf["positions"][ticker]
if shares is None:
shares = pos["shares"]
if shares > pos["shares"]:
return {"success": False, "error": f"Only have {pos['shares']} shares of {ticker}"}
if price is None:
price = pos["current_price"]
proceeds = shares * price
pf["cash"] += proceeds
realized_pnl = (price - pos["avg_cost"]) * shares
if shares >= pos["shares"]:
del pf["positions"][ticker]
else:
pos["shares"] -= shares
_save_json(_portfolio_path(game_id, username), pf)
# Log trade
trades = _load_json(_trades_path(game_id, username), [])
trades.append({
"action": "SELL",
"ticker": ticker,
"shares": shares,
"price": price,
"proceeds": round(proceeds, 2),
"realized_pnl": round(realized_pnl, 2),
"entry_price": pos["avg_cost"],
"reason": reason,
"timestamp": datetime.now().isoformat(),
})
_save_json(_trades_path(game_id, username), trades)
return {"success": True, "ticker": ticker, "shares": shares, "price": price, "proceeds": round(proceeds, 2), "realized_pnl": round(realized_pnl, 2)}
def update_price(game_id, username, ticker, price):
"""Update current price for a position."""
pf = _load_json(_portfolio_path(game_id, username))
if pf and ticker in pf["positions"]:
pos = pf["positions"][ticker]
pos["current_price"] = price
new_stop = price * 0.90
if new_stop > pos.get("trailing_stop", 0):
pos["trailing_stop"] = new_stop
_save_json(_portfolio_path(game_id, username), pf)
def get_portfolio(game_id, username):
"""Get player's portfolio with unrealized P&L."""
pf = _load_json(_portfolio_path(game_id, username))
if not pf:
return None
game = get_game(game_id)
starting_cash = game.get("starting_cash", 100_000) if game else 100_000
total_value = pf["cash"]
positions_out = {}
for ticker, pos in pf["positions"].items():
unrealized_pnl = (pos["current_price"] - pos["avg_cost"]) * pos["shares"]
market_value = pos["current_price"] * pos["shares"]
total_value += market_value
positions_out[ticker] = {
**pos,
"unrealized_pnl": round(unrealized_pnl, 2),
"market_value": round(market_value, 2),
}
total_pnl = total_value - starting_cash
return {
"username": username,
"game_id": game_id,
"cash": round(pf["cash"], 2),
"positions": positions_out,
"total_value": round(total_value, 2),
"total_pnl": round(total_pnl, 2),
"pnl_pct": round(total_pnl / starting_cash * 100, 2),
"num_positions": len(positions_out),
}
def get_trades(game_id, username):
"""Get player's trade history."""
return _load_json(_trades_path(game_id, username), [])
def daily_snapshot(game_id, username):
"""Take daily snapshot for a player."""
p = get_portfolio(game_id, username)
if not p:
return None
snapshots = _load_json(_snapshots_path(game_id, username), [])
today = date.today().isoformat()
snapshots = [s for s in snapshots if s["date"] != today]
snapshots.append({
"date": today,
"total_value": p["total_value"],
"total_pnl": p["total_pnl"],
"pnl_pct": p["pnl_pct"],
"cash": p["cash"],
"num_positions": p["num_positions"],
})
_save_json(_snapshots_path(game_id, username), snapshots)
return snapshots[-1]
def get_snapshots(game_id, username):
"""Get player's daily snapshots."""
return _load_json(_snapshots_path(game_id, username), [])
def get_leaderboard(game_id):
"""Get game leaderboard sorted by % return."""
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)
num_trades = len([t for t in trades if t.get("action") == "SELL"])
board.append({
"username": username,
"total_value": p["total_value"],
"total_pnl": p["total_pnl"],
"pnl_pct": p["pnl_pct"],
"num_positions": p["num_positions"],
"num_trades": num_trades,
"cash": p["cash"],
})
return sorted(board, key=lambda x: x["pnl_pct"], reverse=True)
# ── Convenience: find default game ──
def get_default_game_id():
"""Get the first active game (usually 'GARP Challenge')."""
games = list_games(active_only=True)
if games:
return games[0]["game_id"]
return None
# ── Initialize default game ──
def ensure_default_game():
"""Create default GARP Challenge game with 'case' player if it doesn't exist."""
games = list_games(active_only=True)
for g in games:
if g["name"] == "GARP Challenge":
return g["game_id"]
game_id = create_game("GARP Challenge", starting_cash=100_000.0, 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']}")
board = get_leaderboard(game_id)
for entry in board:
print(f" {entry['username']}: ${entry['total_value']:,.2f} ({entry['pnl_pct']:+.2f}%)")

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

@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""Market Watch Web Portal — modern dark-themed dashboard."""
import json
import os
import sys
import traceback
from datetime import datetime
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
from urllib.parse import urlparse, parse_qs
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import game_engine
PORT = 8889
PORTAL_DIR = os.path.dirname(os.path.abspath(__file__))
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
daemon_threads = True
def _fetch_live_prices(tickers):
"""Fetch live prices via yfinance. Returns {ticker: price}."""
try:
import yfinance as yf
data = yf.download(tickers, period="1d", progress=False)
prices = {}
if len(tickers) == 1:
t = tickers[0]
if "Close" in data.columns and len(data) > 0:
prices[t] = float(data["Close"].iloc[-1])
else:
if "Close" in data.columns:
for t in tickers:
try:
val = data["Close"][t].iloc[-1]
if val == val: # not NaN
prices[t] = float(val)
except Exception:
pass
return prices
except Exception:
return {}
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
try:
path = urlparse(self.path).path.rstrip("/") or "/"
if path == "/":
return self._serve_file("index.html", "text/html")
# API endpoints
if path == "/api/games":
games = game_engine.list_games(active_only=False)
# Enrich with summary
for g in games:
board = game_engine.get_leaderboard(g["game_id"])
g["leaderboard"] = board
trades_all = []
for p in g.get("players", []):
trades_all.extend(game_engine.get_trades(g["game_id"], p))
g["total_trades"] = len(trades_all)
sells = [t for t in trades_all if t.get("action") == "SELL"]
wins = [t for t in sells if t.get("realized_pnl", 0) > 0]
g["win_rate"] = round(len(wins)/len(sells)*100, 1) if sells else None
return self._json(games)
# /api/game/{id}
parts = path.split("/")
if len(parts) >= 4 and parts[1] == "api" and parts[2] == "game":
gid = parts[3]
if len(parts) == 4:
game = game_engine.get_game(gid)
if not game:
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)
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)
if len(parts) == 5 and parts[4] == "portfolio":
game = game_engine.get_game(gid)
if not game:
return self._json({"error": "not found"}, 404)
portfolios = {}
all_tickers = []
for p in game.get("players", []):
pf = game_engine.get_portfolio(gid, p)
if pf:
portfolios[p] = pf
all_tickers.extend(pf["positions"].keys())
# Fetch live prices
all_tickers = list(set(all_tickers))
if all_tickers:
live = _fetch_live_prices(all_tickers)
for p, pf in portfolios.items():
total_value = pf["cash"]
for ticker, pos in pf["positions"].items():
if ticker in live:
pos["live_price"] = live[ticker]
pos["current_price"] = live[ticker]
pos["unrealized_pnl"] = round((live[ticker] - pos["avg_cost"]) * pos["shares"], 2)
pos["market_value"] = round(live[ticker] * pos["shares"], 2)
total_value += pos["market_value"]
pf["total_value"] = round(total_value, 2)
starting = game.get("starting_cash", 100000)
pf["total_pnl"] = round(total_value - starting, 2)
pf["pnl_pct"] = round((total_value - starting) / starting * 100, 2)
return self._json(portfolios)
self._error(404)
except Exception as e:
self._json({"error": str(e), "trace": traceback.format_exc()}, 500)
def _serve_file(self, filename, content_type):
filepath = os.path.join(PORTAL_DIR, filename)
with open(filepath, "rb") as f:
data = f.read()
self.send_response(200)
self.send_header("Content-Type", content_type)
self.end_headers()
self.wfile.write(data)
def _json(self, data, code=200):
body = json.dumps(data, default=str).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(body)
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
def main():
game_engine.ensure_default_game()
print(f"📊 Market Watch Portal → http://localhost:{PORT}")
server = ThreadedHTTPServer(("0.0.0.0", PORT), Handler)
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nStopped")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Portfolio module — backward-compatible wrapper around game_engine.
All operations now delegate to game_engine using the default game and 'case' player.
"""
import json
import game_engine
INITIAL_CASH = 100_000.0
def _default():
"""Get default game ID."""
return game_engine.get_default_game_id() or game_engine.ensure_default_game()
def buy(ticker, shares, price, reason="GARP signal"):
return game_engine.buy(_default(), "case", ticker, shares, price, reason)
def sell(ticker, shares=None, price=None, reason="GARP exit"):
return game_engine.sell(_default(), "case", ticker, shares, price, reason)
def update_price(ticker, price):
game_engine.update_price(_default(), "case", ticker, price)
def get_portfolio():
return game_engine.get_portfolio(_default(), "case")
def get_history():
return game_engine.get_trades(_default(), "case")
def daily_snapshot():
return game_engine.daily_snapshot(_default(), "case")
def get_snapshots():
return game_engine.get_snapshots(_default(), "case")
if __name__ == "__main__":
p = get_portfolio()
if p:
print(json.dumps(p, indent=2))
else:
print("No default game found. Run: python3 game_engine.py")

View File

@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Daily runner for Market Watch - scans, trades (as Case), snapshots, alerts."""
import json
import os
import sys
import requests
from datetime import datetime
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import game_engine
from scanner import run_scan
from trader import run_trading_logic
CREDS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", ".credentials", "telegram-bot.env")
CHAT_ID = "6443752046"
def load_telegram_token():
if os.path.exists(CREDS_FILE):
with open(CREDS_FILE) as f:
for line in f:
if line.startswith("TELEGRAM_BOT_TOKEN="):
return line.strip().split("=", 1)[1]
return os.environ.get("TELEGRAM_BOT_TOKEN")
def send_telegram(message, token):
if not token:
return
try:
url = f"https://api.telegram.org/bot{token}/sendMessage"
requests.post(url, json={"chat_id": CHAT_ID, "text": message, "parse_mode": "HTML"}, timeout=10)
except Exception as e:
print(f"Telegram error: {e}")
def main():
print(f"📊 Market Watch Daily Run — {datetime.now().strftime('%Y-%m-%d %H:%M')}")
token = load_telegram_token()
game_id = game_engine.ensure_default_game()
game = game_engine.get_game(game_id)
# 1. Run GARP scan
print("\n[1/3] Running GARP scan...")
candidates = run_scan() # returns list of candidates directly
print(f" Found {len(candidates)} candidates")
# 2. Run trading logic for Case
print("\n[2/3] Running trading logic for Case...")
result = run_trading_logic(game_id, "case", candidates)
# 3. Snapshots for all players
print("\n[3/3] Taking snapshots...")
for username in game["players"]:
snap = game_engine.daily_snapshot(game_id, username)
if snap:
print(f" {username}: ${snap['total_value']:,.2f} ({snap['pnl_pct']:+.2f}%)")
# Telegram summary
p = game_engine.get_portfolio(game_id, "case")
pnl_emoji = "📈" if p["total_pnl"] >= 0 else "📉"
summary = f"📊 <b>Market Watch Daily</b>\n"
summary += f"{pnl_emoji} Portfolio: ${p['total_value']:,.2f} ({p['pnl_pct']:+.2f}%)\n"
summary += f"💰 Cash: ${p['cash']:,.2f} | Positions: {p['num_positions']}\n"
num_trades = len(result.get("sells", [])) + len(result.get("buys", []))
if num_trades:
summary += f"\n<b>{num_trades} trades executed</b>\n"
for s in result.get("sells", []):
summary += f"🔴 SELL {s['ticker']}{s['reason'][:50]}\n"
for b in result.get("buys", []):
summary += f"🟢 BUY {b['ticker']}{b['reason'][:50]}\n"
else:
summary += "\nNo trades today."
if candidates:
top5 = ", ".join(c["ticker"] for c in candidates[:5])
summary += f"\n🔍 Top picks: {top5}"
# Leaderboard
board = game_engine.get_leaderboard(game_id)
if len(board) > 1:
summary += "\n\n<b>Leaderboard:</b>\n"
medals = ["🥇", "🥈", "🥉"]
for i, entry in enumerate(board[:5]):
medal = medals[i] if i < 3 else f"#{i+1}"
summary += f"{medal} {entry['username']}: {entry['pnl_pct']:+.2f}%\n"
send_telegram(summary, token)
print("\n✅ Daily run complete")
if __name__ == "__main__":
main()

233
projects/market-watch/scanner.py Executable file
View File

@ -0,0 +1,233 @@
#!/usr/bin/env python3
"""GARP stock scanner - scans S&P 500 + S&P 400 MidCap for growth-at-reasonable-price candidates."""
import json
import os
import re
import sys
import time
from datetime import date, datetime
import numpy as np
import requests
import yfinance as yf
DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
SCANS_DIR = os.path.join(DATA_DIR, "scans")
TICKERS_CACHE = os.path.join(DATA_DIR, "tickers.json")
HEADERS = {"User-Agent": "MarketWatch/1.0 (paper trading bot; contact: case-lgn@protonmail.com)"}
def _scrape_tickers(url):
"""Scrape tickers from a Wikipedia S&P constituents page."""
import io
import pandas as pd
resp = requests.get(url, timeout=30, headers=HEADERS)
tables = pd.read_html(io.StringIO(resp.text))
if tables:
df = tables[0]
col = "Symbol" if "Symbol" in df.columns else df.columns[0]
tickers = df[col].astype(str).str.strip().tolist()
tickers = [t.replace(".", "-") for t in tickers if re.match(r'^[A-Z]{1,5}(\.[A-Z])?$', t.replace("-", "."))]
return tickers
return []
def get_sp500_tickers():
return _scrape_tickers("https://en.wikipedia.org/wiki/List_of_S%26P_500_companies")
def get_sp400_tickers():
return _scrape_tickers("https://en.wikipedia.org/wiki/List_of_S%26P_400_companies")
def get_all_tickers(use_cache=True):
"""Get combined ticker list, with caching."""
if use_cache and os.path.exists(TICKERS_CACHE):
cache = json.loads(open(TICKERS_CACHE).read())
# Use cache if less than 7 days old
cached_date = cache.get("date", "")
if cached_date and (date.today() - date.fromisoformat(cached_date)).days < 7:
return cache["tickers"]
print("Fetching ticker lists from Wikipedia...")
sp500 = get_sp500_tickers()
print(f" S&P 500: {len(sp500)} tickers")
sp400 = get_sp400_tickers()
print(f" S&P 400: {len(sp400)} tickers")
all_tickers = sorted(set(sp500 + sp400))
os.makedirs(DATA_DIR, exist_ok=True)
with open(TICKERS_CACHE, "w") as f:
json.dump({"date": date.today().isoformat(), "tickers": all_tickers, "sp500": len(sp500), "sp400": len(sp400)}, f)
print(f" Combined: {len(all_tickers)} unique tickers")
return all_tickers
def compute_rsi(prices, period=14):
"""Compute RSI from a price series."""
if len(prices) < period + 1:
return None
deltas = np.diff(prices)
gains = np.where(deltas > 0, deltas, 0)
losses = np.where(deltas < 0, -deltas, 0)
avg_gain = np.mean(gains[-period:])
avg_loss = np.mean(losses[-period:])
if avg_loss == 0:
return 100.0
rs = avg_gain / avg_loss
return round(100 - (100 / (1 + rs)), 2)
def scan_ticker(ticker):
"""Evaluate a single ticker against GARP criteria. Returns dict or None."""
try:
stock = yf.Ticker(ticker)
info = stock.info
if not info or info.get("regularMarketPrice") is None:
return None
# Market cap filter
market_cap = info.get("marketCap", 0)
if not market_cap or market_cap < 5e9:
return None
# P/E filters
trailing_pe = info.get("trailingPE")
forward_pe = info.get("forwardPE")
if trailing_pe is None or trailing_pe <= 0 or trailing_pe >= 25:
return None
if forward_pe is None or forward_pe <= 0 or forward_pe >= 15:
return None
# Revenue growth
revenue_growth = info.get("revenueGrowth")
if revenue_growth is None or revenue_growth < 0.10:
return None
# EPS growth (earnings growth)
earnings_growth = info.get("earningsGrowth")
if earnings_growth is None or earnings_growth < 0.15:
return None
# ROE
roe = info.get("returnOnEquity")
if roe is None or roe < 0.05:
return None
# Optional filters (don't disqualify if unavailable)
peg = info.get("pegRatio")
if peg is not None and peg > 1.2:
return None
quick_ratio = info.get("quickRatio")
if quick_ratio is not None and quick_ratio < 1.5:
return None
de_ratio = info.get("debtToEquity")
if de_ratio is not None and de_ratio > 35:
return None
# Get price history for RSI and 52-week high
hist = stock.history(period="3mo")
if hist.empty or len(hist) < 20:
return None
closes = hist["Close"].values
current_price = closes[-1]
rsi = compute_rsi(closes)
# 52-week high
week52_high = info.get("fiftyTwoWeekHigh", current_price)
pct_from_high = ((week52_high - current_price) / week52_high) * 100 if week52_high else 0
return {
"ticker": ticker,
"price": round(current_price, 2),
"market_cap": market_cap,
"market_cap_b": round(market_cap / 1e9, 1),
"trailing_pe": round(trailing_pe, 2),
"forward_pe": round(forward_pe, 2),
"peg_ratio": round(peg, 2) if peg else None,
"revenue_growth": round(revenue_growth * 100, 1),
"earnings_growth": round(earnings_growth * 100, 1),
"roe": round(roe * 100, 1),
"quick_ratio": round(quick_ratio, 2) if quick_ratio else None,
"debt_to_equity": round(de_ratio, 1) if de_ratio else None,
"rsi": rsi,
"week52_high": round(week52_high, 2) if week52_high else None,
"pct_from_52wk_high": round(pct_from_high, 1),
}
except Exception as e:
return None
def run_scan(batch_size=5, delay=1.0):
"""Run full GARP scan. Returns list of candidates sorted by score."""
tickers = get_all_tickers()
candidates = []
total = len(tickers)
print(f"\nScanning {total} tickers...")
for i in range(0, total, batch_size):
batch = tickers[i:i + batch_size]
for ticker in batch:
idx = i + batch.index(ticker) + 1
sys.stdout.write(f"\r [{idx}/{total}] Scanning {ticker}... ")
sys.stdout.flush()
result = scan_ticker(ticker)
if result:
candidates.append(result)
print(f"\n{ticker} passed GARP filter (PE={result['trailing_pe']}, FwdPE={result['forward_pe']}, RevGr={result['revenue_growth']}%)")
if i + batch_size < total:
time.sleep(delay)
print(f"\n\nScan complete: {len(candidates)} candidates from {total} tickers")
# Sort by a composite score: lower forward PE + higher earnings growth
for c in candidates:
# Simple ranking score: lower is better
c["score"] = c["forward_pe"] - (c["earnings_growth"] / 10) - (c["revenue_growth"] / 10)
candidates.sort(key=lambda x: x["score"])
# Save results
os.makedirs(SCANS_DIR, exist_ok=True)
scan_file = os.path.join(SCANS_DIR, f"{date.today().isoformat()}.json")
scan_data = {
"date": date.today().isoformat(),
"timestamp": datetime.now().isoformat(),
"total_scanned": total,
"candidates_found": len(candidates),
"candidates": candidates,
}
with open(scan_file, "w") as f:
json.dump(scan_data, f, indent=2)
print(f"Results saved to {scan_file}")
return candidates
def load_latest_scan():
"""Load the most recent scan results."""
if not os.path.exists(SCANS_DIR):
return None
files = sorted(f for f in os.listdir(SCANS_DIR) if f.endswith(".json"))
if not files:
return None
with open(os.path.join(SCANS_DIR, files[-1])) as f:
return json.load(f)
if __name__ == "__main__":
candidates = run_scan()
if candidates:
print(f"\nTop candidates:")
for c in candidates[:10]:
print(f" {c['ticker']:6s} Price=${c['price']:8.2f} PE={c['trailing_pe']:5.1f} FwdPE={c['forward_pe']:5.1f} "
f"RevGr={c['revenue_growth']:5.1f}% EPSGr={c['earnings_growth']:5.1f}% RSI={c['rsi']}")
else:
print("No candidates found matching GARP criteria.")

191
projects/market-watch/trader.py Executable file
View File

@ -0,0 +1,191 @@
#!/usr/bin/env python3
"""GARP trading decision engine — multiplayer aware."""
import json
import os
from datetime import datetime
import yfinance as yf
import game_engine
import scanner
MAX_POSITIONS = 15
MAX_POSITION_PCT = 0.10
RSI_BUY_LIMIT = 70
RSI_SELL_LIMIT = 80
NEAR_HIGH_PCT = 2.0
LOG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "logs")
def log_decision(action, ticker, reason, details=None):
os.makedirs(LOG_DIR, exist_ok=True)
entry = {
"timestamp": datetime.now().isoformat(),
"action": action,
"ticker": ticker,
"reason": reason,
"details": details or {},
}
log_file = os.path.join(LOG_DIR, f"{datetime.now().strftime('%Y-%m-%d')}.json")
logs = []
if os.path.exists(log_file):
with open(log_file) as f:
logs = json.load(f)
logs.append(entry)
with open(log_file, "w") as f:
json.dump(logs, f, indent=2, default=str)
return entry
def update_all_prices(game_id, username):
"""Update current prices for all held positions."""
p = game_engine.get_portfolio(game_id, username)
updated = []
for ticker in p["positions"]:
try:
stock = yf.Ticker(ticker)
hist = stock.history(period="5d")
if not hist.empty:
price = float(hist["Close"].iloc[-1])
game_engine.update_price(game_id, username, ticker, price)
updated.append((ticker, price))
except Exception as e:
print(f" Warning: Could not update {ticker}: {e}")
return updated
def check_sell_signals(game_id, username):
"""Check existing positions for sell signals."""
p = game_engine.get_portfolio(game_id, username)
sells = []
if not p["positions"]:
return sells
latest_scan = scanner.load_latest_scan()
scan_tickers = set()
if latest_scan:
scan_tickers = {c["ticker"] for c in latest_scan.get("candidates", [])}
for ticker, pos in list(p["positions"].items()):
sell_reason = None
if pos["current_price"] <= pos.get("trailing_stop", 0):
sell_reason = f"Trailing stop hit (stop={pos.get('trailing_stop', 0):.2f}, price={pos['current_price']:.2f})"
if not sell_reason:
try:
stock = yf.Ticker(ticker)
hist = stock.history(period="3mo")
if not hist.empty and len(hist) >= 15:
rsi = scanner.compute_rsi(hist["Close"].values)
if rsi and rsi > RSI_SELL_LIMIT:
sell_reason = f"RSI overbought ({rsi:.1f} > {RSI_SELL_LIMIT})"
except:
pass
if not sell_reason and latest_scan and ticker not in scan_tickers:
sell_reason = f"No longer passes GARP filter"
if sell_reason:
result = game_engine.sell(game_id, username, ticker, price=pos["current_price"], reason=sell_reason)
log_entry = log_decision("SELL", ticker, sell_reason, result)
sells.append(log_entry)
print(f" SELL {ticker}: {sell_reason}")
return sells
def check_buy_signals(game_id, username, candidates=None):
"""Check scan candidates for buy signals."""
p = game_engine.get_portfolio(game_id, username)
buys = []
if p["num_positions"] >= MAX_POSITIONS:
print(f" Max positions reached ({MAX_POSITIONS}), skipping buys")
return buys
if candidates is None:
latest_scan = scanner.load_latest_scan()
if not latest_scan:
print(" No scan data available")
return buys
candidates = latest_scan.get("candidates", [])
position_size = p["total_value"] / MAX_POSITIONS
max_per_position = p["total_value"] * MAX_POSITION_PCT
existing_tickers = set(p["positions"].keys())
for c in candidates:
if p["num_positions"] + len(buys) >= MAX_POSITIONS:
break
ticker = c["ticker"]
if ticker in existing_tickers:
continue
rsi = c.get("rsi")
if rsi and rsi > RSI_BUY_LIMIT:
log_decision("SKIP", ticker, f"RSI too high ({rsi:.1f} > {RSI_BUY_LIMIT})")
continue
pct_from_high = c.get("pct_from_52wk_high", 0)
if pct_from_high < NEAR_HIGH_PCT:
log_decision("SKIP", ticker, f"Too close to 52wk high ({pct_from_high:.1f}% away)")
continue
price = c["price"]
# Refresh cash from current portfolio state
current_p = game_engine.get_portfolio(game_id, username)
amount = min(position_size, max_per_position, current_p["cash"])
if amount < price:
continue
shares = int(amount / price)
if shares < 1:
continue
reason = (f"GARP signal: PE={c['trailing_pe']}, FwdPE={c['forward_pe']}, "
f"RevGr={c['revenue_growth']}%, EPSGr={c['earnings_growth']}%, RSI={rsi}")
result = game_engine.buy(game_id, username, ticker, shares, price, reason=reason)
if result["success"]:
log_entry = log_decision("BUY", ticker, reason, result)
buys.append(log_entry)
print(f" BUY {ticker}: {shares} shares @ ${price:.2f} = ${shares * price:,.2f}")
else:
log_decision("SKIP", ticker, f"Buy failed: {result.get('error', 'unknown')}")
return buys
def run_trading_logic(game_id, username, candidates=None):
"""Run full trading cycle for a player."""
print(f"\n--- Trading Logic [{username}@{game_id}] ---")
print("\nUpdating prices...")
updated = update_all_prices(game_id, username)
for ticker, price in updated:
print(f" {ticker}: ${price:.2f}")
print("\nChecking sell signals...")
sells = check_sell_signals(game_id, username)
if not sells:
print(" No sell signals")
print("\nChecking buy signals...")
buys = check_buy_signals(game_id, username, candidates)
if not buys:
print(" No buy signals")
return {"sells": sells, "buys": buys, "price_updates": len(updated)}
if __name__ == "__main__":
gid = game_engine.get_default_game_id()
if gid:
run_trading_logic(gid, "case")
else:
print("No default game found. Run game_engine.py first.")

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