Files
group_fqcd_jr/app/api/controllers/trading.py
T

168 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""§T 用户自助场内基金模拟交易 controller(`docs/05` §19 T 段)。
端点与权限码(9 个端点 / 5 个权限码):
| § | 端点 | 权限码 | 摘要 |
|---|---|---|---|
| T001 | `GET /api/v1/users/me/account/dashboard` | `account:read:self` | 我的账户看板 |
| T002 | `POST /api/v1/users/me/orders` | `trade:order:create` | 提交委托(首版市价立即成交) |
| T003 | `GET /api/v1/users/me/orders` | `trade:order:read` | 委托列表 |
| T004 | `GET /api/v1/users/me/orders/{order_no}` | `trade:order:read` | 委托详情 |
| T005 | `POST /api/v1/users/me/orders/{order_no}/cancellations` | `trade:order:cancel` | 撤单 |
| T006 | `GET /api/v1/users/me/holdings` | `holding:read:self` | 持仓列表 |
| T007 | `GET /api/v1/users/me/transactions` | `trade:txn:read` | 成交记录列表 |
| T008 | `GET /api/v1/users/me/transactions/{txn_no}` | `trade:txn:read` | 成交详情 |
| T009 | `GET /api/v1/users/me/cash-ledger` | `account:read:self` | 资金明细 |
设计要点:
- 全部走 `build_request_context`(与 memory / portfolio 一致),数据范围 `self`。
- 不走限流依赖(`enforce_rate_limit`)——场内交易为低频,由底座网关层限流。
- 信封用 `envelope` / `list_envelope`,与 §3.3 一致。
"""
from __future__ import annotations
from fastapi import APIRouter, Depends, Query, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.dependencies.auth import build_request_context
from app.api.dependencies.database import get_session
from app.api.schemas.trading import OrderCreateRequest
from app.api.views.envelope import envelope, list_envelope
from app.core.contracts import RequestContext
from app.service.authorization_service import AuthorizationService
from app.service.suitability_service import SuitabilityService
from app.service.trade_service import TradeService
router = APIRouter(prefix="/api/v1/users/me", tags=["trading"])
def _service(session: AsyncSession, context: RequestContext) -> TradeService:
return TradeService(session, suitability_evaluator=SuitabilityService())
async def _authorize(context: RequestContext, permission: str) -> None:
"""Enforce the endpoint permission declared in docs/05 before DB work."""
await AuthorizationService.require(context, permission)
# T001 账户看板
@router.get("/account/dashboard")
async def get_account_dashboard(
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
await _authorize(context, "account:read:self")
data = await _service(session, context).get_account_dashboard(context)
return envelope(data, context)
# T002 提交委托(市价立即成交)
@router.post("/orders", status_code=status.HTTP_201_CREATED)
async def submit_order(
payload: OrderCreateRequest,
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
await _authorize(context, "trade:order:create")
data = await _service(session, context).submit_order(payload, context)
return envelope(data, context)
# T003 委托列表
@router.get("/orders")
async def list_orders(
limit: int = Query(default=20, ge=1, le=100),
cursor: str | None = Query(default=None),
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
await _authorize(context, "trade:order:read")
cursor_id = int(cursor) if cursor else None
items, next_cursor = await _service(session, context).list_orders(
context, limit=limit, cursor=cursor_id
)
return list_envelope(
{"items": items, "next_cursor": next_cursor, "has_more": next_cursor is not None},
context,
)
# T004 委托详情
@router.get("/orders/{order_no}")
async def get_order(
order_no: str,
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
await _authorize(context, "trade:order:read")
data = await _service(session, context).get_order(order_no, context)
return envelope(data, context)
# T005 撤单
@router.post("/orders/{order_no}/cancellations", status_code=status.HTTP_200_OK)
async def cancel_order(
order_no: str,
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
await _authorize(context, "trade:order:cancel")
order = await _service(session, context).cancel_order(order_no, context)
return envelope(order, context)
# T006 持仓列表
@router.get("/holdings")
async def list_holdings(
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
await _authorize(context, "holding:read:self")
data = await _service(session, context).list_holdings(context)
return envelope(data, context)
# T007 成交记录列表
@router.get("/transactions")
async def list_transactions(
limit: int = Query(default=20, ge=1, le=100),
cursor: str | None = Query(default=None),
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
await _authorize(context, "trade:txn:read")
cursor_id = int(cursor) if cursor else None
data = await _service(session, context).list_transactions(
context, limit=limit, cursor=cursor_id
)
return envelope(data, context)
# T008 成交详情
@router.get("/transactions/{txn_no}")
async def get_transaction(
txn_no: str,
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
await _authorize(context, "trade:txn:read")
item = await _service(session, context).get_transaction(txn_no, context)
return envelope(item, context)
# T009 资金明细
@router.get("/cash-ledger")
async def list_cash_ledger(
limit: int = Query(default=20, ge=1, le=100),
cursor: str | None = Query(default=None),
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
await _authorize(context, "account:read:self")
cursor_id = int(cursor) if cursor else None
data = await _service(session, context).list_cash_ledger(
context, limit=limit, cursor=cursor_id
)
return envelope(data, context)