├── historify ├── app │ ├── routes │ │ ├── __init__.py │ │ ├── main.py │ │ ├── watchlist.py │ │ ├── scheduler.py │ │ ├── settings.py │ │ └── charts.py │ ├── utils │ │ ├── __init__.py │ │ ├── data_fetcher_chunked.py │ │ ├── rate_limiter.py │ │ └── auth.py │ ├── static │ │ ├── image │ │ │ ├── historify.png │ │ │ ├── historify_simple.png │ │ │ ├── historify_favicon.svg │ │ │ ├── historify_logo.svg │ │ │ └── logo_preview.html │ │ ├── js │ │ │ ├── sidebar.js │ │ │ ├── theme.js │ │ │ ├── command-palette.js │ │ │ └── dashboard.js │ │ └── css │ │ │ ├── modal.css │ │ │ └── style.css │ ├── models │ │ ├── __init__.py │ │ ├── watchlist.py │ │ ├── checkpoint.py │ │ ├── stock_data.py │ │ ├── scheduler_job.py │ │ ├── settings.py │ │ └── dynamic_tables.py │ ├── __init__.py │ └── templates │ │ ├── watchlist.html │ │ ├── index.html │ │ ├── charts.html │ │ ├── base.html │ │ ├── download.html │ │ └── scheduler.html ├── run.py ├── .env.sample ├── .claude │ └── settings.local.json ├── start.bat ├── requirements.txt └── logo_info.md ├── .windsurf └── rules │ └── flaskapp-rules.md ├── sample ├── tradingview-yahoo-finance-main │ ├── models.py │ ├── requirements.txt │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── app.py │ └── templates │ │ └── index.html ├── sqlitedb.py └── datafetch.py ├── .gitignore ├── docs └── prd.md └── README.md /historify/app/routes/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /historify/app/utils/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /historify/app/static/image/historify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marketcalls/historify/HEAD/historify/app/static/image/historify.png -------------------------------------------------------------------------------- /historify/app/static/image/historify_simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marketcalls/historify/HEAD/historify/app/static/image/historify_simple.png -------------------------------------------------------------------------------- /historify/run.py: -------------------------------------------------------------------------------- 1 | """ 2 | Historify - Stock Historical Data Management App 3 | Entry point for the application 4 | """ 5 | from app import create_app 6 | 7 | app = create_app() 8 | 9 | if __name__ == '__main__': 10 | app.run(host='0.0.0.0', port=5001, debug=True) 11 | -------------------------------------------------------------------------------- /historify/.env.sample: -------------------------------------------------------------------------------- 1 | # Historify App Environment Variables 2 | FLASK_APP=run.py 3 | FLASK_ENV=development 4 | FLASK_DEBUG=1 5 | SECRET_KEY=historify_secret_key_change_in_production 6 | DATABASE_URI=sqlite:///historify.db 7 | 8 | # Note: OpenAlgo API settings are now managed through the Settings page in the application 9 | -------------------------------------------------------------------------------- /historify/.claude/settings.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "allow": [ 4 | "Bash(find:*)", 5 | "Bash(grep:*)", 6 | "Bash(python:*)", 7 | "Bash(rm:*)", 8 | "Bash(pip3 install:*)", 9 | "Bash(ls:*)", 10 | "Bash(venv/Scripts/python.exe:*)", 11 | "Bash(curl:*)", 12 | "Bash(pkill:*)", 13 | "Bash(node:*)", 14 | "WebFetch(domain:www.gnu.org)", 15 | "Bash(rg:*)", 16 | "Bash(sqlite3:*)" 17 | ], 18 | "deny": [] 19 | } 20 | } -------------------------------------------------------------------------------- /historify/app/models/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Historify - Stock Historical Data Management App 3 | Database Models Package 4 | """ 5 | from flask_sqlalchemy import SQLAlchemy 6 | 7 | # Initialize SQLAlchemy 8 | db = SQLAlchemy() 9 | 10 | # Import models to make them available when this package is imported 11 | from app.models.stock_data import StockData 12 | from app.models.watchlist import WatchlistItem 13 | from app.models.checkpoint import Checkpoint 14 | from app.models.settings import AppSettings 15 | from app.models.scheduler_job import SchedulerJob 16 | -------------------------------------------------------------------------------- /historify/start.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo Starting Historify - Stock Historical Data Management App... 3 | 4 | rem Set environment variables 5 | set FLASK_APP=run.py 6 | set FLASK_ENV=development 7 | set FLASK_DEBUG=1 8 | 9 | rem Check if virtual environment exists, if not create one 10 | if not exist venv ( 11 | echo Creating virtual environment... 12 | python -m venv venv 13 | call venv\Scripts\activate 14 | echo Installing dependencies... 15 | pip install -r requirements.txt 16 | ) else ( 17 | call venv\Scripts\activate 18 | ) 19 | 20 | rem Run the Flask application 21 | echo Starting Flask server at http://localhost:5001 22 | python run.py 23 | 24 | pause 25 | -------------------------------------------------------------------------------- /.windsurf/rules/flaskapp-rules.md: -------------------------------------------------------------------------------- 1 | --- 2 | trigger: always_on 3 | --- 4 | 5 | 1)Write a Modular Flask Application using Jinja Template 6 | 2)Use Tailwind CSS CDN and DaisyUI CDN as well for building frontend 7 | 3)Build a cool moden UI Templates 8 | 4)Database : Sqlite and Flask SqlAlchemy and Alchemy 9 | 5)Routes use modular Blueprints 10 | 6)Run the port at 5001 11 | 7)Any sensitive details keep in .env files 12 | 8)write a documentation (readme.md) 13 | 9)Licensing - MIT License (create License file) 14 | 10)also include .gitignore 15 | 11)Write an CSS files/JS only in static (CSS/JS) folder. Avoid writing inline CSS and JS 16 | 12)Write the base.html with headers, footers, menu 17 | 13)Toggle between light and dark themes -------------------------------------------------------------------------------- /historify/requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==4.9.0 2 | APScheduler==3.11.0 3 | blinker==1.9.0 4 | certifi==2025.4.26 5 | charset-normalizer==3.4.2 6 | click==8.2.1 7 | colorama==0.4.6 8 | Flask==3.1.1 9 | Flask-SQLAlchemy==3.1.1 10 | greenlet==3.2.2 11 | h11==0.16.0 12 | httpcore==1.0.9 13 | httpx==0.28.1 14 | idna==3.10 15 | itsdangerous==2.2.0 16 | Jinja2==3.1.6 17 | llvmlite==0.44.0 18 | MarkupSafe==3.0.2 19 | numba==0.61.2 20 | numpy==2.2.6 21 | openalgo==1.0.15 22 | pandas==2.2.3 23 | python-dateutil==2.9.0.post0 24 | python-dotenv==1.1.0 25 | pytz==2025.2 26 | requests==2.32.3 27 | six==1.17.0 28 | sniffio==1.3.1 29 | SQLAlchemy==2.0.41 30 | typing_extensions==4.13.2 31 | tzdata==2025.2 32 | tzlocal==5.3.1 33 | urllib3==2.4.0 34 | websocket-client==1.8.0 35 | Werkzeug==3.1.3 -------------------------------------------------------------------------------- /sample/tradingview-yahoo-finance-main/models.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | from datetime import datetime 3 | 4 | db = SQLAlchemy() 5 | 6 | class Symbol(db.Model): 7 | id = db.Column(db.Integer, primary_key=True) 8 | ticker = db.Column(db.String(10), unique=True, nullable=False) 9 | name = db.Column(db.String(100)) 10 | created_at = db.Column(db.DateTime, default=datetime.utcnow) 11 | 12 | def to_dict(self): 13 | return { 14 | 'id': self.id, 15 | 'ticker': self.ticker, 16 | 'name': self.name, 17 | 'created_at': self.created_at.isoformat() if self.created_at else None 18 | } 19 | 20 | def __repr__(self): 21 | return f'' 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | env/ 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Flask 24 | instance/ 25 | .webassets-cache 26 | historify.db 27 | 28 | # Virtual Environment 29 | venv/ 30 | ENV/ 31 | env/ 32 | 33 | # Environment variables 34 | .env 35 | 36 | # IDE specific files 37 | .idea/ 38 | .vscode/ 39 | *.swp 40 | *.swo 41 | 42 | # OS specific files 43 | .DS_Store 44 | .DS_Store? 45 | ._* 46 | .Spotlight-V100 47 | .Trashes 48 | ehthumbs.db 49 | Thumbs.db 50 | 51 | # Logs 52 | logs/ 53 | *.log 54 | 55 | # Local data files 56 | *.csv 57 | *.xlsx 58 | *.xls 59 | -------------------------------------------------------------------------------- /sample/tradingview-yahoo-finance-main/requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4==4.12.3 2 | blinker==1.8.2 3 | certifi==2024.2.2 4 | charset-normalizer==3.3.2 5 | click==8.1.7 6 | colorama==0.4.6 7 | Flask==3.0.3 8 | Flask-SQLAlchemy==3.1.1 9 | frozendict==2.4.4 10 | html5lib==1.1 11 | idna==3.7 12 | itsdangerous==2.2.0 13 | Jinja2==3.1.4 14 | lxml==5.2.2 15 | MarkupSafe==2.1.5 16 | multitasking==0.0.11 17 | numpy==1.26.4 18 | pandas==2.2.2 19 | pandas_ta==0.3.14b0 20 | peewee==3.17.5 21 | platformdirs==4.2.2 22 | python-dateutil==2.9.0.post0 23 | pytz==2024.1 24 | requests==2.31.0 25 | setuptools==78.1.0 26 | six==1.16.0 27 | soupsieve==2.5 28 | SQLAlchemy==2.0.40 29 | typing_extensions==4.13.2 30 | tzdata==2024.1 31 | urllib3==2.2.1 32 | webencodings==0.5.1 33 | Werkzeug==3.0.3 34 | yfinance==0.2.61 35 | -------------------------------------------------------------------------------- /sample/tradingview-yahoo-finance-main/.gitignore: -------------------------------------------------------------------------------- 1 | # Virtual Environment 2 | venv/ 3 | env/ 4 | .env/ 5 | .venv/ 6 | 7 | # Python 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | *.so 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # Flask 30 | instance/ 31 | .webassets-cache 32 | 33 | # Database 34 | *.sqlite 35 | *.sqlite3 36 | *.db 37 | 38 | # IDE - VS Code 39 | .vscode/ 40 | .history 41 | 42 | # IDE - PyCharm 43 | .idea/ 44 | *.iml 45 | *.iws 46 | *.ipr 47 | 48 | # IDE - Jupyter Notebook 49 | .ipynb_checkpoints 50 | 51 | # OS specific 52 | .DS_Store 53 | Thumbs.db 54 | 55 | # Logs 56 | logs/ 57 | *.log 58 | 59 | # Environment variables 60 | .env 61 | .env.local 62 | .env.development 63 | .env.test 64 | .env.production -------------------------------------------------------------------------------- /historify/app/static/image/historify_favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | -------------------------------------------------------------------------------- /historify/app/models/watchlist.py: -------------------------------------------------------------------------------- 1 | """ 2 | Historify - Stock Historical Data Management App 3 | Watchlist Model 4 | """ 5 | from app.models import db 6 | from datetime import datetime 7 | 8 | class WatchlistItem(db.Model): 9 | """Model for storing user's watchlist symbols""" 10 | __tablename__ = 'watchlist' 11 | 12 | id = db.Column(db.Integer, primary_key=True) 13 | symbol = db.Column(db.String(20), unique=True, nullable=False) 14 | name = db.Column(db.String(100)) 15 | exchange = db.Column(db.String(10), default="NSE") 16 | added_on = db.Column(db.DateTime, default=datetime.utcnow) 17 | 18 | def __repr__(self): 19 | return f'' 20 | 21 | def to_dict(self): 22 | """Convert the model instance to a dictionary""" 23 | return { 24 | 'id': self.id, 25 | 'symbol': self.symbol, 26 | 'name': self.name, 27 | 'exchange': self.exchange, 28 | 'added_on': self.added_on.isoformat() if self.added_on else None 29 | } 30 | -------------------------------------------------------------------------------- /historify/app/routes/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Historify - Stock Historical Data Management App 3 | Main Routes Blueprint 4 | """ 5 | from flask import Blueprint, render_template, current_app 6 | from app.models import db 7 | 8 | main_bp = Blueprint('main', __name__) 9 | 10 | @main_bp.route('/') 11 | def index(): 12 | """Render the home page""" 13 | return render_template('index.html', title="Historify - Stock Data Manager") 14 | 15 | @main_bp.route('/watchlist/') 16 | def watchlist_page(): 17 | return render_template('watchlist.html') 18 | 19 | @main_bp.route('/dashboard/') 20 | def dashboard_page(): 21 | return render_template('dashboard.html') 22 | 23 | @main_bp.route('/profile/') 24 | def profile_page(): 25 | # existing code for profile page 26 | pass 27 | 28 | @main_bp.route('/import') 29 | def import_symbols(): 30 | """Render the import symbols page""" 31 | return render_template('import.html') 32 | 33 | @main_bp.route('/export') 34 | def export_data(): 35 | """Render the export data page""" 36 | return render_template('export.html') 37 | 38 | @main_bp.route('/download') 39 | def bulk_download(): 40 | """Render the bulk download page""" 41 | return render_template('download.html') 42 | 43 | -------------------------------------------------------------------------------- /historify/app/models/checkpoint.py: -------------------------------------------------------------------------------- 1 | """ 2 | Historify - Stock Historical Data Management App 3 | Checkpoint Model 4 | """ 5 | from app.models import db 6 | from datetime import datetime 7 | 8 | class Checkpoint(db.Model): 9 | """Model for tracking last downloaded data points for incremental updates""" 10 | __tablename__ = 'checkpoints' 11 | 12 | id = db.Column(db.Integer, primary_key=True) 13 | symbol = db.Column(db.String(20), unique=True, nullable=False) 14 | last_downloaded_date = db.Column(db.Date, nullable=True) 15 | last_downloaded_time = db.Column(db.Time, nullable=True) 16 | last_update = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) 17 | 18 | def __repr__(self): 19 | return f'' 20 | 21 | def to_dict(self): 22 | """Convert the model instance to a dictionary""" 23 | return { 24 | 'id': self.id, 25 | 'symbol': self.symbol, 26 | 'last_downloaded_date': self.last_downloaded_date.strftime('%Y-%m-%d') if self.last_downloaded_date else None, 27 | 'last_downloaded_time': self.last_downloaded_time.strftime('%H:%M:%S') if self.last_downloaded_time else None, 28 | 'last_update': self.last_update.isoformat() if self.last_update else None 29 | } 30 | -------------------------------------------------------------------------------- /historify/logo_info.md: -------------------------------------------------------------------------------- 1 | # Historify Logo 2 | 3 | ## New Logo Design 4 | 5 | The new Historify logo features a modern, professional design that represents: 6 | 7 | ### Design Elements: 8 | - **Circular Background**: Blue gradient representing stability and trust 9 | - **Stylized "H"**: The letter H formed by clean white bars representing "Historify" 10 | - **Chart Elements**: Integrated candlestick charts showing the financial data focus 11 | - **Trending Arrow**: Upward trending line indicating growth and analysis 12 | - **Data Points**: Scattered dots representing data analytics 13 | 14 | ### Color Palette: 15 | - **Primary Blue**: `#2563eb` - Main brand color 16 | - **Secondary Blue**: `#3b82f6` - Supporting elements 17 | - **Accent Blue**: `#60a5fa` - Highlights and charts 18 | - **Light Blue**: `#93c5fd` - Subtle accents 19 | - **White**: `#ffffff` - Text and primary elements 20 | 21 | ### File Versions: 22 | 1. **historify_logo.svg** - Main logo (200x200px, scalable) 23 | 2. **historify_favicon.svg** - Favicon version (64x64px, simplified) 24 | 3. **historify_favicon.png** - PNG favicon fallback 25 | 26 | ### Usage: 27 | - The SVG version is now used in the sidebar navigation 28 | - Favicon appears in browser tabs 29 | - Logo is responsive and works in both light and dark themes 30 | 31 | ### ASCII Version: 32 | ``` 33 | ╭─────────────────╮ 34 | ╱ ╲ 35 | ╱ ██ ██ ╲ 36 | ╱ ██ ██ ╲ 37 | │ ██───██ │ 38 | │ ██ ██ │ 39 | │ ┌─┐ ██ ██ ▲─▲ │ 40 | │ │▲│ ██ ██ │ │ │ 41 | │ └─┘ │ │ 42 | ╲ ┌─┐ ┌─┐ ┌─┐ ╱ 43 | ╲ │ │ │▲│ │ │ ╱ 44 | ╲ └─┘ └─┘ └─┘ ╱ 45 | ╰─────────────────╯ 46 | H I S T O R I F Y 47 | ``` 48 | 49 | The new logo provides a professional, modern appearance that clearly communicates Historify's purpose as a financial data management platform while maintaining visual appeal and brand recognition. -------------------------------------------------------------------------------- /historify/app/static/js/sidebar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sidebar navigation functionality 3 | */ 4 | 5 | document.addEventListener('DOMContentLoaded', function() { 6 | const sidebar = document.getElementById('sidebar'); 7 | const sidebarToggle = document.getElementById('sidebar-toggle'); 8 | const sidebarTexts = document.querySelectorAll('.sidebar-text'); 9 | 10 | // Check localStorage for sidebar state 11 | const isSidebarCollapsed = localStorage.getItem('sidebarCollapsed') === 'true'; 12 | 13 | // Apply initial state 14 | if (isSidebarCollapsed) { 15 | sidebar.classList.add('collapsed'); 16 | sidebarTexts.forEach(text => text.style.display = 'none'); 17 | } 18 | 19 | // Toggle sidebar 20 | sidebarToggle.addEventListener('click', function() { 21 | const isCollapsed = sidebar.classList.contains('collapsed'); 22 | 23 | if (isCollapsed) { 24 | // Expand 25 | sidebar.classList.remove('collapsed'); 26 | setTimeout(() => { 27 | sidebarTexts.forEach(text => text.style.display = 'block'); 28 | }, 150); 29 | localStorage.setItem('sidebarCollapsed', 'false'); 30 | } else { 31 | // Collapse 32 | sidebarTexts.forEach(text => text.style.display = 'none'); 33 | sidebar.classList.add('collapsed'); 34 | localStorage.setItem('sidebarCollapsed', 'true'); 35 | } 36 | }); 37 | 38 | // Add tooltips for collapsed state 39 | const navLinks = document.querySelectorAll('.nav-link'); 40 | navLinks.forEach(link => { 41 | link.addEventListener('mouseenter', function() { 42 | if (sidebar.classList.contains('collapsed')) { 43 | const text = this.querySelector('.sidebar-text').textContent; 44 | this.setAttribute('title', text); 45 | } 46 | }); 47 | 48 | link.addEventListener('mouseleave', function() { 49 | this.removeAttribute('title'); 50 | }); 51 | }); 52 | }); -------------------------------------------------------------------------------- /sample/sqlitedb.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | from datetime import datetime 3 | import pandas as pd 4 | from sqlalchemy import create_engine 5 | from openalgo import api 6 | import time 7 | 8 | # Initialize OpenAlgo API 9 | client = api(api_key='df746c00c34b744940773253978b3e9d8b610e428653db230a43085c5311d33d', host='http://127.0.0.1:5000') 10 | 11 | # List of symbols to download 12 | symbols = ["RELIANCE", "ICICIBANK", "SBIN", "TATAMOTORS", "TATASTEEL", 13 | "INFY", "TCS", "MARUTI", "HDFCBANK", "AXISBANK"] 14 | 15 | # Set up SQLite database 16 | engine = create_engine('sqlite:///data.db') 17 | 18 | # Streamlit UI 19 | st.title("OpenAlgo 1-Minute Historical Data Downloader") 20 | start_date = st.date_input("Start Date", datetime.now().date()) 21 | end_date = st.date_input("End Date", datetime.now().date()) 22 | 23 | if st.button("Download Data"): 24 | progress_bar = st.progress(0) 25 | status_text = st.empty() 26 | 27 | for idx, symbol in enumerate(symbols): 28 | try: 29 | status_text.text(f"Downloading {symbol} ({idx+1}/{len(symbols)})...") 30 | 31 | df = client.history( 32 | symbol=symbol, 33 | exchange="NSE", 34 | interval="1m", 35 | start_date=start_date.strftime('%Y-%m-%d'), 36 | end_date=end_date.strftime('%Y-%m-%d') 37 | ) 38 | 39 | 40 | if not isinstance(df, pd.DataFrame): 41 | st.warning(f"No data returned for {symbol}") 42 | continue 43 | 44 | df['symbol'] = symbol 45 | df.reset_index(inplace=True) 46 | df.to_sql(symbol, con=engine, if_exists='replace', index=False) 47 | time.sleep(0.5) # Sleep to avoid hitting API rate limits 48 | 49 | except Exception as e: 50 | st.error(f"Error downloading {symbol}: {e}") 51 | 52 | progress_bar.progress((idx + 1) / len(symbols)) 53 | 54 | status_text.text("✅ All downloads complete!") 55 | st.success("Data has been saved to 'data.db'") -------------------------------------------------------------------------------- /historify/app/utils/data_fetcher_chunked.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example of chunked data fetching to work around API limitations 3 | """ 4 | from datetime import datetime, timedelta 5 | import logging 6 | 7 | def fetch_historical_data_chunked(symbol, start_date, end_date, interval='1d', exchange='NSE', chunk_days=30): 8 | """ 9 | Fetch historical data in chunks to work around API limitations 10 | 11 | Args: 12 | symbol: Stock symbol 13 | start_date: Start date string (YYYY-MM-DD) 14 | end_date: End date string (YYYY-MM-DD) 15 | interval: Data interval 16 | exchange: Exchange name 17 | chunk_days: Maximum days per API request 18 | 19 | Returns: 20 | Combined list of all data points 21 | """ 22 | all_data = [] 23 | 24 | # Convert dates to datetime objects 25 | current_start = datetime.strptime(start_date, '%Y-%m-%d') 26 | final_end = datetime.strptime(end_date, '%Y-%m-%d') 27 | 28 | while current_start < final_end: 29 | # Calculate chunk end date 30 | chunk_end = min(current_start + timedelta(days=chunk_days - 1), final_end) 31 | 32 | logging.info(f"Fetching chunk: {current_start.strftime('%Y-%m-%d')} to {chunk_end.strftime('%Y-%m-%d')}") 33 | 34 | try: 35 | # Fetch data for this chunk 36 | chunk_data = fetch_historical_data( 37 | symbol, 38 | current_start.strftime('%Y-%m-%d'), 39 | chunk_end.strftime('%Y-%m-%d'), 40 | interval, 41 | exchange 42 | ) 43 | 44 | if chunk_data: 45 | all_data.extend(chunk_data) 46 | logging.info(f"Retrieved {len(chunk_data)} records in this chunk") 47 | 48 | except Exception as e: 49 | logging.error(f"Error fetching chunk: {e}") 50 | # Continue with next chunk even if one fails 51 | 52 | # Move to next chunk 53 | current_start = chunk_end + timedelta(days=1) 54 | 55 | logging.info(f"Total records retrieved: {len(all_data)}") 56 | return all_data -------------------------------------------------------------------------------- /sample/tradingview-yahoo-finance-main/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | 22 | --- 23 | 24 | This software uses TradingView Lightweight Charts which is licensed under the Apache License, Version 2.0 (the "License"); you may not use this software except in compliance with the License. You may obtain a copy of the License at: 25 | 26 | http://www.apache.org/licenses/LICENSE-2.0 27 | 28 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 29 | 30 | This software incorporates several parts of tslib (https://github.com/Microsoft/tslib, (c) Microsoft Corporation) that are covered by BSD Zero Clause License. 31 | 32 | ### Attribution Notice 33 | This product includes software developed by TradingView. For more information, please visit: 34 | 35 | https://www.tradingview.com/ 36 | 37 | In compliance with the TradingView Lightweight Charts license, you shall add this attribution notice to the page of your website or mobile application that is available to your users. 38 | -------------------------------------------------------------------------------- /historify/app/static/js/theme.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Theme toggling functionality for Historify app 3 | */ 4 | 5 | document.addEventListener('DOMContentLoaded', () => { 6 | // Get the theme toggle element 7 | const themeToggle = document.getElementById('theme-toggle'); 8 | 9 | // Check for saved theme preference or use device preference 10 | const savedTheme = localStorage.getItem('historify-theme'); 11 | let currentTheme; 12 | 13 | if (savedTheme) { 14 | currentTheme = savedTheme; 15 | document.documentElement.setAttribute('data-theme', currentTheme); 16 | } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { 17 | currentTheme = 'dark'; 18 | document.documentElement.setAttribute('data-theme', 'dark'); 19 | } else { 20 | currentTheme = 'light'; 21 | } 22 | 23 | // Set the initial state of the toggle based on the current theme 24 | if (themeToggle) { 25 | // If current theme is dark, the toggle should show the sun (to switch to light) 26 | // If current theme is light, the toggle should show the moon (to switch to dark) 27 | themeToggle.classList.toggle('swap-active', currentTheme === 'dark'); 28 | 29 | // Add event listener for the toggle 30 | themeToggle.addEventListener('click', () => { 31 | // Get the current theme 32 | const currentTheme = document.documentElement.getAttribute('data-theme'); 33 | 34 | // Toggle the theme 35 | const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; 36 | setTheme(newTheme); 37 | 38 | // Toggle the active state of the swap component 39 | themeToggle.classList.toggle('swap-active'); 40 | }); 41 | } 42 | }); 43 | 44 | /** 45 | * Set the theme and save preference to localStorage 46 | * @param {string} theme - Theme name ('light' or 'dark') 47 | */ 48 | function setTheme(theme) { 49 | document.documentElement.setAttribute('data-theme', theme); 50 | localStorage.setItem('historify-theme', theme); 51 | 52 | // Dispatch a custom event for other components to react to theme changes 53 | const themeChangeEvent = new CustomEvent('themeChange', { detail: { theme } }); 54 | document.dispatchEvent(themeChangeEvent); 55 | 56 | // Log theme change 57 | console.log(`Theme changed to ${theme}`); 58 | } 59 | -------------------------------------------------------------------------------- /historify/app/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Historify - Stock Historical Data Management App 3 | Application Factory 4 | """ 5 | import os 6 | from flask import Flask 7 | from dotenv import load_dotenv 8 | 9 | # Load environment variables from .env file 10 | load_dotenv() 11 | 12 | def create_app(config_class=None): 13 | """Create and configure the Flask application""" 14 | app = Flask(__name__, instance_relative_config=True) 15 | 16 | # Configure the app from environment variables 17 | app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev_key_change_this') 18 | app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URI', 'sqlite:///historify.db') 19 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 20 | 21 | # Ensure the instance folder exists 22 | try: 23 | os.makedirs(app.instance_path, exist_ok=True) 24 | except OSError: 25 | pass 26 | 27 | # Initialize extensions 28 | from app.models import db 29 | db.init_app(app) 30 | 31 | # Register blueprints 32 | from app.routes.main import main_bp 33 | from app.routes.api import api_bp 34 | from app.routes.watchlist import watchlist_bp 35 | from app.routes.charts import charts_bp 36 | from app.routes.scheduler import scheduler_bp 37 | from app.routes.settings import settings_bp 38 | 39 | app.register_blueprint(main_bp) 40 | app.register_blueprint(api_bp, url_prefix='/api') 41 | app.register_blueprint(watchlist_bp, url_prefix='/watchlist') 42 | app.register_blueprint(charts_bp, url_prefix='/charts') 43 | app.register_blueprint(scheduler_bp) 44 | app.register_blueprint(settings_bp) 45 | 46 | # Initialize scheduler 47 | from app.utils.scheduler import scheduler_manager 48 | import logging 49 | logging.basicConfig(level=logging.INFO) 50 | logging.info("Initializing scheduler manager") 51 | scheduler_manager.init_app(app) 52 | 53 | # Add API configuration check middleware 54 | from app.utils.auth import check_api_config_middleware 55 | app.before_request(check_api_config_middleware) 56 | 57 | # Create database tables if they don't exist 58 | with app.app_context(): 59 | db.create_all() 60 | 61 | # Initialize default settings 62 | from app.models.settings import AppSettings 63 | AppSettings.initialize_default_settings() 64 | 65 | return app 66 | -------------------------------------------------------------------------------- /historify/app/routes/watchlist.py: -------------------------------------------------------------------------------- 1 | """ 2 | Historify - Stock Historical Data Management App 3 | Watchlist Routes Blueprint 4 | """ 5 | from flask import Blueprint, request, jsonify, render_template 6 | from app.models import db 7 | from app.models.watchlist import WatchlistItem 8 | 9 | watchlist_bp = Blueprint('watchlist', __name__) 10 | 11 | @watchlist_bp.route('/') 12 | def index(): 13 | """Render the watchlist page""" 14 | return render_template('watchlist.html', title="Historify - Watchlist") 15 | 16 | @watchlist_bp.route('/items', methods=['GET']) 17 | def get_items(): 18 | """Get all watchlist items""" 19 | items = WatchlistItem.query.all() 20 | return jsonify([item.to_dict() for item in items]) 21 | 22 | @watchlist_bp.route('/items', methods=['POST']) 23 | def add_item(): 24 | """Add a new symbol to the watchlist""" 25 | data = request.json 26 | if not data or 'symbol' not in data: 27 | return jsonify({'error': 'Symbol is required'}), 400 28 | 29 | symbol = data['symbol'].strip() 30 | if not symbol: 31 | return jsonify({'error': 'Symbol cannot be empty'}), 400 32 | 33 | # Check if symbol already exists 34 | existing = WatchlistItem.query.filter_by(symbol=symbol).first() 35 | if existing: 36 | return jsonify({'error': 'Symbol already exists', 'item': existing.to_dict()}), 409 37 | 38 | # Create new watchlist item 39 | name = data.get('name', symbol) 40 | exchange = data.get('exchange', 'NSE') 41 | 42 | try: 43 | new_item = WatchlistItem(symbol=symbol, name=name, exchange=exchange) 44 | db.session.add(new_item) 45 | db.session.commit() 46 | return jsonify({'message': 'Symbol added successfully', 'item': new_item.to_dict()}), 201 47 | except Exception as e: 48 | db.session.rollback() 49 | return jsonify({'error': f'Error adding symbol: {str(e)}'}), 400 50 | 51 | @watchlist_bp.route('/items/', methods=['DELETE']) 52 | def delete_item(item_id): 53 | """Remove a symbol from the watchlist""" 54 | item = WatchlistItem.query.get_or_404(item_id) 55 | 56 | try: 57 | db.session.delete(item) 58 | db.session.commit() 59 | return jsonify({'message': 'Symbol removed successfully'}), 200 60 | except Exception as e: 61 | db.session.rollback() 62 | return jsonify({'error': f'Error removing symbol: {str(e)}'}), 400 63 | -------------------------------------------------------------------------------- /historify/app/static/css/modal.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Modal styles for Historify 3 | */ 4 | 5 | /* Modal container */ 6 | .modal { 7 | position: fixed; 8 | top: 0; 9 | left: 0; 10 | right: 0; 11 | bottom: 0; 12 | z-index: 9999; 13 | display: flex; 14 | align-items: center; 15 | justify-content: center; 16 | overflow-y: auto; 17 | transition: all 0.3s ease; 18 | } 19 | 20 | .modal.hidden { 21 | display: none; 22 | } 23 | 24 | /* Modal backdrop */ 25 | .modal-backdrop { 26 | position: absolute; 27 | top: 0; 28 | left: 0; 29 | right: 0; 30 | bottom: 0; 31 | background-color: rgba(0, 0, 0, 0.5); 32 | opacity: 0; 33 | transition: opacity 0.3s ease; 34 | } 35 | 36 | .modal-backdrop.show { 37 | opacity: 1; 38 | } 39 | 40 | /* Modal content */ 41 | .modal-content { 42 | position: relative; 43 | width: 100%; 44 | max-width: 600px; 45 | background-color: white; 46 | border-radius: 0.75rem; 47 | box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); 48 | margin: 1.5rem; 49 | opacity: 0; 50 | transform: scale(0.95); 51 | transition: all 0.3s ease; 52 | z-index: 10; 53 | max-height: 90vh; 54 | overflow: hidden; 55 | } 56 | 57 | .modal-content.show { 58 | opacity: 1; 59 | transform: scale(1); 60 | } 61 | 62 | .modal-content.max-w-4xl { 63 | max-width: 56rem; 64 | } 65 | 66 | /* Dark mode styles */ 67 | [data-theme="dark"] .modal-content { 68 | background-color: var(--bg-secondary); 69 | color: var(--text-primary); 70 | } 71 | 72 | /* Modal parts */ 73 | .modal-header { 74 | display: flex; 75 | justify-content: space-between; 76 | align-items: center; 77 | padding: 1.25rem; 78 | border-bottom: 1px solid var(--border-primary); 79 | } 80 | 81 | .modal-body { 82 | padding: 1.25rem; 83 | max-height: calc(100vh - 15rem); 84 | overflow-y: auto; 85 | } 86 | 87 | .modal-footer { 88 | padding: 1.25rem; 89 | display: flex; 90 | justify-content: flex-end; 91 | gap: 0.75rem; 92 | border-top: 1px solid var(--border-primary); 93 | } 94 | 95 | /* Responsive adjustments */ 96 | @media (min-width: 640px) { 97 | .modal-content { 98 | margin: 0 2rem; 99 | } 100 | } 101 | 102 | @media (max-width: 639px) { 103 | .modal-content { 104 | margin: 0 1rem; 105 | max-height: 90vh; 106 | } 107 | 108 | .modal-body { 109 | max-height: calc(90vh - 10rem); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /historify/app/models/stock_data.py: -------------------------------------------------------------------------------- 1 | """ 2 | Historify - Stock Historical Data Management App 3 | StockData Model 4 | """ 5 | from app.models import db 6 | from datetime import datetime 7 | 8 | class StockData(db.Model): 9 | """Model for storing OHLCV data for all symbols""" 10 | __tablename__ = 'stock_data' 11 | 12 | id = db.Column(db.Integer, primary_key=True) 13 | symbol = db.Column(db.String(20), nullable=False, index=True) 14 | exchange = db.Column(db.String(10), nullable=False, index=True) 15 | date = db.Column(db.Date, nullable=False, index=True) 16 | time = db.Column(db.Time, nullable=False, index=True) 17 | open = db.Column(db.Float, nullable=False) 18 | high = db.Column(db.Float, nullable=False) 19 | low = db.Column(db.Float, nullable=False) 20 | close = db.Column(db.Float, nullable=False) 21 | volume = db.Column(db.Integer, nullable=False) 22 | created_at = db.Column(db.DateTime, default=datetime.utcnow) 23 | 24 | # Create a composite index on symbol, exchange, date, time for efficient querying 25 | __table_args__ = ( 26 | db.UniqueConstraint('symbol', 'exchange', 'date', 'time', name='uix_symbol_exchange_date_time'), 27 | ) 28 | 29 | def __repr__(self): 30 | return f'' 31 | 32 | def to_dict(self): 33 | """Convert the model instance to a dictionary""" 34 | return { 35 | 'id': self.id, 36 | 'symbol': self.symbol, 37 | 'exchange': self.exchange, 38 | 'date': self.date.strftime('%Y-%m-%d'), 39 | 'time': self.time.strftime('%H:%M:%S') if self.time else None, 40 | 'open': self.open, 41 | 'high': self.high, 42 | 'low': self.low, 43 | 'close': self.close, 44 | 'volume': self.volume, 45 | 'created_at': self.created_at.isoformat() if self.created_at else None 46 | } 47 | 48 | @classmethod 49 | def get_data_by_timeframe(cls, symbol, start_date, end_date, timeframe='1d', exchange=None): 50 | """Get stock data for the specified symbol and timeframe""" 51 | query = cls.query.filter( 52 | cls.symbol == symbol, 53 | cls.date >= start_date, 54 | cls.date <= end_date 55 | ) 56 | 57 | if exchange: # Optional: filter by exchange if provided 58 | query = query.filter(cls.exchange == exchange) 59 | 60 | query = query.order_by(cls.date, cls.time) 61 | 62 | return query.all() 63 | -------------------------------------------------------------------------------- /historify/app/models/scheduler_job.py: -------------------------------------------------------------------------------- 1 | """ 2 | Historify - Stock Historical Data Management App 3 | Scheduler Job Model 4 | """ 5 | from app.models import db 6 | from datetime import datetime 7 | import json 8 | 9 | class SchedulerJob(db.Model): 10 | """Model for persisting scheduler jobs""" 11 | __tablename__ = 'scheduler_jobs' 12 | 13 | id = db.Column(db.String(100), primary_key=True) 14 | name = db.Column(db.String(200)) 15 | job_type = db.Column(db.String(50)) # 'daily', 'interval', 'market_close', 'pre_market' 16 | time = db.Column(db.String(10)) # For daily jobs (HH:MM format) 17 | minutes = db.Column(db.Integer) # For interval jobs 18 | symbols = db.Column(db.Text) # JSON string of symbols list 19 | exchanges = db.Column(db.Text) # JSON string of exchanges list 20 | interval = db.Column(db.String(10), default='D') # Data interval (D, W, 1m, 5m, etc.) 21 | is_paused = db.Column(db.Boolean, default=False) 22 | created_at = db.Column(db.DateTime, default=datetime.utcnow) 23 | updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) 24 | 25 | def get_symbols(self): 26 | """Get symbols as list""" 27 | if self.symbols: 28 | return json.loads(self.symbols) 29 | return None 30 | 31 | def set_symbols(self, symbols_list): 32 | """Set symbols from list""" 33 | if symbols_list: 34 | self.symbols = json.dumps(symbols_list) 35 | else: 36 | self.symbols = None 37 | 38 | def get_exchanges(self): 39 | """Get exchanges as list""" 40 | if self.exchanges: 41 | return json.loads(self.exchanges) 42 | return None 43 | 44 | def set_exchanges(self, exchanges_list): 45 | """Set exchanges from list""" 46 | if exchanges_list: 47 | self.exchanges = json.dumps(exchanges_list) 48 | else: 49 | self.exchanges = None 50 | 51 | def to_dict(self): 52 | """Convert to dictionary""" 53 | return { 54 | 'id': self.id, 55 | 'name': self.name, 56 | 'type': self.job_type, 57 | 'time': self.time, 58 | 'minutes': self.minutes, 59 | 'symbols': self.get_symbols(), 60 | 'exchanges': self.get_exchanges(), 61 | 'interval': self.interval, 62 | 'paused': self.is_paused, 63 | 'created_at': self.created_at.isoformat() if self.created_at else None, 64 | 'updated_at': self.updated_at.isoformat() if self.updated_at else None 65 | } -------------------------------------------------------------------------------- /sample/tradingview-yahoo-finance-main/README.md: -------------------------------------------------------------------------------- 1 | # TradingView Chart with Yahoo Finance Data 2 | 3 | This project is a web application that displays TradingView lightweight charts with real-time stock data fetched from Yahoo Finance. The application features an interactive UI built with Flask, SQLAlchemy, and Tailwind CSS with DaisyUI components. Users can view and analyze stock data using technical indicators, manage a watchlist of symbols, and toggle between light and dark themes. 4 | 5 | ## Features 6 | 7 | ### Chart Analysis 8 | - Fetch and display real-time stock data from Yahoo Finance using the yfinance library 9 | - View stock data in multiple timeframes (1 minute, 5 minutes, 15 minutes, hourly, daily, weekly, monthly) 10 | - Technical analysis indicators including EMA and RSI 11 | - Auto-update option for real-time data monitoring 12 | 13 | ### Watchlist Management 14 | - Dynamic watchlist with real-time quotes for multiple symbols 15 | - Add and remove symbols from the watchlist with instant UI updates 16 | - Persistent storage of watchlist symbols in SQLite database 17 | - Smooth animations for symbol addition and removal 18 | 19 | ### User Interface 20 | - Modern, responsive UI built with Tailwind CSS and DaisyUI components 21 | - Toggle between light and dark themes 22 | - Mobile-friendly design with drawer navigation 23 | 24 | ## Installation 25 | 26 | Follow these steps to set up and run the application: 27 | 28 | ### Prerequisites 29 | 30 | - Python 3.x 31 | 32 | ### Steps 33 | 34 | 1. **Clone the repository:** 35 | 36 | ```sh 37 | git clone https://github.com/marketcalls/tradingview-yahoo-finance.git 38 | cd tradingview-yahoo-finance 39 | ``` 40 | 41 | 2. **Create a virtual environment:** 42 | 43 | Windows: 44 | ```sh 45 | python -m venv venv 46 | venv\Scripts\activate 47 | ``` 48 | 49 | macOS/Linux: 50 | ```sh 51 | python -m venv venv 52 | source venv/bin/activate 53 | ``` 54 | 55 | 3. **Install the dependencies:** 56 | 57 | ```sh 58 | pip install -r requirements.txt 59 | ``` 60 | 61 | 4. **Run the Flask application:** 62 | 63 | ```sh 64 | python app.py 65 | ``` 66 | 67 | 5. **Open your web browser and visit:** 68 | 69 | ``` 70 | http://127.0.0.1:5000 71 | ``` 72 | 73 | ## Project Structure 74 | 75 | ``` 76 | ├── app.py # Main Flask application 77 | ├── models.py # SQLAlchemy database models 78 | ├── symbols.txt # Default symbols file 79 | ├── templates/ 80 | │ └── index.html # Main HTML template 81 | ├── static/ 82 | │ └── main.js # JavaScript for chart handling and UI 83 | ├── .gitignore # Git ignore file 84 | └── README.md # Project documentation 85 | ``` 86 | 87 | ## Technologies Used 88 | 89 | - **Backend**: Flask, SQLAlchemy, SQLite 90 | - **Data**: Yahoo Finance API (via yfinance) 91 | - **Frontend**: JavaScript, Tailwind CSS, DaisyUI 92 | - **Charting**: Lightweight-charts.js 93 | 94 | ## License 95 | 96 | This project is licensed under the MIT License. -------------------------------------------------------------------------------- /historify/app/static/css/style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Historify - Stock Historical Data Management App 3 | * Main stylesheet 4 | */ 5 | 6 | /* Global styles */ 7 | body { 8 | transition: background-color 0.3s ease; 9 | } 10 | 11 | .animate-fade-in { 12 | animation: fadeIn 0.5s ease-in; 13 | } 14 | 15 | @keyframes fadeIn { 16 | from { opacity: 0; } 17 | to { opacity: 1; } 18 | } 19 | 20 | /* Chart styles */ 21 | .chart-container { 22 | height: 500px; 23 | position: relative; 24 | } 25 | 26 | .tv-lightweight-charts { 27 | width: 100%; 28 | height: 100%; 29 | } 30 | 31 | /* Toast notifications */ 32 | #toast-container { 33 | position: fixed; 34 | top: 1rem; 35 | right: 1rem; 36 | z-index: 9999; 37 | } 38 | 39 | #toast-container .alert { 40 | transition: opacity 0.3s ease; 41 | } 42 | 43 | /* Watchlist table styles */ 44 | .watchlist-item { 45 | transition: background-color 0.2s ease; 46 | } 47 | 48 | .watchlist-item:hover { 49 | background-color: rgba(0, 0, 0, 0.05); 50 | } 51 | 52 | /* Symbol price change */ 53 | .price-up { 54 | color: #4CAF50; 55 | } 56 | 57 | .price-down { 58 | color: #F44336; 59 | } 60 | 61 | /* Progress bars */ 62 | .progress-container { 63 | height: 8px; 64 | background-color: rgba(0, 0, 0, 0.1); 65 | border-radius: 4px; 66 | overflow: hidden; 67 | } 68 | 69 | .progress-bar { 70 | height: 100%; 71 | background-color: var(--p); 72 | border-radius: 4px; 73 | transition: width 0.3s ease; 74 | } 75 | 76 | /* Dashboard cards */ 77 | .stat-card { 78 | transition: transform 0.3s ease, box-shadow 0.3s ease; 79 | } 80 | 81 | .stat-card:hover { 82 | transform: translateY(-2px); 83 | box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); 84 | } 85 | 86 | /* Indicators badges */ 87 | .indicator-badge { 88 | cursor: pointer; 89 | transition: all 0.2s ease; 90 | } 91 | 92 | .indicator-badge:hover { 93 | filter: brightness(1.1); 94 | } 95 | 96 | /* Custom scrollbar styles */ 97 | ::-webkit-scrollbar { 98 | width: 8px; 99 | height: 8px; 100 | } 101 | 102 | ::-webkit-scrollbar-track { 103 | background: rgba(0, 0, 0, 0.05); 104 | border-radius: 4px; 105 | } 106 | 107 | ::-webkit-scrollbar-thumb { 108 | background: rgba(0, 0, 0, 0.2); 109 | border-radius: 4px; 110 | } 111 | 112 | ::-webkit-scrollbar-thumb:hover { 113 | background: rgba(0, 0, 0, 0.3); 114 | } 115 | 116 | /* Download status animations */ 117 | @keyframes pulse { 118 | 0% { opacity: 0.7; } 119 | 50% { opacity: 1; } 120 | 100% { opacity: 0.7; } 121 | } 122 | 123 | .pulse-animation { 124 | animation: pulse 1.5s infinite; 125 | } 126 | 127 | /* Logo styles */ 128 | .logo { 129 | height: 40px; 130 | width: auto; 131 | } 132 | 133 | /* Custom responsive adjustments */ 134 | @media (max-width: 768px) { 135 | .card-body { 136 | padding: 1rem; 137 | } 138 | 139 | .chart-container { 140 | height: 400px; 141 | } 142 | 143 | .btn-sm-full { 144 | width: 100%; 145 | margin-bottom: 0.5rem; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /historify/app/utils/rate_limiter.py: -------------------------------------------------------------------------------- 1 | """ 2 | Historify - Stock Historical Data Management App 3 | Rate Limiter Utility 4 | """ 5 | import time 6 | import threading 7 | import logging 8 | from functools import wraps 9 | 10 | class RateLimiter: 11 | """ 12 | Rate limiter to control API request frequency 13 | 14 | Ensures we don't exceed broker's rate limits (e.g., 10 symbols per second) 15 | """ 16 | def __init__(self, max_calls, period=1.0): 17 | """ 18 | Initialize rate limiter 19 | 20 | Args: 21 | max_calls: Maximum number of calls allowed in the period 22 | period: Time period in seconds (default: 1 second) 23 | """ 24 | self.max_calls = max_calls 25 | self.period = period 26 | self.calls = [] 27 | self.lock = threading.Lock() 28 | 29 | def __call__(self, func): 30 | """ 31 | Decorator to rate limit function calls 32 | """ 33 | @wraps(func) 34 | def wrapper(*args, **kwargs): 35 | with self.lock: 36 | # Clean up old calls 37 | now = time.time() 38 | self.calls = [call_time for call_time in self.calls if call_time > now - self.period] 39 | 40 | # Check if we've reached the limit 41 | if len(self.calls) >= self.max_calls: 42 | # Wait until we can make another call 43 | sleep_time = self.calls[0] + self.period - now 44 | if sleep_time > 0: 45 | logging.info(f"Rate limit reached. Waiting {sleep_time:.2f} seconds before next call.") 46 | time.sleep(sleep_time) 47 | # Clean up again after waiting 48 | now = time.time() 49 | self.calls = [call_time for call_time in self.calls if call_time > now - self.period] 50 | 51 | # Add current call time 52 | self.calls.append(now) 53 | 54 | # Execute the function 55 | return func(*args, **kwargs) 56 | 57 | return wrapper 58 | 59 | def batch_process(items, batch_size, process_func, *args, **kwargs): 60 | """ 61 | Process items in batches to respect rate limits 62 | 63 | Args: 64 | items: List of items to process 65 | batch_size: Number of items to process in each batch 66 | process_func: Function to process each batch 67 | *args, **kwargs: Additional arguments to pass to process_func 68 | 69 | Returns: 70 | List of results from processing all batches 71 | """ 72 | results = [] 73 | 74 | # Process in batches 75 | for i in range(0, len(items), batch_size): 76 | batch = items[i:i+batch_size] 77 | batch_results = process_func(batch, *args, **kwargs) 78 | results.extend(batch_results) 79 | 80 | # If this isn't the last batch, wait to respect rate limits 81 | if i + batch_size < len(items): 82 | logging.info(f"Processed batch of {len(batch)} items. Waiting before next batch.") 83 | time.sleep(1.0) # Wait 1 second between batches 84 | 85 | return results 86 | 87 | # Create rate limiters with different configurations 88 | # For broker API calls (10 symbols per second) 89 | broker_rate_limiter = RateLimiter(max_calls=10, period=1.0) 90 | -------------------------------------------------------------------------------- /historify/app/utils/auth.py: -------------------------------------------------------------------------------- 1 | """ 2 | Historify - Stock Historical Data Management App 3 | Authentication and Configuration Check Utilities 4 | """ 5 | from functools import wraps 6 | from flask import request, redirect, url_for, flash, jsonify 7 | from app.models.settings import AppSettings 8 | 9 | 10 | def api_configured(): 11 | """Check if OpenAlgo API is properly configured""" 12 | api_key = AppSettings.get_value('openalgo_api_key') 13 | api_host = AppSettings.get_value('openalgo_api_host') 14 | 15 | return bool(api_key and api_key.strip() and api_host and api_host.strip()) 16 | 17 | 18 | def require_api_config(f): 19 | """Decorator to require API configuration before accessing routes""" 20 | @wraps(f) 21 | def decorated_function(*args, **kwargs): 22 | # Allow access to settings page and API endpoints 23 | exempt_routes = [ 24 | 'settings.settings_page', 25 | 'settings.get_settings', 26 | 'settings.update_settings', 27 | 'settings.test_api_connection', 28 | 'settings.get_database_info', 29 | 'settings.clear_cache', 30 | 'settings.optimize_database', 31 | 'settings.reset_settings' 32 | ] 33 | 34 | # Check if current endpoint is exempt 35 | if request.endpoint in exempt_routes: 36 | return f(*args, **kwargs) 37 | 38 | # For API routes, return JSON response 39 | if request.endpoint and request.endpoint.startswith('api'): 40 | if not api_configured(): 41 | return jsonify({ 42 | 'error': 'API not configured', 43 | 'message': 'Please configure OpenAlgo API key and host in Settings' 44 | }), 401 45 | else: 46 | # For web routes, redirect to settings 47 | if not api_configured(): 48 | flash('Please configure your OpenAlgo API key and host URL before using the application.', 'warning') 49 | return redirect(url_for('settings.settings_page')) 50 | 51 | return f(*args, **kwargs) 52 | 53 | return decorated_function 54 | 55 | 56 | def check_api_config_middleware(): 57 | """Middleware to check API configuration on every request""" 58 | # Skip check for static files and certain routes 59 | if (request.endpoint and 60 | (request.endpoint.startswith('static') or 61 | request.endpoint in ['settings.settings_page', 'settings.get_settings', 62 | 'settings.update_settings', 'settings.test_api_connection', 63 | 'settings.get_database_info', 'settings.clear_cache', 64 | 'settings.optimize_database', 'settings.reset_settings'])): 65 | return 66 | 67 | # Check if API is configured 68 | if not api_configured(): 69 | # For AJAX requests, return JSON 70 | if request.is_json or request.headers.get('Content-Type') == 'application/json': 71 | return jsonify({ 72 | 'error': 'API not configured', 73 | 'message': 'Please configure OpenAlgo API key and host in Settings', 74 | 'redirect': url_for('settings.settings_page') 75 | }), 401 76 | 77 | # For regular requests, redirect to settings 78 | flash('Please configure your OpenAlgo API key and host URL before using the application.', 'warning') 79 | return redirect(url_for('settings.settings_page')) -------------------------------------------------------------------------------- /historify/app/static/image/historify_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /historify/app/static/image/logo_preview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Historify Logo Preview 7 | 41 | 42 | 43 |

