mcli-framework 7.2.0__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.
- mcli/__init__.py +160 -0
- mcli/__main__.py +14 -0
- mcli/app/__init__.py +23 -0
- mcli/app/commands_cmd.py +741 -0
- mcli/app/model/__init__.py +0 -0
- mcli/app/video/__init__.py +5 -0
- mcli/chat/__init__.py +34 -0
- mcli/lib/__init__.py +0 -0
- mcli/lib/api/__init__.py +0 -0
- mcli/lib/auth/__init__.py +1 -0
- mcli/lib/config/__init__.py +1 -0
- mcli/lib/erd/__init__.py +25 -0
- mcli/lib/files/__init__.py +0 -0
- mcli/lib/fs/__init__.py +1 -0
- mcli/lib/logger/__init__.py +3 -0
- mcli/lib/performance/__init__.py +17 -0
- mcli/lib/pickles/__init__.py +1 -0
- mcli/lib/shell/__init__.py +0 -0
- mcli/lib/toml/__init__.py +1 -0
- mcli/lib/watcher/__init__.py +0 -0
- mcli/ml/__init__.py +16 -0
- mcli/ml/api/__init__.py +30 -0
- mcli/ml/api/routers/__init__.py +27 -0
- mcli/ml/api/schemas.py +2 -2
- mcli/ml/auth/__init__.py +45 -0
- mcli/ml/auth/models.py +2 -2
- mcli/ml/backtesting/__init__.py +39 -0
- mcli/ml/cli/__init__.py +5 -0
- mcli/ml/cli/main.py +1 -1
- mcli/ml/config/__init__.py +33 -0
- mcli/ml/configs/__init__.py +16 -0
- mcli/ml/dashboard/__init__.py +12 -0
- mcli/ml/dashboard/app_integrated.py +296 -30
- mcli/ml/dashboard/app_training.py +1 -1
- mcli/ml/dashboard/components/__init__.py +7 -0
- mcli/ml/dashboard/pages/__init__.py +6 -0
- mcli/ml/dashboard/pages/cicd.py +1 -1
- mcli/ml/dashboard/pages/debug_dependencies.py +364 -0
- mcli/ml/dashboard/pages/gravity_viz.py +565 -0
- mcli/ml/dashboard/pages/monte_carlo_predictions.py +555 -0
- mcli/ml/dashboard/pages/overview.py +378 -0
- mcli/ml/dashboard/pages/predictions_enhanced.py +20 -6
- mcli/ml/dashboard/pages/scrapers_and_logs.py +22 -6
- mcli/ml/dashboard/pages/test_portfolio.py +423 -0
- mcli/ml/dashboard/pages/trading.py +768 -0
- mcli/ml/dashboard/streamlit_extras_utils.py +297 -0
- mcli/ml/dashboard/utils.py +161 -0
- mcli/ml/dashboard/warning_suppression.py +34 -0
- mcli/ml/data_ingestion/__init__.py +39 -0
- mcli/ml/database/__init__.py +47 -0
- mcli/ml/database/session.py +169 -16
- mcli/ml/experimentation/__init__.py +29 -0
- mcli/ml/features/__init__.py +39 -0
- mcli/ml/mlops/__init__.py +33 -0
- mcli/ml/models/__init__.py +94 -0
- mcli/ml/monitoring/__init__.py +25 -0
- mcli/ml/optimization/__init__.py +27 -0
- mcli/ml/predictions/__init__.py +5 -0
- mcli/ml/predictions/monte_carlo.py +428 -0
- mcli/ml/preprocessing/__init__.py +28 -0
- mcli/ml/scripts/__init__.py +1 -0
- mcli/ml/trading/__init__.py +66 -0
- mcli/ml/trading/alpaca_client.py +417 -0
- mcli/ml/trading/migrations.py +164 -0
- mcli/ml/trading/models.py +418 -0
- mcli/ml/trading/paper_trading.py +326 -0
- mcli/ml/trading/risk_management.py +370 -0
- mcli/ml/trading/trading_service.py +480 -0
- mcli/ml/training/__init__.py +10 -0
- mcli/mygroup/__init__.py +3 -0
- mcli/public/__init__.py +1 -0
- mcli/public/commands/__init__.py +2 -0
- mcli/self/__init__.py +3 -0
- mcli/self/self_cmd.py +514 -15
- mcli/workflow/__init__.py +0 -0
- mcli/workflow/daemon/__init__.py +15 -0
- mcli/workflow/daemon/daemon.py +21 -3
- mcli/workflow/dashboard/__init__.py +5 -0
- mcli/workflow/docker/__init__.py +0 -0
- mcli/workflow/file/__init__.py +0 -0
- mcli/workflow/gcloud/__init__.py +1 -0
- mcli/workflow/git_commit/__init__.py +0 -0
- mcli/workflow/interview/__init__.py +0 -0
- mcli/workflow/politician_trading/__init__.py +4 -0
- mcli/workflow/registry/__init__.py +0 -0
- mcli/workflow/repo/__init__.py +0 -0
- mcli/workflow/scheduler/__init__.py +25 -0
- mcli/workflow/search/__init__.py +0 -0
- mcli/workflow/sync/__init__.py +5 -0
- mcli/workflow/videos/__init__.py +1 -0
- mcli/workflow/wakatime/__init__.py +80 -0
- {mcli_framework-7.2.0.dist-info → mcli_framework-7.4.0.dist-info}/METADATA +4 -1
- {mcli_framework-7.2.0.dist-info → mcli_framework-7.4.0.dist-info}/RECORD +97 -18
- {mcli_framework-7.2.0.dist-info → mcli_framework-7.4.0.dist-info}/WHEEL +0 -0
- {mcli_framework-7.2.0.dist-info → mcli_framework-7.4.0.dist-info}/entry_points.txt +0 -0
- {mcli_framework-7.2.0.dist-info → mcli_framework-7.4.0.dist-info}/licenses/LICENSE +0 -0
- {mcli_framework-7.2.0.dist-info → mcli_framework-7.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""Alpaca Trading API client for executing trades"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from typing import Dict, List, Optional, Tuple, Union
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from alpaca.trading.client import TradingClient
|
|
11
|
+
from alpaca.trading.requests import (
|
|
12
|
+
GetOrdersRequest,
|
|
13
|
+
MarketOrderRequest,
|
|
14
|
+
LimitOrderRequest,
|
|
15
|
+
GetPortfolioHistoryRequest,
|
|
16
|
+
)
|
|
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
|
+
from dotenv import load_dotenv
|
|
23
|
+
|
|
24
|
+
# Load environment variables
|
|
25
|
+
load_dotenv()
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TradingConfig(BaseModel):
|
|
31
|
+
"""Configuration for Alpaca trading"""
|
|
32
|
+
|
|
33
|
+
api_key: str = Field(..., description="Alpaca API key")
|
|
34
|
+
secret_key: str = Field(..., description="Alpaca secret key")
|
|
35
|
+
base_url: str = Field(default="https://paper-api.alpaca.markets", description="Alpaca base URL")
|
|
36
|
+
data_url: str = Field(default="https://data.alpaca.markets", description="Alpaca data URL")
|
|
37
|
+
paper_trading: bool = Field(default=True, description="Use paper trading")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Position(BaseModel):
|
|
41
|
+
"""Represents a trading position"""
|
|
42
|
+
|
|
43
|
+
symbol: str
|
|
44
|
+
quantity: int
|
|
45
|
+
side: str # "long" or "short"
|
|
46
|
+
market_value: float
|
|
47
|
+
cost_basis: float
|
|
48
|
+
unrealized_pl: float
|
|
49
|
+
unrealized_plpc: float
|
|
50
|
+
current_price: float
|
|
51
|
+
qty_available: int
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Order(BaseModel):
|
|
55
|
+
"""Represents a trading order"""
|
|
56
|
+
|
|
57
|
+
id: str
|
|
58
|
+
symbol: str
|
|
59
|
+
side: str # "buy" or "sell"
|
|
60
|
+
quantity: int
|
|
61
|
+
order_type: str
|
|
62
|
+
status: str
|
|
63
|
+
created_at: datetime
|
|
64
|
+
filled_at: Optional[datetime] = None
|
|
65
|
+
filled_price: Optional[float] = None
|
|
66
|
+
filled_quantity: Optional[int] = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Portfolio(BaseModel):
|
|
70
|
+
"""Represents portfolio information"""
|
|
71
|
+
|
|
72
|
+
equity: float
|
|
73
|
+
cash: float
|
|
74
|
+
buying_power: float
|
|
75
|
+
portfolio_value: float
|
|
76
|
+
unrealized_pl: float
|
|
77
|
+
realized_pl: float
|
|
78
|
+
positions: List[Position] = []
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class AlpacaTradingClient:
|
|
82
|
+
"""Client for Alpaca Trading API operations"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, config: TradingConfig):
|
|
85
|
+
self.config = config
|
|
86
|
+
self.trading_client = TradingClient(
|
|
87
|
+
api_key=config.api_key,
|
|
88
|
+
secret_key=config.secret_key,
|
|
89
|
+
paper=config.paper_trading
|
|
90
|
+
)
|
|
91
|
+
self.data_client = StockHistoricalDataClient(
|
|
92
|
+
api_key=config.api_key,
|
|
93
|
+
secret_key=config.secret_key
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def get_account(self) -> Dict:
|
|
97
|
+
"""Get account information"""
|
|
98
|
+
try:
|
|
99
|
+
account = self.trading_client.get_account()
|
|
100
|
+
|
|
101
|
+
# Build response with safe attribute access
|
|
102
|
+
response = {
|
|
103
|
+
"account_id": account.id,
|
|
104
|
+
"equity": float(account.equity) if hasattr(account, 'equity') else 0.0,
|
|
105
|
+
"cash": float(account.cash) if hasattr(account, 'cash') else 0.0,
|
|
106
|
+
"buying_power": float(account.buying_power) if hasattr(account, 'buying_power') else 0.0,
|
|
107
|
+
"currency": account.currency if hasattr(account, 'currency') else "USD",
|
|
108
|
+
"status": account.status.value if hasattr(account.status, 'value') else str(account.status),
|
|
109
|
+
"trading_blocked": account.trading_blocked if hasattr(account, 'trading_blocked') else False,
|
|
110
|
+
"pattern_day_trader": account.pattern_day_trader if hasattr(account, 'pattern_day_trader') else False,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Add optional fields that may not exist in all account types
|
|
114
|
+
if hasattr(account, 'portfolio_value'):
|
|
115
|
+
response["portfolio_value"] = float(account.portfolio_value)
|
|
116
|
+
else:
|
|
117
|
+
response["portfolio_value"] = response["equity"]
|
|
118
|
+
|
|
119
|
+
if hasattr(account, 'long_market_value'):
|
|
120
|
+
response["unrealized_pl"] = float(account.long_market_value) - float(account.cash)
|
|
121
|
+
else:
|
|
122
|
+
response["unrealized_pl"] = 0.0
|
|
123
|
+
|
|
124
|
+
response["realized_pl"] = 0.0 # Not always available in paper accounts
|
|
125
|
+
|
|
126
|
+
return response
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error(f"Failed to get account info: {e}")
|
|
129
|
+
raise
|
|
130
|
+
|
|
131
|
+
def get_positions(self) -> List[Position]:
|
|
132
|
+
"""Get current positions"""
|
|
133
|
+
try:
|
|
134
|
+
positions = self.trading_client.get_all_positions()
|
|
135
|
+
return [
|
|
136
|
+
Position(
|
|
137
|
+
symbol=pos.symbol,
|
|
138
|
+
quantity=int(pos.qty),
|
|
139
|
+
side="long" if int(pos.qty) > 0 else "short",
|
|
140
|
+
market_value=float(pos.market_value),
|
|
141
|
+
cost_basis=float(pos.cost_basis),
|
|
142
|
+
unrealized_pl=float(pos.unrealized_pl),
|
|
143
|
+
unrealized_plpc=float(pos.unrealized_plpc),
|
|
144
|
+
current_price=float(pos.current_price),
|
|
145
|
+
qty_available=int(pos.qty_available),
|
|
146
|
+
)
|
|
147
|
+
for pos in positions
|
|
148
|
+
]
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.error(f"Failed to get positions: {e}")
|
|
151
|
+
return []
|
|
152
|
+
|
|
153
|
+
def get_orders(self, status: Optional[str] = None, limit: int = 100) -> List[Order]:
|
|
154
|
+
"""Get orders with optional status filter"""
|
|
155
|
+
try:
|
|
156
|
+
request = GetOrdersRequest(status=status, limit=limit)
|
|
157
|
+
orders = self.trading_client.get_orders(request)
|
|
158
|
+
return [
|
|
159
|
+
Order(
|
|
160
|
+
id=order.id,
|
|
161
|
+
symbol=order.symbol,
|
|
162
|
+
side=order.side.value,
|
|
163
|
+
quantity=int(order.qty),
|
|
164
|
+
order_type=order.order_type.value,
|
|
165
|
+
status=order.status.value,
|
|
166
|
+
created_at=order.created_at,
|
|
167
|
+
filled_at=order.filled_at,
|
|
168
|
+
filled_price=float(order.filled_avg_price) if order.filled_avg_price else None,
|
|
169
|
+
filled_quantity=int(order.filled_qty) if order.filled_qty else None,
|
|
170
|
+
)
|
|
171
|
+
for order in orders
|
|
172
|
+
]
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logger.error(f"Failed to get orders: {e}")
|
|
175
|
+
return []
|
|
176
|
+
|
|
177
|
+
def place_market_order(
|
|
178
|
+
self,
|
|
179
|
+
symbol: str,
|
|
180
|
+
quantity: int,
|
|
181
|
+
side: str,
|
|
182
|
+
time_in_force: str = "day"
|
|
183
|
+
) -> Order:
|
|
184
|
+
"""Place a market order"""
|
|
185
|
+
try:
|
|
186
|
+
order_side = OrderSide.BUY if side.lower() == "buy" else OrderSide.SELL
|
|
187
|
+
time_in_force_enum = TimeInForce.DAY if time_in_force.lower() == "day" else TimeInForce.GTC
|
|
188
|
+
|
|
189
|
+
order_request = MarketOrderRequest(
|
|
190
|
+
symbol=symbol,
|
|
191
|
+
qty=quantity,
|
|
192
|
+
side=order_side,
|
|
193
|
+
time_in_force=time_in_force_enum,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
order = self.trading_client.submit_order(order_request)
|
|
197
|
+
|
|
198
|
+
return Order(
|
|
199
|
+
id=order.id,
|
|
200
|
+
symbol=order.symbol,
|
|
201
|
+
side=order.side.value,
|
|
202
|
+
quantity=int(order.qty),
|
|
203
|
+
order_type=order.order_type.value,
|
|
204
|
+
status=order.status.value,
|
|
205
|
+
created_at=order.created_at,
|
|
206
|
+
)
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.error(f"Failed to place market order: {e}")
|
|
209
|
+
raise
|
|
210
|
+
|
|
211
|
+
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"
|
|
218
|
+
) -> Order:
|
|
219
|
+
"""Place a limit order"""
|
|
220
|
+
try:
|
|
221
|
+
order_side = OrderSide.BUY if side.lower() == "buy" else OrderSide.SELL
|
|
222
|
+
time_in_force_enum = TimeInForce.DAY if time_in_force.lower() == "day" else TimeInForce.GTC
|
|
223
|
+
|
|
224
|
+
order_request = LimitOrderRequest(
|
|
225
|
+
symbol=symbol,
|
|
226
|
+
qty=quantity,
|
|
227
|
+
side=order_side,
|
|
228
|
+
time_in_force=time_in_force_enum,
|
|
229
|
+
limit_price=limit_price,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
order = self.trading_client.submit_order(order_request)
|
|
233
|
+
|
|
234
|
+
return Order(
|
|
235
|
+
id=order.id,
|
|
236
|
+
symbol=order.symbol,
|
|
237
|
+
side=order.side.value,
|
|
238
|
+
quantity=int(order.qty),
|
|
239
|
+
order_type=order.order_type.value,
|
|
240
|
+
status=order.status.value,
|
|
241
|
+
created_at=order.created_at,
|
|
242
|
+
)
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.error(f"Failed to place limit order: {e}")
|
|
245
|
+
raise
|
|
246
|
+
|
|
247
|
+
def cancel_order(self, order_id: str) -> bool:
|
|
248
|
+
"""Cancel an order"""
|
|
249
|
+
try:
|
|
250
|
+
self.trading_client.cancel_order_by_id(order_id)
|
|
251
|
+
return True
|
|
252
|
+
except Exception as e:
|
|
253
|
+
logger.error(f"Failed to cancel order {order_id}: {e}")
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
def get_portfolio_history(
|
|
257
|
+
self,
|
|
258
|
+
period: str = "1M",
|
|
259
|
+
timeframe: str = "1Day"
|
|
260
|
+
) -> pd.DataFrame:
|
|
261
|
+
"""Get portfolio history"""
|
|
262
|
+
try:
|
|
263
|
+
# Convert period to start/end dates
|
|
264
|
+
end_date = datetime.now()
|
|
265
|
+
if period == "1D":
|
|
266
|
+
start_date = end_date - timedelta(days=1)
|
|
267
|
+
elif period == "1W":
|
|
268
|
+
start_date = end_date - timedelta(weeks=1)
|
|
269
|
+
elif period == "1M":
|
|
270
|
+
start_date = end_date - timedelta(days=30)
|
|
271
|
+
elif period == "3M":
|
|
272
|
+
start_date = end_date - timedelta(days=90)
|
|
273
|
+
elif period == "1Y":
|
|
274
|
+
start_date = end_date - timedelta(days=365)
|
|
275
|
+
else:
|
|
276
|
+
start_date = end_date - timedelta(days=30)
|
|
277
|
+
|
|
278
|
+
# Convert timeframe
|
|
279
|
+
tf = TimeFrame.Day if timeframe == "1Day" else TimeFrame.Hour
|
|
280
|
+
|
|
281
|
+
request = GetPortfolioHistoryRequest(
|
|
282
|
+
start=start_date,
|
|
283
|
+
end=end_date,
|
|
284
|
+
timeframe=tf,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
history = self.trading_client.get_portfolio_history(request)
|
|
288
|
+
|
|
289
|
+
# Convert to DataFrame
|
|
290
|
+
data = []
|
|
291
|
+
for i, timestamp in enumerate(history.timestamp):
|
|
292
|
+
data.append({
|
|
293
|
+
"timestamp": timestamp,
|
|
294
|
+
"equity": float(history.equity[i]) if history.equity else 0,
|
|
295
|
+
"profit_loss": float(history.profit_loss[i]) if history.profit_loss else 0,
|
|
296
|
+
"profit_loss_pct": float(history.profit_loss_pct[i]) if history.profit_loss_pct else 0,
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
return pd.DataFrame(data)
|
|
300
|
+
except Exception as e:
|
|
301
|
+
logger.error(f"Failed to get portfolio history: {e}")
|
|
302
|
+
return pd.DataFrame()
|
|
303
|
+
|
|
304
|
+
def get_stock_data(
|
|
305
|
+
self,
|
|
306
|
+
symbols: List[str],
|
|
307
|
+
start_date: datetime,
|
|
308
|
+
end_date: datetime,
|
|
309
|
+
timeframe: str = "1Day"
|
|
310
|
+
) -> pd.DataFrame:
|
|
311
|
+
"""Get historical stock data"""
|
|
312
|
+
try:
|
|
313
|
+
tf = TimeFrame.Day if timeframe == "1Day" else TimeFrame.Hour
|
|
314
|
+
|
|
315
|
+
request = StockBarsRequest(
|
|
316
|
+
symbol_or_symbols=symbols,
|
|
317
|
+
timeframe=tf,
|
|
318
|
+
start=start_date,
|
|
319
|
+
end=end_date,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
bars = self.data_client.get_stock_bars(request)
|
|
323
|
+
|
|
324
|
+
# Convert to DataFrame
|
|
325
|
+
data = []
|
|
326
|
+
for symbol, bar_list in bars.items():
|
|
327
|
+
for bar in bar_list:
|
|
328
|
+
data.append({
|
|
329
|
+
"symbol": symbol,
|
|
330
|
+
"timestamp": bar.timestamp,
|
|
331
|
+
"open": float(bar.open),
|
|
332
|
+
"high": float(bar.high),
|
|
333
|
+
"low": float(bar.low),
|
|
334
|
+
"close": float(bar.close),
|
|
335
|
+
"volume": int(bar.volume),
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
return pd.DataFrame(data)
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.error(f"Failed to get stock data: {e}")
|
|
341
|
+
return pd.DataFrame()
|
|
342
|
+
|
|
343
|
+
def get_portfolio(self) -> Portfolio:
|
|
344
|
+
"""Get complete portfolio information"""
|
|
345
|
+
try:
|
|
346
|
+
account = self.get_account()
|
|
347
|
+
positions = self.get_positions()
|
|
348
|
+
|
|
349
|
+
return Portfolio(
|
|
350
|
+
equity=account["equity"],
|
|
351
|
+
cash=account["cash"],
|
|
352
|
+
buying_power=account["buying_power"],
|
|
353
|
+
portfolio_value=account["portfolio_value"],
|
|
354
|
+
unrealized_pl=account["unrealized_pl"],
|
|
355
|
+
realized_pl=account["realized_pl"],
|
|
356
|
+
positions=positions,
|
|
357
|
+
)
|
|
358
|
+
except Exception as e:
|
|
359
|
+
logger.error(f"Failed to get portfolio: {e}")
|
|
360
|
+
raise
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def create_trading_client(api_key: str = None, secret_key: str = None, paper_trading: bool = True) -> AlpacaTradingClient:
|
|
364
|
+
"""
|
|
365
|
+
Create a trading client with the given credentials or from environment variables
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
api_key: Alpaca API key (if None, loads from ALPACA_API_KEY env var)
|
|
369
|
+
secret_key: Alpaca secret key (if None, loads from ALPACA_SECRET_KEY env var)
|
|
370
|
+
paper_trading: Whether to use paper trading (default: True)
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
AlpacaTradingClient instance
|
|
374
|
+
"""
|
|
375
|
+
# Load from environment if not provided
|
|
376
|
+
if api_key is None:
|
|
377
|
+
api_key = os.getenv("ALPACA_API_KEY")
|
|
378
|
+
if secret_key is None:
|
|
379
|
+
secret_key = os.getenv("ALPACA_SECRET_KEY")
|
|
380
|
+
|
|
381
|
+
if not api_key or not secret_key:
|
|
382
|
+
raise ValueError(
|
|
383
|
+
"Alpaca API credentials not found. "
|
|
384
|
+
"Please provide api_key and secret_key, or set ALPACA_API_KEY and ALPACA_SECRET_KEY environment variables."
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
base_url = os.getenv("ALPACA_BASE_URL", "https://paper-api.alpaca.markets")
|
|
388
|
+
|
|
389
|
+
config = TradingConfig(
|
|
390
|
+
api_key=api_key,
|
|
391
|
+
secret_key=secret_key,
|
|
392
|
+
base_url=base_url,
|
|
393
|
+
paper_trading=paper_trading
|
|
394
|
+
)
|
|
395
|
+
return AlpacaTradingClient(config)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def get_alpaca_config_from_env() -> Optional[Dict[str, str]]:
|
|
399
|
+
"""
|
|
400
|
+
Get Alpaca configuration from environment variables
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Dictionary with API configuration or None if not configured
|
|
404
|
+
"""
|
|
405
|
+
api_key = os.getenv("ALPACA_API_KEY")
|
|
406
|
+
secret_key = os.getenv("ALPACA_SECRET_KEY")
|
|
407
|
+
base_url = os.getenv("ALPACA_BASE_URL", "https://paper-api.alpaca.markets")
|
|
408
|
+
|
|
409
|
+
if not api_key or not secret_key:
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
"api_key": api_key,
|
|
414
|
+
"secret_key": secret_key,
|
|
415
|
+
"base_url": base_url,
|
|
416
|
+
"is_paper": "paper" in base_url.lower()
|
|
417
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Database migrations for trading functionality"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from sqlalchemy import create_engine, text
|
|
5
|
+
from sqlalchemy.orm import sessionmaker
|
|
6
|
+
from mcli.ml.trading.models import Base
|
|
7
|
+
from mcli.ml.config.settings import get_settings
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_trading_tables():
|
|
13
|
+
"""Create trading-related tables in the database"""
|
|
14
|
+
try:
|
|
15
|
+
# Get database URL from settings
|
|
16
|
+
settings = get_settings()
|
|
17
|
+
engine = create_engine(settings.database.url)
|
|
18
|
+
|
|
19
|
+
# Create all tables
|
|
20
|
+
Base.metadata.create_all(engine)
|
|
21
|
+
|
|
22
|
+
logger.info("Trading tables created successfully")
|
|
23
|
+
return True
|
|
24
|
+
|
|
25
|
+
except Exception as e:
|
|
26
|
+
logger.error(f"Failed to create trading tables: {e}")
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def drop_trading_tables():
|
|
31
|
+
"""Drop trading-related tables from the database"""
|
|
32
|
+
try:
|
|
33
|
+
# Get database URL from settings
|
|
34
|
+
settings = get_settings()
|
|
35
|
+
engine = create_engine(settings.database.url)
|
|
36
|
+
|
|
37
|
+
# Drop all tables
|
|
38
|
+
Base.metadata.drop_all(engine)
|
|
39
|
+
|
|
40
|
+
logger.info("Trading tables dropped successfully")
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
except Exception as e:
|
|
44
|
+
logger.error(f"Failed to drop trading tables: {e}")
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def migrate_trading_data():
|
|
49
|
+
"""Migrate existing data to new trading schema"""
|
|
50
|
+
try:
|
|
51
|
+
# Get database URL from settings
|
|
52
|
+
settings = get_settings()
|
|
53
|
+
engine = create_engine(settings.database.url)
|
|
54
|
+
Session = sessionmaker(bind=engine)
|
|
55
|
+
session = Session()
|
|
56
|
+
|
|
57
|
+
# Check if we need to migrate existing portfolio data
|
|
58
|
+
# This would depend on your existing schema
|
|
59
|
+
|
|
60
|
+
session.close()
|
|
61
|
+
logger.info("Trading data migration completed")
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error(f"Failed to migrate trading data: {e}")
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def verify_trading_schema():
|
|
70
|
+
"""Verify that trading schema is properly set up"""
|
|
71
|
+
try:
|
|
72
|
+
# Get database URL from settings
|
|
73
|
+
settings = get_settings()
|
|
74
|
+
engine = create_engine(settings.database.url)
|
|
75
|
+
|
|
76
|
+
# Check if tables exist
|
|
77
|
+
with engine.connect() as conn:
|
|
78
|
+
# Check for trading_accounts table
|
|
79
|
+
if "sqlite" in settings.database.url:
|
|
80
|
+
# SQLite syntax
|
|
81
|
+
result = conn.execute(text("""
|
|
82
|
+
SELECT name FROM sqlite_master
|
|
83
|
+
WHERE type='table' AND name='trading_accounts';
|
|
84
|
+
"""))
|
|
85
|
+
accounts_exists = result.fetchone() is not None
|
|
86
|
+
|
|
87
|
+
result = conn.execute(text("""
|
|
88
|
+
SELECT name FROM sqlite_master
|
|
89
|
+
WHERE type='table' AND name='portfolios';
|
|
90
|
+
"""))
|
|
91
|
+
portfolios_exists = result.fetchone() is not None
|
|
92
|
+
|
|
93
|
+
result = conn.execute(text("""
|
|
94
|
+
SELECT name FROM sqlite_master
|
|
95
|
+
WHERE type='table' AND name='positions';
|
|
96
|
+
"""))
|
|
97
|
+
positions_exists = result.fetchone() is not None
|
|
98
|
+
|
|
99
|
+
result = conn.execute(text("""
|
|
100
|
+
SELECT name FROM sqlite_master
|
|
101
|
+
WHERE type='table' AND name='trading_orders';
|
|
102
|
+
"""))
|
|
103
|
+
orders_exists = result.fetchone() is not None
|
|
104
|
+
else:
|
|
105
|
+
# PostgreSQL syntax
|
|
106
|
+
result = conn.execute(text("""
|
|
107
|
+
SELECT EXISTS (
|
|
108
|
+
SELECT FROM information_schema.tables
|
|
109
|
+
WHERE table_name = 'trading_accounts'
|
|
110
|
+
);
|
|
111
|
+
"""))
|
|
112
|
+
accounts_exists = result.scalar()
|
|
113
|
+
|
|
114
|
+
result = conn.execute(text("""
|
|
115
|
+
SELECT EXISTS (
|
|
116
|
+
SELECT FROM information_schema.tables
|
|
117
|
+
WHERE table_name = 'portfolios'
|
|
118
|
+
);
|
|
119
|
+
"""))
|
|
120
|
+
portfolios_exists = result.scalar()
|
|
121
|
+
|
|
122
|
+
result = conn.execute(text("""
|
|
123
|
+
SELECT EXISTS (
|
|
124
|
+
SELECT FROM information_schema.tables
|
|
125
|
+
WHERE table_name = 'positions'
|
|
126
|
+
);
|
|
127
|
+
"""))
|
|
128
|
+
positions_exists = result.scalar()
|
|
129
|
+
|
|
130
|
+
result = conn.execute(text("""
|
|
131
|
+
SELECT EXISTS (
|
|
132
|
+
SELECT FROM information_schema.tables
|
|
133
|
+
WHERE table_name = 'trading_orders'
|
|
134
|
+
);
|
|
135
|
+
"""))
|
|
136
|
+
orders_exists = result.scalar()
|
|
137
|
+
|
|
138
|
+
all_tables_exist = all([accounts_exists, portfolios_exists, positions_exists, orders_exists])
|
|
139
|
+
|
|
140
|
+
if all_tables_exist:
|
|
141
|
+
logger.info("Trading schema verification successful")
|
|
142
|
+
else:
|
|
143
|
+
logger.warning("Some trading tables are missing")
|
|
144
|
+
|
|
145
|
+
return all_tables_exist
|
|
146
|
+
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.error(f"Failed to verify trading schema: {e}")
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
# Run migrations
|
|
154
|
+
print("Creating trading tables...")
|
|
155
|
+
if create_trading_tables():
|
|
156
|
+
print("✅ Trading tables created successfully")
|
|
157
|
+
else:
|
|
158
|
+
print("❌ Failed to create trading tables")
|
|
159
|
+
|
|
160
|
+
print("Verifying schema...")
|
|
161
|
+
if verify_trading_schema():
|
|
162
|
+
print("✅ Trading schema verified successfully")
|
|
163
|
+
else:
|
|
164
|
+
print("❌ Trading schema verification failed")
|