sigma-terminal 2.0.2__py3-none-any.whl → 3.3.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.
sigma/cli.py ADDED
@@ -0,0 +1,465 @@
1
+ """CLI entry point for Sigma v3.3.0."""
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from typing import Optional
7
+
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+ from rich.panel import Panel
11
+
12
+ from .app import launch
13
+ from .config import (
14
+ get_settings, save_api_key, save_setting, AVAILABLE_MODELS, LLMProvider,
15
+ is_first_run, mark_first_run_complete, detect_lean_installation, detect_ollama
16
+ )
17
+
18
+
19
+ __version__ = "3.3.0"
20
+
21
+ console = Console()
22
+
23
+
24
+ def show_banner():
25
+ """Show the Sigma banner."""
26
+ banner = """
27
+ [bold white]███████╗██╗ ██████╗ ███╗ ███╗ █████╗ [/bold white]
28
+ [bold white]██╔════╝██║██╔════╝ ████╗ ████║██╔══██╗[/bold white]
29
+ [bold white]███████╗██║██║ ███╗██╔████╔██║███████║[/bold white]
30
+ [bold white]╚════██║██║██║ ██║██║╚██╔╝██║██╔══██║[/bold white]
31
+ [bold white]███████║██║╚██████╔╝██║ ╚═╝ ██║██║ ██║[/bold white]
32
+ [bold white]╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝[/bold white]
33
+
34
+ [dim]v3.3.0[/dim] [bold cyan]σ[/bold cyan] [bold]Finance Research Agent[/bold]
35
+ """
36
+ console.print(banner)
37
+
38
+
39
+ def main():
40
+ """Main CLI entry point."""
41
+ parser = argparse.ArgumentParser(
42
+ prog="sigma",
43
+ description="Sigma v3.3.0 - Finance Research Agent",
44
+ formatter_class=argparse.RawDescriptionHelpFormatter,
45
+ epilog="""
46
+ Examples:
47
+ sigma # Launch interactive app
48
+ sigma ask "analyze AAPL" # Quick query
49
+ sigma quote AAPL GOOGL MSFT # Get quotes
50
+ sigma --setup # Run setup wizard
51
+ """
52
+ )
53
+
54
+ parser.add_argument(
55
+ "--version", "-v",
56
+ action="store_true",
57
+ help="Show version and exit"
58
+ )
59
+
60
+ parser.add_argument(
61
+ "--setup",
62
+ action="store_true",
63
+ help="Run the setup wizard"
64
+ )
65
+
66
+ parser.add_argument(
67
+ "--setkey",
68
+ nargs=2,
69
+ metavar=("PROVIDER", "KEY"),
70
+ help="Set API key for provider (google, openai, anthropic, groq, xai)"
71
+ )
72
+
73
+ parser.add_argument(
74
+ "--provider",
75
+ choices=["google", "openai", "anthropic", "groq", "xai", "ollama"],
76
+ help="Set default AI provider"
77
+ )
78
+
79
+ parser.add_argument(
80
+ "--model",
81
+ help="Set default model"
82
+ )
83
+
84
+ parser.add_argument(
85
+ "--list-models",
86
+ action="store_true",
87
+ help="List available models"
88
+ )
89
+
90
+ parser.add_argument(
91
+ "--status",
92
+ action="store_true",
93
+ help="Show current configuration"
94
+ )
95
+
96
+ # Subcommands
97
+ subparsers = parser.add_subparsers(dest="command", help="Commands")
98
+
99
+ # Ask command
100
+ ask_parser = subparsers.add_parser("ask", help="Ask a question")
101
+ ask_parser.add_argument("query", nargs="+", help="Your question")
102
+
103
+ # Quote command
104
+ quote_parser = subparsers.add_parser("quote", help="Get stock quotes")
105
+ quote_parser.add_argument("symbols", nargs="+", help="Stock symbols")
106
+
107
+ # Chart command
108
+ chart_parser = subparsers.add_parser("chart", help="Generate a chart")
109
+ chart_parser.add_argument("symbol", help="Stock symbol")
110
+ chart_parser.add_argument("--period", default="6mo", help="Time period (default: 6mo)")
111
+ chart_parser.add_argument("--output", "-o", help="Output file path")
112
+
113
+ # Backtest command
114
+ backtest_parser = subparsers.add_parser("backtest", help="Run a backtest")
115
+ backtest_parser.add_argument("symbol", help="Stock symbol")
116
+ backtest_parser.add_argument("--strategy", "-s", default="sma_crossover",
117
+ help="Strategy name (default: sma_crossover)")
118
+ backtest_parser.add_argument("--period", default="1y", help="Time period (default: 1y)")
119
+
120
+ # Compare command
121
+ compare_parser = subparsers.add_parser("compare", help="Compare stocks")
122
+ compare_parser.add_argument("symbols", nargs="+", help="Stock symbols to compare")
123
+
124
+ args = parser.parse_args()
125
+
126
+ if args.version:
127
+ show_banner()
128
+ return 0
129
+
130
+ if args.setup:
131
+ from .setup import run_setup
132
+ result = run_setup()
133
+ if result:
134
+ mark_first_run_complete()
135
+ # After manual setup, ask if user wants to launch
136
+ from rich.prompt import Confirm
137
+ if Confirm.ask("\n[bold cyan]σ[/bold cyan] Launch Sigma now?", default=True):
138
+ launch()
139
+ return 0 if result else 1
140
+
141
+ # Auto-launch setup on first run, then start interactive
142
+ if is_first_run():
143
+ console.print("\n[bold cyan]σ[/bold cyan] [bold]Welcome to Sigma![/bold]")
144
+ console.print("[dim]First time setup detected. Launching setup wizard...[/dim]\n")
145
+ from .setup import run_setup
146
+ result = run_setup()
147
+ mark_first_run_complete() # Always mark complete to not ask again
148
+
149
+ if result:
150
+ console.print("\n[bold green]✓ Setup complete![/bold green]")
151
+ console.print("[dim]Launching Sigma...[/dim]\n")
152
+ import time
153
+ time.sleep(1) # Brief pause for user to see message
154
+ launch()
155
+ return 0
156
+ else:
157
+ console.print("\n[yellow]Setup skipped.[/yellow] You can run [bold]sigma --setup[/bold] later.")
158
+ console.print("[dim]Launching Sigma anyway...[/dim]\n")
159
+ launch()
160
+ return 0
161
+
162
+ if args.list_models:
163
+ console.print("\n[bold]Available Models by Provider:[/bold]\n")
164
+ for provider, models in AVAILABLE_MODELS.items():
165
+ console.print(f" [cyan]{provider}:[/cyan]")
166
+ for model in models:
167
+ console.print(f" • {model}")
168
+ return 0
169
+
170
+ if args.status:
171
+ settings = get_settings()
172
+
173
+ table = Table(title="Sigma Configuration", show_header=False)
174
+ table.add_column("Setting", style="bold")
175
+ table.add_column("Value")
176
+
177
+ table.add_row("Provider", settings.default_provider.value if hasattr(settings.default_provider, 'value') else str(settings.default_provider))
178
+ table.add_row("Model", settings.default_model)
179
+ table.add_row("", "")
180
+ table.add_row("API Keys", "")
181
+ table.add_row(" Google", "[green]OK[/green]" if settings.google_api_key else "[red]--[/red]")
182
+ table.add_row(" OpenAI", "[green]OK[/green]" if settings.openai_api_key else "[red]--[/red]")
183
+ table.add_row(" Anthropic", "[green]OK[/green]" if settings.anthropic_api_key else "[red]--[/red]")
184
+ table.add_row(" Groq", "[green]OK[/green]" if settings.groq_api_key else "[red]--[/red]")
185
+ table.add_row(" xAI", "[green]OK[/green]" if settings.xai_api_key else "[red]--[/red]")
186
+
187
+ console.print(table)
188
+ return 0
189
+
190
+ if args.setkey:
191
+ provider, key = args.setkey
192
+ provider = provider.lower()
193
+
194
+ try:
195
+ provider_enum = LLMProvider(provider)
196
+ save_api_key(provider_enum, key)
197
+ console.print(f"[bold cyan]σ[/bold cyan] API key for {provider} saved.")
198
+ return 0
199
+ except ValueError:
200
+ console.print(f"[red]Error:[/red] Unknown provider '{provider}'")
201
+ console.print(f"Valid providers: google, openai, anthropic, groq, xai")
202
+ return 1
203
+
204
+ if args.provider or args.model:
205
+ if args.provider:
206
+ save_setting("default_provider", args.provider)
207
+ console.print(f"[bold cyan]σ[/bold cyan] Default provider: {args.provider}")
208
+
209
+ if args.model:
210
+ save_setting("default_model", args.model)
211
+ console.print(f"[bold cyan]σ[/bold cyan] Default model: {args.model}")
212
+
213
+ return 0
214
+
215
+ # Handle subcommands
216
+ if args.command == "ask":
217
+ query = " ".join(args.query)
218
+ return handle_ask(query)
219
+
220
+ elif args.command == "quote":
221
+ return handle_quotes(args.symbols)
222
+
223
+ elif args.command == "chart":
224
+ return handle_chart(args.symbol, args.period, args.output)
225
+
226
+ elif args.command == "backtest":
227
+ return handle_backtest(args.symbol, args.strategy, args.period)
228
+
229
+ elif args.command == "compare":
230
+ return handle_compare(args.symbols)
231
+
232
+ # Default: Launch the app
233
+ launch()
234
+ return 0
235
+
236
+
237
+ def handle_ask(query: str) -> int:
238
+ """Handle ask command."""
239
+ import asyncio
240
+ from .llm import get_llm
241
+ from .tools import TOOLS, execute_tool
242
+
243
+ settings = get_settings()
244
+
245
+ console.print(f"\n[dim]Using {settings.default_provider.value} / {settings.default_model}[/dim]\n")
246
+
247
+ try:
248
+ llm = get_llm(settings.default_provider, settings.default_model)
249
+
250
+ async def run_query():
251
+ """Run the query asynchronously."""
252
+ messages = [
253
+ {"role": "system", "content": "You are Sigma, a helpful financial intelligence assistant. Use the tools available to provide accurate, data-driven insights."},
254
+ {"role": "user", "content": query}
255
+ ]
256
+
257
+ async def handle_tool(name: str, args: dict):
258
+ """Handle tool calls."""
259
+ console.print(f"[dim]Executing: {name}[/dim]")
260
+ return execute_tool(name, args)
261
+
262
+ response = await llm.generate(messages, TOOLS, handle_tool)
263
+ return response
264
+
265
+ with console.status("[bold blue]σ analyzing...[/bold blue]"):
266
+ response = asyncio.run(run_query())
267
+
268
+ console.print(Panel(response, title="[bold cyan]σ Sigma[/bold cyan]"))
269
+ return 0
270
+
271
+ except Exception as e:
272
+ console.print(f"[red]Error:[/red] {e}")
273
+ return 1
274
+
275
+
276
+ def handle_quotes(symbols: list) -> int:
277
+ """Handle quote command."""
278
+ from .tools import get_stock_quote
279
+
280
+ table = Table(title="Stock Quotes")
281
+ table.add_column("Symbol", style="cyan")
282
+ table.add_column("Price", justify="right")
283
+ table.add_column("Change", justify="right")
284
+ table.add_column("Change %", justify="right")
285
+ table.add_column("Volume", justify="right")
286
+
287
+ for symbol in symbols:
288
+ quote = get_stock_quote(symbol)
289
+
290
+ if "error" in quote:
291
+ table.add_row(symbol, "[red]Error[/red]", "-", "-", "-")
292
+ continue
293
+
294
+ change = quote.get("change", 0)
295
+ change_pct = quote.get("change_percent", 0)
296
+ change_style = "green" if change >= 0 else "red"
297
+
298
+ table.add_row(
299
+ quote.get("symbol", symbol),
300
+ f"${quote.get('price', 0):,.2f}",
301
+ f"[{change_style}]{change:+.2f}[/{change_style}]",
302
+ f"[{change_style}]{change_pct:+.2f}%[/{change_style}]",
303
+ f"{quote.get('volume', 0):,}",
304
+ )
305
+
306
+ console.print(table)
307
+ return 0
308
+
309
+
310
+ def handle_chart(symbol: str, period: str, output: Optional[str] = None) -> int:
311
+ """Handle chart command."""
312
+ from .charts import create_candlestick_chart
313
+ import yfinance as yf
314
+
315
+ with console.status(f"[bold blue]Generating chart for {symbol}...[/bold blue]"):
316
+ try:
317
+ ticker = yf.Ticker(symbol.upper())
318
+ hist = ticker.history(period=period)
319
+
320
+ if hist.empty:
321
+ console.print(f"[red]Error:[/red] No data found for {symbol}")
322
+ return 1
323
+
324
+ filepath = create_candlestick_chart(symbol, hist)
325
+ except Exception as e:
326
+ console.print(f"[red]Error:[/red] {e}")
327
+ return 1
328
+
329
+ console.print(f"[bold cyan]σ[/bold cyan] Chart saved to: {filepath}")
330
+
331
+ # Try to open the chart
332
+ import subprocess
333
+ try:
334
+ subprocess.run(["open", filepath], check=True)
335
+ except Exception:
336
+ pass
337
+
338
+ return 0
339
+
340
+
341
+ def handle_backtest(symbol: str, strategy: str, period: str) -> int:
342
+ """Handle backtest command."""
343
+ from .backtest import run_backtest, get_available_strategies
344
+
345
+ strategies = get_available_strategies()
346
+
347
+ if strategy not in strategies:
348
+ console.print(f"[red]Error:[/red] Unknown strategy '{strategy}'")
349
+ console.print(f"Available: {', '.join(strategies.keys())}")
350
+ return 1
351
+
352
+ with console.status(f"[bold blue]Running backtest: {strategy} on {symbol}...[/bold blue]"):
353
+ result = run_backtest(symbol, strategy, period)
354
+
355
+ if "error" in result:
356
+ console.print(f"[red]Error:[/red] {result['error']}")
357
+ return 1
358
+
359
+ # Display results
360
+ console.print()
361
+ console.print(Panel(
362
+ f"[bold]{result.get('strategy', strategy.upper())}[/bold]\n"
363
+ f"[dim]{result.get('strategy_description', '')}[/dim]",
364
+ title=f"[bold cyan]Backtest: {symbol.upper()}[/bold cyan]",
365
+ ))
366
+
367
+ # Performance table
368
+ perf = result.get("performance", {})
369
+ table = Table(title="Performance", show_header=False)
370
+ table.add_column("Metric", style="bold")
371
+ table.add_column("Value", justify="right")
372
+
373
+ table.add_row("Initial Capital", perf.get("initial_capital", "$100,000"))
374
+ table.add_row("Final Equity", perf.get("final_equity", "N/A"))
375
+ table.add_row("Total Return", perf.get("total_return", "N/A"))
376
+ table.add_row("Annual Return", perf.get("annual_return", "N/A"))
377
+ table.add_row("Buy & Hold Return", perf.get("buy_hold_return", "N/A"))
378
+ table.add_row("Alpha", perf.get("alpha", "N/A"))
379
+
380
+ console.print(table)
381
+
382
+ # Risk table
383
+ risk = result.get("risk", {})
384
+ risk_table = Table(title="Risk Metrics", show_header=False)
385
+ risk_table.add_column("Metric", style="bold")
386
+ risk_table.add_column("Value", justify="right")
387
+
388
+ risk_table.add_row("Volatility", risk.get("volatility", "N/A"))
389
+ risk_table.add_row("Max Drawdown", risk.get("max_drawdown", "N/A"))
390
+ risk_table.add_row("Sharpe Ratio", risk.get("sharpe_ratio", "N/A"))
391
+ risk_table.add_row("Sortino Ratio", risk.get("sortino_ratio", "N/A"))
392
+ risk_table.add_row("Calmar Ratio", risk.get("calmar_ratio", "N/A"))
393
+
394
+ console.print(risk_table)
395
+
396
+ # Trade stats
397
+ trades = result.get("trades", {})
398
+ trade_table = Table(title="Trade Statistics", show_header=False)
399
+ trade_table.add_column("Metric", style="bold")
400
+ trade_table.add_column("Value", justify="right")
401
+
402
+ trade_table.add_row("Total Trades", str(trades.get("total_trades", 0)))
403
+ trade_table.add_row("Win Rate", trades.get("win_rate", "N/A"))
404
+ trade_table.add_row("Profit Factor", trades.get("profit_factor", "N/A"))
405
+ trade_table.add_row("Avg Win", trades.get("avg_win", "N/A"))
406
+ trade_table.add_row("Avg Loss", trades.get("avg_loss", "N/A"))
407
+
408
+ console.print(trade_table)
409
+
410
+ return 0
411
+
412
+
413
+ def handle_compare(symbols: list) -> int:
414
+ """Handle compare command."""
415
+ from .tools import compare_stocks
416
+
417
+ with console.status(f"[bold blue]Comparing {', '.join(symbols)}...[/bold blue]"):
418
+ result = compare_stocks(symbols)
419
+
420
+ if "error" in result:
421
+ console.print(f"[red]Error:[/red] {result['error']}")
422
+ return 1
423
+
424
+ comparison = result.get("comparison", [])
425
+
426
+ if not comparison:
427
+ console.print("[yellow]No data found for the specified symbols[/yellow]")
428
+ return 1
429
+
430
+ # Display comparison table
431
+ table = Table(title=f"Stock Comparison ({result.get('period', '1y')})")
432
+ table.add_column("Symbol", style="cyan")
433
+ table.add_column("Name", style="dim")
434
+ table.add_column("Price", justify="right")
435
+ table.add_column("Return", justify="right")
436
+ table.add_column("Volatility", justify="right")
437
+ table.add_column("Sharpe", justify="right")
438
+ table.add_column("P/E", justify="right")
439
+
440
+ for stock in comparison:
441
+ return_val = stock.get("total_return", 0)
442
+ return_style = "green" if return_val >= 0 else "red"
443
+
444
+ table.add_row(
445
+ stock.get("symbol", ""),
446
+ stock.get("name", "N/A")[:20],
447
+ f"${stock.get('price', 0):,.2f}",
448
+ f"[{return_style}]{return_val:+.2f}%[/{return_style}]",
449
+ f"{stock.get('volatility', 0):.1f}%",
450
+ f"{stock.get('sharpe', 0):.2f}",
451
+ str(stock.get("pe_ratio", "N/A")),
452
+ )
453
+
454
+ console.print(table)
455
+
456
+ # Summary
457
+ console.print()
458
+ console.print(f"[green]Best:[/green] {result.get('best_performer', 'N/A')}")
459
+ console.print(f"[red]Worst:[/red] {result.get('worst_performer', 'N/A')}")
460
+
461
+ return 0
462
+
463
+
464
+ if __name__ == "__main__":
465
+ sys.exit(main())