1use std::sync::Arc;
2
3use longbridge_httpcli::{HttpClient, Json, Method};
4use serde::{Deserialize, Serialize};
5use tracing::{Subscriber, dispatcher, instrument::WithSubscriber};
6
7use crate::{
8 Config, Result,
9 fund::{
10 FundAnalysis, FundAnalysisDetail, FundAnnualReturn, FundBrief, FundDetail, FundDividends,
11 FundFilters, FundHoldings, FundNavRangeOptions, FundNavValue, FundOrder, FundOrderDetail,
12 FundOrderSubmitResponse, FundOrderValidation, FundPageOptions, FundPerformance,
13 FundPerformanceComparison, FundPositionDetail, FundPositionNav, FundPositionPerformance,
14 FundPositionProfits, FundPositions, FundQuarterlyReturn, FundStockHolding, FundTransaction,
15 FundTrend, GetFundAnalysisOptions, GetFundHoldingsOptions, GetFundOrdersOptions,
16 GetFundPositionDividendsOptions, GetFundPositionOptions, GetFundPositionProfitsOptions,
17 GetFundPositionsOptions, GetFundStockHoldingsOptions, GetFundTransactionsOptions,
18 GetFundsOptions, HotFund, SubmitFundOrderOptions, ValidateFundOrderOptions,
19 },
20};
21
22#[derive(Debug, Deserialize)]
23struct WList<T> {
24 #[serde(default = "Vec::new")]
25 list: Vec<T>,
26}
27
28#[derive(Debug, Deserialize)]
29struct WFunds {
30 #[serde(default)]
31 funds: Vec<FundBrief>,
32}
33
34#[derive(Debug, Deserialize)]
35struct WValue<T> {
36 #[serde(default = "Vec::new")]
37 value: Vec<T>,
38}
39
40#[derive(Debug, Deserialize)]
41struct WHistory<T> {
42 #[serde(default = "Vec::new")]
43 history_value: Vec<T>,
44}
45
46#[derive(Debug, Deserialize)]
47struct WLists<T> {
48 #[serde(default = "Vec::new")]
49 lists: Vec<T>,
50}
51
52#[derive(Debug, Deserialize)]
53struct WOrders {
54 #[serde(default)]
55 orders: Vec<FundOrder>,
56}
57
58#[derive(Serialize)]
63struct CounterIdQuery<T> {
64 counter_id: String,
65 #[serde(flatten)]
66 options: T,
67}
68
69#[derive(Serialize, Default)]
71struct NoQuery {}
72
73#[derive(Serialize)]
79struct CounterIdsQuery {
80 counter_ids: String,
81}
82
83impl CounterIdsQuery {
84 fn single(counter_id: String) -> Self {
85 Self {
86 counter_ids: serde_json::to_string(&[counter_id]).expect("serialize counter_ids array"),
87 }
88 }
89}
90
91struct InnerFundContext {
92 http_cli: HttpClient,
93 log_subscriber: Arc<dyn Subscriber + Send + Sync>,
94}
95
96impl Drop for InnerFundContext {
97 fn drop(&mut self) {
98 dispatcher::with_default(&self.log_subscriber.clone().into(), || {
99 tracing::info!("fund context dropped");
100 });
101 }
102}
103
104#[derive(Clone)]
106pub struct FundContext(Arc<InnerFundContext>);
107
108impl FundContext {
109 pub fn new(config: Arc<Config>) -> Self {
111 let log_subscriber = config.create_log_subscriber("fund");
112 let ctx = Self(Arc::new(InnerFundContext {
113 http_cli: config.create_http_client(),
114 log_subscriber,
115 }));
116 dispatcher::with_default(&ctx.0.log_subscriber.clone().into(), || {
117 tracing::info!("fund context created");
118 });
119 ctx
120 }
121
122 #[inline]
124 pub fn log_subscriber(&self) -> Arc<dyn Subscriber + Send + Sync> {
125 self.0.log_subscriber.clone()
126 }
127
128 pub async fn hot_funds(&self) -> Result<Vec<HotFund>> {
132 Ok(self
133 .0
134 .http_cli
135 .request(Method::GET, "/v1/fund/hot-funds")
136 .response::<Json<WList<HotFund>>>()
137 .send()
138 .with_subscriber(self.0.log_subscriber.clone())
139 .await?
140 .0
141 .list)
142 }
143
144 pub async fn funds(
146 &self,
147 options: impl Into<Option<GetFundsOptions>>,
148 ) -> Result<Vec<FundBrief>> {
149 Ok(self
150 .0
151 .http_cli
152 .request(Method::POST, "/v1/fund/funds")
153 .body(Json(options.into().unwrap_or_default()))
154 .response::<Json<WFunds>>()
155 .send()
156 .with_subscriber(self.0.log_subscriber.clone())
157 .await?
158 .0
159 .funds)
160 }
161
162 pub async fn filters(&self) -> Result<FundFilters> {
164 Ok(self
165 .0
166 .http_cli
167 .request(Method::GET, "/v1/fund/filters")
168 .response::<Json<FundFilters>>()
169 .send()
170 .with_subscriber(self.0.log_subscriber.clone())
171 .await?
172 .0)
173 }
174
175 pub async fn detail(&self, counter_id: impl Into<String>) -> Result<FundDetail> {
177 Ok(self
178 .0
179 .http_cli
180 .request(Method::GET, "/v1/fund/funds/detail")
181 .query_params(CounterIdQuery {
182 counter_id: counter_id.into(),
183 options: NoQuery {},
184 })
185 .response::<Json<FundDetail>>()
186 .send()
187 .with_subscriber(self.0.log_subscriber.clone())
188 .await?
189 .0)
190 }
191
192 pub async fn analysis(
194 &self,
195 counter_id: impl Into<String>,
196 options: impl Into<Option<GetFundAnalysisOptions>>,
197 ) -> Result<FundAnalysis> {
198 Ok(self
199 .0
200 .http_cli
201 .request(Method::GET, "/v1/fund/funds/analysis")
202 .query_params(CounterIdQuery {
203 counter_id: counter_id.into(),
204 options: options.into().unwrap_or_default(),
205 })
206 .response::<Json<FundAnalysis>>()
207 .send()
208 .with_subscriber(self.0.log_subscriber.clone())
209 .await?
210 .0)
211 }
212
213 pub async fn analysis_detail(
215 &self,
216 counter_id: impl Into<String>,
217 options: impl Into<Option<GetFundAnalysisOptions>>,
218 ) -> Result<FundAnalysisDetail> {
219 Ok(self
220 .0
221 .http_cli
222 .request(Method::GET, "/v1/fund/funds/analysis/detail")
223 .query_params(CounterIdQuery {
224 counter_id: counter_id.into(),
225 options: options.into().unwrap_or_default(),
226 })
227 .response::<Json<FundAnalysisDetail>>()
228 .send()
229 .with_subscriber(self.0.log_subscriber.clone())
230 .await?
231 .0)
232 }
233
234 pub async fn trend(
236 &self,
237 counter_id: impl Into<String>,
238 options: impl Into<Option<GetFundAnalysisOptions>>,
239 ) -> Result<FundTrend> {
240 Ok(self
241 .0
242 .http_cli
243 .request(Method::GET, "/v1/fund/funds/trend")
244 .query_params(CounterIdQuery {
245 counter_id: counter_id.into(),
246 options: options.into().unwrap_or_default(),
247 })
248 .response::<Json<FundTrend>>()
249 .send()
250 .with_subscriber(self.0.log_subscriber.clone())
251 .await?
252 .0)
253 }
254
255 pub async fn annual_returns(
257 &self,
258 counter_id: impl Into<String>,
259 options: impl Into<Option<FundPageOptions>>,
260 ) -> Result<Vec<FundAnnualReturn>> {
261 Ok(self
262 .0
263 .http_cli
264 .request(Method::GET, "/v1/fund/funds/returns/annual")
265 .query_params(CounterIdQuery {
266 counter_id: counter_id.into(),
267 options: options.into().unwrap_or_default(),
268 })
269 .response::<Json<WList<FundAnnualReturn>>>()
270 .send()
271 .with_subscriber(self.0.log_subscriber.clone())
272 .await?
273 .0
274 .list)
275 }
276
277 pub async fn quarterly_returns(
279 &self,
280 counter_id: impl Into<String>,
281 options: impl Into<Option<FundPageOptions>>,
282 ) -> Result<Vec<FundQuarterlyReturn>> {
283 Ok(self
284 .0
285 .http_cli
286 .request(Method::GET, "/v1/fund/funds/returns/quarterly")
287 .query_params(CounterIdQuery {
288 counter_id: counter_id.into(),
289 options: options.into().unwrap_or_default(),
290 })
291 .response::<Json<WList<FundQuarterlyReturn>>>()
292 .send()
293 .with_subscriber(self.0.log_subscriber.clone())
294 .await?
295 .0
296 .list)
297 }
298
299 pub async fn performance(&self, counter_id: impl Into<String>) -> Result<Vec<FundPerformance>> {
301 Ok(self
302 .0
303 .http_cli
304 .request(Method::GET, "/v1/fund/funds/performance")
305 .query_params(CounterIdsQuery::single(counter_id.into()))
306 .response::<Json<WValue<FundPerformance>>>()
307 .send()
308 .with_subscriber(self.0.log_subscriber.clone())
309 .await?
310 .0
311 .value)
312 }
313
314 pub async fn performance_comparison(
316 &self,
317 counter_id: impl Into<String>,
318 options: impl Into<Option<GetFundAnalysisOptions>>,
319 ) -> Result<FundPerformanceComparison> {
320 Ok(self
321 .0
322 .http_cli
323 .request(Method::GET, "/v1/fund/funds/performance/comparison")
324 .query_params(CounterIdQuery {
325 counter_id: counter_id.into(),
326 options: options.into().unwrap_or_default(),
327 })
328 .response::<Json<FundPerformanceComparison>>()
329 .send()
330 .with_subscriber(self.0.log_subscriber.clone())
331 .await?
332 .0)
333 }
334
335 pub async fn nav(&self, counter_id: impl Into<String>) -> Result<Vec<FundNavValue>> {
337 Ok(self
338 .0
339 .http_cli
340 .request(Method::GET, "/v1/fund/funds/nav")
341 .query_params(CounterIdsQuery::single(counter_id.into()))
342 .response::<Json<WValue<FundNavValue>>>()
343 .send()
344 .with_subscriber(self.0.log_subscriber.clone())
345 .await?
346 .0
347 .value)
348 }
349
350 pub async fn nav_history(
352 &self,
353 counter_id: impl Into<String>,
354 options: impl Into<Option<FundPageOptions>>,
355 ) -> Result<Vec<FundNavValue>> {
356 Ok(self
357 .0
358 .http_cli
359 .request(Method::GET, "/v1/fund/funds/nav-history")
360 .query_params(CounterIdQuery {
361 counter_id: counter_id.into(),
362 options: options.into().unwrap_or_default(),
363 })
364 .response::<Json<WHistory<FundNavValue>>>()
365 .send()
366 .with_subscriber(self.0.log_subscriber.clone())
367 .await?
368 .0
369 .history_value)
370 }
371
372 pub async fn nav_range(
374 &self,
375 counter_id: impl Into<String>,
376 options: impl Into<Option<FundNavRangeOptions>>,
377 ) -> Result<Vec<FundNavValue>> {
378 Ok(self
379 .0
380 .http_cli
381 .request(Method::GET, "/v1/fund/funds/nav-range")
382 .query_params(CounterIdQuery {
383 counter_id: counter_id.into(),
384 options: options.into().unwrap_or_default(),
385 })
386 .response::<Json<WHistory<FundNavValue>>>()
387 .send()
388 .with_subscriber(self.0.log_subscriber.clone())
389 .await?
390 .0
391 .history_value)
392 }
393
394 pub async fn holdings(
396 &self,
397 counter_id: impl Into<String>,
398 options: impl Into<Option<GetFundHoldingsOptions>>,
399 ) -> Result<FundHoldings> {
400 Ok(self
401 .0
402 .http_cli
403 .request(Method::GET, "/v1/fund/funds/holdings")
404 .query_params(CounterIdQuery {
405 counter_id: counter_id.into(),
406 options: options.into().unwrap_or_default(),
407 })
408 .response::<Json<FundHoldings>>()
409 .send()
410 .with_subscriber(self.0.log_subscriber.clone())
411 .await?
412 .0)
413 }
414
415 pub async fn stock_holdings(
417 &self,
418 counter_id: impl Into<String>,
419 options: impl Into<Option<GetFundStockHoldingsOptions>>,
420 ) -> Result<Vec<FundStockHolding>> {
421 Ok(self
422 .0
423 .http_cli
424 .request(Method::GET, "/v1/fund/funds/stock-holdings")
425 .query_params(CounterIdQuery {
426 counter_id: counter_id.into(),
427 options: options.into().unwrap_or_default(),
428 })
429 .response::<Json<WLists<FundStockHolding>>>()
430 .send()
431 .with_subscriber(self.0.log_subscriber.clone())
432 .await?
433 .0
434 .lists)
435 }
436
437 pub async fn positions(
441 &self,
442 options: impl Into<Option<GetFundPositionsOptions>>,
443 ) -> Result<FundPositions> {
444 Ok(self
445 .0
446 .http_cli
447 .request(Method::GET, "/v1/asset/funds")
448 .query_params(options.into().unwrap_or_default())
449 .response::<Json<FundPositions>>()
450 .send()
451 .with_subscriber(self.0.log_subscriber.clone())
452 .await?
453 .0)
454 }
455
456 pub async fn position(
458 &self,
459 counter_id: impl Into<String>,
460 options: impl Into<Option<GetFundPositionOptions>>,
461 ) -> Result<FundPositionDetail> {
462 Ok(self
463 .0
464 .http_cli
465 .request(Method::GET, "/v1/asset/funds/detail")
466 .query_params(CounterIdQuery {
467 counter_id: counter_id.into(),
468 options: options.into().unwrap_or_default(),
469 })
470 .response::<Json<FundPositionDetail>>()
471 .send()
472 .with_subscriber(self.0.log_subscriber.clone())
473 .await?
474 .0)
475 }
476
477 pub async fn position_performance(
479 &self,
480 counter_id: impl Into<String>,
481 ) -> Result<Vec<FundPositionPerformance>> {
482 Ok(self
483 .0
484 .http_cli
485 .request(Method::GET, "/v1/asset/funds/performance")
486 .query_params(CounterIdsQuery::single(counter_id.into()))
487 .response::<Json<WValue<FundPositionPerformance>>>()
488 .send()
489 .with_subscriber(self.0.log_subscriber.clone())
490 .await?
491 .0
492 .value)
493 }
494
495 pub async fn position_profits(
497 &self,
498 counter_id: impl Into<String>,
499 options: impl Into<Option<GetFundPositionProfitsOptions>>,
500 ) -> Result<FundPositionProfits> {
501 Ok(self
502 .0
503 .http_cli
504 .request(Method::GET, "/v1/asset/funds/profits")
505 .query_params(CounterIdQuery {
506 counter_id: counter_id.into(),
507 options: options.into().unwrap_or_default(),
508 })
509 .response::<Json<FundPositionProfits>>()
510 .send()
511 .with_subscriber(self.0.log_subscriber.clone())
512 .await?
513 .0)
514 }
515
516 pub async fn position_nav(
518 &self,
519 counter_id: impl Into<String>,
520 options: impl Into<Option<FundNavRangeOptions>>,
521 ) -> Result<Vec<FundPositionNav>> {
522 Ok(self
523 .0
524 .http_cli
525 .request(Method::GET, "/v1/asset/funds/nav-history")
526 .query_params(CounterIdQuery {
527 counter_id: counter_id.into(),
528 options: options.into().unwrap_or_default(),
529 })
530 .response::<Json<WHistory<FundPositionNav>>>()
531 .send()
532 .with_subscriber(self.0.log_subscriber.clone())
533 .await?
534 .0
535 .history_value)
536 }
537
538 pub async fn position_dividends(
540 &self,
541 counter_id: impl Into<String>,
542 options: impl Into<Option<GetFundPositionDividendsOptions>>,
543 ) -> Result<FundDividends> {
544 Ok(self
545 .0
546 .http_cli
547 .request(Method::GET, "/v1/asset/funds/dividends")
548 .query_params(CounterIdQuery {
549 counter_id: counter_id.into(),
550 options: options.into().unwrap_or_default(),
551 })
552 .response::<Json<FundDividends>>()
553 .send()
554 .with_subscriber(self.0.log_subscriber.clone())
555 .await?
556 .0)
557 }
558
559 pub async fn orders(
563 &self,
564 options: impl Into<Option<GetFundOrdersOptions>>,
565 ) -> Result<Vec<FundOrder>> {
566 Ok(self
567 .0
568 .http_cli
569 .request(Method::GET, "/v1/fund/orders")
570 .query_params(options.into().unwrap_or_default())
571 .response::<Json<WOrders>>()
572 .send()
573 .with_subscriber(self.0.log_subscriber.clone())
574 .await?
575 .0
576 .orders)
577 }
578
579 pub async fn order(&self, order_id: i64) -> Result<FundOrderDetail> {
581 Ok(self
582 .0
583 .http_cli
584 .request(Method::GET, format!("/v1/fund/orders/{order_id}"))
585 .response::<Json<FundOrderDetail>>()
586 .send()
587 .with_subscriber(self.0.log_subscriber.clone())
588 .await?
589 .0)
590 }
591
592 pub async fn transactions(
594 &self,
595 options: impl Into<Option<GetFundTransactionsOptions>>,
596 ) -> Result<Vec<FundTransaction>> {
597 Ok(self
598 .0
599 .http_cli
600 .request(Method::GET, "/v1/fund/transactions")
601 .query_params(options.into().unwrap_or_default())
602 .response::<Json<WList<FundTransaction>>>()
603 .send()
604 .with_subscriber(self.0.log_subscriber.clone())
605 .await?
606 .0
607 .list)
608 }
609
610 pub async fn validate_order(
612 &self,
613 options: ValidateFundOrderOptions,
614 ) -> Result<FundOrderValidation> {
615 Ok(self
616 .0
617 .http_cli
618 .request(Method::POST, "/v1/fund/orders/validate")
619 .body(Json(options))
620 .response::<Json<FundOrderValidation>>()
621 .send()
622 .with_subscriber(self.0.log_subscriber.clone())
623 .await?
624 .0)
625 }
626
627 pub async fn submit_order(
629 &self,
630 options: SubmitFundOrderOptions,
631 ) -> Result<FundOrderSubmitResponse> {
632 Ok(self
633 .0
634 .http_cli
635 .request(Method::POST, "/v1/fund/orders")
636 .body(Json(options))
637 .response::<Json<FundOrderSubmitResponse>>()
638 .send()
639 .with_subscriber(self.0.log_subscriber.clone())
640 .await?
641 .0)
642 }
643
644 pub async fn cancel_order(&self, order_id: i64) -> Result<()> {
646 #[derive(Debug, serde::Serialize)]
647 struct Req {
648 ut_id: i64,
649 }
650 #[derive(Deserialize)]
651 struct Resp {
652 #[serde(default)]
653 #[allow(dead_code)]
654 msg: String,
655 }
656 self.0
657 .http_cli
658 .request(Method::POST, format!("/v1/fund/orders/{order_id}/cancel"))
659 .body(Json(Req { ut_id: order_id }))
660 .response::<Json<Resp>>()
661 .send()
662 .with_subscriber(self.0.log_subscriber.clone())
663 .await?;
664 Ok(())
665 }
666}