Files
2026-07-12 19:46:07 +03:00

202 lines
7.3 KiB
Python

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()