Compare commits

..

1 Commits
main ... deploy

Author SHA1 Message Date
Deep Koluguri c8deeaf9e0 added tailscale 2025-12-12 20:55:24 -05:00
293 changed files with 2931 additions and 675725 deletions

View File

@ -1,6 +0,0 @@
node_modules/
frontend/node_modules/
backend/node_modules/
frontend/dist/
.git/
.env

View File

@ -1,16 +0,0 @@
name: Build Institutional Trader
on: [push]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
env:
DOCKER_HOST: tcp://172.17.0.1:2375
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Build and push
run: |
docker build -t 192.168.8.250:5000/market:latest .
docker push 192.168.8.250:5000/market:latest

View File

@ -1,148 +0,0 @@
# Backtesting Implementation Summary
## Completed Tasks
### 1. Database Schema Validation ✓
- Created `backend/scripts/validateSchema.js` script
- Validates all required tables exist
- Checks indexes on critical tables
- Verifies PRIMARY KEY constraints
- Checks for data type inconsistencies
- Can be run with: `node backend/scripts/validateSchema.js`
### 2. Historical Data Pull Configuration ✓
- Updated `yahhooscript.py`:
- Changed `LOOKBACK_DAYS_INTRADAY` from `1` to `30` (1 month)
- Enhanced `run_intraday_batched()` function to automatically select interval:
- 1-7 days: Uses 1-minute intervals
- 8-60 days: Uses 5-minute intervals (handles 30-day requirement)
- >60 days: Uses 15-minute intervals
- Handles Yahoo Finance limitations gracefully
### 3. Enhanced Backtester ✓
- Created `backend/src/services/performanceMetrics.js` with:
- Sharpe ratio calculation
- Maximum drawdown tracking
- Win/loss streak calculation
- Session-based performance (PRE/RTH/POST)
- Best/worst performing symbols
- Equity curve generation
- Updated `backend/src/services/backtester.js`:
- Integrated new performance metrics
- Enhanced price lookup with daily data fallback
- Improved error handling for missing data
### 4. Backtest API Endpoints ✓
- Enhanced `backend/src/routes/backtest.js` with:
- `POST /api/backtest/run` - Run single backtest
- `POST /api/backtest/batch` - Run multiple backtests
- `GET /api/backtest/results/:id` - Get backtest results by ID
- `POST /api/backtest/compare` - Compare multiple strategies side-by-side
- Added strategy scoring for ranking
- Maintained backward compatibility with existing endpoints
## Usage
### Running Schema Validation
```bash
cd backend
node scripts/validateSchema.js
```
### Pulling Historical Data
```bash
# Pull data for all symbols (30 days, 5-minute intervals)
python yahhooscript.py
# Pull data for a single symbol
python yahhooscript.py --only AAPL
```
### Running Backtests via API
**Single Backtest:**
```bash
curl -X POST http://localhost:3010/api/backtest/run \
-H "Content-Type: application/json" \
-d '{
"pattern": {
"minRocketScore": 5.0,
"requireTapeAlignment": true,
"session": "RTH"
},
"options": {
"lookbackDays": 30,
"exitStrategy": "target_stop",
"targetPct": 1.5,
"stopPct": 1.5
}
}'
```
**Compare Strategies:**
```bash
curl -X POST http://localhost:3010/api/backtest/compare \
-H "Content-Type: application/json" \
-d '{
"strategies": [
{
"name": "High Score RTH",
"pattern": { "rocketScoreMin": 5, "session": "RTH" }
},
{
"name": "Tape Aligned",
"pattern": { "rocketScoreMin": 3, "tapeAligned": true }
}
],
"options": {
"lookbackDays": 30
}
}'
```
## New Metrics Available
The enhanced backtester now provides:
- **Sharpe Ratio**: Risk-adjusted return metric
- **Maximum Drawdown**: Largest peak-to-trough decline
- **Win/Loss Streaks**: Consecutive wins and losses
- **Session Performance**: Separate metrics for PRE/RTH/POST sessions
- **Symbol Performance**: Best and worst performing symbols
- **Equity Curve**: Portfolio value over time
## Testing
### Quick Test Commands
```bash
# Validate database schema
cd backend
npm run validate-schema
# Test backtester functionality
npm run test-backtester
# Pull historical data (single symbol for testing)
python yahhooscript.py --only AAPL
# Pull full universe (30 days)
python yahhooscript.py
```
See `TESTING_GUIDE.md` for detailed testing instructions.
## Next Steps
1. **Run Schema Validation**: `npm run validate-schema` in backend directory
2. **Pull Historical Data**: Run `python yahhooscript.py --only AAPL` to test, then full universe
3. **Test Backtesting**: Run `npm run test-backtester` or use API endpoints
4. **Monitor Performance**: Check that queries perform well with 30 days of data
5. **Review Results**: Analyze backtest results and refine trading patterns
## Notes
- Yahoo Finance provides 1-minute data for last 7 days only
- For 30-day lookback, the script automatically uses 5-minute intervals
- The backtester falls back to daily data if intraday data is unavailable
- All API endpoints maintain backward compatibility

View File

@ -1,180 +0,0 @@
# Data Flow into Database Tables
This document explains how data flows into three key database tables: `prices_intraday_1m`, `prices_daily`, and `AlertStream_monthly`.
## 1. prices_intraday_1m Table
### Real-Time Data Source (PostgreSQL)
**Service**: `backend/src/services/bookmapWebSocketService.js`
- **Source**: Bookmap trading platform add-on
- **Method**: WebSocket server (port 3001 by default)
- **Process**:
1. Receives real-time trade data from Bookmap add-on via WebSocket
2. Aggregates trades into 1-minute OHLCV bars in memory
3. Flushes completed bars (at least 1 minute old) to PostgreSQL every 30 seconds
4. Uses `INSERT ... ON CONFLICT` to handle duplicates
- **Activation**: Enabled when `ENABLE_BOOKMAP !== 'false'` in environment
- **Direct Insert**: Writes directly to PostgreSQL `prices_intraday_1m` table
### Historical/Batch Data Source (SQLite)
**Script**: `yahhooscript.py`
- **Source**: Yahoo Finance via `yfinance` Python library
- **Method**: Batch downloads with configurable intervals
- **Process**:
1. Fetches 1-minute intraday data for configured symbol universe (S&P 500, S&P 400, Nasdaq-100, plus custom groups)
2. Processes data in batches (default: 110 symbols per batch)
3. Writes to SQLite database at `C:\Users\srk47\Desktop\options_flow.db`
4. Uses `INSERT OR REPLACE` for upserts
- **Note**: This writes to SQLite, not directly to PostgreSQL. A separate sync mechanism would be needed to transfer to PostgreSQL.
## 2. prices_daily Table
**Script**: `yahhooscript.py`
- **Source**: Yahoo Finance via `yfinance` Python library
- **Method**: Incremental daily updates
- **Process**:
1. For each symbol, checks existing data count
2. If < 10 records exist, performs full download (default: 2 years)
3. Otherwise, performs incremental update from last date minus 5 days
4. Writes to SQLite `prices_daily` table
5. Uses `INSERT ... ON CONFLICT` for upserts
- **Scheduling**: Runs in loop mode with configurable interval (`DAILY_INTERVAL_MIN`)
- **Note**: This writes to SQLite, not directly to PostgreSQL. A separate sync mechanism would be needed to transfer to PostgreSQL.
## 3. AlertStream_monthly Table
### CSV Import Method
**Script**: `backend/scripts/importCSV.js`
- **Source**: CSV files
- **Method**: Manual/scripted CSV import
- **Process**:
1. Parses CSV file with AlertStream data
2. Maps columns to both CamelCase and lowercase versions for compatibility
3. Batch inserts (100 records per batch) into PostgreSQL `AlertStream_monthly` table
4. Handles duplicate columns (Date/date, Ticker/ticker, etc.)
### BlackBox API Method
**Script**: `watch_and_upload_blackbox_postgres.py`
- **Source**: BlackBox Stocks API (`https://api.blackboxstocks.com/api/v2/options/getFlowMobile`)
- **Method**: API fetch and sync
- **Process**:
1. Fetches alert/flow data from BlackBox API (requires `BLACKBOX_API_TOKEN`)
2. Maps API response to database schema (snake_case for AlertStream_monthly)
3. Normalizes and prepares data for PostgreSQL
4. Uses chunked upserts (default: 3000 records per chunk) with `row_hash` for deduplication
5. Writes directly to PostgreSQL `AlertStream_monthly` table
- **Usage**: Run with `--table AlertStream_monthly` parameter
- **Default**: Script defaults to `OptionsFlow_monthly` table, but can target AlertStream_monthly
## Data Flow Diagram
```
┌─────────────────────────────────────────────────────────────┐
│ prices_intraday_1m │
├─────────────────────────────────────────────────────────────┤
│ │
│ Real-Time Path: │
│ Bookmap Add-on → WebSocket → bookmapWebSocketService.js │
│ → PostgreSQL (direct insert) │
│ │
│ Historical Path: │
│ Yahoo Finance → yahhooscript.py → SQLite │
│ (Note: Requires separate sync to PostgreSQL) │
│ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ prices_daily │
├─────────────────────────────────────────────────────────────┤
│ │
│ Yahoo Finance → yahhooscript.py → SQLite │
│ (Note: Requires separate sync to PostgreSQL) │
│ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ AlertStream_monthly │
├─────────────────────────────────────────────────────────────┤
│ │
│ Method 1: CSV Import │
│ CSV File → importCSV.js → PostgreSQL │
│ │
│ Method 2: API Sync │
│ BlackBox API → watch_and_upload_blackbox_postgres.py │
│ → PostgreSQL │
│ │
└─────────────────────────────────────────────────────────────┘
```
## Key Files
- **Real-time intraday prices**: `backend/src/services/bookmapWebSocketService.js` (lines 146-213)
- **Historical price data**: `yahhooscript.py` (lines 317-454)
- **CSV import**: `backend/scripts/importCSV.js` (lines 371-526)
- **API sync**: `watch_and_upload_blackbox_postgres.py` (lines 637-733)
## Important Notes
1. **SQLite vs PostgreSQL**: The `yahhooscript.py` script writes to SQLite, not directly to PostgreSQL. If you need this data in PostgreSQL, you'll need a separate sync mechanism.
2. **Bookmap Service**: The Bookmap WebSocket service writes directly to PostgreSQL and is the primary real-time source for `prices_intraday_1m`.
3. **AlertStream Sources**: AlertStream_monthly can be populated from either CSV files or the BlackBox API, depending on your data source preference.
## Configuration
### Bookmap WebSocket Service
- **Environment Variable**: `ENABLE_BOOKMAP` (default: enabled if not set to 'false')
- **Port**: `BOOKMAP_WS_PORT` (default: 3001)
- **Flush Interval**: Every 30 seconds (hardcoded in `bookmapWebSocketService.js`)
### Yahoo Finance Script
- **Database Path**: `DB_PATH` in `yahhooscript.py` (default: `C:\Users\srk47\Desktop\options_flow.db`)
- **Symbol Universe**: Configured via `SEGMENTS`, `EXTRA_SYMBOLS`, and `EXTRA_GROUPS` in `yahhooscript.py`
- **Batch Size**: `BATCH_SIZE` (default: 110)
- **Loop Mode**: `LOOP` (default: True)
### BlackBox API Sync
- **Environment Variable**: `BLACKBOX_API_TOKEN` (required)
- **PostgreSQL Connection**: Configured via environment variables:
- `POSTGRES_HOST` (default: localhost)
- `POSTGRES_PORT` (default: 5432)
- `POSTGRES_DB` (default: institutional_trader)
- `POSTGRES_USER` (default: postgres)
- `POSTGRES_PASSWORD` (default: postgres)
- **Upsert Chunk Size**: `UPSERT_CHUNK` (default: 3000)
## Usage Examples
### Running Yahoo Finance Script
```bash
# One-time run (no loop)
python yahhooscript.py
# Single symbol daily data
python yahhooscript.py --only AAPL --daily
# Single symbol intraday data
python yahhooscript.py --only AAPL --intraday
```
### Running BlackBox API Sync for AlertStream
```bash
# Sync AlertStream for today
python watch_and_upload_blackbox_postgres.py --table AlertStream_monthly
# Sync for specific date range
python watch_and_upload_blackbox_postgres.py --table AlertStream_monthly --start-date 2024-01-01 --end-date 2024-01-31
```
### Importing CSV for AlertStream
```bash
# Using the import script
node backend/scripts/importCSV.js path/to/alertstream.csv
```

View File

@ -36,10 +36,19 @@ sudo bash deploy.sh
```env
NODE_ENV=production
PORT=3000
SUPABASE_URL=your_supabase_url
SUPABASE_ANON_KEY=your_anon_key
SUPABASE_SERVICE_KEY=your_service_key
DATABASE_URL=your_database_url
# Remote PostgreSQL Database
USE_LOCAL_DB=true
LOCAL_DB_HOST=100.121.163.23
LOCAL_DB_PORT=5432
LOCAL_DB_USER=postgres
LOCAL_DB_PASSWORD=your_postgres_password
LOCAL_DB_NAME=institutional_trader
# OR use DATABASE_URL format:
# DATABASE_URL=postgresql://postgres:your_password@100.121.163.23:5432/institutional_trader
# CORS Configuration
CORS_ORIGIN=https://yourdomain.com
JWT_SECRET=your_secret_key_32_chars_min
```
@ -101,9 +110,9 @@ sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com -d api.yourdomain.c
**Option B: PostgreSQL User**
```bash
# Run the SQL script
# Connect to remote database and run the SQL script
cd /opt/institutional_trader/backend/database
sudo -u postgres psql institutional_trader < setup_friend_access.sql
psql -h 100.121.163.23 -p 5432 -U postgres -d institutional_trader < setup_friend_access.sql
# Edit the script first to set username/password!
```
@ -114,6 +123,9 @@ sudo -u postgres psql institutional_trader < setup_friend_access.sql
sudo systemctl status institutional-trader-backend
sudo systemctl status nginx
# Test database connection
psql -h 100.121.163.23 -p 5432 -U postgres -d institutional_trader
# Test health endpoint
curl https://api.yourdomain.com/health

View File

@ -1,43 +0,0 @@
# Stage 1: Build Frontend
FROM node:18-alpine AS frontend-build
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm install
COPY frontend/ ./
RUN npm run build
# Stage 2: Build Backend & Serve
FROM node:18-alpine
WORKDIR /app/backend
# Install python and build dependencies
RUN apk add --no-cache python3 py3-pip python3-dev build-base && \
python3 -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Install backend dependencies
COPY backend/package*.json ./
RUN npm install --production
# Copy backend source
COPY backend/ ./
# Install python dependencies
RUN pip install --no-cache-dir -r python_service/requirements.txt
# Copy built frontend to the public directory expected by server.js
# (__dirname is /app/backend/src, so ../../public is /app/public)
COPY --from=frontend-build /app/frontend/dist /app/public
# Set production environment
ENV NODE_ENV=production
ENV PORT=3010
EXPOSE 3010
CMD ["node", "src/server.js"]
# Cache bust 20260625222919
# Cache bust 20260625223615

View File

@ -1,428 +0,0 @@
# Institutional Flow Trading Platform - Complete Feature Documentation
## Application Overview
A professional-grade institutional trading platform that combines real-time options flow analysis, multi-timeframe price context, alert stream correlation, institutional footprint detection, and automated scoring & badge system.
---
## Feature Sequence (User Journey Flow)
### 1. DATA INGESTION & STREAMING
**Sequence Step 1: Real-time Data Collection**
- **CheddarFlow WebSocket Integration**: Streams live options flow data
- **BlackBox API Sync**: Imports historical and real-time options flow data
- **Yahoo Finance Integration**: Fetches real-time stock prices, intraday data, VWAP calculations
- **Bookmap WebSocket Service**: Provides order flow and price data
- **Alert Stream Processing**: Correlates trading alerts with options flow
- **Database Storage**: Stores all data in PostgreSQL (Supabase)
### 2. DATA PROCESSING & ENRICHMENT
**Sequence Step 2: Options Flow Processing**
- **Options Flow Processor**: Normalizes and aggregates raw flow data
- Processes Symbol, Type (CALL/PUT), Strike, Expiration, Premium, Volume
- Calculates net premium (bullish vs bearish)
- Determines moneyness (ITM/OTM/ATM)
- Aggregates by symbol and time windows
- Session bucketing (PRE/RTH/POST/OFF hours)
**Sequence Step 3: Badge Calculation**
- **Badge System**: Visual indicators for trade characteristics
- 🟢/🔴 (Round Badge): Bullish/Bearish direction based on ITM premium
- 💎 (Diamond): ITM dominance indicator
- ⭐ (Star): OTM flow indicator (>$10K premium)
- 💰 (Money): High Open Interest (>$100K)
- ✔ (Check): New positioning (Volume > Open Interest)
- ⚡ (Flash): Aggressive side sweep with high premium
- 🚀 (Rocket): Compound rocket score indicator
**Sequence Step 4: Scoring & Ranking**
- **Rocket Score Calculation** (0-100 scale):
- Premium tier scoring (0-3 points)
- Net premium imbalance (-1.5 to +1.5 points)
- Volume > OI bonus (1.2 points)
- Session weight (RTH: 1.0, POST: 0.5, PRE: 0.3)
- Catalyst flag (1 point)
- OTM bias (0.8 points)
- Tape alignment (0.5 points)
- **Momentum Score**: Price momentum calculation
- **Signal Tier Classification**: TIER_1, TIER_2, or IGNORE classification
### 3. PRICE CONTEXT ENRICHMENT
**Sequence Step 5: Price Context Service**
- **Multi-timeframe Price Data**:
- Prior day close price
- RTH (Regular Trading Hours) open price (9:30 AM ET)
- Current spot price
- 5-minute, 15-minute, 30-minute price movements
- **VWAP Calculation**: Volume-Weighted Average Price from RTH open
- **Price Reaction Tracking**:
- 5-minute reaction after signal
- 15-minute reaction after signal
- 30-minute reaction after signal
- **VWAP Distance**: Percentage distance from current price to VWAP
- **Price vs Prior Close**: Percentage change from previous close
- **Price vs RTH Open**: Percentage change from market open
### 4. INSTITUTIONAL DETECTION
**Sequence Step 6: Institutional Footprint Detection**
- **Tape Alignment**: Detects if price moves align with flow direction
- **Cluster Detection**: Identifies institutional block trades
- **Aggressor Detection**: Identifies buyer/seller initiated trades (AA/BB)
- **Large Premium Detection**: Flags trades with premium > threshold
- **Flow Trend Analysis**: Tracks flow momentum over time windows
### 5. ALERT CORRELATION
**Sequence Step 7: Alert Stream Matching**
- **Alert Service**: Matches alerts from AlertStream to options flow
- **Alert Type Detection**: News, earnings, FDA, analyst actions, etc.
- **Near Alert Flagging**: Flags flow near alert events
- **Catalyst Correlation**: Links options activity to market events
### 6. TRADE SIGNAL GENERATION
**Sequence Step 8: Automated Trade Signals**
- **Trade Signal Generator**: Converts badge patterns to actionable signals
- **Institutional FOMO (BUY)**: 🟢 + 💎 + ⭐ + 💰 + ⚡
- **Institutional Distribution (SHORT)**: 🔴 + 💎 + ⭐ + 💰
- **Trap Warning (WAIT)**: 🟢 but no tape alignment
- **Slow Accumulation (BUY)**: 🟢 + 💎 but no ⚡
- **Entry Strategy Generation**: Primary and aggressive entry strategies
- **Stop Loss Calculation**: Tight and wide stop levels
- **Profit Targets**: T1, T2, T3 targets with scaling percentages
- **Confidence Scoring**: 0-100% confidence based on badge strength
- **Trade Horizon Classification**: SCALP, INTRADAY, or SWING
### 7. FLOW ANALYSIS FEATURES
**Sequence Step 9: Flow Decay & Reversal Detection**
- **Flow Decay Detector**: Tracks when institutional flow stops
- 30+ minutes = CAUTION
- 60+ minutes = STRONG_DECAY
- Flow stopped + Price rising = FADE (retail chasing)
- Flow stopped + Price dropping = AVOID
- **Flow Reversal Detection**: Detects net premium direction flips
- **Flow Trend Analysis**: Monitors flow momentum over time windows
- **Last Flow Timestamp**: Tracks recency of flow activity
### 8. TRADE PLAN GENERATION
**Sequence Step 10: Comprehensive Trade Plans**
- **Trade Plan Generator**: Auto-generates complete trade plans
- Entry strategies (primary + aggressive)
- Stop loss levels (tight + wide)
- Profit targets (T1, T2, T3 with scaling)
- Time horizon (expected + max hold)
- Risk/Reward calculations
- Reasoning (why this setup works)
- Invalidation conditions
- **Trade Checklist**: Automated checklist for trade validation
- **Historical Win Rate**: Similar setups performance tracking
### 9. MULTI-SIGNAL SCANNER
**Sequence Step 11: Multi-Signal Scanning**
- **Signal Convergence Detection**: Flags when 3+ signals fire within 10 minutes
- Dark pool clusters
- Options flow + block prints
- 52-week highs
- Volume spikes
- ORB (Opening Range Breakouts)
- Shock tape events
- **Stealth vs Aggressive Classification**:
- SCALP: ORB + Shock Tape + high rVol (minutes to hours)
- INTRADAY: Options + Prints + RTH session (2-4 hours)
- SWING: Stealth Dark + low rVol (1-5 days)
- **False Signal Filtering**:
- Dumb money trap detection (OTM lotto tickets)
- Spoofed liquidity detection
- End-of-day noise detection (MOC rebalancing)
- **Signal Scoring**: Multi-factor scoring system
### 10. PHASE CLASSIFIER
**Sequence Step 12: Market Phase Classification**
- **Phase Classification Panel**: Classifies market phases
- **Pattern Detection**: Identifies repeatable market patterns
- **Phase-based Strategy Recommendations**
### 11. DAILY ANALYSIS & POST-MORTEM
**Sequence Step 13: Daily Performance Analysis**
- **Daily Analysis Panel**: Post-market analysis
- **Performance Tracking**: Tracks signal performance
- **Playbook Generator**: Auto-detects repeatable patterns from historical data
- FDA Approval Squeeze pattern
- Earnings Beat Fade pattern
- Options Flow Pre-Positioning pattern
- **Pattern Win Rate Calculation**: Historical win rates for patterns
- **Backtesting**: Strategy backtesting capabilities
### 12. PERFORMANCE TRACKING
**Sequence Step 14: Performance Monitoring**
- **Performance Tracking Panel**: Today's signals performance
- **Win Rate Tracking**: Tracks actual vs predicted outcomes
- **Signal Outcome Analysis**: WIN/LOSS tracking
- **Expectancy Calculation**: Risk-adjusted returns
- **Historical Performance**: Backtested strategy performance
### 13. REAL-TIME DISPLAY
**Sequence Step 15: Frontend Display & Interaction**
- **Options Flow Panel**: Main table with all flow data
- **Top Trades Summary**: Top 5 trades ranked by "best trade potential"
- **Trade Analysis Modal**: Detailed trade analysis view
- **Flow Info Panel**: Real-time flow statistics
- **Watchlist**: User-managed symbol watchlist
- **Alerts Feed**: Real-time alerts stream
- **Interactive Charts**: Price charts with flow overlay
- **WebSocket Updates**: Real-time data streaming
- **Filtering & Sorting**: Advanced filtering capabilities
- **Column Visibility**: Customizable column display
### 14. STRATEGY BACKTESTING
**Sequence Step 16: Strategy Validation**
- **Strategy Backtester**: Validates signals with historical data
- **Pattern Matching**: Finds historical matches for patterns
- **Win Rate Calculation**: Historical win rates
- **Expectancy Analysis**: Risk:Reward ratios
- **Best/Worst Setup Identification**: Optimal conditions analysis
---
## Complete Feature List (Alphabetical/Grouped)
### DATA SOURCES & INTEGRATIONS
1. **BlackBox API Integration**: Historical options flow data sync
2. **Bookmap WebSocket Service**: Order flow and price data
3. **CheddarFlow WebSocket Service**: Real-time options flow streaming
4. **Yahoo Finance Service**: Stock price data, intraday bars, VWAP calculations
5. **Alert Stream Integration**: Alert correlation with flow
6. **PostgreSQL Database**: Central data storage (Supabase)
### DATA PROCESSING
7. **Options Flow Processor**: Normalizes and aggregates raw flow data
8. **Data Normalization**: Symbol, type, strike, expiration standardization
9. **Premium Aggregation**: Bullish vs bearish premium calculation
10. **Session Bucketing**: PRE/RTH/POST/OFF hours classification
11. **Moneyness Calculation**: ITM/OTM/ATM determination
### SCORING & BADGES
12. **Badge System**: Visual indicators (🟢🔴💎⭐💰✔⚡🚀)
13. **Rocket Score**: 0-100 scoring algorithm
14. **Momentum Score**: Price momentum calculation
15. **Signal Tier Classification**: TIER_1, TIER_2, IGNORE
16. **Best Trade Score**: Composite ranking score
17. **Confidence Scoring**: 0-100% confidence ratings
### PRICE ANALYSIS
18. **Price Context Service**: Multi-timeframe price enrichment
19. **VWAP Calculation**: Volume-weighted average price
20. **Price Reaction Tracking**: 5m/15m/30m reaction analysis
21. **Prior Close Tracking**: Previous day close comparison
22. **RTH Open Tracking**: Regular trading hours open price
23. **Spot Price Tracking**: Current market price
24. **Price vs VWAP Distance**: Percentage distance calculation
25. **Support/Resistance Calculation**: Key level identification
### INSTITUTIONAL DETECTION
26. **Tape Alignment Detection**: Price-flow direction alignment
27. **Cluster Detection**: Institutional block trade identification
28. **Aggressor Detection**: Buyer/seller initiated trade detection
29. **Large Premium Detection**: High-value trade flagging
30. **Flow Trend Analysis**: Momentum tracking over time
31. **Institutional Footprint Detection**: Large player activity identification
### ALERT SYSTEM
32. **Alert Stream Processing**: Real-time alert ingestion
33. **Alert Matching Service**: Correlates alerts with flow
34. **Alert Type Detection**: News, earnings, FDA, analyst actions
35. **Near Alert Flagging**: Proximity-based alert correlation
36. **Catalyst Correlation**: Event-flow linking
### TRADE SIGNALS
37. **Trade Signal Generator**: Badge-to-signal conversion
38. **Institutional FOMO Signal**: BUY signal pattern
39. **Institutional Distribution Signal**: SHORT signal pattern
40. **Trap Warning Signal**: WAIT signal pattern
41. **Slow Accumulation Signal**: Swing trade pattern
42. **Entry Strategy Generation**: Primary and aggressive entries
43. **Stop Loss Calculation**: Tight and wide stops
44. **Profit Target Generation**: T1, T2, T3 targets
45. **Trade Horizon Classification**: SCALP/INTRADAY/SWING
### FLOW ANALYSIS
46. **Flow Decay Detection**: Tracks when flow stops
47. **Flow Reversal Detection**: Direction flip identification
48. **Flow Trend Tracking**: Momentum over time windows
49. **Last Flow Timestamp**: Recency tracking
50. **Flow Metadata**: Summary statistics
### TRADE PLANNING
51. **Trade Plan Generator**: Comprehensive trade plan creation
52. **Trade Checklist**: Automated validation checklist
53. **Risk/Reward Calculation**: Automated R:R ratios
54. **Entry Strategy Suggestions**: Primary and aggressive
55. **Exit Strategy Suggestions**: Multiple target levels
56. **Time Horizon Recommendations**: Expected hold duration
### SCANNING
57. **Multi-Signal Scanner**: Multi-factor signal detection
58. **Signal Convergence Detection**: Multiple signal alignment
59. **Stealth vs Aggressive Classification**: Trade style identification
60. **False Signal Filtering**: Noise reduction
61. **Dumb Money Trap Detection**: Retail noise filtering
62. **Spoofed Liquidity Detection**: Fake flow identification
63. **End-of-Day Noise Detection**: MOC filtering
64. **Scanner Enhancement Service**: Advanced scanning logic
### MARKET ANALYSIS
65. **Phase Classifier**: Market phase identification
66. **Pattern Detection**: Repeatable pattern identification
67. **Daily Analysis Panel**: Post-market analysis
68. **Performance Tracking Panel**: Signal outcome tracking
69. **Playbook Generator**: Pattern extraction from history
70. **FDA Approval Squeeze Pattern**: Specific pattern detection
71. **Earnings Beat Fade Pattern**: Specific pattern detection
72. **Options Flow Pre-Positioning Pattern**: Specific pattern detection
### BACKTESTING & VALIDATION
73. **Strategy Backtester**: Historical validation
74. **Pattern Matching**: Historical pattern finding
75. **Win Rate Calculation**: Success rate analysis
76. **Expectancy Analysis**: Risk-adjusted returns
77. **Best Setup Identification**: Optimal conditions
78. **Worst Setup Identification**: Suboptimal conditions
79. **Historical Win Rate Tracking**: Similar setups performance
### USER INTERFACE
80. **Options Flow Panel**: Main data table
81. **Top Trades Summary**: Top 5 ranked trades
82. **Trade Analysis Modal**: Detailed trade view
83. **Flow Info Panel**: Real-time statistics
84. **Watchlist**: Symbol tracking
85. **Alerts Feed**: Real-time alerts display
86. **Price Charts**: Interactive price visualization
87. **Flow Heatmap**: Visual flow representation
88. **Filtering System**: Advanced filtering
89. **Sorting System**: Multi-column sorting
90. **Column Visibility Toggle**: Customizable columns
91. **Row Expansion**: Detailed inline view
92. **Symbol Tooltips**: Quick info on hover
93. **Score Breakdown Tooltips**: Score component details
### REAL-TIME FEATURES
94. **WebSocket Streaming**: Real-time data updates
95. **Live Updates**: Auto-refresh capabilities
96. **Real-time Flow Streaming**: Live options flow
97. **Real-time Price Updates**: Live price streaming
98. **Real-time Alert Streaming**: Live alert feed
### DATA MANAGEMENT
99. **CSV Import**: Historical data import
100. **Data Validation**: Data quality checks
101. **Time Format Fixing**: Time normalization
102. **Database Indexing**: Performance optimization
103. **Query Optimization**: Fast data retrieval
### TECHNICAL INFRASTRUCTURE
104. **Hybrid Architecture**: Node.js + Python services
105. **FastAPI Service**: Python data processing service
106. **Express.js API**: Node.js REST API
107. **PostgreSQL Database**: Central data store
108. **React Frontend**: Modern UI framework
109. **WebSocket Support**: Real-time communication
110. **Health Monitoring**: Service health checks
111. **Automatic Fallback**: SQL fallback when Python unavailable
112. **Error Handling**: Comprehensive error management
113. **Logging System**: Detailed logging
114. **Performance Monitoring**: Response time tracking
### ADVANCED FEATURES
115. **AI Analysis Integration**: AI-powered insights
116. **Symbol Normalization**: Symbol format standardization
117. **Timezone Handling**: Multi-timezone support (ET/CT)
118. **Rate Limiting**: API rate limit management
119. **Caching**: Data caching for performance
120. **Batch Processing**: Efficient bulk operations
---
## Feature Count Summary
**Total Features: 120+**
### By Category:
- **Data Sources & Integrations**: 6 features
- **Data Processing**: 5 features
- **Scoring & Badges**: 6 features
- **Price Analysis**: 8 features
- **Institutional Detection**: 6 features
- **Alert System**: 5 features
- **Trade Signals**: 9 features
- **Flow Analysis**: 5 features
- **Trade Planning**: 6 features
- **Scanning**: 8 features
- **Market Analysis**: 8 features
- **Backtesting & Validation**: 7 features
- **User Interface**: 14 features
- **Real-time Features**: 5 features
- **Data Management**: 5 features
- **Technical Infrastructure**: 11 features
- **Advanced Features**: 6 features
---
## Key User Workflows
### Workflow 1: Real-time Trading
1. Stream real-time options flow
2. See badges and rocket scores auto-calculate
3. View top trades in summary cards
4. Click trade for detailed analysis
5. Review trade plan with entry/stop/targets
6. Execute trade based on signal
### Workflow 2: Multi-Signal Scanning
1. Run multi-signal scanner
2. View convergence events
3. Filter by trade horizon (SCALP/INTRADAY/SWING)
4. Check false signal warnings
5. Review scanner results
6. Add promising symbols to watchlist
### Workflow 3: Daily Analysis
1. Review daily analysis panel
2. Check performance tracking
3. View playbook patterns
4. Analyze win rates
5. Adjust strategies based on patterns
### Workflow 4: Strategy Development
1. Identify pattern
2. Backtest pattern
3. Review win rate and expectancy
4. Generate trade plan template
5. Monitor performance
6. Refine strategy
---
## Technology Stack
### Frontend
- React.js
- Vite
- Tailwind CSS
- Lightweight Charts
- WebSocket Client
### Backend
- Node.js + Express.js
- Python + FastAPI
- PostgreSQL (Supabase)
- WebSocket Server
### Data Sources
- CheddarFlow API
- BlackBox API
- Yahoo Finance API
- Bookmap WebSocket
- Alert Stream Data
---
*Last Updated: Based on current codebase analysis*

View File

@ -1,206 +0,0 @@
# Implementation Complete: Historical Data & Backtesting Setup
## ✅ All Tasks Completed
### 1. Database Schema Validation ✓
**Status:** Complete
**Files Created:**
- `backend/scripts/validateSchema.js` - Comprehensive schema validation script
**Features:**
- Validates all 12 required tables exist
- Checks indexes on critical tables (prices_intraday_1m, prices_daily, OptionsFlow_monthly)
- Verifies PRIMARY KEY constraints
- Detects data type inconsistencies
- Provides clear error messages and warnings
**Run:** `npm run validate-schema` (in backend directory)
### 2. Historical Data Pull (1 Month) ✓
**Status:** Complete
**Files Modified:**
- `yahhooscript.py` - Updated for 30-day lookback with smart interval selection
**Changes:**
- `LOOKBACK_DAYS_INTRADAY` changed from `1` to `30`
- Automatic interval selection:
- 1-7 days: 1-minute intervals (Yahoo Finance limit)
- 8-60 days: 5-minute intervals (handles 30-day requirement)
- >60 days: 15-minute intervals
- Handles Yahoo Finance API limitations gracefully
**Run:**
- Test: `python yahhooscript.py --only AAPL`
- Full: `python yahhooscript.py`
### 3. Enhanced Backtester ✓
**Status:** Complete
**Files Created:**
- `backend/src/services/performanceMetrics.js` - New metrics module
**Files Modified:**
- `backend/src/services/backtester.js` - Enhanced with new metrics
**New Metrics:**
- ✅ Sharpe Ratio (risk-adjusted returns)
- ✅ Maximum Drawdown (peak-to-trough decline)
- ✅ Win/Loss Streaks (consecutive wins/losses)
- ✅ Session Performance (PRE/RTH/POST breakdown)
- ✅ Symbol Performance (best/worst performers)
- ✅ Equity Curve (portfolio value over time)
**Improvements:**
- Daily data fallback when intraday unavailable
- Better error handling for missing data
- Support for 30-day historical analysis
### 4. Backtest API Endpoints ✓
**Status:** Complete
**Files Modified:**
- `backend/src/routes/backtest.js` - Enhanced with new endpoints
**New Endpoints:**
- ✅ `POST /api/backtest/run` - Run single backtest
- ✅ `POST /api/backtest/batch` - Run multiple backtests
- ✅ `GET /api/backtest/results/:id` - Get results by ID
- ✅ `POST /api/backtest/compare` - Compare strategies side-by-side
**Features:**
- Strategy scoring for ranking
- Result storage (in-memory, can be upgraded to database)
- Backward compatibility maintained
- Comprehensive error handling
### 5. Testing Infrastructure ✓
**Status:** Complete
**Files Created:**
- `backend/scripts/testBacktester.js` - Comprehensive test script
- `TESTING_GUIDE.md` - Detailed testing documentation
**Test Script Features:**
- Database connectivity check
- Price data availability verification
- Sample backtest execution
- Metrics validation
**Run:** `npm run test-backtester` (in backend directory)
## 📁 Files Created/Modified
### New Files
1. `backend/scripts/validateSchema.js`
2. `backend/src/services/performanceMetrics.js`
3. `backend/scripts/testBacktester.js`
4. `BACKTESTING_IMPLEMENTATION_SUMMARY.md`
5. `TESTING_GUIDE.md`
6. `IMPLEMENTATION_COMPLETE.md` (this file)
### Modified Files
1. `yahhooscript.py` - Updated for 30-day data pull
2. `backend/src/services/backtester.js` - Enhanced with new metrics
3. `backend/src/routes/backtest.js` - New API endpoints
4. `backend/package.json` - Added npm scripts
## 🚀 Quick Start
### 1. Validate Schema
```bash
cd backend
npm run validate-schema
```
### 2. Pull Historical Data (Test)
```bash
python yahhooscript.py --only AAPL
```
### 3. Test Backtester
```bash
cd backend
npm run test-backtester
```
### 4. Pull Full Universe
```bash
python yahhooscript.py
```
### 5. Test API
```bash
# Start server
cd backend
npm run dev
# In another terminal, test endpoint
curl -X POST http://localhost:3010/api/backtest/run \
-H "Content-Type: application/json" \
-d '{"pattern": {"rocketScoreMin": 5}, "options": {"lookbackDays": 30}}'
```
## 📊 What's New
### Enhanced Metrics
All backtest results now include:
- **Sharpe Ratio**: Measures risk-adjusted returns
- **Max Drawdown**: Largest portfolio decline
- **Streaks**: Win/loss patterns
- **Session Analysis**: Performance by trading session
- **Symbol Ranking**: Best and worst performers
### Smart Data Handling
- Automatically uses 5-minute intervals for 30-day lookback
- Falls back to daily data when intraday unavailable
- Handles missing data gracefully
### Improved API
- Strategy comparison endpoint
- Batch backtesting
- Result storage and retrieval
- Strategy scoring and ranking
## ✅ Success Criteria Met
- [x] All database tables exist and have proper indexes
- [x] 1 month of historical data pull configured (30 days, 5-minute intervals)
- [x] Backtester enhanced with new metrics
- [x] API endpoints created and functional
- [x] Testing infrastructure in place
- [x] Documentation complete
## 📝 Next Steps
1. **Run Schema Validation** - Ensure database is ready
2. **Pull Historical Data** - Start with single symbol, then full universe
3. **Test Backtesting** - Run test script and API endpoints
4. **Analyze Results** - Review backtest outputs and refine patterns
5. **Monitor Performance** - Ensure queries perform well with 30 days of data
## 🔍 Verification Checklist
Before considering implementation complete, verify:
- [ ] Schema validation script runs without errors
- [ ] Historical data pull works for at least one symbol
- [ ] Backtester test script completes successfully
- [ ] API endpoints return valid responses
- [ ] All new metrics are calculated correctly
- [ ] Data quality checks pass
## 📚 Documentation
- **Implementation Summary**: `BACKTESTING_IMPLEMENTATION_SUMMARY.md`
- **Testing Guide**: `TESTING_GUIDE.md`
- **Plan Reference**: `.cursor/plans/historical_data_&_backtesting_setup_c9b61eee.plan.md`
## 🎯 Implementation Status
**All planned tasks completed successfully!**
The system is now ready to:
- Pull 1 month of historical data from Yahoo Finance
- Run comprehensive backtests with enhanced metrics
- Compare multiple trading strategies
- Analyze performance across different sessions and symbols
Ready for production use after data pull and testing.

