Skip to main content

futu_rest/routes/trd/
performance.rs

1//! REST adapters for v1.8 customer+broker-area performance timelines.
2
3use std::sync::Arc;
4
5use axum::Json;
6use axum::extract::{Extension, State};
7use serde_json::Value;
8
9use futu_auth::KeyRecord;
10
11use crate::adapter::{self, RestState};
12
13use super::RawApiResult;
14use super::card_num::normalize_and_resolve_card_num_for_route;
15use super::validation::{
16    read_handler_acc_id_check, validate_header_trd_env_present, validate_header_trd_market,
17};
18
19async fn preflight(
20    state: &RestState,
21    rec: &Option<Extension<Arc<KeyRecord>>>,
22    body: &mut Value,
23    path: &'static str,
24) -> Result<(), (axum::http::StatusCode, Json<Value>)> {
25    normalize_and_resolve_card_num_for_route(state, rec, body, path)?;
26    validate_header_trd_market(body, path)?;
27    validate_header_trd_env_present(body, path)?;
28    read_handler_acc_id_check(
29        state,
30        rec.as_deref().map(|record| record.as_ref()),
31        body,
32        path,
33    )
34}
35
36fn validate_price_context_json(
37    body: &Value,
38    nested_in_range: bool,
39    path: &'static str,
40) -> Result<(), (axum::http::StatusCode, Json<Value>)> {
41    let context = if nested_in_range {
42        body.get("range")
43            .and_then(|range| range.get("price_context"))
44    } else {
45        body.get("price_context")
46    };
47    let Some(context) = context.and_then(Value::as_object) else {
48        return Err(performance_bad_request(path, "price_context is required"));
49    };
50    for field in ["use_pre_market", "use_after_hours", "use_overnight"] {
51        if !context.get(field).is_some_and(Value::is_boolean) {
52            return Err(performance_bad_request(
53                path,
54                &format!("price_context.{field} is required and must be boolean"),
55            ));
56        }
57    }
58    Ok(())
59}
60
61fn performance_bad_request(
62    path: &'static str,
63    message: &str,
64) -> (axum::http::StatusCode, Json<Value>) {
65    (
66        axum::http::StatusCode::BAD_REQUEST,
67        Json(serde_json::json!({"error": message, "endpoint": path})),
68    )
69}
70
71pub async fn asset_trend(
72    State(state): State<RestState>,
73    rec: Option<Extension<Arc<KeyRecord>>>,
74    Json(mut body): Json<Value>,
75) -> RawApiResult {
76    preflight(&state, &rec, &mut body, "/api/trd/asset-trend").await?;
77    validate_price_context_json(&body, true, "/api/trd/asset-trend")?;
78    adapter::proto_request_raw::<
79        futu_proto::trd_get_asset_trend::Request,
80        futu_proto::trd_get_asset_trend::Response,
81    >(&state, futu_core::proto_id::TRD_GET_ASSET_TREND, Some(body))
82    .await
83}
84
85pub async fn yield_trend(
86    State(state): State<RestState>,
87    rec: Option<Extension<Arc<KeyRecord>>>,
88    Json(mut body): Json<Value>,
89) -> RawApiResult {
90    preflight(&state, &rec, &mut body, "/api/trd/yield-trend").await?;
91    validate_price_context_json(&body, true, "/api/trd/yield-trend")?;
92    adapter::proto_request_raw::<
93        futu_proto::trd_get_yield_trend::Request,
94        futu_proto::trd_get_yield_trend::Response,
95    >(&state, futu_core::proto_id::TRD_GET_YIELD_TREND, Some(body))
96    .await
97}
98
99pub async fn return_calendar(
100    State(state): State<RestState>,
101    rec: Option<Extension<Arc<KeyRecord>>>,
102    Json(mut body): Json<Value>,
103) -> RawApiResult {
104    preflight(&state, &rec, &mut body, "/api/trd/return-calendar").await?;
105    validate_price_context_json(&body, false, "/api/trd/return-calendar")?;
106    adapter::proto_request_raw::<
107        futu_proto::trd_get_return_calendar::Request,
108        futu_proto::trd_get_return_calendar::Response,
109    >(
110        &state,
111        futu_core::proto_id::TRD_GET_RETURN_CALENDAR,
112        Some(body),
113    )
114    .await
115}
116
117#[cfg(test)]
118#[path = "performance_tests.rs"]
119mod tests;