import sys from pathlib import Path import typer from rich.console import Console from rich.table import Table from ai.assistant import AssistantService from cashflow_model import FinancialModel from engine.forecast import ForecastService from engine.scenarios import DEFAULT_SCENARIOS, ScenarioService from sync.excel_sync import ExcelSync try: sys.stdout.reconfigure(encoding="utf-8") except (AttributeError, OSError): pass app = typer.Typer(name="cf", help="CashFlow Forecast - personal finance model") console = Console() DATA_DIR = Path("data") MODEL_PATH = DATA_DIR / "model.json" def _load_model() -> FinancialModel: if MODEL_PATH.exists(): return FinancialModel.load(MODEL_PATH) return FinancialModel() def _save_model(model: FinancialModel) -> None: model.save(MODEL_PATH) @app.command() def init() -> None: """Создать пустую финансовую модель""" model = FinancialModel() _save_model(model) console.print("[green]OK[/green] Пустая модель создана в data/model.json") @app.command() def forecast( months: int = typer.Option(12, "--months", "-m", help="Количество месяцев прогноза"), ) -> None: """Запустить прогноз денежных потоков""" model = _load_model() service = ForecastService(model) results = service.forecast_cashflow(months) summary = service.summary(months) if results: table = Table(title=f"Прогноз на {months} мес.") table.add_column("Счёт", style="cyan") table.add_column("Месяц", style="white") table.add_column("Баланс", justify="right", style="green") table.add_column("Доход", justify="right") table.add_column("Расход", justify="right") for r in results: table.add_row( r["account"], str(r["month"]), f"${r['balance']:.2f}", f"${r['income']:.2f}", f"${r['expenses']:.2f}", ) console.print(table) console.print(f"\n[bold]Итог:[/bold] Баланс: ${summary['total_balance']:.2f} | " f"Доход: ${summary['total_income']:.2f} | " f"Расход: ${summary['total_expenses']:.2f}") @app.command() def scenario( name: str = typer.Argument("baseline", help="Имя сценария: baseline, optimistic, pessimistic"), months: int = typer.Option(12, "--months", "-m", help="Количество месяцев"), ) -> None: """Применить сценарий и показать прогноз""" model = _load_model() service = ScenarioService(model) if name in DEFAULT_SCENARIOS: scenario_obj = DEFAULT_SCENARIOS[name] else: console.print(f"[red]Неизвестный сценарий: {name}[/red]") console.print(f"Доступны: {', '.join(DEFAULT_SCENARIOS.keys())}") raise typer.Exit(1) result = service.apply(scenario_obj, months) console.print(f"[bold]Сценарий:[/bold] {result['scenario']}") console.print(f"[dim]{result['scenario_description']}[/dim]") console.print(f"Баланс: ${result['total_balance']:.2f}") console.print(f"Доход: ${result['total_income']:.2f}") console.print(f"Расход: ${result['total_expenses']:.2f}") @app.command() def whatif( income_mult: float = typer.Option(1.0, "--income", "-i", help="Множитель дохода"), expense_mult: float = typer.Option(1.0, "--expense", "-e", help="Множитель расхода"), growth_mult: float = typer.Option(1.0, "--growth", "-g", help="Множитель роста активов"), months: int = typer.Option(12, "--months", "-m", help="Количество месяцев"), ) -> None: """What-if анализ с произвольными множителями""" model = _load_model() service = ScenarioService(model) result = service.what_if(income_mult, expense_mult, growth_mult, months) console.print("[bold]What-if анализ[/bold]") console.print(f"Доход x{income_mult} | Расход x{expense_mult} | Рост x{growth_mult}") console.print(f"Баланс: ${result['total_balance']:.2f}") console.print(f"Доход: ${result['total_income']:.2f}") console.print(f"Расход: ${result['total_expenses']:.2f}") @app.command() def compare( months: int = typer.Option(12, "--months", "-m", help="Количество месяцев"), ) -> None: """Сравнить все сценарии""" model = _load_model() service = ScenarioService(model) results = service.compare(months) table = Table(title="Сравнение сценариев") table.add_column("Сценарий", style="cyan") table.add_column("Баланс", justify="right") table.add_column("Доход", justify="right") table.add_column("Расход", justify="right") for name, r in results.items(): table.add_row( name, f"${r['total_balance']:.2f}", f"${r['total_income']:.2f}", f"${r['total_expenses']:.2f}", ) console.print(table) @app.command() def import_xlsx( path: str = typer.Argument(..., help="Путь к .xlsx файлу"), ) -> None: """Импорт данных из Excel""" sync = ExcelSync() try: model = sync.import_model(path) _save_model(model) console.print(f"[green]OK[/green] Импортировано: {len(model.accounts)} счетов, " f"{len(model.transactions)} транзакций, " f"{len(model.recurring)} регулярных платежей, " f"{len(model.assets)} активов, " f"{len(model.liabilities)} обязательств") except Exception as e: console.print(f"[red]Ошибка импорта: {e}[/red]") raise typer.Exit(1) @app.command() def export_xlsx( path: str = typer.Argument("exports/forecast.xlsx", help="Путь для .xlsx файла"), ) -> None: """Экспорт модели в Excel""" model = _load_model() sync = ExcelSync() try: sync.export_model(model, path) console.print(f"[green]OK[/green] Модель экспортирована в {path}") except Exception as e: console.print(f"[red]Ошибка экспорта: {e}[/red]") raise typer.Exit(1) @app.command() def analyze( months: int = typer.Option(12, "--months", "-m", help="Количество месяцев для анализа"), ) -> None: """AI-анализ финансовой модели""" model = _load_model() assistant = AssistantService(model) result = assistant.analyze(months) console.print("[bold]Промпт для AI:[/bold]") console.print(result["prompt"][:500] + "...\n") console.print("[bold]Сводка:[/bold]") s = result["summary"] console.print(f"Баланс: ${s['total_balance']:.2f}") console.print(f"Доход: ${s['total_income']:.2f}") console.print(f"Расход: ${s['total_expenses']:.2f}") console.print( "\n[yellow]AI-ответ: заглушка. Подключите реальный API в ai/assistant.py[/yellow]" ) if __name__ == "__main__": app()