mcli-framework 7.2.0__py3-none-any.whl → 7.3.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/__init__.py +160 -0
- mcli/__main__.py +14 -0
- mcli/app/__init__.py +23 -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 +23 -6
- mcli/ml/dashboard/components/__init__.py +7 -0
- mcli/ml/dashboard/pages/__init__.py +6 -0
- mcli/ml/dashboard/pages/predictions_enhanced.py +20 -6
- mcli/ml/dashboard/pages/test_portfolio.py +373 -0
- mcli/ml/dashboard/pages/trading.py +714 -0
- mcli/ml/dashboard/utils.py +154 -0
- mcli/ml/data_ingestion/__init__.py +39 -0
- mcli/ml/database/__init__.py +47 -0
- 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/preprocessing/__init__.py +28 -0
- mcli/ml/scripts/__init__.py +1 -0
- mcli/ml/trading/__init__.py +60 -0
- mcli/ml/trading/alpaca_client.py +353 -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 +260 -0
- 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.3.1.dist-info}/METADATA +3 -1
- {mcli_framework-7.2.0.dist-info → mcli_framework-7.3.1.dist-info}/RECORD +85 -13
- {mcli_framework-7.2.0.dist-info → mcli_framework-7.3.1.dist-info}/WHEEL +0 -0
- {mcli_framework-7.2.0.dist-info → mcli_framework-7.3.1.dist-info}/entry_points.txt +0 -0
- {mcli_framework-7.2.0.dist-info → mcli_framework-7.3.1.dist-info}/licenses/LICENSE +0 -0
- {mcli_framework-7.2.0.dist-info → mcli_framework-7.3.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
"""Trading service for managing portfolios and executing trades"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from typing import Dict, List, Optional, Tuple, Union
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from sqlalchemy.orm import Session
|
|
11
|
+
from sqlalchemy import and_, desc, func
|
|
12
|
+
|
|
13
|
+
from mcli.ml.trading.alpaca_client import AlpacaTradingClient, create_trading_client
|
|
14
|
+
from mcli.ml.trading.models import (
|
|
15
|
+
TradingAccount, Portfolio, Position, TradingOrder, PortfolioPerformanceSnapshot,
|
|
16
|
+
TradingSignal, OrderStatus, OrderType, OrderSide, PositionSide, PortfolioType,
|
|
17
|
+
TradingAccountCreate, PortfolioCreate, OrderCreate, PortfolioResponse,
|
|
18
|
+
PositionResponse, OrderResponse, TradingSignalResponse
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TradingService:
|
|
25
|
+
"""Service for managing trading operations"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, db_session: Session):
|
|
28
|
+
self.db = db_session
|
|
29
|
+
|
|
30
|
+
def create_trading_account(self, user_id: UUID, account_data: TradingAccountCreate) -> TradingAccount:
|
|
31
|
+
"""Create a new trading account"""
|
|
32
|
+
try:
|
|
33
|
+
account = TradingAccount(
|
|
34
|
+
user_id=user_id,
|
|
35
|
+
account_name=account_data.account_name,
|
|
36
|
+
account_type=account_data.account_type,
|
|
37
|
+
alpaca_api_key=account_data.alpaca_api_key,
|
|
38
|
+
alpaca_secret_key=account_data.alpaca_secret_key,
|
|
39
|
+
paper_trading=account_data.paper_trading,
|
|
40
|
+
risk_level=account_data.risk_level,
|
|
41
|
+
max_position_size=account_data.max_position_size,
|
|
42
|
+
max_portfolio_risk=account_data.max_portfolio_risk,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
self.db.add(account)
|
|
46
|
+
self.db.commit()
|
|
47
|
+
self.db.refresh(account)
|
|
48
|
+
|
|
49
|
+
logger.info(f"Created trading account {account.id} for user {user_id}")
|
|
50
|
+
return account
|
|
51
|
+
|
|
52
|
+
except Exception as e:
|
|
53
|
+
self.db.rollback()
|
|
54
|
+
logger.error(f"Failed to create trading account: {e}")
|
|
55
|
+
raise
|
|
56
|
+
|
|
57
|
+
def get_trading_account(self, account_id: UUID) -> Optional[TradingAccount]:
|
|
58
|
+
"""Get trading account by ID"""
|
|
59
|
+
return self.db.query(TradingAccount).filter(
|
|
60
|
+
TradingAccount.id == account_id,
|
|
61
|
+
TradingAccount.is_active == True
|
|
62
|
+
).first()
|
|
63
|
+
|
|
64
|
+
def create_portfolio(self, account_id: UUID, portfolio_data: PortfolioCreate) -> Portfolio:
|
|
65
|
+
"""Create a new portfolio"""
|
|
66
|
+
try:
|
|
67
|
+
portfolio = Portfolio(
|
|
68
|
+
trading_account_id=account_id,
|
|
69
|
+
name=portfolio_data.name,
|
|
70
|
+
description=portfolio_data.description,
|
|
71
|
+
initial_capital=Decimal(str(portfolio_data.initial_capital)),
|
|
72
|
+
current_value=Decimal(str(portfolio_data.initial_capital)),
|
|
73
|
+
cash_balance=Decimal(str(portfolio_data.initial_capital)),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
self.db.add(portfolio)
|
|
77
|
+
self.db.commit()
|
|
78
|
+
self.db.refresh(portfolio)
|
|
79
|
+
|
|
80
|
+
logger.info(f"Created portfolio {portfolio.id} for account {account_id}")
|
|
81
|
+
return portfolio
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
self.db.rollback()
|
|
85
|
+
logger.error(f"Failed to create portfolio: {e}")
|
|
86
|
+
raise
|
|
87
|
+
|
|
88
|
+
def get_portfolio(self, portfolio_id: UUID) -> Optional[Portfolio]:
|
|
89
|
+
"""Get portfolio by ID"""
|
|
90
|
+
return self.db.query(Portfolio).filter(
|
|
91
|
+
Portfolio.id == portfolio_id,
|
|
92
|
+
Portfolio.is_active == True
|
|
93
|
+
).first()
|
|
94
|
+
|
|
95
|
+
def get_user_portfolios(self, user_id: UUID) -> List[Portfolio]:
|
|
96
|
+
"""Get all portfolios for a user"""
|
|
97
|
+
return self.db.query(Portfolio).join(TradingAccount).filter(
|
|
98
|
+
TradingAccount.user_id == user_id,
|
|
99
|
+
Portfolio.is_active == True
|
|
100
|
+
).all()
|
|
101
|
+
|
|
102
|
+
def create_alpaca_client(self, account: TradingAccount) -> AlpacaTradingClient:
|
|
103
|
+
"""Create Alpaca client for trading account"""
|
|
104
|
+
if not account.alpaca_api_key or not account.alpaca_secret_key:
|
|
105
|
+
raise ValueError("Alpaca credentials not configured for this account")
|
|
106
|
+
|
|
107
|
+
return create_trading_client(
|
|
108
|
+
api_key=account.alpaca_api_key,
|
|
109
|
+
secret_key=account.alpaca_secret_key,
|
|
110
|
+
paper_trading=account.paper_trading
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def sync_portfolio_with_alpaca(self, portfolio: Portfolio) -> bool:
|
|
114
|
+
"""Sync portfolio data with Alpaca"""
|
|
115
|
+
try:
|
|
116
|
+
account = self.get_trading_account(portfolio.trading_account_id)
|
|
117
|
+
if not account:
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
alpaca_client = self.create_alpaca_client(account)
|
|
121
|
+
alpaca_portfolio = alpaca_client.get_portfolio()
|
|
122
|
+
|
|
123
|
+
# Update portfolio values
|
|
124
|
+
portfolio.current_value = Decimal(str(alpaca_portfolio.portfolio_value))
|
|
125
|
+
portfolio.cash_balance = Decimal(str(alpaca_portfolio.cash))
|
|
126
|
+
portfolio.unrealized_pl = Decimal(str(alpaca_portfolio.unrealized_pl))
|
|
127
|
+
portfolio.realized_pl = Decimal(str(alpaca_portfolio.realized_pl))
|
|
128
|
+
|
|
129
|
+
# Calculate returns
|
|
130
|
+
if portfolio.initial_capital > 0:
|
|
131
|
+
total_return = portfolio.current_value - portfolio.initial_capital
|
|
132
|
+
portfolio.total_return = float(total_return)
|
|
133
|
+
portfolio.total_return_pct = float(total_return / portfolio.initial_capital * 100)
|
|
134
|
+
|
|
135
|
+
# Update positions
|
|
136
|
+
self._sync_positions(portfolio, alpaca_portfolio.positions)
|
|
137
|
+
|
|
138
|
+
# Create performance snapshot
|
|
139
|
+
self._create_performance_snapshot(portfolio)
|
|
140
|
+
|
|
141
|
+
self.db.commit()
|
|
142
|
+
logger.info(f"Synced portfolio {portfolio.id} with Alpaca")
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
except Exception as e:
|
|
146
|
+
self.db.rollback()
|
|
147
|
+
logger.error(f"Failed to sync portfolio with Alpaca: {e}")
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
def _sync_positions(self, portfolio: Portfolio, alpaca_positions: List):
|
|
151
|
+
"""Sync positions with Alpaca data"""
|
|
152
|
+
# Clear existing positions
|
|
153
|
+
self.db.query(Position).filter(Position.portfolio_id == portfolio.id).delete()
|
|
154
|
+
|
|
155
|
+
# Add new positions
|
|
156
|
+
for alpaca_pos in alpaca_positions:
|
|
157
|
+
position = Position(
|
|
158
|
+
portfolio_id=portfolio.id,
|
|
159
|
+
symbol=alpaca_pos.symbol,
|
|
160
|
+
quantity=alpaca_pos.quantity,
|
|
161
|
+
side=PositionSide.LONG if alpaca_pos.quantity > 0 else PositionSide.SHORT,
|
|
162
|
+
average_price=Decimal(str(alpaca_pos.cost_basis / alpaca_pos.quantity)),
|
|
163
|
+
current_price=Decimal(str(alpaca_pos.current_price)),
|
|
164
|
+
market_value=Decimal(str(alpaca_pos.market_value)),
|
|
165
|
+
cost_basis=Decimal(str(alpaca_pos.cost_basis)),
|
|
166
|
+
unrealized_pnl=Decimal(str(alpaca_pos.unrealized_pl)),
|
|
167
|
+
unrealized_pnl_pct=float(alpaca_pos.unrealized_plpc),
|
|
168
|
+
position_size_pct=float(alpaca_pos.market_value / portfolio.current_value * 100),
|
|
169
|
+
weight=float(alpaca_pos.market_value / portfolio.current_value),
|
|
170
|
+
)
|
|
171
|
+
self.db.add(position)
|
|
172
|
+
|
|
173
|
+
def _create_performance_snapshot(self, portfolio: Portfolio):
|
|
174
|
+
"""Create daily performance snapshot"""
|
|
175
|
+
snapshot = PortfolioPerformanceSnapshot(
|
|
176
|
+
portfolio_id=portfolio.id,
|
|
177
|
+
snapshot_date=datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0),
|
|
178
|
+
portfolio_value=portfolio.current_value,
|
|
179
|
+
cash_balance=portfolio.cash_balance,
|
|
180
|
+
daily_return=portfolio.daily_return,
|
|
181
|
+
daily_return_pct=portfolio.daily_return_pct,
|
|
182
|
+
total_return=Decimal(str(portfolio.total_return)),
|
|
183
|
+
total_return_pct=portfolio.total_return_pct,
|
|
184
|
+
volatility=portfolio.volatility,
|
|
185
|
+
sharpe_ratio=portfolio.sharpe_ratio,
|
|
186
|
+
max_drawdown=portfolio.max_drawdown,
|
|
187
|
+
positions_data=self._get_positions_data(portfolio.id),
|
|
188
|
+
)
|
|
189
|
+
self.db.add(snapshot)
|
|
190
|
+
|
|
191
|
+
def _get_positions_data(self, portfolio_id: UUID) -> Dict:
|
|
192
|
+
"""Get positions data for snapshot"""
|
|
193
|
+
positions = self.db.query(Position).filter(Position.portfolio_id == portfolio_id).all()
|
|
194
|
+
return {
|
|
195
|
+
pos.symbol: {
|
|
196
|
+
"quantity": pos.quantity,
|
|
197
|
+
"side": pos.side.value,
|
|
198
|
+
"average_price": float(pos.average_price),
|
|
199
|
+
"current_price": float(pos.current_price),
|
|
200
|
+
"market_value": float(pos.market_value),
|
|
201
|
+
"unrealized_pnl": float(pos.unrealized_pnl),
|
|
202
|
+
"weight": pos.weight,
|
|
203
|
+
}
|
|
204
|
+
for pos in positions
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
def place_order(self, portfolio_id: UUID, order_data: OrderCreate, check_risk: bool = True) -> TradingOrder:
|
|
208
|
+
"""Place a trading order"""
|
|
209
|
+
try:
|
|
210
|
+
portfolio = self.get_portfolio(portfolio_id)
|
|
211
|
+
if not portfolio:
|
|
212
|
+
raise ValueError("Portfolio not found")
|
|
213
|
+
|
|
214
|
+
account = self.get_trading_account(portfolio.trading_account_id)
|
|
215
|
+
if not account:
|
|
216
|
+
raise ValueError("Trading account not found")
|
|
217
|
+
|
|
218
|
+
# Check risk limits if enabled
|
|
219
|
+
if check_risk:
|
|
220
|
+
from mcli.ml.trading.risk_management import RiskManager
|
|
221
|
+
risk_manager = RiskManager(self)
|
|
222
|
+
|
|
223
|
+
order_dict = {
|
|
224
|
+
"symbol": order_data.symbol,
|
|
225
|
+
"quantity": order_data.quantity,
|
|
226
|
+
"side": order_data.side.value,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
risk_ok, warnings = risk_manager.check_risk_limits(portfolio_id, order_dict)
|
|
230
|
+
if not risk_ok:
|
|
231
|
+
raise ValueError(f"Order violates risk limits: {'; '.join(warnings)}")
|
|
232
|
+
|
|
233
|
+
# Create order record
|
|
234
|
+
order = TradingOrder(
|
|
235
|
+
trading_account_id=account.id,
|
|
236
|
+
portfolio_id=portfolio.id,
|
|
237
|
+
symbol=order_data.symbol,
|
|
238
|
+
side=order_data.side,
|
|
239
|
+
order_type=order_data.order_type,
|
|
240
|
+
quantity=order_data.quantity,
|
|
241
|
+
limit_price=Decimal(str(order_data.limit_price)) if order_data.limit_price else None,
|
|
242
|
+
stop_price=Decimal(str(order_data.stop_price)) if order_data.stop_price else None,
|
|
243
|
+
remaining_quantity=order_data.quantity,
|
|
244
|
+
time_in_force=order_data.time_in_force,
|
|
245
|
+
extended_hours=order_data.extended_hours,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
self.db.add(order)
|
|
249
|
+
self.db.flush() # Get the ID
|
|
250
|
+
|
|
251
|
+
# Place order with Alpaca if account has credentials
|
|
252
|
+
if account.alpaca_api_key and account.alpaca_secret_key:
|
|
253
|
+
alpaca_client = self.create_alpaca_client(account)
|
|
254
|
+
|
|
255
|
+
if order_data.order_type == OrderType.MARKET:
|
|
256
|
+
alpaca_order = alpaca_client.place_market_order(
|
|
257
|
+
symbol=order_data.symbol,
|
|
258
|
+
quantity=order_data.quantity,
|
|
259
|
+
side=order_data.side.value,
|
|
260
|
+
time_in_force=order_data.time_in_force
|
|
261
|
+
)
|
|
262
|
+
elif order_data.order_type == OrderType.LIMIT:
|
|
263
|
+
alpaca_order = alpaca_client.place_limit_order(
|
|
264
|
+
symbol=order_data.symbol,
|
|
265
|
+
quantity=order_data.quantity,
|
|
266
|
+
side=order_data.side.value,
|
|
267
|
+
limit_price=order_data.limit_price,
|
|
268
|
+
time_in_force=order_data.time_in_force
|
|
269
|
+
)
|
|
270
|
+
else:
|
|
271
|
+
raise ValueError(f"Unsupported order type: {order_data.order_type}")
|
|
272
|
+
|
|
273
|
+
order.alpaca_order_id = alpaca_order.id
|
|
274
|
+
order.status = OrderStatus.SUBMITTED
|
|
275
|
+
order.submitted_at = datetime.utcnow()
|
|
276
|
+
|
|
277
|
+
self.db.commit()
|
|
278
|
+
self.db.refresh(order)
|
|
279
|
+
|
|
280
|
+
logger.info(f"Placed order {order.id} for portfolio {portfolio_id}")
|
|
281
|
+
return order
|
|
282
|
+
|
|
283
|
+
except Exception as e:
|
|
284
|
+
self.db.rollback()
|
|
285
|
+
logger.error(f"Failed to place order: {e}")
|
|
286
|
+
raise
|
|
287
|
+
|
|
288
|
+
def get_portfolio_positions(self, portfolio_id: UUID) -> List[PositionResponse]:
|
|
289
|
+
"""Get all positions for a portfolio"""
|
|
290
|
+
positions = self.db.query(Position).filter(Position.portfolio_id == portfolio_id).all()
|
|
291
|
+
return [
|
|
292
|
+
PositionResponse(
|
|
293
|
+
id=pos.id,
|
|
294
|
+
symbol=pos.symbol,
|
|
295
|
+
quantity=pos.quantity,
|
|
296
|
+
side=pos.side,
|
|
297
|
+
average_price=float(pos.average_price),
|
|
298
|
+
current_price=float(pos.current_price),
|
|
299
|
+
market_value=float(pos.market_value),
|
|
300
|
+
cost_basis=float(pos.cost_basis),
|
|
301
|
+
unrealized_pnl=float(pos.unrealized_pnl),
|
|
302
|
+
unrealized_pnl_pct=pos.unrealized_pnl_pct,
|
|
303
|
+
realized_pnl=float(pos.realized_pnl),
|
|
304
|
+
position_size_pct=pos.position_size_pct,
|
|
305
|
+
weight=pos.weight,
|
|
306
|
+
created_at=pos.created_at,
|
|
307
|
+
updated_at=pos.updated_at,
|
|
308
|
+
)
|
|
309
|
+
for pos in positions
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
def get_portfolio_orders(self, portfolio_id: UUID, status: Optional[OrderStatus] = None) -> List[OrderResponse]:
|
|
313
|
+
"""Get orders for a portfolio"""
|
|
314
|
+
query = self.db.query(TradingOrder).filter(TradingOrder.portfolio_id == portfolio_id)
|
|
315
|
+
if status:
|
|
316
|
+
query = query.filter(TradingOrder.status == status)
|
|
317
|
+
|
|
318
|
+
orders = query.order_by(desc(TradingOrder.created_at)).all()
|
|
319
|
+
return [
|
|
320
|
+
OrderResponse(
|
|
321
|
+
id=order.id,
|
|
322
|
+
symbol=order.symbol,
|
|
323
|
+
side=order.side,
|
|
324
|
+
order_type=order.order_type,
|
|
325
|
+
quantity=order.quantity,
|
|
326
|
+
limit_price=float(order.limit_price) if order.limit_price else None,
|
|
327
|
+
stop_price=float(order.stop_price) if order.stop_price else None,
|
|
328
|
+
average_fill_price=float(order.average_fill_price) if order.average_fill_price else None,
|
|
329
|
+
status=order.status,
|
|
330
|
+
filled_quantity=order.filled_quantity,
|
|
331
|
+
remaining_quantity=order.remaining_quantity,
|
|
332
|
+
created_at=order.created_at,
|
|
333
|
+
submitted_at=order.submitted_at,
|
|
334
|
+
filled_at=order.filled_at,
|
|
335
|
+
cancelled_at=order.cancelled_at,
|
|
336
|
+
time_in_force=order.time_in_force,
|
|
337
|
+
extended_hours=order.extended_hours,
|
|
338
|
+
alpaca_order_id=order.alpaca_order_id,
|
|
339
|
+
)
|
|
340
|
+
for order in orders
|
|
341
|
+
]
|
|
342
|
+
|
|
343
|
+
def get_portfolio_performance(self, portfolio_id: UUID, days: int = 30) -> pd.DataFrame:
|
|
344
|
+
"""Get portfolio performance history"""
|
|
345
|
+
end_date = datetime.utcnow()
|
|
346
|
+
start_date = end_date - timedelta(days=days)
|
|
347
|
+
|
|
348
|
+
snapshots = self.db.query(PortfolioPerformanceSnapshot).filter(
|
|
349
|
+
PortfolioPerformanceSnapshot.portfolio_id == portfolio_id,
|
|
350
|
+
PortfolioPerformanceSnapshot.snapshot_date >= start_date
|
|
351
|
+
).order_by(PortfolioPerformanceSnapshot.snapshot_date).all()
|
|
352
|
+
|
|
353
|
+
data = []
|
|
354
|
+
for snapshot in snapshots:
|
|
355
|
+
data.append({
|
|
356
|
+
"date": snapshot.snapshot_date,
|
|
357
|
+
"portfolio_value": float(snapshot.portfolio_value),
|
|
358
|
+
"cash_balance": float(snapshot.cash_balance),
|
|
359
|
+
"daily_return": float(snapshot.daily_return),
|
|
360
|
+
"daily_return_pct": snapshot.daily_return_pct,
|
|
361
|
+
"total_return": float(snapshot.total_return),
|
|
362
|
+
"total_return_pct": snapshot.total_return_pct,
|
|
363
|
+
"volatility": snapshot.volatility,
|
|
364
|
+
"sharpe_ratio": snapshot.sharpe_ratio,
|
|
365
|
+
"max_drawdown": snapshot.max_drawdown,
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
return pd.DataFrame(data)
|
|
369
|
+
|
|
370
|
+
def create_trading_signal(
|
|
371
|
+
self,
|
|
372
|
+
portfolio_id: UUID,
|
|
373
|
+
symbol: str,
|
|
374
|
+
signal_type: str,
|
|
375
|
+
confidence: float,
|
|
376
|
+
strength: float,
|
|
377
|
+
model_id: Optional[str] = None,
|
|
378
|
+
target_price: Optional[float] = None,
|
|
379
|
+
stop_loss: Optional[float] = None,
|
|
380
|
+
take_profit: Optional[float] = None,
|
|
381
|
+
position_size: Optional[float] = None,
|
|
382
|
+
expires_hours: int = 24
|
|
383
|
+
) -> TradingSignal:
|
|
384
|
+
"""Create a trading signal"""
|
|
385
|
+
try:
|
|
386
|
+
expires_at = datetime.utcnow() + timedelta(hours=expires_hours) if expires_hours > 0 else None
|
|
387
|
+
|
|
388
|
+
signal = TradingSignal(
|
|
389
|
+
portfolio_id=portfolio_id,
|
|
390
|
+
symbol=symbol,
|
|
391
|
+
signal_type=signal_type,
|
|
392
|
+
confidence=confidence,
|
|
393
|
+
strength=strength,
|
|
394
|
+
model_id=model_id,
|
|
395
|
+
target_price=Decimal(str(target_price)) if target_price else None,
|
|
396
|
+
stop_loss=Decimal(str(stop_loss)) if stop_loss else None,
|
|
397
|
+
take_profit=Decimal(str(take_profit)) if take_profit else None,
|
|
398
|
+
position_size=position_size,
|
|
399
|
+
expires_at=expires_at,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
self.db.add(signal)
|
|
403
|
+
self.db.commit()
|
|
404
|
+
self.db.refresh(signal)
|
|
405
|
+
|
|
406
|
+
logger.info(f"Created trading signal {signal.id} for {symbol}")
|
|
407
|
+
return signal
|
|
408
|
+
|
|
409
|
+
except Exception as e:
|
|
410
|
+
self.db.rollback()
|
|
411
|
+
logger.error(f"Failed to create trading signal: {e}")
|
|
412
|
+
raise
|
|
413
|
+
|
|
414
|
+
def get_active_signals(self, portfolio_id: UUID) -> List[TradingSignalResponse]:
|
|
415
|
+
"""Get active trading signals for a portfolio"""
|
|
416
|
+
signals = self.db.query(TradingSignal).filter(
|
|
417
|
+
TradingSignal.portfolio_id == portfolio_id,
|
|
418
|
+
TradingSignal.is_active == True,
|
|
419
|
+
TradingSignal.expires_at > datetime.utcnow()
|
|
420
|
+
).order_by(desc(TradingSignal.created_at)).all()
|
|
421
|
+
|
|
422
|
+
return [
|
|
423
|
+
TradingSignalResponse(
|
|
424
|
+
id=signal.id,
|
|
425
|
+
symbol=signal.symbol,
|
|
426
|
+
signal_type=signal.signal_type,
|
|
427
|
+
confidence=signal.confidence,
|
|
428
|
+
strength=signal.strength,
|
|
429
|
+
model_id=signal.model_id,
|
|
430
|
+
model_version=signal.model_version,
|
|
431
|
+
target_price=float(signal.target_price) if signal.target_price else None,
|
|
432
|
+
stop_loss=float(signal.stop_loss) if signal.stop_loss else None,
|
|
433
|
+
take_profit=float(signal.take_profit) if signal.take_profit else None,
|
|
434
|
+
position_size=signal.position_size,
|
|
435
|
+
created_at=signal.created_at,
|
|
436
|
+
expires_at=signal.expires_at,
|
|
437
|
+
is_active=signal.is_active,
|
|
438
|
+
)
|
|
439
|
+
for signal in signals
|
|
440
|
+
]
|
|
441
|
+
|
|
442
|
+
def calculate_portfolio_metrics(self, portfolio_id: UUID) -> Dict:
|
|
443
|
+
"""Calculate portfolio performance metrics"""
|
|
444
|
+
portfolio = self.get_portfolio(portfolio_id)
|
|
445
|
+
if not portfolio:
|
|
446
|
+
return {}
|
|
447
|
+
|
|
448
|
+
# Get performance history
|
|
449
|
+
performance_df = self.get_portfolio_performance(portfolio_id, days=90)
|
|
450
|
+
|
|
451
|
+
if performance_df.empty:
|
|
452
|
+
return {
|
|
453
|
+
"total_return": 0.0,
|
|
454
|
+
"total_return_pct": 0.0,
|
|
455
|
+
"volatility": 0.0,
|
|
456
|
+
"sharpe_ratio": 0.0,
|
|
457
|
+
"max_drawdown": 0.0,
|
|
458
|
+
"current_value": float(portfolio.current_value),
|
|
459
|
+
"cash_balance": float(portfolio.cash_balance),
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
# Calculate metrics
|
|
463
|
+
returns = performance_df["daily_return_pct"].dropna()
|
|
464
|
+
|
|
465
|
+
total_return = performance_df["total_return"].iloc[-1] if not performance_df.empty else 0
|
|
466
|
+
total_return_pct = performance_df["total_return_pct"].iloc[-1] if not performance_df.empty else 0
|
|
467
|
+
volatility = returns.std() * (252 ** 0.5) if len(returns) > 1 else 0 # Annualized
|
|
468
|
+
sharpe_ratio = (returns.mean() * 252) / volatility if volatility > 0 else 0 # Annualized
|
|
469
|
+
max_drawdown = performance_df["max_drawdown"].max() if not performance_df.empty else 0
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
"total_return": total_return,
|
|
473
|
+
"total_return_pct": total_return_pct,
|
|
474
|
+
"volatility": volatility,
|
|
475
|
+
"sharpe_ratio": sharpe_ratio,
|
|
476
|
+
"max_drawdown": max_drawdown,
|
|
477
|
+
"current_value": float(portfolio.current_value),
|
|
478
|
+
"cash_balance": float(portfolio.cash_balance),
|
|
479
|
+
"num_positions": len(self.get_portfolio_positions(portfolio_id)),
|
|
480
|
+
}
|
mcli/mygroup/__init__.py
ADDED
mcli/public/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# logger.info("I am in mcli.public.__init__.py")
|
mcli/self/__init__.py
ADDED