Initial commit: ETF Portal with Portfolio Builder and Analyzer components
This commit is contained in:
commit
efc865c2da
52
.gitignore
vendored
Normal file
52
.gitignore
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Streamlit
|
||||
.streamlit/secrets.toml
|
||||
|
||||
# Virtual Environment
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Cache
|
||||
.cache/
|
||||
__pycache__/
|
||||
|
||||
# Local development
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# System
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
9
.streamlit/.streamlit/config.toml
Normal file
9
.streamlit/.streamlit/config.toml
Normal file
@ -0,0 +1,9 @@
|
||||
# .streamlit/config.toml
|
||||
|
||||
[server]
|
||||
# Disable the file watcher to avoid hitting inotify limits
|
||||
fileWatcherType = "none"
|
||||
runOnSave = false
|
||||
|
||||
# Default to headless mode for server
|
||||
headless = true
|
||||
57
Caddyfile
Normal file
57
Caddyfile
Normal file
@ -0,0 +1,57 @@
|
||||
invest.trader-lab.com {
|
||||
# Enable CORS
|
||||
header {
|
||||
Access-Control-Allow-Origin *
|
||||
Access-Control-Allow-Methods "GET, POST, OPTIONS"
|
||||
Access-Control-Allow-Headers "Content-Type"
|
||||
}
|
||||
|
||||
# Main ETF Suite Launcher
|
||||
handle / {
|
||||
reverse_proxy etf-launcher:8500 {
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote}
|
||||
header_up X-Forwarded-For {remote}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
}
|
||||
}
|
||||
|
||||
# Static resources for Streamlit
|
||||
handle /_stcore/* {
|
||||
reverse_proxy etf-launcher:8500 {
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote}
|
||||
header_up X-Forwarded-For {remote}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
}
|
||||
}
|
||||
|
||||
handle /static/* {
|
||||
reverse_proxy etf-launcher:8500 {
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote}
|
||||
header_up X-Forwarded-For {remote}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
}
|
||||
}
|
||||
|
||||
# ETF Portfolio Builder
|
||||
# handle /portfolio* {
|
||||
# reverse_proxy etf-portfolio:8501 {
|
||||
# header_up Host {host}
|
||||
# header_up X-Real-IP {remote}
|
||||
# header_up X-Forwarded-For {remote}
|
||||
# header_up X-Forwarded-Proto {scheme}
|
||||
# }
|
||||
# }
|
||||
|
||||
# ETF Analyzer
|
||||
# handle /analyzer* {
|
||||
# reverse_proxy etf-analyzer:8502 {
|
||||
# header_up Host {host}
|
||||
# header_up X-Real-IP {remote}
|
||||
# header_up X-Forwarded-For {remote}
|
||||
# header_up X-Forwarded-Proto {scheme}
|
||||
# }
|
||||
# }
|
||||
}
|
||||
37
Dockerfile
Normal file
37
Dockerfile
Normal file
@ -0,0 +1,37 @@
|
||||
FROM python:3.8-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
curl \
|
||||
software-properties-common \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements first to leverage Docker cache
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN pip install --no-cache-dir yfinance>=0.2.36
|
||||
|
||||
# Copy the rest of the application
|
||||
COPY . .
|
||||
|
||||
# Install the ETF Suite CLI
|
||||
RUN pip install -e .
|
||||
|
||||
# Expose the port
|
||||
EXPOSE 8500
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV STREAMLIT_SERVER_PORT=8500
|
||||
ENV STREAMLIT_SERVER_ADDRESS=0.0.0.0
|
||||
ENV STREAMLIT_SERVER_HEADLESS=true
|
||||
ENV STREAMLIT_BROWSER_GATHER_USAGE_STATS=false
|
||||
ENV STREAMLIT_SERVER_ENABLE_CORS=true
|
||||
ENV STREAMLIT_SERVER_ENABLE_XSRF_PROTECTION=false
|
||||
|
||||
# Command to run the application
|
||||
CMD ["streamlit", "run", "ETF_Suite_Launcher.py", "--server.port=8500"]
|
||||
173
ETF_Suite_Launcher.py
Normal file
173
ETF_Suite_Launcher.py
Normal file
@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
ETF Portal Launcher
|
||||
|
||||
This script serves as the main entry point for the ETF Portal.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import os
|
||||
|
||||
# Basic page config
|
||||
st.set_page_config(
|
||||
page_title="ETF Portal",
|
||||
page_icon="📊",
|
||||
layout="wide",
|
||||
menu_items={
|
||||
'Get Help': None,
|
||||
'Report a bug': None,
|
||||
'About': None
|
||||
}
|
||||
)
|
||||
|
||||
# Initialize session state for API keys if not already present
|
||||
if 'fmp_api_key' not in st.session_state:
|
||||
st.session_state.fmp_api_key = os.getenv('FMP_API_KEY', '')
|
||||
if 'openai_api_key' not in st.session_state:
|
||||
st.session_state.openai_api_key = os.getenv('OPENAI_API_KEY', '')
|
||||
|
||||
# Custom CSS for improved UI
|
||||
st.markdown("""
|
||||
<style>
|
||||
/* Global text color */
|
||||
.stMarkdown, .stTitle, h1, h2, h3, p, li {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 250px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.welcome-section {
|
||||
text-align: left;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.welcome-section h2 {
|
||||
color: white;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.welcome-section p {
|
||||
color: white;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Style for the sidebar */
|
||||
[data-testid="stSidebar"] {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
[data-testid="stSidebar"] .stTextInput {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
[data-testid="stSidebar"] .stTextInput label {
|
||||
color: white;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
[data-testid="stSidebar"] .stTextInput input {
|
||||
font-size: 0.8rem;
|
||||
padding: 4px 8px;
|
||||
background-color: #2d2d2d;
|
||||
border: 1px solid #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Button styles */
|
||||
.stButton button {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
font-size: 1.2rem;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.stButton button:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.button-column {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Create sidebar for API keys
|
||||
with st.sidebar:
|
||||
st.markdown("### API Configuration")
|
||||
fmp_key = st.text_input(
|
||||
"FMP API Key",
|
||||
value=st.session_state.fmp_api_key,
|
||||
type="password",
|
||||
key="fmp_api_key_field"
|
||||
)
|
||||
if fmp_key != st.session_state.fmp_api_key:
|
||||
st.session_state.fmp_api_key = fmp_key
|
||||
|
||||
openai_key = st.text_input(
|
||||
"OpenAI API Key",
|
||||
value=st.session_state.openai_api_key,
|
||||
type="password",
|
||||
key="openai_api_key_field"
|
||||
)
|
||||
if openai_key != st.session_state.openai_api_key:
|
||||
st.session_state.openai_api_key = openai_key
|
||||
|
||||
# Main content
|
||||
st.markdown('<div class="main-content">', unsafe_allow_html=True)
|
||||
|
||||
# Title with emoji
|
||||
st.title("📊 ETF Portal")
|
||||
|
||||
# Introduction
|
||||
st.markdown("""
|
||||
<div class='welcome-section'>
|
||||
<h2>Welcome to the ETF Portal</h2>
|
||||
<p style='font-size: 1.2rem;'>Your gateway to ETF analysis and portfolio management.</p>
|
||||
<p style='font-size: 1.1rem;'>The portal is currently under maintenance. Please check back later for updates.</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Create two columns for the buttons
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
# ETF Analyzer Button
|
||||
with col1:
|
||||
if st.button("📈 Launch ETF Analyzer", key="analyzer"):
|
||||
st.switch_page("pages/ETF_Analyzer.py")
|
||||
|
||||
# Portfolio Builder Button
|
||||
with col2:
|
||||
if st.button("💼 Launch Portfolio Builder", key="portfolio"):
|
||||
st.switch_page("pages/ETF_Portfolio_Builder.py")
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
# Footer with more information
|
||||
st.markdown("""
|
||||
<div class="footer">
|
||||
<p style='margin: 0;'>ETF Portal - v1.0 | © 2024 ETF Portal</p>
|
||||
<p style='margin: 0;'>For support or questions, please contact the development team.</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
1
cache/48cf9534181b3f9151f2c27d08ecf5c0.json
vendored
Normal file
1
cache/48cf9534181b3f9151f2c27d08ecf5c0.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"data": {"__pandas_series__": true, "index": [
|
||||
1
cache/4ba6cb61b51b694187cfb678fc62dd84.json
vendored
Normal file
1
cache/4ba6cb61b51b694187cfb678fc62dd84.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"data": {"longBusinessSummary": "The fund is an actively managed exchange-traded fund that seeks current income while providing direct and/or indirect exposure to the share price of select U.S. listed securities, subject to a limit on potential investment gains. It uses both traditional and synthetic covered call strategies that are designed to produce higher income levels when the underlying securities experience more volatility. The fund is non-diversified.", "companyOfficers": [], "executiveTeam": [], "maxAge": 86400, "priceHint": 2, "previousClose": 6.1, "open": 6.02, "dayLow": 6.02, "dayHigh": 6.13, "regularMarketPreviousClose": 6.1, "regularMarketOpen": 6.02, "regularMarketDayLow": 6.02, "regularMarketDayHigh": 6.13, "trailingPE": 26.701918, "volume": 2869944, "regularMarketVolume": 2869944, "averageVolume": 1994822, "averageVolume10days": 3513250, "averageDailyVolume10Day": 3513250, "bid": 6.1, "ask": 6.12, "bidSize": 8, "askSize": 30, "yield": 1.6552, "totalAssets": 226379696, "fiftyTwoWeekLow": 5.23, "fiftyTwoWeekHigh": 15.22, "fiftyDayAverage": 6.0538, "twoHundredDayAverage": 8.74755, "navPrice": 6.0906, "currency": "USD", "tradeable": false, "category": "Derivative Income", "ytdReturn": -11.21162, "beta3Year": 0.0, "fundFamily": "YieldMax ETFs", "fundInceptionDate": 1709078400, "legalType": "Exchange Traded Fund", "quoteType": "ETF", "symbol": "ULTY", "language": "en-US", "region": "US", "typeDisp": "ETF", "quoteSourceName": "Delayed Quote", "triggerable": true, "customPriceAlertConfidence": "HIGH", "shortName": "Tidal Trust II YieldMax Ultra O", "longName": "YieldMax Ultra Option Income Strategy ETF", "fiftyTwoWeekLowChange": 0.8699999, "fiftyTwoWeekLowChangePercent": 0.16634797, "fiftyTwoWeekRange": "5.23 - 15.22", "fiftyTwoWeekHighChange": -9.120001, "fiftyTwoWeekHighChangePercent": -0.59921163, "fiftyTwoWeekChangePercent": -57.282913, "dividendYield": 165.52, "trailingThreeMonthReturns": -13.87, "trailingThreeMonthNavReturns": -13.87, "netAssets": 226379696.0, "epsTrailingTwelveMonths": 0.228448, "marketState": "CLOSED", "regularMarketChangePercent": 0.0, "regularMarketPrice": 6.1, "corporateActions": [], "postMarketTime": 1748044768, "regularMarketTime": 1748030400, "exchange": "PCX", "messageBoardId": "finmb_1869805004", "exchangeTimezoneName": "America/New_York", "exchangeTimezoneShortName": "EDT", "gmtOffSetMilliseconds": -14400000, "market": "us_market", "esgPopulated": false, "fiftyDayAverageChange": 0.0461998, "fiftyDayAverageChangePercent": 0.007631537, "twoHundredDayAverageChange": -2.64755, "twoHundredDayAverageChangePercent": -0.3026619, "netExpenseRatio": 1.3, "sourceInterval": 15, "exchangeDataDelayedBy": 0, "cryptoTradeable": false, "hasPrePostMarketData": true, "firstTradeDateMilliseconds": 1709217000000, "postMarketChangePercent": 0.49180672, "postMarketPrice": 6.13, "postMarketChange": 0.03000021, "regularMarketChange": 0.0, "regularMarketDayRange": "6.02 - 6.13", "fullExchangeName": "NYSEArca", "averageDailyVolume3Month": 1994822, "trailingPegRatio": null}, "timestamp": "2025-05-24T13:52:58.466664"}
|
||||
1
cache/86bd26d004d7951035b2e678a3fa6ccf.json
vendored
Normal file
1
cache/86bd26d004d7951035b2e678a3fa6ccf.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"data": {"__pandas_series__": true, "index": [
|
||||
1
cache/86cc9fb558c9ab4da62c8a741e919889.json
vendored
Normal file
1
cache/86cc9fb558c9ab4da62c8a741e919889.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"data": {"__pandas_series__": true, "index": [
|
||||
1
cache/a4d5861d1e785dcd29a4155b49156709.json
vendored
Normal file
1
cache/a4d5861d1e785dcd29a4155b49156709.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"data": {"longBusinessSummary": "The fund is an actively managed exchange-traded fund (\u201cETF\u201d) that seeks current income while maintaining the opportunity for exposure to the share price (i.e., the price returns) of the securities of the companies comprising the Solactive FANG Innovation Index. The fund advisor seeks to employ the fund's investment strategy regardless of whether there are periods of adverse market, economic, or other conditions and will not seek to take temporary defensive positions during such periods. It is non-diversified.", "companyOfficers": [], "executiveTeam": [], "maxAge": 86400, "priceHint": 2, "previousClose": 43.66, "open": 43.12, "dayLow": 43.02, "dayHigh": 43.55, "regularMarketPreviousClose": 43.66, "regularMarketOpen": 43.12, "regularMarketDayLow": 43.02, "regularMarketDayHigh": 43.55, "trailingPE": 36.65259, "volume": 129662, "regularMarketVolume": 129662, "averageVolume": 147330, "averageVolume10days": 111310, "averageDailyVolume10Day": 111310, "bid": 43.11, "ask": 43.35, "bidSize": 2, "askSize": 2, "yield": 0.3039, "totalAssets": 426724160, "fiftyTwoWeekLow": 35.44, "fiftyTwoWeekHigh": 56.44, "fiftyDayAverage": 41.907, "twoHundredDayAverage": 48.12085, "navPrice": 43.62, "currency": "USD", "tradeable": false, "category": "Derivative Income", "ytdReturn": -8.89758, "beta3Year": 0.0, "fundFamily": "REX Advisers, LLC", "fundInceptionDate": 1696982400, "legalType": "Exchange Traded Fund", "quoteType": "ETF", "symbol": "FEPI", "language": "en-US", "region": "US", "typeDisp": "ETF", "quoteSourceName": "Nasdaq Real Time Price", "triggerable": true, "customPriceAlertConfidence": "HIGH", "corporateActions": [], "postMarketTime": 1748040939, "regularMarketTime": 1748030400, "hasPrePostMarketData": true, "firstTradeDateMilliseconds": 1697031000000, "postMarketChangePercent": 0.716766, "postMarketPrice": 43.56, "postMarketChange": 0.310001, "regularMarketChange": -0.40999985, "regularMarketDayRange": "43.02 - 43.55", "fullExchangeName": "NasdaqGM", "averageDailyVolume3Month": 147330, "fiftyTwoWeekLowChange": 7.8100014, "fiftyTwoWeekLowChangePercent": 0.22037251, "fiftyTwoWeekRange": "35.44 - 56.44", "fiftyTwoWeekHighChange": -13.189999, "fiftyTwoWeekHighChangePercent": -0.23369949, "fiftyTwoWeekChangePercent": -20.72947, "dividendYield": 30.39, "trailingThreeMonthReturns": -9.55848, "trailingThreeMonthNavReturns": -9.55848, "netAssets": 426724160.0, "epsTrailingTwelveMonths": 1.1799984, "fiftyDayAverageChange": 1.3429985, "fiftyDayAverageChangePercent": 0.032047115, "twoHundredDayAverageChange": -4.8708496, "twoHundredDayAverageChangePercent": -0.10122119, "netExpenseRatio": 0.65, "sourceInterval": 15, "exchangeDataDelayedBy": 0, "ipoExpectedDate": "2023-10-11", "cryptoTradeable": false, "marketState": "CLOSED", "shortName": "REX FANG & Innovation Equity Pr", "longName": "REX FANG & Innovation Equity Premium Income ETF", "exchange": "NGM", "messageBoardId": "finmb_1843173608", "exchangeTimezoneName": "America/New_York", "exchangeTimezoneShortName": "EDT", "gmtOffSetMilliseconds": -14400000, "market": "us_market", "esgPopulated": false, "regularMarketChangePercent": -0.93907434, "regularMarketPrice": 43.25, "trailingPegRatio": null}, "timestamp": "2025-05-24T13:52:58.472581"}
|
||||
1
cache/f039bfbd574b918f6ccf351f22a2c0c8.json
vendored
Normal file
1
cache/f039bfbd574b918f6ccf351f22a2c0c8.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"data": {"longBusinessSummary": "The fund is an actively managed ETF that seeks current income while maintaining the opportunity for exposure to the share price of the common stock of MicroStrategy Incorporated, subject to a limit on potential investment gains. It will seek to employ its investment strategy as it relates to MSTR regardless of whether there are periods of adverse market, economic, or other conditions and will not seek to take temporary defensive positions during such periods. The fund is non-diversified.", "companyOfficers": [], "executiveTeam": [], "maxAge": 86400, "priceHint": 2, "previousClose": 23.08, "open": 22.68, "dayLow": 21.2901, "dayHigh": 22.7, "regularMarketPreviousClose": 23.08, "regularMarketOpen": 22.68, "regularMarketDayLow": 21.2901, "regularMarketDayHigh": 22.7, "volume": 18359202, "regularMarketVolume": 18359202, "averageVolume": 7904033, "averageVolume10days": 11083420, "averageDailyVolume10Day": 11083420, "bid": 21.5, "ask": 21.63, "bidSize": 12, "askSize": 32, "yield": 1.2471, "totalAssets": 3270944256, "fiftyTwoWeekLow": 17.1, "fiftyTwoWeekHigh": 46.5, "fiftyDayAverage": 22.1824, "twoHundredDayAverage": 26.1411, "navPrice": 23.0514, "currency": "USD", "tradeable": false, "category": "Derivative Income", "ytdReturn": 24.89181, "beta3Year": 0.0, "fundFamily": "YieldMax ETFs", "fundInceptionDate": 1708473600, "legalType": "Exchange Traded Fund", "quoteType": "ETF", "symbol": "MSTY", "language": "en-US", "region": "US", "typeDisp": "ETF", "quoteSourceName": "Delayed Quote", "triggerable": true, "customPriceAlertConfidence": "HIGH", "marketState": "CLOSED", "shortName": "Tidal Trust II YieldMax MSTR Op", "regularMarketChangePercent": -6.7591, "regularMarketPrice": 21.52, "corporateActions": [], "longName": "Yieldmax MSTR Option Income Strategy ETF", "postMarketTime": 1748044797, "regularMarketTime": 1748030400, "regularMarketDayRange": "21.2901 - 22.7", "fullExchangeName": "NYSEArca", "averageDailyVolume3Month": 7904033, "fiftyTwoWeekLowChange": 4.42, "fiftyTwoWeekLowChangePercent": 0.25847954, "fiftyTwoWeekRange": "17.1 - 46.5", "fiftyTwoWeekHighChange": -24.98, "fiftyTwoWeekHighChangePercent": -0.53720427, "fiftyTwoWeekChangePercent": -38.654507, "dividendYield": 124.71, "trailingThreeMonthReturns": 13.21012, "trailingThreeMonthNavReturns": 13.21012, "netAssets": 3270944260.0, "fiftyDayAverageChange": -0.6623993, "fiftyDayAverageChangePercent": -0.02986148, "twoHundredDayAverageChange": -4.6210995, "twoHundredDayAverageChangePercent": -0.17677525, "netExpenseRatio": 0.99, "sourceInterval": 15, "exchangeDataDelayedBy": 0, "cryptoTradeable": false, "hasPrePostMarketData": true, "firstTradeDateMilliseconds": 1708612200000, "postMarketChangePercent": -0.18587786, "postMarketPrice": 21.48, "postMarketChange": -0.040000916, "regularMarketChange": -1.56, "exchange": "PCX", "messageBoardId": "finmb_1850981069", "exchangeTimezoneName": "America/New_York", "exchangeTimezoneShortName": "EDT", "gmtOffSetMilliseconds": -14400000, "market": "us_market", "esgPopulated": false, "trailingPegRatio": null}, "timestamp": "2025-05-24T13:52:58.456018"}
|
||||
82
docker-compose.yml
Normal file
82
docker-compose.yml
Normal file
@ -0,0 +1,82 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
etf-launcher:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
command: streamlit run ETF_Suite_Launcher.py --server.port=8500
|
||||
volumes:
|
||||
- .:/app
|
||||
networks:
|
||||
- etf_network
|
||||
environment:
|
||||
- STREAMLIT_SERVER_PORT=8500
|
||||
- STREAMLIT_SERVER_ADDRESS=0.0.0.0
|
||||
- STREAMLIT_SERVER_HEADLESS=true
|
||||
- STREAMLIT_BROWSER_GATHER_USAGE_STATS=false
|
||||
- STREAMLIT_SERVER_ENABLE_CORS=true
|
||||
- STREAMLIT_SERVER_ENABLE_XSRF_PROTECTION=false
|
||||
- STREAMLIT_SERVER_BASE_URL_PATH=
|
||||
restart: unless-stopped
|
||||
|
||||
# etf-portfolio:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile
|
||||
# command: streamlit run pages/ETF_Dividend_Portfolio_Builder_Wrapper.py --server.port=8501
|
||||
# volumes:
|
||||
# - .:/app
|
||||
# networks:
|
||||
# - etf_network
|
||||
# environment:
|
||||
# - STREAMLIT_SERVER_BASE_URL_PATH=/portfolio
|
||||
# - STREAMLIT_SERVER_HEADLESS=true
|
||||
# - STREAMLIT_BROWSER_GATHER_USAGE_STATS=false
|
||||
# - STREAMLIT_SERVER_ADDRESS=0.0.0.0
|
||||
# - FMP_API_KEY=${FMP_API_KEY}
|
||||
# - OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
# restart: unless-stopped
|
||||
|
||||
# etf-analyzer:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile
|
||||
# container_name: etf-analyzer
|
||||
# restart: unless-stopped
|
||||
# volumes:
|
||||
# - .:/app
|
||||
# environment:
|
||||
# - STREAMLIT_SERVER_HEADLESS=true
|
||||
# - STREAMLIT_SERVER_ENABLECORS=true
|
||||
# - STREAMLIT_SERVER_BASE_URL_PATH=/analyzer
|
||||
# - STREAMLIT_SERVER_ADDRESS=0.0.0.0
|
||||
# command: streamlit run pages/ETF_Analyzer_Wrapper.py --server.port=8502
|
||||
# networks:
|
||||
# - etf_network
|
||||
|
||||
caddy:
|
||||
image: caddy:2
|
||||
container_name: caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
networks:
|
||||
- etf_network
|
||||
depends_on:
|
||||
- etf-launcher
|
||||
# - etf-portfolio
|
||||
# - etf-analyzer
|
||||
|
||||
networks:
|
||||
etf_network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
374
etf_suite_cli.py
Normal file
374
etf_suite_cli.py
Normal file
@ -0,0 +1,374 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
ETF Suite CLI
|
||||
|
||||
A command-line interface for managing the ETF Suite application.
|
||||
"""
|
||||
|
||||
import click
|
||||
import subprocess
|
||||
import webbrowser
|
||||
import time
|
||||
import threading
|
||||
import os
|
||||
import sys
|
||||
import socket
|
||||
import signal
|
||||
import json
|
||||
import psutil
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
from datetime import datetime
|
||||
|
||||
# Path configurations
|
||||
WORKSPACE_PATH = Path(__file__).parent
|
||||
LAUNCHER_PATH = WORKSPACE_PATH / "ETF_Suite_Launcher.py"
|
||||
PORTFOLIO_BUILDER_PATH = WORKSPACE_PATH / "pages" / "ETF_Dividend_Portfolio_Builder.py"
|
||||
ANALYZER_PATH = WORKSPACE_PATH / "pages" / "ETF_Analyzer.py"
|
||||
CONFIG_DIR = WORKSPACE_PATH / "config"
|
||||
CONFIG_FILE = CONFIG_DIR / "etf_suite_config.json"
|
||||
LOGS_DIR = WORKSPACE_PATH / "logs"
|
||||
|
||||
# Default port settings
|
||||
DEFAULT_PORTS = {
|
||||
"launcher": 8500,
|
||||
"portfolio_builder": 8501,
|
||||
"analyzer": 8502
|
||||
}
|
||||
|
||||
# Full path to streamlit executable - may need to be adjusted
|
||||
STREAMLIT_PATH = "/home/pascal/.local/bin/streamlit"
|
||||
|
||||
# Process tracking
|
||||
active_processes = {}
|
||||
|
||||
# Setup logging
|
||||
def setup_logging():
|
||||
"""Set up logging configuration."""
|
||||
LOGS_DIR.mkdir(exist_ok=True)
|
||||
log_file = LOGS_DIR / f"etf_suite_{datetime.now().strftime('%Y%m%d')}.log"
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(log_file),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
return logging.getLogger("etf_suite")
|
||||
|
||||
logger = setup_logging()
|
||||
|
||||
def ensure_config_exists():
|
||||
"""Ensure config directory and file exist."""
|
||||
CONFIG_DIR.mkdir(exist_ok=True)
|
||||
|
||||
if not CONFIG_FILE.exists():
|
||||
with open(CONFIG_FILE, 'w') as f:
|
||||
json.dump({
|
||||
"ports": DEFAULT_PORTS,
|
||||
"streamlit_path": STREAMLIT_PATH
|
||||
}, f, indent=2)
|
||||
|
||||
def get_config():
|
||||
"""Get the configuration from the config file."""
|
||||
ensure_config_exists()
|
||||
with open(CONFIG_FILE, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
def update_config(key, value):
|
||||
"""Update a specific configuration value."""
|
||||
config = get_config()
|
||||
|
||||
# Handle nested configuration like ports.launcher
|
||||
if '.' in key:
|
||||
main_key, sub_key = key.split('.', 1)
|
||||
if main_key not in config:
|
||||
config[main_key] = {}
|
||||
config[main_key][sub_key] = value
|
||||
else:
|
||||
config[key] = value
|
||||
|
||||
with open(CONFIG_FILE, 'w') as f:
|
||||
json.dump(config, f, indent=2)
|
||||
|
||||
def cleanup_streamlit_processes():
|
||||
"""Kill any existing Streamlit processes to prevent conflicts."""
|
||||
click.echo("Cleaning up existing Streamlit processes...")
|
||||
logger.info("Cleaning up existing Streamlit processes")
|
||||
try:
|
||||
config = get_config()
|
||||
ports = config["ports"]
|
||||
|
||||
# Find processes using our target ports
|
||||
for port in ports.values():
|
||||
cmd = f"lsof -i :{port} | grep LISTEN | awk '{{print $2}}' | xargs kill -9 2>/dev/null || true"
|
||||
subprocess.run(cmd, shell=True)
|
||||
|
||||
# Find and kill any lingering Streamlit processes
|
||||
cmd = "pkill -f streamlit || true"
|
||||
subprocess.run(cmd, shell=True)
|
||||
|
||||
# Give processes time to shut down
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
logger.error(f"Error during cleanup: {e}")
|
||||
click.echo(f"Warning during cleanup: {e}")
|
||||
|
||||
def port_is_available(port):
|
||||
"""Check if a port is available."""
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind(("127.0.0.1", port))
|
||||
return True
|
||||
except socket.error:
|
||||
return False
|
||||
|
||||
def open_browser(url, delay=3):
|
||||
"""Open browser after a delay to ensure app is running."""
|
||||
time.sleep(delay)
|
||||
click.echo(f"Opening browser to {url}")
|
||||
webbrowser.open(url)
|
||||
|
||||
def start_component(component: str, open_browser_tab=True, background=False) -> Optional[subprocess.Popen]:
|
||||
"""Start a specific component of the ETF Suite."""
|
||||
config = get_config()
|
||||
streamlit_path = config.get("streamlit_path", STREAMLIT_PATH)
|
||||
ports = config["ports"]
|
||||
|
||||
# Check if streamlit exists at the specified path
|
||||
if not os.path.exists(streamlit_path):
|
||||
error_msg = f"ERROR: Streamlit not found at {streamlit_path}"
|
||||
logger.error(error_msg)
|
||||
click.echo(error_msg)
|
||||
click.echo("Please install streamlit or update the streamlit_path in config")
|
||||
return None
|
||||
|
||||
# Component-specific configurations
|
||||
component_configs = {
|
||||
"launcher": {
|
||||
"path": LAUNCHER_PATH,
|
||||
"port": ports["launcher"],
|
||||
},
|
||||
"portfolio_builder": {
|
||||
"path": PORTFOLIO_BUILDER_PATH,
|
||||
"port": ports["portfolio_builder"],
|
||||
},
|
||||
"analyzer": {
|
||||
"path": ANALYZER_PATH,
|
||||
"port": ports["analyzer"],
|
||||
}
|
||||
}
|
||||
|
||||
if component not in component_configs:
|
||||
error_msg = f"Unknown component: {component}"
|
||||
logger.error(error_msg)
|
||||
click.echo(error_msg)
|
||||
return None
|
||||
|
||||
component_config = component_configs[component]
|
||||
port = component_config["port"]
|
||||
|
||||
if not port_is_available(port):
|
||||
error_msg = f"ERROR: Port {port} is in use."
|
||||
logger.error(error_msg)
|
||||
click.echo(error_msg)
|
||||
click.echo(f"Please stop existing service on port {port} or configure a different port.")
|
||||
return None
|
||||
|
||||
log_file = None
|
||||
if background:
|
||||
# Create log file for background process
|
||||
LOGS_DIR.mkdir(exist_ok=True)
|
||||
log_file = LOGS_DIR / f"{component}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
|
||||
logger.info(f"Starting {component} in background mode. Logs will be written to {log_file}")
|
||||
|
||||
click.echo(f"Starting {component} on port {port}..." + (" (background mode)" if background else ""))
|
||||
|
||||
# Prepare command
|
||||
cmd = [
|
||||
streamlit_path, "run",
|
||||
str(component_config["path"]),
|
||||
"--server.port", str(port),
|
||||
"--server.address", "0.0.0.0",
|
||||
"--server.headless", "true",
|
||||
"--browser.gatherUsageStats", "false"
|
||||
]
|
||||
|
||||
# Add component-specific options
|
||||
if component == "portfolio_builder":
|
||||
cmd.extend(["--server.baseUrlPath", "/portfolio"])
|
||||
elif component == "analyzer":
|
||||
cmd.extend(["--server.baseUrlPath", "/analyzer"])
|
||||
|
||||
try:
|
||||
if background:
|
||||
with open(log_file, 'w') as f:
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=f,
|
||||
stderr=f,
|
||||
start_new_session=True
|
||||
)
|
||||
else:
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True
|
||||
)
|
||||
|
||||
active_processes[component] = process
|
||||
|
||||
if open_browser_tab and not background:
|
||||
url = f"http://localhost:{port}"
|
||||
threading.Thread(target=open_browser, args=(url,)).start()
|
||||
|
||||
return process
|
||||
except Exception as e:
|
||||
error_msg = f"Error starting {component}: {e}"
|
||||
logger.error(error_msg)
|
||||
click.echo(error_msg)
|
||||
return None
|
||||
|
||||
def get_streamlit_processes() -> List[Dict]:
|
||||
"""Get information about running Streamlit processes."""
|
||||
processes = []
|
||||
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
|
||||
try:
|
||||
if 'streamlit' in ' '.join(proc.info['cmdline'] or []):
|
||||
processes.append({
|
||||
'pid': proc.info['pid'],
|
||||
'name': proc.info['name'],
|
||||
'cmdline': ' '.join(proc.info['cmdline'] or [])
|
||||
})
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
pass
|
||||
return processes
|
||||
|
||||
def stop_component(component=None, pid=None):
|
||||
"""Stop a specific component or process."""
|
||||
if pid:
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
click.echo(f"Stopped process {pid}")
|
||||
return
|
||||
except ProcessLookupError:
|
||||
click.echo(f"Process {pid} not found")
|
||||
return
|
||||
|
||||
if component == "all":
|
||||
cleanup_streamlit_processes()
|
||||
return
|
||||
|
||||
if component in active_processes:
|
||||
process = active_processes[component]
|
||||
try:
|
||||
process.terminate()
|
||||
process.wait(timeout=5)
|
||||
click.echo(f"Stopped {component}")
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
click.echo(f"Force killed {component}")
|
||||
del active_processes[component]
|
||||
else:
|
||||
click.echo(f"No active process found for {component}")
|
||||
|
||||
@click.group()
|
||||
def cli():
|
||||
"""ETF Suite CLI - Manage your ETF Suite applications."""
|
||||
pass
|
||||
|
||||
@cli.command()
|
||||
@click.option('--component', type=click.Choice(['launcher', 'portfolio_builder', 'analyzer', 'all']),
|
||||
default='launcher', help='Component to start')
|
||||
@click.option('--no-browser', is_flag=True, help="Don't open browser automatically")
|
||||
@click.option('--background', is_flag=True, help="Run in background mode (daemon)")
|
||||
def start(component, no_browser, background):
|
||||
"""Start ETF Suite components."""
|
||||
if component == "all":
|
||||
for comp in ['launcher', 'portfolio_builder', 'analyzer']:
|
||||
start_component(comp, not no_browser, background)
|
||||
else:
|
||||
start_component(component, not no_browser, background)
|
||||
|
||||
@cli.command()
|
||||
@click.option('--component', type=click.Choice(['launcher', 'portfolio_builder', 'analyzer', 'all']),
|
||||
default='all', help='Component to stop')
|
||||
@click.option('--pid', type=int, help='Process ID to stop')
|
||||
def stop(component, pid):
|
||||
"""Stop ETF Suite components."""
|
||||
stop_component(component, pid)
|
||||
|
||||
@cli.command()
|
||||
@click.option('--component', type=click.Choice(['launcher', 'portfolio_builder', 'analyzer', 'all']),
|
||||
default='all', help='Component to restart')
|
||||
@click.option('--no-browser', is_flag=True, help="Don't open browser automatically")
|
||||
@click.option('--background', is_flag=True, help="Run in background mode (daemon)")
|
||||
def restart(component, no_browser, background):
|
||||
"""Restart ETF Suite components."""
|
||||
if component == "all":
|
||||
stop_component("all")
|
||||
time.sleep(2)
|
||||
for comp in ['launcher', 'portfolio_builder', 'analyzer']:
|
||||
start_component(comp, not no_browser, background)
|
||||
else:
|
||||
stop_component(component)
|
||||
time.sleep(2)
|
||||
start_component(component, not no_browser, background)
|
||||
|
||||
@cli.command()
|
||||
def status():
|
||||
"""Show status of ETF Suite components."""
|
||||
processes = get_streamlit_processes()
|
||||
if not processes:
|
||||
click.echo("No ETF Suite components are running")
|
||||
return
|
||||
|
||||
click.echo("Running components:")
|
||||
for proc in processes:
|
||||
click.echo(f"PID: {proc['pid']}")
|
||||
click.echo(f"Command: {proc['cmdline']}")
|
||||
click.echo("---")
|
||||
|
||||
@cli.command()
|
||||
@click.option('--key', required=True, help='Configuration key to update (e.g., ports.launcher)')
|
||||
@click.option('--value', required=True, help='New value')
|
||||
def config(key, value):
|
||||
"""Update ETF Suite configuration."""
|
||||
try:
|
||||
# Convert value to appropriate type
|
||||
if value.isdigit():
|
||||
value = int(value)
|
||||
elif value.lower() in ('true', 'false'):
|
||||
value = value.lower() == 'true'
|
||||
|
||||
update_config(key, value)
|
||||
click.echo(f"Updated {key} to {value}")
|
||||
except Exception as e:
|
||||
click.echo(f"Error updating config: {e}")
|
||||
|
||||
@cli.command()
|
||||
def logs():
|
||||
"""Show recent logs."""
|
||||
try:
|
||||
log_files = sorted(LOGS_DIR.glob("*.log"), key=lambda x: x.stat().st_mtime, reverse=True)
|
||||
if not log_files:
|
||||
click.echo("No log files found")
|
||||
return
|
||||
|
||||
latest_log = log_files[0]
|
||||
click.echo(f"Showing last 20 lines of {latest_log.name}:")
|
||||
click.echo("---")
|
||||
|
||||
with open(latest_log) as f:
|
||||
lines = f.readlines()
|
||||
for line in lines[-20:]:
|
||||
click.echo(line.strip())
|
||||
except Exception as e:
|
||||
click.echo(f"Error reading logs: {e}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli()
|
||||
36
pages/ETF_Analyzer.py
Normal file
36
pages/ETF_Analyzer.py
Normal file
@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
ETF Analyzer
|
||||
|
||||
This script provides ETF analysis functionality.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add the parent directory to the path for imports
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# Basic page config
|
||||
st.set_page_config(
|
||||
page_title="ETF Analyzer",
|
||||
page_icon="📈",
|
||||
layout="wide",
|
||||
menu_items={
|
||||
'Get Help': None,
|
||||
'Report a bug': None,
|
||||
'About': None
|
||||
}
|
||||
)
|
||||
|
||||
# Main content
|
||||
st.title("📈 ETF Analyzer")
|
||||
|
||||
# Placeholder for analyzer functionality
|
||||
st.markdown("""
|
||||
<div class='welcome-section'>
|
||||
<h2>ETF Analysis Tools</h2>
|
||||
<p style='font-size: 1.2rem;'>Coming soon...</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
3331
pages/ETF_Portfolio_Builder.py
Normal file
3331
pages/ETF_Portfolio_Builder.py
Normal file
File diff suppressed because it is too large
Load Diff
19
requirements.txt
Normal file
19
requirements.txt
Normal file
@ -0,0 +1,19 @@
|
||||
openai
|
||||
langchain
|
||||
langchain_openai
|
||||
chromadb
|
||||
docx2txt
|
||||
pypdf
|
||||
streamlit>=1.28.0
|
||||
tiktoken
|
||||
pdfkit
|
||||
pandas>=1.5.3
|
||||
numpy>=1.24.3
|
||||
matplotlib>=3.7.1
|
||||
seaborn>=0.12.2
|
||||
fmp-python>=0.1.5
|
||||
plotly>=5.14.1
|
||||
requests>=2.31.0
|
||||
reportlab>=3.6.13
|
||||
psutil>=5.9.0
|
||||
click>=8.1.0
|
||||
18
setup.py
Normal file
18
setup.py
Normal file
@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="etf-suite-cli",
|
||||
version="0.1.0",
|
||||
description="Command-line interface for ETF Suite",
|
||||
author="Pascal",
|
||||
py_modules=["etf_suite_cli"],
|
||||
install_requires=[
|
||||
"Click",
|
||||
"psutil",
|
||||
],
|
||||
entry_points="""
|
||||
[console_scripts]
|
||||
etf-suite=etf_suite_cli:cli
|
||||
""",
|
||||
)
|
||||
Loading…
Reference in New Issue
Block a user