View File

@ -1,317 +0,0 @@
# Implementation Roadmap - Quick Reference
## File-by-File Implementation Guide
### Phase 1: Critical Features (Start Here)
#### 1. Price Reaction Tracking
**New File:** `backend/python_service/services/price_reaction_tracker.py`
- Class: `PriceReactionTracker`
- Method: `track_reaction(flow_row, pool)` → returns dict with 5m/15m/30m reactions
- Integration: Call in `main.py` after `enrich_flow_with_prices()`
**Modify:** `backend/python_service/main.py`
```python
# After line 145 (after price enrichment):
from services.price_reaction_tracker import PriceReactionTracker
reaction_tracker = PriceReactionTracker()
df_final = await reaction_tracker.enrich_with_reactions(df_final, pool)
```
---
#### 2. VWAP Integration
**Modify:** `backend/python_service/services/price_context.py`
- Add method: `async def calculate_vwap_at_time(symbol, timestamp, pool)`
- Add method: `async def get_vwap_for_date(symbol, date, pool)`
- Integration: Call in `enrich_flow_with_prices()` method
**Add to enrichment:**
```python
# In enrich_flow_with_prices(), add:
vwap_data = await self.get_vwap_at_time(symbol, flow_ts_utc, pool)
df['vwap_at_signal'] = vwap_data['vwap']
df['price_vs_vwap_pct'] = ((df['u_close'] - df['vwap_at_signal']) / df['vwap_at_signal']) * 100
```
---
#### 3. Signal Tier Classification
**New File:** `backend/python_service/services/signal_tier_classifier.py`
- Class: `SignalTierClassifier`
- Method: `classify_tier(row)` → returns 'TIER_1', 'TIER_2', or 'IGNORE'
**Modify:** `backend/python_service/services/options_flow_processor.py`
- Add method: `process_tier_classification(df)` → adds `signal_tier` column
- Call in `process()` method after `process_badges()`
**Integration:**
```python
# In process() method, after process_badges():
df = self.process_tier_classification(df)
```
---
#### 4. Trade Checklist
**New File:** `backend/python_service/services/trade_checklist.py`
- Class: `TradeChecklist`
- Method: `evaluate(flow_row)` → returns checklist score and details
**Modify:** `backend/python_service/main.py`
- After all enrichments, add checklist evaluation:
```python
from services.trade_checklist import TradeChecklist
checklist = TradeChecklist()
df_final['checklist_result'] = df_final.apply(
lambda row: checklist.evaluate(row), axis=1
)
df_final['checklist_score'] = df_final['checklist_result'].apply(lambda x: x['checklist_score'])
df_final['checklist_passed'] = df_final['checklist_result'].apply(lambda x: x['checklist_passed'])
```
---
### Phase 2: High Value Features
#### 5. Strike Clustering
**New File:** `backend/python_service/services/strike_cluster_detector.py`
- Class: `StrikeClusterDetector`
- Method: `detect_clusters(df, window_minutes=30)` → adds cluster flags
**Integration:** Call in `main.py` after aggregations
---
#### 6. Delta Weighting
**Modify:** `backend/python_service/services/options_flow_processor.py`
- Add method: `calculate_delta_weighted_value(row)`
- Add to `process_aggregations()` or create new method `process_delta_weighting()`
---
#### 7. Index Correlation
**New File:** `backend/python_service/services/index_correlation.py`
- Class: `IndexCorrelationService`
- Method: `check_index_alignment(flow_row, pool)` → returns alignment data
**Integration:** Call in `main.py` after price enrichment
---
### Phase 3: Advanced Features
#### 8. Gamma Exposure
**New File:** `backend/python_service/services/gamma_calculator.py`
- Class: `GammaCalculator`
- Method: `calculate_gex(df)` → adds GEX columns
**Note:** Requires options pricing library (e.g., `py_vollib` or simplified approximation)
---
#### 9. Sweep vs Block Detection
**New File:** `backend/python_service/services/trade_type_detector.py`
- Class: `TradeTypeDetector`
- Method: `detect_trade_type(df)` → adds trade_type column
---
#### 10. DTE Buckets
**Modify:** `backend/python_service/services/options_flow_processor.py`
- Add method: `calculate_dte_bucket(row)`
- Add to `process_moneyness()` or create new method
---
### Phase 4: Analytics
#### 11. Historical Win Rate
**New File:** `backend/python_service/services/pattern_analyzer.py`
- Class: `PatternAnalyzer`
- Methods: `track_pattern()`, `get_pattern_stats()`
**Database:** Create table `signal_patterns_history`
---
#### 12. Enhanced Entry/Exit Logic
**Modify:** `backend/src/services/tradePlanGenerator.js`
- Enhance `generateEntryStrategy()` with VWAP logic
- Enhance `generateExitStrategy()` with flow-based exits
---
## Database Migrations
### Migration 1: Add Enrichment Columns
```sql
ALTER TABLE processed_options_flow ADD COLUMN IF NOT EXISTS
signal_tier VARCHAR(10),
is_tradeable BOOLEAN,
vwap_at_signal NUMERIC,
price_vs_vwap_pct NUMERIC,
price_reaction_5m_pct NUMERIC,
price_reaction_15m_pct NUMERIC,
flow_led_to_move BOOLEAN,
checklist_score INTEGER,
checklist_passed BOOLEAN;
```
### Migration 2: Add Pattern Tracking Table
```sql
CREATE TABLE IF NOT EXISTS signal_patterns_history (
id SERIAL PRIMARY KEY,
pattern_hash VARCHAR(100),
signal_time TIMESTAMPTZ,
symbol VARCHAR(10),
price_at_signal NUMERIC,
price_5m_after NUMERIC,
price_15m_after NUMERIC,
outcome VARCHAR(20),
return_pct NUMERIC,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_pattern_hash ON signal_patterns_history(pattern_hash);
CREATE INDEX idx_signal_time ON signal_patterns_history(signal_time);
```
---
## API Endpoint Additions
### Modify: `backend/python_service/main.py`
Add new endpoints:
```python
@app.get("/api/options-flow/enhanced")
async def get_enhanced_flow(...):
# Same as existing endpoint but with all enrichments enabled
pass
@app.get("/api/options-flow/tier-1")
async def get_tier1_signals(...):
# Filter to only Tier-1 signals
df_final = df_final[df_final['signal_tier'] == 'TIER_1']
pass
@app.get("/api/options-flow/checklist-passed")
async def get_checklist_passed(...):
# Filter to only checklist-passed signals
df_final = df_final[df_final['checklist_passed'] == True]
pass
```
---
## Testing Checklist
### Unit Tests to Add
1. **Price Reaction Tests**
- `test_price_reaction_5m_positive()`
- `test_price_reaction_no_move()`
- `test_flow_led_to_move_detection()`
2. **Tier Classification Tests**
- `test_tier1_classification()`
- `test_tier2_classification()`
- `test_ignore_classification()`
3. **Checklist Tests**
- `test_checklist_5_5_passes()`
- `test_checklist_4_5_passes()`
- `test_checklist_3_5_fails()`
4. **VWAP Tests**
- `test_vwap_calculation()`
- `test_vwap_pullback_detection()`
- `test_vwap_reclaim_detection()`
---
## Performance Considerations
### Optimization Tips
1. **Price Reaction Tracking**
- Batch fetch prices for all signals at once
- Use async queries with connection pooling
- Cache VWAP calculations per symbol/date
2. **Strike Clustering**
- Use pandas groupby operations (already efficient)
- Consider windowing for large datasets
3. **Index Correlation**
- Cache SPY/QQQ flow data (update every minute)
- Use materialized views for index flow aggregations
4. **Gamma Calculation**
- Use simplified approximation (no full Black-Scholes)
- Pre-calculate common strikes
---
## Rollout Strategy
### Week 1: Phase 1 (Critical)
- Day 1-2: Price Reaction Tracking
- Day 3-4: VWAP Integration
- Day 5: Signal Tier Classification
- Day 6-7: Trade Checklist
### Week 2: Phase 2 (High Value)
- Day 1-2: Strike Clustering
- Day 3: Delta Weighting
- Day 4-5: Index Correlation
### Week 3: Phase 3 (Advanced)
- Day 1-2: Gamma Exposure
- Day 3: Sweep vs Block
- Day 4: DTE Buckets
### Week 4: Phase 4 (Analytics)
- Day 1-3: Historical Win Rate Tracking
- Day 4-5: Enhanced Entry/Exit Logic
- Day 6-7: Testing & Refinement
---
## Monitoring & Metrics
### Key Metrics to Track
1. **Signal Quality**
- Tier-1 signal percentage
- Checklist pass rate
- Price reaction success rate
2. **Trade Performance**
- Win rate by tier
- Win rate by checklist score
- Average return by pattern
3. **System Performance**
- Enrichment processing time
- Database query performance
- API response times
---
## Next Steps
1. ✅ Review this roadmap
2. ✅ Prioritize features based on your needs
3. ✅ Start with Phase 1 (Price Reaction + VWAP + Tier + Checklist)
4. ✅ Test each feature before moving to next
5. ✅ Monitor metrics and refine
---
**Remember:** Don't change existing code - extend it with new services and enrichments!

View File

@ -1,164 +0,0 @@
# Phase 1 Data Sources
This document explains where each Phase 1 feature gets its data from.
## Data Flow Overview
```
PostgreSQL Database
OptionsFlow_monthly table (raw options flow data)
OptionsFlowProcessor (normalizes, calculates badges, aggregates)
PriceContextService (adds price context, VWAP)
AlertService (matches alerts)
Phase 1 Services (Signal Tier, Price Reaction, Checklist)
Final Output
```
## 1. Initial Data Source
**Location:** `backend/python_service/main.py` (lines 115-123)
```python
SELECT *
FROM "OptionsFlow_monthly"
WHERE "Premium" IS NOT NULL
AND TRIM("Premium"::text) <> ''
AND "StockEtf" = 'STOCK'
AND "Symbol" NOT IN ('TSLA', 'NVDA')
```
**What it provides:**
- Raw options flow records from the `OptionsFlow_monthly` table
- All columns from the table (Symbol, Premium, Strike, Expiration, etc.)
## 2. Signal Tier Classification
**Service:** `backend/python_service/services/signal_tier_classifier.py`
**Data Sources:**
- **Badges:** From `OptionsFlowProcessor` (calculated from flow data)
- `badge_round`: 🟢 or 🔴 (from direction and net premium)
- `badge_more`: 💎⭐ (from premium thresholds, volume/OI ratios)
- **Premium:** From processed flow data
- `premium_num`: Total premium for the signal
- `bull_total`, `bear_total`: Bullish vs bearish premium
- `prem_cb_itm`, `prem_ps_itm`, `prem_cs_itm`, `prem_pb_itm`: ITM premium breakdown
- **Direction:** From `OptionsFlowProcessor`
- `direction`: 'BULL' or 'BEAR'
- **Volume/OI:** From raw flow data
- `vol_num`: Volume
- `oi_num`: Open Interest
**Database Tables Used:**
- ✅ `OptionsFlow_monthly` (via OptionsFlowProcessor)
- ❌ No direct database queries
## 3. Price Reaction Tracking
**Service:** `backend/python_service/services/price_reaction_tracker.py`
**Data Sources:**
- **Signal Time:** From processed flow data
- `flow_ts_utc`: Timestamp of the signal
- `symbol_norm`: Normalized symbol
- **Price at Signal:** From `PriceContextService`
- `u_close`: Price at the time of the signal
**Database Tables Used:**
- ✅ `prices_intraday_1m` (queried directly)
- Gets price at 5m, 15m, 30m after signal time
- Query: `SELECT close FROM prices_intraday_1m WHERE symbol = $1 AND ts <= $2 ORDER BY ts DESC LIMIT 1`
**Calculation:**
```python
reaction_5m = ((price_5m - price_at_signal) / price_at_signal) * 100
flow_led_to_move = abs(reaction_5m) > 0.5 # 0.5% threshold
```
## 4. Trade Checklist
**Service:** `backend/python_service/services/trade_checklist.py`
**Data Sources:**
- **Badges:** From `OptionsFlowProcessor`
- `badge_round`: 🟢 or 🔴
- `badge_more`: 💎⭐
- **VWAP:** From `PriceContextService`
- `vwap_at_signal`: VWAP at the time of the signal
- `price_vs_vwap_pct`: Percentage distance from VWAP
- **Index Alignment:** From processed flow data (if available)
- `index_aligned`: Boolean indicating if index confirms
**Database Tables Used:**
- ✅ `prices_intraday_1m` (via PriceContextService for VWAP calculation)
- ✅ `prices_daily` (via PriceContextService for prior close)
**Checklist Items:**
1. ✅ Has direction (🟢 or 🔴)
2. ✅ Has diamond (💎)
3. ✅ Has star (⭐)
4. ✅ Price respects VWAP (within ±2%)
5. ✅ Index confirms (if available)
## 5. VWAP Calculation
**Service:** `backend/python_service/services/price_context.py`
**Data Sources:**
- **RTH Open:** From `prices_intraday_1m`
- Gets first bar at 9:30 AM CST for the trading day
- **Price Data:** From `prices_intraday_1m`
- Gets all 1-minute bars from RTH open to signal time
- Calculates: `VWAP = Σ(price × volume) / Σ(volume)`
**Database Tables Used:**
- ✅ `prices_intraday_1m` (queried directly)
- Query for RTH open: `SELECT open FROM prices_intraday_1m WHERE symbol = $1 AND date = $2 AND time >= '09:30:00' ORDER BY ts ASC LIMIT 1`
- Query for VWAP: `SELECT close, volume FROM prices_intraday_1m WHERE symbol = $1 AND ts >= $2 AND ts <= $3 ORDER BY ts ASC`
## Summary Table
| Phase 1 Feature | Primary Data Source | Database Tables | Direct DB Queries? |
|----------------|---------------------|-----------------|-------------------|
| **Signal Tier** | OptionsFlowProcessor (badges, premium) | OptionsFlow_monthly (via processor) | ❌ No |
| **Price Reaction** | prices_intraday_1m | prices_intraday_1m | ✅ Yes |
| **Trade Checklist** | OptionsFlowProcessor (badges) + PriceContextService (VWAP) | OptionsFlow_monthly, prices_intraday_1m | ✅ Yes (via PriceContextService) |
| **VWAP** | prices_intraday_1m | prices_intraday_1m | ✅ Yes |
## Key Dependencies
1. **OptionsFlowProcessor** must run first to calculate badges and normalize data
2. **PriceContextService** must run before Phase 1 to provide VWAP and price context
3. **Price Reaction** requires `prices_intraday_1m` to have data for the signal time + 5/15/30 minutes
4. **VWAP** requires `prices_intraday_1m` to have data from RTH open (9:30 AM CST) to signal time
## Common Issues
### "Not calculated" for all fields
- **Cause:** Python service not running, or data coming from SQL fallback
- **Solution:** Ensure Python service is running at `http://localhost:8010`
### Price Reaction shows "Not calculated"
- **Cause:**
- Historical data (future dates have no price data)
- Missing price data in `prices_intraday_1m` for the time period
- Signal time is invalid
- **Solution:** Check that `prices_intraday_1m` has data for the signal date and time
### VWAP shows "Not calculated"
- **Cause:**
- Signal occurred before RTH open (9:30 AM CST)
- Missing price data in `prices_intraday_1m` for the trading day
- Historical/future dates
- **Solution:** VWAP is only available during RTH hours (9:30 AM - 4:00 PM CST)
### Signal Tier shows "Not calculated"
- **Cause:** Python service not running (this should always work if service is running)
- **Solution:** Check Python service logs for errors in `SignalTierClassifier`

View File

@ -1,318 +0,0 @@
# Phase 1 Data & Filters - Complete Explanation
## 🎯 What is Phase 1?
Phase 1 adds **4 critical features** to help you identify the **best trading signals** and **filter out noise** (hedges, rolls, low-quality signals).
---
## 📊 Phase 1 Data Fields
### 1. **Signal Tier Classification** (`signal_tier`)
**What it does:** Classifies every signal into one of three tiers based on quality.
**Values:**
- **`TIER_1`** 🥇 = **Tradeable alone** (highest quality)
- **`TIER_2`** 🥈 = **Needs confirmation** (wait 10-30 minutes, might become Tier-1)
- **`IGNORE`** 🚫 = **Don't trade** (noise, hedges, or low quality)
**How it works:**
```
TIER_1 = 🟢/🔴 + 💎 + ⭐ + premium > $500K + direction aligned
TIER_2 = 🟢 + 💎 (no ⭐) OR ⭐ without 💎
IGNORE = OTM-only, mixed signals, low volume/OI ratio
```
**Why it matters:**
- **Tier-1 signals** are institutions positioning (not hedging)
- **Tier-2 signals** often become Tier-1 later
- **Ignore signals** are likely hedges/rolls - don't waste time on them
---
### 2. **Price Reaction Tracking** (`price_reaction_5m_pct`, `flow_led_to_move`)
**What it does:** Tracks how price moves **AFTER** the signal appears.
**Fields:**
- `price_reaction_5m_pct` = Price change 5 minutes after signal (%)
- `price_reaction_15m_pct` = Price change 15 minutes after signal (%)
- `flow_led_to_move` = Boolean: Did flow lead to >0.5% price move?
**How it works:**
```
Signal appears at 10:00 AM → Price = $100
5 minutes later (10:05 AM) → Price = $101.25
price_reaction_5m_pct = +1.25%
If |price_reaction_5m_pct| > 0.5% → flow_led_to_move = true
```
**Why it matters:**
- **Flow WITH price reaction** = Real positioning (trade it!)
- **Flow WITHOUT price reaction** = Hedge or roll (ignore it!)
**Example:**
- Signal shows 🟢💎⭐ with $1M premium
- But price doesn't move → It's a hedge, not real positioning
- Filter it out!
---
### 3. **VWAP Integration** (`vwap_at_signal`, `price_vs_vwap_pct`)
**What it does:** Calculates VWAP (Volume Weighted Average Price) and shows distance from it.
**Fields:**
- `vwap_at_signal` = VWAP value at signal time
- `price_vs_vwap_pct` = Percentage distance from VWAP
**How it works:**
```
VWAP = Average price weighted by volume (from 9:30 AM to signal time)
price_vs_vwap_pct = ((Current Price - VWAP) / VWAP) * 100
Example:
- VWAP = $100
- Current Price = $101
- price_vs_vwap_pct = +1.0%
```
**Why it matters:**
- **Price near VWAP** = Good entry opportunity
- **Price far from VWAP** (>2%) = Wait for pullback
- **VWAP pullback/reclaim** = Best entry strategy
**Trading Logic:**
- ✅ **Best entry:** VWAP pullback or VWAP reclaim
- ✅ **Good entry:** Break & hold above prior high
- ❌ **Avoid:** Chasing vertical candles (price too extended)
---
### 4. **Trade Checklist** (`checklist_score`, `checklist_passed`)
**What it does:** Evaluates signals against a 5-point checklist. Requires 4/5 to pass.
**Checklist Items:**
1. ✅ Has direction (🟢 or 🔴)
2. ✅ Has diamond (💎)
3. ✅ Has star (⭐)
4. ✅ Price respects VWAP (within ±2%)
5. ✅ Index confirms (SPY/QQQ alignment - placeholder for Phase 2)
**Fields:**
- `checklist_score` = Score out of 5 (0-5)
- `checklist_passed` = Boolean: True if score >= 4
- `checklist_details` = Object showing which checks passed/failed
**How it works:**
```
Example:
✅ Has direction (🟢) = 1 point
✅ Has diamond (💎) = 1 point
✅ Has star (⭐) = 1 point
✅ Price respects VWAP = 1 point
❌ Index confirms = 0 points (not implemented yet)
Total: 4/5 → checklist_passed = true
```
**Why it matters:**
- **Prevents bad trades** - Only trade signals that pass 4/5 checks
- **Quality filter** - Ensures multiple conditions are met
- **Reduces false signals** - Filters out incomplete setups
---
## 🔍 Phase 1 Filters
### Filter 1: **🥇 Tier-1 Only**
**What it does:** Shows only Tier-1 signals (highest quality, tradeable alone).
**When to use:**
- When you want **only the best signals**
- When you're **short on time** - focus on Tier-1 only
- When you want **institutional positioning** (not hedges)
**What it filters:**
- ✅ Shows: Tier-1 signals only
- ❌ Hides: Tier-2 and Ignore signals
**Example:**
```
Without filter: 50 signals (10 Tier-1, 20 Tier-2, 20 Ignore)
With filter: 10 signals (only Tier-1)
```
---
### Filter 2: **✅ Checklist Passed**
**What it does:** Shows only signals that passed the 4/5 checklist.
**When to use:**
- When you want **validated trades** only
- When you want to **avoid incomplete setups**
- When you want **multiple confirmations**
**What it filters:**
- ✅ Shows: Signals with checklist_score >= 4
- ❌ Hides: Signals with checklist_score < 4
**Example:**
```
Without filter: 50 signals (30 passed, 20 failed)
With filter: 30 signals (only passed)
```
---
### Filter 3: **📈 Flow Led to Move**
**What it does:** Shows only signals where flow led to actual price movement (>0.5%).
**When to use:**
- When you want **real positioning** (not hedges)
- When you want to **filter out noise**
- When you want **signals that actually moved price**
**What it filters:**
- ✅ Shows: Signals where price moved >0.5% after signal
- ❌ Hides: Signals where price didn't move (likely hedges/rolls)
**Example:**
```
Without filter: 50 signals (20 moved price, 30 didn't)
With filter: 20 signals (only ones that moved price)
```
---
## 🎯 Combined Filter Strategy
### Best Signals (All 3 Filters ON):
```
🥇 Tier-1 Only + ✅ Checklist Passed + 📈 Flow Led to Move
Result: Only the highest quality signals that:
- Are Tier-1 (tradeable alone)
- Passed 4/5 checklist
- Led to actual price movement
Example: 50 signals → 5 signals (only the best!)
```
### Conservative Strategy:
```
🥇 Tier-1 Only + ✅ Checklist Passed
Result: High-quality validated signals
(Even if price hasn't moved yet - might be early)
```
### Aggressive Strategy:
```
📈 Flow Led to Move
Result: Any signal that moved price
(Includes Tier-2 if they moved price)
```
---
## 📈 Real-World Example
### Scenario: You see a signal
**Signal Details:**
- Symbol: AAPL
- Badges: 🟢💎⭐
- Premium: $750K
- Time: 10:00 AM
- Price: $150
**Phase 1 Data:**
- `signal_tier` = `TIER_1`
- `checklist_score` = `4`
- `checklist_passed` = `true`
- `price_reaction_5m_pct` = `+1.2%`
- `flow_led_to_move` = `true`
- `vwap_at_signal` = `$149.50`
- `price_vs_vwap_pct` = `+0.33%`
**Analysis:**
- ✅ Tier-1 signal (highest quality)
- ✅ Passed checklist (4/5)
- ✅ Flow led to price move (+1.2% in 5 minutes)
- ✅ Price near VWAP (good entry)
**Decision:** **TRADE IT!** This is a high-quality signal with multiple confirmations.
---
### Scenario: Another signal
**Signal Details:**
- Symbol: MSFT
- Badges: 🟢💎 (no ⭐)
- Premium: $200K
- Time: 11:00 AM
- Price: $300
**Phase 1 Data:**
- `signal_tier` = `TIER_2` ⚠️
- `checklist_score` = `3`
- `checklist_passed` = `false`
- `price_reaction_5m_pct` = `+0.1%`
- `flow_led_to_move` = `false`
- `vwap_at_signal` = `$299.50`
- `price_vs_vwap_pct` = `+0.17%`
**Analysis:**
- ⚠️ Tier-2 signal (needs confirmation)
- ❌ Failed checklist (3/5)
- ❌ Flow didn't lead to price move (only +0.1%)
- ✅ Price near VWAP
**Decision:** **WAIT or IGNORE** - Not enough confirmations, price didn't react.
---
## 🎯 Summary
### Phase 1 Data Helps You:
1. **Identify quality** - Tier-1 vs Tier-2 vs Ignore
2. **Filter hedges** - Flow without price reaction = hedge
3. **Find entries** - VWAP distance shows entry opportunities
4. **Validate trades** - Checklist ensures multiple conditions met
### Phase 1 Filters Help You:
1. **Focus on best signals** - Tier-1 only
2. **Avoid bad trades** - Checklist passed only
3. **Filter noise** - Flow led to move only
### The Goal:
**Trade only the highest quality signals that:**
- Are Tier-1 (institutional positioning)
- Passed checklist (multiple confirmations)
- Led to price movement (real positioning, not hedge)
**Result:** Higher win rate, fewer false signals, better entries!
---
## 💡 Pro Tips
1. **Start with all 3 filters ON** - See only the best signals
2. **If no results, turn off one filter** - Gradually relax criteria
3. **Check VWAP distance** - Best entries are near VWAP
4. **Monitor price reaction** - If flow doesn't move price, it's likely a hedge
5. **Use Tier-2 as watchlist** - They often become Tier-1 later
---
**Remember:** Phase 1 is about **quality over quantity**. Better to trade 5 great signals than 50 mediocre ones!

View File

