testing new features
This commit is contained in:
+155
-27
@@ -6,7 +6,8 @@ from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from ai.assistant import AssistantService
|
||||
from cashflow_model import FinancialModel
|
||||
from cashflow_model import CurrencyConverter, FinancialModel
|
||||
from cli.config import app as config_app
|
||||
from engine.forecast import ForecastService
|
||||
from engine.scenarios import DEFAULT_SCENARIOS, ScenarioService
|
||||
from sync.excel_sync import ExcelSync
|
||||
@@ -22,6 +23,9 @@ console = Console()
|
||||
DATA_DIR = Path("data")
|
||||
MODEL_PATH = DATA_DIR / "model.json"
|
||||
|
||||
CURRENCY_HELP = "Валюта отображения (по умолчанию — базовая валюта модели)"
|
||||
CURRENCY_OPTION = typer.Option(None, "--currency", "-c", help=CURRENCY_HELP)
|
||||
|
||||
|
||||
def _load_model() -> FinancialModel:
|
||||
if MODEL_PATH.exists():
|
||||
@@ -33,26 +37,59 @@ def _save_model(model: FinancialModel) -> None:
|
||||
model.save(MODEL_PATH)
|
||||
|
||||
|
||||
def _get_converter(model: FinancialModel) -> CurrencyConverter:
|
||||
return CurrencyConverter(model.exchange_rates)
|
||||
|
||||
|
||||
def _fmt(amount: float, currency: str, symbol: str) -> str:
|
||||
return f"{symbol}{amount:,.2f}"
|
||||
|
||||
|
||||
def _resolve_currency(model: FinancialModel, currency: str | None) -> tuple[str, str]:
|
||||
target = currency or model.base_currency
|
||||
converter = _get_converter(model)
|
||||
symbol = converter.get_symbol(target)
|
||||
return target, symbol
|
||||
|
||||
|
||||
def _convert_value(
|
||||
converter: CurrencyConverter, amount: float, from_curr: str, to_curr: str
|
||||
) -> float:
|
||||
if to_curr == from_curr:
|
||||
return amount
|
||||
return converter.convert(amount, from_curr, to_curr)
|
||||
|
||||
|
||||
@app.command()
|
||||
def init() -> None:
|
||||
"""Создать пустую финансовую модель"""
|
||||
model = FinancialModel()
|
||||
_save_model(model)
|
||||
console.print("[green]OK[/green] Пустая модель создана в data/model.json")
|
||||
console.print(f"[green]OK[/green] Пустая модель создана в {MODEL_PATH}")
|
||||
|
||||
|
||||
@app.command()
|
||||
def forecast(
|
||||
months: int = typer.Option(12, "--months", "-m", help="Количество месяцев прогноза"),
|
||||
currency: str | None = CURRENCY_OPTION,
|
||||
) -> None:
|
||||
"""Запустить прогноз денежных потоков"""
|
||||
model = _load_model()
|
||||
target_curr, symbol = _resolve_currency(model, currency)
|
||||
converter = _get_converter(model)
|
||||
base_curr = model.base_currency
|
||||
service = ForecastService(model)
|
||||
results = service.forecast_cashflow(months)
|
||||
summary = service.summary(months)
|
||||
|
||||
def _cv(val):
|
||||
return _convert_value(converter, val, base_curr, target_curr)
|
||||
|
||||
def _f(val):
|
||||
return _fmt(val, target_curr, symbol)
|
||||
|
||||
if results:
|
||||
table = Table(title=f"Прогноз на {months} мес.")
|
||||
table = Table(title=f"Прогноз на {months} мес. ({target_curr})")
|
||||
table.add_column("Счёт", style="cyan")
|
||||
table.add_column("Месяц", style="white")
|
||||
table.add_column("Баланс", justify="right", style="green")
|
||||
@@ -62,24 +99,29 @@ def forecast(
|
||||
for r in results:
|
||||
table.add_row(
|
||||
r["account"], str(r["month"]),
|
||||
f"${r['balance']:.2f}",
|
||||
f"${r['income']:.2f}",
|
||||
f"${r['expenses']:.2f}",
|
||||
_f(_cv(r["balance"])),
|
||||
_f(_cv(r["income"])),
|
||||
_f(_cv(r["expenses"])),
|
||||
)
|
||||
console.print(table)
|
||||
|
||||
console.print(f"\n[bold]Итог:[/bold] Баланс: ${summary['total_balance']:.2f} | "
|
||||
f"Доход: ${summary['total_income']:.2f} | "
|
||||
f"Расход: ${summary['total_expenses']:.2f}")
|
||||
cb = _cv(summary["total_balance"])
|
||||
ci = _cv(summary["total_income"])
|
||||
ce = _cv(summary["total_expenses"])
|
||||
console.print(f"\n[bold]Итог:[/bold] Баланс: {_f(cb)} | Доход: {_f(ci)} | Расход: {_f(ce)}")
|
||||
|
||||
|
||||
@app.command()
|
||||
def scenario(
|
||||
name: str = typer.Argument("baseline", help="Имя сценария: baseline, optimistic, pessimistic"),
|
||||
months: int = typer.Option(12, "--months", "-m", help="Количество месяцев"),
|
||||
currency: str | None = CURRENCY_OPTION,
|
||||
) -> None:
|
||||
"""Применить сценарий и показать прогноз"""
|
||||
model = _load_model()
|
||||
target_curr, symbol = _resolve_currency(model, currency)
|
||||
converter = _get_converter(model)
|
||||
base_curr = model.base_currency
|
||||
service = ScenarioService(model)
|
||||
|
||||
if name in DEFAULT_SCENARIOS:
|
||||
@@ -90,11 +132,14 @@ def scenario(
|
||||
raise typer.Exit(1)
|
||||
|
||||
result = service.apply(scenario_obj, months)
|
||||
balance = _convert_value(converter, result["total_balance"], base_curr, target_curr)
|
||||
income = _convert_value(converter, result["total_income"], base_curr, target_curr)
|
||||
expenses = _convert_value(converter, result["total_expenses"], base_curr, target_curr)
|
||||
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}")
|
||||
console.print(f"Баланс: {_fmt(balance, target_curr, symbol)}")
|
||||
console.print(f"Доход: {_fmt(income, target_curr, symbol)}")
|
||||
console.print(f"Расход: {_fmt(expenses, target_curr, symbol)}")
|
||||
|
||||
|
||||
@app.command()
|
||||
@@ -103,41 +148,53 @@ def whatif(
|
||||
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="Количество месяцев"),
|
||||
currency: str | None = CURRENCY_OPTION,
|
||||
) -> None:
|
||||
"""What-if анализ с произвольными множителями"""
|
||||
model = _load_model()
|
||||
target_curr, symbol = _resolve_currency(model, currency)
|
||||
converter = _get_converter(model)
|
||||
base_curr = model.base_currency
|
||||
service = ScenarioService(model)
|
||||
result = service.what_if(income_mult, expense_mult, growth_mult, months)
|
||||
|
||||
balance = _convert_value(converter, result["total_balance"], base_curr, target_curr)
|
||||
income = _convert_value(converter, result["total_income"], base_curr, target_curr)
|
||||
expenses = _convert_value(converter, result["total_expenses"], base_curr, target_curr)
|
||||
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}")
|
||||
console.print(f"Баланс: {_fmt(balance, target_curr, symbol)}")
|
||||
console.print(f"Доход: {_fmt(income, target_curr, symbol)}")
|
||||
console.print(f"Расход: {_fmt(expenses, target_curr, symbol)}")
|
||||
|
||||
|
||||
@app.command()
|
||||
def compare(
|
||||
months: int = typer.Option(12, "--months", "-m", help="Количество месяцев"),
|
||||
currency: str | None = CURRENCY_OPTION,
|
||||
) -> None:
|
||||
"""Сравнить все сценарии"""
|
||||
model = _load_model()
|
||||
target_curr, symbol = _resolve_currency(model, currency)
|
||||
converter = _get_converter(model)
|
||||
base_curr = model.base_currency
|
||||
service = ScenarioService(model)
|
||||
results = service.compare(months)
|
||||
|
||||
table = Table(title="Сравнение сценариев")
|
||||
table = Table(title=f"Сравнение сценариев ({target_curr})")
|
||||
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}",
|
||||
)
|
||||
b = _convert_value(converter, r["total_balance"], base_curr, target_curr)
|
||||
i = _convert_value(converter, r["total_income"], base_curr, target_curr)
|
||||
e = _convert_value(converter, r["total_expenses"], base_curr, target_curr)
|
||||
fb = _fmt(b, target_curr, symbol)
|
||||
fi = _fmt(i, target_curr, symbol)
|
||||
fe = _fmt(e, target_curr, symbol)
|
||||
table.add_row(name, fb, fi, fe)
|
||||
console.print(table)
|
||||
|
||||
|
||||
@@ -155,6 +212,7 @@ def import_xlsx(
|
||||
f"{len(model.recurring)} регулярных платежей, "
|
||||
f"{len(model.assets)} активов, "
|
||||
f"{len(model.liabilities)} обязательств")
|
||||
console.print(f"[dim]Базовая валюта: {model.base_currency}[/dim]")
|
||||
except Exception as e:
|
||||
console.print(f"[red]Ошибка импорта: {e}[/red]")
|
||||
raise typer.Exit(1)
|
||||
@@ -178,24 +236,94 @@ def export_xlsx(
|
||||
@app.command()
|
||||
def analyze(
|
||||
months: int = typer.Option(12, "--months", "-m", help="Количество месяцев для анализа"),
|
||||
currency: str | None = CURRENCY_OPTION,
|
||||
) -> None:
|
||||
"""AI-анализ финансовой модели"""
|
||||
model = _load_model()
|
||||
assistant = AssistantService(model)
|
||||
target_curr, _ = _resolve_currency(model, currency)
|
||||
converter = _get_converter(model)
|
||||
assistant = AssistantService(model, converter=converter, display_currency=target_curr)
|
||||
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(f"Базовая валюта: {model.base_currency}")
|
||||
console.print(
|
||||
"\n[yellow]AI-ответ: заглушка. Подключите реальный API в ai/assistant.py[/yellow]"
|
||||
)
|
||||
|
||||
|
||||
@app.command()
|
||||
def info() -> None:
|
||||
"""Сводка всей финансовой модели"""
|
||||
model = _load_model()
|
||||
converter = _get_converter(model)
|
||||
symbol = converter.get_symbol(model.base_currency)
|
||||
|
||||
console.print("[bold]Метаданные[/bold]")
|
||||
console.print(f" Базовая валюта: {model.base_currency} ({symbol})")
|
||||
console.print(f" Счетов: {len(model.accounts)}")
|
||||
console.print(f" Транзакций: {len(model.transactions)}")
|
||||
console.print(f" Регулярных платежей: {len(model.recurring)}")
|
||||
console.print(f" Активов: {len(model.assets)}")
|
||||
console.print(f" Обязательств: {len(model.liabilities)}")
|
||||
console.print(f" Курсов валют: {len(model.exchange_rates)}")
|
||||
|
||||
if model.accounts:
|
||||
t = Table(title="Счета")
|
||||
t.add_column("Название", style="cyan")
|
||||
t.add_column("Валюта")
|
||||
t.add_column("Баланс", justify="right")
|
||||
for a in model.accounts:
|
||||
t.add_row(a.name, a.currency, f"{a.balance:,.2f}")
|
||||
console.print(t)
|
||||
|
||||
if model.transactions:
|
||||
t = Table(title="Транзакции")
|
||||
t.add_column("Дата")
|
||||
t.add_column("Категория")
|
||||
t.add_column("Сумма", justify="right")
|
||||
t.add_column("Описание")
|
||||
for tx in model.transactions[:10]:
|
||||
t.add_row(tx.date, tx.category, f"{tx.amount:,.2f}", tx.description)
|
||||
if len(model.transactions) > 10:
|
||||
console.print(t)
|
||||
console.print(f"[dim]... и ещё {len(model.transactions) - 10} транзакций[/dim]")
|
||||
else:
|
||||
console.print(t)
|
||||
|
||||
if model.recurring:
|
||||
t = Table(title="Регулярные платежи")
|
||||
t.add_column("Период")
|
||||
t.add_column("Сумма", justify="right")
|
||||
t.add_column("Категория")
|
||||
for r in model.recurring:
|
||||
t.add_row(r.frequency, f"{r.amount:,.2f}", r.category)
|
||||
console.print(t)
|
||||
|
||||
if model.assets:
|
||||
t = Table(title="Активы")
|
||||
t.add_column("Название", style="cyan")
|
||||
t.add_column("Стоимость", justify="right")
|
||||
t.add_column("Рост", justify="right")
|
||||
for a in model.assets:
|
||||
t.add_row(a.name, f"{a.value:,.2f}", f"{a.growth_rate:.1f}%")
|
||||
console.print(t)
|
||||
|
||||
if model.liabilities:
|
||||
t = Table(title="Обязательства")
|
||||
t.add_column("Название", style="cyan")
|
||||
t.add_column("Долг", justify="right")
|
||||
t.add_column("Ставка", justify="right")
|
||||
t.add_column("Платёж", justify="right")
|
||||
for li in model.liabilities:
|
||||
t.add_row(li.name, f"{li.balance:,.2f}", f"{li.interest:.1f}%", f"{li.payment:,.2f}")
|
||||
console.print(t)
|
||||
|
||||
|
||||
app.add_typer(config_app)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
|
||||
Reference in New Issue
Block a user