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

@@ -0,0 +1,297 @@
1
+ """
2
+ REPL (Read-Eval-Print Loop) for LSH secrets management.
3
+ """
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import List
8
+
9
+ import click
10
+ from prompt_toolkit import prompt
11
+ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
12
+ from prompt_toolkit.completion import WordCompleter
13
+ from prompt_toolkit.history import FileHistory
14
+
15
+ from mcli.lib.logger.logger import get_logger
16
+
17
+ logger = get_logger(__name__)
18
+ from mcli.lib.ui.styling import console, error, info, success, warning
19
+
20
+ from .manager import SecretsManager
21
+ from .store import SecretsStore
22
+
23
+
24
+ class SecretsREPL:
25
+ """Interactive REPL for secrets management."""
26
+
27
+ def __init__(self):
28
+ """Initialize the REPL."""
29
+ self.manager = SecretsManager()
30
+ self.store = SecretsStore()
31
+ self.running = False
32
+ self.namespace = "default"
33
+ self.history_file = Path.home() / ".mcli" / "secrets_repl_history"
34
+
35
+ # Commands
36
+ self.commands = {
37
+ "set": self.cmd_set,
38
+ "get": self.cmd_get,
39
+ "list": self.cmd_list,
40
+ "delete": self.cmd_delete,
41
+ "namespace": self.cmd_namespace,
42
+ "export": self.cmd_export,
43
+ "import": self.cmd_import,
44
+ "push": self.cmd_push,
45
+ "pull": self.cmd_pull,
46
+ "sync": self.cmd_sync,
47
+ "status": self.cmd_status,
48
+ "help": self.cmd_help,
49
+ "exit": self.cmd_exit,
50
+ "quit": self.cmd_exit,
51
+ }
52
+
53
+ # Command completer
54
+ self.completer = WordCompleter(list(self.commands.keys()) + ["ns"], ignore_case=True)
55
+
56
+ def run(self):
57
+ """Run the REPL."""
58
+ self.running = True
59
+
60
+ # Print welcome message
61
+ console.print("[bold cyan]MCLI Secrets Management Shell[/bold cyan]")
62
+ console.print("Type 'help' for available commands or 'exit' to quit.\n")
63
+
64
+ # Create history file directory if needed
65
+ self.history_file.parent.mkdir(parents=True, exist_ok=True)
66
+
67
+ while self.running:
68
+ try:
69
+ # Build prompt
70
+ prompt_text = f"[{self.namespace}]> "
71
+
72
+ # Get user input
73
+ user_input = prompt(
74
+ prompt_text,
75
+ completer=self.completer,
76
+ history=FileHistory(str(self.history_file)),
77
+ auto_suggest=AutoSuggestFromHistory(),
78
+ ).strip()
79
+
80
+ if not user_input:
81
+ continue
82
+
83
+ # Parse command and arguments
84
+ parts = user_input.split()
85
+ command = parts[0].lower()
86
+ args = parts[1:] if len(parts) > 1 else []
87
+
88
+ # Handle command aliases
89
+ if command == "ns":
90
+ command = "namespace"
91
+
92
+ # Execute command
93
+ if command in self.commands:
94
+ self.commands[command](args)
95
+ else:
96
+ error(f"Unknown command: {command}")
97
+ console.print("Type 'help' for available commands.")
98
+
99
+ except KeyboardInterrupt:
100
+ console.print("\nUse 'exit' or 'quit' to leave the shell.")
101
+ except EOFError:
102
+ self.cmd_exit([])
103
+ except Exception as e:
104
+ error(f"Error: {e}")
105
+ logger.exception("REPL error")
106
+
107
+ def cmd_set(self, args: List[str]):
108
+ """Set a secret value."""
109
+ if len(args) < 2:
110
+ error("Usage: set <key> <value>")
111
+ return
112
+
113
+ key = args[0]
114
+ value = " ".join(args[1:])
115
+
116
+ try:
117
+ self.manager.set(key, value, self.namespace)
118
+ success(f"Secret '{key}' set in namespace '{self.namespace}'")
119
+ except Exception as e:
120
+ error(f"Failed to set secret: {e}")
121
+
122
+ def cmd_get(self, args: List[str]):
123
+ """Get a secret value."""
124
+ if len(args) != 1:
125
+ error("Usage: get <key>")
126
+ return
127
+
128
+ key = args[0]
129
+ value = self.manager.get(key, self.namespace)
130
+
131
+ if value is not None:
132
+ # Mask the value for security
133
+ masked_value = (
134
+ value[:3] + "*" * (len(value) - 6) + value[-3:]
135
+ if len(value) > 6
136
+ else "*" * len(value)
137
+ )
138
+ info(f"{key} = {masked_value}")
139
+
140
+ if click.confirm("Show full value?", default=False):
141
+ console.print(f"[yellow]{value}[/yellow]")
142
+ else:
143
+ warning(f"Secret '{key}' not found in namespace '{self.namespace}'")
144
+
145
+ def cmd_list(self, args: List[str]):
146
+ """List all secrets."""
147
+ secrets = self.manager.list(self.namespace if args != ["all"] else None)
148
+
149
+ if secrets:
150
+ console.print("[bold]Secrets:[/bold]")
151
+ for secret in secrets:
152
+ console.print(f" • {secret}")
153
+ else:
154
+ info("No secrets found")
155
+
156
+ def cmd_delete(self, args: List[str]):
157
+ """Delete a secret."""
158
+ if len(args) != 1:
159
+ error("Usage: delete <key>")
160
+ return
161
+
162
+ key = args[0]
163
+
164
+ if click.confirm(f"Delete secret '{key}' from namespace '{self.namespace}'?"):
165
+ if self.manager.delete(key, self.namespace):
166
+ success(f"Secret '{key}' deleted")
167
+ else:
168
+ warning(f"Secret '{key}' not found")
169
+
170
+ def cmd_namespace(self, args: List[str]):
171
+ """Switch namespace."""
172
+ if len(args) == 0:
173
+ # List namespaces
174
+ namespaces = set()
175
+ for d in self.manager.secrets_dir.iterdir():
176
+ if d.is_dir() and not d.name.startswith("."):
177
+ namespaces.add(d.name)
178
+
179
+ console.print(f"[bold]Current namespace:[/bold] {self.namespace}")
180
+ if namespaces:
181
+ console.print("[bold]Available namespaces:[/bold]")
182
+ for ns in sorted(namespaces):
183
+ marker = "→" if ns == self.namespace else " "
184
+ console.print(f" {marker} {ns}")
185
+ elif len(args) == 1:
186
+ self.namespace = args[0]
187
+ success(f"Switched to namespace '{self.namespace}'")
188
+ else:
189
+ error("Usage: namespace [<name>]")
190
+
191
+ def cmd_export(self, args: List[str]):
192
+ """Export secrets as environment variables."""
193
+ env_vars = self.manager.export_env(self.namespace)
194
+
195
+ if env_vars:
196
+ if args and args[0] == "file":
197
+ # Export to file
198
+ filename = args[1] if len(args) > 1 else f"{self.namespace}.env"
199
+ with open(filename, "w") as f:
200
+ for key, value in env_vars.items():
201
+ f.write(f"{key}={value}\n")
202
+ success(f"Exported {len(env_vars)} secrets to {filename}")
203
+ else:
204
+ # Display export commands
205
+ console.print("[bold]Export commands:[/bold]")
206
+ for key, value in env_vars.items():
207
+ masked_value = value[:3] + "***" + value[-3:] if len(value) > 6 else "***"
208
+ console.print(f"export {key}={masked_value}")
209
+ else:
210
+ info("No secrets to export")
211
+
212
+ def cmd_import(self, args: List[str]):
213
+ """Import secrets from environment file."""
214
+ if len(args) != 1:
215
+ error("Usage: import <env-file>")
216
+ return
217
+
218
+ env_file = Path(args[0])
219
+ if not env_file.exists():
220
+ error(f"File not found: {env_file}")
221
+ return
222
+
223
+ count = self.manager.import_env(env_file, self.namespace)
224
+ success(f"Imported {count} secrets from {env_file}")
225
+
226
+ def cmd_push(self, args: List[str]):
227
+ """Push secrets to git store."""
228
+ message = " ".join(args) if args else None
229
+ self.store.push(self.manager.secrets_dir, message)
230
+
231
+ def cmd_pull(self, args: List[str]):
232
+ """Pull secrets from git store."""
233
+ self.store.pull(self.manager.secrets_dir)
234
+
235
+ def cmd_sync(self, args: List[str]):
236
+ """Sync secrets with git store."""
237
+ message = " ".join(args) if args else None
238
+ self.store.sync(self.manager.secrets_dir, message)
239
+
240
+ def cmd_status(self, args: List[str]):
241
+ """Show store status."""
242
+ status = self.store.status()
243
+
244
+ console.print("[bold]Secrets Store Status:[/bold]")
245
+ console.print(f" Initialized: {status['initialized']}")
246
+ console.print(f" Path: {status['store_path']}")
247
+
248
+ if status["initialized"]:
249
+ console.print(f" Branch: {status['branch']}")
250
+ console.print(f" Commit: {status['commit']}")
251
+ console.print(f" Clean: {status['clean']}")
252
+
253
+ if status["has_remote"]:
254
+ console.print(f" Remote: {status['remote_url']}")
255
+ else:
256
+ console.print(" Remote: [dim]Not configured[/dim]")
257
+
258
+ def cmd_help(self, args: List[str]):
259
+ """Show help information."""
260
+ console.print("[bold]Available Commands:[/bold]\n")
261
+
262
+ help_text = {
263
+ "set": "Set a secret value",
264
+ "get": "Get a secret value",
265
+ "list": "List all secrets (use 'list all' for all namespaces)",
266
+ "delete": "Delete a secret",
267
+ "namespace": "Switch namespace or list namespaces (alias: ns)",
268
+ "export": "Export secrets as environment variables",
269
+ "import": "Import secrets from .env file",
270
+ "push": "Push secrets to git store",
271
+ "pull": "Pull secrets from git store",
272
+ "sync": "Sync secrets with git store",
273
+ "status": "Show store status",
274
+ "help": "Show this help",
275
+ "exit": "Exit the shell (alias: quit)",
276
+ }
277
+
278
+ for cmd, desc in help_text.items():
279
+ console.print(f" [cyan]{cmd:12}[/cyan] {desc}")
280
+
281
+ console.print("\n[bold]Examples:[/bold]")
282
+ console.print(" set api-key sk-1234567890")
283
+ console.print(" get api-key")
284
+ console.print(" namespace production")
285
+ console.print(" export file production.env")
286
+ console.print(" import .env.local")
287
+
288
+ def cmd_exit(self, args: List[str]):
289
+ """Exit the REPL."""
290
+ self.running = False
291
+ console.print("\nGoodbye!")
292
+
293
+
294
+ def run_repl():
295
+ """Entry point for the REPL."""
296
+ repl = SecretsREPL()
297
+ repl.run()
@@ -0,0 +1,246 @@
1
+ """
2
+ Git-based secrets store for synchronization across machines.
3
+ """
4
+
5
+ import os
6
+ import shutil
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Optional
9
+
10
+ import click
11
+ from git import GitCommandError, Repo
12
+
13
+ from mcli.lib.logger.logger import get_logger
14
+
15
+ logger = get_logger(__name__)
16
+ from mcli.lib.ui.styling import error, info, success, warning
17
+
18
+
19
+ class SecretsStore:
20
+ """Manages git-based secrets synchronization."""
21
+
22
+ def __init__(self, store_path: Optional[Path] = None):
23
+ """Initialize the secrets store.
24
+
25
+ Args:
26
+ store_path: Path to git repository for secrets. Defaults to ~/repos/mcli-secrets
27
+ """
28
+ self.store_path = store_path or Path.home() / "repos" / "mcli-secrets"
29
+ self.config_file = Path.home() / ".mcli" / "secrets-store.conf"
30
+ self.load_config()
31
+
32
+ def load_config(self) -> None:
33
+ """Load store configuration."""
34
+ self.store_config = {}
35
+ if self.config_file.exists():
36
+ with open(self.config_file) as f:
37
+ for line in f:
38
+ line = line.strip()
39
+ if "=" in line:
40
+ key, value = line.split("=", 1)
41
+ self.store_config[key.strip()] = value.strip()
42
+
43
+ # Override store path if configured
44
+ if "store_path" in self.store_config:
45
+ self.store_path = Path(self.store_config["store_path"])
46
+
47
+ def save_config(self) -> None:
48
+ """Save store configuration."""
49
+ self.config_file.parent.mkdir(parents=True, exist_ok=True)
50
+ with open(self.config_file, "w") as f:
51
+ for key, value in self.store_config.items():
52
+ f.write(f"{key}={value}\n")
53
+
54
+ def init(self, remote_url: Optional[str] = None) -> None:
55
+ """Initialize the secrets store repository.
56
+
57
+ Args:
58
+ remote_url: Optional git remote URL
59
+ """
60
+ if self.store_path.exists() and (self.store_path / ".git").exists():
61
+ error("Store already initialized")
62
+ return
63
+
64
+ self.store_path.mkdir(parents=True, exist_ok=True)
65
+
66
+ try:
67
+ repo = Repo.init(self.store_path)
68
+
69
+ # Create README
70
+ readme_path = self.store_path / "README.md"
71
+ readme_path.write_text(
72
+ "# MCLI Secrets Store\n\n"
73
+ "This repository stores encrypted secrets for MCLI.\n\n"
74
+ "**WARNING**: This repository contains encrypted sensitive data.\n"
75
+ "Ensure it is kept private and access is restricted.\n"
76
+ )
77
+
78
+ # Create .gitignore
79
+ gitignore_path = self.store_path / ".gitignore"
80
+ gitignore_path.write_text("*.key\n*.tmp\n.DS_Store\n")
81
+
82
+ repo.index.add([readme_path.name, gitignore_path.name])
83
+ repo.index.commit("Initial commit")
84
+
85
+ if remote_url:
86
+ repo.create_remote("origin", remote_url)
87
+ self.store_config["remote_url"] = remote_url
88
+ self.save_config()
89
+ info(f"Remote added: {remote_url}")
90
+
91
+ success(f"Secrets store initialized at {self.store_path}")
92
+
93
+ except GitCommandError as e:
94
+ error(f"Failed to initialize store: {e}")
95
+
96
+ def push(self, secrets_dir: Path, message: Optional[str] = None) -> None:
97
+ """Push secrets to the store.
98
+
99
+ Args:
100
+ secrets_dir: Directory containing encrypted secrets
101
+ message: Commit message
102
+ """
103
+ if not self._check_initialized():
104
+ return
105
+
106
+ try:
107
+ repo = Repo(self.store_path)
108
+
109
+ # Copy secrets to store
110
+ store_secrets_dir = self.store_path / "secrets"
111
+
112
+ # Remove existing secrets
113
+ if store_secrets_dir.exists():
114
+ shutil.rmtree(store_secrets_dir)
115
+
116
+ # Copy new secrets
117
+ shutil.copytree(secrets_dir, store_secrets_dir)
118
+
119
+ # Add to git
120
+ repo.index.add(["secrets"])
121
+
122
+ # Check if there are changes
123
+ if repo.is_dirty():
124
+ message = message or f"Update secrets from {os.uname().nodename}"
125
+ repo.index.commit(message)
126
+
127
+ # Push if remote exists
128
+ if "origin" in repo.remotes:
129
+ info("Pushing to remote...")
130
+ repo.remotes.origin.push()
131
+ success("Secrets pushed to remote")
132
+ else:
133
+ success("Secrets committed locally")
134
+ else:
135
+ info("No changes to push")
136
+
137
+ except GitCommandError as e:
138
+ error(f"Failed to push secrets: {e}")
139
+
140
+ def pull(self, secrets_dir: Path) -> None:
141
+ """Pull secrets from the store.
142
+
143
+ Args:
144
+ secrets_dir: Directory to store pulled secrets
145
+ """
146
+ if not self._check_initialized():
147
+ return
148
+
149
+ try:
150
+ repo = Repo(self.store_path)
151
+
152
+ # Pull from remote if exists
153
+ if "origin" in repo.remotes:
154
+ info("Pulling from remote...")
155
+ repo.remotes.origin.pull()
156
+
157
+ # Copy secrets from store
158
+ store_secrets_dir = self.store_path / "secrets"
159
+
160
+ if not store_secrets_dir.exists():
161
+ warning("No secrets found in store")
162
+ return
163
+
164
+ # Backup existing secrets
165
+ if secrets_dir.exists():
166
+ backup_dir = secrets_dir.parent / f"{secrets_dir.name}.backup"
167
+ if backup_dir.exists():
168
+ shutil.rmtree(backup_dir)
169
+ shutil.move(str(secrets_dir), str(backup_dir))
170
+ info(f"Existing secrets backed up to {backup_dir}")
171
+
172
+ # Copy secrets from store
173
+ shutil.copytree(store_secrets_dir, secrets_dir)
174
+
175
+ success(f"Secrets pulled to {secrets_dir}")
176
+
177
+ except GitCommandError as e:
178
+ error(f"Failed to pull secrets: {e}")
179
+
180
+ def sync(self, secrets_dir: Path, message: Optional[str] = None) -> None:
181
+ """Synchronize secrets (pull then push).
182
+
183
+ Args:
184
+ secrets_dir: Directory containing secrets
185
+ message: Commit message
186
+ """
187
+ if not self._check_initialized():
188
+ return
189
+
190
+ info("Synchronizing secrets...")
191
+
192
+ # First pull
193
+ self.pull(secrets_dir)
194
+
195
+ # Then push
196
+ self.push(secrets_dir, message)
197
+
198
+ def status(self) -> Dict[str, Any]:
199
+ """Get status of the secrets store.
200
+
201
+ Returns:
202
+ Status information
203
+ """
204
+ status = {
205
+ "initialized": False,
206
+ "store_path": str(self.store_path),
207
+ "has_remote": False,
208
+ "remote_url": None,
209
+ "clean": True,
210
+ "branch": None,
211
+ "commit": None,
212
+ }
213
+
214
+ if not self._check_initialized(silent=True):
215
+ return status
216
+
217
+ try:
218
+ repo = Repo(self.store_path)
219
+ status["initialized"] = True
220
+ status["clean"] = not repo.is_dirty()
221
+ status["branch"] = repo.active_branch.name
222
+ status["commit"] = str(repo.head.commit)[:8]
223
+
224
+ if "origin" in repo.remotes:
225
+ status["has_remote"] = True
226
+ status["remote_url"] = repo.remotes.origin.url
227
+
228
+ except Exception:
229
+ pass
230
+
231
+ return status
232
+
233
+ def _check_initialized(self, silent: bool = False) -> bool:
234
+ """Check if store is initialized.
235
+
236
+ Args:
237
+ silent: Don't print error message
238
+
239
+ Returns:
240
+ True if initialized
241
+ """
242
+ if not self.store_path.exists() or not (self.store_path / ".git").exists():
243
+ if not silent:
244
+ error("Store not initialized. Run 'mcli secrets store init' first.")
245
+ return False
246
+ return True
@@ -13,9 +13,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
13
13
  import numpy as np
