Initial commit
This commit is contained in:
1
backend/app/api/__init__.py
Normal file
1
backend/app/api/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
||||
79
backend/app/api/routes.py
Normal file
79
backend/app/api/routes.py
Normal file
@ -0,0 +1,79 @@
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from app.api.schemas import (
|
||||
AShareIndexFlowResponse,
|
||||
AShareSectorFlowResponse,
|
||||
HealthResponse,
|
||||
HistoryResponse,
|
||||
MetaResponse,
|
||||
OverviewResponse,
|
||||
PushRecord,
|
||||
PushRecordsResponse,
|
||||
RulesResponse,
|
||||
SourceDiagnosticsResponse,
|
||||
)
|
||||
from app.services.ashare_flow_service import ashare_flow_service
|
||||
from app.services.alert_service import alert_service
|
||||
from app.services.monitoring_service import monitoring_service
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/health", response_model=HealthResponse)
|
||||
def health() -> HealthResponse:
|
||||
return HealthResponse(status="ok")
|
||||
|
||||
|
||||
@router.get("/meta", response_model=MetaResponse)
|
||||
def meta() -> MetaResponse:
|
||||
return monitoring_service.get_meta()
|
||||
|
||||
|
||||
@router.get("/overview", response_model=OverviewResponse)
|
||||
def overview() -> OverviewResponse:
|
||||
return monitoring_service.get_overview()
|
||||
|
||||
|
||||
@router.get("/history", response_model=HistoryResponse)
|
||||
def history() -> HistoryResponse:
|
||||
return monitoring_service.get_history()
|
||||
|
||||
|
||||
@router.get("/push-records", response_model=PushRecordsResponse)
|
||||
def push_records() -> PushRecordsResponse:
|
||||
return monitoring_service.get_push_records()
|
||||
|
||||
|
||||
@router.post("/push-records/test", response_model=PushRecord)
|
||||
def send_test_push_record() -> PushRecord:
|
||||
return PushRecord(**alert_service.send_test_alert())
|
||||
|
||||
|
||||
@router.get("/rules", response_model=RulesResponse)
|
||||
def rules() -> RulesResponse:
|
||||
return monitoring_service.get_rules()
|
||||
|
||||
|
||||
@router.get("/source-diagnostics", response_model=SourceDiagnosticsResponse)
|
||||
def source_diagnostics() -> SourceDiagnosticsResponse:
|
||||
return monitoring_service.get_source_diagnostics()
|
||||
|
||||
|
||||
@router.get("/ashare/index-flows/realtime", response_model=AShareIndexFlowResponse)
|
||||
def ashare_index_realtime() -> AShareIndexFlowResponse:
|
||||
return AShareIndexFlowResponse(**ashare_flow_service.get_index_realtime())
|
||||
|
||||
|
||||
@router.get("/ashare/index-flows/daily", response_model=AShareIndexFlowResponse)
|
||||
def ashare_index_daily(trade_date: str | None = Query(default=None)) -> AShareIndexFlowResponse:
|
||||
return AShareIndexFlowResponse(**ashare_flow_service.get_index_daily(trade_date))
|
||||
|
||||
|
||||
@router.get("/ashare/sector-flows/realtime", response_model=AShareSectorFlowResponse)
|
||||
def ashare_sector_realtime() -> AShareSectorFlowResponse:
|
||||
return AShareSectorFlowResponse(**ashare_flow_service.get_sector_realtime())
|
||||
|
||||
|
||||
@router.get("/ashare/sector-flows/daily", response_model=AShareSectorFlowResponse)
|
||||
def ashare_sector_daily(trade_date: str | None = Query(default=None)) -> AShareSectorFlowResponse:
|
||||
return AShareSectorFlowResponse(**ashare_flow_service.get_sector_daily(trade_date))
|
||||
201
backend/app/api/schemas.py
Normal file
201
backend/app/api/schemas.py
Normal file
@ -0,0 +1,201 @@
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
Precision = Literal["realtime_exact", "close_final", "historical_exact", "unavailable"]
|
||||
MarketState = Literal["pre_open", "trading_am", "midday_break", "trading_pm", "finalizing", "closed"]
|
||||
PushStatus = Literal["pending", "sent", "failed", "skipped"]
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
status: str
|
||||
|
||||
|
||||
class ValueWithStatus(BaseModel):
|
||||
amount_hkd_billion: float | None = None
|
||||
precision: Precision
|
||||
label: str
|
||||
|
||||
|
||||
class OverviewSnapshot(BaseModel):
|
||||
trade_date: str
|
||||
snapshot_time: str | None = None
|
||||
market_state: MarketState
|
||||
total_net_inflow: ValueWithStatus
|
||||
cumulative_net_inflow: ValueWithStatus
|
||||
shanghai_net_inflow: ValueWithStatus
|
||||
shenzhen_net_inflow: ValueWithStatus
|
||||
buy_amount: ValueWithStatus
|
||||
sell_amount: ValueWithStatus
|
||||
net_buy_amount: ValueWithStatus
|
||||
one_min_change: ValueWithStatus
|
||||
five_min_change: ValueWithStatus
|
||||
threshold_progress: float = Field(ge=0, le=1)
|
||||
next_threshold_hkd_billion: float
|
||||
source_name: str
|
||||
source_url: str | None = None
|
||||
updated_at: str | None = None
|
||||
unavailable_reason: str | None = None
|
||||
|
||||
|
||||
class TimelinePoint(BaseModel):
|
||||
timestamp: str
|
||||
amount_hkd_billion: float | None = None
|
||||
precision: Precision
|
||||
|
||||
|
||||
class BenchmarkTimelinePoint(BaseModel):
|
||||
timestamp: str
|
||||
value: float | None = None
|
||||
|
||||
|
||||
class BenchmarkTimelineSeries(BaseModel):
|
||||
key: str
|
||||
label: str
|
||||
unit: str
|
||||
detail_url: str | None = None
|
||||
points: list[BenchmarkTimelinePoint]
|
||||
|
||||
|
||||
class PushRecord(BaseModel):
|
||||
id: str
|
||||
triggered_at: str
|
||||
push_type: str
|
||||
rule_code: str
|
||||
trigger_value_hkd_billion: float | None = None
|
||||
description: str
|
||||
email_subject: str
|
||||
email_summary: str
|
||||
status: PushStatus
|
||||
error_message: str | None = None
|
||||
|
||||
|
||||
class PushRecordsResponse(BaseModel):
|
||||
records: list[PushRecord]
|
||||
|
||||
|
||||
class OverviewResponse(BaseModel):
|
||||
snapshot: OverviewSnapshot
|
||||
minute_timeline: list[TimelinePoint]
|
||||
benchmark_series: list[BenchmarkTimelineSeries]
|
||||
recent_push_records: list[PushRecord]
|
||||
|
||||
|
||||
class StatPoint(BaseModel):
|
||||
period: str
|
||||
amount_hkd_billion: float
|
||||
|
||||
|
||||
class RecentTradeDay(BaseModel):
|
||||
trade_date: str
|
||||
total_net_inflow_hkd_billion: float
|
||||
precision: Precision
|
||||
|
||||
|
||||
class HistorySummary(BaseModel):
|
||||
cumulative_net_inflow_hkd_billion: float
|
||||
trading_day_count: int
|
||||
max_single_day_inflow_hkd_billion: float
|
||||
max_single_day_outflow_hkd_billion: float
|
||||
longest_inflow_streak: int
|
||||
longest_outflow_streak: int
|
||||
|
||||
|
||||
class HistoryResponse(BaseModel):
|
||||
start_date: str
|
||||
daily: list[StatPoint]
|
||||
weekly: list[StatPoint]
|
||||
monthly: list[StatPoint]
|
||||
cumulative: list[StatPoint]
|
||||
benchmark_history: dict[str, list[StatPoint]]
|
||||
recent_trade_days: list[RecentTradeDay]
|
||||
summary: HistorySummary
|
||||
|
||||
|
||||
class RuleItem(BaseModel):
|
||||
key: str
|
||||
label: str
|
||||
value: str
|
||||
description: str
|
||||
|
||||
|
||||
class RulesResponse(BaseModel):
|
||||
items: list[RuleItem]
|
||||
|
||||
|
||||
class SourceDiagnosticsResponse(BaseModel):
|
||||
source_name: str
|
||||
realtime_available: bool
|
||||
historical_available: bool
|
||||
last_success_at: str | None = None
|
||||
last_failure_at: str | None = None
|
||||
last_error_reason: str | None = None
|
||||
last_success_url: str | None = None
|
||||
last_persisted_at: str | None = None
|
||||
|
||||
|
||||
class MetaResponse(BaseModel):
|
||||
product_name: str
|
||||
version: str
|
||||
timezone: str
|
||||
market_state: MarketState
|
||||
current_trade_date: str
|
||||
source_name: str
|
||||
source_strategy: str
|
||||
note: str
|
||||
|
||||
|
||||
class AShareFlowRecord(BaseModel):
|
||||
trade_date: str
|
||||
code: str
|
||||
name: str
|
||||
detail_url: str | None = None
|
||||
latest_price: float | None = None
|
||||
change_amount: float | None = None
|
||||
change_percent: float | None = None
|
||||
main_net_inflow: float | None = None
|
||||
main_net_inflow_ratio: float | None = None
|
||||
super_large_net_inflow: float | None = None
|
||||
super_large_net_inflow_ratio: float | None = None
|
||||
large_net_inflow: float | None = None
|
||||
large_net_inflow_ratio: float | None = None
|
||||
medium_net_inflow: float | None = None
|
||||
medium_net_inflow_ratio: float | None = None
|
||||
small_net_inflow: float | None = None
|
||||
small_net_inflow_ratio: float | None = None
|
||||
rolling_net_inflow_5d: float | None = None
|
||||
rolling_net_inflow_10d: float | None = None
|
||||
rolling_net_inflow_30d: float | None = None
|
||||
rolling_net_inflow_60d: float | None = None
|
||||
rolling_net_inflow_90d: float | None = None
|
||||
updated_at: str | None = None
|
||||
source_name: str
|
||||
source_url: str | None = None
|
||||
precision: Precision
|
||||
snapshot_time: str | None = None
|
||||
sector_type: str | None = None
|
||||
sector_type_label: str | None = None
|
||||
|
||||
|
||||
class AShareSectorGroup(BaseModel):
|
||||
label: str
|
||||
records: list[AShareFlowRecord]
|
||||
|
||||
|
||||
class AShareIndexFlowResponse(BaseModel):
|
||||
trade_date: str
|
||||
updated_at: str | None = None
|
||||
source_name: str
|
||||
source_url: str | None = None
|
||||
precision: Precision
|
||||
records: list[AShareFlowRecord]
|
||||
|
||||
|
||||
class AShareSectorFlowResponse(BaseModel):
|
||||
trade_date: str
|
||||
updated_at: str | None = None
|
||||
source_name: str
|
||||
source_url: str | None = None
|
||||
precision: Precision
|
||||
sector_types: dict[str, AShareSectorGroup]
|
||||
Reference in New Issue
Block a user