@ -1,226 +0,0 @@
# Phase 1 Frontend Update Summary
## ✅ Completed Updates
### 1. New Filter Options Added
**Location:** `frontend/src/components/dashboard/OptionsFlowPanel.jsx`
**New Filters:**
- ✅ **Tier-1 Only** - Filter to show only Tier-1 tradeable signals
- ✅ **Checklist Passed** - Filter to show only signals that passed the 4/5 checklist
- ✅ **Flow Led to Move** - Filter to show only signals where flow led to actual price movement
**Implementation:**
- Added to `initialFilters` state
- Added checkbox UI in filters section
- Integrated into `filteredData` useMemo hook
---
### 2. New Columns Added
**New Columns in Table:**
#### Signal Tier Column
- **Accessor:** `SignalTier`
- **Group:** `core`
- **Display:** Badge showing Tier-1 🥇, Tier-2 🥈, or Ignore 🚫
- **Color Coding:**
- Tier-1: Green badge
- Tier-2: Yellow badge
- Ignore: Gray badge
#### Checklist Column
- **Accessor:** `Checklist`
- **Group:** `core`
- **Display:** Score out of 5 with pass/fail indicator
- **Features:**
- Shows score (e.g., "4/5 ✅" or "3/5 ❌")
- Color coded: Green for passed, Red for failed
- Tooltip shows detailed check results
#### Price Reaction 5m Column
- **Accessor:** `PriceReaction5m`
- **Group:** `price`
- **Display:** Percentage price change 5 minutes after signal
- **Features:**
- Color coded: Green (positive), Red (negative)
- Shows checkmark if flow led to move
#### VWAP Distance Column
- **Accessor:** `VWAPDistance`
- **Group:** `price`
- **Display:** Percentage distance from VWAP
- **Features:**
- Color coded by distance:
- Green: Within 1% of VWAP
- Yellow: 1-2% from VWAP
- Red: >2% from VWAP
- Shows VWAP value in tooltip
---
### 3. Enhanced Best Trade Score
**Updated:** `calculateBestTradeScore()` function
**New Bonuses:**
- Tier-1 signal: +25 points
- Checklist passed: +20 points
- Flow led to move: +15 points
- Checklist score: +2 points per point (0-10 points)
**Result:** Tier-1 signals with checklist passed and flow-led moves rank higher
---
### 4. Column Visibility Updates
**Updated Column Groups:**
- Added `SignalTier` and `Checklist` to `core` group
- Added `PriceReaction5m` and `VWAPDistance` to `price` group
**Default Visible Columns:**
- Added `SignalTier` and `Checklist` to default visible columns
- Added `PriceReaction5m` to default visible columns
---
## 📊 Frontend Features
### Filter UI
```
[Min Score] [Min Premium] [Session] [Start Date] [End Date]
[🥇 Tier-1 Only] [✅ Checklist Passed] [📈 Flow Led to Move]
[Apply] [Reset]
```
### Column Display Examples
**Signal Tier:**
```
🥇 Tier-1 (green badge)
🥈 Tier-2 (yellow badge)
🚫 Ignore (gray badge)
```
**Checklist:**
```
4/5 ✅ (green badge)
3/5 ❌ (red badge)
```
**Price Reaction 5m:**
```
+1.25% ✓ (green, with checkmark if flow led to move)
-0.75% (red)
```
**VWAP Distance:**
```
+0.5% (green - near VWAP)
V: $45.23 (VWAP value)
```
---
## 🧪 Testing
### Test Script Created
**File:** `test_phase1_api.js`
**Usage:**
```bash
node test_phase1_api.js
```
**What it tests:**
1. Health endpoint
2. Options flow endpoint
3. Presence of Phase 1 fields in response
4. Statistics (Tier-1 count, checklist passed count, etc.)
---
## 🎯 Usage Examples
### Filter to Best Signals Only
1. Check "🥇 Tier-1 Only"
2. Check "✅ Checklist Passed"
3. Check "📈 Flow Led to Move"
4. Click "Apply"
**Result:** Only shows highest quality signals that:
- Are Tier-1 (tradeable alone)
- Passed the 4/5 checklist
- Led to actual price movement
### View Phase 1 Metrics
1. Open column selector
2. Enable "Tier", "Checklist", "5m Reaction", "vs VWAP"
3. Sort by any Phase 1 column
---
## 📝 API Response Fields Used
The frontend now uses these new fields from the API:
### Signal Classification
- `signal_tier` - 'TIER_1', 'TIER_2', or 'IGNORE'
- `is_tradeable` - Boolean
### Checklist
- `checklist_score` - 0-5
- `checklist_passed` - Boolean
- `checklist_details` - Object with individual check results
### Price Reaction
- `price_reaction_5m_pct` - Percentage change 5m after signal
- `price_reaction_15m_pct` - Percentage change 15m after signal
- `flow_led_to_move` - Boolean
### VWAP
- `vwap_at_signal` - VWAP value at signal time
- `price_vs_vwap_pct` - Percentage distance from VWAP
---
## 🚀 Next Steps
1. **Test the API:**
```bash
node test_phase1_api.js
```
2. **Start the frontend:**
```bash
cd frontend
npm run dev
```
3. **Verify:**
- New filters appear in filter panel
- New columns appear in table
- Filters work correctly
- Data displays correctly
4. **Monitor Performance:**
- Check API response times
- Monitor price reaction tracking queries
- Watch for any performance issues
---
## ⚠️ Notes
- All new fields are optional (won't break if missing)
- Filters default to `false` (all signals shown by default)
- New columns are included in default visible columns
- Best trade score calculation includes Phase 1 bonuses
---
**Status:** ✅ Frontend Updated and Ready for Testing

View File

@ -1,234 +0,0 @@
# Phase 1 Implementation Summary
## ✅ Completed Features
### 1. Price Reaction Tracking
**File:** `backend/python_service/services/price_reaction_tracker.py`
**What it does:**
- Tracks price movement 5, 15, and 30 minutes after a signal appears
- Calculates price reaction percentages
- Detects high/low breaks
- Flags if flow led to actual price movement (filters hedges/rolls)
**New Fields Added:**
- `price_reaction_5m_pct` - Price change 5 minutes after signal (%)
- `price_reaction_15m_pct` - Price change 15 minutes after signal (%)
- `price_reaction_30m_pct` - Price change 30 minutes after signal (%)
- `high_break_5m` - Boolean: did price break high within 5m?
- `low_break_5m` - Boolean: did price break low within 5m?
- `flow_led_to_move` - Boolean: did flow lead to >0.5% price move?
**Integration:** Called in `main.py` after price enrichment
---
### 2. VWAP Integration
**File:** `backend/python_service/services/price_context.py` (extended)
**What it does:**
- Calculates VWAP (Volume Weighted Average Price) for each trading day
- VWAP = SUM(price * volume) / SUM(volume) from RTH open (9:30 AM) to signal time
- Calculates distance from VWAP for entry/exit logic
**New Fields Added:**
- `vwap_at_signal` - VWAP value at signal time
- `price_vs_vwap_pct` - Percentage distance from VWAP
**Integration:** Integrated into `enrich_flow_with_prices()` method
**Usage:**
- Entry: VWAP pullback or VWAP reclaim
- Exit: Price rejection of VWAP
- Filter: Price too extended from VWAP (>2%) = wait for pullback
---
### 3. Signal Tier Classification
**File:** `backend/python_service/services/signal_tier_classifier.py`
**What it does:**
- Classifies signals into Tier-1, Tier-2, or Ignore
- Tier-1: 🟢/🔴 + 💎 + ⭐ + premium > 500K + direction aligned (tradeable alone)
- Tier-2: 🟢 + 💎 (no ⭐) OR ⭐ without 💎 (needs confirmation)
- Ignore: OTM-only, mixed signals, low volume/OI ratio
**New Fields Added:**
- `signal_tier` - 'TIER_1', 'TIER_2', or 'IGNORE'
- `is_tradeable` - Boolean: True for Tier-1 only
**Integration:** Called in `main.py` after rocket score calculation
**Filtering Logic:**
- OTM-only prints → Ignore
- Mixed 🟢/🔴 with no net premium edge → Ignore
- Big premium but volume << OI (likely rolls/hedges) Ignore
---
### 4. Trade Checklist
**File:** `backend/python_service/services/trade_checklist.py`
**What it does:**
- Evaluates signals against 5-point checklist
- Requires 4/5 checks to pass
- Prevents bad trades
**Checklist Items:**
1. ✅ Has direction (🟢 or 🔴)
2. ✅ Has diamond (💎)
3. ✅ Has star (⭐)
4. ✅ Price respects VWAP (within ±2%, or ±1% for direction)
5. ✅ Index confirms (placeholder - will be added in Phase 2)
**New Fields Added:**
- `checklist_score` - Score out of 5 (0-5)
- `checklist_passed` - Boolean: True if score >= 4
- `checklist_details` - Dict with individual check results
**Integration:** Called in `main.py` after price reaction tracking
**Usage:**
- Only trade signals with `checklist_passed = True`
- Review `checklist_details` to see which checks passed/failed
---
## 📊 Data Flow
```
Raw Options Flow
OptionsFlowProcessor.process()
PriceContextService.enrich_flow_with_prices() [VWAP added here]
AlertService.match_alerts_to_flows()
OptionsFlowProcessor.process_rocket_score()
SignalTierClassifier.classify_tiers() [NEW]
PriceReactionTracker.enrich_with_reactions() [NEW]
TradeChecklist.evaluate_all() [NEW]
OutputFormatter.format_final_output()
API Response
```
---
## 🔍 New API Response Fields
All new fields are automatically included in the API response. Key fields to use:
### For Filtering:
- `signal_tier` - Filter to 'TIER_1' for best signals
- `is_tradeable` - Boolean flag for tradeable signals
- `checklist_passed` - Boolean flag for checklist-passed signals
- `flow_led_to_move` - Boolean flag for signals that moved price
### For Entry/Exit:
- `vwap_at_signal` - VWAP value for entry logic
- `price_vs_vwap_pct` - Distance from VWAP
- `price_reaction_5m_pct` - Price reaction after signal
### For Analysis:
- `checklist_score` - Checklist score (0-5)
- `checklist_details` - Detailed check results (JSON)
---
## 🎯 Usage Examples
### Filter to Tier-1 Signals Only:
```python
# In your frontend or API consumer:
filtered = [row for row in data if row.get('signal_tier') == 'TIER_1']
```
### Filter to Checklist-Passed Signals:
```python
filtered = [row for row in data if row.get('checklist_passed') == True]
```
### Filter to Signals That Led to Price Moves:
```python
filtered = [row for row in data if row.get('flow_led_to_move') == True]
```
### Combined Filter (Best Signals):
```python
best_signals = [
row for row in data
if row.get('signal_tier') == 'TIER_1'
and row.get('checklist_passed') == True
and row.get('flow_led_to_move') == True
]
```
---
## 🚀 Next Steps
### Immediate:
1. Test the API endpoint to see new fields in response
2. Update frontend to display new fields
3. Add filters for Tier-1 and checklist-passed signals
### Phase 2 (Next):
- Strike clustering detection
- Delta weighting
- Index correlation (SPY/QQQ/VIX)
---
## 📝 Notes
- All new services follow existing code patterns
- No breaking changes to existing functionality
- New fields are optional (won't break if missing)
- Price reaction tracking may be slower for large datasets (batched queries)
- VWAP calculation only works during RTH (9:30 AM - 4:00 PM)
---
## 🐛 Known Limitations
1. **Price Reaction Tracking:**
- Only tracks if price data exists at 5m/15m/30m intervals
- May be None for signals near market close
- Uses 0.5% threshold for "flow led to move" (configurable)
2. **VWAP Calculation:**
- Only calculated during RTH (9:30 AM - 4:00 PM)
- Returns None for PRE/POST market signals
- Requires 1-minute bar data
3. **Index Correlation:**
- Currently returns False (placeholder)
- Will be implemented in Phase 2
4. **Checklist:**
- Index confirmation check always fails (Phase 2)
- VWAP check may fail if VWAP not available (PRE/POST market)
---
## ✅ Testing Checklist
- [ ] API returns new fields in response
- [ ] Tier classification works correctly
- [ ] Price reaction tracking populates correctly
- [ ] VWAP calculation works during RTH
- [ ] Checklist evaluation works correctly
- [ ] No errors in logs
- [ ] Performance is acceptable
---
**Implementation Date:** Phase 1 Complete
**Status:** ✅ Ready for Testing

View File

@ -1,205 +0,0 @@
# Phase 1 Manual Check Update
## ✅ Changes Made
### 1. **Removed Automatic Phase 1 Filtering**
- **Before:** Phase 1 filters automatically filtered the results
- **After:** Original filtering remains unchanged - Phase 1 is now manual check only
**Files Modified:**
- `frontend/src/components/dashboard/OptionsFlowPanel.jsx`
- Removed Phase 1 filter checkboxes from UI
- Removed automatic filtering logic from `filteredData`
- Kept Phase 1 data status indicator
---
### 2. **Added Phase 1 Check Button on Cards**
- **Location:** Each card in `OptionsFlowCardList` component
- **Button:** "Phase 1" button with checkmark icon
- **Behavior:** Opens Phase 1 Eligibility Modal when clicked
**Files Modified:**
- `frontend/src/components/dashboard/OptionsFlowCardList.jsx`
- Added Phase 1 button to each card
- Button shows different styling if Phase 1 data is available
- Integrated Phase 1 Eligibility Modal
---
### 3. **Created Phase 1 Eligibility Modal**
- **Component:** `Phase1EligibilityModal.jsx`
- **Shows:**
- Overall eligibility status (Fully Eligible / Partially Eligible / Not Eligible)
- Signal Tier (Tier-1 / Tier-2 / Ignore)
- Trade Checklist (Score, Pass/Fail, Detailed checks)
- Price Reaction (5m, 15m, Flow led to move)
- VWAP Analysis (VWAP value, Distance from VWAP)
- Summary section
**Files Created:**
- `frontend/src/components/tables/Phase1EligibilityModal.jsx`
---
### 4. **Added Phase 1 Button to Table View**
- **Location:** Expanded row details in table view
- **Button:** "Phase 1" button next to "AI Analysis" button
- **Behavior:** Opens same Phase 1 Eligibility Modal
**Files Modified:**
- `frontend/src/components/tables/ExpandedRowDetails.jsx`
- Added Phase 1 button
- Integrated Phase 1 Eligibility Modal
---
## 🎯 How It Works Now
### Card View:
1. User sees all filtered results (original filtering unchanged)
2. Each card has a "Phase 1" button
3. Click button → Opens Phase 1 Eligibility Modal
4. Modal shows detailed Phase 1 analysis for that specific signal
### Table View:
1. User sees all filtered results (original filtering unchanged)
2. Click row to expand details
3. In expanded details, click "Phase 1" button
4. Modal shows detailed Phase 1 analysis
---
## 📊 Phase 1 Eligibility Modal Features
### Overall Status:
- ✅ **Fully Eligible** - Meets all criteria (Tier-1 + Checklist Passed + Flow Led to Move)
- ⚠️ **Partially Eligible** - Meets some criteria
- ❌ **Not Eligible** - Doesn't meet criteria
### Detailed Sections:
1. **Signal Tier**
- Shows Tier-1 🥇, Tier-2 🥈, or Ignore 🚫
- Explains what each tier means
2. **Trade Checklist**
- Shows score (X/5)
- Shows pass/fail status
- Lists individual check results:
- ✅ Has direction (🟢/🔴)
- ✅ Has diamond (💎)
- ✅ Has star (⭐)
- ✅ Price respects VWAP
- ✅ Index confirms
3. **Price Reaction**
- Shows 5m and 15m price reactions
- Indicates if flow led to move
- Shows high/low breaks
4. **VWAP Analysis**
- Shows VWAP value at signal time
- Shows distance from VWAP
- Provides entry guidance
5. **Summary**
- Quick overview of all Phase 1 metrics
---
## 🔄 Historical Date Support
### Phase 1 works with backtesting/previous dates:
1. **Signal Tier Classification**
- ✅ Works for any date (based on badge combinations)
- No date dependency
2. **Price Reaction Tracking**
- ✅ Works for historical dates
- Queries price data at signal time + 5m/15m/30m
- Works as long as price data exists in database
3. **VWAP Calculation**
- ✅ Works for historical dates
- Calculates VWAP from RTH open (9:30 AM) to signal time
- Works as long as 1-minute bar data exists
4. **Trade Checklist**
- ✅ Works for any date
- Checks badges, VWAP distance, etc.
- No date dependency
**Note:** For historical dates, price reaction and VWAP will only work if:
- Price data exists in `prices_intraday_1m` table for that date
- Signal time is during RTH hours (for VWAP)
---
## 🎨 UI Changes
### Removed:
- ❌ Phase 1 filter checkboxes (Tier-1 Only, Checklist Passed, Flow Led to Move)
- ❌ Automatic filtering based on Phase 1 criteria
### Added:
- ✅ "Phase 1" button on each card
- ✅ "Phase 1" button in expanded row details
- ✅ Phase 1 Eligibility Modal
- ✅ Phase 1 data status indicator (shows if data is available)
---
## 📝 Usage
### To Check Phase 1 Eligibility:
1. **Card View:**
- Find the signal card
- Click "Phase 1" button
- Review eligibility in modal
2. **Table View:**
- Click row to expand
- Click "Phase 1" button in expanded details
- Review eligibility in modal
### For Backtesting:
1. Select historical date range (e.g., 12/01/2025 - 12/05/2025)
2. Apply filters (original filters work as before)
3. Click "Phase 1" button on any signal
4. Modal shows Phase 1 analysis for that historical signal
5. Price reaction and VWAP work if price data exists for that date
---
## ✅ Benefits
1. **No Automatic Filtering** - Original filters work as before
2. **Manual Control** - Check Phase 1 eligibility when you want
3. **Detailed Analysis** - See all Phase 1 metrics in one place
4. **Historical Support** - Works with backtesting dates
5. **Non-Intrusive** - Doesn't change existing workflow
---
## 🔍 Example Workflow
1. User filters by date range: 12/01/2025 - 12/05/2025
2. Applies original filters (min premium, session, etc.)
3. Sees 20 signals in results
4. Clicks "Phase 1" button on a signal
5. Modal shows:
- Tier-1 ✅
- Checklist: 4/5 ✅
- Price Reaction: +1.2% ✅
- VWAP: Near VWAP ✅
- **Status: Fully Eligible**
6. User decides to trade based on Phase 1 analysis
---
**Status:** ✅ Complete - Ready to Use!

View File

@ -11,11 +11,10 @@ Complete guide for deploying the Institutional Trader platform on a Proxmox serv
5. [Database Configuration](#5-database-configuration)
6. [Systemd Services](#6-systemd-services)
7. [Nginx Configuration](#7-nginx-configuration)
8. [DNS Configuration](#8-dns-configuration)
9. [SSL Certificates](#9-ssl-certificates)
10. [Database Access for Friend](#10-database-access-for-friend)
11. [Monitoring & Maintenance](#11-monitoring--maintenance)
12. [Troubleshooting](#12-troubleshooting)
8. [SSL Certificates](#8-ssl-certificates)
9. [Database Access for Friend](#9-database-access-for-friend)
10. [Monitoring & Maintenance](#10-monitoring--maintenance)
11. [Troubleshooting](#11-troubleshooting)
---
@ -39,10 +38,18 @@ Complete guide for deploying the Institutional Trader platform on a Proxmox serv
- Configure:
- **Hostname**: `institutional-trader`
- **Password**: Set root password
- **CPU**: 2-4 cores
- **Memory**: 4-8 GB RAM
- **Disk**: 20-50 GB
- **CPU**: 2-4 cores ✅ (You have 4 CPUs - perfect!)
- **Memory**: 4-8 GB RAM ✅ (You have 31.75 GiB - excellent!)
- **Disk**: 20-50 GB ✅ (You have 294.23 GiB - plenty of space!)
- **Network**: Bridge with static IP (recommended) or DHCP
- **Unprivileged**: Yes/No (see note below)
**Note on Unprivileged Containers:**
- ✅ **Your configuration will work** - Unprivileged containers are fine for this deployment
- Services run on ports > 1024 (Node.js: 3000, Python: 8000) - no issues
- Nginx can run as reverse proxy (may need to bind to ports > 1024 or use host network)
- PostgreSQL can be installed and run normally
- SSL certificates work fine with Certbot
2. **Start Container:**
```bash
@ -81,6 +88,43 @@ Complete guide for deploying the Institutional Trader platform on a Proxmox serv
## 3. System Dependencies
### Important: Unprivileged Container Considerations
If your container is **unprivileged** (which yours is), there are a few adjustments:
1. **Nginx Port Binding**:
- Unprivileged containers cannot bind to ports < 1024
- **Solution**: Use Nginx on host or configure port forwarding
- Alternative: Run Nginx on port 8080/8443 and forward from host
2. **Systemd**:
- May need to enable systemd in container
- Check: `systemctl status` should work
3. **File Permissions**:
- Some operations may need different permissions
- Usually not an issue for this application
**Your Configuration Analysis:**
- ✅ **4 CPUs** - Perfect (recommended 2-4)
- ✅ **31.75 GiB RAM** - Excellent (recommended 4-8 GB)
- ✅ **294.23 GiB Disk** - Plenty of space (recommended 20-50 GB)
- ⚠️ **Unprivileged: Yes** - Will work, but see Nginx note below
### Enabling Systemd (if needed)
If systemd doesn't work in your container, you may need to enable it:
```bash
# Check if systemd is working
systemctl status
# If it fails, you may need to enable it in Proxmox
# Edit container config on Proxmox host:
pct set <container-id> -features nesting=1
pct set <container-id> -features keyctl=1
```
Run these commands on your Ubuntu container/VM:
```bash
@ -94,7 +138,7 @@ apt install -y nodejs
# Install Python 3.11+ and pip
apt install -y python3 python3-pip python3-venv
# Install PostgreSQL client (if using local DB)
# Install PostgreSQL client (for remote database connection)
apt install -y postgresql-client
# Install Nginx
@ -161,7 +205,7 @@ SUPABASE_ANON_KEY=your_anon_key
SUPABASE_SERVICE_KEY=your_service_role_key
# OR Direct PostgreSQL Connection (if using local/remote PostgreSQL)
DATABASE_URL=postgresql://postgres:password@localhost:5432/institutional_trader
DATABASE_URL=postgresql://postgres:password@100.121.163.23:5432/institutional_trader
# CORS Configuration
CORS_ORIGIN=https://yourdomain.com
@ -172,7 +216,7 @@ RATE_LIMIT_WINDOW=15
RATE_LIMIT_MAX=100
# Python Service (if using)
PYTHON_SERVICE_URL=http://localhost:8010
PYTHON_SERVICE_URL=http://localhost:8000
```
### 4.3 Python Service Setup
@ -207,8 +251,6 @@ nano .env.production
```
**Frontend `.env.production` configuration:**
**⚠️ Critical:** The frontend must have a `.env.production` file with the correct API URL, otherwise it will try to connect to `localhost:3010` which won't work in production.
```env
VITE_API_URL=https://api.yourdomain.com
VITE_WS_URL=wss://api.yourdomain.com
@ -239,42 +281,40 @@ This creates a `dist` folder with production-ready files.
- Use `DATABASE_URL` or `SUPABASE_URL` + keys in backend `.env`
- See [Database Access for Friend](#9-database-access-for-friend) section for sharing access
### Option B: Local PostgreSQL (Self-Hosted)
### Option B: Remote PostgreSQL Database
```bash
# Install PostgreSQL
apt install -y postgresql postgresql-contrib
Your database is hosted at `100.121.163.23:5432`. Configure connection:
# Start PostgreSQL
systemctl start postgresql
systemctl enable postgresql
# Create database and user
sudo -u postgres psql
# In PostgreSQL prompt:
CREATE DATABASE institutional_trader;
CREATE USER trader_user WITH PASSWORD 'secure_password_here';
GRANT ALL PRIVILEGES ON DATABASE institutional_trader TO trader_user;
\q
# Import schema
cd /opt/institutional_trader/backend/database
sudo -u postgres psql institutional_trader < schema.sql
sudo -u postgres psql institutional_trader < missing_tables.sql
# Import other SQL files as needed
```
Update backend `.env`:
**Update backend `.env`:**
```env
USE_LOCAL_DB=true
LOCAL_DB_HOST=localhost
LOCAL_DB_HOST=100.121.163.23
LOCAL_DB_PORT=5432
LOCAL_DB_USER=trader_user
LOCAL_DB_PASSWORD=secure_password_here
LOCAL_DB_USER=postgres
LOCAL_DB_PASSWORD=your_postgres_password
LOCAL_DB_NAME=institutional_trader
```
**Or use DATABASE_URL format:**
```env
DATABASE_URL=postgresql://postgres:your_password@100.121.163.23:5432/institutional_trader
```
**Test connection:**
```bash
# Test from container
psql -h 100.121.163.23 -p 5432 -U postgres -d institutional_trader
# Or test from Proxmox host
apt install -y postgresql-client
psql -h 100.121.163.23 -p 5432 -U postgres -d institutional_trader
```
**Note:** Make sure the remote PostgreSQL server allows connections from your Proxmox container IP. You may need to:
1. Configure `pg_hba.conf` on the database server to allow your container's IP
2. Configure firewall rules to allow port 5432 from your container
3. Ensure PostgreSQL is listening on the correct interface (not just localhost)
---
## 6. Systemd Services
@ -291,7 +331,7 @@ Add:
```ini
[Unit]
Description=Institutional Trader Backend API
After=network.target postgresql.service
After=network.target
[Service]
Type=simple
@ -329,15 +369,14 @@ Add:
```ini
[Unit]
Description=Institutional Trader Python Service
After=network.target postgresql.service
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/institutional_trader/backend/python_service
Environment="PATH=/opt/institutional_trader/backend/python_service/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
EnvironmentFile=/opt/institutional_trader/backend/python_service/.env
ExecStart=/opt/institutional_trader/backend/python_service/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8010
ExecStart=/opt/institutional_trader/backend/python_service/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000
Restart=always
RestartSec=10
StandardOutput=journal
@ -347,8 +386,6 @@ StandardError=journal
WantedBy=multi-user.target
```
**Note:** The `EnvironmentFile` directive is optional - the service will also load `.env` automatically via `load_dotenv()` in the code. However, adding it explicitly ensures environment variables are available even if the code path changes.
Enable and start:
```bash
sudo systemctl daemon-reload
@ -374,8 +411,30 @@ sudo journalctl -u institutional-trader-* -f
## 7. Nginx Configuration
### ⚠️ Important: Unprivileged Container Nginx Setup
If your container is **unprivileged**, Nginx cannot bind to ports 80/443 directly. You have two options:
**Option A: Run Nginx on Proxmox Host (Recommended)**
- Install Nginx on the Proxmox host
- Configure it to proxy to your container's services
- Container services run on ports 3000 (backend) and 8000 (Python)
- See "Nginx on Host" section below
**Option B: Use Port Forwarding**
- Run Nginx in container on ports 8080/8443
- Forward ports from Proxmox host to container
- Configure firewall rules
**Option C: Enable Privileged Mode (Less Secure)**
- Convert container to privileged mode
- Allows binding to ports 80/443
- Less secure but simpler
### 7.1 Create Nginx Configuration
**For Container (if using Option B):**
```bash
sudo nano /etc/nginx/sites-available/institutional-trader
```
@ -485,96 +544,7 @@ server {
}
```
### 7.2 Multi-Container Setup (Nginx on Separate Container)
If you're running nginx on a separate container/VM that proxies to your application containers:
**Example configuration for proxying to another Proxmox container:**
```nginx
server {
listen 443 ssl http2;
server_name traderideas.deepteklabs.com;
ssl_certificate /etc/letsencrypt/live/traderideas.deepteklabs.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/traderideas.deepteklabs.com/privkey.pem;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
# Backend API Routes
location /api/ {
proxy_pass http://192.168.8.151:3000/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffering off;
}
# WebSocket Support (if using WebSockets)
location /ws {
proxy_pass http://192.168.8.151:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400; # 24 hours for long-lived connections
}
# Health Check
location /health {
proxy_pass http://192.168.8.151:3000/health;
access_log off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# Frontend (served from application container)
location / {
proxy_pass http://192.168.8.151:8080/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header Cache-Control;
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
}
# Static assets with caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|otf|map)$ {
proxy_pass http://192.168.8.151:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
}
```
**Important notes for multi-container setup:**
- Replace `192.168.8.151` with your application container's IP
- Ensure containers can communicate (same network/bridge)
- Port 3000 = Backend API, Port 8080 = Frontend (adjust as needed)
- Test connectivity: `curl http://192.168.8.151:3000/health` from nginx container
### 7.3 Enable Site
### 7.2 Enable Site
```bash
# Create symlink
@ -587,181 +557,92 @@ sudo nginx -t
sudo systemctl reload nginx
```
---
### 7.3 Nginx on Proxmox Host (For Unprivileged Containers)
## 8. DNS Configuration
**⚠️ Important:** Before setting up SSL certificates, you must configure DNS records pointing to your server.
### 8.1 Find Your Server's Public IP Address
If your container is unprivileged, install Nginx on the Proxmox host:
**On Proxmox Host:**
```bash
# On your server, find the public IP
curl ifconfig.me
# Or
hostname -I
# Or check in Proxmox Web UI: Datacenter → Your Node → Network
# Install Nginx
apt install -y nginx
# Create configuration
nano /etc/nginx/sites-available/institutional-trader
```
### 8.2 Configure DNS A Records
**Nginx Config (on Proxmox host, pointing to container):**
```nginx
# Backend API Server
server {
listen 443 ssl http2;
server_name api.yourdomain.com;
Go to your domain registrar's DNS management panel (where you registered `traderideas.deepteklabs.com`) and add these A records:
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
| Type | Name | Value | TTL |
|------|------|-------|-----|
| A | `@` (or blank) | `<your-server-ip>` | 3600 |
| A | `www` | `<your-server-ip>` | 3600 |
| A | `api` | `<your-server-ip>` | 3600 |
location / {
proxy_pass http://<container-ip>:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
**Example for `traderideas.deepteklabs.com`:**
- `traderideas.deepteklabs.com``<your-server-ip>` (A record with name `@` or blank)
- `www.traderideas.deepteklabs.com``<your-server-ip>` (A record with name `www`)
- `api.traderideas.deepteklabs.com``<your-server-ip>` (A record with name `api`)
location /ws {
proxy_pass http://<container-ip>:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_read_timeout 86400;
}
}
### 8.3 Verify DNS Propagation
# Frontend Server
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
Wait 5-15 minutes for DNS to propagate, then verify:
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
```bash
# Check if DNS records are resolving
dig traderideas.deepteklabs.com +short
dig www.traderideas.deepteklabs.com +short
dig api.traderideas.deepteklabs.com +short
root /var/www/institutional-trader/dist;
index index.html;
# All should return your server's IP address
# Alternative: Use nslookup
nslookup traderideas.deepteklabs.com
nslookup www.traderideas.deepteklabs.com
nslookup api.traderideas.deepteklabs.com
# Or use online tools:
# - https://dnschecker.org
# - https://www.whatsmydns.net
location / {
try_files $uri $uri/ /index.html;
}
}
```
**Common DNS Providers:**
- **Cloudflare**: Dashboard → DNS → Records → Add record
- **Namecheap**: Domain List → Manage → Advanced DNS → Add New Record
- **GoDaddy**: DNS Management → Add Record
- **Google Domains**: DNS → Custom Records → Add Record
### 8.4 Test Domain Accessibility
Once DNS is propagated, test that domains are reachable:
**Copy frontend files to host:**
```bash
# From your server
curl -I http://traderideas.deepteklabs.com
curl -I http://www.traderideas.deepteklabs.com
curl -I http://api.traderideas.deepteklabs.com
# On container, create archive
cd /opt/institutional_trader/frontend
tar czf /tmp/frontend-dist.tar.gz dist/
# Should return HTTP responses (even if 502/503, that's OK - means DNS is working)
# On Proxmox host, copy from container
pct pull <container-id> /tmp/frontend-dist.tar.gz /tmp/
tar xzf /tmp/frontend-dist.tar.gz -C /var/www/institutional-trader/
```
### 8.5 Cloudflare Proxy (Orange Cloud)
**If your domain uses Cloudflare proxy (orange cloud icon):**
When DNS resolves to Cloudflare IPs (like `104.21.x.x` or `172.67.x.x`), your domain is behind Cloudflare proxy. This affects SSL certificate setup:
**Check if using Cloudflare proxy:**
```bash
dig traderideas.deepteklabs.com +short
# If returns Cloudflare IPs (104.x.x.x, 172.x.x.x), proxy is enabled
```
**Options for SSL with Cloudflare:**
1. **Use DNS Challenge (Recommended)** - Works with proxy enabled
2. **Temporarily Disable Proxy** - Get certs, then re-enable proxy
3. **Use Cloudflare SSL** - Let Cloudflare handle SSL (easiest)
See Section 9.2 for detailed instructions.
**⚠️ Only proceed to SSL setup once DNS records are working!**
---
## 9. SSL Certificates
## 8. SSL Certificates
### 9.1 Install Certbot
### 8.1 Install Certbot
```bash
sudo apt install -y certbot python3-certbot-nginx
```
### 9.2 Obtain SSL Certificate
**⚠️ Prerequisites:**
- DNS A records must be configured and propagated (see Section 8)
- Domains must resolve to your server's IP (or Cloudflare if using proxy)
- Port 80 must be accessible from the internet (for HTTP challenge) OR DNS API access (for DNS challenge)
**⚠️ Important:** If your nginx config already references SSL certificates that don't exist, you need to fix this first.
**Check if using Cloudflare proxy:**
```bash
dig traderideas.deepteklabs.com +short
# If returns Cloudflare IPs (104.x.x.x, 172.x.x.x), you're using Cloudflare proxy
```
**If using Cloudflare Proxy, use Option D (DNS Challenge) or Option E (Cloudflare SSL)**
**Option A: Use Standalone Mode (Works if NOT using Cloudflare proxy)**
```bash
# Stop nginx temporarily
sudo systemctl stop nginx
# Get certificates using standalone mode
sudo certbot certonly --standalone -d traderideas.deepteklabs.com -d www.traderideas.deepteklabs.com -d api.traderideas.deepteklabs.com
# Start nginx again
sudo systemctl start nginx
# Now certbot can update the nginx config
sudo certbot --nginx -d traderideas.deepteklabs.com -d www.traderideas.deepteklabs.com -d api.traderideas.deepteklabs.com
```
**Option B: Temporarily Comment Out SSL in Nginx Config**
```bash
# Edit nginx config
sudo nano /etc/nginx/sites-available/institutional-trader
# Temporarily comment out SSL lines:
# - Comment out: listen 443 ssl http2;
# - Comment out: ssl_certificate and ssl_certificate_key lines
# - Change: listen 443 ssl http2; to: listen 80;
# Example temporary config (HTTP only):
# server {
# listen 80;
# server_name traderideas.deepteklabs.com;
# # ssl_certificate /etc/letsencrypt/live/traderideas.deepteklabs.com/fullchain.pem;
# # ssl_certificate_key /etc/letsencrypt/live/traderideas.deepteklabs.com/privkey.pem;
# ...
# }
# Test and reload
sudo nginx -t
sudo systemctl reload nginx
# Now get certificates (certbot will automatically update the config)
sudo certbot --nginx -d traderideas.deepteklabs.com -d www.traderideas.deepteklabs.com -d api.traderideas.deepteklabs.com
# Certbot will automatically:
# - Get the certificates
# - Update your nginx config to use them
# - Add SSL redirects
```
**Option C: Use Certbot with Nginx Plugin (If config is clean and NOT using Cloudflare proxy)**
### 8.2 Obtain SSL Certificate
```bash
# Replace with your actual domain
sudo certbot --nginx -d traderideas.deepteklabs.com -d www.traderideas.deepteklabs.com -d api.traderideas.deepteklabs.com
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com -d api.yourdomain.com
```
Follow the prompts:
@ -769,85 +650,7 @@ Follow the prompts:
- Agree to terms
- Choose whether to redirect HTTP to HTTPS (recommended: Yes)
**Option D: DNS Challenge (Recommended for Cloudflare proxy)**
If your domain is behind Cloudflare proxy, use DNS challenge:
```bash
# Install Cloudflare plugin for certbot
sudo apt install -y python3-pip
sudo pip3 install certbot-dns-cloudflare
# Create Cloudflare API token directory
sudo mkdir -p /etc/letsencrypt
sudo chmod 700 /etc/letsencrypt
# Create Cloudflare credentials file
sudo nano /etc/letsencrypt/cloudflare.ini
```
Add your Cloudflare API token:
```ini
# Cloudflare API token (get from: Cloudflare Dashboard → My Profile → API Tokens → Create Token)
# Permissions needed: Zone:Zone:Read, Zone:DNS:Edit
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
```
```bash
# Secure the credentials file
sudo chmod 600 /etc/letsencrypt/cloudflare.ini
# Get certificates using DNS challenge
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d traderideas.deepteklabs.com \
-d www.traderideas.deepteklabs.com \
-d api.traderideas.deepteklabs.com
# Update nginx config manually (certbot won't auto-update with DNS challenge)
sudo nano /etc/nginx/sites-available/institutional-trader
# Update ssl_certificate paths to:
# ssl_certificate /etc/letsencrypt/live/traderideas.deepteklabs.com/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/traderideas.deepteklabs.com/privkey.pem;
sudo nginx -t
sudo systemctl reload nginx
```
**Option E: Use Cloudflare SSL (Easiest with Cloudflare proxy)**
If using Cloudflare proxy, you can let Cloudflare handle SSL:
1. **In Cloudflare Dashboard:**
- Go to SSL/TLS → Overview
- Set encryption mode to **"Full"** or **"Full (strict)"**
- Cloudflare will automatically provide SSL certificates
2. **On your server, use self-signed or Cloudflare Origin Certificate:**
```bash
# Generate self-signed certificate (Cloudflare will accept it)
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/ssl/private/nginx-selfsigned.key \
-out /etc/ssl/certs/nginx-selfsigned.crt
# Update nginx config to use self-signed cert
sudo nano /etc/nginx/sites-available/institutional-trader
# Change to:
# ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt;
# ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key;
sudo nginx -t
sudo systemctl reload nginx
```
3. **Or get Cloudflare Origin Certificate:**
- Cloudflare Dashboard → SSL/TLS → Origin Server → Create Certificate
- Copy certificate and key
- Save to `/etc/ssl/certs/cloudflare-origin.crt` and `/etc/ssl/private/cloudflare-origin.key`
- Update nginx config to use these files
### 9.3 Auto-Renewal
### 8.3 Auto-Renewal
Certbot sets up auto-renewal automatically. Test it:
@ -857,7 +660,7 @@ sudo certbot renew --dry-run
---
## 10. Database Access for Friend
## 9. Database Access for Friend
### Option A: Supabase (Recommended)
@ -934,13 +737,14 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, USAGE ON SEQUENCES TO fr
**Connection String for Friend:**
```
postgresql://friend_user:secure_password_here@your-server-ip:5432/institutional_trader
postgresql://friend_user:secure_password_here@100.121.163.23:5432/institutional_trader
```
**Important:** If your friend needs to connect from outside your network:
1. Configure PostgreSQL to accept remote connections
2. Set up firewall rules
3. Consider using SSH tunnel for security
**Important:** Make sure the PostgreSQL server at `100.121.163.23`:
1. Allows remote connections from your friend's IP
2. Has `pg_hba.conf` configured to allow connections
3. Has firewall rules allowing port 5432
4. Consider using SSH tunnel for additional security
### Option C: SSH Tunnel (Most Secure)
@ -948,12 +752,14 @@ Create an SSH tunnel for your friend:
```bash
# On your friend's machine
ssh -L 5432:localhost:5432 user@your-server-ip
ssh -L 5432:100.121.163.23:5432 user@your-proxmox-server-ip
# Then they can connect using:
# postgresql://friend_user:password@localhost:5432/institutional_trader
```
This creates a secure tunnel through your Proxmox server to the database.
### Option D: Read-Only Access (If Friend Only Needs to Read Data)
```sql
@ -967,7 +773,7 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO friend_reado
---
## 11. Monitoring & Maintenance
## 10. Monitoring & Maintenance
### 10.1 Health Checks
@ -1029,123 +835,18 @@ sudo systemctl reload nginx
- Use Supabase Dashboard > Database > Backups
- Or use `pg_dump` with connection string
**For Local PostgreSQL:**
**For Remote PostgreSQL:**
```bash
# Create backup
sudo -u postgres pg_dump institutional_trader > backup_$(date +%Y%m%d).sql
pg_dump -h 100.121.163.23 -p 5432 -U postgres -d institutional_trader > backup_$(date +%Y%m%d).sql
# Restore backup
sudo -u postgres psql institutional_trader < backup_20240101.sql
psql -h 100.121.163.23 -p 5432 -U postgres -d institutional_trader < backup_20240101.sql
```
---
## 12. Troubleshooting
### Git Pull Fails - DNS Resolution Error
**Error:** `fatal: unable to access 'https://github.com/...': Could not resolve host: github.com`
**This means your container/VM cannot resolve DNS names.**
**Quick Fix:**
```bash
# 1. Check current DNS configuration
cat /etc/resolv.conf
# 2. If empty or missing, add DNS servers
sudo nano /etc/resolv.conf
```
Add these lines:
```
nameserver 8.8.8.8
nameserver 8.8.4.4
nameserver 1.1.1.1
```
**For LXC Containers (Persistent Fix):**
```bash
# Edit container DNS configuration
# On Proxmox host, edit container config:
nano /etc/pve/lxc/<container-id>.conf
# Add DNS servers:
nameserver: 8.8.8.8
nameserver: 8.8.4.4
```
Or via Proxmox Web UI:
- Go to your container → Options → DNS
- Set DNS servers: `8.8.8.8, 8.8.4.4`
**For Ubuntu VMs/Containers (Systemd-resolved):**
```bash
# Edit systemd-resolved config
sudo nano /etc/systemd/resolved.conf
```
Uncomment and set:
```
[Resolve]
DNS=8.8.8.8 8.8.4.4 1.1.1.1
FallbackDNS=1.1.1.1 8.8.8.8
```
**If using Tailscale (DNS server 100.100.100.100):**
If your `/etc/resolv.conf` shows Tailscale DNS (`100.100.100.100`) but it's timing out, add fallback DNS servers:
```bash
# Edit systemd-resolved config
sudo nano /etc/systemd/resolved.conf
```
Set:
```
[Resolve]
DNS=100.100.100.100 8.8.8.8 8.8.4.4 1.1.1.1
FallbackDNS=8.8.8.8 8.8.4.4 1.1.1.1
```
This keeps Tailscale DNS as primary (for Tailscale network access) but adds public DNS as fallback.
```bash
# Restart systemd-resolved
sudo systemctl restart systemd-resolved
# Test DNS
nslookup github.com
ping -c 2 github.com
```
**Alternative: Use IP Address (Temporary Workaround)**
If DNS still doesn't work, you can manually update git remote:
```bash
# Get GitHub IP
nslookup github.com 8.8.8.8
# Or use known GitHub IPs (may change)
# Update git remote to use IP (not recommended for long-term)
git remote set-url origin https://140.82.121.3/deepkoluguri/INSTITUTIONAL-FLOW-TRADING-PLATFORM.git
```
**Verify DNS is Working:**
```bash
# Test DNS resolution
nslookup github.com
dig github.com
# Test connectivity
ping -c 2 github.com
curl -I https://github.com
```
## 11. Troubleshooting
### Backend Won't Start
@ -1163,78 +864,14 @@ node -e "import('./src/db.js').then(m => m.testConnection())"
### Python Service Won't Start
**Step 1: Check detailed error logs**
```bash
# View recent logs with full error messages
sudo journalctl -u institutional-trader-python -n 100 --no-pager
# Check logs
sudo journalctl -u institutional-trader-python -n 50
# Follow logs in real-time
sudo journalctl -u institutional-trader-python -f
```
**Step 2: Verify systemd service file has correct port**
```bash
# Check current service file
sudo cat /etc/systemd/system/institutional-trader-python.service
# If port is 8000, update it to 8010:
sudo nano /etc/systemd/system/institutional-trader-python.service
# Change: --port 8000 to --port 8010
# Then reload and restart:
sudo systemctl daemon-reload
sudo systemctl restart institutional-trader-python
```
**Step 3: Test manually to see actual error**
```bash
# Test manually
cd /opt/institutional_trader/backend/python_service
source venv/bin/activate
# Check if .env file exists and has correct DB config
cat .env
# Test startup manually
uvicorn main:app --host 0.0.0.0 --port 8010
```
**Step 4: Common issues and fixes**
**Database connection timeout:**
- The service now has a 10-second timeout and will start even if DB is temporarily unavailable
- Check database is reachable: `psql -h <DB_HOST> -U <DB_USER> -d institutional_trader`
- Verify `.env` file in `backend/python_service/` has correct database credentials
**Port already in use:**
```bash
# Check if port 8010 is in use
sudo netstat -tlnp | grep 8010
# Or
sudo lsof -i :8010
# Kill process if needed
sudo kill -9 <PID>
```
**Missing dependencies:**
```bash
cd /opt/institutional_trader/backend/python_service
source venv/bin/activate
pip install -r requirements.txt
```
**Step 5: Verify backend .env has correct Python service URL**
```bash
# Check backend .env
cat /opt/institutional_trader/backend/.env | grep PYTHON_SERVICE_URL
# Should be:
# PYTHON_SERVICE_URL=http://localhost:8010
# If not, update it:
nano /opt/institutional_trader/backend/.env
# Add or update: PYTHON_SERVICE_URL=http://localhost:8010
# Then restart backend:
sudo systemctl restart institutional-trader-backend
uvicorn main:app --host 0.0.0.0 --port 8000
```
### Nginx Errors
@ -1253,8 +890,8 @@ sudo systemctl reload nginx
### Database Connection Issues
```bash
# Test PostgreSQL connection (local)
psql -h localhost -U trader_user -d institutional_trader
# Test PostgreSQL connection (remote)
psql -h 100.121.163.23 -p 5432 -U postgres -d institutional_trader
# Test Supabase connection
psql "postgresql://postgres:password@db.project.supabase.co:5432/postgres"

View File

@ -1,863 +0,0 @@
# Trading Playbook Implementation Suggestions
## Current State Analysis
### ✅ What You Already Have
- Badge system: 🟢/🔴, 💎, ⭐, 💰, ⚡, 🚀
- Rocket scoring algorithm
- Price context: RTH open, prior close, 5m/15m momentum
- Tape alignment detection
- Trade signal generation
- Session bucketing (PRE/RTH/POST)
- Premium filtering and aggregations
### ❌ What's Missing (High Impact)
---
## PART A — TRADING LOGIC ENHANCEMENTS
### 1⃣ Signal Tier Classification System
**Current Gap:** All signals are treated equally. You need to classify them into Tier-1, Tier-2, and Ignore categories.
**Suggestion:**
- **Add a new service:** `backend/python_service/services/signal_tier_classifier.py`
- Classify signals based on badge combinations
- Tier-1: 🟢/🔴 + 💎 + ⭐ + premium > 500K + direction aligned
- Tier-2: 🟢 + 💎 (no ⭐) OR ⭐ without 💎
- Ignore: OTM-only, mixed signals, low volume/OI ratio
**Implementation Approach:**
```python
# In options_flow_processor.py, add after process_badges():
def classify_signal_tier(row):
badge_round = row.get('badge_round', '')
badge_more = row.get('badge_more', '')
premium = row.get('premium_num', 0) or 0
direction = row.get('direction', '')
bull_total = row.get('bull_total', 0) or 0
bear_total = row.get('bear_total', 0) or 0
has_diamond = '💎' in badge_more
has_star = '⭐' in badge_more
# Tier-1 conditions
if (badge_round in ['🟢', '🔴'] and
has_diamond and has_star and
premium > 500000):
# Check direction alignment
if (badge_round == '🟢' and direction == 'BULL' and (bull_total - bear_total) > 0):
return 'TIER_1'
elif (badge_round == '🔴' and direction == 'BEAR' and (bear_total - bull_total) > 0):
return 'TIER_1'
# Tier-2 conditions
if (badge_round == '🟢' and has_diamond and not has_star):
return 'TIER_2'
if (has_star and not has_diamond):
return 'TIER_2'
# Ignore conditions
# (Add logic for OTM-only, mixed signals, etc.)
return 'IGNORE'
```
**Database Addition:**
- Add `signal_tier` column to processed flow output
- Add `is_tradeable` boolean flag
---
### 2⃣ VWAP Integration
**Current Gap:** You have price context but no VWAP calculation or VWAP-based entry/exit logic.
**Suggestion:**
- **Extend:** `backend/python_service/services/price_context.py`
- Add `calculate_vwap()` method
- Calculate VWAP for each symbol on each trading day
- Store VWAP at signal time
- Calculate distance from VWAP (percentage)
**Implementation Approach:**
```python
# Add to PriceContextService:
async def get_vwap_at_time(self, symbol: str, timestamp: datetime, pool: asyncpg.Pool):
"""Calculate VWAP up to the given timestamp for the trading day"""
# Query all 1m bars from RTH open to timestamp
# Calculate: SUM(price * volume) / SUM(volume)
# Return VWAP value and distance from current price
```
**New Fields to Add:**
- `vwap_at_signal` - VWAP value at signal time
- `price_vs_vwap_pct` - Percentage distance from VWAP
- `vwap_reclaimed` - Boolean: did price reclaim VWAP after signal?
**Entry Strategy Integration:**
- Best entry: VWAP pullback or VWAP reclaim
- Good entry: Break & hold above prior high
- Avoid: Chasing vertical candles
---
### 3⃣ Price Reaction Tracking (MOST IMPORTANT)
**Current Gap:** No tracking of how price moves AFTER the signal appears.
**Suggestion:**
- **New service:** `backend/python_service/services/price_reaction_tracker.py`
- Track price 5 minutes, 15 minutes, 30 minutes after signal
- Calculate price change percentage
- Identify if flow led to price movement or was just hedging
**Implementation Approach:**
```python
class PriceReactionTracker:
async def track_reaction(self, flow_row, pool):
signal_time = flow_row['flow_ts_utc']
symbol = flow_row['symbol_norm']
price_at_signal = flow_row['u_close']
# Get price 5m, 15m, 30m after signal
price_5m = await get_price_at_time(symbol, signal_time + timedelta(minutes=5))
price_15m = await get_price_at_time(symbol, signal_time + timedelta(minutes=15))
price_30m = await get_price_at_time(symbol, signal_time + timedelta(minutes=30))
# Calculate reactions
reaction_5m = ((price_5m - price_at_signal) / price_at_signal) * 100 if price_5m else None
reaction_15m = ((price_15m - price_at_signal) / price_at_signal) * 100 if price_15m else None
reaction_30m = ((price_30m - price_at_signal) / price_at_signal) * 100 if price_30m else None
# High/Low break confirmation
high_break = price_5m > flow_row.get('u_high', 0)
low_break = price_5m < flow_row.get('u_low', 0)
return {
'price_reaction_5m_pct': reaction_5m,
'price_reaction_15m_pct': reaction_15m,
'price_reaction_30m_pct': reaction_30m,
'high_break_5m': high_break,
'low_break_5m': low_break,
'flow_led_to_move': reaction_5m and abs(reaction_5m) > 0.5 # 0.5% threshold
}
```
**Database Addition:**
- Add columns: `price_reaction_5m_pct`, `price_reaction_15m_pct`, `high_break_5m`, `low_break_5m`
- Add flag: `flow_led_to_move` (boolean)
**Why This Matters:**
- Flow without price reaction = hedge or roll (ignore)
- Flow with price reaction = real positioning (trade it)
---
### 4⃣ Strike Clustering Detection
**Current Gap:** No detection of multiple large trades at the same strike (institutional layering).
**Suggestion:**
- **New service:** `backend/python_service/services/strike_cluster_detector.py`
- Group trades by strike and expiration
- Identify clusters: 3+ trades at same strike within 30 minutes
- Calculate cluster premium total
- Flag as "institutional positioning" vs "single trade"
**Implementation Approach:**
```python
class StrikeClusterDetector:
def detect_clusters(self, df: pd.DataFrame, window_minutes: int = 30):
"""Detect strike clusters within time window"""
df = df.copy()
# Group by symbol, exp_date, strike
clusters = df.groupby(['symbol_norm', 'exp_date', 'strike_num']).apply(
lambda g: self._find_clusters_in_group(g, window_minutes)
)
return clusters
def _find_clusters_in_group(self, group, window_minutes):
"""Find time-based clusters within a strike group"""
# Sort by time
group = group.sort_values('flow_ts_utc')
# Rolling window: if 3+ trades within window_minutes, it's a cluster
# Return cluster flags and cluster IDs
```
**New Fields:**
- `is_cluster_trade` - Boolean
- `cluster_id` - Unique ID for the cluster
- `cluster_size` - Number of trades in cluster
- `cluster_total_premium` - Sum of all premiums in cluster
**Why This Matters:**
- Institutions rarely place one order — they layer
- Clusters = stronger signal than single prints
---
### 5⃣ Gamma Exposure (GEX) Calculation
**Current Gap:** No gamma exposure tracking. This explains why some rockets fail.
**Suggestion:**
- **New service:** `backend/python_service/services/gamma_calculator.py`
- Calculate call GEX and put GEX per strike
- Net dealer gamma = Call GEX - Put GEX
- Positive GEX = price pinned (resistance)
- Negative GEX = explosive moves possible
**Implementation Approach:**
```python
class GammaCalculator:
def calculate_gex(self, df: pd.DataFrame):
"""
Calculate Gamma Exposure (GEX)
GEX = OI * Spot^2 * Gamma * 0.01 * Multiplier
Simplified: GEX ≈ OI * Spot^2 * 0.01 (for rough estimate)
"""
# For each strike, calculate:
# - Call GEX (positive for calls)
# - Put GEX (negative for puts)
# - Net GEX = Call GEX + Put GEX
# Add to flow row:
# - strike_gex (GEX at this strike)
# - net_dealer_gex (aggregate GEX for symbol)
# - gex_pin_level (strike with highest GEX)
```
**New Fields:**
- `strike_gex` - GEX at this strike
- `net_dealer_gex` - Net GEX for the symbol
- `gex_pin_level` - Strike where GEX is highest (pin level)
- `is_gex_positive` - Boolean: positive GEX = pinning, negative = explosive
**Why This Matters:**
- +GEX = Price pinned (rockets may fail at pin level)
- -GEX = Explosive moves (rockets more likely to work)
---
### 6⃣ Delta Weighting (Smart Money Filter)
**Current Gap:** No delta weighting. ITM delta > OTM lottery tickets.
**Suggestion:**
- **Extend:** `backend/python_service/services/options_flow_processor.py`
- Add delta calculation (approximate: use Black-Scholes or simplified formula)
- Calculate: `delta_weighted_premium = delta * volume * premium`
- Filter out low delta-weighted trades (YOLO prints)
**Implementation Approach:**
```python
def calculate_delta_weighted_value(row):
"""Calculate delta-weighted premium value"""
# Simplified delta approximation:
# For CALL: delta ≈ N(d1) where d1 = (ln(S/K) + (r+σ²/2)*T) / (σ*√T)
# For rough estimate: delta ≈ 0.5 for ATM, 0.8+ for ITM, 0.2- for OTM
spot = row.get('spot_num', 0)
strike = row.get('strike_num', 0)
cp = row.get('cp_norm', '')
moneyness = row.get('moneyness', '')
# Simplified delta based on moneyness
if moneyness == 'ITM':
delta = 0.7 if cp == 'CALL' else 0.7
elif moneyness == 'OTM':
delta = 0.3 if cp == 'CALL' else 0.3
else: # ATM
delta = 0.5
volume = row.get('vol_num', 0) or 0
premium = row.get('premium_num', 0) or 0
return delta * volume * premium
```
**New Fields:**
- `delta_approx` - Approximate delta value
- `delta_weighted_premium` - Delta * Volume * Premium
- `is_smart_money` - Boolean: delta_weighted_premium > threshold
**Why This Matters:**
- Filters out YOLO OTM lottery prints
- ITM delta > OTM = real positioning
---
### 7⃣ Time-to-Expiration Buckets
**Current Gap:** No DTE-based classification.
**Suggestion:**
- **Extend:** `backend/python_service/services/options_flow_processor.py`
- Calculate DTE (days to expiration)
- Bucket into: 0DTE, 1-3 DTE, 7-14 DTE, Monthly
- Different logic per bucket
**Implementation Approach:**
```python
def calculate_dte_bucket(row):
"""Calculate days to expiration and bucket"""
exp_date = row.get('exp_date')
flow_date = row.get('flow_date_cst')
if not exp_date or not flow_date:
return None
if isinstance(flow_date, datetime):
flow_date = flow_date.date()
if isinstance(exp_date, datetime):
exp_date = exp_date.date()
dte = (exp_date - flow_date).days
if dte == 0:
return '0DTE'
elif 1 <= dte <= 3:
return '1-3DTE'
elif 4 <= dte <= 6:
return '4-6DTE'
elif 7 <= dte <= 14:
return '7-14DTE'
elif 15 <= dte <= 30:
return 'MONTHLY'
else:
return 'LONG_TERM'
```
**New Fields:**
- `dte` - Days to expiration
- `dte_bucket` - Bucket classification
- `is_0dte` - Boolean flag
**Why This Matters:**
- 0DTE → intraday pressure (gamma risk)
- Longer DTE → directional thesis (less gamma risk)
---
### 8⃣ Sweep vs Block Detection
**Current Gap:** No distinction between sweeps (urgency) and blocks (positioning).
**Suggestion:**
- **New service:** `backend/python_service/services/trade_type_detector.py`
- Detect multiple trades at same strike/expiration within 2 seconds = SWEEP
- Single large trade = BLOCK
- Different trading implications
**Implementation Approach:**
```python
class TradeTypeDetector:
def detect_trade_type(self, df: pd.DataFrame):
"""Detect if trade is sweep or block"""
df = df.copy()
df = df.sort_values(['symbol_norm', 'exp_date', 'strike_num', 'flow_ts_utc'])
# Group by symbol, exp, strike
groups = df.groupby(['symbol_norm', 'exp_date', 'strike_num'])
def classify_group(group):
# If multiple trades within 2 seconds = sweep
# If single large trade = block
# Otherwise = regular trade
if len(group) == 1:
return 'BLOCK' if group.iloc[0]['premium_num'] > 500000 else 'REGULAR'
# Check time differences
time_diffs = group['flow_ts_utc'].diff().dt.total_seconds()
has_sweep = (time_diffs <= 2).any()
if has_sweep:
return 'SWEEP'
else:
return 'CLUSTER'
df['trade_type'] = groups.apply(classify_group).values
return df
```
**New Fields:**
- `trade_type` - 'SWEEP', 'BLOCK', 'CLUSTER', 'REGULAR'
- `is_sweep` - Boolean
- `is_block` - Boolean
**Why This Matters:**
- Sweeps = urgency (institutions hitting multiple exchanges)
- Blocks = positioning (single large order)
---
### 9⃣ Historical Win Rate Tracking
**Current Gap:** No tracking of which patterns actually work.
**Suggestion:**
- **New service:** `backend/python_service/services/pattern_analyzer.py`
- Track pattern → outcome mapping
- Calculate win rate per pattern
- Average return per pattern
- Max drawdown per pattern
**Database Addition:**
- **New table:** `signal_patterns_history`
- Columns: pattern_hash, signal_time, price_at_signal, price_5m_after, price_15m_after, outcome, return_pct
**Implementation Approach:**
```python
class PatternAnalyzer:
def track_pattern(self, flow_row, price_reaction):
"""Track pattern and outcome"""
pattern_hash = self._hash_pattern(flow_row)
# Store in database:
# - Pattern signature (badge combo + premium tier + DTE)
# - Outcome (price reaction)
# - Return percentage
def get_pattern_stats(self, pattern_hash):
"""Get historical stats for a pattern"""
# Query database for all instances of this pattern
# Calculate: win_rate, avg_return, max_drawdown
```
**New Fields:**
- `pattern_hash` - Unique identifier for pattern
- `historical_win_rate` - Win rate for this pattern
- `historical_avg_return` - Average return for this pattern
- `pattern_confidence` - Confidence based on historical performance
**Why This Matters:**
- Discover which patterns actually work
- 🚀🚀 without 💎 fails more often
- 🟢💎⭐ + VWAP reclaim wins most
---
### 🔟 Index & Correlation Filter
**Current Gap:** No SPY/QQQ/VIX alignment check.
**Suggestion:**
- **New service:** `backend/python_service/services/index_correlation.py`
- Fetch SPY/QQQ flow at signal time
- Check VIX direction
- Rule: Single stock flow works best when index agrees
**Implementation Approach:**
```python
class IndexCorrelationService:
async def check_index_alignment(self, flow_row, pool):
"""Check if index flow aligns with stock flow"""
symbol = flow_row['symbol_norm']
signal_time = flow_row['flow_ts_utc']
direction = flow_row['direction']
# Get SPY/QQQ flow in same time window
spy_flow = await self.get_index_flow('SPY', signal_time, pool)
qqq_flow = await self.get_index_flow('QQQ', signal_time, pool)
# Get VIX direction
vix_direction = await self.get_vix_direction(signal_time, pool)
# Check alignment
index_bullish = (spy_flow.get('net_premium', 0) > 0) or (qqq_flow.get('net_premium', 0) > 0)
index_bearish = (spy_flow.get('net_premium', 0) < 0) or (qqq_flow.get('net_premium', 0) < 0)
aligned = (
(direction == 'BULL' and index_bullish) or
(direction == 'BEAR' and index_bearish)
)
return {
'index_aligned': aligned,
'spy_flow_direction': 'BULL' if spy_flow.get('net_premium', 0) > 0 else 'BEAR',
'qqq_flow_direction': 'BULL' if qqq_flow.get('net_premium', 0) > 0 else 'BEAR',
'vix_direction': vix_direction
}
```
**New Fields:**
- `index_aligned` - Boolean: does index flow agree?
- `spy_flow_direction` - SPY flow direction
- `qqq_flow_direction` - QQQ flow direction
- `vix_direction` - VIX direction (up/down)
**Why This Matters:**
- Single stock flow works best when index agrees
- Contrarian flow (stock vs index) = lower probability
---
## PART B — TRADE CHECKLIST IMPLEMENTATION
### Trade Entry Checklist
**Suggestion:**
- **New service:** `backend/python_service/services/trade_checklist.py`
- Implement 5-point checklist
- Return checklist score (0-5)
- Only allow trades with 4/5 or 5/5
**Implementation Approach:**
```python
class TradeChecklist:
def evaluate(self, flow_row):
"""Evaluate trade checklist"""
checks = {
'has_direction': flow_row.get('badge_round') in ['🟢', '🔴'],
'has_diamond': '💎' in flow_row.get('badge_more', ''),
'has_star': '⭐' in flow_row.get('badge_more', ''),
'price_respects_vwap': self._check_vwap_respect(flow_row),
'index_confirms': flow_row.get('index_aligned', False)
}
score = sum(checks.values())
passed = score >= 4
return {
'checklist_score': score,
'checklist_passed': passed,
'checks': checks
}
```
**New Fields:**
- `checklist_score` - 0-5 score
- `checklist_passed` - Boolean: 4/5 or 5/5
- `checklist_details` - JSON with individual check results
---
## PART C — ENHANCED ENTRY/EXIT LOGIC
### Entry Strategy Enhancement
**Current Gap:** Entry logic exists but doesn't use VWAP pullback/reclaim.
**Suggestion:**
- **Extend:** `backend/src/services/tradePlanGenerator.js`
- Add VWAP pullback entry
- Add VWAP reclaim entry
- Add prior high break entry
- Avoid chasing vertical candles
**Implementation:**
```javascript
function generateEntryStrategy(signal, currentPrice, priceContext) {
const vwap = priceContext.vwap;
const priorHigh = priceContext.priorHigh;
const vwapDistance = ((currentPrice - vwap) / vwap) * 100;
if (signal === 'BUY') {
// Best: VWAP pullback or VWAP reclaim
if (currentPrice < vwap && vwapDistance > -1) {
return {
type: 'VWAP_PULLBACK',
entry: vwap * 0.998, // Slightly below VWAP
reason: 'VWAP pullback entry'
};
}
// Good: Break & hold above prior high
if (currentPrice > priorHigh) {
return {
type: 'BREAKOUT',
entry: priorHigh * 1.001, // Slightly above prior high
reason: 'Prior high breakout'
};
}
// Avoid: Chasing vertical candles
if (vwapDistance > 2) {
return {
type: 'WAIT',
reason: 'Price too extended from VWAP - wait for pullback'
};
}
}
// Similar for SELL signals...
}
```
---
### Exit Strategy Enhancement
**Current Gap:** Exit logic is basic. Need flow-based exits.
**Suggestion:**
- **Extend:** `backend/src/services/tradePlanGenerator.js`
- Exit when flow stalls
- Exit when opposite 💎 appears
- Exit when net premium flips
- Exit when price rejects VWAP
- Scale out at +30-50% option gain
**Implementation:**
```javascript
function generateExitStrategy(signal, entryPrice, currentPrice, flowData) {
const exits = [];
// Flow stalls
if (flowData.recentFlowVolume < flowData.avgFlowVolume * 0.3) {
exits.push({
type: 'FLOW_STALL',
reason: 'Flow volume dropped significantly'
});
}
// Opposite diamond appears
if (signal === 'BUY' && flowData.hasBearDiamond) {
exits.push({
type: 'OPPOSITE_SIGNAL',
reason: 'Bear diamond (💎) appeared - exit long'
});
}
// Net premium flips
if (signal === 'BUY' && flowData.netPremium < 0) {
exits.push({
type: 'PREMIUM_FLIP',
reason: 'Net premium flipped negative'
});
}
// Price rejects VWAP
if (currentPrice < priceContext.vwap && signal === 'BUY') {
exits.push({
type: 'VWAP_REJECTION',
reason: 'Price rejected VWAP - exit'
});
}
// Scale out at gains
const gainPct = ((currentPrice - entryPrice) / entryPrice) * 100;
if (gainPct >= 30) {
exits.push({
type: 'SCALE_OUT',
reason: `+${gainPct.toFixed(1)}% gain - scale out 50%`
});
}
return exits;
}
```
---
## PART D — DATABASE SCHEMA ADDITIONS
### New Columns for `processed_options_flow` (or new enrichment table)
```sql
-- Signal classification
signal_tier VARCHAR(10), -- 'TIER_1', 'TIER_2', 'IGNORE'
is_tradeable BOOLEAN,
-- VWAP
vwap_at_signal NUMERIC,
price_vs_vwap_pct NUMERIC,
vwap_reclaimed BOOLEAN,
-- Price reaction
price_reaction_5m_pct NUMERIC,
price_reaction_15m_pct NUMERIC,
price_reaction_30m_pct NUMERIC,
high_break_5m BOOLEAN,
low_break_5m BOOLEAN,
flow_led_to_move BOOLEAN,
-- Strike clustering
is_cluster_trade BOOLEAN,
cluster_id VARCHAR(50),
cluster_size INTEGER,
cluster_total_premium NUMERIC,
-- Gamma exposure
strike_gex NUMERIC,
net_dealer_gex NUMERIC,
gex_pin_level NUMERIC,
is_gex_positive BOOLEAN,
-- Delta weighting
delta_approx NUMERIC,
delta_weighted_premium NUMERIC,
is_smart_money BOOLEAN,
-- DTE
dte INTEGER,
dte_bucket VARCHAR(20),
is_0dte BOOLEAN,
-- Trade type
trade_type VARCHAR(20), -- 'SWEEP', 'BLOCK', 'CLUSTER', 'REGULAR'
is_sweep BOOLEAN,
is_block BOOLEAN,
-- Index correlation
index_aligned BOOLEAN,
spy_flow_direction VARCHAR(10),
qqq_flow_direction VARCHAR(10),
vix_direction VARCHAR(10),
-- Checklist
checklist_score INTEGER,
checklist_passed BOOLEAN,
checklist_details JSONB,
-- Pattern tracking
pattern_hash VARCHAR(100),
historical_win_rate NUMERIC,
historical_avg_return NUMERIC,
pattern_confidence NUMERIC
```
---
## PART E — IMPLEMENTATION PRIORITY
### Phase 1 (Highest Impact - Do First)
1. ✅ **Price Reaction Tracking** - Most important filter
2. ✅ **VWAP Integration** - Critical for entry/exit
3. ✅ **Signal Tier Classification** - Filter noise
4. ✅ **Trade Checklist** - Prevent bad trades
### Phase 2 (High Value)
5. ✅ **Strike Clustering** - Identify institutional layering
6. ✅ **Delta Weighting** - Filter YOLO prints
7. ✅ **Index Correlation** - Context filter
### Phase 3 (Nice to Have)
8. ✅ **Gamma Exposure** - Explains pinning behavior
9. ✅ **Sweep vs Block** - Trade type classification
10. ✅ **DTE Buckets** - Time-based filtering
### Phase 4 (Analytics)
11. ✅ **Historical Win Rate** - Pattern analysis
12. ✅ **Enhanced Entry/Exit** - Refine trading logic
---
## PART F — API ENDPOINT SUGGESTIONS
### New Endpoints to Add
1. **`GET /api/options-flow/enhanced`**
- Returns flow with all new enrichments
- Parameters: `include_price_reaction`, `include_gex`, etc.
2. **`GET /api/options-flow/checklist`**
- Returns only signals that pass checklist (4/5 or 5/5)
3. **`GET /api/options-flow/tier-1`**
- Returns only Tier-1 tradeable signals
4. **`GET /api/patterns/stats`**
- Returns historical win rates per pattern
5. **`GET /api/options-flow/vwap-analysis`**
- Returns VWAP-based entry opportunities
---
## PART G — FRONTEND DISPLAY SUGGESTIONS
### New UI Elements to Add
1. **Signal Tier Badge**
- Display "TIER-1", "TIER-2", or "IGNORE" badge
- Color code: Green (Tier-1), Yellow (Tier-2), Gray (Ignore)
2. **Price Reaction Indicator**
- Show 5m/15m price reaction percentage
- Green if positive reaction, Red if negative
- "Flow Led to Move" indicator
3. **VWAP Distance Display**
- Show current price vs VWAP
- Visual indicator: Above/Below VWAP
- Entry opportunity: "VWAP Pullback" or "VWAP Reclaim"
4. **Checklist Score Display**
- Show checklist score (X/5)
- Green if passed (4/5+), Red if failed
- Expandable details showing each check
5. **Index Alignment Indicator**
- Show SPY/QQQ flow direction
- Show if aligned (green) or not (red)
6. **Gamma Pin Level**
- Display GEX pin level on chart
- Show if price is near pin (resistance)
7. **Strike Cluster Visualization**
- Show cluster size and total premium
- Highlight clustered strikes
---
## PART H — TESTING SUGGESTIONS
### Test Cases to Add
1. **Price Reaction Tests**
- Test: Flow with no price reaction = should be filtered
- Test: Flow with 5m price reaction = should be tradeable
2. **Tier Classification Tests**
- Test: 🟢 + 💎 + ⭐ + premium > 500K = Tier-1
- Test: 🟢 + 💎 (no ⭐) = Tier-2
- Test: OTM-only = Ignore
3. **Checklist Tests**
- Test: 4/5 checks = passed
- Test: 3/5 checks = failed
4. **VWAP Tests**
- Test: VWAP pullback entry detection
- Test: VWAP reclaim entry detection
---
## SUMMARY
### Key Takeaways
1. **Price Reaction is #1 Priority** - This filters out hedges/rolls
2. **VWAP Integration is Critical** - Needed for proper entry/exit
3. **Tier Classification Reduces Noise** - Focus on Tier-1 signals
4. **Checklist Prevents Bad Trades** - Enforce 4/5 minimum
5. **Strike Clustering Identifies Institutions** - Multiple trades = stronger signal
6. **Index Correlation Adds Context** - Single stock works best with index alignment
### Implementation Strategy
- Start with Phase 1 (Price Reaction + VWAP + Tier Classification + Checklist)
- These 4 features will have the biggest impact on trade quality
- Then move to Phase 2 for additional filtering
- Phase 3 and 4 can be added over time as analytics improve
### Expected Outcomes
- **Higher Win Rate**: Filtering out hedges/rolls and low-quality signals
- **Better Entries**: VWAP-based entry logic
- **Better Exits**: Flow-based exit signals
- **Reduced Noise**: Tier classification and checklist
- **Institutional Detection**: Strike clustering and delta weighting
---
**Note:** All suggestions are additive - they don't change existing code, just extend it with new services and enrichments.

View File

@ -1,241 +0,0 @@
# Testing Guide for Historical Data & Backtesting
## Prerequisites
1. Database connection configured (see `.env` file)
2. Node.js installed and in PATH
3. Python 3.x installed with required packages:
- `yfinance`
- `pandas`
- `psycopg2`
- `python-dotenv`
## Step 1: Validate Database Schema
Run the schema validation script to ensure all tables and indexes exist:
```bash
cd backend
node scripts/validateSchema.js
```
**Expected Output:**
- ✅ All required tables exist
- ✅ Indexes on critical tables
- ✅ PRIMARY KEY constraints verified
- ⚠️ Any warnings about data types
**If issues are found:**
- Run `backend/database/schema.sql` in your database
- Run `backend/database/missing_tables.sql` if needed
## Step 2: Pull Historical Data
### Test with Single Symbol First
```bash
python yahhooscript.py --only AAPL
```
This will:
- Pull 30 days of 5-minute intraday data for AAPL
- Pull daily data (2 years) for AAPL
- Store in PostgreSQL database
**Verify data was pulled:**
```sql
-- Check intraday data
SELECT COUNT(*), MIN(ts), MAX(ts)
FROM prices_intraday_1m
WHERE symbol = 'AAPL'
AND ts >= NOW() - INTERVAL '30 days';
-- Check daily data
SELECT COUNT(*), MIN("Date"), MAX("Date")
FROM prices_daily
WHERE symbol = 'AAPL';
```
### Pull Full Universe
```bash
python yahhooscript.py
```
This will pull data for all symbols in your universe (S&P 500, S&P 400, Nasdaq-100, plus custom groups).
**Note:** This may take 30-60 minutes depending on:
- Number of symbols
- Yahoo Finance rate limiting
- Network speed
**Monitor progress:**
- Script shows batch progress
- Check for errors in output
- Verify data counts in database
## Step 3: Test Backtester
### Run Test Script
```bash
cd backend
node scripts/testBacktester.js
```
This will:
- Test database connectivity
- Check price data availability
- Run a sample backtest
- Verify enhanced metrics are calculated
### Test via API
Start the backend server:
```bash
cd backend
npm run dev
```
**Run Single Backtest:**
```bash
curl -X POST http://localhost:3010/api/backtest/run \
-H "Content-Type: application/json" \
-d '{
"pattern": {
"rocketScoreMin": 5.0,
"session": "RTH"
},
"options": {
"lookbackDays": 30,
"targetPct": 1.5,
"stopPct": 1.5
}
}'
```
**Compare Strategies:**
```bash
curl -X POST http://localhost:3010/api/backtest/compare \
-H "Content-Type: application/json" \
-d '{
"strategies": [
{
"name": "High Score RTH",
"pattern": { "rocketScoreMin": 5, "session": "RTH" }
},
{
"name": "Tape Aligned",
"pattern": { "rocketScoreMin": 3, "tapeAligned": true }
}
],
"options": {
"lookbackDays": 30
}
}'
```
## Step 4: Validate Results
### Check Metrics
Verify that all new metrics are being calculated:
```javascript
{
"winRate": 70.1,
"avgWin": 2.7,
"avgLoss": -1.3,
"expectancy": 1.5,
"sharpeRatio": 1.23, // ✅ New
"maxDrawdown": 15.0, // ✅ New
"streaks": { // ✅ New
"maxWinStreak": 5,
"maxLossStreak": 2
},
"sessionPerformance": [ // ✅ New
{ "session": "RTH", "winRate": 72.5, "totalTrades": 80 }
],
"symbolPerformance": { // ✅ New
"best": [{"symbol": "AAPL", "avgReturn": 3.2}],
"worst": [{"symbol": "TSLA", "avgReturn": -1.5}]
}
}
```
### Verify Data Quality
```sql
-- Check data completeness for a symbol
SELECT
symbol,
COUNT(*) as total_bars,
COUNT(DISTINCT DATE(ts)) as trading_days,
MIN(ts) as earliest,
MAX(ts) as latest
FROM prices_intraday_1m
WHERE symbol = 'AAPL'
AND ts >= NOW() - INTERVAL '30 days'
GROUP BY symbol;
-- Should show ~20-22 trading days (excluding weekends/holidays)
-- Should show ~390 bars per day for 5-minute data (6.5 hours * 12 bars/hour)
```
## Troubleshooting
### No Data Found in Backtest
**Possible causes:**
1. No options flow data in date range
- Check: `SELECT COUNT(*) FROM "OptionsFlow_monthly" WHERE "CreatedDate" >= CURRENT_DATE - INTERVAL '30 days'`
2. Pattern criteria too strict
- Try lowering `rocketScoreMin` or removing filters
3. No price data available
- Run: `python yahhooscript.py --only SYMBOL`
### Yahoo Finance Rate Limiting
**Symptoms:**
- Empty data returned
- Timeout errors
- JSON parsing errors
**Solutions:**
1. Increase `SLEEP_BETWEEN_BATCHES` in `yahhooscript.py`
2. Reduce `BATCH_SIZE` (try 50 instead of 110)
3. Run during off-peak hours
4. Use VPN if IP is rate-limited
### Database Connection Issues
**Check:**
1. `.env` file has correct credentials
2. Database is not paused (Supabase)
3. Network connectivity
4. Firewall rules
**Test connection:**
```bash
cd backend
node -e "import('./src/db.js').then(m => m.testConnection())"
```
## Success Criteria
✅ Schema validation passes
✅ 30 days of historical data pulled
✅ Backtester runs without errors
✅ All enhanced metrics are calculated
✅ API endpoints return valid results
✅ Data quality checks pass
## Next Steps
After successful testing:
1. Run full universe data pull (if not done)
2. Test with various pattern combinations
3. Compare multiple strategies
4. Analyze results and refine patterns
5. Set up scheduled data pulls (optional)

View File

@ -1,288 +0,0 @@
# Trade Plan Calculation - Data Flow & Sources
## Overview
The trade plan is automatically generated for every options flow signal with a rocket score >= 3. It calculates entry prices, stop losses, profit targets, and risk/reward ratios based on real-time price data.
## Entry Point
**File:** `backend/src/routes/optionsFlow.js` (line 228-233)
```javascript
if (rocketScore >= 3) {
const tradePlan = generateTradePlan(enrichedRow);
enrichedRow.tradePlan = tradePlan;
enrichedRow.tradePlanDisplay = formatTradePlanDisplay(tradePlan);
}
```
## Data Sources
### 1. **Current Price** (Primary Reference)
**Sources (in priority order):**
- **`flow.yahooPriceData.currentPrice`** - **REAL-TIME from Yahoo Finance API** ⭐ (PRIMARY)
- `flow.spot_num` - Spot price from flow data (fallback)
- `flow.u_close` - Close price at signal time (from database, may be stale)
- `flow.last_px` - Last price (fallback)
- `flow.Price` - Fallback price field
**Yahoo Finance API:** `https://query1.finance.yahoo.com/v8/finance/chart/{symbol}?interval=1d&range=5d`
- Returns real-time `regularMarketPrice` or `previousClose`
- **This is the PRIMARY source for current price** (not database)
### 2. **VWAP (Volume Weighted Average Price)**
**Sources:**
- `flow.vwap_at_signal` - VWAP calculated at signal time (from Python service)
- `flow.vwap_lb` - VWAP from SQL query
- `flow.vwap` - Fallback
**Calculation:**
- Sum of (price × volume) from RTH open to signal time / Sum of volume
- **Note:** VWAP may still come from database/Python service, but entry prices use Yahoo Finance current price
### 3. **RTH Open (Regular Trading Hours Open)**
**Sources (in priority order):**
- **`flow.yahooPriceData.open`** - **REAL-TIME from Yahoo Finance API** ⭐ (PRIMARY)
- `flow.rth_open` - From database (fallback, may be stale)
- Falls back to current price if unavailable
**Yahoo Finance API:** Returns `regularMarketOpen` for today
### 4. **Opening Range (OR High/Low)**
**Sources (in priority order):**
- **`flow.yahooPriceData.high` / `flow.yahooPriceData.low`** - **REAL-TIME from Yahoo Finance API** ⭐ (PRIMARY)
- Uses today's high/low as OR levels (more accurate than stale database data)
- `flow.or_high` / `flow.or_low` - From database (fallback, may be stale)
- Calculated fallback: `currentPrice * 1.01` (high) or `currentPrice * 0.99` (low)
**Yahoo Finance API:** Returns `regularMarketDayHigh` and `regularMarketDayLow` for today
- **Validation:** Only used if within 10% of current price (to avoid stale data)
### 5. **Prior High**
**Sources (in priority order):**
- **`flow.yahooPriceData.volumeHistory[1].high`** - **REAL-TIME from Yahoo Finance API** ⭐ (PRIMARY)
- Gets yesterday's high from volumeHistory (last 5 days data)
- `flow.prior_high` - From database (fallback, may be stale)
- `flow.priorHigh` - Alternative field name
- **Fallback:** `currentPrice * 1.02` (2% above current)
**Yahoo Finance API:** Returns volumeHistory with last 5 days including high/low/close
- **Validation:** Only used if above current price and within 10% above
### 6. **Prior Close**
**Sources (in priority order):**
- **`flow.yahooPriceData.previousClose`** - **REAL-TIME from Yahoo Finance API** ⭐ (PRIMARY)
- `flow.prior_close` or `flow.priorClose` - From database (fallback, may be stale)
**Yahoo Finance API:** Returns `previousClose` for previous trading day
### 7. **Strike Price**
**Sources:**
- `flow.strike_num` - Strike price from options flow
- `flow.Strike` - Alternative field name
- **Validation:** Only used for T3 target if within 20% of current price
## Calculation Flow
### Step 1: Determine Signal (BUY/SELL/WAIT)
**Function:** `determineSignal(flowData)`
- Based on badges (🟢/🔴, 💎, ⭐, 💰, ⚡)
- Checks tape alignment
- Checks rocket score
### Step 2: Generate Entry Strategy
**Function:** `generateEntryStrategy(flowData, signal)`
#### For BUY Signals:
- **Primary Entry:** VWAP (if available) OR current price
- **Aggressive Entry:** OR high (if reasonable) OR current price * 1.005
- **Conditional Triggers:**
- VWAP reclaim (if price < VWAP)
- Prior high break (if price < prior high)
- OR breakout (if price < OR high)
#### For SELL Signals:
- **Primary Entry:** VWAP (if available) OR current price
- **Aggressive Entry:** OR low (if reasonable) OR current price * 0.995
- **Conditional Triggers:**
- VWAP rejection (if price > VWAP)
- OR breakdown (if price > OR low)
#### For WAIT Signals:
- **Primary Entry:** `null` (no entry yet)
- **Conditional Triggers:**
- Price breaks prior high (if within 10% above current)
- OR breakout (if within 5% above current)
- VWAP reclaim (if within 5% of current)
- Tape alignment turns positive
- Flow resumes (if flow is dead/stale)
### Step 3: Calculate Stop Loss
**Function:** `calculateStopLoss(flow, entryPrice)`
**Reference Price:**
- Uses `entryPrice` if available (for BUY/SELL)
- Uses `currentPrice` if no entry (for WAIT signals)
#### For BULL/BUY Signals:
- **Tight Stop:** `referencePrice * 0.985` (-1.5%)
- **Wide Stop:**
- OR low (if within 5% of reference price) OR
- `referencePrice * 0.975` (-2.5%)
#### For BEAR/SELL Signals:
- **Tight Stop:** `referencePrice * 1.015` (+1.5%)
- **Wide Stop:**
- OR high (if within 5% of reference price) OR
- `referencePrice * 1.025` (+2.5%)
### Step 4: Calculate Profit Targets
**Function:** `calculateTargets(flow, entryPrice, signal)`
**Reference Price:**
- Uses `entryPrice` if available
- Uses `currentPrice` if no entry (for WAIT signals)
#### For BUY/BULL Signals:
- **T1:** `referencePrice * 1.015` (+1.5%) - Take 50%
- **T2:** `referencePrice * 1.025` (+2.5%) - Take 30%
- **T3:**
- Strike price (if within 20% above reference) OR
- `referencePrice * 1.04` (+4%) - Runner 20%
#### For SELL/BEAR Signals:
- **T1:** `referencePrice * 0.985` (-1.5%) - Take 50%
- **T2:** `referencePrice * 0.975` (-2.5%) - Take 30%
- **T3:**
- Strike price (if within 20% below reference) OR
- `referencePrice * 0.96` (-4%) - Runner 20%
### Step 5: Calculate Risk/Reward
**Function:** `calculateRiskReward(entry, stop, target)`
**Formula:**
- Risk = `|stop - entry| / entry * 100`
- Reward = `|target - entry| / entry * 100`
- Ratio = `reward / risk`
### Step 6: Calculate Readiness (WAIT signals only)
**Function:** `calculateReadiness(plan)`
**Scoring:**
- Confidence * 0.6 (60% weight)
- Entry strategy exists: +15 points
- Stop loss exists: +10 points
- Targets exist: +10 points
- Reasoning exists: +5 points
- **Max:** 100%
## Key Validations
### Price Level Validation
All price levels are validated against current price to avoid stale data:
1. **OR High/Low:** Only used if within 10% of current price
2. **Prior High:** Only used if above current and within 10% above
3. **VWAP:** Only used if within 5% of current price
4. **Strike Price:** Only used for T3 if within 20% of current price
### Fallback Logic
If any price data is missing or stale:
- Uses current price as base
- Calculates percentage-based levels (e.g., +1.5%, -2.5%)
- Never uses unreasonable/stale data
## Example Calculation
**Given:**
- Current Price: $131.81
- VWAP: $130.50
- OR High: $132.00
- OR Low: $130.00
- Strike: $155.00 (too far, ignored)
- Signal: WAIT (BULL direction)
**Entry Strategy:**
- Primary: `null` (WAIT signal)
- Conditional Triggers:
- VWAP reclaim at $130.50 (if price < VWAP)
- OR breakout at $132.00 (if price < OR high)
**Stop Loss (using current price $131.81):**
- Tight: $131.81 * 0.985 = $129.83 (-1.5%)
- Wide: $130.00 (OR low, within 5%)
**Targets (using current price $131.81):**
- T1: $131.81 * 1.015 = $133.79 (+1.5%)
- T2: $131.81 * 1.025 = $135.11 (+2.5%)
- T3: $131.81 * 1.04 = $137.08 (+4%) - Strike $155 ignored (too far)
## Data Fetching
### Yahoo Finance API (PRIMARY SOURCE) ⭐
**Service:** `backend/src/services/yahooFinanceService.js`
**Function:** `batchFetchYahooFinanceData(symbols)`
**API Endpoint:**
```
https://query1.finance.yahoo.com/v8/finance/chart/{symbol}?interval=1d&range=5d
```
**Returns:**
- `currentPrice` - Real-time market price
- `previousClose` - Previous day's close
- `open` - Today's open
- `high` - Today's high
- `low` - Today's low
- `volume` - Today's volume
- `volumeHistory` - Last 5 days with high/low/close/volume
**When Fetched:**
- Fetched in `optionsFlow.js` route before trade plan generation
- Passed to trade plan generator as `yahooPriceData` or `stockPriceData`
- **This is the PRIMARY source** - database is only used as fallback
### Database Queries (FALLBACK ONLY)
**Note:** Database queries are only used if Yahoo Finance data is unavailable
### Price at Signal Time (Fallback)
```sql
SELECT close, high, low, volume, ts
FROM prices_intraday_1m
WHERE UPPER(symbol) = UPPER($1)
AND ts <= $2 -- signal time
ORDER BY ts DESC
LIMIT 1
```
### RTH Open (Fallback)
```sql
SELECT open, ts
FROM prices_intraday_1m
WHERE UPPER(symbol) = UPPER($1)
AND (timezone('America/Chicago', ts))::date = $2 -- flow date
AND (timezone('America/Chicago', ts))::time >= time '09:30:00'
ORDER BY ts ASC
LIMIT 1
```
### Prior Close (Fallback)
```sql
SELECT close
FROM prices_daily
WHERE UPPER(symbol) = UPPER($1)
AND "Date" = $2 - INTERVAL '1 day' -- prior day
ORDER BY "Date" DESC
LIMIT 1
```
## Summary
**Entry Prices:** Derived from VWAP, OR levels, or current price (from Yahoo Finance)
**Stop Loss:** Percentage-based (-1.5% tight, -2.5% wide) with OR level fallback (from Yahoo Finance)
**Targets:** Percentage-based (+1.5%, +2.5%, +4%) with strike price option
**All calculations:**
- **PRIMARY:** Use Yahoo Finance real-time data (currentPrice, open, high, low, previousClose, volumeHistory)
- **FALLBACK:** Use database data only if Yahoo Finance unavailable
- Validate all levels against current price to avoid stale data
**Key Change:** Trade plans now use **Yahoo Finance API** as the primary data source for all price calculations, ensuring real-time accuracy instead of stale database data.

View File

@ -61,13 +61,13 @@ cp .env.example .env
```bash
# Development mode (with auto-reload)
cd backend/python_service
uvicorn main:app --reload --port 8010
uvicorn main:app --reload --port 8000
# Or use the Python script
python main.py
```
The service will be available at `http://localhost:8010`
The service will be available at `http://localhost:8000`
### 3. Node.js Backend Setup
@ -78,7 +78,7 @@ npm install
# Configure environment
# Add to your .env file:
PYTHON_SERVICE_URL=http://localhost:8010
PYTHON_SERVICE_URL=http://localhost:8000
USE_PYTHON_SERVICE=true # Set to 'false' to disable Python service
```
@ -110,7 +110,7 @@ LOCAL_DB_NAME=institutional_trader
**Node.js Backend (.env in `backend/`):**
```env
# Python Service Configuration
PYTHON_SERVICE_URL=http://localhost:8010
PYTHON_SERVICE_URL=http://localhost:8000
USE_PYTHON_SERVICE=true # Enable/disable Python service
# Existing database configuration...
@ -154,10 +154,10 @@ If Python service is unavailable or disabled:
```bash
# Health check
curl http://localhost:8010/health
curl http://localhost:8000/health
# Get options flow
curl "http://localhost:8010/api/options-flow?start_date=2024-01-01&end_date=2024-01-02"
curl "http://localhost:8000/api/options-flow?start_date=2024-01-01&end_date=2024-01-02"
```
### Test Node.js Integration
@ -186,12 +186,12 @@ curl "http://localhost:3010/api/options/flow?startDate=2024-01-01&endDate=2024-0
1. Check database credentials in `.env`
2. Verify PostgreSQL is running
3. Check port 8010 is not in use
3. Check port 8000 is not in use
4. Review Python service logs
### Node.js Can't Connect to Python Service
1. Verify Python service is running: `curl http://localhost:8010/health`
1. Verify Python service is running: `curl http://localhost:8000/health`
2. Check `PYTHON_SERVICE_URL` in Node.js `.env`
3. Check firewall/network settings
4. Node.js will automatically fallback to SQL if Python unavailable

View File

@ -18,7 +18,7 @@ The hybrid Node.js + Python architecture is now fully integrated and ready for p
│ HTTP
│ (with fallback)
┌──────▼──────────┐
│ Python Service │ ← FastAPI service (port 8010)
│ Python Service │ ← FastAPI service (port 8000)
│ (FastAPI) │ - Data processing
│ │ - Complex analytics
└──────┬──────────┘
@ -61,7 +61,7 @@ The hybrid Node.js + Python architecture is now fully integrated and ready for p
```bash
cd backend/python_service
pip install -r requirements.txt
uvicorn main:app --reload --port 8010
uvicorn main:app --reload --port 8000
```
### 2. Start Node.js Backend
@ -89,7 +89,7 @@ curl "http://localhost:3010/api/options/flow?startDate=2024-01-01&endDate=2024-0
**Node.js (.env in `backend/`):**
```env
# Python Service Configuration
PYTHON_SERVICE_URL=http://localhost:8010
PYTHON_SERVICE_URL=http://localhost:8000
USE_PYTHON_SERVICE=true # Set to 'false' to disable
# Database (existing)
@ -178,7 +178,7 @@ Periodic health checks log status:
1. **Test with Python service:**
```bash
# Start Python service
cd backend/python_service && uvicorn main:app --port 8010
cd backend/python_service && uvicorn main:app --port 8000
# Test endpoint
curl "http://localhost:3010/api/options/flow"
@ -232,7 +232,7 @@ WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8010"]
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
```
```dockerfile
@ -249,7 +249,7 @@ CMD ["npm", "start"]
### Python Service Not Starting
1. Check database credentials
2. Verify port 8010 is available
2. Verify port 8000 is available
3. Check Python dependencies
4. Review service logs

View File

@ -47,7 +47,7 @@ LOCAL_DB_NAME=institutional_trader
### Node.js Backend
Add to `backend/.env`:
```env
PYTHON_SERVICE_URL=http://localhost:8010
PYTHON_SERVICE_URL=http://localhost:8000
USE_PYTHON_SERVICE=true
# Your existing database config...
@ -58,20 +58,13 @@ USE_PYTHON_SERVICE=true
### Terminal 1: Python Service
```bash
cd backend/python_service
# Activate virtual environment:
# Git Bash on Windows:
source venv/Scripts/activate
# PowerShell/CMD on Windows:
venv\Scripts\activate
# Linux/macOS:
source venv/bin/activate
uvicorn main:app --reload --port 8010
source venv/bin/activate # or venv\Scripts\activate on Windows
uvicorn main:app --reload --port 8000
```
You should see:
```
INFO: Uvicorn running on http://0.0.0.0:8010
INFO: Uvicorn running on http://0.0.0.0:8000
INFO: Application startup complete.
```
@ -133,13 +126,13 @@ Check Node.js logs - you should see:
### Python Service Won't Start
- Check database credentials in `.env`
- Verify PostgreSQL is running
- Check port 8010 is available
- Check port 8000 is available
- Review Python service logs
### Node.js Can't Connect
- Verify `PYTHON_SERVICE_URL` in `.env`
- Check Python service is running
- Test: `curl http://localhost:8010/health`
- Test: `curl http://localhost:8000/health`
### No Data Returned
- Check database has data

View File

@ -29,7 +29,7 @@
- Log the fallback for monitoring
**To fix:**
1. Start Python service: `cd backend/python_service && uvicorn main:app --port 8010`
1. Start Python service: `cd backend/python_service && uvicorn main:app --port 8000`
2. Check `PYTHON_SERVICE_URL` in `.env` matches the service URL
3. Verify network connectivity

View File

@ -1,116 +0,0 @@
# Database Date/Time Fix Script
This script fixes date and time format issues in the `OptionsFlow_monthly` and `options_flow` tables.
## What It Fixes
1. **CreatedDate** - Normalizes all dates to `YYYY-MM-DD` format
- Converts `MM/DD/YYYY``YYYY-MM-DD`
- Converts `M/D/YYYY``YYYY-MM-DD`
- Keeps already valid `YYYY-MM-DD` dates
2. **CreatedTime** - Normalizes all times to `HH:MM:SS AM/PM` format with consistent spacing
- Adds space before AM/PM: `"3:59:59PM"``"3:59:59 PM"`
- Fixes missing seconds: `"3:59 PM"``"3:59:00 PM"`
- Fixes missing colon: `"359PM"``"3:59:00 PM"`
- Fixes missing hour: `":59 AM"``"9:59:00 AM"`
3. **ExpirationDate** - Normalizes to `YYYY-MM-DD` format
- Same normalization as CreatedDate
4. **options_flow table** - Fixes invalid timestamps
- Sets NULL for dates outside valid range
- Updates invalid `created_at` to current time
## Usage
### Option 1: Run SQL File Directly
```bash
# Connect to your PostgreSQL database
psql -h localhost -U postgres -d institutional_trader
# Run the fix script
\i backend/database/fix_all_dates_and_times.sql
```
### Option 2: Use Node.js Script
```bash
# Analyze current state (no changes)
node backend/scripts/fixDatabaseDates.js analyze
# Run fixes
node backend/scripts/fixDatabaseDates.js fix
# Verify fixes
node backend/scripts/fixDatabaseDates.js verify
# Run all (analyze, fix, verify)
node backend/scripts/fixDatabaseDates.js all
```
### Option 3: Use in Supabase SQL Editor
1. Open Supabase Dashboard → SQL Editor
2. Copy contents of `fix_all_dates_and_times.sql`
3. Paste and run in SQL Editor
## What to Expect
### Before Fix:
```
CreatedDate: "12/17/2025", "2025-12-17", "12-17-2025"
CreatedTime: "3:59:59PM", "359PM", "3:59 PM", ":59 AM"
```
### After Fix:
```
CreatedDate: "2025-12-17" (all normalized)
CreatedTime: "3:59:59 PM" (all with space, consistent format)
```
## Safety
- **Read-only analysis first**: Run `analyze` command to see what will be changed
- **Non-destructive**: Script only updates format, doesn't delete data
- **Idempotent**: Safe to run multiple times (won't change already-correct data)
- **Verification**: Includes verification queries to check results
## Database Requirements
- PostgreSQL 12+ recommended
- Connection to `OptionsFlow_monthly` table
- Sufficient permissions for UPDATE operations
## Troubleshooting
### Permission Errors
```sql
-- Grant necessary permissions
GRANT UPDATE ON "OptionsFlow_monthly" TO your_user;
```
### Timeout Errors
```sql
-- For large tables, increase timeout
SET statement_timeout = '10min';
```
### Check Specific Records
```sql
-- See problematic entries
SELECT "CreatedDate", "CreatedTime", "Symbol"
FROM "OptionsFlow_monthly"
WHERE "CreatedTime"::text !~ '^\d{1,2}:\d{2}(:\d{2})?\s+[AP]M$'
AND "CreatedTime" IS NOT NULL
LIMIT 100;
```
## Notes
- Script preserves NULL values (doesn't change them)
- Ambiguous formats (like "12 AM" - could be hour or minute) default to hour format
- Large tables may take several minutes to update
- Recommended to backup database before running on production

View File

@ -1,37 +0,0 @@
-- Check for CreatedTime entries that might still cause parsing issues
-- Specifically looking for times that don't have a space before AM/PM
-- Find times without space before AM/PM
SELECT
'Times without space before AM/PM' as issue_type,
COUNT(*) as count
FROM "OptionsFlow_monthly"
WHERE "CreatedTime" IS NOT NULL
AND "CreatedTime"::text ~ '[0-9][AP]M$' -- Ends with digit followed by AM/PM (no space)
AND "CreatedTime"::text !~ '\s+[AP]M$'; -- Doesn't have space before AM/PM
-- Show sample of problematic entries
SELECT
"CreatedDate",
"CreatedTime",
"Symbol",
COUNT(*) as count
FROM "OptionsFlow_monthly"
WHERE "CreatedTime" IS NOT NULL
AND "CreatedTime"::text ~ '[0-9][AP]M$' -- Ends with digit followed by AM/PM (no space)
AND "CreatedTime"::text !~ '\s+[AP]M$' -- Doesn't have space before AM/PM
GROUP BY "CreatedDate", "CreatedTime", "Symbol"
ORDER BY count DESC
LIMIT 50;
-- Check for times with inconsistent spacing patterns
SELECT
'Detailed spacing analysis' as check_type,
COUNT(*) FILTER (WHERE "CreatedTime"::text ~ ':\d{2}:\d{2}[AP]M$') as no_space_seconds,
COUNT(*) FILTER (WHERE "CreatedTime"::text ~ ':\d{2}[AP]M$') as no_space_minutes,
COUNT(*) FILTER (WHERE "CreatedTime"::text ~ '\d{1,2}[AP]M$') as no_space_hour_only,
COUNT(*) FILTER (WHERE "CreatedTime"::text ~ '\s+[AP]M$') as has_space
FROM "OptionsFlow_monthly"
WHERE "CreatedTime" IS NOT NULL
AND "CreatedTime"::text ~* '[AP]M$';

View File

@ -1,249 +0,0 @@
-- ============================================
-- COMPREHENSIVE DATE/TIME FIX SCRIPT
-- Fixes all date and time format issues in OptionsFlow_monthly and options_flow tables
-- ============================================
-- This script:
-- 1. Identifies problematic date/time entries
-- 2. Normalizes CreatedDate to YYYY-MM-DD format
-- 3. Normalizes CreatedTime to HH:MM:SS AM/PM format (with consistent spacing)
-- 4. Normalizes ExpirationDate to YYYY-MM-DD format
-- 5. Fixes timestamp issues in options_flow table
-- ============================================
-- ============================================
-- STEP 1: ANALYSIS - Identify problematic entries
-- ============================================
-- Check CreatedDate formats
SELECT
'CreatedDate Analysis' as check_type,
COUNT(*) FILTER (WHERE "CreatedDate"::text ~ '^\d{4}-\d{2}-\d{2}$') as valid_iso_date,
COUNT(*) FILTER (WHERE "CreatedDate"::text ~ '^\d{1,2}/\d{1,2}/\d{2,4}$') as slash_format,
COUNT(*) FILTER (WHERE "CreatedDate" IS NOT NULL AND "CreatedDate"::text !~ '^\d{4}-\d{2}-\d{2}$' AND "CreatedDate"::text !~ '^\d{1,2}/\d{1,2}/\d{2,4}$') as invalid_format,
COUNT(*) FILTER (WHERE "CreatedDate" IS NULL) as null_dates
FROM "OptionsFlow_monthly";
-- Check CreatedTime formats
SELECT
'CreatedTime Analysis' as check_type,
COUNT(*) FILTER (WHERE "CreatedTime"::text ~ '^\d{1,2}:\d{2}:\d{2}\s+[AP]M$') as valid_with_space,
COUNT(*) FILTER (WHERE "CreatedTime"::text ~ '^\d{1,2}:\d{2}:\d{2}[AP]M$') as valid_no_space,
COUNT(*) FILTER (WHERE "CreatedTime"::text ~ '^\d{1,2}:\d{2}\s+[AP]M$') as valid_minutes_only_space,
COUNT(*) FILTER (WHERE "CreatedTime"::text ~ '^\d{1,2}:\d{2}[AP]M$') as valid_minutes_only_no_space,
COUNT(*) FILTER (WHERE "CreatedTime" IS NOT NULL AND "CreatedTime"::text !~* '(AM|PM)') as no_ampm,
COUNT(*) FILTER (WHERE "CreatedTime"::text ~ '^\d{1,2}\s*[AP]M$' AND "CreatedTime"::text !~ ':') as missing_colon,
COUNT(*) FILTER (WHERE "CreatedTime" IS NULL) as null_times
FROM "OptionsFlow_monthly";
-- Check ExpirationDate formats
SELECT
'ExpirationDate Analysis' as check_type,
COUNT(*) FILTER (WHERE "ExpirationDate"::text ~ '^\d{4}-\d{2}-\d{2}$') as valid_iso_date,
COUNT(*) FILTER (WHERE "ExpirationDate"::text ~ '^\d{1,2}/\d{1,2}/\d{2,4}$') as slash_format,
COUNT(*) FILTER (WHERE "ExpirationDate" IS NOT NULL AND "ExpirationDate"::text !~ '^\d{4}-\d{2}-\d{2}$' AND "ExpirationDate"::text !~ '^\d{1,2}/\d{1,2}/\d{2,4}$') as invalid_format,
COUNT(*) FILTER (WHERE "ExpirationDate" IS NULL) as null_dates
FROM "OptionsFlow_monthly";
-- ============================================
-- STEP 2: FIX CreatedDate - Normalize to YYYY-MM-DD
-- ============================================
UPDATE "OptionsFlow_monthly"
SET "CreatedDate" =
CASE
-- Already in YYYY-MM-DD format - keep as-is
WHEN "CreatedDate"::text ~ '^\d{4}-\d{2}-\d{2}$'
THEN "CreatedDate"::text
-- MM/DD/YYYY format - convert to YYYY-MM-DD
WHEN "CreatedDate"::text ~ '^(\d{1,2})/(\d{1,2})/(\d{2,4})$'
THEN
TO_CHAR(
to_date("CreatedDate"::text, 'MM/DD/YYYY'),
'YYYY-MM-DD'
)
-- M/D/YYYY format - convert to YYYY-MM-DD
WHEN "CreatedDate"::text ~ '^(\d{1,2})/(\d{1,2})/(\d{4})$'
THEN
TO_CHAR(
to_date("CreatedDate"::text, 'MM/DD/YYYY'),
'YYYY-MM-DD'
)
-- Try to parse as date and reformat
ELSE
CASE
WHEN to_date("CreatedDate"::text, 'YYYY-MM-DD') IS NOT NULL
THEN TO_CHAR(to_date("CreatedDate"::text, 'YYYY-MM-DD'), 'YYYY-MM-DD')
WHEN to_date("CreatedDate"::text, 'MM/DD/YYYY') IS NOT NULL
THEN TO_CHAR(to_date("CreatedDate"::text, 'MM/DD/YYYY'), 'YYYY-MM-DD')
ELSE "CreatedDate"::text -- Keep original if can't parse
END
END
WHERE "CreatedDate" IS NOT NULL
AND "CreatedDate"::text !~ '^\d{4}-\d{2}-\d{2}$'; -- Only update if not already correct
-- ============================================
-- STEP 3: FIX CreatedTime - Normalize spacing and format
-- ============================================
UPDATE "OptionsFlow_monthly"
SET "CreatedTime" =
CASE
-- Already properly formatted with space: "HH:MM:SS AM" or "H:MM:SS AM"
WHEN "CreatedTime"::text ~ '^\d{1,2}:\d{2}:\d{2}\s+[AP]M$'
THEN "CreatedTime"::text -- Keep as-is
-- Missing space before AM/PM: "HH:MM:SSAM" -> "HH:MM:SS AM"
WHEN "CreatedTime"::text ~ '^(\d{1,2}:\d{2}:\d{2})([AP]M)$'
THEN REGEXP_REPLACE("CreatedTime"::text, '^(\d{1,2}:\d{2}:\d{2})([AP]M)$', '\1 \2', 'i')
-- Minutes only with space: "HH:MM AM" -> "HH:MM:00 AM"
WHEN "CreatedTime"::text ~ '^(\d{1,2}:\d{2})\s+([AP]M)$'
THEN REGEXP_REPLACE("CreatedTime"::text, '^(\d{1,2}:\d{2})\s+([AP]M)$', '\1:00 \2', 'i')
-- Minutes only without space: "HH:MMAM" -> "HH:MM:00 AM"
WHEN "CreatedTime"::text ~ '^(\d{1,2}:\d{2})([AP]M)$'
THEN REGEXP_REPLACE("CreatedTime"::text, '^(\d{1,2}:\d{2})([AP]M)$', '\1:00 \2', 'i')
-- Just hour with space: "H AM" or "HH AM" -> "H:00:00 AM" or "HH:00:00 AM"
WHEN "CreatedTime"::text ~ '^(\d{1,2})\s+([AP]M)$'
THEN LPAD(REGEXP_REPLACE("CreatedTime"::text, '^(\d{1,2})\s+([AP]M)$', '\1', 'i'), 2, '0') || ':00:00 ' ||
UPPER(REGEXP_REPLACE("CreatedTime"::text, '^(\d{1,2})\s+([AP]M)$', '\2', 'i'))
-- Just hour without space: "HAM" or "HHAM" -> "H:00:00 AM" or "HH:00:00 AM"
WHEN "CreatedTime"::text ~ '^(\d{1,2})([AP]M)$'
THEN LPAD(REGEXP_REPLACE("CreatedTime"::text, '^(\d{1,2})([AP]M)$', '\1', 'i'), 2, '0') || ':00:00 ' ||
UPPER(REGEXP_REPLACE("CreatedTime"::text, '^(\d{1,2})([AP]M)$', '\2', 'i'))
-- Single digit hour with colon: ":MM AM" -> "H:MM:00 AM" (default to 9 AM or 1 PM)
WHEN "CreatedTime"::text ~ '^:(\d{1,2})\s+([AP]M)$'
THEN
CASE
WHEN UPPER(REGEXP_REPLACE("CreatedTime"::text, '^:(\d{1,2})\s+([AP]M)$', '\2', 'i')) = 'AM'
THEN '9:' || LPAD(REGEXP_REPLACE("CreatedTime"::text, '^:(\d{1,2})\s+([AP]M)$', '\1', 'i'), 2, '0') || ':00 AM'
ELSE '1:' || LPAD(REGEXP_REPLACE("CreatedTime"::text, '^:(\d{1,2})\s+([AP]M)$', '\1', 'i'), 2, '0') || ':00 PM'
END
-- Missing colon, just number and AM/PM (e.g., "34AM", "12PM")
-- If > 12, assume it's minutes; otherwise could be hour or minute
WHEN "CreatedTime"::text ~ '^(\d{2})([AP]M)$' AND (REGEXP_REPLACE("CreatedTime"::text, '^(\d{2})([AP]M)$', '\1', 'i'))::int > 12
THEN
CASE
WHEN UPPER(REGEXP_REPLACE("CreatedTime"::text, '^(\d{2})([AP]M)$', '\2', 'i')) = 'AM'
THEN '9:' || REGEXP_REPLACE("CreatedTime"::text, '^(\d{2})([AP]M)$', '\1', 'i') || ':00 AM'
ELSE '1:' || REGEXP_REPLACE("CreatedTime"::text, '^(\d{2})([AP]M)$', '\1', 'i') || ':00 PM'
END
-- Single or double digit <= 12 with AM/PM - ambiguous, default to hour
WHEN "CreatedTime"::text ~ '^(\d{1,2})([AP]M)$' AND (REGEXP_REPLACE("CreatedTime"::text, '^(\d{1,2})([AP]M)$', '\1', 'i'))::int <= 12
THEN LPAD(REGEXP_REPLACE("CreatedTime"::text, '^(\d{1,2})([AP]M)$', '\1', 'i'), 2, '0') || ':00:00 ' ||
UPPER(REGEXP_REPLACE("CreatedTime"::text, '^(\d{1,2})([AP]M)$', '\2', 'i'))
-- Keep as-is if doesn't match any pattern
ELSE "CreatedTime"::text
END
WHERE "CreatedTime" IS NOT NULL
AND (
-- Update if missing space before AM/PM
"CreatedTime"::text ~ '^\d{1,2}:\d{2}(:[0-9]{2})?[AP]M$'
-- Or if missing colon or seconds
OR ("CreatedTime"::text ~ '^\d{1,2}\s*[AP]M$' AND "CreatedTime"::text !~ ':')
OR "CreatedTime"::text ~ '^:\d{1,2}\s*[AP]M$'
OR ("CreatedTime"::text ~ '^\d{1,2}:\d{2}\s*[AP]M$' AND "CreatedTime"::text !~ ':\d{2}\s*[AP]M$')
);
-- ============================================
-- STEP 4: FIX ExpirationDate - Normalize to YYYY-MM-DD
-- ============================================
UPDATE "OptionsFlow_monthly"
SET "ExpirationDate" =
CASE
-- Already in YYYY-MM-DD format - keep as-is
WHEN "ExpirationDate"::text ~ '^\d{4}-\d{2}-\d{2}$'
THEN "ExpirationDate"::text
-- MM/DD/YYYY format - convert to YYYY-MM-DD
WHEN "ExpirationDate"::text ~ '^(\d{1,2})/(\d{1,2})/(\d{2,4})$'
THEN
TO_CHAR(
to_date("ExpirationDate"::text, 'MM/DD/YYYY'),
'YYYY-MM-DD'
)
-- Try to parse as date and reformat
ELSE
CASE
WHEN to_date("ExpirationDate"::text, 'YYYY-MM-DD') IS NOT NULL
THEN TO_CHAR(to_date("ExpirationDate"::text, 'YYYY-MM-DD'), 'YYYY-MM-DD')
WHEN to_date("ExpirationDate"::text, 'MM/DD/YYYY') IS NOT NULL
THEN TO_CHAR(to_date("ExpirationDate"::text, 'MM/DD/YYYY'), 'YYYY-MM-DD')
ELSE "ExpirationDate"::text -- Keep original if can't parse
END
END
WHERE "ExpirationDate" IS NOT NULL
AND "ExpirationDate"::text !~ '^\d{4}-\d{2}-\d{2}$'; -- Only update if not already correct
-- ============================================
-- STEP 5: FIX options_flow table timestamps (if they exist)
-- ============================================
-- Fix expiration dates that might be NULL or invalid
UPDATE options_flow
SET expiration = NULL
WHERE expiration IS NOT NULL
AND (expiration < '1970-01-01'::timestamp OR expiration > '2100-01-01'::timestamp);
-- Fix created_at dates that might be invalid
UPDATE options_flow
SET created_at = NOW()
WHERE created_at IS NOT NULL
AND (created_at < '1970-01-01'::timestamp OR created_at > '2100-01-01'::timestamp);
-- ============================================
-- STEP 6: VERIFICATION - Check results
-- ============================================
-- Verify CreatedDate fixes
SELECT
'CreatedDate Verification' as check_type,
COUNT(*) FILTER (WHERE "CreatedDate"::text ~ '^\d{4}-\d{2}-\d{2}$') as valid_format,
COUNT(*) FILTER (WHERE "CreatedDate" IS NOT NULL AND "CreatedDate"::text !~ '^\d{4}-\d{2}-\d{2}$') as still_invalid,
COUNT(*) FILTER (WHERE "CreatedDate" IS NULL) as null_count
FROM "OptionsFlow_monthly";
-- Verify CreatedTime fixes
SELECT
'CreatedTime Verification' as check_type,
COUNT(*) FILTER (WHERE "CreatedTime"::text ~ '^\d{1,2}:\d{2}:\d{2}\s+[AP]M$') as valid_format,
COUNT(*) FILTER (WHERE "CreatedTime"::text ~ '^\d{1,2}:\d{2}\s+[AP]M$') as minutes_only_valid,
COUNT(*) FILTER (WHERE "CreatedTime" IS NOT NULL AND "CreatedTime"::text !~ '^\d{1,2}:\d{2}(:\d{2})?\s+[AP]M$') as still_invalid,
COUNT(*) FILTER (WHERE "CreatedTime" IS NULL) as null_count
FROM "OptionsFlow_monthly";
-- Verify ExpirationDate fixes
SELECT
'ExpirationDate Verification' as check_type,
COUNT(*) FILTER (WHERE "ExpirationDate"::text ~ '^\d{4}-\d{2}-\d{2}$') as valid_format,
COUNT(*) FILTER (WHERE "ExpirationDate" IS NOT NULL AND "ExpirationDate"::text !~ '^\d{4}-\d{2}-\d{2}$') as still_invalid,
COUNT(*) FILTER (WHERE "ExpirationDate" IS NULL) as null_count
FROM "OptionsFlow_monthly";
-- Show sample of fixed entries
SELECT
"CreatedDate",
"CreatedTime",
"ExpirationDate",
"Symbol",
COUNT(*) as count
FROM "OptionsFlow_monthly"
WHERE "CreatedDate"::text ~ '^\d{4}-\d{2}-\d{2}$'
AND ("CreatedTime"::text ~ '^\d{1,2}:\d{2}(:\d{2})?\s+[AP]M$' OR "CreatedTime" IS NULL)
GROUP BY "CreatedDate", "CreatedTime", "ExpirationDate", "Symbol"
ORDER BY "CreatedDate" DESC, "CreatedTime" DESC
LIMIT 20;
-- ============================================
-- SUMMARY
-- ============================================
SELECT
'✅ Date/Time Fix Complete' as status,
'Run the verification queries above to check results' as next_step;

View File

@ -147,19 +147,13 @@ base AS (
-- AND ofm."Symbol" NOT IN ('TSLA','NVDA') -- your panel excludes these
),
/* 2) Window restriction (CST date window) and compute UTC + DTE */
/* 2) Window restriction (CST date window) and compute UTC */
flow AS (
SELECT
b.*,
(b.flow_ts_local)::date AS flow_date_cst,
/* Convert local CST clock to a real point in time (timestamptz) */
(b.flow_ts_local AT TIME ZONE 'America/Chicago') AS flow_ts_utc,
/* Days to Expiration (DTE) */
CASE
WHEN b.exp_date IS NOT NULL AND (b.flow_ts_local)::date IS NOT NULL
THEN (b.exp_date - (b.flow_ts_local)::date)
ELSE NULL
END AS dte
(b.flow_ts_local AT TIME ZONE 'America/Chicago') AS flow_ts_utc
FROM base b
WHERE (b.flow_ts_local)::date BETWEEN (SELECT start_day FROM cfg) AND (SELECT end_day FROM cfg)
),
@ -232,18 +226,6 @@ agg AS (
SUM(CASE WHEN cp_norm='PUT' AND side_norm='BUY' AND moneyness='OTM' THEN oi_num ELSE 0 END)
OVER (PARTITION BY exp_date, symbol_norm ORDER BY flow_ts_utc, rid) AS oi_pb_otm,
/* UNKNOWN SIDE tracking - premium and count for flows where direction is NULL (running sum) */
SUM(CASE WHEN direction IS NULL THEN premium_num ELSE 0 END)
OVER (PARTITION BY exp_date, symbol_norm ORDER BY flow_ts_utc, rid) AS prem_unknown_sym,
COUNT(*) FILTER (WHERE direction IS NULL)
OVER (PARTITION BY exp_date, symbol_norm ORDER BY flow_ts_utc, rid) AS cnt_unknown_sym,
/* Total premium by symbol/expiration for rule-based rockets (total sum, not running) */
SUM(premium_num) OVER (PARTITION BY exp_date, symbol_norm) AS prem_total_sym,
/* Directional premium totals by symbol/expiration (for rule-based rockets) */
SUM(CASE WHEN direction='BULL' THEN premium_num ELSE 0 END) OVER (PARTITION BY exp_date, symbol_norm) AS prem_bull_sym,
SUM(CASE WHEN direction='BEAR' THEN premium_num ELSE 0 END) OVER (PARTITION BY exp_date, symbol_norm) AS prem_bear_sym,
SUM(1) OVER (PARTITION BY exp_date, symbol_norm, direction ORDER BY flow_ts_utc, rid) AS dir_count
FROM mny m
),
@ -548,59 +530,35 @@ scored AS (
WHEN fe.cp_norm='PUT' AND fe.strike_num < fe.spot_num THEN 1
ELSE 0 END
+ 0.5 * fe.tape_alignment
AS rocket_score,
/* Rule-based rockets (SQLite style) */
CASE
WHEN fe.premium_num >= 250000
AND ABS(fe.prem_bull_sym - fe.prem_bear_sym) / NULLIF(fe.prem_total_sym, 0) >= 0.6
AND COALESCE(fe.vol_num, 0) > COALESCE(fe.oi_num, 0)
AND fe.tape_alignment = 1
THEN '🚀🚀🚀'
WHEN fe.premium_num >= 150000
AND fe.tape_alignment = 1
THEN '🚀🚀'
WHEN fe.premium_num >= 80000
AND fe.tape_alignment = 1
THEN '🚀'
ELSE NULL
END AS rocket_rule
AS rocket_score
FROM flow_enriched fe
),
rocket2 AS (
SELECT
s.*,
/* rocket label core - prefer rule-based, fallback to score-based, then original rocket */
COALESCE(
s.rocket_rule,
CASE
WHEN s.rocket_score >= 5.0 THEN '🚀🚀🚀'
WHEN s.rocket_score >= 3.5 THEN '🚀🚀'
WHEN s.rocket_score >= 2.0 THEN '🚀'
ELSE COALESCE(s.rocket, '')
END
) AS rocket_base,
/* rocket label core */
CASE
WHEN s.rocket_score >= 5.0 THEN '🚀🚀🚀'
WHEN s.rocket_score >= 3.5 THEN '🚀🚀'
WHEN s.rocket_score >= 2.0 THEN '🚀'
ELSE COALESCE(s.rocket, '')
END AS rocket_base,
/* append [ITM/OTM %] */
CASE
WHEN s.mny_pct IS NULL THEN
COALESCE(
s.rocket_rule,
CASE
WHEN s.rocket_score >= 5.0 THEN '🚀🚀🚀'
WHEN s.rocket_score >= 3.5 THEN '🚀🚀'
WHEN s.rocket_score >= 2.0 THEN '🚀'
ELSE COALESCE(s.rocket, '')
END
)
(CASE
WHEN s.rocket_score >= 5.0 THEN '🚀🚀🚀'
WHEN s.rocket_score >= 3.5 THEN '🚀🚀'
WHEN s.rocket_score >= 2.0 THEN '🚀'
ELSE COALESCE(s.rocket, '')
END)
ELSE
COALESCE(
s.rocket_rule,
CASE
WHEN s.rocket_score >= 5.0 THEN '🚀🚀🚀'
WHEN s.rocket_score >= 3.5 THEN '🚀🚀'
WHEN s.rocket_score >= 2.0 THEN '🚀'
ELSE COALESCE(s.rocket, '')
END
)
(CASE
WHEN s.rocket_score >= 5.0 THEN '🚀🚀🚀'
WHEN s.rocket_score >= 3.5 THEN '🚀🚀'
WHEN s.rocket_score >= 2.0 THEN '🚀'
ELSE COALESCE(s.rocket, '')
END)
|| ' [' || CASE WHEN s.mny_pct >= 0 THEN 'ITM ' ELSE 'OTM ' END
|| TRIM(TO_CHAR(ABS(s.mny_pct)::numeric, 'FM999990.0')) || '%]'
END AS Rocket_with_mny
@ -658,11 +616,6 @@ SELECT
fe."ExpirationDate", fe."Price", fe."CallPut", fe."Side", fe."Strike", fe."Spot", fe."Volume", fe."OI",
/* DTE and unknown-side tracking */
fe.dte AS "DTE",
fe.prem_unknown_sym AS "PremUnknownSym",
fe.cnt_unknown_sym AS "CntUnknownSym",
/* trader-facing context — now meaningful */
fe.pct_vs_prior_close AS "PctVsPriorClose",
fe.pct_vs_rth_open AS "PctVsRthOpen",

View File

@ -3,6 +3,7 @@
--
-- Usage:
-- For Supabase: Run in Supabase SQL Editor
-- For Remote PostgreSQL: psql -h 100.121.163.23 -p 5432 -U postgres -d institutional_trader < setup_friend_access.sql
-- For Local PostgreSQL: sudo -u postgres psql institutional_trader < setup_friend_access.sql
--
-- IMPORTANT: Replace 'friend_user' and 'secure_password_here' with actual values!
@ -126,13 +127,13 @@ ORDER BY grantee, table_name;
-- Connection Strings for Friend
-- ============================================
-- For Remote PostgreSQL (100.121.163.23):
-- postgresql://friend_user:secure_password_here@100.121.163.23:5432/institutional_trader
--
-- For Supabase:
-- postgresql://friend_user:secure_password_here@db.[project-ref].supabase.co:5432/postgres
--
-- For Local PostgreSQL:
-- postgresql://friend_user:secure_password_here@your-server-ip:5432/institutional_trader
--
-- For SSH Tunnel (most secure):
-- 1. Friend runs: ssh -L 5432:localhost:5432 user@your-server-ip
-- 1. Friend runs: ssh -L 5432:100.121.163.23:5432 user@your-proxmox-server-ip
-- 2. Friend connects to: postgresql://friend_user:password@localhost:5432/institutional_trader

View File

@ -10,8 +10,20 @@ SUPABASE_ANON_KEY=your_production_anon_key
SUPABASE_SERVICE_KEY=your_service_role_key
# Direct PostgreSQL Connection (optional, for heavy queries)
# Option 1: Remote PostgreSQL Database
DATABASE_URL=postgresql://postgres:[password]@100.121.163.23:5432/institutional_trader
# Option 2: Supabase Database
# Get this from: Supabase Dashboard > Settings > Database > Connection String (URI)
DATABASE_URL=postgresql://postgres:[password]@db.[project-ref].supabase.co:5432/postgres
# DATABASE_URL=postgresql://postgres:[password]@db.[project-ref].supabase.co:5432/postgres
# Option 3: Use LOCAL_DB variables (for remote database)
USE_LOCAL_DB=true
LOCAL_DB_HOST=100.121.163.23
LOCAL_DB_PORT=5432
LOCAL_DB_USER=postgres
LOCAL_DB_PASSWORD=your_postgres_password
LOCAL_DB_NAME=institutional_trader
# CORS Configuration
CORS_ORIGIN=https://your-frontend-domain.com

View File

@ -19,13 +19,10 @@
"express-rate-limit": "^7.1.5",
"express-ws": "^5.0.2",
"helmet": "^7.1.0",
"ml-logistic-regression": "^2.0.0",
"ml-matrix": "^6.13.0",
"node-cache": "^5.1.2",
"node-fetch": "^3.3.2",
"pg": "^8.11.3",
"ws": "^8.14.2",
"xml2js": "^0.6.2"
"ws": "^8.14.2"
},
"devDependencies": {
"nodemon": "^3.0.1"
@ -566,7 +563,6 @@
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@ -935,12 +931,6 @@
"node": ">= 0.10"
}
},
"node_modules/is-any-array": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-any-array/-/is-any-array-3.0.0.tgz",
"integrity": "sha512-o4h+tylWykC4BD1vaejp6gDxoM13bwW8FGuNs4yIKpj8xbBJcRxJx8vZpq0dCr7ZDEfeKjmsi/euolKhX6f/ww==",
"license": "MIT"
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@ -1082,54 +1072,6 @@
"node": "*"
}
},
"node_modules/ml-array-max": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ml-array-max/-/ml-array-max-2.0.0.tgz",
"integrity": "sha512-QQZ4kENwpWmyNb98UXRDFXrmtIXuXtt1+bSbda/2KA85+F+rrJP8hZk6QOkCQXM2Th9mUDYdq/PNByPdT9ID4A==",
"license": "MIT",
"dependencies": {
"is-any-array": "^3.0.0"
}
},
"node_modules/ml-array-min": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ml-array-min/-/ml-array-min-2.0.0.tgz",
"integrity": "sha512-GRj6Ky6sW9vGL6yIjgsHmXZ9YgrdmcQ8nCxPqEGeKc6dkfYg1XDYxGFxADUjNuZyoCd5PUscWAS4N+cFaX6hFg==",
"license": "MIT",
"dependencies": {
"is-any-array": "^3.0.0"
}
},
"node_modules/ml-array-rescale": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ml-array-rescale/-/ml-array-rescale-2.0.0.tgz",
"integrity": "sha512-2GGtKfSno94/kIloWGvpp/U5Q5vLvLrza+SAaGsLeo6Xj4mEbA6Gqx+oTfZFkxnd1grT2X007HfJNs3T5BsiVg==",
"license": "MIT",
"dependencies": {
"is-any-array": "^3.0.0",
"ml-array-max": "^2.0.0",
"ml-array-min": "^2.0.0"
}
},
"node_modules/ml-logistic-regression": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ml-logistic-regression/-/ml-logistic-regression-2.0.0.tgz",
"integrity": "sha512-xHhB91ut8GRRbJyB1ZQfKsl1MHmE1PqMeRjxhks96M5BGvCbC9eEojf4KgRMKM2LxFblhVUcVzweAoPB48Nt0A==",
"license": "MIT",
"dependencies": {
"ml-matrix": "^6.5.0"
}
},
"node_modules/ml-matrix": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/ml-matrix/-/ml-matrix-6.13.0.tgz",
"integrity": "sha512-QpV0UTUkglg6vPUgThKGBEtit2ac6habSoZ33bwI9rU0UHZLqw6G3ukTIE8zWiUF3sjK8YAlhx/o/b9layzH8A==",
"license": "MIT",
"dependencies": {
"is-any-array": "^3.0.0",
"ml-array-rescale": "^2.0.0"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -1321,7 +1263,6 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT",
"peer": true,
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",
@ -1556,15 +1497,6 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/sax": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
"integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=11.0.0"
}
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
@ -1961,28 +1893,6 @@
}
}
},
"node_modules/xml2js": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
"integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
"license": "MIT",
"dependencies": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/xmlbuilder": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@ -10,9 +10,7 @@
"build": "echo 'No build step required for Node.js'",
"setup-local-db": "node scripts/testAndSetup.js",
"import-csv": "node scripts/importCSV.js",
"verify-data": "node scripts/verifyData.js",
"validate-schema": "node scripts/validateSchema.js",
"test-backtester": "node scripts/testBacktester.js"
"verify-data": "node scripts/verifyData.js"
},
"keywords": [
"trading",
@ -33,13 +31,10 @@
"express-rate-limit": "^7.1.5",
"express-ws": "^5.0.2",
"helmet": "^7.1.0",
"ml-logistic-regression": "^2.0.0",
"ml-matrix": "^6.13.0",
"node-cache": "^5.1.2",
"node-fetch": "^3.3.2",
"pg": "^8.11.3",
"ws": "^8.14.2",
"xml2js": "^0.6.2"
"ws": "^8.14.2"
},
"devDependencies": {
"nodemon": "^3.0.1"

View File

@ -1,86 +0,0 @@
{
"success": true,
"data": [
{
"CreatedDate": "2025-01-15",
"CreatedTime": "10:30:45 AM",
"Symbol": "(5🟩) AAPL · RTH ⚡ 🟢💎⭐💰 ⚡ 🔥",
"Rocket": "🚀🚀🚀 [ITM 2.5%]",
"NetPremium": "1.50 M",
"Premium": "850.00 K",
"premium_num": 850000,
"bull_total": 1500000,
"bear_total": 50000,
"PctVsPriorClose": 1.2,
"PctVsRthOpen": 0.8,
"Pct5m": 0.3,
"Pct15m": 0.5,
"TapeAlign": "↗︎",
"NearAlert": "EARNINGS",
"ExpirationDate": "2025-01-17",
"Price": "185.50",
"CallPut": "CALL",
"Side": "AA",
"Strike": "185",
"Spot": "185.50",
"Volume": "5000",
"OI": "3000",
"direction": "BULL",
"badge_round": "🟢",
"badge_more": "💎⭐💰✔",
"flash": "⚡",
"rocket_score": 5.2,
"session_bucket": "RTH",
"flow_ts_utc": "2025-01-15T16:30:45+00:00",
"mny_pct": 2.5,
"vwap_at_signal": 185.20,
"price_vs_vwap_pct": 0.16,
"signal_tier": "TIER_1",
"checklist_score": 8.5,
"checklist_passed": true,
"premium_zscore": 2.3,
"premium_percentile_intraday": 85.5,
"relative_premium_score": 72.5,
"aggression_score": 68.0,
"size_concentration_score": 75.0,
"repeat_trade_velocity": 55.0,
"strike_clustering_score": 60.0,
"signal_strength": 64.7,
"early_noise_reject": false,
"flow_acceleration": 12500.5,
"time_between_hits": 3.2,
"follow_on_ratio": 0.75,
"strike_laddering_detected": true,
"delta_exposure": 92500000,
"gamma_exposure": 1250000000,
"volatility_intent": "LONG_VOL",
"net_gamma_exposure_per_symbol": 8500000000,
"gamma_flip_proximity": 0.15,
"dealer_hedge_pressure_score": 72.5,
"market_regime": "TREND",
"flow_state": "ACTIONABLE",
"confidence_score": 78.5,
"institutional_likelihood": 0.85,
"dealer_pain_level": 65.0,
"expected_move_vs_implied": 1.35
}
],
"count": 1,
"timestamp": "2025-01-15T16:35:00.123456"
}

View File

@ -1,168 +0,0 @@
# Institutional-Grade Options Flow Analytics
This document describes the institutional-grade enhancements to the options flow pipeline.
## Overview
The pipeline has been refactored to convert static retail-style flow detection into dynamic, dealer-aware, time-sequenced signals suitable for intraday momentum and 1-5 day swing trades.
## New Analytics Modules
### 1. Relative Premium Scoring (`relative_premium_scorer.py`)
**Purpose**: Replace static premium filter (minPremium = $80K) with context-aware relative scoring.
**New Fields**:
- `premium_zscore`: Z-score of premium relative to 20-day rolling window per ticker
- `premium_percentile_intraday`: Percentile rank within same-day flow
- `relative_premium_score`: Composite score (0-100) combining z-score, intraday percentile, and median normalization
**Usage**: Premium of $80K might be significant for AAPL but noise for TSLA. This module computes relative significance.
### 2. Signal Component Scoring (`signal_component_scorer.py`)
**Purpose**: Convert binary badge logic (💎 ⭐ 🟢 🔴) into continuous numeric signal components.
**New Fields**:
- `aggression_score`: Measures trade aggression (ITM premiums, ask-side trades)
- `size_concentration_score`: Measures size concentration (single large trade vs many small ones)
- `repeat_trade_velocity`: Measures repeat trade frequency (urgency building)
- `strike_clustering_score`: Measures strike clustering (laddering patterns)
- `signal_strength`: Composite score = 0.30 * aggression + 0.30 * size_concentration + 0.20 * repeat_velocity + 0.20 * strike_clustering
**Note**: Badges remain display-only. Signal strength is computed from components.
### 3. Tier-0 Noise Rejection (`noise_rejector.py`)
**Purpose**: Reject low-quality signals before enrichment to reduce processing overhead.
**New Fields**:
- `early_noise_reject`: Boolean flag indicating if signal should be rejected as noise
**Rejection Criteria**:
- Single isolated trade (no repeat activity within 30 minutes)
- Far OTM weekly lottos (>15% OTM with <7 days to expiry)
- Delta-adjusted premium below threshold (<$50K)
### 4. Time-Sequenced Flow Analysis (`time_sequenced_analyzer.py`)
**Purpose**: Analyze flow patterns over time to detect urgency, distribution, and continuation.
**New Fields**:
- `flow_acceleration`: Change in premium per minute (Δ premium / minute)
- `time_between_hits`: Average time between consecutive trades (minutes)
- `follow_on_ratio`: Fraction of trades in same direction after initial trade (0-1)
- `strike_laddering_detected`: Boolean indicating sequential strike accumulation
**Interpretation**:
- Escalating premium + decreasing time gaps = urgency
- Flat premium + widening gaps = distribution
### 5. Intent Classification (`intent_classifier.py`)
**Purpose**: Replace naive direction (BULL/BEAR) with nuanced volatility and hedging intent.
**New Fields**:
- `delta_exposure`: Delta exposure (contracts * delta * 100 * spot_price)
- `gamma_exposure`: Gamma exposure (contracts * gamma * 100 * spot_price^2)
- `volatility_intent`: Enum (LONG_VOL, SHORT_VOL, DIRECTIONAL, HEDGE_UNWIND)
**Note**: Direction (BULL/BEAR) becomes secondary metadata.
### 6. Dealer-Aware Flow Context (`dealer_flow_context.py`)
**Purpose**: Track dealer hedging pressure and gamma exposure.
**New Fields**:
- `net_gamma_exposure_per_symbol`: Sum of gamma exposures for symbol (positive = long gamma, negative = short gamma)
- `gamma_flip_proximity`: Proximity to gamma flip point (-1 to 1)
- `dealer_hedge_pressure_score`: Dealer hedge pressure score (0-100)
**Usage**: Validates flow continuation, flow reversals, and gamma squeeze setups.
### 7. Market Regime Detection (`market_regime_detector.py`)
**Purpose**: Identify market regime to gate trade signal generation.
**New Fields**:
- `market_regime`: Enum (TREND, RANGE, HIGH_VOL_EVENT)
**Trade Signal Gating**:
- Trend → continuation bias
- Range → fade or vol-sell bias
- Event → volatility expansion bias
### 8. Flow Decay & Reversal Validation (`flow_decay_validator.py`)
**Purpose**: Validate flow decay/reversal signals with anchors.
**New Fields**:
- `flow_state`: Enum (ACTIONABLE, INFORMATIONAL)
**Validation Criteria**: Flow decay/reversal is actionable ONLY IF:
- Premium contracts (relative_premium_score >= 60)
- Dealer hedge pressure decreases
- Price fails near VWAP / opening range / key level
- Otherwise marked as INFORMATIONAL
### 9. Institutional Confidence Metrics (`institutional_confidence.py`)
**Purpose**: Calculate confidence scores for institutional flow signals.
**New Fields**:
- `confidence_score`: Overall confidence score (0-100)
- `institutional_likelihood`: Likelihood flow is institutional (0-1)
- `dealer_pain_level`: Dealer pain level (0-100)
- `expected_move_vs_implied`: Expected move vs implied move ratio
## Integration
All modules are integrated into the main processing pipeline in `main.py`:
1. Basic flow processing (normalization, badges, rocket score)
2. Price context enrichment
3. Alert matching
4. **Institutional analytics pipeline** (NEW):
- Tier-0 noise rejection
- Relative premium scoring
- Signal component scoring
- Time-sequenced analysis
- Intent classification
- Dealer flow context
- Market regime detection
- Flow decay validation
- Confidence metrics
5. Filtering (premium, relative premium, badges, direction)
6. Output formatting
## Filtering Changes
**Before**:
- Static premium filter: `premium_num > 80000`
- Badge requirements: 🟢/🔴 + 💎 + ⭐
**After**:
- Static premium filter: `premium_num > min_premium` (still applied)
- **Relative premium filter**: `relative_premium_score >= 60.0` (NEW)
- **Noise rejection filter**: `early_noise_reject == False` (NEW)
- Badge requirements: 🟢/🔴 + 💎 + ⭐ (still applied, but badges are now display-only)
## API Response
All new fields are included in the API response. The response maintains backward compatibility - existing fields remain unchanged, new fields are additive.
## Design Philosophy
1. **Flow represents pressure, not prediction**: Signals indicate who is forced to act next (dealers hedging)
2. **Institutions trade urgency and forced hedging**: Focus on dealer pain and gamma exposure
3. **Fewer, higher-quality signals > more alerts**: Noise rejection and relative premium filtering reduce false positives
4. **Every signal must answer**: "Who is forced to act next?"
## Success Criteria
If implemented correctly:
- Signal count decreases (noise filtered out)
- Average signal quality increases (relative premium, signal strength)
- False positives reduce (noise rejection, dealer context validation)
- Trades align with intraday momentum and short-term swing horizons (time-sequenced analysis)

View File

@ -27,7 +27,7 @@ cp .env.example .env
```bash
python main.py
# Or with uvicorn:
uvicorn main:app --reload --port 8010
uvicorn main:app --reload --port 8000
```
## API Endpoints
@ -46,7 +46,7 @@ Get processed options flow data
**Example:**
```bash
curl http://localhost:8010/api/options-flow?start_date=2024-01-01&end_date=2024-01-02
curl http://localhost:8000/api/options-flow?start_date=2024-01-01&end_date=2024-01-02
```
### GET /api/options-flow/stats
@ -60,7 +60,7 @@ Get flow statistics
The Node.js API layer can call this service via HTTP:
```javascript
const response = await fetch('http://localhost:8010/api/options-flow?start_date=2024-01-01');
const response = await fetch('http://localhost:8000/api/options-flow?start_date=2024-01-01');
const data = await response.json();
```

View File

@ -4,7 +4,7 @@
```bash
cd backend/python_service
uvicorn main:app --reload --port 8010
uvicorn main:app --reload --port 8000
```
## Using Virtual Environment (Recommended)
@ -26,20 +26,20 @@ source venv/bin/activate
pip install -r requirements.txt
# Start service
uvicorn main:app --reload --port 8010
uvicorn main:app --reload --port 8000
```
## Verify Service is Running
Once started, you should see:
```
INFO: Uvicorn running on http://127.0.0.1:8010
INFO: Uvicorn running on http://127.0.0.1:8000
INFO: Application startup complete.
```
Test the health endpoint:
```bash
curl http://localhost:8010/health
curl http://localhost:8000/health
```
Expected response:

View File

@ -28,10 +28,10 @@ Start the service and test the endpoint:
```bash
# Terminal 1: Start Python service
cd backend/python_service
uvicorn main:app --reload --port 8010
uvicorn main:app --reload --port 8000
# Terminal 2: Test endpoint
curl "http://localhost:8010/api/options-flow?start_date=2024-01-01&end_date=2024-01-02"
curl "http://localhost:8000/api/options-flow?start_date=2024-01-01&end_date=2024-01-02"
```
### 3. Integration Test with Node.js
@ -41,7 +41,7 @@ Test the full stack:
```bash
# Terminal 1: Start Python service
cd backend/python_service
uvicorn main:app --reload --port 8010
uvicorn main:app --reload --port 8000
# Terminal 2: Start Node.js backend
cd backend

View File

@ -3,7 +3,6 @@ Database connection module for Python service
Uses asyncpg for async PostgreSQL connections
"""
import os
import asyncio
import asyncpg
from typing import Optional
from dotenv import load_dotenv
@ -13,12 +12,9 @@ load_dotenv()
# Database connection pool
_pool: Optional[asyncpg.Pool] = None
# Connection timeout in seconds (reduced from default 60s to fail faster)
CONNECTION_TIMEOUT = 10
async def get_pool() -> asyncpg.Pool:
"""Get or create database connection pool with timeout"""
"""Get or create database connection pool"""
global _pool
if _pool is None:
@ -27,7 +23,7 @@ async def get_pool() -> asyncpg.Pool:
if use_local_db:
config = {
'host': os.getenv('LOCAL_DB_HOST', '192.168.8.151'),
'host': os.getenv('LOCAL_DB_HOST', '100.121.163.23'),
'port': int(os.getenv('LOCAL_DB_PORT', '5432')),
'user': os.getenv('LOCAL_DB_USER', 'postgres'),
'password': os.getenv('LOCAL_DB_PASSWORD', 'postgres'),
@ -51,29 +47,12 @@ async def get_pool() -> asyncpg.Pool:
' - OR DATABASE_URL'
)
# Create pool with timeout to prevent hanging
# Use min_size=1 to speed up initial connection (pool will grow as needed)
try:
_pool = await asyncio.wait_for(
asyncpg.create_pool(
**config,
min_size=1, # Start with 1 connection, pool grows as needed
max_size=20,
command_timeout=60,
timeout=CONNECTION_TIMEOUT # Connection timeout per connection
),
timeout=CONNECTION_TIMEOUT # Overall timeout for pool creation
)
except asyncio.TimeoutError:
raise ConnectionError(
f'Database connection timeout after {CONNECTION_TIMEOUT}s. '
f'Check if database is reachable at {config.get("host", "unknown")}:{config.get("port", "unknown")}'
)
except Exception as e:
raise ConnectionError(
f'Failed to create database connection pool: {str(e)}. '
f'Check database configuration and network connectivity.'
)
_pool = await asyncpg.create_pool(
**config,
min_size=5,
max_size=20,
command_timeout=60
)
return _pool

View File

@ -2,23 +2,19 @@
FastAPI service for options flow processing
Replaces complex SQL with Python/pandas logic
"""
from fastapi import FastAPI, HTTPException, Query, Body
from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import Optional, List, Dict, Any
from typing import Optional, List
from datetime import datetime, timedelta
import pandas as pd
import asyncpg
import pytz
from db import get_pool, close_pool
from services.options_flow_processor import OptionsFlowProcessor
from services.price_context import PriceContextService
from services.alert_service import AlertService
from services.output_formatter import OutputFormatter
from services.price_reaction_tracker import PriceReactionTracker
from services.signal_tier_classifier import SignalTierClassifier
from services.trade_checklist import TradeChecklist
from utils.logger import logger
from utils.error_handler import handle_processing_error
@ -51,11 +47,7 @@ class OptionsFlowResponse(BaseModel):
@app.on_event("startup")
async def startup():
"""Initialize database pool on startup"""
try:
pool = await get_pool()
logger.info("✅ Database pool initialized")
except Exception as e:
logger.error(f"❌ Failed to initialize database pool: {e}")
await get_pool()
@app.on_event("shutdown")
@ -71,7 +63,7 @@ async def health():
pool = await get_pool()
async with pool.acquire() as conn:
await conn.fetchval("SELECT 1")
return {"status": "healthy"}
return {"status": "healthy", "service": "options-flow-processor"}
except Exception as e:
return {"status": "unhealthy", "error": str(e)}
@ -106,8 +98,6 @@ async def get_options_flow(
# Load raw options flow data (with timeout handling)
try:
async with pool.acquire() as conn:
# Build query with date filtering
# Note: CreatedDate is TEXT, so we need to handle date comparisons carefully
query = """
SELECT *
FROM "OptionsFlow_monthly"
@ -116,55 +106,13 @@ async def get_options_flow(
AND "StockEtf" = 'STOCK'
AND "Symbol" NOT IN ('TSLA', 'NVDA')
"""
# Add date filtering using the parsed dates
params = []
if start_date:
# CreatedDate is TEXT, so compare as strings (assuming YYYY-MM-DD format)
query += ' AND "CreatedDate" >= $1'
params.append(start_date)
if end_date:
param_idx = len(params) + 1
query += ' AND "CreatedDate" <= $' + str(param_idx)
params.append(end_date)
# Add LIMIT for safety (prevent loading millions of rows)
# Limit to 500k rows max
query += ' LIMIT 500000'
logger.info(f"Executing query with date range: {start_date} to {end_date}")
# Execute with timeout
try:
# Set statement timeout for this query (60 seconds)
await conn.execute('SET statement_timeout = 60000')
rows = await conn.fetch(query, *params)
await conn.execute('RESET statement_timeout')
logger.info(f"✅ Fetched {len(rows)} rows from database")
except Exception as query_error:
await conn.execute('RESET statement_timeout')
raise query_error
rows = await conn.fetch(query)
except Exception as e:
error_type = type(e).__name__
error_msg = str(e)
logger.error(f"Database query error: {error_type} - {error_msg}")
# Provide more helpful error messages
if 'TimeoutError' in error_type or 'timeout' in error_msg.lower():
raise HTTPException(
status_code=504,
detail=f"Database query timed out. The query may be too large. Try narrowing the date range or ensure database indexes are optimized."
)
elif 'Connection' in error_type or 'connection' in error_msg.lower():
raise HTTPException(
status_code=503,
detail=f"Database connection error: {error_msg}. Check database connectivity and configuration."
)
else:
raise HTTPException(
status_code=500,
detail=f"Database query failed: {error_msg}"
)
logger.error(f"Database query error: {type(e).__name__} - {str(e)}")
raise HTTPException(
status_code=500,
detail=f"Database query failed: {str(e)}"
)
if not rows:
return OptionsFlowResponse(
@ -181,7 +129,7 @@ async def get_options_flow(
processor = OptionsFlowProcessor(tol_pct=tol_pct)
df_processed = processor.process(df, start_dt, end_dt)
# Enrich with price context (optimized batch queries) - includes VWAP
# Enrich with price context (optimized batch queries)
price_service = PriceContextService(pool)
df_with_prices = await price_service.enrich_flow_with_prices(df_processed, pool)
@ -192,120 +140,9 @@ async def get_options_flow(
# Recalculate rocket score with price context and alerts
df_final = processor.process_rocket_score(df_final)
# Apply institutional-grade analytics pipeline
logger.info("🔹 Applying institutional-grade analytics...")
from services.relative_premium_scorer import RelativePremiumScorer
from services.noise_rejector import NoiseRejector
from services.signal_component_scorer import SignalComponentScorer
from services.time_sequenced_analyzer import TimeSequencedAnalyzer
from services.intent_classifier import IntentClassifier
from services.dealer_flow_context import DealerFlowContext
from services.market_regime_detector import MarketRegimeDetector
from services.flow_decay_validator import FlowDecayValidator
from services.institutional_confidence import InstitutionalConfidence
# 1. Tier-0 Noise Rejection (mark but don't filter yet - filtering happens later)
logger.info("1⃣ Applying tier-0 noise rejection...")
noise_rejector = NoiseRejector()
df_final = noise_rejector.mark_noise_rejections(df_final)
# 2. Relative Premium Scoring
logger.info("2⃣ Calculating relative premium scores...")
premium_scorer = RelativePremiumScorer(pool)
df_final = await premium_scorer.enrich_with_relative_premium(df_final)
# 3. Signal Component Scoring (convert badges to continuous scores)
logger.info("3⃣ Converting badges to continuous signal components...")
signal_scorer = SignalComponentScorer()
df_final = signal_scorer.enrich_with_signal_components(df_final)
# 4. Time-Sequenced Flow Analysis
logger.info("4⃣ Analyzing time-sequenced flow patterns...")
time_analyzer = TimeSequencedAnalyzer()
df_final = time_analyzer.enrich_with_time_sequenced_metrics(df_final)
# 5. Intent Classification
logger.info("5⃣ Classifying volatility and hedging intent...")
intent_classifier = IntentClassifier()
df_final = intent_classifier.enrich_with_intent_classification(df_final)
# 6. Dealer-Aware Flow Context
logger.info("6⃣ Analyzing dealer hedging pressure...")
dealer_context = DealerFlowContext()
df_final = dealer_context.enrich_with_dealer_context(df_final)
# 7. Market Regime Detection
logger.info("7⃣ Detecting market regime...")
regime_detector = MarketRegimeDetector()
df_final = regime_detector.enrich_with_market_regime(df_final)
# 8. Flow Decay & Reversal Validation
logger.info("8⃣ Validating flow decay and reversal signals...")
flow_validator = FlowDecayValidator()
df_final = flow_validator.enrich_with_flow_state(df_final)
# 9. Institutional Confidence Metrics
logger.info("9⃣ Calculating institutional confidence metrics...")
confidence_calc = InstitutionalConfidence()
df_final = confidence_calc.enrich_with_confidence_metrics(df_final)
# Phase 1 Enhancements (BEFORE filtering so all signals get Phase 1 data):
# Initialize Phase 1 columns with None/empty values first
if not df_final.empty:
# Initialize all Phase 1 columns to ensure they exist
df_final['signal_tier'] = None
df_final['is_tradeable'] = False
df_final['checklist_score'] = None
df_final['checklist_passed'] = False
df_final['checklist_details'] = None
df_final['price_reaction_5m_pct'] = None
df_final['price_reaction_15m_pct'] = None
df_final['price_reaction_30m_pct'] = None
df_final['high_break_5m'] = None
df_final['low_break_5m'] = None
df_final['flow_led_to_move'] = None
# VWAP fields should already be set by PriceContextService, but ensure they exist
if 'vwap_at_signal' not in df_final.columns:
df_final['vwap_at_signal'] = None
if 'price_vs_vwap_pct' not in df_final.columns:
df_final['price_vs_vwap_pct'] = None
# 1. Signal Tier Classification
logger.info("🔍 Classifying signal tiers...")
if not df_final.empty:
try:
tier_classifier = SignalTierClassifier()
df_final = tier_classifier.classify_tiers(df_final)
# Debug: Check if tiers were calculated
if 'signal_tier' in df_final.columns:
tier_counts = df_final['signal_tier'].value_counts()
logger.info(f"✅ Signal tiers calculated: {tier_counts.to_dict()}")
else:
logger.warning("⚠️ signal_tier column not found after classification")
except Exception as e:
logger.error(f"❌ Error in signal tier classification: {str(e)}", exc_info=True)
# 2. Price Reaction Tracking (disabled - will be calculated on-demand)
# Price reaction requires Yahoo Finance and is calculated when modal opens
# 3. Trade Checklist Evaluation
logger.info("✅ Evaluating trade checklist...")
if not df_final.empty:
try:
checklist = TradeChecklist()
df_final = checklist.evaluate_all(df_final)
# Debug: Check if checklist was calculated
if 'checklist_score' in df_final.columns:
checklist_count = df_final['checklist_score'].notna().sum()
logger.info(f"✅ Checklist scores calculated: {checklist_count}/{len(df_final)} signals")
else:
logger.warning("⚠️ checklist_score column not found after evaluation")
except Exception as e:
logger.error(f"❌ Error in trade checklist evaluation: {str(e)}", exc_info=True)
# Check if DataFrame is empty before filtering
if df_final.empty:
logger.warning("⚠️ No data after processing, returning empty result")
logger.warning("No data after processing, returning empty result")
return OptionsFlowResponse(
success=True,
data=[],
@ -316,13 +153,6 @@ async def get_options_flow(
# Filter by minimum premium and badge requirements (matching SQL WHERE clause)
logger.info(f"📊 Before filtering: {len(df_final)} rows")
# Apply noise rejection filter first (exclude early_noise_reject = True)
if 'early_noise_reject' in df_final.columns:
before_noise = len(df_final)
df_final = df_final[~df_final['early_noise_reject']].copy()
after_noise = len(df_final)
logger.info(f"📊 After noise rejection filter: {after_noise} rows (removed {before_noise - after_noise})")
# Only filter if columns exist
if 'premium_num' in df_final.columns:
before_premium = len(df_final)
@ -332,14 +162,6 @@ async def get_options_flow(
else:
logger.warning("⚠️ premium_num column not found, skipping premium filter")
# Apply relative premium filter if available
if 'relative_premium_score' in df_final.columns:
before_relative = len(df_final)
min_relative_threshold = 60.0 # Configurable threshold
df_final = df_final[df_final['relative_premium_score'] >= min_relative_threshold].copy()
after_relative = len(df_final)
logger.info(f"📊 After relative premium filter (>={min_relative_threshold}): {after_relative} rows (removed {before_relative - after_relative})")
if df_final.empty:
logger.warning("⚠️ No data after premium filter")
return OptionsFlowResponse(
@ -393,19 +215,6 @@ async def get_options_flow(
# Format output to match SQL format
df_final = OutputFormatter.format_final_output(df_final)
# Debug: Log Phase 1 columns before converting to dict
phase1_columns = ['signal_tier', 'checklist_score', 'checklist_passed', 'price_reaction_5m_pct', 'flow_led_to_move', 'vwap_at_signal', 'price_vs_vwap_pct']
existing_phase1_cols = [col for col in phase1_columns if col in df_final.columns]
if existing_phase1_cols:
logger.info(f"✅ Phase 1 columns in final output: {existing_phase1_cols}")
# Sample a row to check values
if len(df_final) > 0:
sample_row = df_final.iloc[0]
sample_phase1 = {col: sample_row.get(col) for col in existing_phase1_cols}
logger.info(f"📊 Sample Phase 1 data: {sample_phase1}")
else:
logger.warning("⚠️ No Phase 1 columns found in final output!")
# Convert DataFrame to list of dicts
result_data = df_final.to_dict('records')
@ -423,12 +232,6 @@ async def get_options_flow(
record[key] = None
else:
record[key] = value.isoformat()
elif isinstance(value, dict):
# Keep dicts as-is (e.g., checklist_details)
pass
elif isinstance(value, (list, tuple)):
# Keep lists as-is
pass
return OptionsFlowResponse(
success=True,
@ -438,10 +241,18 @@ async def get_options_flow(
)
except Exception as e:
logger.error(f"Error processing options flow: {e}", exc_info=True)
error_info = handle_processing_error(
e,
context={
'start_date': start_date,
'end_date': end_date,
'min_premium': min_premium
},
raise_error=False
)
raise HTTPException(
status_code=500,
detail=f"Processing failed: {str(e)}"
detail=error_info.get('error_message', str(e)) if isinstance(error_info, dict) else str(e)
)
@ -482,349 +293,7 @@ async def get_flow_stats(
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/phase1/calculate")
async def calculate_phase1_for_signal(request: Dict[str, Any] = Body(...)):
"""
Calculate Phase 1 metrics for a specific signal on-demand
This is called when the Phase 1 modal opens - fetches Yahoo Finance data and calculates Phase 1
"""
try:
from services.yahoo_finance_service import YahooFinanceService
symbol = request.get('symbol')
flow_ts_utc = request.get('flow_ts_utc')
flow_date_cst = request.get('flow_date_cst')
row_data = request.get('row_data', {})
if not symbol or not flow_ts_utc:
raise HTTPException(
status_code=400,
detail="symbol and flow_ts_utc are required"
)
# Log the symbol we received
logger.info(f"📥 Phase 1 calculation request for symbol: '{symbol}' (type: {type(symbol).__name__}, length: {len(symbol) if symbol else 0})")
pool = await get_pool()
# Parse timestamps - handle various formats
logger.info(f"📥 Received flow_ts_utc: {flow_ts_utc} (type: {type(flow_ts_utc).__name__})")
if isinstance(flow_ts_utc, str):
try:
original_ts = flow_ts_utc
# Remove any trailing Z and handle ISO format
ts_str = flow_ts_utc.strip()
if ts_str.endswith('Z'):
# Replace Z with +00:00 for fromisoformat
ts_str = ts_str[:-1] + '+00:00'
logger.info(f"📅 Parsing UTC timestamp (Z suffix): {original_ts} -> {ts_str}")
elif 'T' in ts_str and '+' not in ts_str and '-' not in ts_str[-6:]:
# ISO format without timezone - check if it might be local time
# For now, assume UTC as the field name suggests, but log it
ts_str = ts_str + '+00:00'
logger.info(f"📅 Parsing timestamp without timezone, assuming UTC: {original_ts} -> {ts_str}")
# Try parsing with fromisoformat
try:
flow_ts_utc = datetime.fromisoformat(ts_str)
logger.info(f"✅ Parsed flow_ts_utc: {flow_ts_utc} (timezone: {flow_ts_utc.tzinfo})")
except ValueError:
# Fallback: try parsing just the date part
date_part = ts_str.split('T')[0].split(' ')[0]
flow_ts_utc = datetime.strptime(date_part, '%Y-%m-%d')
logger.warning(f"⚠️ Could only parse date part from {original_ts}, using {flow_ts_utc}")
except (ValueError, AttributeError) as e:
logger.error(f"❌ Error parsing flow_ts_utc '{flow_ts_utc}': {e}")
raise HTTPException(
status_code=400,
detail=f"Invalid flow_ts_utc format: {flow_ts_utc}. Error: {str(e)}"
)
elif isinstance(flow_ts_utc, (int, float)):
# Unix timestamp - assume UTC
flow_ts_utc = datetime.fromtimestamp(flow_ts_utc, tz=pytz.UTC)
logger.info(f"📅 Parsed Unix timestamp: {flow_ts_utc} (UTC)")
# Ensure flow_ts_utc is timezone-aware (assume UTC if naive)
if flow_ts_utc.tzinfo is None:
flow_ts_utc = pytz.UTC.localize(flow_ts_utc)
logger.info(f"📅 flow_ts_utc was naive, localized to UTC: {flow_ts_utc}")
if isinstance(flow_date_cst, str):
try:
# Extract just the date part if it's a datetime string
# Handle formats like "2025-12-10T05:00:00.000Z" or "2025-12-10"
original_date = flow_date_cst.strip()
# Split on 'T' first, then on space, take first part
if 'T' in original_date:
date_str = original_date.split('T')[0]
elif ' ' in original_date:
date_str = original_date.split(' ')[0]
else:
date_str = original_date
# Validate it's in YYYY-MM-DD format (exactly 10 characters, 2 dashes)
if len(date_str) == 10 and date_str.count('-') == 2:
flow_date_cst = datetime.strptime(date_str, '%Y-%m-%d').date()
logger.debug(f"Parsed flow_date_cst: {flow_date_cst} from '{original_date}'")
else:
raise ValueError(f"Date string '{date_str}' is not in YYYY-MM-DD format (length={len(date_str)}, dashes={date_str.count('-')})")
except (ValueError, AttributeError) as e:
logger.warning(f"Error parsing flow_date_cst '{flow_date_cst}': {e}")
# Try to extract date from flow_ts_utc if available
if flow_ts_utc and isinstance(flow_ts_utc, datetime):
flow_date_cst = flow_ts_utc.date()
logger.info(f"Using date from flow_ts_utc: {flow_date_cst}")
else:
# Default to today if we can't parse
flow_date_cst = datetime.now().date()
logger.warning(f"Using current date as fallback for flow_date_cst")
elif flow_date_cst is None:
# If not provided, try to get from flow_ts_utc
if flow_ts_utc and isinstance(flow_ts_utc, datetime):
flow_date_cst = flow_ts_utc.date()
else:
flow_date_cst = datetime.now().date()
logger.warning(f"flow_date_cst not provided, using current date as fallback")
elif flow_date_cst is None:
# If not provided, try to get from flow_ts_utc
if flow_ts_utc and isinstance(flow_ts_utc, datetime):
flow_date_cst = flow_ts_utc.date()
else:
flow_date_cst = datetime.now().date()
logger.warning(f"flow_date_cst not provided, using current date as fallback")
# Initialize services with Yahoo Finance enabled
price_service = PriceContextService(pool)
price_service.use_yahoo_finance = True
price_service.yahoo_service = YahooFinanceService()
reaction_tracker = PriceReactionTracker()
reaction_tracker.use_yahoo_finance = True
reaction_tracker.yahoo_service = YahooFinanceService()
tier_classifier = SignalTierClassifier()
checklist = TradeChecklist()
# Create a minimal DataFrame row for processing
df_row = pd.DataFrame([{
'symbol_norm': symbol,
'flow_ts_utc': flow_ts_utc,
'flow_date_cst': flow_date_cst,
**row_data # Include all other row data (badges, premium, etc.)
}])
# Calculate Phase 1 metrics
result = {}
# 1. Signal Tier (doesn't need price data)
try:
df_with_tier = tier_classifier.classify_tiers(df_row)
if not df_with_tier.empty:
result['signal_tier'] = df_with_tier.iloc[0].get('signal_tier')
# Convert numpy bool to Python bool
is_tradeable_val = df_with_tier.iloc[0].get('is_tradeable', False)
result['is_tradeable'] = bool(is_tradeable_val) if is_tradeable_val is not None else False
except Exception as e:
logger.error(f"Error calculating signal tier: {e}")
result['signal_tier'] = None
result['is_tradeable'] = False
# 2. Price Reaction (needs Yahoo Finance)
try:
# Check if signal is recent enough for intraday data
now = datetime.now(pytz.timezone('America/Chicago'))
time_diff_hours = (now - flow_ts_utc.replace(tzinfo=now.tzinfo)).total_seconds() / 3600
if time_diff_hours > 168: # 7 days
logger.warning(f"⚠️ Price reaction unavailable: Signal is {time_diff_hours/24:.1f} days old")
logger.warning(f" Yahoo Finance only provides intraday data for the last 7 days")
logger.warning(f" For historical signals, price reaction data requires your own intraday price database")
else:
logger.info(f"📊 Calculating price reaction for {symbol} (signal is {time_diff_hours:.1f} hours old)")
df_with_reactions = await reaction_tracker.enrich_with_reactions(df_row, pool)
if not df_with_reactions.empty:
result['price_reaction_5m_pct'] = df_with_reactions.iloc[0].get('price_reaction_5m_pct')
result['price_reaction_15m_pct'] = df_with_reactions.iloc[0].get('price_reaction_15m_pct')
result['price_reaction_30m_pct'] = df_with_reactions.iloc[0].get('price_reaction_30m_pct')
# Convert numpy bools to Python bools
flow_led = df_with_reactions.iloc[0].get('flow_led_to_move')
result['flow_led_to_move'] = bool(flow_led) if flow_led is not None else None
high_break = df_with_reactions.iloc[0].get('high_break_5m')
result['high_break_5m'] = bool(high_break) if high_break is not None else None
low_break = df_with_reactions.iloc[0].get('low_break_5m')
result['low_break_5m'] = bool(low_break) if low_break is not None else None
except Exception as e:
logger.error(f"Error calculating price reaction: {e}")
result['price_reaction_5m_pct'] = None
result['price_reaction_15m_pct'] = None
result['price_reaction_30m_pct'] = None
result['flow_led_to_move'] = None
result['high_break_5m'] = None
result['low_break_5m'] = None
# 3. VWAP (needs Yahoo Finance)
try:
# Ensure flow_ts_utc is timezone-aware
# If it's naive (no timezone), assume it's UTC (as the name suggests)
# If it has timezone info, use it
if flow_ts_utc.tzinfo is None:
# Naive datetime - assume UTC (as name suggests)
flow_ts_utc = pytz.UTC.localize(flow_ts_utc)
logger.debug(f"flow_ts_utc was naive, assumed UTC: {flow_ts_utc}")
# Convert to Eastern Time for market hours check (US market opens at 9:30 AM ET)
et_tz = pytz.timezone('America/New_York')
signal_time_et = flow_ts_utc.astimezone(et_tz)
cst_tz = pytz.timezone('America/Chicago')
signal_time_cst = flow_ts_utc.astimezone(cst_tz)
# Check if signal is recent enough and during market hours
now_et = datetime.now(et_tz)
now_cst = datetime.now(cst_tz)
time_diff_hours = (now_et - signal_time_et).total_seconds() / 3600
signal_hour_et = signal_time_et.hour
signal_minute_et = signal_time_et.minute
logger.info(f"📅 Signal time: {signal_time_et.strftime('%Y-%m-%d %H:%M:%S %Z')} (ET) / {signal_time_cst.strftime('%H:%M:%S %Z')} (CST)")
logger.info(f"📅 Current time: {now_et.strftime('%Y-%m-%d %H:%M:%S %Z')} (ET) / {now_cst.strftime('%H:%M:%S %Z')} (CST)")
if time_diff_hours > 168: # 7 days
logger.warning(f"⚠️ VWAP unavailable: Signal is {time_diff_hours/24:.1f} days old")
logger.warning(f" Yahoo Finance only provides intraday data for the last 7 days")
elif signal_hour_et < 9 or (signal_hour_et == 9 and signal_minute_et < 30):
logger.warning(f"⚠️ VWAP unavailable: Signal time is {signal_time_et.strftime('%H:%M')} ET (before RTH open at 9:30 AM ET)")
logger.warning(f" VWAP can only be calculated after market open (9:30 AM Eastern Time)")
else:
logger.info(f"📊 Calculating VWAP for {symbol} at {signal_time_et.strftime('%Y-%m-%d %H:%M')} ET")
vwap_data = await price_service.calculate_vwap_at_time(symbol, flow_ts_utc)
if vwap_data:
result['vwap_at_signal'] = vwap_data.get('vwap')
# Get price at signal time
price_at_time = await price_service.get_price_at_time(symbol, flow_ts_utc)
if price_at_time and price_at_time.get('close') and result.get('vwap_at_signal'):
price_vs_vwap = ((price_at_time['close'] - result['vwap_at_signal']) / result['vwap_at_signal']) * 100
result['price_vs_vwap_pct'] = round(price_vs_vwap, 2)
else:
result['price_vs_vwap_pct'] = None
else:
result['vwap_at_signal'] = None
result['price_vs_vwap_pct'] = None
except Exception as e:
logger.error(f"Error calculating VWAP: {e}")
result['vwap_at_signal'] = None
result['price_vs_vwap_pct'] = None
# 4. Trade Checklist (needs VWAP, but we can calculate with what we have)
try:
# Add VWAP data to row for checklist
df_row['vwap_at_signal'] = result.get('vwap_at_signal')
df_row['price_vs_vwap_pct'] = result.get('price_vs_vwap_pct')
df_with_checklist = checklist.evaluate_all(df_row)
if not df_with_checklist.empty:
result['checklist_score'] = df_with_checklist.iloc[0].get('checklist_score')
# Convert numpy bool to Python bool
checklist_passed_val = df_with_checklist.iloc[0].get('checklist_passed')
result['checklist_passed'] = bool(checklist_passed_val) if checklist_passed_val is not None else False
# Extract and convert checklist_details immediately
checklist_details_raw = df_with_checklist.iloc[0].get('checklist_details')
if checklist_details_raw is not None:
# Convert nested numpy types in checklist_details
import numpy as np
if isinstance(checklist_details_raw, dict):
result['checklist_details'] = {
k: bool(v) if isinstance(v, np.bool_) else
(int(v) if isinstance(v, np.integer) else
(float(v) if isinstance(v, np.floating) else v))
for k, v in checklist_details_raw.items()
}
else:
result['checklist_details'] = {}
else:
result['checklist_details'] = {}
except Exception as e:
logger.error(f"Error calculating checklist: {e}")
result['checklist_score'] = None
result['checklist_passed'] = False
result['checklist_details'] = {}
# Convert numpy types to native Python types for JSON serialization
def convert_to_native(obj):
"""Recursively convert numpy types to native Python types"""
import numpy as np
# Handle None and NaN first
if obj is None:
return None
try:
if hasattr(pd, 'isna') and pd.isna(obj):
return None
except (TypeError, ValueError):
pass
try:
if isinstance(obj, float) and np.isnan(obj):
return None
except (TypeError, ValueError):
pass
# Handle numpy types
try:
if isinstance(obj, np.bool_):
return bool(obj)
elif isinstance(obj, np.integer):
return int(obj)
elif isinstance(obj, np.floating):
return float(obj)
elif isinstance(obj, np.ndarray):
return obj.tolist()
except (TypeError, AttributeError):
pass
# Handle collections
try:
if isinstance(obj, dict):
return {str(k): convert_to_native(v) for k, v in obj.items()}
except (TypeError, AttributeError):
return {}
try:
if isinstance(obj, (list, tuple)):
return [convert_to_native(item) for item in obj]
except (TypeError, AttributeError):
return []
# Handle pandas Series/DataFrame (shouldn't happen, but just in case)
if hasattr(obj, '__dict__') and not isinstance(obj, dict):
try:
# Try to convert to string representation
return str(obj)
except:
return None
return obj
# Convert result dictionary
result_converted = convert_to_native(result)
return {
'success': True,
'data': result_converted
}
except Exception as e:
logger.error(f"Error in Phase 1 calculation: {e}", exc_info=True)
raise HTTPException(
status_code=500,
detail=f"Phase 1 calculation failed: {str(e)}"
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8010)
uvicorn.run(app, host="0.0.0.0", port=8000)

View File

@ -1,11 +1,10 @@
fastapi>=0.104.1
uvicorn[standard]>=0.24.0
pandas>=2.2.0
numpy>=1.26.0
asyncpg>=0.29.0
python-dotenv>=1.0.0
pydantic>=2.9.0
python-multipart>=0.0.6
pytz>=2023.3
yfinance>=0.2.28
fastapi==0.104.1
uvicorn[standard]==0.24.0
pandas==2.1.3
numpy==1.26.2
asyncpg==0.29.0
python-dotenv==1.0.0
pydantic==2.5.0
python-multipart==0.0.6
pytz==2023.3

View File

@ -1,256 +0,0 @@
"""
Dealer-Aware Flow Context Service
Tracks dealer hedging pressure, gamma exposure, and flow continuation patterns
"""
import pandas as pd
import numpy as np
from typing import Dict, Optional
from datetime import timedelta
from utils.logger import logger
class DealerFlowContext:
"""
Analyzes flow from dealer perspective:
- Net gamma exposure per symbol
- Gamma flip proximity (when dealers become long/short gamma)
- Dealer hedge pressure (forced hedging activity)
"""
def __init__(self):
# Configuration
self.analysis_window_minutes = 120 # Look back window for gamma tracking
self.gamma_flip_threshold = 0.3 # Gamma flip when net gamma crosses threshold
def calculate_net_gamma_exposure_per_symbol(
self,
df: pd.DataFrame,
symbol: str,
timestamp: pd.Timestamp
) -> float:
"""
Calculate net gamma exposure for a symbol at a given time.
Sums all gamma exposures from recent flow.
Positive = dealers are long gamma (hedging by selling on rallies, buying on dips)
Negative = dealers are short gamma (hedging by buying on rallies, selling on dips)
"""
if df.empty:
return 0.0
# Look at flow up to this timestamp
window_start = timestamp - timedelta(minutes=self.analysis_window_minutes)
mask = (
(df['symbol_norm'] == symbol.upper()) &
(df['flow_ts_utc'] >= window_start) &
(df['flow_ts_utc'] <= timestamp) &
(df['gamma_exposure'].notna())
)
recent_flow = df[mask]
if recent_flow.empty:
return 0.0
# Sum gamma exposures
net_gamma = recent_flow['gamma_exposure'].sum()
return float(net_gamma)
def calculate_gamma_flip_proximity(
self,
df: pd.DataFrame,
row_idx: int
) -> Optional[float]:
"""
Calculate proximity to gamma flip (when dealers switch from long to short gamma or vice versa).
Returns: -1.0 to 1.0
- Positive = approaching long gamma (dealers becoming volatility buyers)
- Negative = approaching short gamma (dealers becoming volatility sellers)
- 0.0 = near flip point
"""
if row_idx >= len(df):
return None
current_row = df.iloc[row_idx]
symbol = current_row.get('symbol_norm')
flow_ts_utc = current_row.get('flow_ts_utc')
gamma_exposure = current_row.get('gamma_exposure', 0) or 0
if pd.isna(symbol) or pd.isna(flow_ts_utc):
return None
# Calculate current net gamma
net_gamma = self.calculate_net_gamma_exposure_per_symbol(df, symbol, flow_ts_utc)
# Normalize to -1 to 1 scale
# Use exponential scaling to emphasize near-flip conditions
if net_gamma == 0:
return 0.0
# Simple normalization: divide by a large threshold
# More sophisticated: use percentile or adaptive scaling
threshold = 1000000000 # 1B in gamma exposure as normalization factor
normalized = net_gamma / threshold
# Clamp to -1 to 1
normalized = max(-1.0, min(1.0, normalized))
# Invert: negative normalized = positive proximity (approaching long gamma)
# This is because dealer short gamma (negative) means they're selling volatility
return float(-normalized)
def calculate_dealer_hedge_pressure_score(
self,
df: pd.DataFrame,
row_idx: int
) -> float:
"""
Calculate dealer hedge pressure score (0-100).
Higher score = more forced hedging by dealers:
- High net gamma exposure (dealers must hedge)
- Recent flow creating gamma imbalance
- Flow continuation (dealers hedging creates more flow)
"""
if row_idx >= len(df):
return 0.0
current_row = df.iloc[row_idx]
symbol = current_row.get('symbol_norm')
flow_ts_utc = current_row.get('flow_ts_utc')
gamma_exposure = current_row.get('gamma_exposure', 0) or 0
if pd.isna(symbol) or pd.isna(flow_ts_utc):
return 0.0
score = 0.0
# Net gamma component (0-40 points)
# High absolute net gamma = more hedge pressure
net_gamma = abs(self.calculate_net_gamma_exposure_per_symbol(df, symbol, flow_ts_utc))
if net_gamma > 0:
# Normalize: 500M = 20 points, 1B = 40 points
normalized_gamma = min(1.0, net_gamma / 1000000000) # 1B threshold
score += normalized_gamma * 40.0
# Recent gamma accumulation (0-30 points)
# Recent flow creating gamma imbalance
window_start = flow_ts_utc - timedelta(minutes=30)
mask = (
(df['symbol_norm'] == symbol.upper()) &
(df['flow_ts_utc'] >= window_start) &
(df['flow_ts_utc'] <= flow_ts_utc) &
(df['gamma_exposure'].notna())
)
recent_gamma = df[mask]['gamma_exposure'].sum()
if abs(recent_gamma) > 0:
# Normalize recent gamma accumulation
normalized_recent = min(1.0, abs(recent_gamma) / 500000000) # 500M threshold
score += normalized_recent * 30.0
# Flow continuation component (0-30 points)
# If flow is continuing in same direction, dealers are likely hedging
follow_on_ratio = current_row.get('follow_on_ratio')
if follow_on_ratio is not None and not pd.isna(follow_on_ratio):
# High follow-on ratio = continuation = hedge pressure
score += follow_on_ratio * 30.0
return min(100.0, max(0.0, score))
def validate_flow_continuation(
self,
df: pd.DataFrame,
row_idx: int
) -> bool:
"""
Validate if flow continuation is likely based on dealer hedge pressure.
Returns True if continuation is expected (dealers forced to hedge).
"""
current_row = df.iloc[row_idx]
dealer_pressure = current_row.get('dealer_hedge_pressure_score', 0) or 0
net_gamma = current_row.get('net_gamma_exposure_per_symbol', 0) or 0
# High dealer pressure + significant gamma exposure = continuation likely
if dealer_pressure > 50.0 and abs(net_gamma) > 100000000: # 100M threshold
return True
return False
def validate_flow_reversal(
self,
df: pd.DataFrame,
row_idx: int
) -> bool:
"""
Validate if flow reversal is likely.
Reversal happens when:
- Dealers finish hedging (gamma exposure neutralizes)
- Price fails at key levels (VWAP, opening range)
- Flow shows distribution pattern (decreasing premium, widening gaps)
"""
current_row = df.iloc[row_idx]
dealer_pressure = current_row.get('dealer_hedge_pressure_score', 0) or 0
follow_on_ratio = current_row.get('follow_on_ratio')
flow_acceleration = current_row.get('flow_acceleration')
# Low dealer pressure + low follow-on ratio = reversal likely
if dealer_pressure < 30.0:
if follow_on_ratio is not None and follow_on_ratio < 0.3:
return True
# Negative flow acceleration = flow weakening = reversal
if flow_acceleration is not None and flow_acceleration < -10000:
return True
return False
def enrich_with_dealer_context(self, df: pd.DataFrame) -> pd.DataFrame:
"""
Add dealer-aware flow context metrics to DataFrame.
Adds: net_gamma_exposure_per_symbol, gamma_flip_proximity, dealer_hedge_pressure_score
"""
if df.empty:
return df
df = df.copy()
# Initialize columns
df['net_gamma_exposure_per_symbol'] = 0.0
df['gamma_flip_proximity'] = None
df['dealer_hedge_pressure_score'] = 0.0
# Sort by timestamp for proper gamma tracking
df_sorted = df.sort_values('flow_ts_utc').reset_index(drop=True)
# Calculate metrics for each row
for idx in df_sorted.index:
original_idx = df_sorted.index[idx]
row = df_sorted.iloc[idx]
symbol = row.get('symbol_norm')
flow_ts_utc = row.get('flow_ts_utc')
if pd.notna(symbol) and pd.notna(flow_ts_utc):
# Net gamma exposure
net_gamma = self.calculate_net_gamma_exposure_per_symbol(
df_sorted, symbol, flow_ts_utc
)
df.at[original_idx, 'net_gamma_exposure_per_symbol'] = float(net_gamma)
# Gamma flip proximity
flip_prox = self.calculate_gamma_flip_proximity(df_sorted, idx)
if flip_prox is not None:
df.at[original_idx, 'gamma_flip_proximity'] = float(flip_prox)
# Dealer hedge pressure
pressure = self.calculate_dealer_hedge_pressure_score(df_sorted, idx)
df.at[original_idx, 'dealer_hedge_pressure_score'] = float(pressure)
logger.info(f"Dealer context enrichment complete. Mean pressure: {df['dealer_hedge_pressure_score'].mean():.2f}")
return df

View File

@ -1,118 +0,0 @@
"""
Flow Decay & Reversal Validation Service
Validates flow decay/reversal signals with anchors (premium, dealer pressure, price levels)
"""
import pandas as pd
import numpy as np
from typing import Optional
from enum import Enum
from utils.logger import logger
class FlowState(Enum):
"""Flow state classification"""
ACTIONABLE = "ACTIONABLE" # Flow decay/reversal is actionable (trade signal)
INFORMATIONAL = "INFORMATIONAL" # Flow decay/reversal is informational only (no trade signal)
class FlowDecayValidator:
"""
Validates flow decay and reversal signals.
Flow decay/reversal is actionable ONLY IF:
- Premium contracts (high relative premium)
- Dealer hedge pressure decreases
- Price fails near VWAP / opening range / key level
Otherwise mark as INFORMATIONAL.
"""
def __init__(self):
# Configuration
self.min_relative_premium_for_actionable = 60.0 # Minimum relative premium score
self.vwap_failure_threshold_pct = 0.5 # Price within 0.5% of VWAP = failure
self.dealer_pressure_decrease_threshold = 20.0 # Dealer pressure decrease threshold
def validate_flow_decay_reversal(
self,
df: pd.DataFrame,
row_idx: int
) -> str:
"""
Validate if flow decay/reversal is actionable.
Returns FlowState enum value as string.
"""
if row_idx >= len(df):
return FlowState.INFORMATIONAL.value
current_row = df.iloc[row_idx]
# Check 1: Premium contracts (relative premium score)
relative_premium_score = current_row.get('relative_premium_score', 0) or 0
if relative_premium_score < self.min_relative_premium_for_actionable:
return FlowState.INFORMATIONAL.value
# Check 2: Dealer hedge pressure decreases
# Look at recent dealer pressure trend
symbol = current_row.get('symbol_norm')
flow_ts_utc = current_row.get('flow_ts_utc')
if pd.notna(symbol) and pd.notna(flow_ts_utc):
from datetime import timedelta
window_start = flow_ts_utc - timedelta(minutes=30)
mask = (
(df['symbol_norm'] == symbol.upper()) &
(df['flow_ts_utc'] >= window_start) &
(df['flow_ts_utc'] <= flow_ts_utc) &
(df['dealer_hedge_pressure_score'].notna())
)
recent_pressure = df[mask]['dealer_hedge_pressure_score']
if len(recent_pressure) >= 2:
current_pressure = current_row.get('dealer_hedge_pressure_score', 0) or 0
recent_avg = recent_pressure.iloc[:-1].mean()
pressure_decrease = recent_avg - current_pressure
if pressure_decrease < self.dealer_pressure_decrease_threshold:
return FlowState.INFORMATIONAL.value
# Check 3: Price fails near VWAP / opening range / key level
price_vs_vwap_pct = current_row.get('price_vs_vwap_pct')
pct_vs_rth_open = current_row.get('pct_vs_rth_open')
price_failure = False
# VWAP failure
if price_vs_vwap_pct is not None and not pd.isna(price_vs_vwap_pct):
if abs(price_vs_vwap_pct) <= self.vwap_failure_threshold_pct:
price_failure = True
# Opening range failure
if pct_vs_rth_open is not None and not pd.isna(pct_vs_rth_open):
if abs(pct_vs_rth_open) <= 0.3: # Within 0.3% of open
price_failure = True
if not price_failure:
return FlowState.INFORMATIONAL.value
# All checks passed: actionable
return FlowState.ACTIONABLE.value
def enrich_with_flow_state(self, df: pd.DataFrame) -> pd.DataFrame:
"""
Add flow state validation to DataFrame.
Adds: flow_state
"""
if df.empty:
return df
df = df.copy()
df['flow_state'] = FlowState.ACTIONABLE.value # Default to actionable
# Only validate flow decay/reversal cases
# For now, mark all as actionable (can be enhanced based on flow_acceleration, follow_on_ratio)
# This is a placeholder - in production, you'd identify decay/reversal patterns first
logger.info(f"Flow state validation complete. Actionable: {(df['flow_state'] == FlowState.ACTIONABLE.value).sum()}")
return df

View File

@ -1,189 +0,0 @@
"""
Institutional Confidence Metrics Service
Calculates confidence scores for institutional flow signals
"""
import pandas as pd
import numpy as np
from typing import Optional
from utils.logger import logger
class InstitutionalConfidence:
"""
Calculates institutional confidence metrics:
- confidence_score (0-100)
- institutional_likelihood (0-1)
- dealer_pain_level (0-100)
- expected_move_vs_implied (ratio)
"""
def __init__(self):
# Configuration
self.min_premium_for_institutional = 200000 # Minimum premium for institutional classification
def calculate_confidence_score(self, row: pd.Series) -> float:
"""
Calculate overall confidence score (0-100) combining multiple factors.
Higher = more confidence in signal quality.
"""
score = 0.0
# Relative premium component (0-25 points)
relative_premium = row.get('relative_premium_score', 0) or 0
score += (relative_premium / 100.0) * 25.0
# Signal strength component (0-25 points)
signal_strength = row.get('signal_strength', 0) or 0
score += (signal_strength / 100.0) * 25.0
# Dealer pressure component (0-20 points)
dealer_pressure = row.get('dealer_hedge_pressure_score', 0) or 0
score += (dealer_pressure / 100.0) * 20.0
# Flow continuation component (0-15 points)
follow_on_ratio = row.get('follow_on_ratio')
if follow_on_ratio is not None and not pd.isna(follow_on_ratio):
score += follow_on_ratio * 15.0
# Strike laddering component (0-15 points)
strike_laddering = row.get('strike_laddering_detected', False)
if strike_laddering:
score += 15.0
return min(100.0, max(0.0, score))
def calculate_institutional_likelihood(self, row: pd.Series) -> float:
"""
Calculate likelihood that flow is institutional (0-1).
Based on premium size, trade characteristics, and patterns.
"""
premium_num = row.get('premium_num', 0) or 0
relative_premium = row.get('relative_premium_score', 0) or 0
size_concentration = row.get('size_concentration_score', 0) or 0
likelihood = 0.0
# Premium size component (0-40%)
if premium_num >= 1000000: # $1M+
likelihood += 0.40
elif premium_num >= 500000: # $500K+
likelihood += 0.30
elif premium_num >= self.min_premium_for_institutional:
likelihood += 0.20
# Relative premium component (0-30%)
if relative_premium >= 80:
likelihood += 0.30
elif relative_premium >= 60:
likelihood += 0.20
elif relative_premium >= 40:
likelihood += 0.10
# Size concentration component (0-30%)
# Institutional trades are often concentrated
if size_concentration >= 70:
likelihood += 0.30
elif size_concentration >= 50:
likelihood += 0.20
elif size_concentration >= 30:
likelihood += 0.10
return min(1.0, max(0.0, likelihood))
def calculate_dealer_pain_level(self, row: pd.Series) -> float:
"""
Calculate dealer pain level (0-100).
Higher = dealers in pain (large gamma exposure, forced to hedge).
"""
dealer_pressure = row.get('dealer_hedge_pressure_score', 0) or 0
net_gamma = abs(row.get('net_gamma_exposure_per_symbol', 0) or 0)
gamma_flip_prox = row.get('gamma_flip_proximity')
pain = 0.0
# Dealer pressure component (0-50 points)
pain += (dealer_pressure / 100.0) * 50.0
# Gamma exposure component (0-30 points)
# Large absolute gamma = more pain
if net_gamma > 0:
normalized_gamma = min(1.0, net_gamma / 1000000000) # 1B threshold
pain += normalized_gamma * 30.0
# Gamma flip proximity component (0-20 points)
# Near flip = high pain (dealers forced to adjust)
if gamma_flip_prox is not None and not pd.isna(gamma_flip_prox):
# Absolute value of proximity (closer to 0 = more pain)
pain += (1.0 - abs(gamma_flip_prox)) * 20.0
return min(100.0, max(0.0, pain))
def calculate_expected_move_vs_implied(self, row: pd.Series) -> Optional[float]:
"""
Calculate expected move vs implied move ratio.
Estimates expected move from flow characteristics vs implied volatility.
Returns: ratio (expected_move / implied_move)
- >1.0 = flow suggests larger move than implied
- <1.0 = flow suggests smaller move than implied
- None if cannot calculate
"""
# Simplified calculation: use premium and delta exposure as proxies
premium_num = row.get('premium_num', 0) or 0
spot_num = row.get('spot_num', 0) or 0
delta_exposure = abs(row.get('delta_exposure', 0) or 0)
if pd.isna(spot_num) or spot_num == 0:
return None
# Estimate expected move from premium paid
# High premium relative to spot = expectation of larger move
if premium_num > 0:
prem_to_spot_ratio = premium_num / spot_num
# Estimate implied move (simplified: assume 1% IV = 1% move expectation)
# This is a placeholder - in production, use actual IV from options chain
implied_move_pct = 2.0 # Default 2% implied move
# Estimate expected move from premium
# Premium of 2% of spot = expectation of ~2% move (rough approximation)
expected_move_pct = prem_to_spot_ratio * 100.0
# Calculate ratio
if implied_move_pct > 0:
ratio = expected_move_pct / implied_move_pct
return float(ratio)
return None
def enrich_with_confidence_metrics(self, df: pd.DataFrame) -> pd.DataFrame:
"""
Add institutional confidence metrics to DataFrame.
Adds: confidence_score, institutional_likelihood, dealer_pain_level, expected_move_vs_implied
"""
if df.empty:
return df
df = df.copy()
# Initialize columns
df['confidence_score'] = 0.0
df['institutional_likelihood'] = 0.0
df['dealer_pain_level'] = 0.0
df['expected_move_vs_implied'] = None
# Calculate metrics
df['confidence_score'] = df.apply(self.calculate_confidence_score, axis=1)
df['institutional_likelihood'] = df.apply(self.calculate_institutional_likelihood, axis=1)
df['dealer_pain_level'] = df.apply(self.calculate_dealer_pain_level, axis=1)
# Expected move vs implied (some rows may not have this)
for idx in df.index:
expected_move = self.calculate_expected_move_vs_implied(df.iloc[idx])
if expected_move is not None:
df.at[idx, 'expected_move_vs_implied'] = float(expected_move)
logger.info(f"Confidence metrics complete. Mean confidence: {df['confidence_score'].mean():.2f}")
return df

View File

@ -1,232 +0,0 @@
"""
Intent Classification Service
Replaces naive direction (BULL/BEAR) with nuanced volatility and hedging intent classification
"""
import pandas as pd
import numpy as np
from typing import Optional
from enum import Enum
from utils.logger import logger
from utils.error_handler import safe_divide
class VolatilityIntent(Enum):
"""Volatility and hedging intent classification"""
LONG_VOL = "LONG_VOL" # Buying volatility (call/put buying, expecting large moves)
SHORT_VOL = "SHORT_VOL" # Selling volatility (premium collection, expecting low vol)
DIRECTIONAL = "DIRECTIONAL" # Directional positioning (directional bias)
HEDGE_UNWIND = "HEDGE_UNWIND" # Hedging or unwinding existing positions
class IntentClassifier:
"""
Classifies options flow intent beyond simple BULL/BEAR direction.
Identifies volatility trading, hedging, and directional positioning.
"""
def __init__(self):
# Configuration thresholds
self.otm_threshold_pct = 5.0 # Strikes >5% OTM considered OTM
self.long_vol_premium_threshold = 100000 # Minimum premium for long vol signal
self.short_vol_premium_threshold = 200000 # Minimum premium for short vol signal
def estimate_delta(self, row: pd.Series) -> float:
"""
Estimate option delta from moneyness.
Returns delta estimate (0-1 for calls, -1-0 for puts, use absolute value).
"""
spot_num = row.get('spot_num', 0) or 0
strike_num = row.get('strike_num', 0) or 0
cp_norm = row.get('cp_norm', '')
if pd.isna(spot_num) or spot_num == 0 or pd.isna(strike_num) or pd.isna(cp_norm):
return 0.5 # Default to ATM
moneyness_ratio = strike_num / spot_num if spot_num > 0 else 1.0
if cp_norm == 'CALL':
if strike_num <= spot_num:
# ITM call: delta 0.5 to 1.0
delta = 0.5 + (1.0 - moneyness_ratio) * 0.5
else:
# OTM call: delta 0.5 to 0.0
delta = max(0.0, 0.5 * (1.5 - moneyness_ratio) / 0.5)
else: # PUT
if strike_num >= spot_num:
# ITM put: delta -0.5 to -1.0 (return absolute)
delta = abs(0.5 + (moneyness_ratio - 1.0) * 0.5)
else:
# OTM put: delta -0.5 to 0.0 (return absolute)
delta = max(0.0, 0.5 * (1.0 - moneyness_ratio) / 0.5)
return float(delta)
def estimate_gamma(self, row: pd.Series, delta: float) -> float:
"""
Estimate option gamma (sensitivity of delta to price changes).
Higher near ATM, lower ITM/OTM.
Returns gamma estimate (positive value).
"""
spot_num = row.get('spot_num', 0) or 0
strike_num = row.get('strike_num', 0) or 0
if pd.isna(spot_num) or spot_num == 0 or pd.isna(strike_num):
return 0.0
# Gamma is highest at-the-money
moneyness_ratio = strike_num / spot_num if spot_num > 0 else 1.0
# Simple approximation: gamma peaks at 1.0 (ATM) and decays away
# Use normal distribution approximation
distance_from_atm = abs(moneyness_ratio - 1.0)
# Gamma ≈ exp(-distance^2 / (2*sigma^2)) where sigma ≈ 0.1 (10% moneyness)
gamma = np.exp(-(distance_from_atm ** 2) / (2 * 0.01))
return float(gamma)
def calculate_delta_exposure(self, row: pd.Series) -> float:
"""
Calculate delta exposure: contracts * delta * 100 * spot_price.
Positive = long delta (bullish), Negative = short delta (bearish).
"""
premium_num = row.get('premium_num', 0) or 0
spot_num = row.get('spot_num', 0) or 0
vol_num = row.get('vol_num', 0) or 0
cp_norm = row.get('cp_norm', '')
side_norm = row.get('side_norm', '')
if pd.isna(spot_num) or spot_num == 0:
return 0.0
# Estimate delta
delta = self.estimate_delta(row)
# Determine sign based on call/put and buy/sell
if cp_norm == 'CALL':
if side_norm == 'BUY':
delta_sign = 1.0 # Long calls = positive delta
else: # SELL
delta_sign = -1.0 # Short calls = negative delta
else: # PUT
if side_norm == 'BUY':
delta_sign = -1.0 # Long puts = negative delta
else: # SELL
delta_sign = 1.0 # Short puts = positive delta
# Delta exposure = contracts * delta * 100 * spot
# Use volume as proxy for contracts
contracts = vol_num if not pd.isna(vol_num) else 0
delta_exposure = contracts * delta * delta_sign * 100 * spot_num
return float(delta_exposure)
def calculate_gamma_exposure(self, row: pd.Series) -> float:
"""
Calculate gamma exposure: contracts * gamma * 100 * spot_price^2.
Positive = long gamma (volatility long), Negative = short gamma (volatility short).
"""
premium_num = row.get('premium_num', 0) or 0
spot_num = row.get('spot_num', 0) or 0
vol_num = row.get('vol_num', 0) or 0
cp_norm = row.get('cp_norm', '')
side_norm = row.get('side_norm', '')
if pd.isna(spot_num) or spot_num == 0:
return 0.0
# Estimate delta and gamma
delta = self.estimate_delta(row)
gamma = self.estimate_gamma(row, delta)
# Determine sign: buying options = long gamma, selling = short gamma
if side_norm == 'BUY':
gamma_sign = 1.0 # Long gamma
else: # SELL
gamma_sign = -1.0 # Short gamma
# Gamma exposure = contracts * gamma * 100 * spot^2
contracts = vol_num if not pd.isna(vol_num) else 0
gamma_exposure = contracts * gamma * gamma_sign * 100 * (spot_num ** 2)
return float(gamma_exposure)
def classify_volatility_intent(self, row: pd.Series) -> str:
"""
Classify volatility intent based on trade characteristics.
Returns VolatilityIntent enum value as string.
"""
premium_num = row.get('premium_num', 0) or 0
spot_num = row.get('spot_num', 0) or 0
strike_num = row.get('strike_num', 0) or 0
cp_norm = row.get('cp_norm', '')
side_norm = row.get('side_norm', '')
vol_num = row.get('vol_num', 0) or 0
oi_num = row.get('oi_num', 0) or 0
# Calculate moneyness
if pd.isna(spot_num) or spot_num == 0 or pd.isna(strike_num):
mny_pct = 0.0
else:
if cp_norm == 'CALL':
mny_pct = ((strike_num - spot_num) / spot_num * 100.0) if spot_num > 0 else 0
else: # PUT
mny_pct = ((spot_num - strike_num) / spot_num * 100.0) if spot_num > 0 else 0
is_otm = abs(mny_pct) > self.otm_threshold_pct
# Long volatility: buying OTM options (calls or puts)
# High premium, OTM strikes, buying side
if (side_norm == 'BUY' and
is_otm and
premium_num >= self.long_vol_premium_threshold):
return VolatilityIntent.LONG_VOL.value
# Short volatility: selling options, collecting premium
# High premium, selling side, vol > OI (opening new short positions)
if (side_norm == 'SELL' and
premium_num >= self.short_vol_premium_threshold and
vol_num > oi_num):
return VolatilityIntent.SHORT_VOL.value
# Directional: ITM options, buying side, strong directional flow
if (side_norm == 'BUY' and
not is_otm and
premium_num >= 50000):
return VolatilityIntent.DIRECTIONAL.value
# Hedge/Unwind: Selling existing positions (vol < OI)
# Or buying protective puts/calls
if (side_norm == 'SELL' and vol_num < oi_num) or \
(side_norm == 'BUY' and is_otm and premium_num < self.long_vol_premium_threshold):
return VolatilityIntent.HEDGE_UNWIND.value
# Default: directional (fallback)
return VolatilityIntent.DIRECTIONAL.value
def enrich_with_intent_classification(self, df: pd.DataFrame) -> pd.DataFrame:
"""
Add intent classification metrics to DataFrame.
Adds: delta_exposure, gamma_exposure, volatility_intent
"""
if df.empty:
return df
df = df.copy()
# Initialize columns
df['delta_exposure'] = 0.0
df['gamma_exposure'] = 0.0
df['volatility_intent'] = VolatilityIntent.DIRECTIONAL.value
# Calculate metrics
df['delta_exposure'] = df.apply(self.calculate_delta_exposure, axis=1)
df['gamma_exposure'] = df.apply(self.calculate_gamma_exposure, axis=1)
df['volatility_intent'] = df.apply(self.classify_volatility_intent, axis=1)
# Log distribution
intent_counts = df['volatility_intent'].value_counts()
logger.info(f"Intent classification complete. Distribution: {intent_counts.to_dict()}")
return df

View File

@ -1,104 +0,0 @@
"""
Market Regime Detection Service
Identifies market regime to gate trade signal generation
"""
import pandas as pd
import numpy as np
from typing import Optional
from enum import Enum
from utils.logger import logger
class MarketRegime(Enum):
"""Market regime classification"""
TREND = "TREND" # Trending market (continuation bias)
RANGE = "RANGE" # Range-bound market (fade or vol-sell bias)
HIGH_VOL_EVENT = "HIGH_VOL_EVENT" # High volatility event (volatility expansion bias)
class MarketRegimeDetector:
"""
Detects market regime to inform trade signal bias:
- Trend: continuation trades preferred
- Range: mean reversion / vol selling preferred
- High Vol Event: volatility expansion trades preferred
"""
def __init__(self):
# Configuration
self.trend_threshold_pct = 1.5 # >1.5% move = trending
self.range_threshold_pct = 0.5 # <0.5% move = ranging
self.high_vol_threshold_pct = 3.0 # >3% move = high vol event
self.lookback_minutes = 60 # Lookback window for regime detection
def detect_regime(
self,
df: pd.DataFrame,
row_idx: int
) -> str:
"""
Detect market regime for a given flow event.
Returns MarketRegime enum value as string.
"""
if row_idx >= len(df):
return MarketRegime.RANGE.value
current_row = df.iloc[row_idx]
symbol = current_row.get('symbol_norm')
flow_ts_utc = current_row.get('flow_ts_utc')
if pd.isna(symbol) or pd.isna(flow_ts_utc):
return MarketRegime.RANGE.value
# Get price movement over lookback window
pct_vs_rth_open = current_row.get('pct_vs_rth_open')
pct_vs_prior_close = current_row.get('pct_vs_prior_close')
pct_15m_momo = current_row.get('pct_15m_momo')
# Use most appropriate price metric
price_move_pct = None
if pct_vs_rth_open is not None and not pd.isna(pct_vs_rth_open):
price_move_pct = abs(pct_vs_rth_open)
elif pct_vs_prior_close is not None and not pd.isna(pct_vs_prior_close):
price_move_pct = abs(pct_vs_prior_close)
elif pct_15m_momo is not None and not pd.isna(pct_15m_momo):
price_move_pct = abs(pct_15m_momo)
if price_move_pct is None:
return MarketRegime.RANGE.value # Default to range
# Classify regime
if price_move_pct >= self.high_vol_threshold_pct:
return MarketRegime.HIGH_VOL_EVENT.value
elif price_move_pct >= self.trend_threshold_pct:
return MarketRegime.TREND.value
elif price_move_pct <= self.range_threshold_pct:
return MarketRegime.RANGE.value
else:
# Between range and trend threshold - classify based on momentum
if pct_15m_momo is not None and not pd.isna(pct_15m_momo):
if abs(pct_15m_momo) > 0.75:
return MarketRegime.TREND.value
return MarketRegime.RANGE.value
def enrich_with_market_regime(self, df: pd.DataFrame) -> pd.DataFrame:
"""
Add market regime classification to DataFrame.
Adds: market_regime
"""
if df.empty:
return df
df = df.copy()
df['market_regime'] = MarketRegime.RANGE.value
# Detect regime for each row
for idx in df.index:
regime = self.detect_regime(df, idx)
df.at[idx, 'market_regime'] = regime
regime_counts = df['market_regime'].value_counts()
logger.info(f"Market regime detection complete. Distribution: {regime_counts.to_dict()}")
return df

View File

@ -1,213 +0,0 @@
"""
Tier-0 Noise Rejection Service
Filters out low-quality signals before enrichment to reduce processing overhead
"""
import pandas as pd
import numpy as np
from typing import Optional
from datetime import timedelta
from utils.logger import logger
from utils.error_handler import safe_divide
class NoiseRejector:
"""
Rejects early-stage noise before enrichment to optimize processing.
Rejects if:
- Single isolated trade (no repeat activity)
- Far OTM weekly lottos
- Delta-adjusted premium below threshold
"""
def __init__(self):
# Configuration
self.repeat_activity_window_minutes = 30 # Minutes to look for repeat activity
self.min_delta_adjusted_premium = 50000 # Minimum delta-adjusted premium
self.max_otm_percentage = 15.0 # Reject strikes >15% OTM
self.min_expiry_days = 1 # Minimum days to expiry (reject same-day/weekly lottos)
def is_isolated_trade(
self,
df: pd.DataFrame,
row_idx: int
) -> bool:
"""
Check if trade is isolated (no repeat activity within window).
Returns True if isolated (should reject).
"""
if row_idx >= len(df):
return True
current_row = df.iloc[row_idx]
symbol = current_row.get('symbol_norm')
direction = current_row.get('direction')
flow_ts_utc = current_row.get('flow_ts_utc')
if pd.isna(symbol) or pd.isna(flow_ts_utc):
return True
# Look for trades in same direction within window
window_start = flow_ts_utc - timedelta(minutes=self.repeat_activity_window_minutes)
same_symbol = df['symbol_norm'] == symbol
same_direction = df['direction'] == direction
in_window = (df['flow_ts_utc'] >= window_start) & (df['flow_ts_utc'] < flow_ts_utc)
# Exclude current row
other_trades = df[same_symbol & same_direction & in_window]
# If no other trades in window, it's isolated
return len(other_trades) == 0
def is_far_otm_lotto(
self,
row: pd.Series
) -> bool:
"""
Check if trade is a far OTM weekly lotto (low probability, low signal value).
Returns True if should reject.
"""
spot_num = row.get('spot_num', 0) or 0
strike_num = row.get('strike_num', 0) or 0
cp_norm = row.get('cp_norm', '')
exp_date = row.get('exp_date')
flow_date = row.get('flow_date_cst')
if pd.isna(spot_num) or spot_num == 0 or pd.isna(strike_num) or pd.isna(cp_norm):
return False
# Calculate moneyness percentage
if cp_norm == 'CALL':
otm_pct = ((strike_num - spot_num) / spot_num * 100.0) if spot_num > 0 else 0
else: # PUT
otm_pct = ((spot_num - strike_num) / spot_num * 100.0) if spot_num > 0 else 0
# Reject if >15% OTM
if otm_pct > self.max_otm_percentage:
return True
# Check if weekly lotto (expiry within 7 days)
if pd.notna(exp_date) and pd.notna(flow_date):
try:
if isinstance(exp_date, str):
from datetime import datetime
exp_date = datetime.strptime(exp_date, '%Y-%m-%d').date()
if isinstance(flow_date, str):
from datetime import datetime
flow_date = datetime.strptime(flow_date, '%Y-%m-%d').date()
days_to_expiry = (exp_date - flow_date).days
# Reject weekly lottos that are also far OTM
if days_to_expiry <= 7 and otm_pct > 10.0:
return True
except (ValueError, TypeError, AttributeError):
pass
return False
def calculate_delta_adjusted_premium(
self,
row: pd.Series
) -> float:
"""
Calculate delta-adjusted premium (premium * |delta|).
Approximates intrinsic value component of premium.
For simplicity, we estimate delta from moneyness:
- ATM: delta 0.5
- ITM: delta increases toward 1.0
- OTM: delta decreases toward 0.0
"""
premium_num = row.get('premium_num', 0) or 0
spot_num = row.get('spot_num', 0) or 0
strike_num = row.get('strike_num', 0) or 0
cp_norm = row.get('cp_norm', '')
if pd.isna(premium_num) or premium_num == 0 or pd.isna(spot_num) or spot_num == 0:
return 0.0
# Estimate delta from moneyness
if cp_norm == 'CALL':
if strike_num <= spot_num:
# ITM call: delta from 0.5 to 1.0
moneyness_ratio = strike_num / spot_num if spot_num > 0 else 1.0
# Strike at spot = 0.5, strike at 0 = 1.0
estimated_delta = 0.5 + (1.0 - moneyness_ratio) * 0.5
else:
# OTM call: delta from 0.5 to 0.0
moneyness_ratio = strike_num / spot_num if spot_num > 0 else 1.0
# Strike at spot = 0.5, strike at 1.15x spot = 0.0
estimated_delta = max(0.0, 0.5 * (1.5 - moneyness_ratio) / 0.5)
else: # PUT
if strike_num >= spot_num:
# ITM put: delta from -0.5 to -1.0 (use absolute value)
moneyness_ratio = strike_num / spot_num if spot_num > 0 else 1.0
estimated_delta = 0.5 + (moneyness_ratio - 1.0) * 0.5
else:
# OTM put: delta from 0.5 to 0.0 (use absolute value)
moneyness_ratio = strike_num / spot_num if spot_num > 0 else 1.0
estimated_delta = max(0.0, 0.5 * (1.0 - moneyness_ratio) / 0.5)
# Delta-adjusted premium
delta_adj_premium = premium_num * abs(estimated_delta)
return float(delta_adj_premium)
def should_reject(
self,
df: pd.DataFrame,
row_idx: int
) -> bool:
"""
Determine if row should be rejected as noise.
Returns True if should reject (mark early_noise_reject = True).
"""
if row_idx >= len(df):
return True
row = df.iloc[row_idx]
# Reject isolated trades
if self.is_isolated_trade(df, row_idx):
return True
# Reject far OTM lottos
if self.is_far_otm_lotto(row):
return True
# Reject if delta-adjusted premium below threshold
delta_adj_premium = self.calculate_delta_adjusted_premium(row)
if delta_adj_premium < self.min_delta_adjusted_premium:
return True
return False
def mark_noise_rejections(self, df: pd.DataFrame) -> pd.DataFrame:
"""
Mark rows for early noise rejection.
Adds: early_noise_reject (boolean)
"""
if df.empty:
return df
df = df.copy()
# Initialize column
df['early_noise_reject'] = False
# Sort by timestamp for proper isolation detection
df_sorted = df.sort_values('flow_ts_utc').reset_index(drop=True)
# Mark rejections
for idx in df_sorted.index:
if self.should_reject(df_sorted, idx):
original_idx = df_sorted.index[idx]
df.at[original_idx, 'early_noise_reject'] = True
rejection_count = df['early_noise_reject'].sum()
logger.info(f"Noise rejection: {rejection_count}/{len(df)} rows marked for rejection ({rejection_count/len(df)*100:.1f}%)")
return df

View File

@ -84,10 +84,20 @@ class OptionsFlowProcessor:
def parse_timestamp(self, date_str, time_str) -> Optional[datetime]:
"""Parse timestamp from date and time strings"""
if pd.isna(date_str) or pd.isna(time_str):
if pd.isna(date_str):
return None
date_str = str(date_str).strip()
# If time_str is None/NaN, parse just the date and use midnight as default
if pd.isna(time_str) or time_str is None:
# Try to parse just the date
date_obj = self.parse_date(date_str)
if date_obj:
# Use midnight (00:00:00) as default time
return datetime.combine(date_obj.date(), datetime.min.time())
return None
time_str = str(time_str).strip()
# Try various formats

View File

@ -139,21 +139,7 @@ class OutputFormatter:
'Rocket_with_mny': 'Rocket',
}
# Only rename columns that exist (don't drop Phase 1 columns)
existing_mapping = {k: v for k, v in column_mapping.items() if k in df.columns}
df = df.rename(columns=existing_mapping)
# Ensure Phase 1 columns are preserved (don't drop them)
# Phase 1 columns should remain as-is (signal_tier, checklist_score, etc.)
# Ensure all new institutional analytics columns are preserved:
# - premium_zscore, premium_percentile_intraday, relative_premium_score
# - aggression_score, size_concentration_score, repeat_trade_velocity, strike_clustering_score, signal_strength
# - early_noise_reject, flow_acceleration, time_between_hits, follow_on_ratio, strike_laddering_detected
# - delta_exposure, gamma_exposure, volatility_intent
# - net_gamma_exposure_per_symbol, gamma_flip_proximity, dealer_hedge_pressure_score
# - market_regime, flow_state
# - confidence_score, institutional_likelihood, dealer_pain_level, expected_move_vs_implied
df = df.rename(columns=column_mapping)
return df

View File

@ -1,15 +1,12 @@
"""
Price Context Service
Handles price data enrichment for options flow
Uses Yahoo Finance for real-time data instead of database
"""
import pandas as pd
import asyncpg
from datetime import datetime, timedelta
from typing import Dict, Optional
import pytz
from services.yahoo_finance_service import YahooFinanceService
from utils.logger import logger
class PriceContextService:
@ -18,8 +15,6 @@ class PriceContextService:
def __init__(self, pool: asyncpg.Pool):
self.pool = pool
self.ct_tz = pytz.timezone('America/Chicago')
self.use_yahoo_finance = False # Temporarily disabled - focus on Phase 1 button
self.yahoo_service = YahooFinanceService() if self.use_yahoo_finance else None
def get_session_bucket(self, flow_ts_local: datetime) -> str:
"""Determine session bucket from flow timestamp"""
@ -42,108 +37,65 @@ class PriceContextService:
self,
symbol: str,
timestamp: datetime,
pool: asyncpg.Pool = None # Not used anymore, kept for compatibility
pool: asyncpg.Pool
) -> Optional[Dict]:
"""Get price data at or before a specific timestamp using Yahoo Finance"""
if not self.use_yahoo_finance or not self.yahoo_service:
return None # Yahoo Finance disabled
try:
price = self.yahoo_service.get_price_at_time(symbol, timestamp)
if price:
return {
'close': price,
'high': price, # Approximate
'low': price, # Approximate
'volume': None,
'ts': timestamp
}
return None
except Exception as e:
logger.debug(f"Error getting price at time from Yahoo Finance: {e}")
"""Get price data at or before a specific timestamp"""
async with pool.acquire() as conn:
row = await conn.fetchrow("""
SELECT close, high, low, volume, ts
FROM prices_intraday_1m
WHERE UPPER(symbol) = $1
AND ts <= $2
ORDER BY ts DESC
LIMIT 1
""", symbol.upper(), timestamp)
if row:
return dict(row)
return None
async def get_rth_open(
self,
symbol: str,
flow_date_cst: datetime.date,
pool: asyncpg.Pool = None # Not used anymore, kept for compatibility
pool: asyncpg.Pool
) -> Optional[Dict]:
"""Get first RTH bar for a given date using Yahoo Finance"""
if not self.use_yahoo_finance or not self.yahoo_service:
return None # Yahoo Finance disabled
try:
rth_open_price = self.yahoo_service.get_rth_open(symbol, flow_date_cst)
if rth_open_price:
rth_time = self.ct_tz.localize(
datetime.combine(flow_date_cst, datetime.min.time().replace(hour=9, minute=30))
)
return {
'open': rth_open_price,
'ts': rth_time
}
return None
except Exception as e:
logger.debug(f"Error getting RTH open from Yahoo Finance: {e}")
"""Get first RTH bar for a given date"""
async with pool.acquire() as conn:
row = await conn.fetchrow("""
SELECT open, ts
FROM prices_intraday_1m
WHERE UPPER(symbol) = $1
AND (timezone('America/Chicago', ts))::date = $2
AND (timezone('America/Chicago', ts))::time >= time '09:30:00'
ORDER BY ts ASC
LIMIT 1
""", symbol.upper(), flow_date_cst)
if row:
return dict(row)
return None
async def get_prior_close(
self,
symbol: str,
flow_date_cst: datetime.date,
pool: asyncpg.Pool = None # Not used anymore, kept for compatibility
pool: asyncpg.Pool
) -> Optional[float]:
"""Get prior day's close using Yahoo Finance"""
if not self.use_yahoo_finance or not self.yahoo_service:
return None # Yahoo Finance disabled
try:
return self.yahoo_service.get_prior_close(symbol, flow_date_cst)
except Exception as e:
logger.debug(f"Error getting prior close from Yahoo Finance: {e}")
return None
async def calculate_vwap_at_time(
self,
symbol: str,
timestamp: datetime,
pool: asyncpg.Pool = None # Not used anymore, kept for compatibility
) -> Optional[Dict]:
"""
Calculate VWAP (Volume Weighted Average Price) up to the given timestamp
for the trading day using Yahoo Finance. VWAP = SUM(price * volume) / SUM(volume)
"""
try:
# Convert timestamp to CST if needed
if timestamp.tzinfo is None:
timestamp = self.ct_tz.localize(timestamp)
else:
timestamp = timestamp.astimezone(self.ct_tz)
"""Get prior day's close"""
async with pool.acquire() as conn:
prior_date = flow_date_cst - timedelta(days=1)
row = await conn.fetchrow("""
SELECT close
FROM prices_daily
WHERE UPPER(symbol) = $1
AND "Date" = $2
ORDER BY "Date" DESC
LIMIT 1
""", symbol.upper(), prior_date)
# Convert to Eastern Time for market hours check (US market opens at 9:30 AM ET)
et_tz = pytz.timezone('America/New_York')
timestamp_et = timestamp.astimezone(et_tz) if timestamp.tzinfo else et_tz.localize(timestamp)
trade_time = timestamp_et.time()
# Only calculate VWAP if we're at or after RTH open (9:30 AM ET)
if trade_time < pd.Timestamp('09:30:00').time():
# Before RTH, return None (no VWAP yet)
logger.debug(f"Before RTH open (9:30 AM ET): {timestamp_et.strftime('%H:%M:%S %Z')}")
return None
# Calculate VWAP from RTH open (9:30 AM) to the given timestamp
if not self.use_yahoo_finance or not self.yahoo_service:
return None # Yahoo Finance disabled
vwap = await self.yahoo_service.calculate_vwap(symbol, timestamp)
if vwap:
return {
'vwap': vwap,
'total_volume': None, # Yahoo Finance doesn't provide this easily
'bar_count': None
}
return None
except Exception as e:
logger.debug(f"Error calculating VWAP from Yahoo Finance: {e}")
if row:
return row['close']
return None
async def enrich_flow_with_prices(
@ -160,89 +112,111 @@ class PriceContextService:
if df.empty:
return df
# Batch fetch all price data using Yahoo Finance
# Get unique symbols and dates
unique_symbols = df['symbol_norm'].unique().tolist()
unique_dates = df['flow_date_cst'].unique().tolist()
# Batch fetch prices at flow times using Yahoo Finance
# Group by symbol to reduce API calls
price_data_dict = {}
for symbol in unique_symbols:
symbol_flows = df[df['symbol_norm'] == symbol]
timestamps = symbol_flows['flow_ts_utc'].dropna().unique().tolist()
# Batch fetch all price data
async with pool.acquire() as conn:
# Get unique symbols and dates
unique_symbols = df['symbol_norm'].unique().tolist()
unique_dates = df['flow_date_cst'].unique().tolist()
if not timestamps:
continue
# For each timestamp, get the latest price <= that timestamp
# Only fetch unique timestamps per symbol
for ts in timestamps:
try:
price_data = await self.get_price_at_time(symbol, ts)
if price_data:
price_data_dict[(symbol.upper(), ts)] = price_data
# Batch fetch prices at flow times (for each symbol, get prices <= each timestamp)
price_data_dict = {}
for symbol in unique_symbols:
symbol_flows = df[df['symbol_norm'] == symbol]
timestamps = symbol_flows['flow_ts_utc'].dropna().unique().tolist()
if not timestamps:
continue
# For each timestamp, get the latest price <= that timestamp
for ts in timestamps:
row = await conn.fetchrow("""
SELECT close, high, low, volume, ts
FROM prices_intraday_1m
WHERE UPPER(symbol) = $1
AND ts <= $2
ORDER BY ts DESC
LIMIT 1
""", symbol.upper(), ts)
# Also get 5m and 15m ago prices (only if needed)
# Skip if we already have this timestamp
ts_5m = ts - timedelta(minutes=5)
if (symbol.upper(), ts_5m) not in price_data_dict and self.use_yahoo_finance and self.yahoo_service:
try:
price_5m = self.yahoo_service.get_price_at_time(symbol, ts_5m)
if price_5m:
price_data_dict[(symbol.upper(), ts_5m)] = {'close': price_5m}
except Exception as e:
pass
if row:
price_data_dict[(symbol.upper(), ts)] = dict(row)
ts_15m = ts - timedelta(minutes=15)
if (symbol.upper(), ts_15m) not in price_data_dict and self.use_yahoo_finance and self.yahoo_service:
try:
price_15m = self.yahoo_service.get_price_at_time(symbol, ts_15m)
if price_15m:
price_data_dict[(symbol.upper(), ts_15m)] = {'close': price_15m}
except Exception as e:
pass
except Exception as e:
logger.debug(f"Error fetching price for {symbol} at {ts}: {e}")
# Add small delay on error to avoid rate limiting
import asyncio
await asyncio.sleep(0.2)
# Batch fetch RTH opens using Yahoo Finance
rth_open_dict = {}
for symbol in unique_symbols:
for date in unique_dates:
try:
rth_data = await self.get_rth_open(symbol, date)
if rth_data:
rth_open_dict[(symbol.upper(), date)] = rth_data
except Exception as e:
logger.debug(f"Error fetching RTH open for {symbol} on {date}: {e}")
# Batch fetch prior closes using Yahoo Finance
prior_close_dict = {}
for symbol in unique_symbols:
for date in unique_dates:
try:
prior_close = await self.get_prior_close(symbol, date)
if prior_close:
prior_close_dict[(symbol.upper(), date)] = prior_close
except Exception as e:
logger.debug(f"Error fetching prior close for {symbol} before {date}: {e}")
# Batch fetch VWAP at signal times using Yahoo Finance
vwap_dict = {}
for symbol in unique_symbols:
symbol_flows = df[df['symbol_norm'] == symbol]
timestamps = symbol_flows['flow_ts_utc'].dropna().unique().tolist()
# Also get 5m and 15m ago prices (with error handling)
try:
ts_5m = ts - timedelta(minutes=5)
row_5m = await conn.fetchrow("""
SELECT close
FROM prices_intraday_1m
WHERE UPPER(symbol) = $1
AND ts <= $2
ORDER BY ts DESC
LIMIT 1
""", symbol.upper(), ts_5m)
if row_5m:
price_data_dict[(symbol.upper(), ts_5m)] = {'close': row_5m['close']}
except Exception as e:
# Log but don't fail on 5m price lookup
pass
try:
ts_15m = ts - timedelta(minutes=15)
row_15m = await conn.fetchrow("""
SELECT close
FROM prices_intraday_1m
WHERE UPPER(symbol) = $1
AND ts <= $2
ORDER BY ts DESC
LIMIT 1
""", symbol.upper(), ts_15m)
if row_15m:
price_data_dict[(symbol.upper(), ts_15m)] = {'close': row_15m['close']}
except Exception as e:
# Log but don't fail on 15m price lookup
pass
for ts in timestamps:
try:
vwap_data = await self.calculate_vwap_at_time(symbol, ts)
if vwap_data:
vwap_dict[(symbol.upper(), ts)] = vwap_data
except Exception as e:
logger.debug(f"Error calculating VWAP for {symbol} at {ts}: {e}")
# Batch fetch RTH opens
rth_open_dict = {}
for symbol in unique_symbols:
for date in unique_dates:
row = await conn.fetchrow("""
SELECT open, ts
FROM prices_intraday_1m
WHERE UPPER(symbol) = $1
AND (timezone('America/Chicago', ts))::date = $2
AND (timezone('America/Chicago', ts))::time >= time '09:30:00'
ORDER BY ts ASC
LIMIT 1
""", symbol.upper(), date)
if row:
rth_open_dict[(symbol.upper(), date)] = dict(row)
# Batch fetch prior closes
prior_close_dict = {}
for symbol in unique_symbols:
for date in unique_dates:
# Handle date conversion
if isinstance(date, datetime):
prior_date = (date - timedelta(days=1)).date()
elif isinstance(date, pd.Timestamp):
prior_date = (date - timedelta(days=1)).date()
else:
# Assume it's already a date object
prior_date = date - timedelta(days=1) if hasattr(date, '__sub__') else date
row = await conn.fetchrow("""
SELECT close
FROM prices_daily
WHERE UPPER(symbol) = $1
AND "Date" = $2
ORDER BY "Date" DESC
LIMIT 1
""", symbol.upper(), prior_date)
if row:
prior_close_dict[(symbol.upper(), date)] = row['close']
# Build price data for each flow row
price_data = []
@ -274,20 +248,6 @@ class PriceContextService:
price_15m_ago_data = price_data_dict.get(price_15m_key)
price_15m_ago = price_15m_ago_data.get('close') if price_15m_ago_data else None
# Get VWAP at signal time
vwap_data = vwap_dict.get((symbol.upper(), flow_ts_utc)) if not pd.isna(flow_ts_utc) else None
vwap_at_signal = vwap_data['vwap'] if vwap_data else None
# Calculate price vs VWAP percentage
price_vs_vwap_pct = None
if vwap_at_signal and price_at_time and price_at_time.get('close'):
try:
price_vs_vwap_pct = round(
((price_at_time['close'] - vwap_at_signal) / vwap_at_signal) * 100, 2
)
except (TypeError, ZeroDivisionError):
pass
price_data.append({
'u_close': price_at_time['close'] if price_at_time else None,
'u_high': price_at_time['high'] if price_at_time else None,
@ -297,8 +257,6 @@ class PriceContextService:
'prior_close': prior_close,
'close_5m_ago': price_5m_ago,
'close_15m_ago': price_15m_ago,
'vwap_at_signal': vwap_at_signal,
'price_vs_vwap_pct': price_vs_vwap_pct,
})
# Merge price data

View File

@ -1,153 +0,0 @@
"""
Price Reaction Tracker
Tracks how price moves after a signal appears - critical for filtering hedges/rolls
Uses Yahoo Finance for real-time data instead of database
"""
import pandas as pd
import asyncpg
from datetime import datetime, timedelta
from typing import Dict, Optional
from utils.logger import logger
from services.yahoo_finance_service import YahooFinanceService
class PriceReactionTracker:
"""Service for tracking price reactions after flow signals"""
def __init__(self):
self.reaction_threshold_pct = 0.5 # 0.5% threshold for "flow led to move"
self.use_yahoo_finance = False # Temporarily disabled - focus on Phase 1 button
self.yahoo_service = YahooFinanceService() if self.use_yahoo_finance else None
async def get_price_at_time(
self,
symbol: str,
timestamp: datetime,
pool: asyncpg.Pool = None # Not used anymore, kept for compatibility
) -> Optional[float]:
"""Get price at or before a specific timestamp using Yahoo Finance"""
if not self.use_yahoo_finance or not self.yahoo_service:
return None # Yahoo Finance disabled
try:
return self.yahoo_service.get_price_at_time(symbol, timestamp)
except Exception as e:
logger.debug(f"Error getting price at time from Yahoo Finance: {e}")
return None
async def enrich_with_reactions(
self,
flow_df: pd.DataFrame,
pool: asyncpg.Pool
) -> pd.DataFrame:
"""Enrich flow data with price reaction tracking"""
df = flow_df.copy()
if df.empty:
return df
logger.info(f"Tracking price reactions for {len(df)} signals...")
# Get unique symbols and timestamps for batch processing
unique_symbols = df['symbol_norm'].unique().tolist()
# Batch fetch price reactions
reaction_data = []
async with pool.acquire() as conn:
for idx, row in df.iterrows():
symbol = row['symbol_norm']
signal_time = row['flow_ts_utc']
price_at_signal = row.get('u_close')
if pd.isna(signal_time) or not price_at_signal or price_at_signal == 0:
reaction_data.append({
'price_reaction_5m_pct': None,
'price_reaction_15m_pct': None,
'price_reaction_30m_pct': None,
'high_break_5m': False,
'low_break_5m': False,
'flow_led_to_move': False
})
continue
# Get prices at 5m, 15m, 30m after signal
price_5m = None
price_15m = None
price_30m = None
try:
ts_5m = signal_time + timedelta(minutes=5)
price_5m = await self.get_price_at_time(symbol, ts_5m, pool)
except Exception as e:
logger.debug(f"Error fetching 5m price for {symbol}: {e}")
try:
ts_15m = signal_time + timedelta(minutes=15)
price_15m = await self.get_price_at_time(symbol, ts_15m, pool)
except Exception as e:
logger.debug(f"Error fetching 15m price for {symbol}: {e}")
try:
ts_30m = signal_time + timedelta(minutes=30)
price_30m = await self.get_price_at_time(symbol, ts_30m, pool)
except Exception as e:
logger.debug(f"Error fetching 30m price for {symbol}: {e}")
# Calculate reaction percentages
reaction_5m = None
reaction_15m = None
reaction_30m = None
if price_5m and price_at_signal:
try:
reaction_5m = round(((price_5m - price_at_signal) / price_at_signal) * 100, 2)
except (TypeError, ZeroDivisionError):
pass
if price_15m and price_at_signal:
try:
reaction_15m = round(((price_15m - price_at_signal) / price_at_signal) * 100, 2)
except (TypeError, ZeroDivisionError):
pass
if price_30m and price_at_signal:
try:
reaction_30m = round(((price_30m - price_at_signal) / price_at_signal) * 100, 2)
except (TypeError, ZeroDivisionError):
pass
# High/Low break confirmation
high_at_signal = row.get('u_high', 0) or 0
low_at_signal = row.get('u_low', 0) or 0
high_break_5m = False
low_break_5m = False
if price_5m:
if high_at_signal > 0 and price_5m > high_at_signal:
high_break_5m = True
if low_at_signal > 0 and price_5m < low_at_signal:
low_break_5m = True
# Determine if flow led to move
flow_led_to_move = False
if reaction_5m is not None:
flow_led_to_move = abs(reaction_5m) >= self.reaction_threshold_pct
reaction_data.append({
'price_reaction_5m_pct': reaction_5m,
'price_reaction_15m_pct': reaction_15m,
'price_reaction_30m_pct': reaction_30m,
'high_break_5m': high_break_5m,
'low_break_5m': low_break_5m,
'flow_led_to_move': flow_led_to_move
})
# Merge reaction data
reaction_df = pd.DataFrame(reaction_data, index=df.index)
df = pd.concat([df, reaction_df], axis=1)
logger.info(f"✅ Price reaction tracking complete. {df['flow_led_to_move'].sum()} signals led to price moves")
return df

View File

@ -1,258 +0,0 @@
"""
Relative Premium Scoring Service
Replaces static premium filter with context-aware relative premium scoring
"""
import pandas as pd
import numpy as np
from typing import Dict, Optional
from datetime import datetime, timedelta
import asyncpg
from utils.logger import logger
from utils.error_handler import safe_divide
class RelativePremiumScorer:
"""
Computes relative premium metrics to replace static premium filtering.
Premium of $80K might be significant for AAPL but noise for TSLA.
"""
def __init__(self, pool: asyncpg.Pool = None):
self.pool = pool
# Configuration
self.rolling_window_days = 20 # Rolling window for z-score calculation
self.min_relative_premium_threshold = 0.60 # 60th percentile minimum
async def fetch_historical_premium_stats(
self,
symbol: str,
reference_date: datetime.date
) -> Dict[str, float]:
"""
Fetch historical premium statistics for a symbol.
Returns: {mean, std, min, max, median} for rolling window
"""
if not self.pool:
logger.warning("No database pool available for historical premium stats")
return {}
try:
# Calculate window start date
window_start = reference_date - timedelta(days=self.rolling_window_days + 5) # Extra buffer
async with self.pool.acquire() as conn:
query = """
SELECT
AVG(premium_num) as mean_premium,
STDDEV(premium_num) as std_premium,
MIN(premium_num) as min_premium,
MAX(premium_num) as max_premium,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY premium_num) as median_premium
FROM (
SELECT
CAST(REGEXP_REPLACE("Premium"::text, '[^\d.]', '', 'g') AS numeric) as premium_num
FROM "OptionsFlow_monthly"
WHERE UPPER(TRIM("Symbol")) = $1
AND "Premium" IS NOT NULL
AND TRIM("Premium"::text) <> ''
AND "CreatedDate" >= $2
AND "CreatedDate" <= $3
AND "StockEtf" = 'STOCK'
) subq
WHERE premium_num > 0
"""
row = await conn.fetchrow(
query,
symbol.upper(),
window_start.strftime('%Y-%m-%d'),
reference_date.strftime('%Y-%m-%d')
)
if row and row['mean_premium']:
return {
'mean': float(row['mean_premium']) if row['mean_premium'] else 0.0,
'std': float(row['std_premium']) if row['std_premium'] else 0.0,
'min': float(row['min_premium']) if row['min_premium'] else 0.0,
'max': float(row['max_premium']) if row['max_premium'] else 0.0,
'median': float(row['median_premium']) if row['median_premium'] else 0.0,
}
except Exception as e:
logger.debug(f"Error fetching historical premium stats for {symbol}: {e}")
return {}
async def calculate_intraday_percentile(
self,
df: pd.DataFrame,
symbol: str,
flow_date: datetime.date,
premium: float
) -> Optional[float]:
"""
Calculate percentile rank of premium within same-day flow for the symbol.
Returns 0-100 percentile value.
"""
try:
# Filter to same symbol and date
same_day_flow = df[
(df['symbol_norm'] == symbol.upper()) &
(df['flow_date_cst'] == flow_date) &
(df['premium_num'].notna()) &
(df['premium_num'] > 0)
]
if len(same_day_flow) < 3: # Need at least 3 trades for meaningful percentile
return None
# Calculate percentile rank
all_premiums = same_day_flow['premium_num'].values
percentile = (np.sum(all_premiums <= premium) / len(all_premiums)) * 100.0
return float(percentile)
except Exception as e:
logger.debug(f"Error calculating intraday percentile for {symbol}: {e}")
return None
def calculate_zscore(
self,
premium: float,
stats: Dict[str, float]
) -> Optional[float]:
"""
Calculate z-score of premium relative to historical distribution.
Returns z-score (standard deviations from mean).
"""
if not stats or stats.get('std', 0) == 0:
return None
mean = stats.get('mean', 0)
std = stats.get('std', 1)
if std == 0:
return None
zscore = (premium - mean) / std
return float(zscore)
def calculate_relative_premium_score(
self,
premium: float,
zscore: Optional[float],
intraday_percentile: Optional[float],
stats: Dict[str, float]
) -> float:
"""
Calculate composite relative premium score (0-100).
Combines z-score and intraday percentile with median normalization.
Logic:
- High z-score (unusual size) = higher score
- High intraday percentile (large relative to today's flow) = higher score
- Normalize by median to account for symbol-specific scaling
"""
score = 0.0
# Z-score component (40% weight)
# Z-score > 1 = 1 std above mean, Z-score > 2 = 2 std above mean
if zscore is not None:
# Normalize z-score to 0-40 range (clamp at ±3 sigma)
zscore_normalized = max(0, min(40, (zscore + 3) * (40 / 6)))
score += zscore_normalized
# Intraday percentile component (40% weight)
if intraday_percentile is not None:
# Percentile is already 0-100, scale to 0-40
score += (intraday_percentile / 100.0) * 40.0
# Median normalization component (20% weight)
# Premium > median gets bonus, premium < median gets penalty
if stats and stats.get('median', 0) > 0:
median_ratio = premium / stats['median']
# Ratio > 1.5 = strong, ratio > 2.0 = very strong
if median_ratio >= 2.0:
score += 20.0
elif median_ratio >= 1.5:
score += 15.0
elif median_ratio >= 1.0:
score += 10.0
elif median_ratio >= 0.5:
score += 5.0
return min(100.0, max(0.0, score))
async def enrich_with_relative_premium(
self,
df: pd.DataFrame
) -> pd.DataFrame:
"""
Add relative premium metrics to DataFrame.
Adds: premium_zscore, premium_percentile_intraday, relative_premium_score
"""
if df.empty:
return df
df = df.copy()
# Initialize new columns
df['premium_zscore'] = None
df['premium_percentile_intraday'] = None
df['relative_premium_score'] = 0.0
# Cache historical stats per symbol to avoid repeated queries
stats_cache: Dict[str, Dict[str, float]] = {}
# Group by symbol and date for batch processing
unique_symbols = df['symbol_norm'].unique()
unique_dates = df['flow_date_cst'].unique()
logger.info(f"Calculating relative premium scores for {len(unique_symbols)} symbols")
for symbol in unique_symbols:
# Fetch historical stats once per symbol
if self.pool:
# Use the most recent date for this symbol as reference
symbol_dates = df[df['symbol_norm'] == symbol]['flow_date_cst'].unique()
if len(symbol_dates) > 0:
reference_date = max(symbol_dates)
stats = await self.fetch_historical_premium_stats(symbol, reference_date)
stats_cache[symbol] = stats
else:
stats_cache[symbol] = {}
else:
stats_cache[symbol] = {}
# Calculate metrics for each row
for idx, row in df.iterrows():
premium = row.get('premium_num')
if pd.isna(premium) or premium <= 0:
continue
symbol = row['symbol_norm']
flow_date = row.get('flow_date_cst')
# Get historical stats
stats = stats_cache.get(symbol, {})
# Calculate z-score
zscore = self.calculate_zscore(premium, stats)
if zscore is not None:
df.at[idx, 'premium_zscore'] = float(zscore)
# Calculate intraday percentile (using current DataFrame subset)
intraday_percentile = self.calculate_intraday_percentile(
df, symbol, flow_date, premium
)
if intraday_percentile is not None:
df.at[idx, 'premium_percentile_intraday'] = float(intraday_percentile)
# Calculate composite relative premium score
relative_score = self.calculate_relative_premium_score(
premium, zscore, intraday_percentile, stats
)
df.at[idx, 'relative_premium_score'] = float(relative_score)
logger.info(f"Relative premium scoring complete. Mean score: {df['relative_premium_score'].mean():.2f}")
return df

View File

@ -1,331 +0,0 @@
"""
Signal Component Scorer
Converts binary badge logic into continuous numeric signal components
"""
import pandas as pd
import numpy as np
from typing import Dict, Optional
from utils.logger import logger
class SignalComponentScorer:
"""
Replaces binary badge logic (💎 🟢 🔴) with continuous numeric scores.
Badges remain display-only, but signal_strength is computed from components.
"""
def __init__(self):
# Signal strength weights (must sum to 1.0)
self.aggression_weight = 0.30
self.size_concentration_weight = 0.30
self.repeat_velocity_weight = 0.20
self.strike_clustering_weight = 0.20
def calculate_aggression_score(self, row: pd.Series) -> float:
"""
Measures trade aggression: premium paid at ask vs bid, ITM vs OTM preference.
Higher score = more aggressive buying:
- ITM premiums indicate willingness to pay intrinsic value
- Ask-side trades (AA) indicate market orders
- Large premium relative to spot price
"""
score = 0.0
premium_num = row.get('premium_num', 0) or 0
spot_num = row.get('spot_num', 0) or 0
cp_norm = row.get('cp_norm', '')
side = str(row.get('Side', '')).upper()
# ITM premium component (0-40 points)
bull_prem_itm = row.get('bull_prem_itm', 0) or 0
bear_prem_itm = row.get('bear_prem_itm', 0) or 0
total_prem_itm = bull_prem_itm + bear_prem_itm
bull_total = row.get('bull_total', 0) or 0
bear_total = row.get('bear_total', 0) or 0
total_premium = bull_total + bear_total
if total_premium > 0:
itm_ratio = total_prem_itm / total_premium
score += itm_ratio * 40.0
# Ask-side aggression component (0-30 points)
# AA = buying at ask (market orders, urgency)
has_aa = 'AA' in side or 'AT_ASK' in side
has_bb = 'BB' in side or 'AT_BID' in side
if has_aa and not has_bb:
score += 30.0
elif has_aa:
score += 15.0
# Premium-to-spot ratio component (0-30 points)
# Large premium relative to spot indicates aggressive positioning
if spot_num > 0 and premium_num > 0:
prem_to_spot_ratio = premium_num / spot_num
# Normalize: 0.01 (1%) = 10 points, 0.05 (5%) = 30 points
ratio_score = min(30.0, prem_to_spot_ratio * 600.0)
score += ratio_score
return min(100.0, max(0.0, score))
def calculate_size_concentration_score(self, row: pd.Series) -> float:
"""
Measures size concentration: how much premium is concentrated in single strikes/expiries.
Higher score = more concentrated (single large trade vs many small ones):
- High premium in single trade
- OI buildup in specific strikes
- Directional consistency (all calls or all puts)
"""
score = 0.0
premium_num = row.get('premium_num', 0) or 0
bull_total = row.get('bull_total', 0) or 0
bear_total = row.get('bear_total', 0) or 0
total_premium = bull_total + bear_total
# Single-trade premium concentration (0-40 points)
# Premium of current trade relative to running total
if total_premium > 0:
concentration_ratio = premium_num / total_premium
# Single trade = 50%+ of total = very concentrated
if concentration_ratio >= 0.5:
score += 40.0
elif concentration_ratio >= 0.3:
score += 30.0
elif concentration_ratio >= 0.2:
score += 20.0
elif concentration_ratio >= 0.1:
score += 10.0
# OI concentration component (0-30 points)
# High OI in OTM strikes indicates concentrated positioning
oi_cb_otm = row.get('oi_cb_otm', 0) or 0
oi_pb_otm = row.get('oi_pb_otm', 0) or 0
oi_all = row.get('oi_all', 0) or 0
if oi_all > 0:
otm_oi_ratio = (oi_cb_otm + oi_pb_otm) / oi_all
score += otm_oi_ratio * 30.0
# Directional consistency (0-30 points)
# Pure directional flow (all bull or all bear) = more concentrated
if total_premium > 0:
direction_strength = abs(bull_total - bear_total) / total_premium
score += direction_strength * 30.0
return min(100.0, max(0.0, score))
def calculate_repeat_trade_velocity(self, df: pd.DataFrame, row_idx: int) -> float:
"""
Measures repeat trade velocity: frequency of trades in same direction/expiry.
Higher score = faster follow-on trades (urgency building):
- Time between consecutive trades decreasing
- Multiple trades in same direction
- Same expiry date (rolling accumulation)
"""
if row_idx >= len(df):
return 0.0
current_row = df.iloc[row_idx]
symbol = current_row.get('symbol_norm')
direction = current_row.get('direction')
exp_date = current_row.get('exp_date')
flow_ts_utc = current_row.get('flow_ts_utc')
if pd.isna(symbol) or pd.isna(direction) or pd.isna(flow_ts_utc):
return 0.0
# Look back at recent trades for same symbol
# Sort by timestamp to find preceding trades
df_sorted = df.sort_values('flow_ts_utc').reset_index(drop=True)
current_idx = df_sorted.index[df_sorted.index == row_idx]
if len(current_idx) == 0:
return 0.0
current_pos = current_idx[0]
# Look at last 10 trades for this symbol
symbol_mask = (df_sorted['symbol_norm'] == symbol) & \
(df_sorted.index < current_pos) & \
(df_sorted['flow_ts_utc'] <= flow_ts_utc)
recent_trades = df_sorted[symbol_mask].tail(10)
if len(recent_trades) == 0:
return 0.0
score = 0.0
# Time compression component (0-40 points)
# Decreasing time gaps = urgency building
if len(recent_trades) >= 2:
time_gaps = []
prev_ts = None
for _, trade in recent_trades.iterrows():
ts = trade.get('flow_ts_utc')
if pd.notna(ts) and prev_ts:
gap_minutes = (ts - prev_ts).total_seconds() / 60.0
if gap_minutes > 0:
time_gaps.append(gap_minutes)
prev_ts = ts
if len(time_gaps) >= 2:
# Check if gaps are decreasing (accelerating)
recent_gaps = time_gaps[-2:]
if recent_gaps[1] < recent_gaps[0]:
compression_ratio = recent_gaps[1] / recent_gaps[0] if recent_gaps[0] > 0 else 0
# 50% compression = 20 points, 80% compression = 40 points
score += (1.0 - compression_ratio) * 40.0
# Directional consistency (0-35 points)
# Same direction trades = building position
same_direction = recent_trades[recent_trades['direction'] == direction]
if len(recent_trades) > 0:
consistency_ratio = len(same_direction) / len(recent_trades)
score += consistency_ratio * 35.0
# Expiry concentration (0-25 points)
# Same expiry = rolling accumulation
if pd.notna(exp_date):
same_expiry = recent_trades[recent_trades['exp_date'] == exp_date]
if len(recent_trades) > 0:
expiry_ratio = len(same_expiry) / len(recent_trades)
score += expiry_ratio * 25.0
return min(100.0, max(0.0, score))
def calculate_strike_clustering_score(self, df: pd.DataFrame, row_idx: int) -> float:
"""
Measures strike clustering: trades accumulating at specific strike levels.
Higher score = more clustering (laddering or concentrated strikes):
- Multiple trades at same strike
- Sequential strikes (laddering)
- Strikes near current price (pin risk)
"""
if row_idx >= len(df):
return 0.0
current_row = df.iloc[row_idx]
symbol = current_row.get('symbol_norm')
strike_num = current_row.get('strike_num')
spot_num = current_row.get('spot_num', 0) or 0
flow_ts_utc = current_row.get('flow_ts_utc')
exp_date = current_row.get('exp_date')
if pd.isna(symbol) or pd.isna(strike_num) or pd.isna(flow_ts_utc):
return 0.0
# Look at recent trades for same symbol and expiry
df_sorted = df.sort_values('flow_ts_utc').reset_index(drop=True)
symbol_mask = (df_sorted['symbol_norm'] == symbol) & \
(df_sorted['flow_ts_utc'] <= flow_ts_utc)
if pd.notna(exp_date):
symbol_mask = symbol_mask & (df_sorted['exp_date'] == exp_date)
recent_trades = df_sorted[symbol_mask].tail(20)
if len(recent_trades) <= 1:
return 0.0
score = 0.0
# Exact strike clustering (0-40 points)
# Multiple trades at same strike
same_strike_count = len(recent_trades[recent_trades['strike_num'] == strike_num])
if len(recent_trades) > 0:
clustering_ratio = same_strike_count / len(recent_trades)
score += clustering_ratio * 40.0
# Strike laddering detection (0-35 points)
# Sequential strikes (e.g., 100, 105, 110) indicate laddering
unique_strikes = sorted(recent_trades['strike_num'].dropna().unique())
if len(unique_strikes) >= 3:
# Check if strikes are roughly evenly spaced (laddering)
if spot_num > 0:
strike_diffs = [abs(unique_strikes[i+1] - unique_strikes[i]) for i in range(len(unique_strikes)-1)]
if len(strike_diffs) > 0:
avg_diff = np.mean(strike_diffs)
std_diff = np.std(strike_diffs)
# Low std relative to mean = regular spacing (laddering)
if avg_diff > 0:
regularity = 1.0 - min(1.0, std_diff / avg_diff)
score += regularity * 35.0
# Proximity to spot (0-25 points)
# Strikes near spot price = pin risk, more significant
if spot_num > 0:
strike_pct_diff = abs(strike_num - spot_num) / spot_num * 100.0
# Within 2% = 25 points, within 5% = 15 points, within 10% = 5 points
if strike_pct_diff <= 2.0:
score += 25.0
elif strike_pct_diff <= 5.0:
score += 15.0
elif strike_pct_diff <= 10.0:
score += 5.0
return min(100.0, max(0.0, score))
def calculate_signal_strength(self, row: pd.Series) -> float:
"""
Calculate composite signal_strength from component scores.
signal_strength =
0.30 * aggression_score +
0.30 * size_concentration_score +
0.20 * repeat_trade_velocity +
0.20 * strike_clustering_score
"""
aggression = row.get('aggression_score', 0) or 0
size_conc = row.get('size_concentration_score', 0) or 0
repeat_vel = row.get('repeat_trade_velocity', 0) or 0
strike_clust = row.get('strike_clustering_score', 0) or 0
signal_strength = (
self.aggression_weight * aggression +
self.size_concentration_weight * size_conc +
self.repeat_velocity_weight * repeat_vel +
self.strike_clustering_weight * strike_clust
)
return min(100.0, max(0.0, signal_strength))
def enrich_with_signal_components(self, df: pd.DataFrame) -> pd.DataFrame:
"""
Add signal component scores to DataFrame.
Adds: aggression_score, size_concentration_score, repeat_trade_velocity,
strike_clustering_score, signal_strength
"""
if df.empty:
return df
df = df.copy()
# Initialize columns
df['aggression_score'] = 0.0
df['size_concentration_score'] = 0.0
df['repeat_trade_velocity'] = 0.0
df['strike_clustering_score'] = 0.0
df['signal_strength'] = 0.0
# Calculate component scores (some require DataFrame context)
df['aggression_score'] = df.apply(self.calculate_aggression_score, axis=1)
df['size_concentration_score'] = df.apply(self.calculate_size_concentration_score, axis=1)
# These require DataFrame context, so iterate
for idx in df.index:
df.at[idx, 'repeat_trade_velocity'] = self.calculate_repeat_trade_velocity(df, idx)
df.at[idx, 'strike_clustering_score'] = self.calculate_strike_clustering_score(df, idx)
# Calculate composite signal_strength
df['signal_strength'] = df.apply(self.calculate_signal_strength, axis=1)
logger.info(f"Signal component scoring complete. Mean signal_strength: {df['signal_strength'].mean():.2f}")
return df

Some files were not shown because too many files have changed in this diff Show More