feat(analytics): брокерские потоки по месяцам и ручные цены инструментов

GET /analytics/cashflow-broker читает metric_cash_flow_broker. POST
/instruments/{id}/prices апсертит price_manual по (instrument_id, d);
pricing/prices.py подмешивает его в ту же серию, что price_daily, с тем же
протягиванием и порогом устаревания.
This commit is contained in:
Dmitry
2026-09-19 10:38:59 +03:00
parent ae82df6fa7
commit 6b32c79e79
5 changed files with 205 additions and 8 deletions
+62
View File
@@ -160,6 +160,68 @@ async def test_the_instrument_card_carries_lots_events_and_prices(
assert body["prices"][0]["close"] == "110.0000000000"
async def test_manual_price_fills_the_gap_and_shows_up_as_manual(
client: AsyncClient, auth_headers: dict[str, str], portfolio: dict[str, int]
):
silent = portfolio["silent"]
d = today_local().isoformat()
r = await client.post(
f"/api/v1/instruments/{silent}/prices",
headers=auth_headers,
json={"d": d, "price": "1050", "currency": "RUB", "note": "ручная оценка"},
)
assert r.status_code == 201, r.text
assert r.json()["price"] == "1050.0000000000"
r = await client.get(f"/api/v1/instruments/{silent}", headers=auth_headers)
prices = r.json()["prices"]
assert prices[-1] == {
"d": d,
"close": "1050.0000000000",
"currency": "RUB",
"accrued_interest": None,
"source": "manual",
}
# a second call for the same day replaces it rather than adding a row
r = await client.post(
f"/api/v1/instruments/{silent}/prices",
headers=auth_headers,
json={"d": d, "price": "1100", "currency": "RUB"},
)
assert r.status_code == 201, r.text
r = await client.get(f"/api/v1/instruments/{silent}", headers=auth_headers)
manual_prices = [p for p in r.json()["prices"] if p["source"] == "manual"]
assert len(manual_prices) == 1
assert manual_prices[0]["close"] == "1100.0000000000"
async def test_manual_price_on_an_unknown_instrument_is_a_problem(
client: AsyncClient, auth_headers: dict[str, str]
):
r = await client.post(
"/api/v1/instruments/999999/prices",
headers=auth_headers,
json={"d": today_local().isoformat(), "price": "1", "currency": "RUB"},
)
assert r.status_code == 404
async def test_cashflow_broker_reports_the_months_money_moved(
client: AsyncClient, auth_headers: dict[str, str], portfolio: dict[str, int]
):
r = await client.get("/api/v1/analytics/cashflow-broker", headers=auth_headers)
assert r.status_code == 200, r.text
rows = r.json()
assert rows # the fixture's 20000 ₽ deposit lands in exactly one month
total_deposits = sum(float(row["deposits_rub"]) for row in rows)
total_withdrawals = sum(float(row["withdrawals_rub"]) for row in rows)
assert total_deposits == 20000.0
assert total_withdrawals == 0.0
assert all(row["net_rub"] == row["deposits_rub"] for row in rows)
async def test_events_can_be_filtered_down_to_the_external_flows(
client: AsyncClient, auth_headers: dict[str, str], portfolio: dict[str, int]
):