mcli-framework 7.3.1__py3-none-any.whl → 7.4.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.

@@ -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
- settings.database.url,
18
- **settings.get_database_config(),
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
- async_engine = create_async_engine(
31
- settings.database.async_url,
32
- pool_size=settings.database.pool_size,
33
- max_overflow=settings.database.max_overflow,
34
- pool_timeout=settings.database.pool_timeout,
35
- pool_pre_ping=True,
36
- echo=settings.debug,
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 = SessionLocal()
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.rollback()
96
- raise
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.close()
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
+ ]
@@ -25,7 +25,11 @@ from mcli.ml.trading.models import (
25
25
  TradingSignalResponse,
26
26
  )
27
27
  from mcli.ml.trading.trading_service import TradingService
28
- from mcli.ml.trading.alpaca_client import AlpacaTradingClient
28
+ from mcli.ml.trading.alpaca_client import (
29
+ AlpacaTradingClient,
30
+ create_trading_client,
31
+ get_alpaca_config_from_env,
32
+ )
29
33
  from mcli.ml.trading.risk_management import RiskManager
30
34
  from mcli.ml.trading.paper_trading import PaperTradingEngine
31
35
 
@@ -55,6 +59,8 @@ __all__ = [
55
59
  # Services
56
60
  "TradingService",
57
61
  "AlpacaTradingClient",
62
+ "create_trading_client",
63
+ "get_alpaca_config_from_env",
58
64
  "RiskManager",
59
65
  "PaperTradingEngine",
60
66
  ]