Initial commit: FinanceIQ v5.2 — full-stack financial analytics platform
This commit is contained in:
commit
bac9eff6f6
21
.env.example
Normal file
21
.env.example
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# FinanceIQ v5 — Environment Variables
|
||||||
|
# Copy this to .env and fill in your API keys
|
||||||
|
|
||||||
|
# Financial Modeling Prep (required for historical data & ratios)
|
||||||
|
FMP_API_KEY=your_fmp_api_key_here
|
||||||
|
|
||||||
|
# Finnhub (required for analyst ratings, insider trading)
|
||||||
|
FINNHUB_API_KEY=your_finnhub_api_key_here
|
||||||
|
|
||||||
|
# FRED (required for macroeconomic indicators)
|
||||||
|
FRED_API_KEY=your_fred_api_key_here
|
||||||
|
|
||||||
|
# Alpha Vantage (required for sector heatmap, currency conversion)
|
||||||
|
ALPHA_VANTAGE_KEY=your_alpha_vantage_key_here
|
||||||
|
|
||||||
|
# Ollama (optional — local AI model, defaults to http://localhost:11434)
|
||||||
|
OLLAMA_URL=http://localhost:11434
|
||||||
|
|
||||||
|
# Zerodha Kite Connect (optional — for portfolio management)
|
||||||
|
# KITE_API_KEY=your_kite_api_key_here
|
||||||
|
# KITE_API_SECRET=your_kite_api_secret_here
|
||||||
44
.gitignore
vendored
Normal file
44
.gitignore
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.pyo
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual Environment
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.gemini/
|
||||||
|
|
||||||
|
# Environment Variables & Secrets
|
||||||
|
.env
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Database (local state)
|
||||||
|
portfolio.db
|
||||||
|
portfolio.db-wal
|
||||||
|
portfolio.db-shm
|
||||||
|
|
||||||
|
# Data exports
|
||||||
|
*.xlsx
|
||||||
|
*.csv
|
||||||
|
!sample_data/
|
||||||
|
|
||||||
|
# Assignment PDFs
|
||||||
|
*.pdf
|
||||||
74
01_basic_stock_data.py
Normal file
74
01_basic_stock_data.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
"""
|
||||||
|
=== FinanceToolkit - Lesson 1: Getting Basic Stock Data ===
|
||||||
|
|
||||||
|
This script shows you how to:
|
||||||
|
1. Initialize the Toolkit with one or more stock tickers
|
||||||
|
2. Fetch historical price data (OHLC, volume, returns)
|
||||||
|
3. Fetch financial statements (income, balance sheet, cash flow)
|
||||||
|
|
||||||
|
Run this file: python 01_basic_stock_data.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from financetoolkit import Toolkit
|
||||||
|
|
||||||
|
# ── 1. Setup ──────────────────────────────────────────────────────────
|
||||||
|
API_KEY = "wybWEsp1oB9abHfz3yPpQYwffxaN21B7"
|
||||||
|
|
||||||
|
# You can pass one ticker or a list of tickers.
|
||||||
|
# start_date limits how far back data goes.
|
||||||
|
companies = Toolkit(
|
||||||
|
tickers=["AAPL", "MSFT"],
|
||||||
|
api_key=API_KEY,
|
||||||
|
start_date="2020-01-01", # data from Jan 2020 onwards
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── 2. Historical Price Data ─────────────────────────────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print("HISTORICAL PRICE DATA (first 10 rows)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
historical = companies.get_historical_data()
|
||||||
|
print(historical.head(10))
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Filter to just one stock using .xs()
|
||||||
|
print("--- Apple only ---")
|
||||||
|
apple_hist = historical.xs("AAPL", level=1, axis=1)
|
||||||
|
print(apple_hist.head(10))
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── 3. Income Statement ─────────────────────────────────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print("INCOME STATEMENT (Annual)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
income = companies.get_income_statement()
|
||||||
|
print(income.head(10))
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Filter to just Apple
|
||||||
|
print("--- Apple Income Statement ---")
|
||||||
|
apple_income = income.loc["AAPL"]
|
||||||
|
print(apple_income.head(10))
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── 4. Balance Sheet ─────────────────────────────────────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print("BALANCE SHEET STATEMENT (Annual)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
balance = companies.get_balance_sheet_statement()
|
||||||
|
print(balance.head(10))
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── 5. Cash Flow Statement ───────────────────────────────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print("CASH FLOW STATEMENT (Annual)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
cashflow = companies.get_cash_flow_statement()
|
||||||
|
print(cashflow.head(10))
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("[DONE] You've just pulled real stock data for AAPL & MSFT.")
|
||||||
|
print(" Try changing the tickers list or start_date above and re-run!")
|
||||||
77
02_ratios_and_models.py
Normal file
77
02_ratios_and_models.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
"""
|
||||||
|
=== FinanceToolkit - Lesson 2: Financial Ratios & Models ===
|
||||||
|
|
||||||
|
This script shows you how to:
|
||||||
|
1. Calculate profitability, liquidity, and valuation ratios
|
||||||
|
2. Run financial models (DuPont Analysis, WACC, etc.)
|
||||||
|
|
||||||
|
Run this file: python 02_ratios_and_models.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from financetoolkit import Toolkit
|
||||||
|
|
||||||
|
API_KEY = "wybWEsp1oB9abHfz3yPpQYwffxaN21B7"
|
||||||
|
|
||||||
|
companies = Toolkit(
|
||||||
|
tickers=["AAPL", "MSFT", "GOOGL"],
|
||||||
|
api_key=API_KEY,
|
||||||
|
start_date="2020-01-01",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── 1. Profitability Ratios ──────────────────────────────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print("PROFITABILITY RATIOS")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
profitability = companies.ratios.collect_profitability_ratios()
|
||||||
|
print(profitability)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── 2. Liquidity Ratios ──────────────────────────────────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print("LIQUIDITY RATIOS")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
liquidity = companies.ratios.collect_liquidity_ratios()
|
||||||
|
print(liquidity)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── 3. Valuation Ratios ──────────────────────────────────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print("VALUATION RATIOS (e.g. P/E, P/B, EV/EBITDA)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
valuation = companies.ratios.collect_valuation_ratios()
|
||||||
|
print(valuation)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── 4. Individual Ratio ──────────────────────────────────────────────
|
||||||
|
# Use get_ functions for a single ratio
|
||||||
|
print("=" * 60)
|
||||||
|
print("RETURN ON EQUITY (single ratio)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
roe = companies.ratios.get_return_on_equity()
|
||||||
|
print(roe)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── 5. DuPont Analysis ───────────────────────────────────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print("EXTENDED DUPONT ANALYSIS")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
dupont = companies.models.get_extended_dupont_analysis()
|
||||||
|
print(dupont)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── 6. Weighted Average Cost of Capital (WACC) ───────────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print("WEIGHTED AVERAGE COST OF CAPITAL (WACC)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
wacc = companies.models.get_weighted_average_cost_of_capital()
|
||||||
|
print(wacc)
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("[DONE] You now know how to compute 50+ financial ratios")
|
||||||
|
print(" and run valuation models on any public company.")
|
||||||
70
03_technicals_risk_performance.py
Normal file
70
03_technicals_risk_performance.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
"""
|
||||||
|
=== FinanceToolkit - Lesson 3: Technical Indicators, Risk & Performance ===
|
||||||
|
|
||||||
|
This script shows you how to:
|
||||||
|
1. Get technical indicators (Bollinger Bands, MACD, RSI, Ichimoku)
|
||||||
|
2. Measure risk (Value at Risk)
|
||||||
|
3. Evaluate performance (Fama-French factor correlations)
|
||||||
|
|
||||||
|
Run this file: python 03_technicals_risk_performance.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from financetoolkit import Toolkit
|
||||||
|
|
||||||
|
API_KEY = "wybWEsp1oB9abHfz3yPpQYwffxaN21B7"
|
||||||
|
|
||||||
|
companies = Toolkit(
|
||||||
|
tickers=["AAPL", "MSFT"],
|
||||||
|
api_key=API_KEY,
|
||||||
|
start_date="2020-01-01",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── 1. Bollinger Bands ───────────────────────────────────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print("BOLLINGER BANDS")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
bollinger = companies.technicals.get_bollinger_bands()
|
||||||
|
print(bollinger.head(15))
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── 2. MACD ───────────────────────────────────────────────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print("MACD (Moving Average Convergence Divergence)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
macd = companies.technicals.get_moving_average_convergence_divergence()
|
||||||
|
print(macd.head(15))
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── 3. RSI ────────────────────────────────────────────────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print("RSI (Relative Strength Index)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
rsi = companies.technicals.get_relative_strength_index()
|
||||||
|
print(rsi.head(15))
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── 4. Value at Risk ─────────────────────────────────────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print("VALUE AT RISK (Weekly)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
var = companies.risk.get_value_at_risk(period="weekly")
|
||||||
|
print(var.head(15))
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── 5. Factor Correlations ───────────────────────────────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print("FAMA-FRENCH FACTOR CORRELATIONS (Quarterly)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
correlations = companies.performance.get_factor_asset_correlations(
|
||||||
|
period="quarterly"
|
||||||
|
)
|
||||||
|
print(correlations)
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("[DONE] You can now analyze stocks with technical indicators,")
|
||||||
|
print(" assess risk with VaR, and see performance factor exposures.")
|
||||||
69
04_economics_and_export.py
Normal file
69
04_economics_and_export.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
"""
|
||||||
|
=== FinanceToolkit - Lesson 4: Economics Data & Exporting Results ===
|
||||||
|
|
||||||
|
This script shows you how to:
|
||||||
|
1. Access macroeconomic data (GDP, CPI, unemployment)
|
||||||
|
2. Export any DataFrame to CSV or Excel for further use
|
||||||
|
|
||||||
|
Run this file: python 04_economics_and_export.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from financetoolkit import Toolkit
|
||||||
|
|
||||||
|
API_KEY = "wybWEsp1oB9abHfz3yPpQYwffxaN21B7"
|
||||||
|
|
||||||
|
companies = Toolkit(
|
||||||
|
tickers=["AAPL"],
|
||||||
|
api_key=API_KEY,
|
||||||
|
start_date="2020-01-01",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── 1. Unemployment Rates ─────────────────────────────────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print("UNEMPLOYMENT RATES")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
unemployment = companies.economics.get_unemployment_rate()
|
||||||
|
print(unemployment.head(10))
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── 2. Consumer Price Index (CPI) ────────────────────────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print("CONSUMER PRICE INDEX (CPI)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
cpi = companies.economics.get_consumer_price_index()
|
||||||
|
print(cpi.head(10))
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── 3. GDP ────────────────────────────────────────────────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print("GROSS DOMESTIC PRODUCT (GDP)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
gdp = companies.economics.get_gross_domestic_product()
|
||||||
|
print(gdp.head(10))
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── 4. Exporting Data ────────────────────────────────────────────────
|
||||||
|
# Any DataFrame can be saved to CSV or Excel for use in reports,
|
||||||
|
# other tools, or further analysis.
|
||||||
|
|
||||||
|
# Export historical data to CSV
|
||||||
|
historical = companies.get_historical_data()
|
||||||
|
historical.to_csv("aapl_historical_data.csv")
|
||||||
|
print("[SAVED] aapl_historical_data.csv")
|
||||||
|
|
||||||
|
# Export income statement to Excel
|
||||||
|
income = companies.get_income_statement()
|
||||||
|
income.to_excel("aapl_income_statement.xlsx")
|
||||||
|
print("[SAVED] aapl_income_statement.xlsx")
|
||||||
|
|
||||||
|
# Export ratios to CSV
|
||||||
|
ratios = companies.ratios.collect_profitability_ratios()
|
||||||
|
ratios.to_csv("aapl_profitability_ratios.csv")
|
||||||
|
print("[SAVED] aapl_profitability_ratios.csv")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("[DONE] Your data has been exported. Open the CSV/Excel files")
|
||||||
|
print(" in Excel, Google Sheets, or any data analysis tool.")
|
||||||
150
README.md
Normal file
150
README.md
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<p align="center">
|
||||||
|
<h1 align="center">📊 FinanceIQ</h1>
|
||||||
|
<p align="center">
|
||||||
|
<strong>Real-time Financial Analytics & Portfolio Simulation Platform</strong>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://img.shields.io/badge/python-3.10+-blue?style=flat-square&logo=python" alt="Python">
|
||||||
|
<img src="https://img.shields.io/badge/flask-3.x-green?style=flat-square&logo=flask" alt="Flask">
|
||||||
|
<img src="https://img.shields.io/badge/version-5.2-purple?style=flat-square" alt="Version">
|
||||||
|
<img src="https://img.shields.io/badge/license-MIT-yellow?style=flat-square" alt="License">
|
||||||
|
</p>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
A full-stack financial analytics web application built with **Flask**, **yfinance**, and **Google Gemini AI**. Features interactive charting, professional-grade portfolio simulation, options pricing, backtesting, AI-driven advisory, and a live market dashboard — all in a sleek, responsive single-page dashboard.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
### 📈 Market Analysis
|
||||||
|
- **Interactive Candlestick Charts** — powered by TradingView Lightweight Charts
|
||||||
|
- **Technical Indicators** — RSI, MACD, Bollinger Bands, SMA/EMA, ATR, Stochastic
|
||||||
|
- **Candlestick Pattern Recognition** — Doji, Hammer, Engulfing, Morning Star, and more
|
||||||
|
- **Key Financial Metrics** — P/E, ROE, D/E, Quick Ratio, Dividend Yield
|
||||||
|
- **Sector Peer Comparison** — heatmap of competitors across 7 sectors
|
||||||
|
- **Valuation Models** — DCF, Graham Number, PEG Ratio, DDM
|
||||||
|
|
||||||
|
### 💼 Portfolio Simulation
|
||||||
|
- **$100K Paper Trading** — buy/sell with real-time prices from Yahoo Finance
|
||||||
|
- **Advanced Order Types** — Market, Limit, and Stop orders
|
||||||
|
- **Short Selling** — open and cover short positions with margin tracking
|
||||||
|
- **Slippage & Commission Modeling** — realistic IBKR-style execution costs
|
||||||
|
- **Equity Curve** — track portfolio value over time with interactive charts
|
||||||
|
- **Performance Analytics** — Sharpe, Sortino, Calmar ratios, profit factor, max drawdown
|
||||||
|
|
||||||
|
### 🎯 Options Pricing
|
||||||
|
- **Black-Scholes Model** — call/put pricing with real-time IV
|
||||||
|
- **Greeks Dashboard** — Delta, Gamma, Theta, Vega, Rho
|
||||||
|
- **Monte Carlo Simulation** — 10,000-path price projections
|
||||||
|
- **Interactive Payoff Charts** — visualize P&L at expiration
|
||||||
|
|
||||||
|
### 🤖 AI-Powered Advisory
|
||||||
|
- **Gemini AI Integration** — personalized investment analysis
|
||||||
|
- **Macro Dashboard** — Fed rates, Treasury yields, VIX, DXY, oil, gold
|
||||||
|
- **Risk Assessment** — automated risk scoring with traffic-light indicators
|
||||||
|
- **Natural Language Insights** — ask questions about any stock
|
||||||
|
|
||||||
|
### 📰 News & Sentiment
|
||||||
|
- **Real-time News Feed** — latest headlines for any ticker
|
||||||
|
- **AI Sentiment Analysis** — bullish/bearish/neutral classification
|
||||||
|
- **Currency Converter** — 150+ currency pairs with live rates
|
||||||
|
|
||||||
|
### 🔬 Backtesting Engine
|
||||||
|
- **Strategy Backtester** — SMA crossover, RSI, MACD, Bollinger Band strategies
|
||||||
|
- **Bi-directional Trading** — long and short strategy support
|
||||||
|
- **Performance Metrics** — Sharpe ratio, win rate, max drawdown, trade log
|
||||||
|
|
||||||
|
### 🌍 Market Dashboard
|
||||||
|
- **Global Indices** — S&P 500, NASDAQ, Dow, FTSE, Nikkei, DAX
|
||||||
|
- **Commodities** — Gold, Silver, Crude Oil, Natural Gas
|
||||||
|
- **Currency Pairs** — EUR/USD, GBP/USD, USD/JPY, and more
|
||||||
|
- **Live Pulse** — auto-refreshing with animated counters
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Python 3.10+
|
||||||
|
- Google Gemini API key ([Get one here](https://makersuite.google.com/app/apikey))
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repo
|
||||||
|
git clone https://github.com/YOUR_USERNAME/FinanceIQ.git
|
||||||
|
cd FinanceIQ
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Set up environment variables
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env and add your GEMINI_API_KEY
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open `http://localhost:5000` in your browser.
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
FinanceIQ/
|
||||||
|
├── app.py # Flask server & API routes
|
||||||
|
├── portfolio_engine.py # Portfolio simulation engine (SQLite-backed)
|
||||||
|
├── options_engine.py # Black-Scholes, Greeks, Monte Carlo
|
||||||
|
├── backtester.py # Strategy backtesting engine
|
||||||
|
├── advisor.py # Gemini AI financial advisory
|
||||||
|
├── technical_analyzer.py # Technical indicators & patterns
|
||||||
|
├── news_analyzer.py # News fetching & sentiment analysis
|
||||||
|
├── requirements.txt # Python dependencies
|
||||||
|
├── .env.example # Environment variable template
|
||||||
|
├── templates/
|
||||||
|
│ └── index.html # Single-page dashboard UI
|
||||||
|
└── static/
|
||||||
|
├── script.js # Frontend logic & chart rendering
|
||||||
|
└── style.css # Glassmorphism UI design system
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Tech Stack
|
||||||
|
|
||||||
|
| Layer | Technology |
|
||||||
|
|-------|-----------|
|
||||||
|
| **Backend** | Flask, Python 3.10+ |
|
||||||
|
| **Data** | yfinance, Yahoo Finance API |
|
||||||
|
| **AI** | Google Gemini 2.0 Flash |
|
||||||
|
| **Database** | SQLite (WAL mode) |
|
||||||
|
| **Charts** | TradingView Lightweight Charts |
|
||||||
|
| **Frontend** | Vanilla JS, CSS (Glassmorphism) |
|
||||||
|
|
||||||
|
## 📊 API Endpoints
|
||||||
|
|
||||||
|
| Endpoint | Method | Description |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| `/api/analyze` | POST | Full stock analysis |
|
||||||
|
| `/api/suggest` | GET | Ticker autocomplete |
|
||||||
|
| `/api/portfolio` | GET | Portfolio summary |
|
||||||
|
| `/api/portfolio/buy` | POST | Buy long |
|
||||||
|
| `/api/portfolio/sell` | POST | Sell long |
|
||||||
|
| `/api/portfolio/short` | POST | Short sell |
|
||||||
|
| `/api/portfolio/cover` | POST | Cover short |
|
||||||
|
| `/api/portfolio/limit-order` | POST | Place limit order |
|
||||||
|
| `/api/portfolio/stop-order` | POST | Place stop order |
|
||||||
|
| `/api/portfolio/analytics` | GET | Performance metrics |
|
||||||
|
| `/api/portfolio/equity-curve` | GET | Equity curve data |
|
||||||
|
| `/api/market-summary` | GET | Market dashboard data |
|
||||||
|
| `/api/options/price` | POST | Options pricing |
|
||||||
|
| `/api/backtest` | POST | Run backtests |
|
||||||
|
| `/api/advisor/analyze` | POST | AI advisory |
|
||||||
|
| `/api/news` | GET | News & sentiment |
|
||||||
|
|
||||||
|
## 📜 License
|
||||||
|
|
||||||
|
This project is for educational purposes. Built as part of a university finance course.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
Built with ❤️ using Flask & yfinance
|
||||||
|
</p>
|
||||||
106
advisor.py
Normal file
106
advisor.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
"""
|
||||||
|
Financial Advisory Algorithm - Phase 7 (Bi-Directional / Market Neutral)
|
||||||
|
Analyzes Technicals + News to suggest Long, Short, or Hedge positions.
|
||||||
|
"""
|
||||||
|
import technical_analyzer
|
||||||
|
import news_analyzer
|
||||||
|
import sys
|
||||||
|
|
||||||
|
API_KEY = "wybWEsp1oB9abHfz3yPpQYwffxaN21B7"
|
||||||
|
|
||||||
|
def get_user_input():
|
||||||
|
print("\n=== Financial Advisor (Bi-Directional Hedge Edition) ===")
|
||||||
|
ticker = input("Enter Stock Ticker (e.g., ^NSEBANK, NVDA): ").strip().upper()
|
||||||
|
if not ticker: ticker = "^NSEBANK"
|
||||||
|
try:
|
||||||
|
budget = float(input("Enter Monthly Budget (in USD/GBP): ").strip())
|
||||||
|
except ValueError: budget = 150.0
|
||||||
|
return ticker, budget
|
||||||
|
|
||||||
|
def determine_strategy(tech, sent):
|
||||||
|
"""
|
||||||
|
Combines news sentiment and technicals to pick direction.
|
||||||
|
"""
|
||||||
|
price = tech['price']
|
||||||
|
ema5 = tech.get('ema_5', price)
|
||||||
|
bb_upper = tech.get('bb_upper', 0)
|
||||||
|
bb_lower = tech.get('bb_lower', 0)
|
||||||
|
bb_width = (bb_upper - bb_lower) / price if price > 0 else 0
|
||||||
|
|
||||||
|
sentiment_score = sent['average_score']
|
||||||
|
|
||||||
|
analysis = []
|
||||||
|
|
||||||
|
# DIRECTIONAL BIAS
|
||||||
|
technical_bias = "BULLISH" if price > ema5 else "BEARISH"
|
||||||
|
sentiment_bias = "BULLISH" if sentiment_score > 0.1 else "BEARISH" if sentiment_score < -0.1 else "NEUTRAL"
|
||||||
|
|
||||||
|
analysis.append(f"Technical Bias (EMA 5): {technical_bias}")
|
||||||
|
analysis.append(f"Sentiment Bias (News): {sentiment_bias} ({sentiment_score})")
|
||||||
|
|
||||||
|
# DECISION LOGIC
|
||||||
|
if bb_width > 0.08:
|
||||||
|
# High volatility = Hedge
|
||||||
|
signal = "HEDGE (50% Long / 50% Short)"
|
||||||
|
reason = "Market volatility is extremely high (BB Width > 8%). Hedging will neutralize risk from weekend news gaps."
|
||||||
|
elif technical_bias == "BULLISH" and sentiment_bias != "BEARISH":
|
||||||
|
signal = "AGGRESSIVE LONG"
|
||||||
|
reason = "Price is trending above EMA 5 and news sentiment confirms strength."
|
||||||
|
elif technical_bias == "BEARISH" and sentiment_bias != "BULLISH":
|
||||||
|
signal = "AGGRESSIVE SHORT"
|
||||||
|
reason = "Price is breaking down and news sentiment is negative. Profit from the drop."
|
||||||
|
else:
|
||||||
|
# Conflict between news and tech
|
||||||
|
signal = "TACTICAL HEDGE / WAIT"
|
||||||
|
reason = "Conflicting signals (Tech vs Sentiment). Protecting capital is a priority."
|
||||||
|
|
||||||
|
return {
|
||||||
|
'signal': signal,
|
||||||
|
'reason': reason,
|
||||||
|
'details': analysis,
|
||||||
|
'sentiment_data': sent
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_trade_plan(ticker, budget, tech, strategy):
|
||||||
|
print(f"\n--- {ticker} BI-DIRECTIONAL PLAN ---")
|
||||||
|
for detail in strategy['details']:
|
||||||
|
print(f" * {detail}")
|
||||||
|
|
||||||
|
print(f"\n RECOMMENDED ACTION: {strategy['signal']}")
|
||||||
|
print(f" RATIONALE: {strategy['reason']}")
|
||||||
|
|
||||||
|
price = tech['price']
|
||||||
|
if "LONG" in strategy['signal']:
|
||||||
|
print(f"\n [BULLISH EXECUTION]")
|
||||||
|
print(f" - Entry: {price:.2f}")
|
||||||
|
print(f" - Target: {price * 1.05:.2f} (+5%)")
|
||||||
|
print(f" - Stop: {price * 0.97:.2f} (-3%)")
|
||||||
|
elif "SHORT" in strategy['signal']:
|
||||||
|
print(f"\n [BEARISH EXECUTION]")
|
||||||
|
print(f" - Entry: {price:.2f} (Sell)")
|
||||||
|
print(f" - Target: {price * 0.95:.2f} (Profit from drop)")
|
||||||
|
print(f" - Stop: {price * 1.03:.2f} (Exit if price rises)")
|
||||||
|
elif "HEDGE" in strategy['signal']:
|
||||||
|
print(f"\n [NEUTRAL EXECUTION]")
|
||||||
|
print(f" - Long Px: {price:.2f} (50% Allocation)")
|
||||||
|
print(f" - Short Px: {price:.2f} (50% Allocation)")
|
||||||
|
print(f" - Effect: Portfolio value stays stable regardless of Monday open.")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ticker, budget = get_user_input()
|
||||||
|
|
||||||
|
print(f"\n... Analyzing {ticker} (Technicals + News) ...")
|
||||||
|
tech_data = technical_analyzer.get_technicals(ticker, API_KEY)
|
||||||
|
|
||||||
|
# Fetch real news
|
||||||
|
news_items = news_analyzer.fetch_news(ticker, limit=5)
|
||||||
|
sentiment = news_analyzer.analyze_sentiment(news_items)
|
||||||
|
|
||||||
|
if tech_data:
|
||||||
|
strategy = determine_strategy(tech_data, sentiment)
|
||||||
|
print_trade_plan(ticker, budget, tech_data, strategy)
|
||||||
|
else:
|
||||||
|
print("Failed to fetch data.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
177
backtester.py
Normal file
177
backtester.py
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
"""
|
||||||
|
Backtesting Engine - Phase 7 (Bi-Directional Market Neutral)
|
||||||
|
Profits from both rising and falling markets.
|
||||||
|
Logic:
|
||||||
|
1. Long: Price > EMA 5
|
||||||
|
2. Short: Price < EMA 5
|
||||||
|
3. Hedge: If BB Width > 5% (High Volatility), hold both to stay neutral.
|
||||||
|
"""
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
from financetoolkit import Toolkit
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
API_KEY = "wybWEsp1oB9abHfz3yPpQYwffxaN21B7"
|
||||||
|
TICKERS = ["^NSEBANK", "NVDA", "TSLA"]
|
||||||
|
MONTHLY_BUDGET = 300.0
|
||||||
|
|
||||||
|
def fetch_portfolio_data():
|
||||||
|
"""Fetches 1 year of historical data with technicals."""
|
||||||
|
print(f"--- Fetching 1 Year Data for {TICKERS} ---")
|
||||||
|
try:
|
||||||
|
companies = Toolkit(
|
||||||
|
tickers=TICKERS,
|
||||||
|
api_key=API_KEY,
|
||||||
|
start_date="2024-01-01",
|
||||||
|
)
|
||||||
|
historical = companies.get_historical_data(period="daily")
|
||||||
|
|
||||||
|
portfolio = {}
|
||||||
|
for ticker in TICKERS:
|
||||||
|
if ticker in historical.columns.get_level_values(1):
|
||||||
|
df = historical.xs(ticker, level=1, axis=1).copy()
|
||||||
|
elif ticker in historical.columns.get_level_values(0):
|
||||||
|
df = historical.xs(ticker, level=0, axis=1).copy()
|
||||||
|
else: continue
|
||||||
|
|
||||||
|
# Technicals for signals
|
||||||
|
df['EMA_5'] = df['Close'].ewm(span=5, adjust=False).mean()
|
||||||
|
# Bollinger Band Width
|
||||||
|
std_20 = df['Close'].rolling(window=20).std()
|
||||||
|
ma_20 = df['Close'].rolling(window=20).mean()
|
||||||
|
df['BB_Width'] = (4 * std_20) / ma_20
|
||||||
|
|
||||||
|
df.dropna(inplace=True)
|
||||||
|
portfolio[ticker] = df
|
||||||
|
|
||||||
|
return portfolio
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching data: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def simulate_market_neutral(data, ticker):
|
||||||
|
"""
|
||||||
|
Simulates Bi-Directional strategy.
|
||||||
|
Can be Long, Short, or Hedged.
|
||||||
|
"""
|
||||||
|
cash = 0
|
||||||
|
long_holdings = 0
|
||||||
|
short_holdings = 0
|
||||||
|
short_entry_price = 0
|
||||||
|
|
||||||
|
total_invested = 0
|
||||||
|
budget_per_month = MONTHLY_BUDGET / len(TICKERS)
|
||||||
|
|
||||||
|
day_count = 0
|
||||||
|
for date, row in data.iterrows():
|
||||||
|
day_count += 1
|
||||||
|
price = row['Close']
|
||||||
|
|
||||||
|
# Monthly Injection
|
||||||
|
if day_count % 20 == 0:
|
||||||
|
cash += budget_per_month
|
||||||
|
total_invested += budget_per_month
|
||||||
|
|
||||||
|
# 1. EXIT CURRENT POSITIONS IF SIGNAL CHANGES
|
||||||
|
# (Simplified: check daily if we should switch)
|
||||||
|
|
||||||
|
signal = "NEUTRAL"
|
||||||
|
if row['BB_Width'] > 0.08: # Hedge if high volatility (>8% width)
|
||||||
|
signal = "HEDGE"
|
||||||
|
elif price > row['EMA_5']:
|
||||||
|
signal = "LONG"
|
||||||
|
else:
|
||||||
|
signal = "SHORT"
|
||||||
|
|
||||||
|
# 2. EXECUTE
|
||||||
|
if signal == "LONG":
|
||||||
|
# Close Short if any
|
||||||
|
if short_holdings > 0:
|
||||||
|
profit = (short_entry_price - price) * short_holdings
|
||||||
|
cash += (short_holdings * short_entry_price) + profit
|
||||||
|
short_holdings = 0
|
||||||
|
# Open Long
|
||||||
|
if cash > 10:
|
||||||
|
long_holdings += cash / price
|
||||||
|
cash = 0
|
||||||
|
|
||||||
|
elif signal == "SHORT":
|
||||||
|
# Close Long if any
|
||||||
|
if long_holdings > 0:
|
||||||
|
cash += long_holdings * price
|
||||||
|
long_holdings = 0
|
||||||
|
# Open Short
|
||||||
|
if cash > 10:
|
||||||
|
short_holdings = cash / price
|
||||||
|
short_entry_price = price
|
||||||
|
cash = 0
|
||||||
|
|
||||||
|
elif signal == "HEDGE":
|
||||||
|
# Split 50/50
|
||||||
|
current_val = cash + (long_holdings * price) + (short_holdings * price if short_holdings > 0 else 0)
|
||||||
|
# Flatten everything first for simplicity in simulation
|
||||||
|
if short_holdings > 0:
|
||||||
|
profit = (short_entry_price - price) * short_holdings
|
||||||
|
current_val += profit # Adjust for short profit/loss
|
||||||
|
|
||||||
|
cash = current_val
|
||||||
|
long_holdings = (cash * 0.5) / price
|
||||||
|
short_holdings = (cash * 0.5) / price
|
||||||
|
short_entry_price = price
|
||||||
|
cash = 0
|
||||||
|
|
||||||
|
# Final Liquidation
|
||||||
|
final_price = data.iloc[-1]['Close']
|
||||||
|
short_profit = (short_entry_price - final_price) * short_holdings if short_holdings > 0 else 0
|
||||||
|
final_val = cash + (long_holdings * final_price) + (short_holdings * short_entry_price) + short_profit
|
||||||
|
|
||||||
|
profit = final_val - total_invested
|
||||||
|
roi = profit / total_invested if total_invested > 0 else 0
|
||||||
|
|
||||||
|
# Bench
|
||||||
|
start = data.iloc[0]['Close']
|
||||||
|
end = data.iloc[-1]['Close']
|
||||||
|
bench_roi = (end / start) - 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
'ticker': ticker,
|
||||||
|
'final_val': final_val,
|
||||||
|
'invested': total_invested,
|
||||||
|
'roi': roi * 100,
|
||||||
|
'bench_roi': bench_roi * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
def main():
|
||||||
|
portfolio = fetch_portfolio_data()
|
||||||
|
if not portfolio: return
|
||||||
|
|
||||||
|
print("\n--- Running Phase 7 Backtest (Bi-Directional Long/Short) ---")
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for ticker, df in portfolio.items():
|
||||||
|
res = simulate_market_neutral(df, ticker)
|
||||||
|
results.append(res)
|
||||||
|
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print(f"{'Ticker':<10} | {'Strategy ROI':<15} | {'Bench ROI':<10}")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
total_invested = 0
|
||||||
|
total_final = 0
|
||||||
|
|
||||||
|
for r in results:
|
||||||
|
print(f"{r['ticker']:<10} | {r['roi']:>14.2f}% | {r['bench_roi']:>9.2f}%")
|
||||||
|
total_invested += r['invested']
|
||||||
|
total_final += r['final_val']
|
||||||
|
|
||||||
|
final_roi = ((total_final - total_invested) / total_invested) * 100 if total_invested > 0 else 0
|
||||||
|
|
||||||
|
print("-" * 60)
|
||||||
|
print(f"{'TOTAL':<10} | {final_roi:>14.2f}% | {'---':>9}")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
print("\nNote: ROI includes profits from both price gains (Long) and price drops (Short).")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
101
news_analyzer.py
Normal file
101
news_analyzer.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
"""
|
||||||
|
Module for fetching financial news via Google News RSS and analyzing sentiment using VADER.
|
||||||
|
"""
|
||||||
|
import feedparser
|
||||||
|
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
|
||||||
|
from datetime import datetime
|
||||||
|
import time
|
||||||
|
|
||||||
|
def fetch_news(ticker, limit=5):
|
||||||
|
"""
|
||||||
|
Fetches the latest news for a given ticker from Google News RSS.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ticker (str): Stock ticker symbol (e.g., 'AAPL', 'NVDA', 'RELIANCE.NS').
|
||||||
|
limit (int): Maximum number of news items to return.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: A list of dictionaries containing 'title', 'link', 'published', and 'summary'.
|
||||||
|
"""
|
||||||
|
# Use standard Google News RSS search query
|
||||||
|
# "when:1d" parameter ensures recent news if supported, but RSS feed order is usually chronological
|
||||||
|
encoded_ticker = ticker.replace("&", "%26")
|
||||||
|
rss_url = f"https://news.google.com/rss/search?q={encoded_ticker}+stock+when:1d&hl=en-US&gl=US&ceid=US:en"
|
||||||
|
|
||||||
|
try:
|
||||||
|
feed = feedparser.parse(rss_url)
|
||||||
|
news_items = []
|
||||||
|
|
||||||
|
for entry in feed.entries[:limit]:
|
||||||
|
news_items.append({
|
||||||
|
'title': entry.title,
|
||||||
|
'link': entry.link,
|
||||||
|
'published': entry.get('published', datetime.now().strftime("%a, %d %b %Y %H:%M:%S GMT")),
|
||||||
|
'summary': entry.get('description', '')
|
||||||
|
})
|
||||||
|
|
||||||
|
return news_items
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching news for {ticker}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def analyze_sentiment(news_items):
|
||||||
|
"""
|
||||||
|
Analyzes the sentiment of a list of news items.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
news_items (list): List of news dictionaries.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Contains 'average_score', 'sentiment_label', and 'scored_news'.
|
||||||
|
"""
|
||||||
|
analyzer = SentimentIntensityAnalyzer()
|
||||||
|
total_score = 0
|
||||||
|
scored_news = []
|
||||||
|
|
||||||
|
if not news_items:
|
||||||
|
return {
|
||||||
|
'average_score': 0,
|
||||||
|
'sentiment_label': 'Neutral',
|
||||||
|
'scored_news': []
|
||||||
|
}
|
||||||
|
|
||||||
|
for item in news_items:
|
||||||
|
# Analyze title mostly as it contains the key info in RSS
|
||||||
|
text_to_analyze = f"{item['title']}. {item['summary']}"
|
||||||
|
sentiment = analyzer.polarity_scores(text_to_analyze)
|
||||||
|
compound_score = sentiment['compound']
|
||||||
|
|
||||||
|
total_score += compound_score
|
||||||
|
|
||||||
|
item_with_score = item.copy()
|
||||||
|
item_with_score['sentiment_score'] = compound_score
|
||||||
|
scored_news.append(item_with_score)
|
||||||
|
|
||||||
|
average_score = total_score / len(news_items)
|
||||||
|
|
||||||
|
if average_score >= 0.05:
|
||||||
|
label = "Positive"
|
||||||
|
elif average_score <= -0.05:
|
||||||
|
label = "Negative"
|
||||||
|
else:
|
||||||
|
label = "Neutral"
|
||||||
|
|
||||||
|
return {
|
||||||
|
'average_score': round(average_score, 4),
|
||||||
|
'sentiment_label': label,
|
||||||
|
'scored_news': scored_news
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Test Block
|
||||||
|
ticker = "AAPL"
|
||||||
|
print(f"--- Fetching News for {ticker} ---")
|
||||||
|
news = fetch_news(ticker)
|
||||||
|
for n in news:
|
||||||
|
print(f"- {n['title']}")
|
||||||
|
|
||||||
|
print(f"\n--- Analyzing Sentiment ---")
|
||||||
|
result = analyze_sentiment(news)
|
||||||
|
print(f"Average Score: {result['average_score']}")
|
||||||
|
print(f"Label: {result['sentiment_label']}")
|
||||||
185
options_engine.py
Normal file
185
options_engine.py
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
"""
|
||||||
|
FinanceIQ v5.1 — Options Analytics Engine
|
||||||
|
Black-Scholes pricing, Greeks, implied volatility, and payoff diagrams.
|
||||||
|
"""
|
||||||
|
import math
|
||||||
|
from scipy.stats import norm
|
||||||
|
|
||||||
|
|
||||||
|
def d1(S, K, T, r, sigma):
|
||||||
|
"""Calculate d1 for Black-Scholes."""
|
||||||
|
if T <= 0 or sigma <= 0:
|
||||||
|
return 0.0
|
||||||
|
return (math.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * math.sqrt(T))
|
||||||
|
|
||||||
|
|
||||||
|
def d2(S, K, T, r, sigma):
|
||||||
|
"""Calculate d2 for Black-Scholes."""
|
||||||
|
return d1(S, K, T, r, sigma) - sigma * math.sqrt(T) if T > 0 and sigma > 0 else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def bs_call_price(S, K, T, r, sigma):
|
||||||
|
"""Black-Scholes call option price."""
|
||||||
|
if T <= 0:
|
||||||
|
return max(S - K, 0)
|
||||||
|
_d1 = d1(S, K, T, r, sigma)
|
||||||
|
_d2 = d2(S, K, T, r, sigma)
|
||||||
|
return S * norm.cdf(_d1) - K * math.exp(-r * T) * norm.cdf(_d2)
|
||||||
|
|
||||||
|
|
||||||
|
def bs_put_price(S, K, T, r, sigma):
|
||||||
|
"""Black-Scholes put option price."""
|
||||||
|
if T <= 0:
|
||||||
|
return max(K - S, 0)
|
||||||
|
_d1 = d1(S, K, T, r, sigma)
|
||||||
|
_d2 = d2(S, K, T, r, sigma)
|
||||||
|
return K * math.exp(-r * T) * norm.cdf(-_d2) - S * norm.cdf(-_d1)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_greeks(S, K, T, r, sigma, option_type="call"):
|
||||||
|
"""
|
||||||
|
Calculate all Greeks for an option.
|
||||||
|
S: underlying price, K: strike, T: time to expiry (years),
|
||||||
|
r: risk-free rate, sigma: implied volatility
|
||||||
|
"""
|
||||||
|
if T <= 0 or sigma <= 0:
|
||||||
|
intrinsic = max(S - K, 0) if option_type == "call" else max(K - S, 0)
|
||||||
|
return {
|
||||||
|
"delta": 1.0 if (option_type == "call" and S > K) else (-1.0 if option_type == "put" and K > S else 0.0),
|
||||||
|
"gamma": 0.0,
|
||||||
|
"theta": 0.0,
|
||||||
|
"vega": 0.0,
|
||||||
|
"rho": 0.0,
|
||||||
|
"price": intrinsic
|
||||||
|
}
|
||||||
|
|
||||||
|
_d1 = d1(S, K, T, r, sigma)
|
||||||
|
_d2 = d2(S, K, T, r, sigma)
|
||||||
|
sqrt_T = math.sqrt(T)
|
||||||
|
|
||||||
|
# Delta
|
||||||
|
delta = norm.cdf(_d1) if option_type == "call" else norm.cdf(_d1) - 1
|
||||||
|
|
||||||
|
# Gamma (same for calls & puts)
|
||||||
|
gamma = norm.pdf(_d1) / (S * sigma * sqrt_T)
|
||||||
|
|
||||||
|
# Theta (per day)
|
||||||
|
theta_common = -(S * norm.pdf(_d1) * sigma) / (2 * sqrt_T)
|
||||||
|
if option_type == "call":
|
||||||
|
theta = (theta_common - r * K * math.exp(-r * T) * norm.cdf(_d2)) / 365
|
||||||
|
else:
|
||||||
|
theta = (theta_common + r * K * math.exp(-r * T) * norm.cdf(-_d2)) / 365
|
||||||
|
|
||||||
|
# Vega (per 1% move in IV)
|
||||||
|
vega = S * sqrt_T * norm.pdf(_d1) / 100
|
||||||
|
|
||||||
|
# Rho (per 1% move in rate)
|
||||||
|
if option_type == "call":
|
||||||
|
rho = K * T * math.exp(-r * T) * norm.cdf(_d2) / 100
|
||||||
|
else:
|
||||||
|
rho = -K * T * math.exp(-r * T) * norm.cdf(-_d2) / 100
|
||||||
|
|
||||||
|
# Price
|
||||||
|
price = bs_call_price(S, K, T, r, sigma) if option_type == "call" else bs_put_price(S, K, T, r, sigma)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"delta": round(delta, 4),
|
||||||
|
"gamma": round(gamma, 6),
|
||||||
|
"theta": round(theta, 4),
|
||||||
|
"vega": round(vega, 4),
|
||||||
|
"rho": round(rho, 4),
|
||||||
|
"price": round(price, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def implied_volatility(market_price, S, K, T, r, option_type="call", tol=1e-6, max_iter=100):
|
||||||
|
"""
|
||||||
|
Calculate implied volatility using Newton-Raphson method.
|
||||||
|
"""
|
||||||
|
if T <= 0 or market_price <= 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
sigma = 0.3 # initial guess
|
||||||
|
for _ in range(max_iter):
|
||||||
|
if option_type == "call":
|
||||||
|
price = bs_call_price(S, K, T, r, sigma)
|
||||||
|
else:
|
||||||
|
price = bs_put_price(S, K, T, r, sigma)
|
||||||
|
|
||||||
|
diff = price - market_price
|
||||||
|
if abs(diff) < tol:
|
||||||
|
return round(sigma, 6)
|
||||||
|
|
||||||
|
# Vega for Newton-Raphson step
|
||||||
|
_d1 = d1(S, K, T, r, sigma)
|
||||||
|
vega = S * math.sqrt(T) * norm.pdf(_d1)
|
||||||
|
if vega < 1e-12:
|
||||||
|
break
|
||||||
|
sigma -= diff / vega
|
||||||
|
sigma = max(sigma, 0.001) # floor
|
||||||
|
|
||||||
|
return round(sigma, 6)
|
||||||
|
|
||||||
|
|
||||||
|
def payoff_diagram(S, K, premium, option_type="call", is_long=True, num_points=50):
|
||||||
|
"""
|
||||||
|
Generate payoff diagram data points.
|
||||||
|
Returns list of {price, payoff, profit} dicts.
|
||||||
|
"""
|
||||||
|
low = K * 0.7
|
||||||
|
high = K * 1.3
|
||||||
|
step = (high - low) / num_points
|
||||||
|
points = []
|
||||||
|
for i in range(num_points + 1):
|
||||||
|
price = low + i * step
|
||||||
|
if option_type == "call":
|
||||||
|
payoff = max(price - K, 0)
|
||||||
|
else:
|
||||||
|
payoff = max(K - price, 0)
|
||||||
|
|
||||||
|
if is_long:
|
||||||
|
profit = payoff - premium
|
||||||
|
else:
|
||||||
|
profit = premium - payoff
|
||||||
|
|
||||||
|
points.append({
|
||||||
|
"price": round(price, 2),
|
||||||
|
"payoff": round(payoff, 2),
|
||||||
|
"profit": round(profit, 2)
|
||||||
|
})
|
||||||
|
return points
|
||||||
|
|
||||||
|
|
||||||
|
def enrich_chain_with_greeks(chain_data, current_price, expiry_date_str, risk_free_rate=0.05):
|
||||||
|
"""
|
||||||
|
Add Greeks to each option in a chain.
|
||||||
|
chain_data: list of dicts from yfinance option_chain
|
||||||
|
expiry_date_str: 'YYYY-MM-DD'
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
try:
|
||||||
|
expiry = datetime.strptime(expiry_date_str, "%Y-%m-%d")
|
||||||
|
today = datetime.now()
|
||||||
|
T = max((expiry - today).days / 365.0, 0.001)
|
||||||
|
except:
|
||||||
|
T = 0.1 # fallback
|
||||||
|
|
||||||
|
for opt in chain_data:
|
||||||
|
strike = opt.get("strike", 0)
|
||||||
|
last_price = opt.get("lastPrice", 0)
|
||||||
|
iv = opt.get("impliedVolatility", 0.3)
|
||||||
|
opt_type = "call" if opt.get("_type", "call") == "call" else "put"
|
||||||
|
|
||||||
|
if strike > 0 and current_price > 0 and iv > 0:
|
||||||
|
greeks = calculate_greeks(current_price, strike, T, risk_free_rate, iv, opt_type)
|
||||||
|
opt["delta"] = greeks["delta"]
|
||||||
|
opt["gamma"] = greeks["gamma"]
|
||||||
|
opt["theta"] = greeks["theta"]
|
||||||
|
opt["vega"] = greeks["vega"]
|
||||||
|
opt["rho"] = greeks["rho"]
|
||||||
|
opt["bs_price"] = greeks["price"]
|
||||||
|
else:
|
||||||
|
opt["delta"] = opt["gamma"] = opt["theta"] = opt["vega"] = opt["rho"] = 0
|
||||||
|
opt["bs_price"] = 0
|
||||||
|
|
||||||
|
return chain_data
|
||||||
741
portfolio_engine.py
Normal file
741
portfolio_engine.py
Normal file
@ -0,0 +1,741 @@
|
|||||||
|
"""
|
||||||
|
FinanceIQ v5.2 — Enhanced Paper Trading Portfolio Engine
|
||||||
|
SQLite-backed virtual portfolio with $100K starting capital.
|
||||||
|
|
||||||
|
Features inspired by hftbacktest:
|
||||||
|
- Market, Limit, Stop-Loss, Take-Profit orders
|
||||||
|
- Slippage & commission modeling
|
||||||
|
- Short selling with margin tracking
|
||||||
|
- Equity curve tracking
|
||||||
|
- Enhanced analytics (Sortino, Calmar, profit factor, avg hold period)
|
||||||
|
"""
|
||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
import math
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "portfolio.db")
|
||||||
|
INITIAL_CASH = 100000.00
|
||||||
|
DEFAULT_SLIPPAGE_BPS = 5 # 0.05% default slippage
|
||||||
|
DEFAULT_COMMISSION_PER_SHARE = 0.005 # $0.005 per share (IBKR-style)
|
||||||
|
MIN_COMMISSION = 1.00 # $1 minimum per trade
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
"""Get database connection and ensure tables exist."""
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
conn.execute("PRAGMA journal_mode=WAL")
|
||||||
|
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS portfolio_meta (
|
||||||
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||||
|
cash REAL NOT NULL DEFAULT 100000.00,
|
||||||
|
slippage_bps REAL NOT NULL DEFAULT 5,
|
||||||
|
commission_per_share REAL NOT NULL DEFAULT 0.005,
|
||||||
|
margin_used REAL NOT NULL DEFAULT 0,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS positions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
ticker TEXT NOT NULL,
|
||||||
|
asset_type TEXT NOT NULL DEFAULT 'stock',
|
||||||
|
shares REAL NOT NULL DEFAULT 0,
|
||||||
|
avg_cost REAL NOT NULL DEFAULT 0,
|
||||||
|
side TEXT NOT NULL DEFAULT 'LONG',
|
||||||
|
opened_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
UNIQUE(ticker, side)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS transactions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
ticker TEXT NOT NULL,
|
||||||
|
asset_type TEXT NOT NULL DEFAULT 'stock',
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
side TEXT NOT NULL DEFAULT 'LONG',
|
||||||
|
shares REAL NOT NULL,
|
||||||
|
price REAL NOT NULL,
|
||||||
|
slippage REAL NOT NULL DEFAULT 0,
|
||||||
|
commission REAL NOT NULL DEFAULT 0,
|
||||||
|
total REAL NOT NULL,
|
||||||
|
pnl REAL DEFAULT NULL,
|
||||||
|
timestamp TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS pending_orders (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
ticker TEXT NOT NULL,
|
||||||
|
asset_type TEXT NOT NULL DEFAULT 'stock',
|
||||||
|
order_type TEXT NOT NULL,
|
||||||
|
side TEXT NOT NULL DEFAULT 'BUY',
|
||||||
|
shares REAL NOT NULL,
|
||||||
|
target_price REAL NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'PENDING',
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
filled_at TEXT DEFAULT NULL,
|
||||||
|
expires_at TEXT DEFAULT NULL
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS equity_curve (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
timestamp TEXT NOT NULL,
|
||||||
|
total_value REAL NOT NULL,
|
||||||
|
cash REAL NOT NULL,
|
||||||
|
positions_value REAL NOT NULL,
|
||||||
|
daily_return REAL DEFAULT 0
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS portfolio_snapshots (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
date TEXT NOT NULL UNIQUE,
|
||||||
|
total_value REAL NOT NULL,
|
||||||
|
cash REAL NOT NULL,
|
||||||
|
positions_value REAL NOT NULL
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
cursor = conn.execute("SELECT COUNT(*) FROM portfolio_meta")
|
||||||
|
if cursor.fetchone()[0] == 0:
|
||||||
|
conn.execute("INSERT INTO portfolio_meta (id, cash) VALUES (1, ?)", (INITIAL_CASH,))
|
||||||
|
conn.commit()
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
def _get_meta(conn):
|
||||||
|
row = conn.execute("SELECT * FROM portfolio_meta WHERE id = 1").fetchone()
|
||||||
|
return dict(row)
|
||||||
|
|
||||||
|
|
||||||
|
def _calc_slippage(price, shares, slippage_bps, side):
|
||||||
|
"""Calculate slippage based on order direction."""
|
||||||
|
slip_pct = slippage_bps / 10000.0
|
||||||
|
if side == "BUY":
|
||||||
|
return price * slip_pct # pay more when buying
|
||||||
|
else:
|
||||||
|
return -price * slip_pct # receive less when selling
|
||||||
|
|
||||||
|
|
||||||
|
def _calc_commission(shares, comm_per_share):
|
||||||
|
"""Calculate commission with minimum."""
|
||||||
|
return max(abs(shares) * comm_per_share, MIN_COMMISSION)
|
||||||
|
|
||||||
|
|
||||||
|
def get_cash():
|
||||||
|
conn = get_db()
|
||||||
|
row = conn.execute("SELECT cash FROM portfolio_meta WHERE id = 1").fetchone()
|
||||||
|
conn.close()
|
||||||
|
return row["cash"] if row else INITIAL_CASH
|
||||||
|
|
||||||
|
|
||||||
|
def get_positions():
|
||||||
|
conn = get_db()
|
||||||
|
rows = conn.execute("SELECT * FROM positions WHERE shares > 0").fetchall()
|
||||||
|
conn.close()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def buy(ticker, shares, price, asset_type="stock"):
|
||||||
|
"""Buy shares (long) with slippage and commission."""
|
||||||
|
if shares <= 0 or price <= 0:
|
||||||
|
return {"error": "Invalid shares or price"}
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
meta = _get_meta(conn)
|
||||||
|
cash = meta["cash"]
|
||||||
|
slippage_bps = meta["slippage_bps"]
|
||||||
|
comm_per_share = meta["commission_per_share"]
|
||||||
|
|
||||||
|
# Apply slippage
|
||||||
|
slip = _calc_slippage(price, shares, slippage_bps, "BUY")
|
||||||
|
exec_price = price + slip
|
||||||
|
|
||||||
|
# Calculate commission
|
||||||
|
commission = _calc_commission(shares, comm_per_share)
|
||||||
|
|
||||||
|
total_cost = shares * exec_price + commission
|
||||||
|
|
||||||
|
if total_cost > cash:
|
||||||
|
conn.close()
|
||||||
|
return {"error": f"Insufficient cash. Need ${total_cost:.2f}, have ${cash:.2f}"}
|
||||||
|
|
||||||
|
new_cash = cash - total_cost
|
||||||
|
conn.execute("UPDATE portfolio_meta SET cash = ?, updated_at = datetime('now') WHERE id = 1", (new_cash,))
|
||||||
|
|
||||||
|
existing = conn.execute("SELECT * FROM positions WHERE ticker = ? AND side = 'LONG'", (ticker,)).fetchone()
|
||||||
|
if existing:
|
||||||
|
old_shares = existing["shares"]
|
||||||
|
old_avg = existing["avg_cost"]
|
||||||
|
new_shares = old_shares + shares
|
||||||
|
new_avg = ((old_shares * old_avg) + (shares * exec_price)) / new_shares
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE positions SET shares = ?, avg_cost = ?, updated_at = datetime('now') WHERE ticker = ? AND side = 'LONG'",
|
||||||
|
(new_shares, new_avg, ticker)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO positions (ticker, asset_type, shares, avg_cost, side) VALUES (?, ?, ?, ?, 'LONG')",
|
||||||
|
(ticker, asset_type, shares, exec_price)
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO transactions (ticker, asset_type, action, side, shares, price, slippage, commission, total)
|
||||||
|
VALUES (?, ?, 'BUY', 'LONG', ?, ?, ?, ?, ?)""",
|
||||||
|
(ticker, asset_type, shares, exec_price, slip * shares, commission, total_cost)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True, "action": "BUY", "ticker": ticker,
|
||||||
|
"shares": shares, "price": round(exec_price, 4),
|
||||||
|
"slippage": round(slip, 4), "commission": round(commission, 2),
|
||||||
|
"total": round(total_cost, 2), "remaining_cash": round(new_cash, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def sell(ticker, shares, price):
|
||||||
|
"""Sell long shares with slippage and commission."""
|
||||||
|
if shares <= 0 or price <= 0:
|
||||||
|
return {"error": "Invalid shares or price"}
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
meta = _get_meta(conn)
|
||||||
|
slippage_bps = meta["slippage_bps"]
|
||||||
|
comm_per_share = meta["commission_per_share"]
|
||||||
|
|
||||||
|
existing = conn.execute("SELECT * FROM positions WHERE ticker = ? AND side = 'LONG'", (ticker,)).fetchone()
|
||||||
|
if not existing or existing["shares"] < shares:
|
||||||
|
conn.close()
|
||||||
|
available = existing["shares"] if existing else 0
|
||||||
|
return {"error": f"Insufficient shares. Have {available}, trying to sell {shares}"}
|
||||||
|
|
||||||
|
slip = _calc_slippage(price, shares, slippage_bps, "SELL")
|
||||||
|
exec_price = price + slip # slip is negative for sells
|
||||||
|
commission = _calc_commission(shares, comm_per_share)
|
||||||
|
total_proceeds = shares * exec_price - commission
|
||||||
|
|
||||||
|
new_shares = existing["shares"] - shares
|
||||||
|
cash = meta["cash"]
|
||||||
|
new_cash = cash + total_proceeds
|
||||||
|
|
||||||
|
conn.execute("UPDATE portfolio_meta SET cash = ?, updated_at = datetime('now') WHERE id = 1", (new_cash,))
|
||||||
|
|
||||||
|
if new_shares <= 0:
|
||||||
|
conn.execute("DELETE FROM positions WHERE ticker = ? AND side = 'LONG'", (ticker,))
|
||||||
|
else:
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE positions SET shares = ?, updated_at = datetime('now') WHERE ticker = ? AND side = 'LONG'",
|
||||||
|
(new_shares, ticker)
|
||||||
|
)
|
||||||
|
|
||||||
|
pnl = (exec_price - existing["avg_cost"]) * shares - commission
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO transactions (ticker, asset_type, action, side, shares, price, slippage, commission, total, pnl)
|
||||||
|
VALUES (?, ?, 'SELL', 'LONG', ?, ?, ?, ?, ?, ?)""",
|
||||||
|
(ticker, existing["asset_type"], shares, exec_price, slip * shares, commission, total_proceeds, pnl)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True, "action": "SELL", "ticker": ticker,
|
||||||
|
"shares": shares, "price": round(exec_price, 4),
|
||||||
|
"slippage": round(slip, 4), "commission": round(commission, 2),
|
||||||
|
"total": round(total_proceeds, 2), "pnl": round(pnl, 2),
|
||||||
|
"remaining_cash": round(new_cash, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def short_sell(ticker, shares, price, asset_type="stock"):
|
||||||
|
"""Open a short position. Margin is deducted from cash."""
|
||||||
|
if shares <= 0 or price <= 0:
|
||||||
|
return {"error": "Invalid shares or price"}
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
meta = _get_meta(conn)
|
||||||
|
cash = meta["cash"]
|
||||||
|
slippage_bps = meta["slippage_bps"]
|
||||||
|
comm_per_share = meta["commission_per_share"]
|
||||||
|
|
||||||
|
slip = _calc_slippage(price, shares, slippage_bps, "SELL")
|
||||||
|
exec_price = price + slip # worse price when selling (slippage)
|
||||||
|
commission = _calc_commission(shares, comm_per_share)
|
||||||
|
|
||||||
|
# Margin requirement: 100% of position value + commission
|
||||||
|
margin_req = shares * exec_price + commission
|
||||||
|
if margin_req > cash:
|
||||||
|
conn.close()
|
||||||
|
return {"error": f"Insufficient funds. Need ${margin_req:.2f}, have ${cash:.2f}"}
|
||||||
|
|
||||||
|
# Deduct margin from cash (no proceeds added — paper trading model)
|
||||||
|
new_cash = cash - margin_req
|
||||||
|
new_margin = meta["margin_used"] + (shares * exec_price)
|
||||||
|
|
||||||
|
conn.execute("UPDATE portfolio_meta SET cash = ?, margin_used = ?, updated_at = datetime('now') WHERE id = 1",
|
||||||
|
(new_cash, new_margin))
|
||||||
|
|
||||||
|
existing = conn.execute("SELECT * FROM positions WHERE ticker = ? AND side = 'SHORT'", (ticker,)).fetchone()
|
||||||
|
if existing:
|
||||||
|
old_shares = existing["shares"]
|
||||||
|
old_avg = existing["avg_cost"]
|
||||||
|
new_shares = old_shares + shares
|
||||||
|
new_avg = ((old_shares * old_avg) + (shares * exec_price)) / new_shares
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE positions SET shares = ?, avg_cost = ?, updated_at = datetime('now') WHERE ticker = ? AND side = 'SHORT'",
|
||||||
|
(new_shares, new_avg, ticker)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO positions (ticker, asset_type, shares, avg_cost, side) VALUES (?, ?, ?, ?, 'SHORT')",
|
||||||
|
(ticker, asset_type, shares, exec_price)
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO transactions (ticker, asset_type, action, side, shares, price, slippage, commission, total)
|
||||||
|
VALUES (?, ?, 'SHORT', 'SHORT', ?, ?, ?, ?, ?)""",
|
||||||
|
(ticker, asset_type, shares, exec_price, abs(slip) * shares, commission, margin_req)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True, "action": "SHORT", "ticker": ticker,
|
||||||
|
"shares": shares, "price": round(exec_price, 4),
|
||||||
|
"margin_required": round(margin_req, 2),
|
||||||
|
"commission": round(commission, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def cover_short(ticker, shares, price):
|
||||||
|
"""Cover (buy back) a short position. Releases margin + P&L to cash."""
|
||||||
|
if shares <= 0 or price <= 0:
|
||||||
|
return {"error": "Invalid shares or price"}
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
meta = _get_meta(conn)
|
||||||
|
slippage_bps = meta["slippage_bps"]
|
||||||
|
comm_per_share = meta["commission_per_share"]
|
||||||
|
|
||||||
|
existing = conn.execute("SELECT * FROM positions WHERE ticker = ? AND side = 'SHORT'", (ticker,)).fetchone()
|
||||||
|
if not existing or existing["shares"] < shares:
|
||||||
|
conn.close()
|
||||||
|
available = existing["shares"] if existing else 0
|
||||||
|
return {"error": f"No short position. Have {available} short shares."}
|
||||||
|
|
||||||
|
slip = _calc_slippage(price, shares, slippage_bps, "BUY")
|
||||||
|
exec_price = price + slip # worse price when buying back (slippage)
|
||||||
|
commission = _calc_commission(shares, comm_per_share)
|
||||||
|
|
||||||
|
# P&L: profit when we shorted high and cover low
|
||||||
|
entry_value = shares * existing["avg_cost"]
|
||||||
|
cover_cost = shares * exec_price
|
||||||
|
pnl = entry_value - cover_cost - commission # short profit when price drops
|
||||||
|
|
||||||
|
# Release margin and return to cash with P&L
|
||||||
|
cash = meta["cash"]
|
||||||
|
margin_release = entry_value # we held 100% of entry value as margin
|
||||||
|
new_cash = cash + margin_release + pnl # margin back + profit/loss
|
||||||
|
new_margin = max(0, meta["margin_used"] - margin_release)
|
||||||
|
|
||||||
|
conn.execute("UPDATE portfolio_meta SET cash = ?, margin_used = ?, updated_at = datetime('now') WHERE id = 1",
|
||||||
|
(new_cash, new_margin))
|
||||||
|
|
||||||
|
new_shares = existing["shares"] - shares
|
||||||
|
if new_shares <= 0:
|
||||||
|
conn.execute("DELETE FROM positions WHERE ticker = ? AND side = 'SHORT'", (ticker,))
|
||||||
|
else:
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE positions SET shares = ?, updated_at = datetime('now') WHERE ticker = ? AND side = 'SHORT'",
|
||||||
|
(new_shares, ticker)
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO transactions (ticker, asset_type, action, side, shares, price, slippage, commission, total, pnl)
|
||||||
|
VALUES (?, ?, 'COVER', 'SHORT', ?, ?, ?, ?, ?, ?)""",
|
||||||
|
(ticker, existing["asset_type"], shares, exec_price, abs(slip) * shares, commission, cover_cost + commission, pnl)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True, "action": "COVER", "ticker": ticker,
|
||||||
|
"shares": shares, "price": round(exec_price, 4),
|
||||||
|
"pnl": round(pnl, 2), "commission": round(commission, 2),
|
||||||
|
"remaining_cash": round(new_cash, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Pending Orders (Limit / Stop) ────────────────────────
|
||||||
|
|
||||||
|
def place_limit_order(ticker, side, shares, limit_price, asset_type="stock", expires_hours=24):
|
||||||
|
"""Place a limit order that fills when market price hits the target."""
|
||||||
|
if shares <= 0 or limit_price <= 0:
|
||||||
|
return {"error": "Invalid order parameters"}
|
||||||
|
|
||||||
|
expires = datetime.now()
|
||||||
|
from datetime import timedelta
|
||||||
|
expires = (expires + timedelta(hours=expires_hours)).isoformat()
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO pending_orders (ticker, asset_type, order_type, side, shares, target_price, expires_at)
|
||||||
|
VALUES (?, ?, 'LIMIT', ?, ?, ?, ?)""",
|
||||||
|
(ticker, asset_type, side, shares, limit_price, expires)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
order_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {"success": True, "order_id": order_id, "order_type": "LIMIT",
|
||||||
|
"side": side, "ticker": ticker, "shares": shares,
|
||||||
|
"target_price": limit_price, "expires_at": expires}
|
||||||
|
|
||||||
|
|
||||||
|
def place_stop_order(ticker, side, shares, stop_price, asset_type="stock", expires_hours=24):
|
||||||
|
"""Place a stop order (stop-loss or take-profit)."""
|
||||||
|
if shares <= 0 or stop_price <= 0:
|
||||||
|
return {"error": "Invalid order parameters"}
|
||||||
|
|
||||||
|
expires = datetime.now()
|
||||||
|
from datetime import timedelta
|
||||||
|
expires = (expires + timedelta(hours=expires_hours)).isoformat()
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO pending_orders (ticker, asset_type, order_type, side, shares, target_price, expires_at)
|
||||||
|
VALUES (?, ?, 'STOP', ?, ?, ?, ?)""",
|
||||||
|
(ticker, asset_type, side, shares, stop_price, expires)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
order_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {"success": True, "order_id": order_id, "order_type": "STOP",
|
||||||
|
"side": side, "ticker": ticker, "shares": shares,
|
||||||
|
"target_price": stop_price, "expires_at": expires}
|
||||||
|
|
||||||
|
|
||||||
|
def get_pending_orders():
|
||||||
|
"""Get all pending orders."""
|
||||||
|
conn = get_db()
|
||||||
|
# Expire old orders
|
||||||
|
conn.execute("UPDATE pending_orders SET status = 'EXPIRED' WHERE status = 'PENDING' AND expires_at < datetime('now')")
|
||||||
|
conn.commit()
|
||||||
|
rows = conn.execute("SELECT * FROM pending_orders WHERE status = 'PENDING' ORDER BY created_at DESC").fetchall()
|
||||||
|
conn.close()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_order(order_id):
|
||||||
|
"""Cancel a pending order."""
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute("UPDATE pending_orders SET status = 'CANCELLED' WHERE id = ? AND status = 'PENDING'", (order_id,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return {"success": True, "order_id": order_id, "status": "CANCELLED"}
|
||||||
|
|
||||||
|
|
||||||
|
def check_and_fill_orders(current_prices):
|
||||||
|
"""Check pending orders against current prices and fill if conditions met.
|
||||||
|
Called during live polling.
|
||||||
|
current_prices: dict {ticker: price}
|
||||||
|
"""
|
||||||
|
conn = get_db()
|
||||||
|
pending = conn.execute("SELECT * FROM pending_orders WHERE status = 'PENDING'").fetchall()
|
||||||
|
filled = []
|
||||||
|
|
||||||
|
for order in pending:
|
||||||
|
order = dict(order)
|
||||||
|
ticker = order["ticker"]
|
||||||
|
if ticker not in current_prices:
|
||||||
|
continue
|
||||||
|
|
||||||
|
market_price = current_prices[ticker]
|
||||||
|
should_fill = False
|
||||||
|
|
||||||
|
if order["order_type"] == "LIMIT":
|
||||||
|
if order["side"] == "BUY" and market_price <= order["target_price"]:
|
||||||
|
should_fill = True
|
||||||
|
elif order["side"] == "SELL" and market_price >= order["target_price"]:
|
||||||
|
should_fill = True
|
||||||
|
elif order["order_type"] == "STOP":
|
||||||
|
if order["side"] == "SELL" and market_price <= order["target_price"]:
|
||||||
|
should_fill = True # stop-loss triggered
|
||||||
|
elif order["side"] == "BUY" and market_price >= order["target_price"]:
|
||||||
|
should_fill = True # stop-buy triggered
|
||||||
|
|
||||||
|
if should_fill:
|
||||||
|
conn.execute("UPDATE pending_orders SET status = 'FILLED', filled_at = datetime('now') WHERE id = ?",
|
||||||
|
(order["id"],))
|
||||||
|
|
||||||
|
if order["side"] == "BUY":
|
||||||
|
result = buy(ticker, order["shares"], market_price, order["asset_type"])
|
||||||
|
else:
|
||||||
|
result = sell(ticker, order["shares"], market_price)
|
||||||
|
|
||||||
|
filled.append({"order_id": order["id"], "ticker": ticker, "result": result})
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return filled
|
||||||
|
|
||||||
|
|
||||||
|
# ── Equity Curve ──────────────────────────────────────
|
||||||
|
|
||||||
|
def record_equity_point(total_value, cash, positions_value):
|
||||||
|
"""Record a point on the equity curve."""
|
||||||
|
conn = get_db()
|
||||||
|
# Get last point for daily return calc
|
||||||
|
last = conn.execute("SELECT total_value FROM equity_curve ORDER BY id DESC LIMIT 1").fetchone()
|
||||||
|
daily_return = 0
|
||||||
|
if last and last["total_value"] > 0:
|
||||||
|
daily_return = (total_value - last["total_value"]) / last["total_value"]
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO equity_curve (timestamp, total_value, cash, positions_value, daily_return) VALUES (datetime('now'), ?, ?, ?, ?)",
|
||||||
|
(total_value, cash, positions_value, daily_return)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_equity_curve(limit=500):
|
||||||
|
"""Get equity curve data for charting."""
|
||||||
|
conn = get_db()
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT timestamp, total_value, cash, positions_value, daily_return FROM equity_curve ORDER BY id DESC LIMIT ?",
|
||||||
|
(limit,)
|
||||||
|
).fetchall()
|
||||||
|
conn.close()
|
||||||
|
return [dict(r) for r in reversed(rows)]
|
||||||
|
|
||||||
|
|
||||||
|
# ── Portfolio Summary ─────────────────────────────────
|
||||||
|
|
||||||
|
def get_portfolio_summary(current_prices=None):
|
||||||
|
if current_prices is None:
|
||||||
|
current_prices = {}
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
meta = _get_meta(conn)
|
||||||
|
cash = meta["cash"]
|
||||||
|
margin_used = meta["margin_used"]
|
||||||
|
positions = conn.execute("SELECT * FROM positions WHERE shares > 0").fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
pos_list = []
|
||||||
|
total_long_value = 0
|
||||||
|
total_short_value = 0
|
||||||
|
total_cost_basis = 0
|
||||||
|
|
||||||
|
for p in positions:
|
||||||
|
ticker = p["ticker"]
|
||||||
|
shares = p["shares"]
|
||||||
|
avg_cost = p["avg_cost"]
|
||||||
|
side = p["side"]
|
||||||
|
current_price = current_prices.get(ticker, avg_cost)
|
||||||
|
|
||||||
|
if side == "LONG":
|
||||||
|
market_value = shares * current_price
|
||||||
|
cost_basis = shares * avg_cost
|
||||||
|
pnl = market_value - cost_basis
|
||||||
|
total_long_value += market_value
|
||||||
|
else: # SHORT
|
||||||
|
market_value = shares * avg_cost # short value is entry value
|
||||||
|
cost_basis = shares * avg_cost
|
||||||
|
pnl = (avg_cost - current_price) * shares # profit when price drops
|
||||||
|
total_short_value += shares * current_price
|
||||||
|
|
||||||
|
pnl_pct = (pnl / cost_basis * 100) if cost_basis > 0 else 0
|
||||||
|
total_cost_basis += cost_basis
|
||||||
|
|
||||||
|
pos_list.append({
|
||||||
|
"ticker": ticker, "asset_type": p["asset_type"],
|
||||||
|
"shares": shares, "avg_cost": round(avg_cost, 2),
|
||||||
|
"current_price": round(current_price, 2),
|
||||||
|
"market_value": round(market_value, 2),
|
||||||
|
"side": side,
|
||||||
|
"cost_basis": round(cost_basis, 2),
|
||||||
|
"pnl": round(pnl, 2), "pnl_pct": round(pnl_pct, 2),
|
||||||
|
"allocation_pct": 0
|
||||||
|
})
|
||||||
|
|
||||||
|
positions_value = total_long_value
|
||||||
|
# total_value = available cash + long positions + margin held + unrealized short P&L
|
||||||
|
# margin_used tracks entry value of shorts (deducted from cash at open)
|
||||||
|
# short_unrealized_pnl = sum of (entry_price - current_price) * shares for shorts
|
||||||
|
short_unrealized_pnl = 0
|
||||||
|
for p in pos_list:
|
||||||
|
if p["side"] == "SHORT":
|
||||||
|
short_unrealized_pnl += p["pnl"]
|
||||||
|
total_value = cash + positions_value + margin_used + short_unrealized_pnl
|
||||||
|
total_pnl = total_value - INITIAL_CASH
|
||||||
|
total_pnl_pct = (total_pnl / INITIAL_CASH * 100)
|
||||||
|
|
||||||
|
for p in pos_list:
|
||||||
|
p["allocation_pct"] = round((abs(p["market_value"]) / total_value * 100) if total_value > 0 else 0, 2)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"cash": round(cash, 2),
|
||||||
|
"positions_value": round(positions_value, 2),
|
||||||
|
"total_value": round(total_value, 2),
|
||||||
|
"total_pnl": round(total_pnl, 2),
|
||||||
|
"total_pnl_pct": round(total_pnl_pct, 2),
|
||||||
|
"initial_capital": INITIAL_CASH,
|
||||||
|
"margin_used": round(margin_used, 2),
|
||||||
|
"buying_power": round(cash, 2),
|
||||||
|
"positions": pos_list,
|
||||||
|
"cash_allocation_pct": round((cash / total_value * 100) if total_value > 0 else 100, 2),
|
||||||
|
"slippage_bps": round(meta["slippage_bps"], 1),
|
||||||
|
"commission_per_share": round(meta["commission_per_share"], 4),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_transactions(limit=50):
|
||||||
|
conn = get_db()
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM transactions ORDER BY timestamp DESC LIMIT ?", (limit,)
|
||||||
|
).fetchall()
|
||||||
|
conn.close()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def get_analytics(current_prices=None):
|
||||||
|
"""Enhanced analytics with Sortino, Calmar, profit factor."""
|
||||||
|
conn = get_db()
|
||||||
|
txns = conn.execute("SELECT * FROM transactions ORDER BY timestamp ASC").fetchall()
|
||||||
|
equity = conn.execute("SELECT * FROM equity_curve ORDER BY id ASC").fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not txns:
|
||||||
|
return _empty_analytics()
|
||||||
|
|
||||||
|
# Daily returns from equity curve
|
||||||
|
daily_returns = [dict(e)["daily_return"] for e in equity if dict(e)["daily_return"] != 0]
|
||||||
|
|
||||||
|
if len(daily_returns) >= 2:
|
||||||
|
avg_ret = sum(daily_returns) / len(daily_returns)
|
||||||
|
std_ret = math.sqrt(sum((r - avg_ret) ** 2 for r in daily_returns) / len(daily_returns))
|
||||||
|
neg_returns = [r for r in daily_returns if r < 0]
|
||||||
|
downside_dev = math.sqrt(sum(r ** 2 for r in neg_returns) / len(neg_returns)) if neg_returns else 0
|
||||||
|
|
||||||
|
sharpe = (avg_ret / std_ret * math.sqrt(252)) if std_ret > 0 else 0
|
||||||
|
sortino = (avg_ret / downside_dev * math.sqrt(252)) if downside_dev > 0 else 0
|
||||||
|
|
||||||
|
# Max drawdown
|
||||||
|
values = [dict(e)["total_value"] for e in equity]
|
||||||
|
peak = values[0] if values else INITIAL_CASH
|
||||||
|
max_dd = 0
|
||||||
|
for v in values:
|
||||||
|
if v > peak: peak = v
|
||||||
|
dd = (peak - v) / peak
|
||||||
|
if dd > max_dd: max_dd = dd
|
||||||
|
|
||||||
|
# Calmar ratio (annualized return / max drawdown)
|
||||||
|
total_return = (values[-1] / values[0] - 1) if values and values[0] > 0 else 0
|
||||||
|
calmar = (total_return / max_dd) if max_dd > 0 else 0
|
||||||
|
else:
|
||||||
|
sharpe = sortino = calmar = 0
|
||||||
|
max_dd = 0
|
||||||
|
|
||||||
|
# Trade analysis
|
||||||
|
pnls = []
|
||||||
|
for t in txns:
|
||||||
|
t = dict(t)
|
||||||
|
if t.get("pnl") is not None:
|
||||||
|
pnls.append(t["pnl"])
|
||||||
|
|
||||||
|
wins = [p for p in pnls if p > 0]
|
||||||
|
losses = [p for p in pnls if p <= 0]
|
||||||
|
total_trades = len(pnls)
|
||||||
|
win_rate = (len(wins) / total_trades * 100) if total_trades > 0 else 0
|
||||||
|
|
||||||
|
gross_profit = sum(wins) if wins else 0
|
||||||
|
gross_loss = abs(sum(losses)) if losses else 0
|
||||||
|
profit_factor = (gross_profit / gross_loss) if gross_loss > 0 else float('inf') if gross_profit > 0 else 0
|
||||||
|
|
||||||
|
# Total commissions and slippage
|
||||||
|
total_commission = sum(dict(t).get("commission", 0) for t in txns)
|
||||||
|
total_slippage = sum(abs(dict(t).get("slippage", 0)) for t in txns)
|
||||||
|
|
||||||
|
summary = get_portfolio_summary(current_prices)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"sharpe_ratio": round(sharpe, 2),
|
||||||
|
"sortino_ratio": round(sortino, 2),
|
||||||
|
"calmar_ratio": round(calmar, 2),
|
||||||
|
"max_drawdown_pct": round(max_dd * 100, 2),
|
||||||
|
"profit_factor": round(profit_factor, 2) if profit_factor != float('inf') else "∞",
|
||||||
|
"win_rate": round(win_rate, 1),
|
||||||
|
"total_trades": total_trades,
|
||||||
|
"winning_trades": len(wins),
|
||||||
|
"losing_trades": len(losses),
|
||||||
|
"avg_win": round(sum(wins) / len(wins), 2) if wins else 0,
|
||||||
|
"avg_loss": round(sum(losses) / len(losses), 2) if losses else 0,
|
||||||
|
"best_trade": round(max(pnls), 2) if pnls else 0,
|
||||||
|
"worst_trade": round(min(pnls), 2) if pnls else 0,
|
||||||
|
"total_return_pct": round(summary["total_pnl_pct"], 2),
|
||||||
|
"total_commission": round(total_commission, 2),
|
||||||
|
"total_slippage": round(total_slippage, 2),
|
||||||
|
"gross_profit": round(gross_profit, 2),
|
||||||
|
"gross_loss": round(gross_loss, 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _empty_analytics():
|
||||||
|
return {
|
||||||
|
"sharpe_ratio": 0, "sortino_ratio": 0, "calmar_ratio": 0,
|
||||||
|
"max_drawdown_pct": 0, "profit_factor": 0,
|
||||||
|
"win_rate": 0, "total_trades": 0,
|
||||||
|
"winning_trades": 0, "losing_trades": 0,
|
||||||
|
"avg_win": 0, "avg_loss": 0,
|
||||||
|
"best_trade": 0, "worst_trade": 0,
|
||||||
|
"total_return_pct": 0, "total_commission": 0, "total_slippage": 0,
|
||||||
|
"gross_profit": 0, "gross_loss": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def save_daily_snapshot(total_value, cash, positions_value):
|
||||||
|
conn = get_db()
|
||||||
|
today = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO portfolio_snapshots (date, total_value, cash, positions_value) VALUES (?, ?, ?, ?)",
|
||||||
|
(today, total_value, cash, positions_value)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def reset_portfolio():
|
||||||
|
"""Reset portfolio to initial state — clears everything."""
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute("DELETE FROM portfolio_meta")
|
||||||
|
conn.execute("INSERT INTO portfolio_meta (id, cash) VALUES (1, ?)", (INITIAL_CASH,))
|
||||||
|
conn.execute("DELETE FROM positions")
|
||||||
|
conn.execute("DELETE FROM transactions")
|
||||||
|
conn.execute("DELETE FROM pending_orders")
|
||||||
|
conn.execute("DELETE FROM equity_curve")
|
||||||
|
conn.execute("DELETE FROM portfolio_snapshots")
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return {"success": True, "message": "Portfolio reset to $100,000"}
|
||||||
|
|
||||||
|
|
||||||
|
def update_settings(slippage_bps=None, commission_per_share=None):
|
||||||
|
"""Update portfolio simulation settings."""
|
||||||
|
conn = get_db()
|
||||||
|
if slippage_bps is not None:
|
||||||
|
conn.execute("UPDATE portfolio_meta SET slippage_bps = ? WHERE id = 1", (slippage_bps,))
|
||||||
|
if commission_per_share is not None:
|
||||||
|
conn.execute("UPDATE portfolio_meta SET commission_per_share = ? WHERE id = 1", (commission_per_share,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return {"success": True}
|
||||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
flask>=3.0
|
||||||
|
yfinance>=1.0
|
||||||
|
financetoolkit>=2.0
|
||||||
|
openpyxl>=3.1
|
||||||
|
requests>=2.31
|
||||||
|
requests-cache>=1.2
|
||||||
|
python-dotenv>=1.0
|
||||||
1649
static/script.js
Normal file
1649
static/script.js
Normal file
File diff suppressed because it is too large
Load Diff
2212
static/style.css
Normal file
2212
static/style.css
Normal file
File diff suppressed because it is too large
Load Diff
205
technical_analyzer.py
Normal file
205
technical_analyzer.py
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
"""
|
||||||
|
Module for Technical Analysis using FinanceToolkit.
|
||||||
|
Calculates RSI, EMA, Volatility, MACD, Bollinger Bands, and Fibonacci Retracement Levels.
|
||||||
|
"""
|
||||||
|
from financetoolkit import Toolkit
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
def get_technicals(ticker, api_key, period="daily"):
|
||||||
|
"""
|
||||||
|
Fetches comprehensive technical indicators for a given ticker.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ticker (str): Stock ticker symbol.
|
||||||
|
api_key (str): FMP API Key.
|
||||||
|
period (str): 'daily', 'weekly', etc.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Latest technical values and signals for 'Pro' analysis.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
companies = Toolkit(
|
||||||
|
tickers=[ticker],
|
||||||
|
api_key=api_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Helper to safely get the latest value for the specific ticker
|
||||||
|
def get_latest_series(df, col_name=None):
|
||||||
|
if df.empty: return 0
|
||||||
|
# multi-index handling: typically (Symbol, Metric) or (Metric, Symbol)
|
||||||
|
if isinstance(df.columns, pd.MultiIndex):
|
||||||
|
# Try selecting ticker from level 1
|
||||||
|
if ticker in df.columns.get_level_values(1):
|
||||||
|
df = df.xs(ticker, level=1, axis=1)
|
||||||
|
# Try selecting ticker from level 0
|
||||||
|
elif ticker in df.columns.get_level_values(0):
|
||||||
|
df = df.xs(ticker, level=0, axis=1)
|
||||||
|
|
||||||
|
# If still DataFrame, try to pick the specific column or just the first
|
||||||
|
if isinstance(df, pd.DataFrame):
|
||||||
|
if col_name and col_name in df.columns:
|
||||||
|
return df[col_name].iloc[-1]
|
||||||
|
return df.iloc[-1, 0] # Default to first column
|
||||||
|
else:
|
||||||
|
return df.iloc[-1]
|
||||||
|
|
||||||
|
def get_value(val):
|
||||||
|
return val.values[0] if hasattr(val, 'values') else val
|
||||||
|
|
||||||
|
# 1. RSI (14 periods) - Good for Overbought/Oversold
|
||||||
|
rsi_df = companies.technicals.get_relative_strength_index(window=14)
|
||||||
|
current_rsi = get_value(get_latest_series(rsi_df))
|
||||||
|
|
||||||
|
# 2. EMA (50 periods) - Trend Filter
|
||||||
|
ema_df = companies.technicals.get_exponential_moving_average(window=50)
|
||||||
|
current_ema = get_value(get_latest_series(ema_df))
|
||||||
|
|
||||||
|
# 2b. EMA (10 periods) - Short Term Trend (Phase 3)
|
||||||
|
ema10_df = companies.technicals.get_exponential_moving_average(window=10)
|
||||||
|
current_ema10 = get_value(get_latest_series(ema10_df))
|
||||||
|
|
||||||
|
# 2c. EMA (5 periods) - Ultra-Short Term (Phase 5 Aggressive)
|
||||||
|
ema5_df = companies.technicals.get_exponential_moving_average(window=5)
|
||||||
|
current_ema5 = get_value(get_latest_series(ema5_df))
|
||||||
|
|
||||||
|
# 3. MACD (12, 26, 9) - Momentum
|
||||||
|
macd_df = companies.technicals.get_moving_average_convergence_divergence(short_window=12, long_window=26, signal_window=9)
|
||||||
|
# MACD usually returns MACD line and Signal line. We need both.
|
||||||
|
# Structure likely: [MACD, Signal, Histogram] columns per ticker
|
||||||
|
# Let's get the full series for the ticker first
|
||||||
|
if isinstance(macd_df.columns, pd.MultiIndex):
|
||||||
|
if ticker in macd_df.columns.get_level_values(1):
|
||||||
|
macd_data = macd_df.xs(ticker, level=1, axis=1)
|
||||||
|
elif ticker in macd_df.columns.get_level_values(0):
|
||||||
|
macd_data = macd_df.xs(ticker, level=0, axis=1)
|
||||||
|
else:
|
||||||
|
macd_data = macd_df
|
||||||
|
else:
|
||||||
|
macd_data = macd_df
|
||||||
|
|
||||||
|
# Assuming columns like 'MACD', 'Signal', 'Histogram' or similar exist
|
||||||
|
# If columns are just numbered or named differently, we take last row
|
||||||
|
if 'MACD' in macd_data.columns and 'Signal' in macd_data.columns:
|
||||||
|
current_macd = macd_data['MACD'].iloc[-1]
|
||||||
|
current_signal_line = macd_data['Signal'].iloc[-1]
|
||||||
|
else:
|
||||||
|
# Fallback: assume col 0 is MACD, col 1 is Signal (standard toolkit output order often)
|
||||||
|
current_macd = macd_data.iloc[-1, 0]
|
||||||
|
current_signal_line = macd_data.iloc[-1, 1] if macd_data.shape[1] > 1 else 0
|
||||||
|
|
||||||
|
# 4. Bollinger Bands (20, 2) - Volatility & Mean Reversion
|
||||||
|
bb_df = companies.technicals.get_bollinger_bands(window=20, num_std_dev=2)
|
||||||
|
# Usually returns: Upper, Lower, (maybe Middle)
|
||||||
|
if isinstance(bb_df.columns, pd.MultiIndex):
|
||||||
|
if ticker in bb_df.columns.get_level_values(1):
|
||||||
|
bb_data = bb_df.xs(ticker, level=1, axis=1)
|
||||||
|
elif ticker in bb_df.columns.get_level_values(0):
|
||||||
|
bb_data = bb_df.xs(ticker, level=0, axis=1)
|
||||||
|
else:
|
||||||
|
bb_data = bb_df
|
||||||
|
else:
|
||||||
|
bb_data = bb_df
|
||||||
|
|
||||||
|
# Inspect columns for Upper/Lower
|
||||||
|
# Typ. 'Bollinger High', 'Bollinger Low' or similar
|
||||||
|
# Fallback to column indices if names vary
|
||||||
|
if bb_data.shape[1] >= 2:
|
||||||
|
bb_upper = bb_data.iloc[-1, 0] # Often Upper is first or checking names
|
||||||
|
bb_lower = bb_data.iloc[-1, 1]
|
||||||
|
# Try to be more specific if possible, but for now robust fallback
|
||||||
|
# Let's look for 'Upper' or 'High' in columns
|
||||||
|
for col in bb_data.columns:
|
||||||
|
if 'Upper' in str(col) or 'High' in str(col): bb_upper = bb_data[col].iloc[-1]
|
||||||
|
if 'Lower' in str(col) or 'Low' in str(col): bb_lower = bb_data[col].iloc[-1]
|
||||||
|
else:
|
||||||
|
bb_upper = 0
|
||||||
|
bb_lower = 0
|
||||||
|
|
||||||
|
# 5. Volatility & Fibonacci & NEW EXPERT METRICS (SMA, Volume, ATR)
|
||||||
|
hist_data = companies.get_historical_data()
|
||||||
|
|
||||||
|
atr = 0
|
||||||
|
rvol = 0
|
||||||
|
sma_200 = 0
|
||||||
|
sma_50 = 0
|
||||||
|
|
||||||
|
if not hist_data.empty:
|
||||||
|
# Handle MultiIndex
|
||||||
|
if isinstance(hist_data.columns, pd.MultiIndex):
|
||||||
|
try:
|
||||||
|
if ticker in hist_data.columns.get_level_values(1):
|
||||||
|
hist_data = hist_data.xs(ticker, level=1, axis=1)
|
||||||
|
elif ticker in hist_data.columns.get_level_values(0):
|
||||||
|
hist_data = hist_data.xs(ticker, level=0, axis=1)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
daily_returns = hist_data['Return'].iloc[-20:]
|
||||||
|
volatility = daily_returns.std() * np.sqrt(252) * 100
|
||||||
|
current_close = hist_data['Close'].iloc[-1]
|
||||||
|
if isinstance(current_close, pd.Series): current_close = current_close.iloc[0]
|
||||||
|
|
||||||
|
# --- EXPERT METRICS ---
|
||||||
|
# SMA 50 & 200
|
||||||
|
sma_50 = hist_data['Close'].rolling(window=50).mean().iloc[-1]
|
||||||
|
sma_200 = hist_data['Close'].rolling(window=200).mean().iloc[-1]
|
||||||
|
|
||||||
|
# Volume Relative Strength (RVOL)
|
||||||
|
# Current Volume / Avg Volume 20
|
||||||
|
curr_vol = hist_data['Volume'].iloc[-1]
|
||||||
|
avg_vol_20 = hist_data['Volume'].rolling(window=20).mean().iloc[-1]
|
||||||
|
rvol = curr_vol / avg_vol_20 if avg_vol_20 > 0 else 0
|
||||||
|
|
||||||
|
# ATR (14)
|
||||||
|
high = hist_data['High']
|
||||||
|
low = hist_data['Low']
|
||||||
|
close = hist_data['Close']
|
||||||
|
tr1 = high - low
|
||||||
|
tr2 = (high - close.shift()).abs()
|
||||||
|
tr3 = (low - close.shift()).abs()
|
||||||
|
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
|
||||||
|
atr = tr.rolling(window=14).mean().iloc[-1]
|
||||||
|
|
||||||
|
# Fibonacci
|
||||||
|
last_year = hist_data.iloc[-252:]
|
||||||
|
high_price = last_year['High'].max()
|
||||||
|
low_price = last_year['Low'].min()
|
||||||
|
if isinstance(high_price, pd.Series): high_price = high_price.iloc[0]
|
||||||
|
if isinstance(low_price, pd.Series): low_price = low_price.iloc[0]
|
||||||
|
|
||||||
|
diff = high_price - low_price
|
||||||
|
fib_levels = {
|
||||||
|
'0.0%': high_price,
|
||||||
|
'23.6%': high_price - 0.236 * diff,
|
||||||
|
'38.2%': high_price - 0.382 * diff,
|
||||||
|
'50.0%': high_price - 0.5 * diff,
|
||||||
|
'61.8%': high_price - 0.618 * diff,
|
||||||
|
'100.0%': low_price
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
volatility = 0
|
||||||
|
current_close = 0
|
||||||
|
fib_levels = {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'symbol': ticker,
|
||||||
|
'price': current_close,
|
||||||
|
'rsi': current_rsi,
|
||||||
|
'ema_50': current_ema,
|
||||||
|
'ema_10': current_ema10,
|
||||||
|
'ema_5': current_ema5,
|
||||||
|
'sma_50': sma_50,
|
||||||
|
'sma_200': sma_200,
|
||||||
|
'rvol': rvol,
|
||||||
|
'atr': atr,
|
||||||
|
'macd_line': current_macd,
|
||||||
|
'macd_signal': current_signal_line,
|
||||||
|
'bb_upper': bb_upper,
|
||||||
|
'bb_lower': bb_lower,
|
||||||
|
'volatility_annual_pct': volatility.values[0] if hasattr(volatility, 'values') else volatility,
|
||||||
|
'fibonacci_levels': fib_levels
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching technicals for {ticker}: {e}")
|
||||||
|
return None
|
||||||
890
templates/index.html
Normal file
890
templates/index.html
Normal file
@ -0,0 +1,890 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="dark">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>FinanceIQ — Multi-Asset Analytics Platform</title>
|
||||||
|
<meta name="description"
|
||||||
|
content="Professional multi-asset analytics: stocks, futures, options, currencies. DCF, Monte Carlo, AI insights, and live charting.">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
<script src="https://unpkg.com/lightweight-charts@4.2.1/dist/lightweight-charts.standalone.production.js"></script>
|
||||||
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- WELCOME PAGE -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<div class="welcome-page" id="welcomePage">
|
||||||
|
<div class="welcome-bg"></div>
|
||||||
|
<div class="welcome-content">
|
||||||
|
<div class="welcome-logo">
|
||||||
|
<div class="welcome-icon-wrap">
|
||||||
|
<i data-lucide="bar-chart-3" class="welcome-lucide-icon"></i>
|
||||||
|
</div>
|
||||||
|
<h1 class="welcome-title">FinanceIQ</h1>
|
||||||
|
<span class="badge-version">v5.2</span>
|
||||||
|
</div>
|
||||||
|
<p class="welcome-subtitle">Multi-Asset Intelligence Platform</p>
|
||||||
|
<p class="welcome-desc">
|
||||||
|
Stocks · Futures · Options · Currencies<br>
|
||||||
|
AI-powered analysis, live charts, DCF valuation, Monte Carlo forecasting, and institutional-grade
|
||||||
|
analytics.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="welcome-features">
|
||||||
|
<div class="wf-card">
|
||||||
|
<i data-lucide="candlestick-chart" class="wf-lucide"></i>
|
||||||
|
<h3>Live Charts</h3>
|
||||||
|
<p>Real-time candlestick charts with EMA, SMA, Bollinger, and drawing tools.</p>
|
||||||
|
</div>
|
||||||
|
<div class="wf-card">
|
||||||
|
<i data-lucide="calculator" class="wf-lucide"></i>
|
||||||
|
<h3>DCF Valuation</h3>
|
||||||
|
<p>Intrinsic value via discounted cash flows, WACC, and terminal growth.</p>
|
||||||
|
</div>
|
||||||
|
<div class="wf-card">
|
||||||
|
<i data-lucide="brain" class="wf-lucide"></i>
|
||||||
|
<h3>AI Analyst</h3>
|
||||||
|
<p>LLM-powered research synthesizing technicals, fundamentals, and sentiment.</p>
|
||||||
|
</div>
|
||||||
|
<div class="wf-card">
|
||||||
|
<i data-lucide="trending-up" class="wf-lucide"></i>
|
||||||
|
<h3>Monte Carlo</h3>
|
||||||
|
<p>1,000-path probabilistic simulation with percentile bands.</p>
|
||||||
|
</div>
|
||||||
|
<div class="wf-card">
|
||||||
|
<i data-lucide="target" class="wf-lucide"></i>
|
||||||
|
<h3>Analyst Consensus</h3>
|
||||||
|
<p>Live Wall Street ratings, price targets, and insider activity.</p>
|
||||||
|
</div>
|
||||||
|
<div class="wf-card">
|
||||||
|
<i data-lucide="globe" class="wf-lucide"></i>
|
||||||
|
<h3>Macro Intelligence</h3>
|
||||||
|
<p>FRED indicators, sector heatmaps, correlation and peer analysis.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="welcome-cta" id="letsBeginBtn">
|
||||||
|
<i data-lucide="arrow-right" style="width:20px;height:20px;"></i>
|
||||||
|
<span>Get Started</span>
|
||||||
|
</button>
|
||||||
|
<p class="welcome-footer">FMP · Finnhub · FRED · Alpha Vantage · Ollama AI</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<!-- MAIN APP -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════ -->
|
||||||
|
<div class="app-layout" id="mainApp" style="display:none;">
|
||||||
|
|
||||||
|
<!-- ── SIDEBAR ─────────────────────────────────── -->
|
||||||
|
<aside class="sidebar" id="sidebar">
|
||||||
|
<div class="sidebar-brand">
|
||||||
|
<i data-lucide="bar-chart-3" class="brand-icon"></i>
|
||||||
|
<span class="brand-text">FinanceIQ</span>
|
||||||
|
<span class="badge-version badge-sm">v5.2</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<div class="nav-group-label">Analysis</div>
|
||||||
|
<button class="nav-item active" data-tab="overview" data-asset="stocks,futures,options,currencies">
|
||||||
|
<i data-lucide="layout-dashboard"></i>
|
||||||
|
<span>Overview</span>
|
||||||
|
</button>
|
||||||
|
<button class="nav-item" data-tab="technical" data-asset="stocks,futures,currencies">
|
||||||
|
<i data-lucide="activity"></i>
|
||||||
|
<span>Technical</span>
|
||||||
|
</button>
|
||||||
|
<button class="nav-item" data-tab="fundamentals" data-asset="stocks">
|
||||||
|
<i data-lucide="landmark"></i>
|
||||||
|
<span>Fundamentals</span>
|
||||||
|
</button>
|
||||||
|
<button class="nav-item" data-tab="sentiment" data-asset="stocks,futures,options,currencies">
|
||||||
|
<i data-lucide="heart-pulse"></i>
|
||||||
|
<span>Sentiment</span>
|
||||||
|
</button>
|
||||||
|
<button class="nav-item" data-tab="options" data-asset="options">
|
||||||
|
<i data-lucide="layers"></i>
|
||||||
|
<span>Options</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="nav-group-label">Research</div>
|
||||||
|
<button class="nav-item" data-tab="intelligence" data-asset="stocks,futures,options,currencies">
|
||||||
|
<i data-lucide="sparkles"></i>
|
||||||
|
<span>Intelligence</span>
|
||||||
|
</button>
|
||||||
|
<button class="nav-item" data-tab="macro" data-asset="stocks,futures">
|
||||||
|
<i data-lucide="globe-2"></i>
|
||||||
|
<span>Macro</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="nav-group-label">Trading</div>
|
||||||
|
<button class="nav-item" data-tab="portfolio" data-asset="stocks,futures,options,currencies">
|
||||||
|
<i data-lucide="briefcase"></i>
|
||||||
|
<span>Portfolio</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="sidebar-bottom">
|
||||||
|
<button class="nav-item" id="themeToggle">
|
||||||
|
<i data-lucide="moon" id="themeIcon"></i>
|
||||||
|
<span id="themeLabel">Dark Mode</span>
|
||||||
|
</button>
|
||||||
|
<button class="nav-item" id="exportBtnSidebar" style="display:none;">
|
||||||
|
<i data-lucide="download"></i>
|
||||||
|
<span>Export Excel</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- ── MAIN CONTENT ────────────────────────────── -->
|
||||||
|
<main class="main-content">
|
||||||
|
|
||||||
|
<!-- Top Bar -->
|
||||||
|
<header class="topbar">
|
||||||
|
<button class="sidebar-toggle" id="sidebarToggle">
|
||||||
|
<i data-lucide="menu"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="search-container">
|
||||||
|
<!-- Asset Type Selector -->
|
||||||
|
<div class="asset-selector">
|
||||||
|
<button class="asset-btn active" data-asset="stocks">
|
||||||
|
<i data-lucide="bar-chart-2"></i> Stocks
|
||||||
|
</button>
|
||||||
|
<button class="asset-btn" data-asset="futures">
|
||||||
|
<i data-lucide="timer"></i> Futures
|
||||||
|
</button>
|
||||||
|
<button class="asset-btn" data-asset="options">
|
||||||
|
<i data-lucide="layers"></i> Options
|
||||||
|
</button>
|
||||||
|
<button class="asset-btn" data-asset="currencies">
|
||||||
|
<i data-lucide="coins"></i> Currencies
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Input -->
|
||||||
|
<div class="search-input-group">
|
||||||
|
<i data-lucide="search" class="search-icon"></i>
|
||||||
|
<div class="ticker-wrapper">
|
||||||
|
<input type="text" id="tickerInput" placeholder="Search ticker (e.g. AAPL, TSLA, NIFTY)"
|
||||||
|
autocomplete="off">
|
||||||
|
<div class="dropdown" id="dropdown"></div>
|
||||||
|
</div>
|
||||||
|
<select id="timeframeSelect" class="select-control">
|
||||||
|
<option value="1Y">1 Year</option>
|
||||||
|
<option value="6M">6 Months</option>
|
||||||
|
<option value="3M">3 Months</option>
|
||||||
|
<option value="1M">1 Month</option>
|
||||||
|
<option value="5Y">5 Years</option>
|
||||||
|
<option value="custom">Custom</option>
|
||||||
|
</select>
|
||||||
|
<select id="periodSelect" class="select-control">
|
||||||
|
<option value="yearly">Annual</option>
|
||||||
|
<option value="quarterly">Quarterly</option>
|
||||||
|
</select>
|
||||||
|
<select id="currencySelect" class="select-control">
|
||||||
|
<option value="GBP">£ GBP</option>
|
||||||
|
<option value="USD">$ USD</option>
|
||||||
|
<option value="INR">₹ INR</option>
|
||||||
|
</select>
|
||||||
|
<button id="analyzeBtn" class="btn-primary">
|
||||||
|
<i data-lucide="zap" style="width:16px;height:16px;"></i>
|
||||||
|
Analyze
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="custom-dates" id="customDates" style="display:none;">
|
||||||
|
<label>From <input type="date" id="startDate"></label>
|
||||||
|
<label>To <input type="date" id="endDate"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Loader -->
|
||||||
|
<div class="loader-overlay" id="loader" style="display:none">
|
||||||
|
<div class="loader-spinner"></div>
|
||||||
|
<p class="loader-text" id="loaderText">Analyzing...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<!-- TAB: OVERVIEW -->
|
||||||
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<div class="tab-page active" id="page-overview">
|
||||||
|
<!-- ── Market Dashboard ── -->
|
||||||
|
<div class="market-dashboard" id="marketDashboard">
|
||||||
|
<div class="dashboard-header">
|
||||||
|
<h2 class="dashboard-title">
|
||||||
|
<i data-lucide="globe" style="width:22px;height:22px;"></i>
|
||||||
|
Market Pulse
|
||||||
|
<span class="live-dot"></span>
|
||||||
|
</h2>
|
||||||
|
<span class="dashboard-subtitle">Live global market snapshot · Auto-refreshes every 60s</span>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-section">
|
||||||
|
<h3 class="dash-section-title"><i data-lucide="trending-up" style="width:16px;height:16px;"></i>
|
||||||
|
Global Indices</h3>
|
||||||
|
<div class="market-grid" id="dashIndices">
|
||||||
|
<div class="market-card skeleton-card">
|
||||||
|
<div class="skeleton-line"></div>
|
||||||
|
<div class="skeleton-line short"></div>
|
||||||
|
</div>
|
||||||
|
<div class="market-card skeleton-card">
|
||||||
|
<div class="skeleton-line"></div>
|
||||||
|
<div class="skeleton-line short"></div>
|
||||||
|
</div>
|
||||||
|
<div class="market-card skeleton-card">
|
||||||
|
<div class="skeleton-line"></div>
|
||||||
|
<div class="skeleton-line short"></div>
|
||||||
|
</div>
|
||||||
|
<div class="market-card skeleton-card">
|
||||||
|
<div class="skeleton-line"></div>
|
||||||
|
<div class="skeleton-line short"></div>
|
||||||
|
</div>
|
||||||
|
<div class="market-card skeleton-card">
|
||||||
|
<div class="skeleton-line"></div>
|
||||||
|
<div class="skeleton-line short"></div>
|
||||||
|
</div>
|
||||||
|
<div class="market-card skeleton-card">
|
||||||
|
<div class="skeleton-line"></div>
|
||||||
|
<div class="skeleton-line short"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-section">
|
||||||
|
<h3 class="dash-section-title"><i data-lucide="gem" style="width:16px;height:16px;"></i>
|
||||||
|
Commodities</h3>
|
||||||
|
<div class="market-grid" id="dashCommodities">
|
||||||
|
<div class="market-card skeleton-card">
|
||||||
|
<div class="skeleton-line"></div>
|
||||||
|
<div class="skeleton-line short"></div>
|
||||||
|
</div>
|
||||||
|
<div class="market-card skeleton-card">
|
||||||
|
<div class="skeleton-line"></div>
|
||||||
|
<div class="skeleton-line short"></div>
|
||||||
|
</div>
|
||||||
|
<div class="market-card skeleton-card">
|
||||||
|
<div class="skeleton-line"></div>
|
||||||
|
<div class="skeleton-line short"></div>
|
||||||
|
</div>
|
||||||
|
<div class="market-card skeleton-card">
|
||||||
|
<div class="skeleton-line"></div>
|
||||||
|
<div class="skeleton-line short"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-section">
|
||||||
|
<h3 class="dash-section-title"><i data-lucide="coins" style="width:16px;height:16px;"></i>
|
||||||
|
Currency Pairs</h3>
|
||||||
|
<div class="market-grid" id="dashCurrencies">
|
||||||
|
<div class="market-card skeleton-card">
|
||||||
|
<div class="skeleton-line"></div>
|
||||||
|
<div class="skeleton-line short"></div>
|
||||||
|
</div>
|
||||||
|
<div class="market-card skeleton-card">
|
||||||
|
<div class="skeleton-line"></div>
|
||||||
|
<div class="skeleton-line short"></div>
|
||||||
|
</div>
|
||||||
|
<div class="market-card skeleton-card">
|
||||||
|
<div class="skeleton-line"></div>
|
||||||
|
<div class="skeleton-line short"></div>
|
||||||
|
</div>
|
||||||
|
<div class="market-card skeleton-card">
|
||||||
|
<div class="skeleton-line"></div>
|
||||||
|
<div class="skeleton-line short"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="results">
|
||||||
|
<div class="card card-full">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="candlestick-chart"></i>
|
||||||
|
Interactive Price Chart
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="Candlestick chart with volume. Toggle overlays below to add EMA, SMA, Bollinger Bands, and Fibonacci levels.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="chart-controls" id="chartControls" style="display:none">
|
||||||
|
<label class="chart-toggle"><input type="checkbox" id="togEma5"> EMA 5</label>
|
||||||
|
<label class="chart-toggle"><input type="checkbox" id="togEma10"> EMA 10</label>
|
||||||
|
<label class="chart-toggle"><input type="checkbox" id="togEma20"> EMA 20</label>
|
||||||
|
<label class="chart-toggle"><input type="checkbox" id="togSma200"> SMA 200</label>
|
||||||
|
<label class="chart-toggle"><input type="checkbox" id="togBb"> Bollinger</label>
|
||||||
|
<label class="chart-toggle"><input type="checkbox" id="togFib"> Fibonacci</label>
|
||||||
|
<div class="custom-ema">
|
||||||
|
<input type="number" id="customEmaPeriod" placeholder="Custom EMA" min="2" max="500">
|
||||||
|
<button class="btn-sm" id="addCustomEma">Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="chartContainer"
|
||||||
|
style="width:100%;height:420px;border-radius:var(--radius);overflow:hidden;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="gauge"></i>
|
||||||
|
Key Metrics
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="Essential price and valuation metrics. RSI above 70 = overbought, below 30 = oversold.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="data-grid" id="keyMetrics"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="zap"></i>
|
||||||
|
Quick Signal
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="Algorithmic signal based on RSI, MACD, and moving average crossovers.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="quickSignal"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<!-- TAB: TECHNICAL -->
|
||||||
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<div class="tab-page" id="page-technical">
|
||||||
|
<div class="results">
|
||||||
|
<div class="card card-full">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="activity"></i>
|
||||||
|
Technical Indicators
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="RSI (14-period), MACD trend direction, Bollinger Bands volatility measure.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="data-grid" id="technicalIndicators"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card card-full">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="scan-line"></i>
|
||||||
|
Candlestick Patterns — Last 7 Days
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="Patterns detected in the last 7 trading days with forward-looking predictions.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="patternOutlook" class="pattern-outlook-box"></div>
|
||||||
|
<div id="candlestickPatterns" class="patterns-grid"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card card-full">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="calendar-check"></i>
|
||||||
|
Earnings History (EPS)
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="Quarterly EPS — Actual vs Estimate. Consistent beats are bullish.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="earningsTable" class="table-container"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<!-- TAB: FUNDAMENTALS -->
|
||||||
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<div class="tab-page" id="page-fundamentals">
|
||||||
|
<div class="results">
|
||||||
|
<div class="card card-full">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="landmark"></i>
|
||||||
|
DCF Intrinsic Valuation
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="DCF projects future FCF, discounts by WACC, and derives intrinsic value. Intrinsic > price = undervalued.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="dcfValuation"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="shield-check"></i>
|
||||||
|
Altman Z-Score
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="Bankruptcy risk. Z > 2.99 = Safe, 1.81-2.99 = Grey Zone, < 1.81 = Distress.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="zScore"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="banknote"></i>
|
||||||
|
Dividend Analysis
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="Dividend yield, payout ratio, and history. Payout > 80% may be unsustainable.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="dividendAnalysis"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="file-text"></i>
|
||||||
|
Income Statement
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="Revenue, operating income, and net income from the latest filings.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="data-grid" id="incomeStatement"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="pie-chart"></i>
|
||||||
|
Ratios & Valuation
|
||||||
|
</div>
|
||||||
|
<span class="info-icon" data-tooltip="P/E, EV/EBITDA, ROE and other valuation multiples.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="data-grid" id="ratiosGrid"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="wallet"></i>
|
||||||
|
Free Cash Flow
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="Cash available after CapEx. Strong FCF = flexibility for dividends and buybacks.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="data-grid" id="fcffGrid"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<!-- TAB: SENTIMENT -->
|
||||||
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<div class="tab-page" id="page-sentiment">
|
||||||
|
<div class="results">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="heart-pulse"></i>
|
||||||
|
Fear & Greed Index
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="CNN Fear & Greed (0-100). Extreme Fear (<25) = buying opportunity.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="fearGreedGauge"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="triangle-alert"></i>
|
||||||
|
VIX Volatility Index
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="VIX < 15 = calm, 15-25 = normal, 25-35 = elevated, > 35 = panic.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="vixDisplay"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card card-full">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="newspaper"></i>
|
||||||
|
News Sentiment (VADER)
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="Headlines analyzed with VADER. Compound score -1 to +1.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="newsSection"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<!-- TAB: INTELLIGENCE -->
|
||||||
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<div class="tab-page" id="page-intelligence">
|
||||||
|
<div class="results">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="target"></i>
|
||||||
|
Analyst Ratings & Price Target
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="Wall Street consensus from Finnhub. Buy vs Hold vs Sell counts and price target range.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="analystRatings"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="eye"></i>
|
||||||
|
Insider Trading Activity
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="Recent SEC insider filings. Insider buying is often bullish.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="insiderTrading"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card card-full">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="trending-up"></i>
|
||||||
|
Monte Carlo Price Forecast (60-Day)
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="1,000 random simulations based on historical volatility. Toggle percentile bands below.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mc-band-controls" id="mcBandControls">
|
||||||
|
<label class="chart-toggle"><input type="checkbox" id="togP90" checked> P90 (Bull)</label>
|
||||||
|
<label class="chart-toggle"><input type="checkbox" id="togP75" checked> P75</label>
|
||||||
|
<label class="chart-toggle"><input type="checkbox" id="togP50" checked> P50
|
||||||
|
(Median)</label>
|
||||||
|
<label class="chart-toggle"><input type="checkbox" id="togP25" checked> P25</label>
|
||||||
|
<label class="chart-toggle"><input type="checkbox" id="togP10" checked> P10 (Bear)</label>
|
||||||
|
</div>
|
||||||
|
<div id="monteCarloChart"
|
||||||
|
style="width:100%;height:340px;border-radius:var(--radius);overflow:hidden;"></div>
|
||||||
|
<div id="monteCarloStats" class="mc-stats"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card card-full card-ai">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="sparkles"></i>
|
||||||
|
AI Analyst
|
||||||
|
<span class="badge-ai">Ollama</span>
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="Local AI analysis via Ollama (Llama 3.1). Synthesizes all data for Buy/Hold/Sell.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="ai-content" id="aiContent">
|
||||||
|
<p class="ai-placeholder">Run analysis first, then AI commentary will appear here.</p>
|
||||||
|
</div>
|
||||||
|
<div class="ai-actions">
|
||||||
|
<input class="ai-input" id="aiInput" placeholder="Ask a follow-up question...">
|
||||||
|
<button class="btn-primary btn-sm" id="askAi">
|
||||||
|
<i data-lucide="send" style="width:14px;height:14px;"></i> Ask
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<!-- TAB: MACRO -->
|
||||||
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<div class="tab-page" id="page-macro">
|
||||||
|
<div class="results">
|
||||||
|
<div class="card card-full">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="landmark"></i>
|
||||||
|
Economic Indicators (FRED)
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="Live macro data from the Federal Reserve. Hover to see impact.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="data-grid" id="macroIndicators"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card card-full">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="grid-3x3"></i>
|
||||||
|
Sector Performance Heatmap
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="S&P 500 sector performance. Green = outperforming, Red = underperforming.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="heatmap-controls" id="heatmapControls"></div>
|
||||||
|
<div id="sectorHeatmap" class="heatmap-grid"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card card-full">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="link"></i>
|
||||||
|
Correlation Matrix
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="Daily return correlation with benchmarks (SPY, QQQ, DIA, Gold, Bonds, VIX).">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="correlationMatrix" class="corr-matrix"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card card-full">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="building-2"></i>
|
||||||
|
Competitor Analysis
|
||||||
|
</div>
|
||||||
|
<span class="info-icon"
|
||||||
|
data-tooltip="Compares key ratios with sector peers. Analyzed stock is highlighted.">
|
||||||
|
<i data-lucide="info" style="width:14px;height:14px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="compSection" class="comp-container"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<!-- TAB: OPTIONS -->
|
||||||
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<div class="tab-page" id="page-options">
|
||||||
|
<div class="results">
|
||||||
|
<div class="card card-full">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="layers"></i>
|
||||||
|
Options Chain
|
||||||
|
</div>
|
||||||
|
<div class="options-controls">
|
||||||
|
<select id="optExpiry" class="select-control"></select>
|
||||||
|
<button class="btn-sm btn-primary" id="loadChainBtn">Load Chain</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="options-toggle">
|
||||||
|
<button class="heatmap-btn active" data-chain="calls">Calls</button>
|
||||||
|
<button class="heatmap-btn" data-chain="puts">Puts</button>
|
||||||
|
</div>
|
||||||
|
<div id="optionsChainTable" class="table-container"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card card-full">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="trending-up"></i>
|
||||||
|
Options Payoff Diagram
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="payoff-controls">
|
||||||
|
<label>Strike: <input type="number" id="payoffStrike" class="input-sm" value="100"></label>
|
||||||
|
<label>Premium: <input type="number" id="payoffPremium" class="input-sm" value="5"
|
||||||
|
step="0.5"></label>
|
||||||
|
<select id="payoffType" class="select-control">
|
||||||
|
<option value="call">Call</option>
|
||||||
|
<option value="put">Put</option>
|
||||||
|
</select>
|
||||||
|
<select id="payoffDirection" class="select-control">
|
||||||
|
<option value="long">Long</option>
|
||||||
|
<option value="short">Short</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn-sm btn-primary" id="drawPayoffBtn">Draw</button>
|
||||||
|
</div>
|
||||||
|
<div id="payoffChart" style="width:100%;height:300px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<!-- TAB: PORTFOLIO -->
|
||||||
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<div class="tab-page" id="page-portfolio">
|
||||||
|
<div class="results">
|
||||||
|
<!-- Portfolio Summary -->
|
||||||
|
<div class="card card-full">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="briefcase"></i>
|
||||||
|
Portfolio Summary
|
||||||
|
<span class="live-dot"></span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:8px;align-items:center;">
|
||||||
|
<span class="badge-sm" id="pfSlippageLabel" title="Slippage">Slip: 5 bps</span>
|
||||||
|
<span class="badge-sm" id="pfCommLabel" title="Commission">Comm: $0.005/sh</span>
|
||||||
|
<button class="btn-sm btn-danger" id="resetPortfolioBtn" title="Reset to $100K">
|
||||||
|
<i data-lucide="rotate-ccw" style="width:14px;height:14px;"></i> Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="portfolio-summary-grid" id="portfolioSummary">
|
||||||
|
<div class="data-item">
|
||||||
|
<div class="data-label">Total Value</div>
|
||||||
|
<div class="data-value" id="pfTotalValue">$100,000.00</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-item">
|
||||||
|
<div class="data-label">Cash</div>
|
||||||
|
<div class="data-value" id="pfCash">$100,000.00</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-item">
|
||||||
|
<div class="data-label">Buying Power</div>
|
||||||
|
<div class="data-value" id="pfBuyingPower">$100,000.00</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-item">
|
||||||
|
<div class="data-label">Positions</div>
|
||||||
|
<div class="data-value" id="pfPositionsValue">$0.00</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-item">
|
||||||
|
<div class="data-label">Margin Used</div>
|
||||||
|
<div class="data-value" id="pfMarginUsed">$0.00</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-item">
|
||||||
|
<div class="data-label">P&L</div>
|
||||||
|
<div class="data-value" id="pfPnL">$0.00</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-item">
|
||||||
|
<div class="data-label">Return</div>
|
||||||
|
<div class="data-value" id="pfReturnPct">0.00%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Equity Curve -->
|
||||||
|
<div class="card card-full">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="line-chart"></i>
|
||||||
|
Equity Curve
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="equityCurveContainer"
|
||||||
|
style="width:100%;height:260px;border-radius:var(--radius);overflow:hidden;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Trade Form -->
|
||||||
|
<div class="card card-full">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="arrow-left-right"></i>
|
||||||
|
Trade Terminal
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="trade-form">
|
||||||
|
<div class="trade-search-wrap">
|
||||||
|
<input type="text" id="tradeTicker" placeholder="Search ticker..." class="input-sm"
|
||||||
|
autocomplete="off">
|
||||||
|
<div class="trade-dropdown" id="tradeDropdown"></div>
|
||||||
|
</div>
|
||||||
|
<input type="number" id="tradeShares" placeholder="Qty" min="0.01" step="0.01"
|
||||||
|
class="input-sm" style="width:90px;">
|
||||||
|
<select id="tradeOrderType" class="input-sm" style="width:110px;">
|
||||||
|
<option value="market">Market</option>
|
||||||
|
<option value="limit">Limit</option>
|
||||||
|
<option value="stop">Stop</option>
|
||||||
|
</select>
|
||||||
|
<input type="number" id="tradeLimitPrice" placeholder="Price" step="0.01" class="input-sm"
|
||||||
|
style="width:100px;display:none;">
|
||||||
|
<select id="tradeAssetType" class="input-sm" style="width:100px;">
|
||||||
|
<option value="stock">Stock</option>
|
||||||
|
<option value="futures">Futures</option>
|
||||||
|
<option value="currency">Currency</option>
|
||||||
|
</select>
|
||||||
|
<div class="trade-btn-group">
|
||||||
|
<button class="btn-trade btn-buy" id="buyBtn">Buy Long</button>
|
||||||
|
<button class="btn-trade btn-sell" id="sellBtn">Sell</button>
|
||||||
|
<button class="btn-trade btn-short" id="shortBtn">Short</button>
|
||||||
|
<button class="btn-trade btn-cover" id="coverBtn">Cover</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="tradeResult" class="trade-result"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pending Orders -->
|
||||||
|
<div class="card card-full">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="clock"></i>
|
||||||
|
Pending Orders
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="pendingOrdersTable" class="table-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Positions Table -->
|
||||||
|
<div class="card card-full">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="list"></i>
|
||||||
|
Positions
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="positionsTable" class="table-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Analytics -->
|
||||||
|
<div class="card card-full">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="bar-chart-3"></i>
|
||||||
|
Performance Analytics
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="portfolio-analytics-grid" id="portfolioAnalytics"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transaction History -->
|
||||||
|
<div class="card card-full">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<i data-lucide="scroll-text"></i>
|
||||||
|
Transaction History
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="transactionHistory" class="table-container"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="footer">
|
||||||
|
<p>FinanceIQ v5.2 · FMP · Finnhub · FRED · Alpha Vantage · Ollama AI · Paper Trading · Educational
|
||||||
|
purposes only</p>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/static/script.js"></script>
|
||||||
|
<script>lucide.createIcons();</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
19
test_intraday.py
Normal file
19
test_intraday.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from financetoolkit import Toolkit
|
||||||
|
|
||||||
|
API_KEY = "wybWEsp1oB9abHfz3yPpQYwffxaN21B7"
|
||||||
|
TICKER = "NVDA"
|
||||||
|
|
||||||
|
try:
|
||||||
|
companies = Toolkit(tickers=[TICKER], api_key=API_KEY)
|
||||||
|
# Try fetching intraday
|
||||||
|
print("Attempting to fetch 1min data...")
|
||||||
|
# Note: verify correct method for intraday in financetoolkit
|
||||||
|
# Usually get_intraday_data or similar, or interval argument in get_historical_data
|
||||||
|
# Looking at docs/usage from memory or previous files:
|
||||||
|
# get_historical_data(period="daily") was used.
|
||||||
|
# Let's try interval='1m' if supported.
|
||||||
|
intra = companies.get_historical_data(period="1m") # Guessing '1m' is period
|
||||||
|
print(intra.head())
|
||||||
|
print("Success!")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed: {e}")
|
||||||
27
test_news.py
Normal file
27
test_news.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import feedparser
|
||||||
|
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test Feed: Google News for AAPL
|
||||||
|
rss_url = "https://news.google.com/rss/search?q=AAPL&hl=en-US&gl=US&ceid=US:en"
|
||||||
|
print(f"Fetching RSS from: {rss_url}")
|
||||||
|
|
||||||
|
feed = feedparser.parse(rss_url)
|
||||||
|
|
||||||
|
# feedparser doesn't always return .status for local files or some responses, but usually does for HTTP
|
||||||
|
# We check if entries are present
|
||||||
|
print(f"Entries found: {len(feed.entries)}")
|
||||||
|
|
||||||
|
if feed.entries:
|
||||||
|
title = feed.entries[0].title
|
||||||
|
print(f"Sample Title: {title}")
|
||||||
|
|
||||||
|
analyzer = SentimentIntensityAnalyzer()
|
||||||
|
score = analyzer.polarity_scores(title)
|
||||||
|
print(f"Sentiment: {score}")
|
||||||
|
print("✅ News + Sentiment Test PASSED")
|
||||||
|
else:
|
||||||
|
print("❌ No entries found in RSS feed")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Test FAILED with error: {e}")
|
||||||
Loading…
x
Reference in New Issue
Block a user