14
14
  import pandas as pd
15
15
 
16
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../.."))
17
-
18
- from ml.models.recommendation_models import PortfolioRecommendation, StockRecommendationModel
16
+ from mcli.ml.models.recommendation_models import PortfolioRecommendation, StockRecommendationModel
19
17
 
20
18
  logger = logging.getLogger(__name__)
21
19
 
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env python3
2
+ """Entry point for backtesting CLI."""
3
+
4
+ import click
5
+
6
+ from mcli.lib.ui.styling import error, info, success
7
+
8
+
9
+ @click.group(name="mcli-backtest", help="Backtesting CLI for MCLI trading strategies")
10
+ def cli():
11
+ """Main CLI group for backtesting."""
12
+ pass
13
+
14
+
15
+ @cli.command(name="run", help="Run a backtest on historical data")
16
+ @click.option("--strategy", required=True, help="Strategy to backtest")
17
+ @click.option("--start-date", required=True, help="Start date (YYYY-MM-DD)")
18
+ @click.option("--end-date", required=True, help="End date (YYYY-MM-DD)")
19
+ @click.option("--initial-capital", default=100000, help="Initial capital")
20
+ @click.option("--output", help="Output file for results")
21
+ def run_backtest(strategy: str, start_date: str, end_date: str, initial_capital: float, output: str):
22
+ """Run a backtest with the specified parameters."""
23
+ info(f"Running backtest for strategy: {strategy}")
24
+ info(f"Period: {start_date} to {end_date}")
25
+ info(f"Initial capital: ${initial_capital:,.2f}")
26
+
27
+ # TODO: Implement actual backtesting logic
28
+ error("Backtesting functionality not yet implemented")
29
+
30
+
31
+ @cli.command(name="list", help="List available strategies")
32
+ def list_strategies():
33
+ """List all available trading strategies."""
34
+ info("Available strategies:")
35
+ # TODO: Implement strategy listing
36
+ error("Strategy listing not yet implemented")
37
+
38
+
39
+ @cli.command(name="analyze", help="Analyze backtest results")
40
+ @click.argument("results_file")
41
+ def analyze_results(results_file: str):
42
+ """Analyze backtest results from a file."""
43
+ info(f"Analyzing results from: {results_file}")
44
+ # TODO: Implement results analysis
45
+ error("Results analysis not yet implemented")
46
+
47
+
48
+ def main():
49
+ """Main entry point."""
50
+ cli()
51
+
52
+
53
+ if __name__ == "__main__":
54
+ main()
@@ -9,7 +9,7 @@ import pandas as pd
9
9
  import torch
