futu_rest/routes/trd/
performance.rs1use 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;