Historify Logo Variations

44 | 45 |
46 |

Main Logo (SVG)

47 |
48 |
49 | Historify Logo 50 |

Full Size (150px)

51 |
52 |
53 | Historify Logo 54 |

Medium (100px)

55 |
56 |
57 | Historify Logo 58 |

Small (64px)

59 |
60 |
61 | Historify Logo 62 |

Sidebar (40px)

63 |
64 |
65 |
66 | 67 |
68 |

Favicon (SVG)

69 |
70 |
71 | Historify Favicon 72 |

Favicon 64px

73 |
74 |
75 | Historify Favicon 76 |

Favicon 32px

77 |
78 |
79 | Historify Favicon 80 |

Favicon 16px

81 |
82 |
83 |
84 | 85 |
86 |

Usage Examples

87 |
88 |
89 | Historify 90 | Historify 91 |
92 |
93 | 94 |
95 |
96 | Historify 97 | Historify 98 |
99 |
100 |
101 | 102 | -------------------------------------------------------------------------------- /historify/app/templates/watchlist.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}{{ title }}{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |

Watchlist

10 | 11 |
12 | 18 | 24 |
25 |
26 | 27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 48 | 49 |
SymbolNameExchangeLast PriceChange %Actions
44 | 45 | Loading watchlist... 46 |
50 |
51 |
52 |
53 | 54 | 55 | 56 | 98 | 99 | 100 | 101 | 111 | {% endblock %} 112 | 113 | {% block extra_js %} 114 | 115 | {% endblock %} 116 | -------------------------------------------------------------------------------- /historify/app/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}{{ title }}{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |

Historify

10 |

Download, store, and visualize historical and real-time stock market data with ease.

11 |
12 | Manage Watchlist 13 | View Charts 14 |
15 |
16 |
17 |
18 | 19 |
20 | 21 |
22 |
23 |

Download Historical Data

24 |

Download historical stock data from various exchanges and time intervals.

25 | 26 |
27 |
28 | 31 |
32 |
33 | 34 | Select All 35 |
36 |
37 | 38 |
39 | 40 | Loading symbols... 41 |
42 |
43 |
44 |
45 | 46 |
47 | 50 | 58 |
59 | 60 |
61 | 64 | 75 |
76 | 77 | 92 | 93 |
94 | 97 |
98 | 102 | 106 |
107 |
108 | 109 |
110 | 116 |
117 |
118 |
119 |
120 | 121 | 122 |
123 |
124 |

Download Status

125 |
126 |

No active downloads

127 |
128 |
129 |
130 |
131 | 132 | 133 | {% endblock %} 134 | 135 | {% block extra_js %} 136 | 137 | {% endblock %} 138 | -------------------------------------------------------------------------------- /historify/app/models/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Historify - Stock Historical Data Management App 3 | Settings Model for Database Configuration Storage 4 | """ 5 | from app.models import db 6 | from datetime import datetime 7 | from sqlalchemy import text 8 | import json 9 | import logging 10 | 11 | class AppSettings(db.Model): 12 | """Model for storing application settings in database""" 13 | __tablename__ = 'app_settings' 14 | 15 | id = db.Column(db.Integer, primary_key=True) 16 | key = db.Column(db.String(255), unique=True, nullable=False, index=True) 17 | value = db.Column(db.Text, nullable=True) 18 | data_type = db.Column(db.String(50), default='string') # string, json, boolean, integer, float 19 | description = db.Column(db.Text, nullable=True) 20 | is_encrypted = db.Column(db.Boolean, default=False) 21 | created_at = db.Column(db.DateTime, default=datetime.utcnow) 22 | updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) 23 | 24 | def __repr__(self): 25 | return f'' 26 | 27 | @classmethod 28 | def get_value(cls, key, default=None): 29 | """Get a setting value by key""" 30 | try: 31 | setting = cls.query.filter_by(key=key).first() 32 | if not setting: 33 | return default 34 | 35 | # Convert based on data type 36 | if setting.data_type == 'json': 37 | return json.loads(setting.value) if setting.value else default 38 | elif setting.data_type == 'boolean': 39 | return setting.value.lower() in ('true', '1', 'yes') if setting.value else default 40 | elif setting.data_type == 'integer': 41 | return int(setting.value) if setting.value else default 42 | elif setting.data_type == 'float': 43 | return float(setting.value) if setting.value else default 44 | else: 45 | return setting.value if setting.value is not None else default 46 | 47 | except Exception as e: 48 | logging.error(f"Error getting setting {key}: {str(e)}") 49 | return default 50 | 51 | @classmethod 52 | def set_value(cls, key, value, data_type='string', description=None): 53 | try: 54 | if data_type == 'json': 55 | str_value = json.dumps(value) 56 | elif data_type == 'boolean': 57 | str_value = str(bool(value)).lower() 58 | else: 59 | str_value = str(value) if value is not None else None 60 | 61 | setting = cls.query.filter_by(key=key).first() 62 | 63 | if setting: 64 | # Update existing setting 65 | setting.value = str_value 66 | setting.data_type = data_type 67 | if description is not None: 68 | setting.description = description 69 | setting.updated_at = datetime.utcnow() 70 | else: 71 | # Create new setting 72 | new_setting = cls(key=key, value=str_value, data_type=data_type, description=description) 73 | db.session.add(new_setting) 74 | 75 | # The caller (e.g., route handler) is responsible for db.session.commit() 76 | return True 77 | 78 | except Exception as e: 79 | # The caller (route handler) should manage transactions (commit/rollback). 80 | current_app.logger.error(f"[AppSettings.set_value] Error for key '{key}': {e}", exc_info=True) 81 | return False 82 | 83 | @classmethod 84 | def get_all_settings(cls): 85 | """Get all settings as a dictionary""" 86 | try: 87 | settings = {} 88 | for setting in cls.query.all(): 89 | if setting.data_type == 'json': 90 | settings[setting.key] = json.loads(setting.value) if setting.value else None 91 | elif setting.data_type == 'boolean': 92 | settings[setting.key] = setting.value.lower() in ('true', '1', 'yes') if setting.value else False 93 | elif setting.data_type == 'integer': 94 | settings[setting.key] = int(setting.value) if setting.value else 0 95 | elif setting.data_type == 'float': 96 | settings[setting.key] = float(setting.value) if setting.value else 0.0 97 | else: 98 | settings[setting.key] = setting.value 99 | return settings 100 | except Exception as e: 101 | logging.error(f"Error getting all settings: {str(e)}") 102 | return {} 103 | 104 | @classmethod 105 | def initialize_default_settings(cls): 106 | """Initialize default settings""" 107 | defaults = [ 108 | ('openalgo_api_key', '', 'string', 'OpenAlgo API Key for market data'), 109 | ('openalgo_api_host', 'http://127.0.0.1:5000', 'string', 'OpenAlgo API Host URL'), 110 | ('batch_size', '10', 'integer', 'Number of symbols to process per batch'), 111 | ('rate_limit_delay', '100', 'integer', 'Delay between API requests in milliseconds'), 112 | ('default_date_range', '30', 'integer', 'Default date range in days'), 113 | ('theme', 'system', 'string', 'Application theme (light, dark, system)'), 114 | ('auto_refresh', 'true', 'boolean', 'Enable auto-refresh for real-time quotes'), 115 | ('show_tooltips', 'true', 'boolean', 'Show tooltips throughout the application'), 116 | ('chart_height', '400', 'integer', 'Default chart height in pixels'), 117 | ] 118 | 119 | for key, value, data_type, description in defaults: 120 | if not cls.query.filter_by(key=key).first(): 121 | cls.set_value(key, value, data_type, description) 122 | 123 | logging.info("Default settings initialized") 124 | 125 | def to_dict(self): 126 | """Convert to dictionary""" 127 | return { 128 | 'id': self.id, 129 | 'key': self.key, 130 | 'value': self.value, 131 | 'data_type': self.data_type, 132 | 'description': self.description, 133 | 'is_encrypted': self.is_encrypted, 134 | 'created_at': self.created_at.isoformat() if self.created_at else None, 135 | 'updated_at': self.updated_at.isoformat() if self.updated_at else None 136 | } -------------------------------------------------------------------------------- /historify/app/routes/scheduler.py: -------------------------------------------------------------------------------- 1 | """ 2 | Historify - Stock Historical Data Management App 3 | Scheduler Routes Blueprint 4 | """ 5 | from flask import Blueprint, request, jsonify, render_template 6 | from app.utils.scheduler import scheduler_manager 7 | import logging 8 | 9 | scheduler_bp = Blueprint('scheduler', __name__) 10 | 11 | @scheduler_bp.route('/scheduler') 12 | def scheduler_page(): 13 | """Scheduler management page""" 14 | return render_template('scheduler.html') 15 | 16 | @scheduler_bp.route('/api/scheduler/jobs', methods=['GET']) 17 | def get_scheduler_jobs(): 18 | """Get all scheduled jobs""" 19 | try: 20 | logging.info("Getting scheduler jobs") 21 | jobs = scheduler_manager.get_jobs() 22 | logging.info(f"Found {len(jobs)} jobs") 23 | return jsonify(jobs) 24 | except Exception as e: 25 | logging.error(f"Error in get_scheduler_jobs: {str(e)}") 26 | return jsonify({'error': str(e)}), 500 27 | 28 | @scheduler_bp.route('/api/scheduler/jobs', methods=['POST']) 29 | def create_scheduler_job(): 30 | """Create a new scheduled job""" 31 | try: 32 | data = request.json 33 | job_type = data.get('type') 34 | 35 | if job_type == 'daily': 36 | time_str = data.get('time') 37 | if not time_str: 38 | return jsonify({'error': 'Time is required for daily jobs'}), 400 39 | 40 | job_id = scheduler_manager.add_daily_download_job( 41 | time_str=time_str, 42 | symbols=data.get('symbols'), 43 | exchanges=data.get('exchanges'), 44 | interval=data.get('interval', 'D'), 45 | job_id=data.get('job_id') 46 | ) 47 | 48 | elif job_type == 'interval': 49 | minutes = data.get('minutes') 50 | if not minutes: 51 | return jsonify({'error': 'Minutes is required for interval jobs'}), 400 52 | 53 | job_id = scheduler_manager.add_interval_download_job( 54 | minutes=int(minutes), 55 | symbols=data.get('symbols'), 56 | exchanges=data.get('exchanges'), 57 | interval=data.get('interval', 'D'), 58 | job_id=data.get('job_id') 59 | ) 60 | 61 | elif job_type == 'market_close': 62 | job_id = scheduler_manager.add_market_close_job( 63 | job_id=data.get('job_id') 64 | ) 65 | 66 | elif job_type == 'pre_market': 67 | job_id = scheduler_manager.add_pre_market_job( 68 | job_id=data.get('job_id') 69 | ) 70 | 71 | else: 72 | return jsonify({'error': 'Invalid job type'}), 400 73 | 74 | return jsonify({ 75 | 'job_id': job_id, 76 | 'message': 'Job created successfully' 77 | }) 78 | 79 | except Exception as e: 80 | logging.error(f"Error creating scheduler job: {str(e)}") 81 | return jsonify({'error': str(e)}), 500 82 | 83 | @scheduler_bp.route('/api/scheduler/jobs/', methods=['DELETE']) 84 | def delete_scheduler_job(job_id): 85 | """Delete a scheduled job""" 86 | try: 87 | success = scheduler_manager.remove_job(job_id) 88 | if success: 89 | return jsonify({'message': 'Job deleted successfully'}) 90 | else: 91 | return jsonify({'error': 'Failed to delete job'}), 400 92 | except Exception as e: 93 | return jsonify({'error': str(e)}), 500 94 | 95 | @scheduler_bp.route('/api/scheduler/jobs//pause', methods=['POST']) 96 | def pause_scheduler_job(job_id): 97 | """Pause a scheduled job""" 98 | try: 99 | success = scheduler_manager.pause_job(job_id) 100 | if success: 101 | return jsonify({'message': 'Job paused successfully'}) 102 | else: 103 | return jsonify({'error': 'Failed to pause job'}), 400 104 | except Exception as e: 105 | return jsonify({'error': str(e)}), 500 106 | 107 | @scheduler_bp.route('/api/scheduler/jobs//resume', methods=['POST']) 108 | def resume_scheduler_job(job_id): 109 | """Resume a paused job""" 110 | try: 111 | success = scheduler_manager.resume_job(job_id) 112 | if success: 113 | return jsonify({'message': 'Job resumed successfully'}) 114 | else: 115 | return jsonify({'error': 'Failed to resume job'}), 400 116 | except Exception as e: 117 | return jsonify({'error': str(e)}), 500 118 | 119 | @scheduler_bp.route('/api/scheduler/jobs//run', methods=['POST']) 120 | def run_scheduler_job_now(job_id): 121 | """Run a job immediately""" 122 | try: 123 | # Get job info 124 | jobs = scheduler_manager.get_jobs() 125 | job_info = next((j for j in jobs if j['id'] == job_id), None) 126 | 127 | if not job_info: 128 | return jsonify({'error': 'Job not found'}), 404 129 | 130 | # Execute the download within app context 131 | from flask import current_app 132 | with current_app.app_context(): 133 | scheduler_manager._execute_download( 134 | symbols=job_info.get('symbols'), 135 | exchanges=job_info.get('exchanges'), 136 | interval=job_info.get('interval', 'D') 137 | ) 138 | 139 | return jsonify({'message': 'Job executed successfully'}) 140 | 141 | except Exception as e: 142 | logging.error(f"Error running job {job_id}: {str(e)}") 143 | return jsonify({'error': str(e)}), 500 144 | 145 | @scheduler_bp.route('/api/scheduler/test', methods=['POST']) 146 | def test_scheduler(): 147 | """Test scheduler with a quick job""" 148 | try: 149 | logging.info("Creating test scheduler job") 150 | 151 | # Add a simple interval job for testing 152 | job_id = scheduler_manager.add_interval_download_job( 153 | minutes=1, # Run every minute for testing 154 | symbols=None, # Use all watchlist symbols 155 | exchanges=None, 156 | interval='D', 157 | job_id='test_job_interval' 158 | ) 159 | 160 | return jsonify({ 161 | 'message': 'Test job created - runs every minute', 162 | 'job_id': job_id 163 | }) 164 | 165 | except Exception as e: 166 | logging.error(f"Error creating test job: {str(e)}") 167 | return jsonify({'error': str(e)}), 500 -------------------------------------------------------------------------------- /historify/app/models/dynamic_tables.py: -------------------------------------------------------------------------------- 1 | """ 2 | Historify - Stock Historical Data Management App 3 | Dynamic Table Factory for Symbol-Exchange-Interval Combinations 4 | """ 5 | from app.models import db 6 | from datetime import datetime 7 | import re 8 | import logging 9 | 10 | # Dictionary to store dynamically created model classes 11 | _table_models = {} 12 | 13 | def get_table_name(symbol, exchange, interval): 14 | """Generate a valid table name for the symbol-exchange-interval combination""" 15 | # Replace any non-alphanumeric characters with underscore 16 | symbol_clean = re.sub(r'[^a-zA-Z0-9]', '_', symbol) 17 | exchange_clean = re.sub(r'[^a-zA-Z0-9]', '_', exchange) 18 | interval_clean = re.sub(r'[^a-zA-Z0-9]', '_', interval) 19 | 20 | # Create table name in format: data_symbol_exchange_interval 21 | return f"data_{symbol_clean}_{exchange_clean}_{interval_clean}".lower() 22 | 23 | def get_table_model(symbol, exchange, interval): 24 | """ 25 | Get or create a SQLAlchemy model for the specified symbol-exchange-interval 26 | 27 | Args: 28 | symbol: Stock symbol (e.g., 'RELIANCE') 29 | exchange: Exchange code (e.g., 'NSE') 30 | interval: Data interval (e.g., '1m', 'D') 31 | 32 | Returns: 33 | SQLAlchemy model class for the specified combination 34 | """ 35 | table_name = get_table_name(symbol, exchange, interval) 36 | 37 | # If model already exists, return it 38 | if table_name in _table_models: 39 | return _table_models[table_name] 40 | 41 | # Create a new model class dynamically 42 | class_name = f"Data_{symbol}_{exchange}_{interval}".replace('-', '_') 43 | 44 | # Create the model class dynamically 45 | model = type(class_name, (db.Model,), { 46 | '__tablename__': table_name, 47 | 'id': db.Column(db.Integer, primary_key=True), 48 | 'date': db.Column(db.Date, nullable=False, index=True), 49 | 'time': db.Column(db.Time, nullable=True, index=True), 50 | 'open': db.Column(db.Float, nullable=False), 51 | 'high': db.Column(db.Float, nullable=False), 52 | 'low': db.Column(db.Float, nullable=False), 53 | 'close': db.Column(db.Float, nullable=False), 54 | 'volume': db.Column(db.Integer, nullable=False), 55 | 'created_at': db.Column(db.DateTime, default=datetime.utcnow), 56 | '__table_args__': ( 57 | db.UniqueConstraint('date', 'time', name=f'uix_{table_name}_date_time'), 58 | ) 59 | }) 60 | 61 | # Add methods to the model 62 | def to_dict(self): 63 | return { 64 | 'id': self.id, 65 | 'date': self.date.strftime('%Y-%m-%d'), 66 | 'time': self.time.strftime('%H:%M:%S') if self.time else None, 67 | 'open': self.open, 68 | 'high': self.high, 69 | 'low': self.low, 70 | 'close': self.close, 71 | 'volume': self.volume, 72 | 'created_at': self.created_at.isoformat() if self.created_at else None 73 | } 74 | 75 | model.to_dict = to_dict 76 | 77 | # Store the model in our dictionary 78 | _table_models[table_name] = model 79 | 80 | logging.info(f"Created dynamic table model: {class_name} ({table_name})") 81 | 82 | return model 83 | 84 | def ensure_table_exists(symbol, exchange, interval): 85 | """ 86 | Ensure the table for the specified combination exists in the database 87 | 88 | Args: 89 | symbol: Stock symbol 90 | exchange: Exchange code 91 | interval: Data interval 92 | 93 | Returns: 94 | The model class for the table 95 | """ 96 | model = get_table_model(symbol, exchange, interval) 97 | 98 | # Create the table if it doesn't exist 99 | from sqlalchemy import inspect 100 | inspector = inspect(db.engine) 101 | if not inspector.has_table(model.__tablename__): 102 | model.__table__.create(db.engine) 103 | logging.info(f"Created table in database: {model.__tablename__}") 104 | 105 | return model 106 | 107 | def get_data_by_timeframe(symbol, exchange, interval, start_date, end_date): 108 | """ 109 | Get data for the specified symbol, exchange, interval and date range 110 | 111 | Args: 112 | symbol: Stock symbol 113 | exchange: Exchange code 114 | interval: Data interval 115 | start_date: Start date (datetime.date) 116 | end_date: End date (datetime.date) 117 | 118 | Returns: 119 | List of data points 120 | """ 121 | model = ensure_table_exists(symbol, exchange, interval) 122 | 123 | query = model.query.filter( 124 | model.date >= start_date, 125 | model.date <= end_date 126 | ).order_by(model.date, model.time) 127 | 128 | return query.all() 129 | 130 | def get_available_tables(): 131 | """ 132 | Get a list of all available data tables 133 | 134 | Returns: 135 | List of dictionaries with symbol, exchange, interval info 136 | """ 137 | # Query all tables that start with 'data_' 138 | from sqlalchemy import inspect 139 | inspector = inspect(db.engine) 140 | tables = [] 141 | 142 | for table_name in inspector.get_table_names(): 143 | if table_name.startswith('data_'): 144 | # Parse table name to extract symbol, exchange, interval 145 | parts = table_name.split('_') 146 | if len(parts) >= 4: # data_symbol_exchange_interval 147 | tables.append({ 148 | 'table_name': table_name, 149 | 'symbol': parts[1].upper(), 150 | 'exchange': parts[2].upper(), 151 | 'interval': parts[3] 152 | }) 153 | 154 | return tables 155 | 156 | def get_earliest_date(symbol, exchange, interval): 157 | """ 158 | Get the earliest available date for the specified symbol, exchange, and interval 159 | 160 | Args: 161 | symbol: Stock symbol 162 | exchange: Exchange code 163 | interval: Data interval 164 | 165 | Returns: 166 | datetime.date: Earliest available date or None if no data exists 167 | """ 168 | try: 169 | model = ensure_table_exists(symbol, exchange, interval) 170 | 171 | # Query the earliest date from the table 172 | earliest_record = model.query.order_by(model.date.asc()).first() 173 | 174 | if earliest_record: 175 | return earliest_record.date 176 | else: 177 | return None 178 | 179 | except Exception as e: 180 | logging.error(f"Error getting earliest date for {symbol} ({exchange}) {interval}: {str(e)}") 181 | return None 182 | -------------------------------------------------------------------------------- /sample/tradingview-yahoo-finance-main/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, jsonify, request 2 | import yfinance as yf 3 | import pandas as pd 4 | import pandas_ta as ta 5 | from datetime import datetime, timedelta 6 | import json 7 | import os 8 | from models import db, Symbol 9 | 10 | app = Flask(__name__) 11 | 12 | # Configure SQLAlchemy with SQLite 13 | basedir = os.path.abspath(os.path.dirname(__file__)) 14 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'symbols.db') 15 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 16 | 17 | # Initialize database 18 | db.init_app(app) 19 | 20 | def fetch_yahoo_data(ticker, interval, ema_period=20, rsi_period=14): 21 | ticker = yf.Ticker(ticker) 22 | 23 | end_date = datetime.now() 24 | if interval in ['1m', '5m']: 25 | start_date = end_date - timedelta(days=7) 26 | elif interval in ['15m', '60m']: 27 | start_date = end_date - timedelta(days=60) 28 | elif interval == '1d': 29 | start_date = end_date - timedelta(days=365*5) 30 | elif interval == '1wk': 31 | start_date = end_date - timedelta(weeks=365*5) 32 | elif interval == '1mo': 33 | start_date = end_date - timedelta(days=365*5) 34 | 35 | data = ticker.history(start=start_date, end=end_date, interval=interval) 36 | data['EMA'] = ta.ema(data['Close'], length=ema_period) 37 | data['RSI'] = ta.rsi(data['Close'], length=rsi_period) 38 | 39 | candlestick_data = [ 40 | { 41 | 'time': int(row.Index.timestamp()), 42 | 'open': row.Open, 43 | 'high': row.High, 44 | 'low': row.Low, 45 | 'close': row.Close 46 | } 47 | for row in data.itertuples() 48 | ] 49 | 50 | ema_data = [ 51 | { 52 | 'time': int(row.Index.timestamp()), 53 | 'value': row.EMA 54 | } 55 | for row in data.itertuples() if not pd.isna(row.EMA) 56 | ] 57 | 58 | rsi_data = [ 59 | { 60 | 'time': int(row.Index.timestamp()), 61 | 'value': row.RSI if not pd.isna(row.RSI) else 0 # Convert NaN to zero 62 | } 63 | for row in data.itertuples() 64 | ] 65 | 66 | return candlestick_data, ema_data, rsi_data 67 | 68 | @app.route('/') 69 | def index(): 70 | return render_template('index.html') 71 | 72 | @app.route('/api/data////') 73 | def get_data(ticker, interval, ema_period, rsi_period): 74 | candlestick_data, ema_data, rsi_data = fetch_yahoo_data(ticker, interval, ema_period, rsi_period) 75 | return jsonify({'candlestick': candlestick_data, 'ema': ema_data, 'rsi': rsi_data}) 76 | 77 | # Create database tables on startup if they don't exist 78 | with app.app_context(): 79 | db.create_all() 80 | 81 | # Add default symbols if the database is empty 82 | if Symbol.query.count() == 0: 83 | default_symbols = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'META', 'TSLA', 'NVDA', 'NFLX'] 84 | for ticker in default_symbols: 85 | if not Symbol.query.filter_by(ticker=ticker).first(): 86 | symbol = Symbol(ticker=ticker) 87 | db.session.add(symbol) 88 | db.session.commit() 89 | print(f'Added {len(default_symbols)} default symbols') 90 | 91 | @app.route('/api/symbols') 92 | def get_symbols(): 93 | # Get symbols from database 94 | db_symbols = Symbol.query.all() 95 | symbol_list = [symbol.ticker for symbol in db_symbols] 96 | 97 | # Get real quotes for symbols 98 | try: 99 | if not symbol_list: 100 | return jsonify([]) 101 | 102 | symbols_str = ' '.join(symbol_list) 103 | tickers = yf.Tickers(symbols_str) 104 | 105 | symbols_data = [] 106 | for symbol in db_symbols: 107 | try: 108 | ticker_info = tickers.tickers[symbol.ticker].info 109 | quote_data = { 110 | 'id': symbol.id, 111 | 'symbol': symbol.ticker, 112 | 'price': ticker_info.get('currentPrice', 0), 113 | 'change': ticker_info.get('regularMarketChangePercent', 0), 114 | 'name': ticker_info.get('shortName', symbol.ticker), 115 | } 116 | symbols_data.append(quote_data) 117 | except Exception as e: 118 | # Fallback data if we can't get info for a particular symbol 119 | symbols_data.append({ 120 | 'id': symbol.id, 121 | 'symbol': symbol.ticker, 122 | 'price': 0, 123 | 'change': 0, 124 | 'name': symbol.ticker, 125 | }) 126 | print(f"Error getting data for {symbol.ticker}: {e}") 127 | 128 | return jsonify(symbols_data) 129 | 130 | except Exception as e: 131 | print(f"Error fetching quotes: {e}") 132 | # Fallback to just returning the symbols without data 133 | return jsonify([{'id': s.id, 'symbol': s.ticker, 'price': 0, 'change': 0, 'name': s.ticker} for s in db_symbols]) 134 | 135 | @app.route('/api/symbols', methods=['POST']) 136 | def add_symbol(): 137 | data = request.json 138 | if not data or 'symbol' not in data: 139 | return jsonify({'error': 'Symbol is required'}), 400 140 | 141 | ticker = data['symbol'].strip().upper() 142 | if not ticker: 143 | return jsonify({'error': 'Symbol cannot be empty'}), 400 144 | 145 | # Check if symbol already exists 146 | existing = Symbol.query.filter_by(ticker=ticker).first() 147 | if existing: 148 | return jsonify({'error': 'Symbol already exists', 'symbol': existing.to_dict()}), 409 149 | 150 | # Validate symbol with yfinance 151 | try: 152 | info = yf.Ticker(ticker).info 153 | if 'regularMarketPrice' not in info and 'currentPrice' not in info: 154 | return jsonify({'error': 'Invalid symbol'}), 400 155 | 156 | # Add symbol to database 157 | symbol = Symbol(ticker=ticker, name=info.get('shortName', ticker)) 158 | db.session.add(symbol) 159 | db.session.commit() 160 | 161 | return jsonify({'message': 'Symbol added successfully', 'symbol': symbol.to_dict()}), 201 162 | except Exception as e: 163 | return jsonify({'error': f'Error adding symbol: {str(e)}'}), 400 164 | 165 | @app.route('/api/symbols/', methods=['DELETE']) 166 | def delete_symbol(symbol_id): 167 | symbol = Symbol.query.get_or_404(symbol_id) 168 | db.session.delete(symbol) 169 | db.session.commit() 170 | return jsonify({'message': 'Symbol deleted successfully'}), 200 171 | 172 | if __name__ == '__main__': 173 | app.run(debug=True) -------------------------------------------------------------------------------- /sample/datafetch.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import logging 3 | import os 4 | import time 5 | import gc 6 | from openalgo import api 7 | from datetime import datetime, timedelta 8 | 9 | # Initialize the API client 10 | client = api(api_key='f54abe985aa0897aa56463e4064176bfa33a737e17583e3431ee3bb1da887e58', host='http://127.0.0.1:5000') 11 | 12 | # Path to the CSV file 13 | symbols_file = "symbols.csv" 14 | output_folder = "symbols" 15 | checkpoint_file = "checkpoint.txt" 16 | 17 | # Create the output folder if it doesn't exist 18 | os.makedirs(output_folder, exist_ok=True) 19 | 20 | # Set up logging 21 | logging.basicConfig( 22 | filename="data_download.log", 23 | level=logging.INFO, 24 | format="%(asctime)s - %(levelname)s - %(message)s", 25 | ) 26 | 27 | # Function to get start date based on user selection 28 | def get_date_range(option): 29 | today = datetime.now() 30 | if option == 1: 31 | return today.strftime("%Y-%m-%d"), today.strftime("%Y-%m-%d") 32 | elif option == 2: 33 | return (today - timedelta(days=5)).strftime("%Y-%m-%d"), today.strftime("%Y-%m-%d") 34 | elif option == 3: 35 | return (today - timedelta(days=30)).strftime("%Y-%m-%d"), today.strftime("%Y-%m-%d") 36 | elif option == 4: 37 | return (today - timedelta(days=90)).strftime("%Y-%m-%d"), today.strftime("%Y-%m-%d") 38 | elif option == 5: 39 | return (today - timedelta(days=365)).strftime("%Y-%m-%d"), today.strftime("%Y-%m-%d") 40 | elif option == 6: 41 | return (today - timedelta(days=365 * 2)).strftime("%Y-%m-%d"), today.strftime("%Y-%m-%d") 42 | elif option == 7: 43 | return (today - timedelta(days=365 * 5)).strftime("%Y-%m-%d"), today.strftime("%Y-%m-%d") 44 | elif option == 8: 45 | return (today - timedelta(days=365 * 10)).strftime("%Y-%m-%d"), today.strftime("%Y-%m-%d") 46 | else: 47 | raise ValueError("Invalid selection") 48 | 49 | # Prompt user for fresh download or continuation 50 | print("Select download mode:") 51 | print("1) Fresh download") 52 | print("2) Continue from the last checkpoint") 53 | 54 | try: 55 | mode_choice = int(input("Enter your choice (1-2): ")) 56 | if mode_choice not in [1, 2]: 57 | raise ValueError("Invalid selection") 58 | except ValueError: 59 | print("Invalid input. Please restart the script and select a valid option.") 60 | exit() 61 | 62 | # Prompt user for time period 63 | print("Select the time period for data download:") 64 | print("1) Download Today's Data") 65 | print("2) Download Last 5 Days Data") 66 | print("3) Download Last 30 Days Data") 67 | print("4) Download Last 90 Days Data") 68 | print("5) Download Last 1 Year Data") 69 | print("6) Download Last 2 Years Data") 70 | print("7) Download Last 5 Years Data") 71 | print("8) Download Last 10 Years Data") 72 | 73 | try: 74 | user_choice = int(input("Enter your choice (1-8): ")) 75 | start_date, end_date = get_date_range(user_choice) 76 | except ValueError as e: 77 | print("Invalid input. Please restart the script and select a valid option.") 78 | exit() 79 | 80 | # Read symbols from CSV 81 | symbols = pd.read_csv(symbols_file, header=None)[0].tolist() 82 | 83 | # Handle checkpoint logic 84 | if mode_choice == 2 and os.path.exists(checkpoint_file): 85 | with open(checkpoint_file, "r") as f: 86 | last_processed = f.read().strip() 87 | # Skip symbols up to the last processed one 88 | if last_processed in symbols: 89 | symbols = symbols[symbols.index(last_processed) + 1:] 90 | elif mode_choice == 1: 91 | # Remove existing checkpoint for fresh download 92 | if os.path.exists(checkpoint_file): 93 | os.remove(checkpoint_file) 94 | 95 | # Process symbols in batches 96 | batch_size = 10 # Adjust this value based on your memory availability 97 | for i in range(0, len(symbols), batch_size): 98 | batch = symbols[i:i + batch_size] 99 | for symbol in batch: 100 | logging.info(f"Starting download for {symbol}") 101 | try: 102 | # Skip already downloaded symbols 103 | output_file = os.path.join(output_folder, f"{symbol}.csv") 104 | if os.path.exists(output_file): 105 | logging.info(f"Skipping {symbol}, already downloaded") 106 | continue 107 | 108 | # Fetch historical data for the symbol 109 | for attempt in range(3): # Retry up to 3 times 110 | try: 111 | response = client.history( 112 | symbol=symbol, 113 | exchange="NSE", 114 | interval="1m", 115 | start_date=start_date, 116 | end_date=end_date 117 | ) 118 | break 119 | except Exception as e: 120 | logging.warning(f"Retry {attempt + 1} for {symbol} due to error: {e}") 121 | time.sleep(5) # Wait before retrying 122 | else: 123 | logging.error(f"Failed to download data for {symbol} after 3 attempts") 124 | continue 125 | 126 | # Convert the response to a DataFrame if it's a dictionary 127 | if isinstance(response, dict): 128 | if "timestamp" in response: 129 | df = pd.DataFrame(response) 130 | else: 131 | logging.error(f"Response for {symbol} missing 'timestamp' key: {response}") 132 | continue 133 | else: 134 | df = response 135 | 136 | # Ensure the DataFrame is not empty 137 | if df.empty: 138 | logging.warning(f"No data available for {symbol}") 139 | continue 140 | 141 | # Reset the index to extract the timestamp 142 | df.reset_index(inplace=True) 143 | 144 | # Rename and split the timestamp column 145 | df['DATE'] = pd.to_datetime(df['timestamp']).dt.date 146 | df['TIME'] = pd.to_datetime(df['timestamp']).dt.time 147 | 148 | # Add SYMBOL column and rearrange columns 149 | df['SYMBOL'] = symbol 150 | df = df[['SYMBOL', 'DATE', 'TIME', 'open', 'high', 'low', 'close', 'volume']] 151 | df.columns = ['SYMBOL', 'DATE', 'TIME', 'OPEN', 'HIGH', 'LOW', 'CLOSE', 'VOLUME'] 152 | 153 | # Save to CSV file 154 | df.to_csv(output_file, index=False) 155 | logging.info(f"Data for {symbol} saved to {output_file}") 156 | 157 | # Save checkpoint after successfully processing the symbol 158 | with open(checkpoint_file, "w") as f: 159 | f.write(symbol) 160 | 161 | # Clear DataFrame and force garbage collection 162 | del df 163 | gc.collect() 164 | 165 | except Exception as e: 166 | logging.error(f"Failed to download data for {symbol}: {e}") 167 | 168 | # Delay to avoid rate limiting 169 | time.sleep(3) 170 | 171 | logging.info(f"Batch of {batch_size} symbols completed.") 172 | 173 | logging.info("All data downloaded.") -------------------------------------------------------------------------------- /historify/app/templates/charts.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}{{ title }}{% endblock %} 4 | 5 | {% block extra_css %} 6 | 7 | 61 | {% endblock %} 62 | 63 | {% block extra_js %} 64 | 65 | 66 | 67 | 68 | {% endblock %} 69 | 70 | {% block content %} 71 |
72 |
73 |
74 |

TradingView Charts

75 | 76 |
77 | 78 |
79 |
80 | 86 | 97 |
98 |
99 | 100 | 101 |
102 | 111 |
112 | 113 | 114 |
115 |
116 | 117 | 118 | 119 |
120 |
121 | 122 | 123 |
124 | 128 |
129 |
130 |
131 | 132 | 133 |
134 |
135 |
136 | 137 | 138 |
139 |
140 |
141 | 142 | 143 |
144 |

Active Indicators

145 |
146 |
147 | EMA 20 148 | × 149 |
150 |
151 | RSI 14 152 | × 153 |
154 |
155 |
156 |
157 |
158 | 159 | 160 |
161 |
162 |

Data Summary

163 | 164 |
165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 |
SymbolExchangeIntervalOpenHighLowCloseChangeVolume
No data available
186 |
187 |
188 |
189 | {% endblock %} 190 | -------------------------------------------------------------------------------- /docs/prd.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Product Requirements Document (PRD) for Historical Stock Market Data Management Tool 4 | 5 | *(Designed for LLM Model Consumption)* 6 | 7 | --- 8 | 9 | ### 1. **Product Overview** 10 | 11 | Build a web-based application to download, store, and visualize historical and real-time stock market data with the following key features: 12 | 13 | - Download historical and incremental stock data via OpenAlgo API (Python library). 14 | - Store all data directly in a SQLite database (no intermediate CSV files). 15 | - Provide a dynamic watchlist with real-time quotes and persistent storage. 16 | - Visualize data using TradingView lightweight charts with multiple timeframes and auto-update. 17 | - Responsive user interface built with Python Flask backend, Tailwind CSS, and DaisyUI frontend. 18 | 19 | Supported Exchanges 20 | 21 | 22 | NSE: NSE Equity 23 | 24 | NFO: NSE Futures & Options 25 | 26 | CDS: NSE Currency 27 | 28 | BSE: BSE Equity 29 | 30 | BFO: BSE Futures & Options 31 | 32 | BCD: BSE Currency 33 | 34 | MCX: MCX Commodity 35 | 36 | Supported Intervals 37 | 38 | intervals() function in openalgo python library returns the following intervals 39 | 40 | {'data': {'days': ['D'], 41 | 'hours': ['1h'], 42 | 'minutes': ['10m', '15m', '1m', '30m', '3m', '5m'], 43 | 'months': [], 44 | 'seconds': [], 45 | 'weeks': []}, 46 | 'status': 'success'} 47 | 48 | --- 49 | 50 | ### 2. **Core Functionalities** 51 | 52 | #### 2.1 Data Download and Management 53 | 54 | - **Data Source:** Use OpenAlgo Python API client to fetch historical stock data. 55 | - **Download Modes:** 56 | - Fresh download: Download full historical data for selected symbols and date ranges. 57 | - Incremental update: Continue downloading new data from last checkpoint to keep database current. 58 | - **Date Ranges:** Support predefined ranges (today, last 5 days, 30 days, 90 days, 1 year, 2 years, 5 years, 10 years). 59 | - **Batch Processing:** Process symbols in configurable batches with retry and error logging. 60 | - **Checkpointing:** Track last successfully downloaded symbol and timestamp for resuming interrupted downloads. 61 | - **Data Storage:** 62 | - Store all OHLCV data directly into SQLite tables. 63 | - Use a unified table schema for all symbols with columns: SYMBOL, DATE, TIME, OPEN, HIGH, LOW, CLOSE, VOLUME. 64 | - Implement upsert logic to avoid duplicate records during incremental updates. 65 | - No CSV files are saved or used as intermediate storage. 66 | 67 | 68 | #### 2.2 Watchlist Management 69 | 70 | - Allow users to create and manage a dynamic watchlist of stock symbols. 71 | - Persist watchlist data in SQLite to maintain state across sessions. 72 | - Support adding/removing symbols with immediate UI updates and smooth animations. 73 | - Display real-time quotes for all watchlist symbols, updating automatically. 74 | 75 | 76 | #### 2.3 Data Visualization 77 | 78 | - Integrate TradingView Lightweight Charts for interactive data visualization. 79 | - Support multiple timeframes: 1 minute, 5 minutes, 15 minutes, hourly, daily, weekly, monthly. 80 | - Enable auto-update of charts for real-time data monitoring. 81 | - Provide chart features such as zoom, pan, tooltips, and markers. 82 | - Allow users to select symbols and timeframes for visualization from the watchlist. 83 | - Use Python libraries (e.g., lightweight-charts-python) for seamless integration with Flask backend and frontend. 84 | 85 | --- 86 | 87 | ### 3. **User Interface Requirements** 88 | 89 | - Responsive web UI built with Tailwind CSS and DaisyUI components. 90 | - Clean and intuitive layout with: 91 | - Symbol selection and watchlist management panel. 92 | - Date range and timeframe selectors for data download and charting. 93 | - Download progress and status indicators. 94 | - Interactive chart display area with TradingView charts. 95 | - Real-time feedback on data download status and errors. 96 | - Smooth animations for watchlist updates. 97 | 98 | --- 99 | 100 | ### 4. **Data Model** 101 | 102 | **SQLite Database Schema:** 103 | 104 | 105 | | Table Name | Columns | Description | 106 | | :-- | :-- | :-- | 107 | | stock_data | id (PK), symbol (TEXT), date (DATE), time (TIME), open (REAL), high (REAL), low (REAL), close (REAL), volume (INTEGER) | Stores OHLCV data for all symbols | 108 | | watchlist | id (PK), symbol (TEXT UNIQUE), added_on (DATETIME) | Stores user’s watchlist symbols | 109 | | checkpoints | id (PK), symbol (TEXT), last_downloaded_date (DATE), last_downloaded_time (TIME) | Tracks last downloaded data points | 110 | 111 | - Indexes on (symbol, date, time) for efficient querying. 112 | 113 | --- 114 | 115 | ### 5. **API Endpoints** 116 | 117 | | Endpoint | Method | Parameters | Description | 118 | | :-- | :-- | :-- | :-- | 119 | | `/api/symbols` | GET | None | Returns list of available symbols | 120 | | `/api/download` | POST | symbols[], start_date, end_date, mode (fresh/continue) | Triggers data download and storage | 121 | | `/api/watchlist` | GET/POST/DELETE | symbol (for POST/DELETE) | Manage watchlist symbols | 122 | | `/api/data` | GET | symbol, start_date, end_date, timeframe | Fetch OHLCV data for chart visualization | 123 | | `/api/quotes` | GET | symbols[] | Fetch real-time quotes for watchlist symbols | 124 | 125 | 126 | --- 127 | 128 | ### 6. **TradingView Chart Integration Features** 129 | 130 | - **Data Input:** Feed OHLCV data from SQLite via backend API to TradingView charts. 131 | - **Timeframes Supported:** 1m, 5m, 15m, 1h, 1d, 1w, 1mo. 132 | - **Real-Time Updates:** Auto-refresh chart data at user-configurable intervals. 133 | - **Watchlist Sync:** Selecting a symbol from the watchlist updates the chart instantly. 134 | - **Chart Features:** Zoom, pan, tooltips, markers, multi-pane support. 135 | - **Technology:** Use `lightweight-charts-python` or equivalent Python wrapper for TradingView charts embedded in Flask frontend. 136 | - **Smooth UI:** Ensure animations and transitions for chart updates and watchlist changes. 137 | 138 | --- 139 | 140 | ### 7. **Non-Functional Requirements** 141 | 142 | - **Performance:** Efficient batch downloads and database writes; responsive UI updates. 143 | - **Reliability:** Robust error handling, retries, and checkpointing for uninterrupted data downloads. 144 | - **Security:** Secure API keys, sanitize inputs, and protect database access. 145 | - **Maintainability:** Modular codebase with clear separation of backend, frontend, and data layers. 146 | - **Scalability:** Design database and API to handle growing data volumes and user watchlists. 147 | 148 | --- 149 | 150 | ### 8. **Development and Deployment Notes** 151 | 152 | - Use Python Flask for backend REST API and business logic. 153 | - Tailwind CSS and DaisyUI for frontend styling and UI components. 154 | - SQLite as lightweight embedded database, suitable for local or small-scale deployments. 155 | - Use OpenAlgo Python client for data fetching; no CSV intermediate files. 156 | - Integrate TradingView lightweight charts via Python wrappers for seamless embedding. 157 | - Provide clear and user-friendly UI feedback during operations. 158 | 159 | --- 160 | 161 | ### 9. **Summary** 162 | 163 | This PRD defines a fully integrated stock market data management tool that: 164 | 165 | - Downloads and updates historical stock data directly into SQLite without CSV files. 166 | - Maintains a dynamic, persistent watchlist with real-time quotes. 167 | - Provides rich, interactive TradingView-style charts supporting multiple timeframes and real-time updates. 168 | - Offers a clean, responsive UI with Tailwind and DaisyUI. 169 | - Is designed for extensibility, reliability, and ease of use. 170 | 171 | --- 172 | 173 | *This document is structured to guide LLM-based code generation and system design, emphasizing direct database storage, rich visualization, and user-centric features.* 174 | 175 | -------------------------------------------------------------------------------- /historify/app/static/js/command-palette.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Command Palette functionality 3 | */ 4 | 5 | const commandPalette = { 6 | modal: null, 7 | searchInput: null, 8 | resultsContainer: null, 9 | 10 | init() { 11 | this.modal = document.getElementById('command-palette'); 12 | this.searchInput = document.getElementById('command-search'); 13 | this.resultsContainer = document.getElementById('command-results'); 14 | 15 | // Keyboard shortcuts 16 | document.addEventListener('keydown', (e) => { 17 | // Cmd/Ctrl + K to open 18 | if ((e.metaKey || e.ctrlKey) && e.key === 'k') { 19 | e.preventDefault(); 20 | this.open(); 21 | } 22 | 23 | // Escape to close 24 | if (e.key === 'Escape' && !this.modal.classList.contains('hidden')) { 25 | this.close(); 26 | } 27 | }); 28 | 29 | // Click trigger 30 | const trigger = document.getElementById('command-palette-trigger'); 31 | if (trigger) { 32 | trigger.addEventListener('click', () => this.open()); 33 | } 34 | 35 | // Search input handler 36 | this.searchInput.addEventListener('input', (e) => { 37 | this.search(e.target.value); 38 | }); 39 | 40 | // Handle result clicks 41 | this.resultsContainer.addEventListener('click', (e) => { 42 | const resultItem = e.target.closest('.command-result'); 43 | if (resultItem) { 44 | this.executeCommand(resultItem.dataset.action, resultItem.dataset.value); 45 | } 46 | }); 47 | }, 48 | 49 | open() { 50 | this.modal.classList.remove('hidden'); 51 | this.searchInput.value = ''; 52 | this.searchInput.focus(); 53 | this.showDefaultResults(); 54 | 55 | // Add show animation 56 | setTimeout(() => { 57 | this.modal.querySelector('.modal-backdrop').classList.add('show'); 58 | this.modal.querySelector('.modal-content').classList.add('show'); 59 | }, 10); 60 | }, 61 | 62 | close() { 63 | this.modal.querySelector('.modal-backdrop').classList.remove('show'); 64 | this.modal.querySelector('.modal-content').classList.remove('show'); 65 | 66 | setTimeout(() => { 67 | this.modal.classList.add('hidden'); 68 | }, 300); 69 | }, 70 | 71 | showDefaultResults() { 72 | const defaultCommands = [ 73 | { icon: 'fa-download', label: 'Download Data', description: 'Bulk download historical data', action: 'navigate', value: '/download' }, 74 | { icon: 'fa-file-import', label: 'Import Symbols', description: 'Import symbols from CSV/Excel', action: 'navigate', value: '/import' }, 75 | { icon: 'fa-file-export', label: 'Export Data', description: 'Export data to various formats', action: 'navigate', value: '/export' }, 76 | { icon: 'fa-chart-line', label: 'View Charts', description: 'Open interactive charts', action: 'navigate', value: '/charts' }, 77 | { icon: 'fa-eye', label: 'Watchlist', description: 'Manage your watchlist', action: 'navigate', value: '/watchlist' }, 78 | { icon: 'fa-cog', label: 'Settings', description: 'Configure application settings', action: 'navigate', value: '/settings' }, 79 | ]; 80 | 81 | this.renderResults(defaultCommands); 82 | }, 83 | 84 | search(query) { 85 | if (!query) { 86 | this.showDefaultResults(); 87 | return; 88 | } 89 | 90 | // Simulated search - in production, this would query the backend 91 | const allCommands = [ 92 | { icon: 'fa-download', label: 'Download Data', description: 'Bulk download historical data', action: 'navigate', value: '/download' }, 93 | { icon: 'fa-file-import', label: 'Import Symbols', description: 'Import symbols from CSV/Excel', action: 'navigate', value: '/import' }, 94 | { icon: 'fa-file-export', label: 'Export Data', description: 'Export data to various formats', action: 'navigate', value: '/export' }, 95 | { icon: 'fa-chart-line', label: 'View Charts', description: 'Open interactive charts', action: 'navigate', value: '/charts' }, 96 | { icon: 'fa-eye', label: 'Watchlist', description: 'Manage your watchlist', action: 'navigate', value: '/watchlist' }, 97 | { icon: 'fa-plus', label: 'Add Symbol to Watchlist', description: 'Add a new symbol', action: 'modal', value: 'add-symbol' }, 98 | { icon: 'fa-sync', label: 'Refresh Data', description: 'Refresh all data', action: 'function', value: 'refreshData' }, 99 | { icon: 'fa-moon', label: 'Toggle Dark Mode', description: 'Switch theme', action: 'function', value: 'toggleTheme' }, 100 | { icon: 'fa-cog', label: 'Settings', description: 'Configure application settings', action: 'navigate', value: '/settings' }, 101 | ]; 102 | 103 | const filtered = allCommands.filter(cmd => 104 | cmd.label.toLowerCase().includes(query.toLowerCase()) || 105 | cmd.description.toLowerCase().includes(query.toLowerCase()) 106 | ); 107 | 108 | this.renderResults(filtered); 109 | }, 110 | 111 | renderResults(results) { 112 | if (results.length === 0) { 113 | this.resultsContainer.innerHTML = ` 114 |
115 | 116 |

No results found

117 |
118 | `; 119 | return; 120 | } 121 | 122 | const html = results.map((result, index) => ` 123 |
125 |
126 |
127 | 128 |
129 |
130 |
${result.label}
131 |
${result.description}
132 |
133 |
134 | 135 |
136 |
137 |
138 | `).join(''); 139 | 140 | this.resultsContainer.innerHTML = html; 141 | }, 142 | 143 | executeCommand(action, value) { 144 | switch (action) { 145 | case 'navigate': 146 | window.location.href = value; 147 | break; 148 | case 'modal': 149 | this.close(); 150 | // Handle modal opening based on value 151 | if (value === 'add-symbol' && window.watchlistManager) { 152 | window.watchlistManager.showAddModal(); 153 | } 154 | break; 155 | case 'function': 156 | this.close(); 157 | // Execute function based on value 158 | if (value === 'toggleTheme' && window.themeToggle) { 159 | window.themeToggle.click(); 160 | } 161 | if (value === 'refreshData') { 162 | window.location.reload(); 163 | } 164 | break; 165 | } 166 | } 167 | }; 168 | 169 | // Initialize when DOM is ready 170 | document.addEventListener('DOMContentLoaded', () => { 171 | commandPalette.init(); 172 | }); 173 | 174 | // Make close function available globally 175 | window.closeCommandPalette = () => commandPalette.close(); -------------------------------------------------------------------------------- /historify/app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %}Historify - Stock Historical Data Management{% endblock %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {% block extra_css %}{% endblock %} 29 | 30 | 31 | 32 | 123 | 124 | 125 |
126 | 127 |
128 |
129 |
130 |

131 | {% block page_title %}Dashboard{% endblock %} 132 |

133 | {% block breadcrumb %}{% endblock %} 134 |
135 | 136 |
137 | 138 | 143 |
144 |
145 |
146 | 147 | 148 |
149 |
150 | {% block content %}{% endblock %} 151 |
152 |
153 |
154 | 155 | 156 |
157 | 158 | 159 | 174 | 175 | 176 | 177 | 178 | 179 | {% block extra_js %}{% endblock %} 180 | 181 | -------------------------------------------------------------------------------- /historify/app/templates/download.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Bulk Download - Historify{% endblock %} 4 | 5 | {% block content %} 6 |
7 | 8 |
9 |
10 |
11 |

Bulk Download

12 |

Download historical data for multiple symbols

13 |
14 |
15 | 19 | 23 |
24 |
25 |
26 | 27 | 28 |
29 | 30 |
31 |

Select Symbols

32 | 33 | 34 |
35 | 37 |
38 | 39 | 40 |
41 | 42 |
43 | 44 |

Loading symbols...

45 |
46 |
47 | 48 |
49 | Selected: 0 symbols 50 |
51 |
52 | 53 | 54 |
55 |

Download Settings

56 | 57 |
58 | 59 |
60 | 63 |
64 | 65 | 66 |
67 |
68 | 69 | 70 |
71 | 74 | 84 |
85 | 86 | 87 |
88 | 91 |
92 | 96 | 100 |
101 |
102 | 103 | 104 | 108 |
109 |
110 |
111 | 112 | 113 | 168 | 169 | 170 |
171 |

Recent Downloads

172 |
173 | 174 |

No recent downloads

175 |
176 |
177 |
178 | 179 | 180 |
181 | {% endblock %} 182 | 183 | {% block extra_js %} 184 | 185 | {% endblock %} -------------------------------------------------------------------------------- /historify/app/templates/scheduler.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Scheduler Manager - Historify{% endblock %} 4 | 5 | {% block content %} 6 |
7 | 8 |
9 |
10 |
11 |

Scheduler Manager

12 |

Automate data downloads at specific times

13 |
14 | 18 |
19 |
20 | 21 | 22 |
23 | 34 | 35 | 46 | 47 | 58 |
59 | 60 | 61 |
62 |

Active Jobs

63 | 64 |
65 |
66 | 67 |

Loading scheduled jobs...

68 |
69 |
70 |
71 |
72 | 73 | 74 | 179 | 180 | 181 |
182 | {% endblock %} 183 | 184 | {% block extra_js %} 185 | 186 | {% endblock %} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Historify - Modern Stock Data Management Dashboard 2 | 3 | Historify is a professional-grade web application for downloading, managing, and visualizing historical stock market data. Built with a modern, intuitive admin dashboard interface, it provides comprehensive tools for bulk data operations, real-time monitoring, and advanced charting capabilities. 4 | 5 | ![Historify Architecture](historify/app/static/image/historify.png) 6 | 7 | ## 🚀 Key Features 8 | 9 | ### Modern Dashboard Interface 10 | - **Professional Design System**: Clean, modern UI inspired by Stripe, Vercel, and Supabase dashboards 11 | - **Dark/Light Mode**: Seamless theme switching with persistent preferences 12 | - **Responsive Layout**: Fully responsive design that works on desktop and mobile devices 13 | - **Command Palette**: Quick access to all features with keyboard shortcuts (Cmd/Ctrl + K) 14 | 15 | ### Data Management 16 | - **Bulk Symbol Import**: 17 | - CSV and Excel file support with drag-and-drop interface 18 | - Paste data directly from clipboard 19 | - Manual entry with auto-complete 20 | - Real-time validation and duplicate detection 21 | - Exchange auto-detection with manual override 22 | 23 | - **Bulk Data Export**: 24 | - Multiple export formats (Individual CSV, Combined CSV, ZIP archives) 25 | - Custom date range selection with presets 26 | - Configurable intervals (1m, 5m, 15m, 30m, 1h, daily) 27 | - Background processing for large exports 28 | - Export queue management with progress tracking 29 | 30 | - **Bulk Download**: 31 | - One-click download for entire watchlist 32 | - Smart scheduling with API rate limit respect 33 | - Resume capability for interrupted downloads 34 | - Parallel processing with thread pool management 35 | - Real-time progress tracking with ETA 36 | 37 | ### Advanced Features 38 | - **Multiple Exchange Support**: NSE, BSE, NFO, MCX, CDS and more 39 | - **Dynamic Watchlist**: Real-time quotes with auto-refresh 40 | - **TradingView Charts**: Professional-grade charting with technical indicators 41 | - **Technical Indicators**: EMA, RSI with customizable parameters 42 | - **Incremental Updates**: Checkpoint system for efficient data updates 43 | - **Data Quality Monitoring**: Track data completeness and quality metrics 44 | 45 | ### Scheduler Manager 46 | - **Automated Data Downloads**: Schedule downloads at specific times in IST 47 | - **Flexible Scheduling Options**: 48 | - Daily schedules at specific times 49 | - Interval-based schedules (every N minutes) 50 | - Pre-configured market close (3:35 PM IST) and pre-market (8:30 AM IST) schedules 51 | - **Job Management**: Pause, resume, delete, or run jobs immediately 52 | - **Background Processing**: Non-blocking scheduled downloads 53 | - **Watchlist Integration**: Automatically download all watchlist symbols 54 | - **Custom Symbol Selection**: Schedule downloads for specific symbols 55 | 56 | ## 📦 Installation 57 | 58 | 1. **Clone the repository**: 59 | ```bash 60 | git clone https://github.com/marketcalls/historify.git 61 | cd historify/historify 62 | ``` 63 | 64 | 2. **Create virtual environment**: 65 | ```bash 66 | python -m venv venv 67 | source venv/bin/activate # On Windows: venv\Scripts\activate 68 | ``` 69 | 70 | 3. **Install dependencies**: 71 | ```bash 72 | pip install -r requirements.txt 73 | ``` 74 | 75 | **Note**: The scheduler feature requires APScheduler which is included in requirements.txt 76 | 77 | 4. **Configure environment**: 78 | ```bash 79 | cp .env.sample .env 80 | # Edit .env with your API keys and settings 81 | ``` 82 | 83 | 5. **Run the application**: 84 | ```bash 85 | python run.py 86 | ``` 87 | 88 | 6. **Access the dashboard**: 89 | Open `http://localhost:5001` in your browser 90 | 91 | ## 🏗️ Project Structure 92 | 93 | ``` 94 | historify/ 95 | ├── app/ 96 | │ ├── models/ # Database models 97 | │ ├── routes/ # API and page routes 98 | │ ├── static/ 99 | │ │ ├── css/ # Stylesheets and design system 100 | │ │ ├── js/ # JavaScript modules 101 | │ │ └── image/ # Static images 102 | │ ├── templates/ # Jinja2 templates 103 | │ └── utils/ # Utility functions 104 | ├── instance/ # Instance-specific data 105 | ├── requirements.txt # Python dependencies 106 | └── run.py # Application entry point 107 | ``` 108 | 109 | ## 🎨 Design System 110 | 111 | The application features a comprehensive design system with: 112 | 113 | - **Color Palette**: Semantic colors for success, warning, error, and info states 114 | - **Typography**: Inter font family with clear hierarchy 115 | - **Components**: Reusable buttons, cards, tables, modals, and form elements 116 | - **Animations**: Smooth transitions and loading states 117 | - **Icons**: FontAwesome integration for consistent iconography 118 | 119 | ## 🔌 API Endpoints 120 | 121 | ### Data Management 122 | - `POST /api/download` - Download historical data 123 | - `POST /api/import-symbols` - Import symbols to watchlist 124 | - `POST /api/export` - Export data in various formats 125 | - `GET /api/export/queue` - Check export queue status 126 | - `GET /api/export/download/` - Download exported CSV file 127 | 128 | ### Watchlist 129 | - `GET /api/symbols` - Get available symbols 130 | - `GET /api/quotes` - Fetch real-time quotes 131 | - `GET /watchlist/items` - Manage watchlist 132 | 133 | ### Charts 134 | - `GET /charts/api/chart-data/////` - Chart data with indicators 135 | 136 | ### Scheduler 137 | - `GET /api/scheduler/jobs` - Get all scheduled jobs 138 | - `POST /api/scheduler/jobs` - Create a new scheduled job 139 | - `DELETE /api/scheduler/jobs/` - Delete a scheduled job 140 | - `POST /api/scheduler/jobs//pause` - Pause a scheduled job 141 | - `POST /api/scheduler/jobs//resume` - Resume a paused job 142 | - `POST /api/scheduler/jobs//run` - Run a job immediately 143 | - `POST /api/scheduler/test` - Create a test job 144 | 145 | ## 💻 Technology Stack 146 | 147 | - **Backend**: Flask, SQLAlchemy, SQLite, APScheduler 148 | - **Frontend**: 149 | - Tailwind CSS + DaisyUI for styling 150 | - TradingView Lightweight Charts 5.0.0 151 | - Vanilla JavaScript with modern ES6+ 152 | - **Data Processing**: Pandas, NumPy for technical analysis 153 | - **API Integration**: OpenAlgo API for market data 154 | - **Task Scheduling**: APScheduler with IST timezone support 155 | 156 | ## 🔧 Configuration 157 | 158 | ### Environment Variables (.env) 159 | ```env 160 | # API Configuration 161 | OPENALGO_API_KEY=your_api_key_here 162 | OPENALGO_API_URL=http://127.0.0.1:5000 163 | 164 | # Database 165 | DATABASE_URI=sqlite:///historify.db 166 | 167 | # App Settings 168 | SECRET_KEY=your_secret_key_here 169 | DEBUG=False 170 | ``` 171 | 172 | ### OpenAlgo Integration 173 | Add to your OpenAlgo `.env`: 174 | ```env 175 | CORS_ALLOWED_ORIGINS = 'http://127.0.0.1:5000,http://127.0.0.1:5001' 176 | ``` 177 | 178 | ## 📊 Usage Guide 179 | 180 | ### Importing Symbols 181 | 1. Navigate to Import page from sidebar 182 | 2. Choose import method: 183 | - **File Upload**: Drag & drop CSV/Excel files 184 | - **Paste Data**: Copy/paste from spreadsheets 185 | - **Manual Entry**: Type symbols with auto-complete 186 | 3. Map columns and validate data 187 | 4. Review validation results 188 | 5. Import valid symbols 189 | 190 | ### Exporting Data 191 | 1. Go to Export page 192 | 2. Select symbols (use search/filters) 193 | 3. Choose date range and interval 194 | 4. Select export format 195 | 5. Configure options (headers, metadata) 196 | 6. Start export (background processing for large datasets) 197 | 198 | ### Bulk Download 199 | 1. Access Download page 200 | 2. Select symbols or use watchlist 201 | 3. Configure intervals and date ranges 202 | 4. Choose download mode (fresh/continue) 203 | 5. Monitor real-time progress 204 | 6. Handle failures with automatic retry 205 | 206 | ### Scheduler Configuration 207 | 1. Navigate to Scheduler page from sidebar 208 | 2. Quick setup options: 209 | - **Market Close Download**: Automatically download at 3:35 PM IST 210 | - **Pre-Market Download**: Download before market opens at 8:30 AM IST 211 | - **Test Scheduler**: Run a test job in 10 seconds 212 | 3. Custom schedules: 213 | - **Daily Schedule**: Set specific time in IST 214 | - **Interval Schedule**: Run every N minutes 215 | - Configure data interval (1m, 5m, 15m, daily, etc.) 216 | - Select all watchlist symbols or specific symbols 217 | 4. Manage jobs: 218 | - View next run time and status 219 | - Pause/resume jobs as needed 220 | - Run jobs immediately 221 | - Delete unwanted schedules 222 | 223 | ## 🚦 Performance Optimizations 224 | 225 | - **Batch Processing**: Symbols processed in configurable batches 226 | - **Rate Limiting**: Respects API rate limits (10 symbols/second) 227 | - **Database Optimization**: Dynamic table creation per symbol-interval 228 | - **Lazy Loading**: Virtual scrolling for large datasets 229 | - **Background Jobs**: Queue system for long-running operations 230 | - **Efficient Export**: Streams CSV data directly without loading entire dataset in memory 231 | - **Scheduled Downloads**: Non-blocking background processing for automated downloads 232 | 233 | ## 🛡️ Security Features 234 | 235 | - **Input Validation**: Comprehensive validation for all user inputs 236 | - **Session Management**: Secure session handling for export operations 237 | - **Environment Variables**: Sensitive data in .env files 238 | - **SQL Injection Prevention**: SQLAlchemy ORM queries 239 | - **XSS Protection**: Template auto-escaping 240 | - **Safe Dynamic Table Creation**: Sanitized table names for symbol-interval combinations 241 | 242 | ## 🤝 Contributing 243 | 244 | Contributions are welcome! Please: 245 | 246 | 1. Fork the repository 247 | 2. Create a feature branch 248 | 3. Commit your changes 249 | 4. Push to the branch 250 | 5. Create a Pull Request 251 | 252 | ## 🐛 Known Issues & Fixes 253 | 254 | ### Data Export 255 | - **Fixed**: Export was returning only 1 record with incorrect dates 256 | - **Solution**: Implemented proper database querying and CSV generation from dynamic tables 257 | 258 | ### Recent Updates 259 | - **v1.2.0**: Added Scheduler Manager for automated downloads 260 | - **v1.1.5**: Fixed data export functionality with proper date handling 261 | - **v1.1.0**: Upgraded to TradingView Charts 5.0.0 262 | 263 | ## 📝 License 264 | 265 | This project is licensed under the AGPLv3 License - see the [LICENSE](LICENSE) file for details. 266 | 267 | ## 🙏 Acknowledgments 268 | 269 | - TradingView for the excellent charting library 270 | - OpenAlgo for market data API 271 | - APScheduler for robust task scheduling 272 | - The Flask and SQLAlchemy communities 273 | 274 | ## 📞 Support 275 | 276 | For issues and feature requests, please use the [GitHub issue tracker](https://github.com/marketcalls/historify/issues). -------------------------------------------------------------------------------- /historify/app/routes/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Historify - Stock Historical Data Management App 3 | Settings Routes Blueprint 4 | """ 5 | from flask import Blueprint, request, jsonify, render_template 6 | from app.models.settings import AppSettings 7 | from app.models import db 8 | import logging 9 | 10 | settings_bp = Blueprint('settings', __name__) 11 | 12 | @settings_bp.route('/settings') 13 | def settings_page(): 14 | """Render the settings page""" 15 | return render_template('settings.html') 16 | 17 | @settings_bp.route('/api/settings', methods=['GET']) 18 | def get_settings(): 19 | """Get all application settings""" 20 | try: 21 | settings = AppSettings.get_all_settings() 22 | return jsonify(settings) 23 | except Exception as e: 24 | current_app.logger.error(f"Error getting settings: {str(e)}") 25 | return jsonify({'error': str(e)}), 500 26 | 27 | @settings_bp.route('/api/settings', methods=['POST']) 28 | def update_settings(): 29 | """Update application settings""" 30 | try: 31 | data = request.get_json() 32 | if not data: 33 | return jsonify({'status': 'error', 'message': 'No data received'}), 400 34 | 35 | results = {} 36 | errors = {} 37 | something_changed = False 38 | 39 | # Special handling for API key and host to ensure they are fresh 40 | api_key_to_set = data.pop('openalgo_api_key', None) 41 | api_host_to_set = data.pop('openalgo_api_host', None) 42 | 43 | if api_key_to_set is not None: 44 | try: 45 | # Delete existing API key entries first 46 | existing_keys = AppSettings.query.filter_by(key='openalgo_api_key').all() 47 | for ek in existing_keys: 48 | db.session.delete(ek) 49 | # Add the new API key if provided 50 | if api_key_to_set: # Only add if it's not empty 51 | AppSettings.set_value('openalgo_api_key', api_key_to_set, data_type='string', description='OpenAlgo API Key') 52 | results['openalgo_api_key'] = 'set' 53 | else: 54 | results['openalgo_api_key'] = 'cleared' 55 | something_changed = True 56 | except Exception as e: 57 | db.session.rollback() 58 | current_app.logger.error(f"Error setting openalgo_api_key: {e}", exc_info=True) 59 | errors['openalgo_api_key'] = str(e) 60 | 61 | if api_host_to_set is not None: 62 | try: 63 | # Delete existing API host entries first 64 | existing_hosts = AppSettings.query.filter_by(key='openalgo_api_host').all() 65 | for eh in existing_hosts: 66 | db.session.delete(eh) 67 | # Add the new API host 68 | AppSettings.set_value('openalgo_api_host', api_host_to_set, data_type='string', description='OpenAlgo API Host URL') 69 | results['openalgo_api_host'] = 'set' 70 | something_changed = True 71 | except Exception as e: 72 | db.session.rollback() 73 | current_app.logger.error(f"Error setting openalgo_api_host: {e}", exc_info=True) 74 | errors['openalgo_api_host'] = str(e) 75 | 76 | # Process other settings 77 | for key, value_info in data.items(): 78 | if isinstance(value_info, dict) and 'value' in value_info and 'type' in value_info: 79 | value = value_info['value'] 80 | data_type = value_info['type'] 81 | description = value_info.get('description') 82 | try: 83 | AppSettings.set_value(key, value, data_type, description) 84 | results[key] = 'updated' 85 | something_changed = True 86 | except Exception as e: 87 | db.session.rollback() 88 | current_app.logger.error(f"Error updating setting {key}: {e}", exc_info=True) 89 | errors[key] = str(e) 90 | else: 91 | # Fallback for simple key-value pairs 92 | try: 93 | AppSettings.set_value(key, value_info, 'string') # Default to string 94 | results[key] = 'updated_as_string' 95 | something_changed = True 96 | except Exception as e: 97 | db.session.rollback() 98 | current_app.logger.error(f"Error updating setting {key} (as string): {e}", exc_info=True) 99 | errors[key] = str(e) 100 | 101 | if something_changed and not errors: 102 | try: 103 | db.session.commit() 104 | except Exception as e: 105 | db.session.rollback() 106 | current_app.logger.error(f"Error during final commit: {e}", exc_info=True) 107 | return jsonify({'status': 'error', 'message': f'Error committing settings: {str(e)}'}), 500 108 | elif errors: 109 | db.session.rollback() 110 | return jsonify({'status': 'error', 'message': 'Error updating one or more settings', 'errors': errors}), 400 111 | 112 | if not errors: 113 | return jsonify({'status': 'success', 'message': 'Settings updated successfully', 'updated_settings': results}), 200 114 | else: 115 | return jsonify({'status': 'error', 'message': 'Failed to update some settings', 'errors': errors, 'updated_settings': results}), 400 116 | 117 | except Exception as e: 118 | current_app.logger.error(f"Error updating settings: {str(e)}") 119 | return jsonify({'error': str(e)}), 500 120 | 121 | @settings_bp.route('/api/settings/test-api', methods=['POST']) 122 | def test_api_connection(): 123 | """Test OpenAlgo API connection by fetching RELIANCE NSE quotes""" 124 | try: 125 | from app.utils.data_fetcher import OPENALGO_AVAILABLE 126 | 127 | if not OPENALGO_AVAILABLE: 128 | return jsonify({ 129 | 'success': False, 130 | 'message': 'OpenAlgo module not available' 131 | }) 132 | 133 | # Get current API settings 134 | api_key = AppSettings.get_value('openalgo_api_key') 135 | host = AppSettings.get_value('openalgo_api_host', 'http://127.0.0.1:5000') 136 | 137 | if not api_key: 138 | return jsonify({ 139 | 'success': False, 140 | 'message': 'API key not configured' 141 | }) 142 | 143 | # Test the connection by fetching RELIANCE quotes 144 | from openalgo import api 145 | client = api(api_key=api_key, host=host) 146 | 147 | try: 148 | # Test with RELIANCE NSE quotes 149 | response = client.quotes(symbol='RELIANCE', exchange='NSE') 150 | 151 | if isinstance(response, dict) and response.get('status') == 'success': 152 | quote_data = response.get('data', {}) 153 | ltp = quote_data.get('ltp', 0) 154 | return jsonify({ 155 | 'success': True, 156 | 'message': f'API connection successful! RELIANCE LTP: ₹{ltp}', 157 | 'data': { 158 | 'symbol': 'RELIANCE', 159 | 'exchange': 'NSE', 160 | 'ltp': ltp, 161 | 'host': host 162 | } 163 | }) 164 | else: 165 | error_msg = response.get('message', 'Unknown API error') if isinstance(response, dict) else 'Invalid response format' 166 | return jsonify({ 167 | 'success': False, 168 | 'message': f'API error: {error_msg}' 169 | }) 170 | 171 | except Exception as e: 172 | return jsonify({ 173 | 'success': False, 174 | 'message': f'API connection failed: {str(e)}' 175 | }) 176 | 177 | except Exception as e: 178 | logging.error(f"Error testing API: {str(e)}") 179 | return jsonify({ 180 | 'success': False, 181 | 'message': f'Connection test failed: {str(e)}' 182 | }) 183 | 184 | @settings_bp.route('/api/settings/database-info', methods=['GET']) 185 | def get_database_info(): 186 | """Get database information""" 187 | try: 188 | # Get database size and stats 189 | from sqlalchemy import text 190 | 191 | # Get table information 192 | result = db.session.execute(text(""" 193 | SELECT name FROM sqlite_master 194 | WHERE type='table' AND name NOT LIKE 'sqlite_%' 195 | """)) 196 | tables = [row[0] for row in result.fetchall()] 197 | 198 | # Get total record count 199 | total_records = 0 200 | table_stats = {} 201 | 202 | for table in tables: 203 | try: 204 | result = db.session.execute(text(f"SELECT COUNT(*) FROM {table}")) 205 | count = result.fetchone()[0] 206 | table_stats[table] = count 207 | total_records += count 208 | except Exception as e: 209 | table_stats[table] = f"Error: {str(e)}" 210 | 211 | # Get database file size (approximate) 212 | import os 213 | db_path = 'instance/historify.db' 214 | db_size = "Unknown" 215 | if os.path.exists(db_path): 216 | size_bytes = os.path.getsize(db_path) 217 | if size_bytes < 1024: 218 | db_size = f"{size_bytes} B" 219 | elif size_bytes < 1024 * 1024: 220 | db_size = f"{size_bytes / 1024:.1f} KB" 221 | elif size_bytes < 1024 * 1024 * 1024: 222 | db_size = f"{size_bytes / (1024 * 1024):.1f} MB" 223 | else: 224 | db_size = f"{size_bytes / (1024 * 1024 * 1024):.1f} GB" 225 | 226 | return jsonify({ 227 | 'db_size': db_size, 228 | 'total_records': total_records, 229 | 'table_count': len(tables), 230 | 'table_stats': table_stats 231 | }) 232 | 233 | except Exception as e: 234 | logging.error(f"Error getting database info: {str(e)}") 235 | return jsonify({'error': str(e)}), 500 236 | 237 | @settings_bp.route('/api/settings/clear-cache', methods=['POST']) 238 | def clear_cache(): 239 | """Clear application cache""" 240 | try: 241 | # Clear any cached data (implement based on your caching strategy) 242 | # For now, just return success 243 | return jsonify({ 244 | 'success': True, 245 | 'message': 'Cache cleared successfully' 246 | }) 247 | except Exception as e: 248 | return jsonify({ 249 | 'success': False, 250 | 'message': f'Failed to clear cache: {str(e)}' 251 | }) 252 | 253 | @settings_bp.route('/api/settings/optimize-database', methods=['POST']) 254 | def optimize_database(): 255 | """Optimize database""" 256 | try: 257 | # Run SQLite VACUUM command 258 | db.session.execute(text("VACUUM")) 259 | db.session.commit() 260 | 261 | return jsonify({ 262 | 'success': True, 263 | 'message': 'Database optimized successfully' 264 | }) 265 | except Exception as e: 266 | logging.error(f"Error optimizing database: {str(e)}") 267 | return jsonify({ 268 | 'success': False, 269 | 'message': f'Database optimization failed: {str(e)}' 270 | }) 271 | 272 | @settings_bp.route('/api/settings/reset', methods=['POST']) 273 | def reset_settings(): 274 | """Reset settings to defaults""" 275 | try: 276 | # Delete all settings and reinitialize 277 | AppSettings.query.delete() 278 | db.session.commit() 279 | 280 | AppSettings.initialize_default_settings() 281 | 282 | return jsonify({ 283 | 'success': True, 284 | 'message': 'Settings reset to defaults' 285 | }) 286 | except Exception as e: 287 | logging.error(f"Error resetting settings: {str(e)}") 288 | db.session.rollback() 289 | return jsonify({ 290 | 'success': False, 291 | 'message': f'Failed to reset settings: {str(e)}' 292 | }) -------------------------------------------------------------------------------- /historify/app/routes/charts.py: -------------------------------------------------------------------------------- 1 | """ 2 | Historify - Stock Historical Data Management App 3 | Charts Routes Blueprint 4 | """ 5 | from flask import Blueprint, render_template, request, jsonify, current_app 6 | from app.models.stock_data import StockData 7 | from app.models.watchlist import WatchlistItem 8 | from app.models.dynamic_tables import get_data_by_timeframe, ensure_table_exists, get_available_tables 9 | from datetime import datetime, timedelta 10 | import pandas as pd 11 | import numpy as np 12 | import logging 13 | import pytz 14 | 15 | charts_bp = Blueprint('charts', __name__) 16 | 17 | # Technical indicator calculation functions 18 | def calculate_ema(data, period=20): 19 | """Calculate Exponential Moving Average""" 20 | if len(data) < period: 21 | return [None] * len(data) 22 | 23 | df = pd.DataFrame(data) 24 | df['ema'] = df['close'].ewm(span=period, adjust=False).mean() 25 | return df['ema'].tolist() 26 | 27 | def calculate_sma(data, period=20): 28 | """Calculate Simple Moving Average""" 29 | if len(data) < period: 30 | return [None] * len(data) 31 | 32 | df = pd.DataFrame(data) 33 | df['sma'] = df['close'].rolling(window=period).mean() 34 | return df['sma'].tolist() 35 | 36 | def calculate_rsi(data, period=14): 37 | """Calculate Relative Strength Index""" 38 | if len(data) < period + 1: 39 | return [None] * len(data) 40 | 41 | df = pd.DataFrame(data) 42 | delta = df['close'].diff() 43 | 44 | # Make two series: one for gains and one for losses 45 | gain = delta.clip(lower=0) 46 | loss = -delta.clip(upper=0) 47 | 48 | # First value is sum of gains/losses 49 | avg_gain = gain.rolling(window=period).mean().fillna(0) 50 | avg_loss = loss.rolling(window=period).mean().fillna(0) 51 | 52 | # Calculate RS and RSI 53 | rs = avg_gain / avg_loss.replace(0, np.finfo(float).eps) 54 | rsi = 100 - (100 / (1 + rs)) 55 | 56 | return rsi.tolist() 57 | 58 | def calculate_macd(data, fast_period=12, slow_period=26, signal_period=9): 59 | """Calculate MACD (Moving Average Convergence Divergence)""" 60 | if len(data) < slow_period + signal_period: 61 | return {'macd': [None] * len(data), 'signal': [None] * len(data), 'histogram': [None] * len(data)} 62 | 63 | df = pd.DataFrame(data) 64 | 65 | # Calculate MACD line 66 | ema_fast = df['close'].ewm(span=fast_period, adjust=False).mean() 67 | ema_slow = df['close'].ewm(span=slow_period, adjust=False).mean() 68 | df['macd'] = ema_fast - ema_slow 69 | 70 | # Calculate signal line 71 | df['signal'] = df['macd'].ewm(span=signal_period, adjust=False).mean() 72 | 73 | # Calculate histogram 74 | df['histogram'] = df['macd'] - df['signal'] 75 | 76 | return { 77 | 'macd': df['macd'].tolist(), 78 | 'signal': df['signal'].tolist(), 79 | 'histogram': df['histogram'].tolist() 80 | } 81 | 82 | @charts_bp.route('/') 83 | def index(): 84 | """Render the charts page""" 85 | symbol = request.args.get('symbol') 86 | timeframe = request.args.get('timeframe', '1d') 87 | 88 | # Get all watchlist symbols for the dropdown 89 | watchlist_items = WatchlistItem.query.all() 90 | 91 | return render_template( 92 | 'charts.html', 93 | title="Historify - Charts", 94 | symbol=symbol, 95 | timeframe=timeframe, 96 | watchlist_items=watchlist_items 97 | ) 98 | 99 | @charts_bp.route('/timeframes') 100 | def get_timeframes(): 101 | """Get available chart timeframes""" 102 | timeframes = [ 103 | {"value": "1m", "label": "1 Minute"}, 104 | {"value": "5m", "label": "5 Minutes"}, 105 | {"value": "15m", "label": "15 Minutes"}, 106 | {"value": "30m", "label": "30 Minutes"}, 107 | {"value": "1h", "label": "1 Hour"}, 108 | {"value": "1d", "label": "Daily"}, 109 | {"value": "1w", "label": "Weekly"} 110 | ] 111 | return jsonify(timeframes) 112 | 113 | @charts_bp.route('/debug/tables') 114 | def debug_tables(): 115 | """Debug endpoint to check available tables""" 116 | try: 117 | tables = get_available_tables() 118 | return jsonify({ 119 | 'tables': tables, 120 | 'count': len(tables) 121 | }) 122 | except Exception as e: 123 | return jsonify({'error': str(e)}), 500 124 | 125 | @charts_bp.route('/api/chart-data/////') 126 | def get_chart_data(symbol, exchange, interval, ema_period=20, rsi_period=14): 127 | """Get chart data with indicators for TradingView chart""" 128 | try: 129 | logging.info(f"Fetching chart data for {symbol} ({exchange}) with interval '{interval}', EMA period {ema_period}, RSI period {rsi_period}") 130 | 131 | # Debug: Check available tables first 132 | available_tables = get_available_tables() 133 | matching_tables = [t for t in available_tables if t['symbol'] == symbol.upper() and t['exchange'] == exchange.upper()] 134 | logging.info(f"Available tables for {symbol} ({exchange}): {matching_tables}") 135 | 136 | # Get start and end dates based on interval - use all available data 137 | end_date = datetime.now().date() 138 | 139 | # Instead of hardcoded limits, get the earliest available data from the table 140 | from app.models.dynamic_tables import get_earliest_date 141 | earliest_date = get_earliest_date(symbol, exchange, interval) 142 | 143 | if earliest_date: 144 | start_date = earliest_date 145 | logging.info(f"Using earliest available date: {start_date}") 146 | else: 147 | # Fallback to reasonable defaults only if no data exists 148 | if interval in ['1m', '5m', '15m', '30m']: 149 | start_date = end_date - timedelta(days=7) # 1 week for minute data 150 | elif interval == '1h': 151 | start_date = end_date - timedelta(days=30) # 1 month for hourly data 152 | elif interval == '1d': 153 | start_date = end_date - timedelta(days=365*10) # 10 years for daily data 154 | elif interval == '1w': 155 | start_date = end_date - timedelta(days=365*10) # 10 years for weekly data 156 | else: 157 | start_date = end_date - timedelta(days=365) # Default to 1 year 158 | 159 | logging.info(f"Date range: {start_date} to {end_date}") 160 | 161 | # Fetch data from dynamic table - try multiple interval formats 162 | data = get_data_by_timeframe(symbol, exchange, interval, start_date, end_date) 163 | 164 | # If no data found and interval is daily, try alternative formats 165 | if not data and interval in ['1d', 'D']: 166 | alternative_interval = 'D' if interval == '1d' else '1d' 167 | logging.info(f"No data found with {interval}, trying {alternative_interval}") 168 | data = get_data_by_timeframe(symbol, exchange, alternative_interval, start_date, end_date) 169 | 170 | # Same for weekly 171 | if not data and interval in ['1w', 'W']: 172 | alternative_interval = 'W' if interval == '1w' else '1w' 173 | logging.info(f"No data found with {interval}, trying {alternative_interval}") 174 | data = get_data_by_timeframe(symbol, exchange, alternative_interval, start_date, end_date) 175 | 176 | if not data: 177 | logging.warning(f"No data found for {symbol} ({exchange}) with {interval} interval") 178 | return jsonify({ 179 | 'error': f'No data found for {symbol} ({exchange}) with {interval} interval', 180 | 'candlestick': [], 181 | 'ema': [], 182 | 'rsi': [], 183 | 'macd': [], 184 | 'signal': [], 185 | 'histogram': [] 186 | }), 200 # Return empty data with 200 status to avoid errors 187 | 188 | # Convert to OHLCV format for TradingView 189 | ohlcv_data = [] 190 | for item in data: 191 | try: 192 | # Create datetime from the database date and time 193 | # Handle case where time might be None (daily data) 194 | if item.time is None: 195 | # For daily data, use just the date 196 | if interval in ['D', '1d', 'W', '1w']: 197 | # For daily/weekly data, use the date string in YYYY-MM-DD format 198 | time_obj = item.date.strftime('%Y-%m-%d') 199 | else: 200 | # For intraday data without time, use start of day (00:00:00) 201 | db_datetime_naive = datetime.combine(item.date, datetime.min.time()) 202 | ist_tz = pytz.timezone('Asia/Kolkata') 203 | ist_datetime_aware = ist_tz.localize(db_datetime_naive) 204 | time_obj = int(ist_datetime_aware.timestamp()) # Get UTC Unix timestamp 205 | else: 206 | # For intraday data with time 207 | db_datetime_naive = datetime.combine(item.date, item.time) 208 | ist_tz = pytz.timezone('Asia/Kolkata') 209 | ist_datetime_aware = ist_tz.localize(db_datetime_naive) 210 | time_obj = int(ist_datetime_aware.timestamp()) # Get UTC Unix timestamp 211 | 212 | # Log some sample data for debugging 213 | if len(ohlcv_data) < 3: # Log first few items 214 | logging.info(f"Data point: date={item.date}, time={item.time}, time_obj={time_obj}, interval={interval}") 215 | 216 | except Exception as e: 217 | logging.error(f"Error processing datetime: {e}, date: {item.date}, time: {item.time}") 218 | # Provide a fallback timestamp (current time) 219 | time_obj = int(datetime.now().timestamp()) 220 | 221 | # No need for additional logging here since we already log above 222 | 223 | ohlcv_data.append({ 224 | 'time': time_obj, 225 | 'open': item.open, 226 | 'high': item.high, 227 | 'low': item.low, 228 | 'close': item.close, 229 | 'volume': item.volume 230 | }) 231 | 232 | # Sort by time - now we're using timestamps directly 233 | # No need for a custom sorting function, just sort by the timestamp 234 | ohlcv_data = sorted(ohlcv_data, key=lambda x: x['time']) 235 | 236 | # Calculate indicators 237 | ema_data = [] 238 | ema_values = calculate_ema(ohlcv_data, ema_period) 239 | for i, item in enumerate(ohlcv_data): 240 | if i < len(ema_values) and ema_values[i] is not None: 241 | # Use the same time object format for consistency 242 | ema_data.append({ 243 | 'time': item['time'], # This is already a time object 244 | 'value': ema_values[i] 245 | }) 246 | 247 | rsi_data = [] 248 | rsi_values = calculate_rsi(ohlcv_data, rsi_period) 249 | for i, item in enumerate(ohlcv_data): 250 | if i < len(rsi_values) and rsi_values[i] is not None: 251 | rsi_data.append({ 252 | 'time': item['time'], # This is already a time object 253 | 'value': rsi_values[i] 254 | }) 255 | 256 | # No MACD calculation as per user request 257 | macd_data = [] 258 | signal_data = [] 259 | histogram_data = [] 260 | 261 | # Log success and return data 262 | logging.info(f"Successfully processed {len(ohlcv_data)} data points for {symbol} ({exchange})") 263 | return jsonify({ 264 | 'candlestick': ohlcv_data, 265 | 'ema': ema_data, 266 | 'rsi': rsi_data, 267 | 'macd': macd_data, 268 | 'signal': signal_data, 269 | 'histogram': histogram_data 270 | }) 271 | 272 | except Exception as e: 273 | # Log the error with traceback 274 | logging.error(f"Error processing chart data for {symbol} ({exchange}): {str(e)}", exc_info=True) 275 | 276 | # Return a more informative error message with empty data arrays 277 | return jsonify({ 278 | 'error': f"Error processing chart data: {str(e)}", 279 | 'candlestick': [], 280 | 'ema': [], 281 | 'rsi': [], 282 | 'macd': [], 283 | 'signal': [], 284 | 'histogram': [] 285 | }), 200 # Return 200 with empty data to avoid client-side errors 286 | -------------------------------------------------------------------------------- /historify/app/static/js/dashboard.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dashboard functionality for Historify app 3 | */ 4 | 5 | document.addEventListener('DOMContentLoaded', () => { 6 | // Elements 7 | const symbolCheckboxesContainer = document.getElementById('symbol-checkboxes'); 8 | const selectAllCheckbox = document.getElementById('select-all-symbols'); 9 | const intervalSelect = document.getElementById('interval-select'); 10 | const dateRangeSelect = document.getElementById('date-range'); 11 | const customDateRange = document.getElementById('custom-date-range'); 12 | const startDateInput = document.getElementById('start-date'); 13 | const endDateInput = document.getElementById('end-date'); 14 | const downloadForm = document.getElementById('download-form'); 15 | const downloadBtn = document.getElementById('download-btn'); 16 | const downloadStatus = document.getElementById('download-status'); 17 | const latestDataTable = document.getElementById('latest-data-table'); 18 | 19 | // Set default dates for custom range 20 | const today = new Date(); 21 | const thirtyDaysAgo = new Date(); 22 | thirtyDaysAgo.setDate(today.getDate() - 30); 23 | 24 | startDateInput.valueAsDate = thirtyDaysAgo; 25 | endDateInput.valueAsDate = today; 26 | 27 | // Load watchlist symbols for checkboxes 28 | loadWatchlistSymbols(); 29 | 30 | // Latest data section has been removed 31 | 32 | // Event listeners 33 | dateRangeSelect.addEventListener('change', handleDateRangeChange); 34 | downloadForm.addEventListener('submit', handleDownloadSubmit); 35 | if (selectAllCheckbox) { 36 | selectAllCheckbox.addEventListener('change', handleSelectAllChange); 37 | } 38 | // Listen for changes in the symbol checkboxes container to rebind select-all if needed 39 | symbolCheckboxesContainer.addEventListener('change', (e) => { 40 | if (e.target.classList.contains('symbol-checkbox')) { 41 | updateSelectAllCheckboxState(); 42 | } 43 | }); 44 | 45 | /** 46 | * Load watchlist symbols to populate checkboxes 47 | */ 48 | function loadWatchlistSymbols() { 49 | fetch('/watchlist/items') 50 | .then(response => response.json()) 51 | .then(data => { 52 | if (data.length === 0) { 53 | symbolCheckboxesContainer.innerHTML = ` 54 |
55 | 56 | 57 | 58 | No symbols in watchlist. Add symbols in the Watchlist page. 59 |
60 | `; 61 | return; 62 | } 63 | 64 | let checkboxesHtml = ''; 65 | data.forEach(item => { 66 | checkboxesHtml += ` 67 |
68 | 72 |
73 | `; 74 | }); 75 | 76 | symbolCheckboxesContainer.innerHTML = checkboxesHtml; 77 | // Add event listeners to newly created checkboxes for individual changes 78 | const individualSymbolCheckboxes = document.querySelectorAll('.symbol-checkbox'); 79 | individualSymbolCheckboxes.forEach(checkbox => { 80 | checkbox.addEventListener('change', updateSelectAllCheckboxState); 81 | }); 82 | updateSelectAllCheckboxState(); // Initial check for Select All state 83 | }) 84 | .catch(error => { 85 | console.error('Error loading watchlist:', error); 86 | symbolCheckboxesContainer.innerHTML = ` 87 |
88 | 89 | 90 | 91 | Error loading watchlist symbols. Please try refreshing the page. 92 |
93 | `; 94 | }); 95 | } 96 | 97 | /** 98 | * Handle date range selection change 99 | */ 100 | function handleDateRangeChange() { 101 | const selectedValue = dateRangeSelect.value; 102 | 103 | if (selectedValue === 'custom') { 104 | customDateRange.classList.remove('hidden'); 105 | } else { 106 | customDateRange.classList.add('hidden'); 107 | } 108 | } 109 | 110 | /** 111 | * Handle Select All checkbox change 112 | */ 113 | function handleSelectAllChange() { 114 | const individualSymbolCheckboxes = document.querySelectorAll('.symbol-checkbox'); 115 | individualSymbolCheckboxes.forEach(checkbox => { 116 | checkbox.checked = selectAllCheckbox.checked; 117 | }); 118 | updateSelectAllCheckboxState(); 119 | } 120 | 121 | /** 122 | * Update Select All checkbox based on individual checkbox states 123 | */ 124 | function updateSelectAllCheckboxState() { 125 | if (!selectAllCheckbox) return; // Guard if select-all is not on the page 126 | const individualSymbolCheckboxes = document.querySelectorAll('.symbol-checkbox'); 127 | const allChecked = Array.from(individualSymbolCheckboxes).every(cb => cb.checked); 128 | const someChecked = Array.from(individualSymbolCheckboxes).some(cb => cb.checked); 129 | 130 | if (individualSymbolCheckboxes.length === 0) { 131 | selectAllCheckbox.checked = false; 132 | selectAllCheckbox.indeterminate = false; 133 | return; 134 | } 135 | 136 | selectAllCheckbox.checked = allChecked; 137 | selectAllCheckbox.indeterminate = !allChecked && someChecked; 138 | } 139 | 140 | /** 141 | * Convert date range selection to actual start/end dates 142 | */ 143 | function getDateRange() { 144 | const selectedValue = dateRangeSelect.value; 145 | const today = new Date(); 146 | let startDate = new Date(); 147 | let endDate = today; 148 | 149 | switch (selectedValue) { 150 | case 'today': 151 | startDate = today; 152 | break; 153 | case '5d': 154 | startDate.setDate(today.getDate() - 5); 155 | break; 156 | case '30d': 157 | startDate.setDate(today.getDate() - 30); 158 | break; 159 | case '90d': 160 | startDate.setDate(today.getDate() - 90); 161 | break; 162 | case '1y': 163 | startDate.setFullYear(today.getFullYear() - 1); 164 | break; 165 | case '2y': 166 | startDate.setFullYear(today.getFullYear() - 2); 167 | break; 168 | case '5y': 169 | startDate.setFullYear(today.getFullYear() - 5); 170 | break; 171 | case '10y': 172 | startDate.setFullYear(today.getFullYear() - 10); 173 | break; 174 | case 'custom': 175 | startDate = new Date(startDateInput.value); 176 | endDate = new Date(endDateInput.value); 177 | break; 178 | } 179 | 180 | return { 181 | startDate: startDate.toISOString().split('T')[0], 182 | endDate: endDate.toISOString().split('T')[0] 183 | }; 184 | } 185 | 186 | /** 187 | * Handle download form submission 188 | */ 189 | function handleDownloadSubmit(event) { 190 | event.preventDefault(); 191 | 192 | // Get selected symbols and their exchanges 193 | const checkboxes = document.querySelectorAll('.symbol-checkbox:checked'); 194 | const symbols = Array.from(checkboxes).map(cb => cb.value); 195 | const exchanges = Array.from(checkboxes).map(cb => cb.dataset.exchange || 'NSE'); // Get exchange from data attribute, default to NSE 196 | const interval = intervalSelect ? intervalSelect.value : 'D'; // get selected interval 197 | 198 | if (symbols.length === 0) { 199 | showStatus('error', 'Please select at least one symbol.'); 200 | return; 201 | } 202 | 203 | // Get date range 204 | const { startDate, endDate } = getDateRange(); 205 | 206 | // Get download mode 207 | const downloadMode = document.querySelector('input[name="download-mode"]:checked').value; 208 | 209 | // Show loading state 210 | downloadBtn.disabled = true; 211 | downloadBtn.innerHTML = ` Downloading...`; 212 | 213 | // Show initial status 214 | showStatus('info', `Starting download for ${symbols.length} symbols...`); 215 | 216 | // Make API request 217 | fetch('/api/download', { 218 | method: 'POST', 219 | headers: { 220 | 'Content-Type': 'application/json', 221 | }, 222 | body: JSON.stringify({ 223 | symbols, 224 | exchanges, 225 | interval, // send interval to backend 226 | start_date: startDate, 227 | end_date: endDate, 228 | mode: downloadMode 229 | }) 230 | }) 231 | .then(response => response.json()) 232 | .then(data => { 233 | downloadBtn.disabled = false; 234 | downloadBtn.innerHTML = `Download Data 235 | 236 | 237 | `; 238 | 239 | // Show completion status 240 | if (data.status === 'success') { 241 | showStatus('success', `Successfully downloaded data for ${data.success.length} symbols.`); 242 | } else if (data.status === 'partial') { 243 | showStatus('warning', data.message); 244 | 245 | // Show failed symbols 246 | data.failed.forEach(item => { 247 | showStatus('error', `Failed to download ${item.symbol}: ${item.error}`); 248 | }); 249 | } else { 250 | showStatus('error', data.message || 'Download failed.'); 251 | } 252 | 253 | // Latest data section has been removed, so no need to refresh it 254 | }) 255 | .catch(error => { 256 | downloadBtn.disabled = false; 257 | downloadBtn.innerHTML = `Download Data`; 258 | showStatus('error', 'Download failed. Please try again.'); 259 | console.error('Download error:', error); 260 | }); 261 | } 262 | 263 | /** 264 | * Show a status message 265 | */ 266 | function showStatus(type, message) { 267 | const timestamp = new Date().toLocaleTimeString(); 268 | 269 | let alertClass = ''; 270 | let icon = ''; 271 | 272 | switch (type) { 273 | case 'success': 274 | alertClass = 'alert-success'; 275 | icon = ''; 276 | break; 277 | case 'warning': 278 | alertClass = 'alert-warning'; 279 | icon = ''; 280 | break; 281 | case 'error': 282 | alertClass = 'alert-error'; 283 | icon = ''; 284 | break; 285 | default: 286 | alertClass = 'alert-info'; 287 | icon = ''; 288 | } 289 | 290 | const statusHtml = ` 291 |
292 | ${icon} 293 |
294 |
${timestamp}
295 |
${message}
296 |
297 |
298 | `; 299 | 300 | // Add new status to the top 301 | downloadStatus.innerHTML = statusHtml + downloadStatus.innerHTML; 302 | 303 | // Limit number of status messages 304 | const statusElements = downloadStatus.querySelectorAll('.alert'); 305 | if (statusElements.length > 10) { 306 | statusElements[statusElements.length - 1].remove(); 307 | } 308 | } 309 | 310 | // loadLatestData function removed as requested 311 | }); 312 | -------------------------------------------------------------------------------- /sample/tradingview-yahoo-finance-main/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | TradingView Chart with Yahoo Finance Data 7 | 8 | 9 | 10 | 53 | 143 | 144 | 145 |
146 | 147 | 148 | 149 |
150 | 172 | 173 |
174 |
175 |
176 |
177 |
178 | 181 |
182 |
183 | 184 | 187 |
188 |
189 |
190 | 191 |
192 | 195 | 204 |
205 | 206 |
207 | 210 | 211 |
212 | 213 |
214 | 217 | 218 |
219 | 220 |
221 | 224 |
225 |
226 | 227 | Enable 228 |
229 |
230 | 231 | sec 232 |
233 |
234 |
235 | 236 |
237 | 241 |
242 |
243 |
244 |
245 | 246 |
247 |
248 |
249 |
250 | 251 | 252 |
253 | 254 | 261 |
262 |
263 | 264 | 265 | 266 | 283 | 284 | 285 | --------------------------------------------------------------------------------