10
10
  import torch.nn as nn
11
11
  import torch.nn.functional as F
12
- from base_models import BaseStockModel, ModelMetrics, ValidationResult
12
+ from mcli.ml.models.base_models import BaseStockModel, ModelMetrics, ValidationResult
13
13
 
14
14
  logger = logging.getLogger(__name__)
15
15
 
@@ -10,8 +10,8 @@ import pandas as pd
10
10
  import torch
11
11
  import torch.nn as nn
12
12
  import torch.nn.functional as F
13
- from base_models import BaseStockModel, ModelMetrics, ValidationResult
14
- from ensemble_models import DeepEnsembleModel, EnsembleConfig, ModelConfig
13
+ from mcli.ml.models.base_models import BaseStockModel, ModelMetrics, ValidationResult
14
+ from mcli.ml.models.ensemble_models import DeepEnsembleModel, EnsembleConfig, ModelConfig
15
15
 
16
16
  logger = logging.getLogger(__name__)
17
17
 
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env python3
2
+ """Entry point for portfolio optimization CLI."""
3
+
4
+ import click
5
+
6
+ from mcli.lib.ui.styling import error, info, success
7
+
8
+
9
+ @click.group(name="mcli-optimize", help="Portfolio optimization CLI for MCLI trading system")
10
+ def cli():
11
+ """Main CLI group for portfolio optimization."""
12
+ pass
13
+
14
+
15
+ @cli.command(name="portfolio", help="Optimize portfolio allocation")
16
+ @click.option("--symbols", required=True, help="Comma-separated list of symbols")
17
+ @click.option("--start-date", required=True, help="Start date (YYYY-MM-DD)")
18
+ @click.option("--end-date", required=True, help="End date (YYYY-MM-DD)")
19
+ @click.option("--risk-free-rate", default=0.02, help="Risk-free rate")
20
+ @click.option("--output", help="Output file for results")
21
+ def optimize_portfolio(symbols: str, start_date: str, end_date: str, risk_free_rate: float, output: str):
22
+ """Optimize portfolio allocation for given symbols."""
23
+ symbol_list = [s.strip() for s in symbols.split(",")]
24
+ info(f"Optimizing portfolio for: {', '.join(symbol_list)}")
25
+ info(f"Period: {start_date} to {end_date}")
26
+ info(f"Risk-free rate: {risk_free_rate:.2%}")
27
+
28
+ # TODO: Implement actual optimization
29
+ error("Portfolio optimization not yet implemented")
30
+
31
+
32
+ @cli.command(name="efficient-frontier", help="Generate efficient frontier")
33
+ @click.option("--symbols", required=True, help="Comma-separated list of symbols")
34
+ @click.option("--points", default=100, help="Number of points on frontier")
35
+ def efficient_frontier(symbols: str, points: int):
36
+ """Generate efficient frontier for given symbols."""
37
+ symbol_list = [s.strip() for s in symbols.split(",")]
38
+ info(f"Generating efficient frontier for: {', '.join(symbol_list)}")
39
+ info(f"Points: {points}")
40
+
41
+ # TODO: Implement efficient frontier generation
42
+ error("Efficient frontier generation not yet implemented")
43
+
44
+
45
+ def main():
46
+ """Main entry point."""
47
+ cli()
48
+
49
+
50
+ if __name__ == "__main__":
51
+ main()
@@ -0,0 +1 @@
1
+ """Model serving module for MCLI ML system."""