mcli-framework 7.5.1__py3-none-any.whl → 7.6.1__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 +51 -39
- mcli/app/completion_helpers.py +4 -13
- mcli/app/main.py +21 -25
- mcli/app/model_cmd.py +119 -9
- mcli/lib/custom_commands.py +16 -11
- mcli/ml/api/app.py +1 -5
- mcli/ml/dashboard/app.py +2 -2
- mcli/ml/dashboard/app_integrated.py +168 -116
- mcli/ml/dashboard/app_supabase.py +7 -3
- mcli/ml/dashboard/app_training.py +3 -6
- mcli/ml/dashboard/components/charts.py +74 -115
- mcli/ml/dashboard/components/metrics.py +24 -44
- mcli/ml/dashboard/components/tables.py +32 -40
- mcli/ml/dashboard/overview.py +102 -78
- mcli/ml/dashboard/pages/cicd.py +103 -56
- mcli/ml/dashboard/pages/debug_dependencies.py +35 -28
- mcli/ml/dashboard/pages/gravity_viz.py +374 -313
- mcli/ml/dashboard/pages/monte_carlo_predictions.py +50 -48
- mcli/ml/dashboard/pages/predictions_enhanced.py +396 -248
- mcli/ml/dashboard/pages/scrapers_and_logs.py +299 -273
- mcli/ml/dashboard/pages/test_portfolio.py +153 -121
- mcli/ml/dashboard/pages/trading.py +238 -169
- mcli/ml/dashboard/pages/workflows.py +129 -84
- mcli/ml/dashboard/streamlit_extras_utils.py +70 -79
- mcli/ml/dashboard/utils.py +24 -21
- mcli/ml/dashboard/warning_suppression.py +6 -4
- mcli/ml/database/session.py +16 -5
- mcli/ml/mlops/pipeline_orchestrator.py +1 -3
- mcli/ml/predictions/monte_carlo.py +6 -18
- mcli/ml/trading/alpaca_client.py +95 -96
- mcli/ml/trading/migrations.py +76 -40
- mcli/ml/trading/models.py +78 -60
- mcli/ml/trading/paper_trading.py +92 -74
- mcli/ml/trading/risk_management.py +106 -85
- mcli/ml/trading/trading_service.py +155 -110
- mcli/ml/training/train_model.py +1 -3
- mcli/{app → self}/completion_cmd.py +6 -6
- mcli/self/self_cmd.py +100 -57
- mcli/test/test_cmd.py +30 -0
- mcli/workflow/daemon/daemon.py +2 -0
- mcli/workflow/model_service/openai_adapter.py +347 -0
- mcli/workflow/politician_trading/models.py +6 -2
- mcli/workflow/politician_trading/scrapers_corporate_registry.py +39 -88
- mcli/workflow/politician_trading/scrapers_free_sources.py +32 -39
- mcli/workflow/politician_trading/scrapers_third_party.py +21 -39
- mcli/workflow/politician_trading/seed_database.py +70 -89
- {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/METADATA +1 -1
- {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/RECORD +56 -54
- /mcli/{app → self}/logs_cmd.py +0 -0
- /mcli/{app → self}/redis_cmd.py +0 -0
- /mcli/{app → self}/visual_cmd.py +0 -0
- /mcli/{app → test}/cron_test_cmd.py +0 -0
- {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/WHEEL +0 -0
- {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/entry_points.txt +0 -0
- {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/licenses/LICENSE +0 -0
- {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/top_level.txt +0 -0
mcli/ml/dashboard/utils.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Shared utility functions for dashboard pages"""
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
3
|
import logging
|
|
4
|
+
import os
|
|
5
5
|
import warnings
|
|
6
6
|
from typing import List, Optional
|
|
7
|
+
|
|
7
8
|
import pandas as pd
|
|
8
9
|
import streamlit as st
|
|
9
10
|
from supabase import Client, create_client
|
|
@@ -56,7 +57,9 @@ def get_politician_names() -> List[str]:
|
|
|
56
57
|
|
|
57
58
|
result = client.table("politicians").select("first_name, last_name").execute()
|
|
58
59
|
names = [f"{row['first_name']} {row['last_name']}" for row in result.data]
|
|
59
|
-
return
|
|
60
|
+
return (
|
|
61
|
+
names if names else ["Nancy Pelosi", "Paul Pelosi", "Dan Crenshaw", "Josh Gottheimer"]
|
|
62
|
+
)
|
|
60
63
|
except Exception as e:
|
|
61
64
|
logger.error(f"Failed to get politician names: {e}")
|
|
62
65
|
return ["Nancy Pelosi", "Paul Pelosi", "Dan Crenshaw", "Josh Gottheimer"]
|
|
@@ -70,11 +73,7 @@ def get_disclosures_data() -> pd.DataFrame:
|
|
|
70
73
|
|
|
71
74
|
try:
|
|
72
75
|
# First, get total count
|
|
73
|
-
count_response = (
|
|
74
|
-
client.table("trading_disclosures")
|
|
75
|
-
.select("*", count="exact")
|
|
76
|
-
.execute()
|
|
77
|
-
)
|
|
76
|
+
count_response = client.table("trading_disclosures").select("*", count="exact").execute()
|
|
78
77
|
total_count = count_response.count
|
|
79
78
|
|
|
80
79
|
if total_count == 0:
|
|
@@ -103,26 +102,30 @@ def get_disclosures_data() -> pd.DataFrame:
|
|
|
103
102
|
def _generate_demo_disclosures() -> pd.DataFrame:
|
|
104
103
|
"""Generate demo trading disclosure data for testing"""
|
|
105
104
|
st.info("🔵 Using demo trading data (Supabase unavailable)")
|
|
106
|
-
|
|
105
|
+
|
|
107
106
|
import random
|
|
108
107
|
from datetime import datetime, timedelta
|
|
109
|
-
|
|
108
|
+
|
|
110
109
|
politicians = ["Nancy Pelosi", "Paul Pelosi", "Dan Crenshaw", "Josh Gottheimer"]
|
|
111
110
|
tickers = ["AAPL", "MSFT", "GOOGL", "AMZN", "NVDA", "TSLA", "META", "AMD"]
|
|
112
111
|
transaction_types = ["Purchase", "Sale"]
|
|
113
|
-
|
|
112
|
+
|
|
114
113
|
data = []
|
|
115
114
|
for _ in range(50):
|
|
116
|
-
data.append(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
115
|
+
data.append(
|
|
116
|
+
{
|
|
117
|
+
"politician_name": random.choice(politicians),
|
|
118
|
+
"ticker_symbol": random.choice(tickers),
|
|
119
|
+
"transaction_type": random.choice(transaction_types),
|
|
120
|
+
"amount_min": random.randint(1000, 100000),
|
|
121
|
+
"amount_max": random.randint(100000, 1000000),
|
|
122
|
+
"disclosure_date": (
|
|
123
|
+
datetime.now() - timedelta(days=random.randint(1, 365))
|
|
124
|
+
).strftime("%Y-%m-%d"),
|
|
125
|
+
"asset_description": f"{random.choice(tickers)} Stock",
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
|
|
126
129
|
return pd.DataFrame(data)
|
|
127
130
|
|
|
128
131
|
|
|
@@ -158,4 +161,4 @@ def get_politician_trading_history(politician_name: str) -> pd.DataFrame:
|
|
|
158
161
|
|
|
159
162
|
except Exception as e:
|
|
160
163
|
logger.warning(f"Failed to fetch trading history for {politician_name}: {e}")
|
|
161
|
-
return pd.DataFrame()
|
|
164
|
+
return pd.DataFrame()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Warning suppression utilities for Streamlit components used outside runtime context"""
|
|
2
2
|
|
|
3
|
-
import warnings
|
|
4
3
|
import logging
|
|
4
|
+
import warnings
|
|
5
5
|
from contextlib import contextmanager
|
|
6
6
|
|
|
7
7
|
|
|
@@ -14,12 +14,12 @@ def suppress_streamlit_warnings():
|
|
|
14
14
|
warnings.filterwarnings("ignore", message=".*No runtime found.*")
|
|
15
15
|
warnings.filterwarnings("ignore", message=".*Session state does not function.*")
|
|
16
16
|
warnings.filterwarnings("ignore", message=".*to view this Streamlit app.*")
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
# Also suppress logging warnings from Streamlit
|
|
19
19
|
streamlit_logger = logging.getLogger("streamlit")
|
|
20
20
|
original_level = streamlit_logger.level
|
|
21
21
|
streamlit_logger.setLevel(logging.ERROR)
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
try:
|
|
24
24
|
yield
|
|
25
25
|
finally:
|
|
@@ -28,7 +28,9 @@ def suppress_streamlit_warnings():
|
|
|
28
28
|
|
|
29
29
|
def suppress_streamlit_warnings_decorator(func):
|
|
30
30
|
"""Decorator to suppress Streamlit warnings for a function"""
|
|
31
|
+
|
|
31
32
|
def wrapper(*args, **kwargs):
|
|
32
33
|
with suppress_streamlit_warnings():
|
|
33
34
|
return func(*args, **kwargs)
|
|
34
|
-
|
|
35
|
+
|
|
36
|
+
return wrapper
|
mcli/ml/database/session.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"""Database session management"""
|
|
2
2
|
|
|
3
|
+
# Synchronous database setup
|
|
4
|
+
# Prioritize DATABASE_URL environment variable over settings
|
|
5
|
+
import os
|
|
3
6
|
from contextlib import asynccontextmanager, contextmanager
|
|
4
7
|
from typing import AsyncGenerator, Generator
|
|
5
8
|
|
|
@@ -12,9 +15,6 @@ from mcli.ml.config import settings
|
|
|
12
15
|
|
|
13
16
|
from .models import Base
|
|
14
17
|
|
|
15
|
-
# Synchronous database setup
|
|
16
|
-
# Prioritize DATABASE_URL environment variable over settings
|
|
17
|
-
import os
|
|
18
18
|
database_url = os.getenv("DATABASE_URL")
|
|
19
19
|
|
|
20
20
|
# Check if DATABASE_URL has placeholder password
|
|
@@ -53,6 +53,7 @@ if not database_url:
|
|
|
53
53
|
|
|
54
54
|
# Try to connect to poolers
|
|
55
55
|
import logging
|
|
56
|
+
|
|
56
57
|
logger = logging.getLogger(__name__)
|
|
57
58
|
|
|
58
59
|
for pooler_url in pooler_urls:
|
|
@@ -61,13 +62,18 @@ if not database_url:
|
|
|
61
62
|
test_engine = create_engine(pooler_url, pool_pre_ping=True)
|
|
62
63
|
with test_engine.connect() as conn:
|
|
63
64
|
from sqlalchemy import text
|
|
65
|
+
|
|
64
66
|
conn.execute(text("SELECT 1"))
|
|
65
67
|
database_url = pooler_url
|
|
66
|
-
logger.info(
|
|
68
|
+
logger.info(
|
|
69
|
+
f"Successfully connected via pooler: {pooler_url.split('@')[1].split(':')[0]}"
|
|
70
|
+
)
|
|
67
71
|
test_engine.dispose()
|
|
68
72
|
break
|
|
69
73
|
except Exception as e:
|
|
70
|
-
logger.warning(
|
|
74
|
+
logger.warning(
|
|
75
|
+
f"Failed to connect via {pooler_url.split('@')[1].split(':')[0]}: {e}"
|
|
76
|
+
)
|
|
71
77
|
continue
|
|
72
78
|
|
|
73
79
|
if not database_url:
|
|
@@ -75,6 +81,7 @@ if not database_url:
|
|
|
75
81
|
database_url = pooler_urls[0]
|
|
76
82
|
|
|
77
83
|
import warnings
|
|
84
|
+
|
|
78
85
|
warnings.warn(
|
|
79
86
|
"Using Supabase connection pooler with service role key. "
|
|
80
87
|
"For better performance, set DATABASE_URL with your actual database password. "
|
|
@@ -84,6 +91,7 @@ if not database_url:
|
|
|
84
91
|
# Default to SQLite for development/testing
|
|
85
92
|
database_url = "sqlite:///./ml_system.db"
|
|
86
93
|
import warnings
|
|
94
|
+
|
|
87
95
|
warnings.warn(
|
|
88
96
|
"No database credentials found. Using SQLite fallback. "
|
|
89
97
|
"Set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY or DATABASE_URL in environment."
|
|
@@ -91,6 +99,7 @@ if not database_url:
|
|
|
91
99
|
|
|
92
100
|
# Debug: Log which database URL is being used
|
|
93
101
|
import logging
|
|
102
|
+
|
|
94
103
|
logger = logging.getLogger(__name__)
|
|
95
104
|
|
|
96
105
|
if "pooler.supabase.com" in database_url:
|
|
@@ -151,6 +160,7 @@ try:
|
|
|
151
160
|
except (AttributeError, Exception):
|
|
152
161
|
# Fallback for async engine
|
|
153
162
|
import os
|
|
163
|
+
|
|
154
164
|
async_database_url = os.getenv("ASYNC_DATABASE_URL")
|
|
155
165
|
if not async_database_url:
|
|
156
166
|
# Convert sync URL to async if possible
|
|
@@ -224,6 +234,7 @@ def get_session() -> Generator[Session, None, None]:
|
|
|
224
234
|
session = SessionLocal()
|
|
225
235
|
# Test the connection
|
|
226
236
|
from sqlalchemy import text
|
|
237
|
+
|
|
227
238
|
session.execute(text("SELECT 1"))
|
|
228
239
|
yield session
|
|
229
240
|
session.commit()
|
|
@@ -20,9 +20,7 @@ import torch
|
|
|
20
20
|
from ml.features.ensemble_features import EnsembleFeatureBuilder
|
|
21
21
|
from ml.features.political_features import PoliticalInfluenceFeatures
|
|
22
22
|
from ml.features.recommendation_engine import RecommendationConfig as FeatureRecommendationConfig
|
|
23
|
-
from ml.features.recommendation_engine import
|
|
24
|
-
StockRecommendationEngine,
|
|
25
|
-
)
|
|
23
|
+
from ml.features.recommendation_engine import StockRecommendationEngine
|
|
26
24
|
from ml.features.stock_features import StockRecommendationFeatures
|
|
27
25
|
from ml.models.ensemble_models import (
|
|
28
26
|
DeepEnsembleModel,
|
|
@@ -49,9 +49,7 @@ class MonteCarloTradingSimulator:
|
|
|
49
49
|
self.final_prices: Optional[np.ndarray] = None
|
|
50
50
|
self.returns: Optional[np.ndarray] = None
|
|
51
51
|
|
|
52
|
-
def estimate_parameters(
|
|
53
|
-
self, historical_prices: pd.Series
|
|
54
|
-
) -> Tuple[float, float]:
|
|
52
|
+
def estimate_parameters(self, historical_prices: pd.Series) -> Tuple[float, float]:
|
|
55
53
|
"""
|
|
56
54
|
Estimate drift (μ) and volatility (σ) from historical data
|
|
57
55
|
|
|
@@ -79,9 +77,7 @@ class MonteCarloTradingSimulator:
|
|
|
79
77
|
|
|
80
78
|
return annual_return, annual_volatility
|
|
81
79
|
|
|
82
|
-
def simulate_price_paths(
|
|
83
|
-
self, drift: float, volatility: float
|
|
84
|
-
) -> np.ndarray:
|
|
80
|
+
def simulate_price_paths(self, drift: float, volatility: float) -> np.ndarray:
|
|
85
81
|
"""
|
|
86
82
|
Generate Monte Carlo price paths using Geometric Brownian Motion
|
|
87
83
|
|
|
@@ -100,9 +96,7 @@ class MonteCarloTradingSimulator:
|
|
|
100
96
|
paths[:, 0] = self.initial_price
|
|
101
97
|
|
|
102
98
|
# Generate random shocks
|
|
103
|
-
random_shocks = np.random.normal(
|
|
104
|
-
0, 1, size=(self.num_simulations, self.days_to_simulate)
|
|
105
|
-
)
|
|
99
|
+
random_shocks = np.random.normal(0, 1, size=(self.num_simulations, self.days_to_simulate))
|
|
106
100
|
|
|
107
101
|
# Simulate paths using Geometric Brownian Motion
|
|
108
102
|
# S(t+dt) = S(t) * exp((μ - σ²/2)dt + σ√dt * Z)
|
|
@@ -110,9 +104,7 @@ class MonteCarloTradingSimulator:
|
|
|
110
104
|
drift_component = (drift - 0.5 * volatility**2) * dt
|
|
111
105
|
shock_component = volatility * np.sqrt(dt) * random_shocks[:, t - 1]
|
|
112
106
|
|
|
113
|
-
paths[:, t] = paths[:, t - 1] * np.exp(
|
|
114
|
-
drift_component + shock_component
|
|
115
|
-
)
|
|
107
|
+
paths[:, t] = paths[:, t - 1] * np.exp(drift_component + shock_component)
|
|
116
108
|
|
|
117
109
|
self.simulated_paths = paths
|
|
118
110
|
self.final_prices = paths[:, -1]
|
|
@@ -173,9 +165,7 @@ class MonteCarloTradingSimulator:
|
|
|
173
165
|
|
|
174
166
|
# Plot sample paths
|
|
175
167
|
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
|
-
)
|
|
168
|
+
sample_indices = np.random.choice(self.num_simulations, num_to_plot, replace=False)
|
|
179
169
|
|
|
180
170
|
for idx in sample_indices:
|
|
181
171
|
fig.add_trace(
|
|
@@ -311,9 +301,7 @@ class MonteCarloTradingSimulator:
|
|
|
311
301
|
)
|
|
312
302
|
|
|
313
303
|
# 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
|
-
)
|
|
304
|
+
fig.add_vline(x=0, line_dash="dash", line_color="red", annotation_text="0%", row=1, col=2)
|
|
317
305
|
|
|
318
306
|
fig.update_xaxes(title_text="Price ($)", row=1, col=1)
|
|
319
307
|
fig.update_xaxes(title_text="Return (%)", row=1, col=2)
|
mcli/ml/trading/alpaca_client.py
CHANGED
|
@@ -7,19 +7,19 @@ from decimal import Decimal
|
|
|
7
7
|
from typing import Dict, List, Optional, Tuple, Union
|
|
8
8
|
|
|
9
9
|
import pandas as pd
|
|
10
|
+
from alpaca.data.historical import StockHistoricalDataClient
|
|
11
|
+
from alpaca.data.requests import StockBarsRequest
|
|
12
|
+
from alpaca.data.timeframe import TimeFrame
|
|
10
13
|
from alpaca.trading.client import TradingClient
|
|
14
|
+
from alpaca.trading.enums import OrderSide, OrderStatus, TimeInForce
|
|
11
15
|
from alpaca.trading.requests import (
|
|
12
16
|
GetOrdersRequest,
|
|
13
|
-
MarketOrderRequest,
|
|
14
|
-
LimitOrderRequest,
|
|
15
17
|
GetPortfolioHistoryRequest,
|
|
18
|
+
LimitOrderRequest,
|
|
19
|
+
MarketOrderRequest,
|
|
16
20
|
)
|
|
17
|
-
from alpaca.trading.enums import OrderSide, TimeInForce, OrderStatus
|
|
18
|
-
from alpaca.data.historical import StockHistoricalDataClient
|
|
19
|
-
from alpaca.data.requests import StockBarsRequest
|
|
20
|
-
from alpaca.data.timeframe import TimeFrame
|
|
21
|
-
from pydantic import BaseModel, Field
|
|
22
21
|
from dotenv import load_dotenv
|
|
22
|
+
from pydantic import BaseModel, Field
|
|
23
23
|
|
|
24
24
|
# Load environment variables
|
|
25
25
|
load_dotenv()
|
|
@@ -29,7 +29,7 @@ logger = logging.getLogger(__name__)
|
|
|
29
29
|
|
|
30
30
|
class TradingConfig(BaseModel):
|
|
31
31
|
"""Configuration for Alpaca trading"""
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
api_key: str = Field(..., description="Alpaca API key")
|
|
34
34
|
secret_key: str = Field(..., description="Alpaca secret key")
|
|
35
35
|
base_url: str = Field(default="https://paper-api.alpaca.markets", description="Alpaca base URL")
|
|
@@ -39,7 +39,7 @@ class TradingConfig(BaseModel):
|
|
|
39
39
|
|
|
40
40
|
class Position(BaseModel):
|
|
41
41
|
"""Represents a trading position"""
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
symbol: str
|
|
44
44
|
quantity: int
|
|
45
45
|
side: str # "long" or "short"
|
|
@@ -53,7 +53,7 @@ class Position(BaseModel):
|
|
|
53
53
|
|
|
54
54
|
class Order(BaseModel):
|
|
55
55
|
"""Represents a trading order"""
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
id: str
|
|
58
58
|
symbol: str
|
|
59
59
|
side: str # "buy" or "sell"
|
|
@@ -68,7 +68,7 @@ class Order(BaseModel):
|
|
|
68
68
|
|
|
69
69
|
class Portfolio(BaseModel):
|
|
70
70
|
"""Represents portfolio information"""
|
|
71
|
-
|
|
71
|
+
|
|
72
72
|
equity: float
|
|
73
73
|
cash: float
|
|
74
74
|
buying_power: float
|
|
@@ -80,19 +80,16 @@ class Portfolio(BaseModel):
|
|
|
80
80
|
|
|
81
81
|
class AlpacaTradingClient:
|
|
82
82
|
"""Client for Alpaca Trading API operations"""
|
|
83
|
-
|
|
83
|
+
|
|
84
84
|
def __init__(self, config: TradingConfig):
|
|
85
85
|
self.config = config
|
|
86
86
|
self.trading_client = TradingClient(
|
|
87
|
-
api_key=config.api_key,
|
|
88
|
-
secret_key=config.secret_key,
|
|
89
|
-
paper=config.paper_trading
|
|
87
|
+
api_key=config.api_key, secret_key=config.secret_key, paper=config.paper_trading
|
|
90
88
|
)
|
|
91
89
|
self.data_client = StockHistoricalDataClient(
|
|
92
|
-
api_key=config.api_key,
|
|
93
|
-
secret_key=config.secret_key
|
|
90
|
+
api_key=config.api_key, secret_key=config.secret_key
|
|
94
91
|
)
|
|
95
|
-
|
|
92
|
+
|
|
96
93
|
def get_account(self) -> Dict:
|
|
97
94
|
"""Get account information"""
|
|
98
95
|
try:
|
|
@@ -101,22 +98,32 @@ class AlpacaTradingClient:
|
|
|
101
98
|
# Build response with safe attribute access
|
|
102
99
|
response = {
|
|
103
100
|
"account_id": account.id,
|
|
104
|
-
"equity": float(account.equity) if hasattr(account,
|
|
105
|
-
"cash": float(account.cash) if hasattr(account,
|
|
106
|
-
"buying_power":
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
"
|
|
110
|
-
"
|
|
101
|
+
"equity": float(account.equity) if hasattr(account, "equity") else 0.0,
|
|
102
|
+
"cash": float(account.cash) if hasattr(account, "cash") else 0.0,
|
|
103
|
+
"buying_power": (
|
|
104
|
+
float(account.buying_power) if hasattr(account, "buying_power") else 0.0
|
|
105
|
+
),
|
|
106
|
+
"currency": account.currency if hasattr(account, "currency") else "USD",
|
|
107
|
+
"status": (
|
|
108
|
+
account.status.value
|
|
109
|
+
if hasattr(account.status, "value")
|
|
110
|
+
else str(account.status)
|
|
111
|
+
),
|
|
112
|
+
"trading_blocked": (
|
|
113
|
+
account.trading_blocked if hasattr(account, "trading_blocked") else False
|
|
114
|
+
),
|
|
115
|
+
"pattern_day_trader": (
|
|
116
|
+
account.pattern_day_trader if hasattr(account, "pattern_day_trader") else False
|
|
117
|
+
),
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
# Add optional fields that may not exist in all account types
|
|
114
|
-
if hasattr(account,
|
|
121
|
+
if hasattr(account, "portfolio_value"):
|
|
115
122
|
response["portfolio_value"] = float(account.portfolio_value)
|
|
116
123
|
else:
|
|
117
124
|
response["portfolio_value"] = response["equity"]
|
|
118
125
|
|
|
119
|
-
if hasattr(account,
|
|
126
|
+
if hasattr(account, "long_market_value"):
|
|
120
127
|
response["unrealized_pl"] = float(account.long_market_value) - float(account.cash)
|
|
121
128
|
else:
|
|
122
129
|
response["unrealized_pl"] = 0.0
|
|
@@ -127,7 +134,7 @@ class AlpacaTradingClient:
|
|
|
127
134
|
except Exception as e:
|
|
128
135
|
logger.error(f"Failed to get account info: {e}")
|
|
129
136
|
raise
|
|
130
|
-
|
|
137
|
+
|
|
131
138
|
def get_positions(self) -> List[Position]:
|
|
132
139
|
"""Get current positions"""
|
|
133
140
|
try:
|
|
@@ -149,7 +156,7 @@ class AlpacaTradingClient:
|
|
|
149
156
|
except Exception as e:
|
|
150
157
|
logger.error(f"Failed to get positions: {e}")
|
|
151
158
|
return []
|
|
152
|
-
|
|
159
|
+
|
|
153
160
|
def get_orders(self, status: Optional[str] = None, limit: int = 100) -> List[Order]:
|
|
154
161
|
"""Get orders with optional status filter"""
|
|
155
162
|
try:
|
|
@@ -173,28 +180,26 @@ class AlpacaTradingClient:
|
|
|
173
180
|
except Exception as e:
|
|
174
181
|
logger.error(f"Failed to get orders: {e}")
|
|
175
182
|
return []
|
|
176
|
-
|
|
183
|
+
|
|
177
184
|
def place_market_order(
|
|
178
|
-
self,
|
|
179
|
-
symbol: str,
|
|
180
|
-
quantity: int,
|
|
181
|
-
side: str,
|
|
182
|
-
time_in_force: str = "day"
|
|
185
|
+
self, symbol: str, quantity: int, side: str, time_in_force: str = "day"
|
|
183
186
|
) -> Order:
|
|
184
187
|
"""Place a market order"""
|
|
185
188
|
try:
|
|
186
189
|
order_side = OrderSide.BUY if side.lower() == "buy" else OrderSide.SELL
|
|
187
|
-
time_in_force_enum =
|
|
188
|
-
|
|
190
|
+
time_in_force_enum = (
|
|
191
|
+
TimeInForce.DAY if time_in_force.lower() == "day" else TimeInForce.GTC
|
|
192
|
+
)
|
|
193
|
+
|
|
189
194
|
order_request = MarketOrderRequest(
|
|
190
195
|
symbol=symbol,
|
|
191
196
|
qty=quantity,
|
|
192
197
|
side=order_side,
|
|
193
198
|
time_in_force=time_in_force_enum,
|
|
194
199
|
)
|
|
195
|
-
|
|
200
|
+
|
|
196
201
|
order = self.trading_client.submit_order(order_request)
|
|
197
|
-
|
|
202
|
+
|
|
198
203
|
return Order(
|
|
199
204
|
id=order.id,
|
|
200
205
|
symbol=order.symbol,
|
|
@@ -207,20 +212,17 @@ class AlpacaTradingClient:
|
|
|
207
212
|
except Exception as e:
|
|
208
213
|
logger.error(f"Failed to place market order: {e}")
|
|
209
214
|
raise
|
|
210
|
-
|
|
215
|
+
|
|
211
216
|
def place_limit_order(
|
|
212
|
-
self,
|
|
213
|
-
symbol: str,
|
|
214
|
-
quantity: int,
|
|
215
|
-
side: str,
|
|
216
|
-
limit_price: float,
|
|
217
|
-
time_in_force: str = "day"
|
|
217
|
+
self, symbol: str, quantity: int, side: str, limit_price: float, time_in_force: str = "day"
|
|
218
218
|
) -> Order:
|
|
219
219
|
"""Place a limit order"""
|
|
220
220
|
try:
|
|
221
221
|
order_side = OrderSide.BUY if side.lower() == "buy" else OrderSide.SELL
|
|
222
|
-
time_in_force_enum =
|
|
223
|
-
|
|
222
|
+
time_in_force_enum = (
|
|
223
|
+
TimeInForce.DAY if time_in_force.lower() == "day" else TimeInForce.GTC
|
|
224
|
+
)
|
|
225
|
+
|
|
224
226
|
order_request = LimitOrderRequest(
|
|
225
227
|
symbol=symbol,
|
|
226
228
|
qty=quantity,
|
|
@@ -228,9 +230,9 @@ class AlpacaTradingClient:
|
|
|
228
230
|
time_in_force=time_in_force_enum,
|
|
229
231
|
limit_price=limit_price,
|
|
230
232
|
)
|
|
231
|
-
|
|
233
|
+
|
|
232
234
|
order = self.trading_client.submit_order(order_request)
|
|
233
|
-
|
|
235
|
+
|
|
234
236
|
return Order(
|
|
235
237
|
id=order.id,
|
|
236
238
|
symbol=order.symbol,
|
|
@@ -243,7 +245,7 @@ class AlpacaTradingClient:
|
|
|
243
245
|
except Exception as e:
|
|
244
246
|
logger.error(f"Failed to place limit order: {e}")
|
|
245
247
|
raise
|
|
246
|
-
|
|
248
|
+
|
|
247
249
|
def cancel_order(self, order_id: str) -> bool:
|
|
248
250
|
"""Cancel an order"""
|
|
249
251
|
try:
|
|
@@ -252,12 +254,8 @@ class AlpacaTradingClient:
|
|
|
252
254
|
except Exception as e:
|
|
253
255
|
logger.error(f"Failed to cancel order {order_id}: {e}")
|
|
254
256
|
return False
|
|
255
|
-
|
|
256
|
-
def get_portfolio_history(
|
|
257
|
-
self,
|
|
258
|
-
period: str = "1M",
|
|
259
|
-
timeframe: str = "1Day"
|
|
260
|
-
) -> pd.DataFrame:
|
|
257
|
+
|
|
258
|
+
def get_portfolio_history(self, period: str = "1M", timeframe: str = "1Day") -> pd.DataFrame:
|
|
261
259
|
"""Get portfolio history"""
|
|
262
260
|
try:
|
|
263
261
|
# Convert period to start/end dates
|
|
@@ -274,78 +272,80 @@ class AlpacaTradingClient:
|
|
|
274
272
|
start_date = end_date - timedelta(days=365)
|
|
275
273
|
else:
|
|
276
274
|
start_date = end_date - timedelta(days=30)
|
|
277
|
-
|
|
275
|
+
|
|
278
276
|
# Convert timeframe
|
|
279
277
|
tf = TimeFrame.Day if timeframe == "1Day" else TimeFrame.Hour
|
|
280
|
-
|
|
278
|
+
|
|
281
279
|
request = GetPortfolioHistoryRequest(
|
|
282
280
|
start=start_date,
|
|
283
281
|
end=end_date,
|
|
284
282
|
timeframe=tf,
|
|
285
283
|
)
|
|
286
|
-
|
|
284
|
+
|
|
287
285
|
history = self.trading_client.get_portfolio_history(request)
|
|
288
|
-
|
|
286
|
+
|
|
289
287
|
# Convert to DataFrame
|
|
290
288
|
data = []
|
|
291
289
|
for i, timestamp in enumerate(history.timestamp):
|
|
292
|
-
data.append(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
290
|
+
data.append(
|
|
291
|
+
{
|
|
292
|
+
"timestamp": timestamp,
|
|
293
|
+
"equity": float(history.equity[i]) if history.equity else 0,
|
|
294
|
+
"profit_loss": float(history.profit_loss[i]) if history.profit_loss else 0,
|
|
295
|
+
"profit_loss_pct": (
|
|
296
|
+
float(history.profit_loss_pct[i]) if history.profit_loss_pct else 0
|
|
297
|
+
),
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
|
|
299
301
|
return pd.DataFrame(data)
|
|
300
302
|
except Exception as e:
|
|
301
303
|
logger.error(f"Failed to get portfolio history: {e}")
|
|
302
304
|
return pd.DataFrame()
|
|
303
|
-
|
|
305
|
+
|
|
304
306
|
def get_stock_data(
|
|
305
|
-
self,
|
|
306
|
-
symbols: List[str],
|
|
307
|
-
start_date: datetime,
|
|
308
|
-
end_date: datetime,
|
|
309
|
-
timeframe: str = "1Day"
|
|
307
|
+
self, symbols: List[str], start_date: datetime, end_date: datetime, timeframe: str = "1Day"
|
|
310
308
|
) -> pd.DataFrame:
|
|
311
309
|
"""Get historical stock data"""
|
|
312
310
|
try:
|
|
313
311
|
tf = TimeFrame.Day if timeframe == "1Day" else TimeFrame.Hour
|
|
314
|
-
|
|
312
|
+
|
|
315
313
|
request = StockBarsRequest(
|
|
316
314
|
symbol_or_symbols=symbols,
|
|
317
315
|
timeframe=tf,
|
|
318
316
|
start=start_date,
|
|
319
317
|
end=end_date,
|
|
320
318
|
)
|
|
321
|
-
|
|
319
|
+
|
|
322
320
|
bars = self.data_client.get_stock_bars(request)
|
|
323
|
-
|
|
321
|
+
|
|
324
322
|
# Convert to DataFrame
|
|
325
323
|
data = []
|
|
326
324
|
for symbol, bar_list in bars.items():
|
|
327
325
|
for bar in bar_list:
|
|
328
|
-
data.append(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
326
|
+
data.append(
|
|
327
|
+
{
|
|
328
|
+
"symbol": symbol,
|
|
329
|
+
"timestamp": bar.timestamp,
|
|
330
|
+
"open": float(bar.open),
|
|
331
|
+
"high": float(bar.high),
|
|
332
|
+
"low": float(bar.low),
|
|
333
|
+
"close": float(bar.close),
|
|
334
|
+
"volume": int(bar.volume),
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
|
|
338
338
|
return pd.DataFrame(data)
|
|
339
339
|
except Exception as e:
|
|
340
340
|
logger.error(f"Failed to get stock data: {e}")
|
|
341
341
|
return pd.DataFrame()
|
|
342
|
-
|
|
342
|
+
|
|
343
343
|
def get_portfolio(self) -> Portfolio:
|
|
344
344
|
"""Get complete portfolio information"""
|
|
345
345
|
try:
|
|
346
346
|
account = self.get_account()
|
|
347
347
|
positions = self.get_positions()
|
|
348
|
-
|
|
348
|
+
|
|
349
349
|
return Portfolio(
|
|
350
350
|
equity=account["equity"],
|
|
351
351
|
cash=account["cash"],
|
|
@@ -360,7 +360,9 @@ class AlpacaTradingClient:
|
|
|
360
360
|
raise
|
|
361
361
|
|
|
362
362
|
|
|
363
|
-
def create_trading_client(
|
|
363
|
+
def create_trading_client(
|
|
364
|
+
api_key: str = None, secret_key: str = None, paper_trading: bool = True
|
|
365
|
+
) -> AlpacaTradingClient:
|
|
364
366
|
"""
|
|
365
367
|
Create a trading client with the given credentials or from environment variables
|
|
366
368
|
|
|
@@ -387,10 +389,7 @@ def create_trading_client(api_key: str = None, secret_key: str = None, paper_tra
|
|
|
387
389
|
base_url = os.getenv("ALPACA_BASE_URL", "https://paper-api.alpaca.markets")
|
|
388
390
|
|
|
389
391
|
config = TradingConfig(
|
|
390
|
-
api_key=api_key,
|
|
391
|
-
secret_key=secret_key,
|
|
392
|
-
base_url=base_url,
|
|
393
|
-
paper_trading=paper_trading
|
|
392
|
+
api_key=api_key, secret_key=secret_key, base_url=base_url, paper_trading=paper_trading
|
|
394
393
|
)
|
|
395
394
|
return AlpacaTradingClient(config)
|
|
396
395
|
|
|
@@ -413,5 +412,5 @@ def get_alpaca_config_from_env() -> Optional[Dict[str, str]]:
|
|
|
413
412
|
"api_key": api_key,
|
|
414
413
|
"secret_key": secret_key,
|
|
415
414
|
"base_url": base_url,
|
|
416
|
-
"is_paper": "paper" in base_url.lower()
|
|
417
|
-
}
|
|
415
|
+
"is_paper": "paper" in base_url.lower(),
|
|
416
|
+
}
|