mcli-framework 7.3.1__py3-none-any.whl → 7.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcli-framework might be problematic. Click here for more details.
- mcli/app/commands_cmd.py +741 -0
- mcli/lib/auth/aws_manager.py +9 -64
- mcli/lib/auth/azure_manager.py +9 -64
- mcli/lib/auth/credential_manager.py +70 -1
- mcli/lib/auth/gcp_manager.py +11 -64
- mcli/ml/dashboard/app.py +6 -39
- mcli/ml/dashboard/app_integrated.py +288 -117
- mcli/ml/dashboard/app_supabase.py +8 -57
- mcli/ml/dashboard/app_training.py +10 -12
- mcli/ml/dashboard/common.py +167 -0
- mcli/ml/dashboard/overview.py +378 -0
- mcli/ml/dashboard/pages/cicd.py +4 -4
- mcli/ml/dashboard/pages/debug_dependencies.py +406 -0
- mcli/ml/dashboard/pages/gravity_viz.py +783 -0
- mcli/ml/dashboard/pages/monte_carlo_predictions.py +555 -0
- mcli/ml/dashboard/pages/predictions_enhanced.py +4 -2
- mcli/ml/dashboard/pages/scrapers_and_logs.py +25 -9
- mcli/ml/dashboard/pages/test_portfolio.py +54 -4
- mcli/ml/dashboard/pages/trading.py +80 -26
- mcli/ml/dashboard/streamlit_extras_utils.py +297 -0
- mcli/ml/dashboard/styles.py +55 -0
- mcli/ml/dashboard/utils.py +7 -0
- mcli/ml/dashboard/warning_suppression.py +34 -0
- mcli/ml/database/session.py +169 -16
- mcli/ml/predictions/monte_carlo.py +428 -0
- mcli/ml/trading/alpaca_client.py +82 -18
- mcli/self/self_cmd.py +182 -737
- {mcli_framework-7.3.1.dist-info → mcli_framework-7.5.0.dist-info}/METADATA +2 -3
- {mcli_framework-7.3.1.dist-info → mcli_framework-7.5.0.dist-info}/RECORD +33 -87
- mcli/__init__.py +0 -160
- mcli/__main__.py +0 -14
- mcli/app/__init__.py +0 -23
- mcli/app/model/__init__.py +0 -0
- mcli/app/video/__init__.py +0 -5
- mcli/chat/__init__.py +0 -34
- mcli/lib/__init__.py +0 -0
- mcli/lib/api/__init__.py +0 -0
- mcli/lib/auth/__init__.py +0 -1
- mcli/lib/config/__init__.py +0 -1
- mcli/lib/erd/__init__.py +0 -25
- mcli/lib/files/__init__.py +0 -0
- mcli/lib/fs/__init__.py +0 -1
- mcli/lib/logger/__init__.py +0 -3
- mcli/lib/performance/__init__.py +0 -17
- mcli/lib/pickles/__init__.py +0 -1
- mcli/lib/shell/__init__.py +0 -0
- mcli/lib/toml/__init__.py +0 -1
- mcli/lib/watcher/__init__.py +0 -0
- mcli/ml/__init__.py +0 -16
- mcli/ml/api/__init__.py +0 -30
- mcli/ml/api/routers/__init__.py +0 -27
- mcli/ml/auth/__init__.py +0 -45
- mcli/ml/backtesting/__init__.py +0 -39
- mcli/ml/cli/__init__.py +0 -5
- mcli/ml/config/__init__.py +0 -33
- mcli/ml/configs/__init__.py +0 -16
- mcli/ml/dashboard/__init__.py +0 -12
- mcli/ml/dashboard/components/__init__.py +0 -7
- mcli/ml/dashboard/pages/__init__.py +0 -6
- mcli/ml/data_ingestion/__init__.py +0 -39
- mcli/ml/database/__init__.py +0 -47
- mcli/ml/experimentation/__init__.py +0 -29
- mcli/ml/features/__init__.py +0 -39
- mcli/ml/mlops/__init__.py +0 -33
- mcli/ml/models/__init__.py +0 -94
- mcli/ml/monitoring/__init__.py +0 -25
- mcli/ml/optimization/__init__.py +0 -27
- mcli/ml/predictions/__init__.py +0 -5
- mcli/ml/preprocessing/__init__.py +0 -28
- mcli/ml/scripts/__init__.py +0 -1
- mcli/ml/trading/__init__.py +0 -60
- mcli/ml/training/__init__.py +0 -10
- mcli/mygroup/__init__.py +0 -3
- mcli/public/__init__.py +0 -1
- mcli/public/commands/__init__.py +0 -2
- mcli/self/__init__.py +0 -3
- mcli/workflow/__init__.py +0 -0
- mcli/workflow/daemon/__init__.py +0 -15
- mcli/workflow/dashboard/__init__.py +0 -5
- mcli/workflow/docker/__init__.py +0 -0
- mcli/workflow/file/__init__.py +0 -0
- mcli/workflow/gcloud/__init__.py +0 -1
- mcli/workflow/git_commit/__init__.py +0 -0
- mcli/workflow/interview/__init__.py +0 -0
- mcli/workflow/politician_trading/__init__.py +0 -4
- mcli/workflow/registry/__init__.py +0 -0
- mcli/workflow/repo/__init__.py +0 -0
- mcli/workflow/scheduler/__init__.py +0 -25
- mcli/workflow/search/__init__.py +0 -0
- mcli/workflow/sync/__init__.py +0 -5
- mcli/workflow/videos/__init__.py +0 -1
- mcli/workflow/wakatime/__init__.py +0 -80
- {mcli_framework-7.3.1.dist-info → mcli_framework-7.5.0.dist-info}/WHEEL +0 -0
- {mcli_framework-7.3.1.dist-info → mcli_framework-7.5.0.dist-info}/entry_points.txt +0 -0
- {mcli_framework-7.3.1.dist-info → mcli_framework-7.5.0.dist-info}/licenses/LICENSE +0 -0
- {mcli_framework-7.3.1.dist-info → mcli_framework-7.5.0.dist-info}/top_level.txt +0 -0
mcli/ml/database/session.py
CHANGED
|
@@ -13,9 +13,121 @@ from mcli.ml.config import settings
|
|
|
13
13
|
from .models import Base
|
|
14
14
|
|
|
15
15
|
# Synchronous database setup
|
|
16
|
+
# Prioritize DATABASE_URL environment variable over settings
|
|
17
|
+
import os
|
|
18
|
+
database_url = os.getenv("DATABASE_URL")
|
|
19
|
+
|
|
20
|
+
# Check if DATABASE_URL has placeholder password
|
|
21
|
+
if database_url and "your_password" in database_url:
|
|
22
|
+
database_url = None # Treat placeholder as not set
|
|
23
|
+
|
|
24
|
+
# If no DATABASE_URL or it's SQLite from settings, try explicit configuration
|
|
25
|
+
if not database_url:
|
|
26
|
+
try:
|
|
27
|
+
# Check if settings has a non-SQLite configuration
|
|
28
|
+
settings_url = settings.database.url
|
|
29
|
+
if settings_url and "sqlite" not in settings_url:
|
|
30
|
+
database_url = settings_url
|
|
31
|
+
except (AttributeError, Exception):
|
|
32
|
+
pass # Continue with database_url=None
|
|
33
|
+
|
|
34
|
+
# If still no valid DATABASE_URL, try to use Supabase REST API via connection pooler
|
|
35
|
+
if not database_url:
|
|
36
|
+
supabase_url = os.getenv("SUPABASE_URL", "")
|
|
37
|
+
supabase_service_key = os.getenv("SUPABASE_SERVICE_ROLE_KEY")
|
|
38
|
+
|
|
39
|
+
if supabase_url and supabase_service_key and "supabase.co" in supabase_url:
|
|
40
|
+
# Extract project reference from Supabase URL
|
|
41
|
+
# Format: https://PROJECT_REF.supabase.co
|
|
42
|
+
project_ref = supabase_url.replace("https://", "").replace("http://", "").split(".")[0]
|
|
43
|
+
|
|
44
|
+
# Use Supabase IPv4-only connection pooler
|
|
45
|
+
# This avoids IPv6 connectivity issues on Streamlit Cloud
|
|
46
|
+
# Try EU region poolers (which are verified to work for this project)
|
|
47
|
+
# Session mode (port 5432) for persistent connections
|
|
48
|
+
# Transaction mode (port 6543) for serverless/short-lived connections
|
|
49
|
+
pooler_urls = [
|
|
50
|
+
f"postgresql://postgres.{project_ref}:{supabase_service_key}@aws-1-eu-north-1.pooler.supabase.com:5432/postgres",
|
|
51
|
+
f"postgresql://postgres.{project_ref}:{supabase_service_key}@aws-1-eu-north-1.pooler.supabase.com:6543/postgres",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
# Try to connect to poolers
|
|
55
|
+
import logging
|
|
56
|
+
logger = logging.getLogger(__name__)
|
|
57
|
+
|
|
58
|
+
for pooler_url in pooler_urls:
|
|
59
|
+
try:
|
|
60
|
+
# Test connection
|
|
61
|
+
test_engine = create_engine(pooler_url, pool_pre_ping=True)
|
|
62
|
+
with test_engine.connect() as conn:
|
|
63
|
+
from sqlalchemy import text
|
|
64
|
+
conn.execute(text("SELECT 1"))
|
|
65
|
+
database_url = pooler_url
|
|
66
|
+
logger.info(f"Successfully connected via pooler: {pooler_url.split('@')[1].split(':')[0]}")
|
|
67
|
+
test_engine.dispose()
|
|
68
|
+
break
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.warning(f"Failed to connect via {pooler_url.split('@')[1].split(':')[0]}: {e}")
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
if not database_url:
|
|
74
|
+
# Fallback to first pooler URL if all fail (will be handled by pool_pre_ping later)
|
|
75
|
+
database_url = pooler_urls[0]
|
|
76
|
+
|
|
77
|
+
import warnings
|
|
78
|
+
warnings.warn(
|
|
79
|
+
"Using Supabase connection pooler with service role key. "
|
|
80
|
+
"For better performance, set DATABASE_URL with your actual database password. "
|
|
81
|
+
"Find it in Supabase Dashboard → Settings → Database → Connection String"
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
# Default to SQLite for development/testing
|
|
85
|
+
database_url = "sqlite:///./ml_system.db"
|
|
86
|
+
import warnings
|
|
87
|
+
warnings.warn(
|
|
88
|
+
"No database credentials found. Using SQLite fallback. "
|
|
89
|
+
"Set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY or DATABASE_URL in environment."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Debug: Log which database URL is being used
|
|
93
|
+
import logging
|
|
94
|
+
logger = logging.getLogger(__name__)
|
|
95
|
+
|
|
96
|
+
if "pooler.supabase.com" in database_url:
|
|
97
|
+
logger.info(f"🔗 Using Supabase connection pooler")
|
|
98
|
+
elif "sqlite" in database_url:
|
|
99
|
+
logger.warning("📁 Using SQLite fallback (database features limited)")
|
|
100
|
+
else:
|
|
101
|
+
# Mask password in display
|
|
102
|
+
display_url = database_url
|
|
103
|
+
if "@" in display_url and ":" in display_url:
|
|
104
|
+
parts = display_url.split("@")
|
|
105
|
+
before_at = parts[0].split(":")
|
|
106
|
+
if len(before_at) >= 3:
|
|
107
|
+
before_at[2] = "***"
|
|
108
|
+
display_url = ":".join(before_at) + "@" + parts[1]
|
|
109
|
+
logger.info(f"🔗 Database URL: {display_url}")
|
|
110
|
+
|
|
111
|
+
# Configure connection arguments based on database type
|
|
112
|
+
if "sqlite" in database_url:
|
|
113
|
+
connect_args = {"check_same_thread": False}
|
|
114
|
+
elif "postgresql" in database_url:
|
|
115
|
+
# Force IPv4 for PostgreSQL to avoid IPv6 connection issues
|
|
116
|
+
connect_args = {
|
|
117
|
+
"connect_timeout": 10,
|
|
118
|
+
"options": "-c statement_timeout=30000", # 30 second query timeout
|
|
119
|
+
}
|
|
120
|
+
else:
|
|
121
|
+
connect_args = {}
|
|
122
|
+
|
|
16
123
|
engine = create_engine(
|
|
17
|
-
|
|
18
|
-
|
|
124
|
+
database_url,
|
|
125
|
+
connect_args=connect_args,
|
|
126
|
+
pool_pre_ping=True, # Verify connections before using them
|
|
127
|
+
pool_recycle=3600, # Recycle connections after 1 hour
|
|
128
|
+
pool_size=5, # Smaller pool for Streamlit Cloud
|
|
129
|
+
max_overflow=10,
|
|
130
|
+
pool_timeout=30,
|
|
19
131
|
)
|
|
20
132
|
|
|
21
133
|
SessionLocal = sessionmaker(
|
|
@@ -27,14 +139,30 @@ SessionLocal = sessionmaker(
|
|
|
27
139
|
|
|
28
140
|
|
|
29
141
|
# Asynchronous database setup
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
142
|
+
try:
|
|
143
|
+
async_engine = create_async_engine(
|
|
144
|
+
settings.database.async_url,
|
|
145
|
+
pool_size=settings.database.pool_size,
|
|
146
|
+
max_overflow=settings.database.max_overflow,
|
|
147
|
+
pool_timeout=settings.database.pool_timeout,
|
|
148
|
+
pool_pre_ping=True,
|
|
149
|
+
echo=settings.debug,
|
|
150
|
+
)
|
|
151
|
+
except (AttributeError, Exception):
|
|
152
|
+
# Fallback for async engine
|
|
153
|
+
import os
|
|
154
|
+
async_database_url = os.getenv("ASYNC_DATABASE_URL")
|
|
155
|
+
if not async_database_url:
|
|
156
|
+
# Convert sync URL to async if possible
|
|
157
|
+
if "sqlite" in database_url:
|
|
158
|
+
async_database_url = database_url.replace("sqlite:///", "sqlite+aiosqlite:///")
|
|
159
|
+
else:
|
|
160
|
+
async_database_url = database_url.replace("postgresql://", "postgresql+asyncpg://")
|
|
161
|
+
|
|
162
|
+
async_engine = create_async_engine(
|
|
163
|
+
async_database_url,
|
|
164
|
+
pool_pre_ping=True,
|
|
165
|
+
)
|
|
38
166
|
|
|
39
167
|
AsyncSessionLocal = async_sessionmaker(
|
|
40
168
|
async_engine,
|
|
@@ -81,21 +209,46 @@ async def get_async_db() -> AsyncGenerator[AsyncSession, None]:
|
|
|
81
209
|
@contextmanager
|
|
82
210
|
def get_session() -> Generator[Session, None, None]:
|
|
83
211
|
"""
|
|
84
|
-
Context manager for database session.
|
|
212
|
+
Context manager for database session with improved error handling.
|
|
85
213
|
|
|
86
214
|
Usage:
|
|
87
215
|
with get_session() as session:
|
|
88
216
|
user = session.query(User).first()
|
|
217
|
+
|
|
218
|
+
Raises:
|
|
219
|
+
ConnectionError: If database connection cannot be established
|
|
220
|
+
Exception: For other database errors
|
|
89
221
|
"""
|
|
90
|
-
session =
|
|
222
|
+
session = None
|
|
91
223
|
try:
|
|
224
|
+
session = SessionLocal()
|
|
225
|
+
# Test the connection
|
|
226
|
+
from sqlalchemy import text
|
|
227
|
+
session.execute(text("SELECT 1"))
|
|
92
228
|
yield session
|
|
93
229
|
session.commit()
|
|
94
|
-
except Exception:
|
|
95
|
-
session
|
|
96
|
-
|
|
230
|
+
except Exception as e:
|
|
231
|
+
if session:
|
|
232
|
+
session.rollback()
|
|
233
|
+
# Provide more helpful error messages
|
|
234
|
+
error_msg = str(e).lower()
|
|
235
|
+
if "cannot assign requested address" in error_msg or "ipv6" in error_msg:
|
|
236
|
+
raise ConnectionError(
|
|
237
|
+
"Database connection failed due to network issues. "
|
|
238
|
+
"This may be an IPv6 connectivity problem. "
|
|
239
|
+
"Please ensure DATABASE_URL uses connection pooler (pooler.supabase.com) instead of direct connection (db.supabase.co). "
|
|
240
|
+
f"Original error: {e}"
|
|
241
|
+
)
|
|
242
|
+
elif "authentication failed" in error_msg or "password" in error_msg:
|
|
243
|
+
raise ConnectionError(
|
|
244
|
+
"Database authentication failed. Please check your database credentials. "
|
|
245
|
+
f"Original error: {e}"
|
|
246
|
+
)
|
|
247
|
+
else:
|
|
248
|
+
raise
|
|
97
249
|
finally:
|
|
98
|
-
session
|
|
250
|
+
if session:
|
|
251
|
+
session.close()
|
|
99
252
|
|
|
100
253
|
|
|
101
254
|
@asynccontextmanager
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""Monte Carlo simulation for politician trading predictions
|
|
2
|
+
|
|
3
|
+
Uses Monte Carlo methods to simulate possible price paths and estimate
|
|
4
|
+
expected returns based on politician trading patterns.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from typing import Dict, List, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pandas as pd
|
|
13
|
+
import plotly.express as px
|
|
14
|
+
import plotly.graph_objects as go
|
|
15
|
+
from plotly.subplots import make_subplots
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MonteCarloTradingSimulator:
|
|
21
|
+
"""Monte Carlo simulator for politician trading predictions"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
initial_price: float,
|
|
26
|
+
days_to_simulate: int = 90,
|
|
27
|
+
num_simulations: int = 1000,
|
|
28
|
+
random_seed: Optional[int] = None,
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Initialize Monte Carlo simulator
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
initial_price: Starting stock price
|
|
35
|
+
days_to_simulate: Number of days to project forward
|
|
36
|
+
num_simulations: Number of Monte Carlo paths to generate
|
|
37
|
+
random_seed: Random seed for reproducibility
|
|
38
|
+
"""
|
|
39
|
+
self.initial_price = initial_price
|
|
40
|
+
self.days_to_simulate = days_to_simulate
|
|
41
|
+
self.num_simulations = num_simulations
|
|
42
|
+
self.random_seed = random_seed
|
|
43
|
+
|
|
44
|
+
if random_seed is not None:
|
|
45
|
+
np.random.seed(random_seed)
|
|
46
|
+
|
|
47
|
+
# Store simulation results
|
|
48
|
+
self.simulated_paths: Optional[np.ndarray] = None
|
|
49
|
+
self.final_prices: Optional[np.ndarray] = None
|
|
50
|
+
self.returns: Optional[np.ndarray] = None
|
|
51
|
+
|
|
52
|
+
def estimate_parameters(
|
|
53
|
+
self, historical_prices: pd.Series
|
|
54
|
+
) -> Tuple[float, float]:
|
|
55
|
+
"""
|
|
56
|
+
Estimate drift (μ) and volatility (σ) from historical data
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
historical_prices: Series of historical prices
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Tuple of (drift, volatility)
|
|
63
|
+
"""
|
|
64
|
+
# Calculate daily returns
|
|
65
|
+
returns = historical_prices.pct_change().dropna()
|
|
66
|
+
|
|
67
|
+
# Estimate parameters
|
|
68
|
+
daily_return = returns.mean()
|
|
69
|
+
daily_volatility = returns.std()
|
|
70
|
+
|
|
71
|
+
# Annualize (assuming 252 trading days)
|
|
72
|
+
annual_return = daily_return * 252
|
|
73
|
+
annual_volatility = daily_volatility * np.sqrt(252)
|
|
74
|
+
|
|
75
|
+
logger.info(
|
|
76
|
+
f"Estimated parameters: drift={annual_return:.4f}, "
|
|
77
|
+
f"volatility={annual_volatility:.4f}"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return annual_return, annual_volatility
|
|
81
|
+
|
|
82
|
+
def simulate_price_paths(
|
|
83
|
+
self, drift: float, volatility: float
|
|
84
|
+
) -> np.ndarray:
|
|
85
|
+
"""
|
|
86
|
+
Generate Monte Carlo price paths using Geometric Brownian Motion
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
drift: Expected return (μ)
|
|
90
|
+
volatility: Standard deviation (σ)
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Array of shape (num_simulations, days_to_simulate + 1) with price paths
|
|
94
|
+
"""
|
|
95
|
+
# Time step (assuming daily)
|
|
96
|
+
dt = 1 / 252 # Daily time step in years
|
|
97
|
+
|
|
98
|
+
# Initialize price paths matrix
|
|
99
|
+
paths = np.zeros((self.num_simulations, self.days_to_simulate + 1))
|
|
100
|
+
paths[:, 0] = self.initial_price
|
|
101
|
+
|
|
102
|
+
# Generate random shocks
|
|
103
|
+
random_shocks = np.random.normal(
|
|
104
|
+
0, 1, size=(self.num_simulations, self.days_to_simulate)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Simulate paths using Geometric Brownian Motion
|
|
108
|
+
# S(t+dt) = S(t) * exp((μ - σ²/2)dt + σ√dt * Z)
|
|
109
|
+
for t in range(1, self.days_to_simulate + 1):
|
|
110
|
+
drift_component = (drift - 0.5 * volatility**2) * dt
|
|
111
|
+
shock_component = volatility * np.sqrt(dt) * random_shocks[:, t - 1]
|
|
112
|
+
|
|
113
|
+
paths[:, t] = paths[:, t - 1] * np.exp(
|
|
114
|
+
drift_component + shock_component
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
self.simulated_paths = paths
|
|
118
|
+
self.final_prices = paths[:, -1]
|
|
119
|
+
self.returns = (self.final_prices - self.initial_price) / self.initial_price
|
|
120
|
+
|
|
121
|
+
logger.info(
|
|
122
|
+
f"Simulated {self.num_simulations} price paths over {self.days_to_simulate} days"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return paths
|
|
126
|
+
|
|
127
|
+
def calculate_statistics(self) -> Dict[str, float]:
|
|
128
|
+
"""
|
|
129
|
+
Calculate statistics from simulation results
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Dictionary with statistical measures
|
|
133
|
+
"""
|
|
134
|
+
if self.final_prices is None:
|
|
135
|
+
raise ValueError("Must run simulation first")
|
|
136
|
+
|
|
137
|
+
stats = {
|
|
138
|
+
"expected_final_price": np.mean(self.final_prices),
|
|
139
|
+
"median_final_price": np.median(self.final_prices),
|
|
140
|
+
"std_final_price": np.std(self.final_prices),
|
|
141
|
+
"min_final_price": np.min(self.final_prices),
|
|
142
|
+
"max_final_price": np.max(self.final_prices),
|
|
143
|
+
"expected_return": np.mean(self.returns) * 100,
|
|
144
|
+
"median_return": np.median(self.returns) * 100,
|
|
145
|
+
"std_return": np.std(self.returns) * 100,
|
|
146
|
+
"probability_profit": np.sum(self.returns > 0) / len(self.returns) * 100,
|
|
147
|
+
"value_at_risk_95": np.percentile(self.returns, 5) * 100, # 95% VaR
|
|
148
|
+
"percentile_5": np.percentile(self.final_prices, 5),
|
|
149
|
+
"percentile_25": np.percentile(self.final_prices, 25),
|
|
150
|
+
"percentile_75": np.percentile(self.final_prices, 75),
|
|
151
|
+
"percentile_95": np.percentile(self.final_prices, 95),
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return stats
|
|
155
|
+
|
|
156
|
+
def create_path_visualization(
|
|
157
|
+
self, num_paths_to_plot: int = 100, show_percentiles: bool = True
|
|
158
|
+
) -> go.Figure:
|
|
159
|
+
"""
|
|
160
|
+
Create visualization of simulated price paths
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
num_paths_to_plot: Number of individual paths to display
|
|
164
|
+
show_percentiles: Whether to show percentile bands
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Plotly figure object
|
|
168
|
+
"""
|
|
169
|
+
if self.simulated_paths is None:
|
|
170
|
+
raise ValueError("Must run simulation first")
|
|
171
|
+
|
|
172
|
+
fig = go.Figure()
|
|
173
|
+
|
|
174
|
+
# Plot sample paths
|
|
175
|
+
num_to_plot = min(num_paths_to_plot, self.num_simulations)
|
|
176
|
+
sample_indices = np.random.choice(
|
|
177
|
+
self.num_simulations, num_to_plot, replace=False
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
for idx in sample_indices:
|
|
181
|
+
fig.add_trace(
|
|
182
|
+
go.Scatter(
|
|
183
|
+
x=list(range(self.days_to_simulate + 1)),
|
|
184
|
+
y=self.simulated_paths[idx],
|
|
185
|
+
mode="lines",
|
|
186
|
+
line=dict(color="lightblue", width=0.5),
|
|
187
|
+
opacity=0.3,
|
|
188
|
+
showlegend=False,
|
|
189
|
+
hoverinfo="skip",
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Add mean path
|
|
194
|
+
mean_path = np.mean(self.simulated_paths, axis=0)
|
|
195
|
+
fig.add_trace(
|
|
196
|
+
go.Scatter(
|
|
197
|
+
x=list(range(self.days_to_simulate + 1)),
|
|
198
|
+
y=mean_path,
|
|
199
|
+
mode="lines",
|
|
200
|
+
line=dict(color="blue", width=3),
|
|
201
|
+
name="Expected Path",
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if show_percentiles:
|
|
206
|
+
# Add percentile bands
|
|
207
|
+
percentile_5 = np.percentile(self.simulated_paths, 5, axis=0)
|
|
208
|
+
percentile_25 = np.percentile(self.simulated_paths, 25, axis=0)
|
|
209
|
+
percentile_75 = np.percentile(self.simulated_paths, 75, axis=0)
|
|
210
|
+
percentile_95 = np.percentile(self.simulated_paths, 95, axis=0)
|
|
211
|
+
|
|
212
|
+
x_vals = list(range(self.days_to_simulate + 1))
|
|
213
|
+
|
|
214
|
+
# 90% confidence band (5th to 95th percentile)
|
|
215
|
+
fig.add_trace(
|
|
216
|
+
go.Scatter(
|
|
217
|
+
x=x_vals + x_vals[::-1],
|
|
218
|
+
y=list(percentile_95) + list(percentile_5[::-1]),
|
|
219
|
+
fill="toself",
|
|
220
|
+
fillcolor="rgba(255, 0, 0, 0.1)",
|
|
221
|
+
line=dict(color="rgba(255, 0, 0, 0)"),
|
|
222
|
+
name="90% Confidence",
|
|
223
|
+
hoverinfo="skip",
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# 50% confidence band (25th to 75th percentile)
|
|
228
|
+
fig.add_trace(
|
|
229
|
+
go.Scatter(
|
|
230
|
+
x=x_vals + x_vals[::-1],
|
|
231
|
+
y=list(percentile_75) + list(percentile_25[::-1]),
|
|
232
|
+
fill="toself",
|
|
233
|
+
fillcolor="rgba(0, 255, 0, 0.2)",
|
|
234
|
+
line=dict(color="rgba(0, 255, 0, 0)"),
|
|
235
|
+
name="50% Confidence",
|
|
236
|
+
hoverinfo="skip",
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
fig.update_layout(
|
|
241
|
+
title=f"Monte Carlo Simulation: {self.num_simulations:,} Price Paths",
|
|
242
|
+
xaxis_title="Days",
|
|
243
|
+
yaxis_title="Price ($)",
|
|
244
|
+
hovermode="x unified",
|
|
245
|
+
template="plotly_white",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return fig
|
|
249
|
+
|
|
250
|
+
def create_distribution_visualization(self) -> go.Figure:
|
|
251
|
+
"""
|
|
252
|
+
Create histogram of final price distribution
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Plotly figure object
|
|
256
|
+
"""
|
|
257
|
+
if self.final_prices is None:
|
|
258
|
+
raise ValueError("Must run simulation first")
|
|
259
|
+
|
|
260
|
+
stats = self.calculate_statistics()
|
|
261
|
+
|
|
262
|
+
fig = make_subplots(
|
|
263
|
+
rows=1,
|
|
264
|
+
cols=2,
|
|
265
|
+
subplot_titles=("Final Price Distribution", "Return Distribution"),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Price distribution
|
|
269
|
+
fig.add_trace(
|
|
270
|
+
go.Histogram(
|
|
271
|
+
x=self.final_prices,
|
|
272
|
+
nbinsx=50,
|
|
273
|
+
name="Final Price",
|
|
274
|
+
marker_color="lightblue",
|
|
275
|
+
showlegend=False,
|
|
276
|
+
),
|
|
277
|
+
row=1,
|
|
278
|
+
col=1,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Add vertical lines for statistics
|
|
282
|
+
fig.add_vline(
|
|
283
|
+
x=stats["expected_final_price"],
|
|
284
|
+
line_dash="dash",
|
|
285
|
+
line_color="blue",
|
|
286
|
+
annotation_text="Mean",
|
|
287
|
+
row=1,
|
|
288
|
+
col=1,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
fig.add_vline(
|
|
292
|
+
x=self.initial_price,
|
|
293
|
+
line_dash="solid",
|
|
294
|
+
line_color="red",
|
|
295
|
+
annotation_text="Current",
|
|
296
|
+
row=1,
|
|
297
|
+
col=1,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Return distribution
|
|
301
|
+
fig.add_trace(
|
|
302
|
+
go.Histogram(
|
|
303
|
+
x=self.returns * 100,
|
|
304
|
+
nbinsx=50,
|
|
305
|
+
name="Returns",
|
|
306
|
+
marker_color="lightgreen",
|
|
307
|
+
showlegend=False,
|
|
308
|
+
),
|
|
309
|
+
row=1,
|
|
310
|
+
col=2,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Add vertical line at 0% return
|
|
314
|
+
fig.add_vline(
|
|
315
|
+
x=0, line_dash="dash", line_color="red", annotation_text="0%", row=1, col=2
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
fig.update_xaxes(title_text="Price ($)", row=1, col=1)
|
|
319
|
+
fig.update_xaxes(title_text="Return (%)", row=1, col=2)
|
|
320
|
+
fig.update_yaxes(title_text="Frequency", row=1, col=1)
|
|
321
|
+
fig.update_yaxes(title_text="Frequency", row=1, col=2)
|
|
322
|
+
|
|
323
|
+
fig.update_layout(
|
|
324
|
+
title="Monte Carlo Simulation Results",
|
|
325
|
+
template="plotly_white",
|
|
326
|
+
height=400,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
return fig
|
|
330
|
+
|
|
331
|
+
def calculate_confidence_intervals(
|
|
332
|
+
self, confidence_levels: List[float] = [0.90, 0.95, 0.99]
|
|
333
|
+
) -> Dict[float, Tuple[float, float]]:
|
|
334
|
+
"""
|
|
335
|
+
Calculate confidence intervals for final price
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
confidence_levels: List of confidence levels (e.g., [0.90, 0.95])
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
Dictionary mapping confidence level to (lower, upper) bounds
|
|
342
|
+
"""
|
|
343
|
+
if self.final_prices is None:
|
|
344
|
+
raise ValueError("Must run simulation first")
|
|
345
|
+
|
|
346
|
+
intervals = {}
|
|
347
|
+
for level in confidence_levels:
|
|
348
|
+
alpha = 1 - level
|
|
349
|
+
lower = np.percentile(self.final_prices, alpha / 2 * 100)
|
|
350
|
+
upper = np.percentile(self.final_prices, (1 - alpha / 2) * 100)
|
|
351
|
+
intervals[level] = (lower, upper)
|
|
352
|
+
|
|
353
|
+
return intervals
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def simulate_politician_trade_impact(
|
|
357
|
+
stock_symbol: str,
|
|
358
|
+
politician_name: str,
|
|
359
|
+
transaction_amount: float,
|
|
360
|
+
historical_prices: pd.Series,
|
|
361
|
+
days_forward: int = 90,
|
|
362
|
+
num_simulations: int = 1000,
|
|
363
|
+
) -> Dict:
|
|
364
|
+
"""
|
|
365
|
+
Simulate potential outcomes of following a politician's trade
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
stock_symbol: Stock ticker symbol
|
|
369
|
+
politician_name: Name of politician
|
|
370
|
+
transaction_amount: Dollar amount of politician's trade
|
|
371
|
+
historical_prices: Historical price data
|
|
372
|
+
days_forward: Days to simulate forward
|
|
373
|
+
num_simulations: Number of Monte Carlo simulations
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
Dictionary with simulation results and statistics
|
|
377
|
+
"""
|
|
378
|
+
if len(historical_prices) < 30:
|
|
379
|
+
logger.warning(f"Insufficient historical data for {stock_symbol}")
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
current_price = historical_prices.iloc[-1]
|
|
383
|
+
|
|
384
|
+
# Initialize simulator
|
|
385
|
+
simulator = MonteCarloTradingSimulator(
|
|
386
|
+
initial_price=current_price,
|
|
387
|
+
days_to_simulate=days_forward,
|
|
388
|
+
num_simulations=num_simulations,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# Estimate parameters from historical data
|
|
392
|
+
drift, volatility = simulator.estimate_parameters(historical_prices)
|
|
393
|
+
|
|
394
|
+
# Run simulation
|
|
395
|
+
simulator.simulate_price_paths(drift, volatility)
|
|
396
|
+
|
|
397
|
+
# Calculate statistics
|
|
398
|
+
stats = simulator.calculate_statistics()
|
|
399
|
+
|
|
400
|
+
# Create visualizations
|
|
401
|
+
path_fig = simulator.create_path_visualization(num_paths_to_plot=100)
|
|
402
|
+
dist_fig = simulator.create_distribution_visualization()
|
|
403
|
+
|
|
404
|
+
# Calculate confidence intervals
|
|
405
|
+
confidence_intervals = simulator.calculate_confidence_intervals()
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
"stock_symbol": stock_symbol,
|
|
409
|
+
"politician_name": politician_name,
|
|
410
|
+
"transaction_amount": transaction_amount,
|
|
411
|
+
"current_price": current_price,
|
|
412
|
+
"simulation_days": days_forward,
|
|
413
|
+
"num_simulations": num_simulations,
|
|
414
|
+
"drift": drift,
|
|
415
|
+
"volatility": volatility,
|
|
416
|
+
"statistics": stats,
|
|
417
|
+
"confidence_intervals": confidence_intervals,
|
|
418
|
+
"path_visualization": path_fig,
|
|
419
|
+
"distribution_visualization": dist_fig,
|
|
420
|
+
"simulated_paths": simulator.simulated_paths,
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# Export main classes and functions
|
|
425
|
+
__all__ = [
|
|
426
|
+
"MonteCarloTradingSimulator",
|
|
427
|
+
"simulate_politician_trade_impact",
|
|
428
|
+
]
|