1use std::collections::HashMap;
5use std::str::FromStr;
6
7use rust_decimal::{Decimal, RoundingStrategy};
8
9use crate::models::{ClosingPrice, Share};
10
11#[derive(Debug, Clone)]
13pub struct AdjustEvent {
14 pub kind: String, pub increase_pct: Option<String>,
16 pub old_shares: Option<String>,
17 pub new_shares: Option<String>,
18 pub dividend: Option<String>,
19 pub price_before_event: String,
20 pub price_after_event: String,
21 pub date: String,
22}
23
24#[derive(Debug, Clone, Default)]
26pub struct AdjustInfo {
27 pub events: Vec<AdjustEvent>,
28 pub valid_gpl_ratio: Option<bool>,
29}
30
31#[derive(Debug, Clone)]
33pub struct AdjustResult {
34 pub prices: Option<Vec<ClosingPrice>>,
35 pub info: Option<AdjustInfo>,
36}
37
38fn dec(s: &str) -> Decimal {
39 Decimal::from_str(s).unwrap_or(Decimal::ZERO)
40}
41
42fn round_fixed(v: Decimal, places: u32) -> String {
43 let r = v.round_dp_with_strategy(places, RoundingStrategy::MidpointNearestEven);
45 format!("{:.*}", places as usize, r)
46}
47
48fn round_int(v: Decimal) -> String {
49 let r = v.round_dp_with_strategy(0, RoundingStrategy::MidpointNearestEven);
50 r.normalize().to_string()
52}
53
54pub fn adjust(
58 cond: u8,
59 closing_prices: &[ClosingPrice],
60 shares: &HashMap<String, Share>,
61 get_info: bool,
62 get_info_only: bool,
63) -> AdjustResult {
64 let cp = closing_prices;
65 let len = cp.len();
66 let should_get_info = get_info || get_info_only;
67
68 let mut info = AdjustInfo::default();
69
70 let default_prices = if get_info_only {
72 None
73 } else {
74 Some(cp.to_vec())
75 };
76 let mut result = AdjustResult {
77 prices: default_prices.clone(),
78 info: if should_get_info {
79 Some(info.clone())
80 } else {
81 None
82 },
83 };
84
85 if !((cond == 1 || cond == 2 || should_get_info) && len > 1) {
86 return result;
87 }
88
89 let mut gaps = Decimal::ZERO;
90 let mut coef = Decimal::ONE;
91 let mut adjusted: Vec<ClosingPrice> = Vec::with_capacity(len);
92 adjusted.push(cp[len - 1].clone());
93
94 if cond == 1 || should_get_info {
95 for i in (0..=len - 2).rev() {
96 let curr = &cp[i];
97 let next = &cp[i + 1];
98 if dec(&curr.pclosing) != dec(&next.price_yesterday) && curr.ins_code == next.ins_code {
99 gaps += Decimal::ONE;
100 }
101 }
102 }
103
104 let gaps_to_lifespan_ratio = gaps / Decimal::from(len as u64);
105 let has_valid_ratio = gaps_to_lifespan_ratio < dec("0.08");
106 info.valid_gpl_ratio = Some(has_valid_ratio);
107
108 if !((cond == 1 && has_valid_ratio) || cond == 2 || should_get_info) {
109 result.info = if should_get_info { Some(info) } else { None };
111 return result;
112 }
113
114 for i in (0..=len - 2).rev() {
115 let curr = &cp[i];
116 let next = &cp[i + 1];
117 let prices_dont_match =
118 dec(&curr.pclosing) != dec(&next.price_yesterday) && curr.ins_code == next.ins_code;
119 let target_share = shares.get(&next.deven);
120
121 if should_get_info && prices_dont_match && (has_valid_ratio || target_share.is_some()) {
122 let price_before_event = curr.pclosing.clone();
123 let price_after_event = next.price_yesterday.clone();
124 let date = curr.deven.clone();
125
126 let event = if let Some(ts) = target_share {
127 let old_shares = Decimal::from(ts.number_of_share_old);
128 let new_shares = Decimal::from(ts.number_of_share_new);
129 let increase_pct = if old_shares.is_zero() {
130 Decimal::ZERO
131 } else {
132 (new_shares - old_shares) / old_shares
133 };
134 AdjustEvent {
135 kind: "capital increase".to_string(),
136 increase_pct: Some(increase_pct.normalize().to_string()),
137 old_shares: Some(ts.number_of_share_old.to_string()),
138 new_shares: Some(ts.number_of_share_new.to_string()),
139 dividend: None,
140 price_before_event,
141 price_after_event,
142 date,
143 }
144 } else {
145 let dividend = dec(&price_before_event) - dec(&price_after_event);
146 AdjustEvent {
147 kind: "dividend".to_string(),
148 increase_pct: None,
149 old_shares: None,
150 new_shares: None,
151 dividend: Some(dividend.normalize().to_string()),
152 price_before_event,
153 price_after_event,
154 date,
155 }
156 };
157 info.events.push(event);
158 }
159
160 if get_info_only {
161 continue;
162 }
163
164 if cond == 1 && prices_dont_match {
165 coef = coef * dec(&next.price_yesterday) / dec(&curr.pclosing);
166 } else if cond == 2 && prices_dont_match {
167 if let Some(ts) = target_share {
168 let old_shares = Decimal::from(ts.number_of_share_old);
169 let new_shares = Decimal::from(ts.number_of_share_new);
170 coef = coef * old_shares / new_shares;
171 }
172 }
173
174 let close = round_fixed(coef * dec(&curr.pclosing), 2);
175 let last = round_fixed(coef * dec(&curr.pdr_cot_val), 2);
176 let low = round_int(coef * dec(&curr.price_min));
177 let high = round_int(coef * dec(&curr.price_max));
178 let yday = round_int(coef * dec(&curr.price_yesterday));
179 let first = round_fixed(coef * dec(&curr.price_first), 2);
180
181 adjusted.push(ClosingPrice {
182 ins_code: curr.ins_code.clone(),
183 deven: curr.deven.clone(),
184 pclosing: close,
185 pdr_cot_val: last,
186 ztot_tran: curr.ztot_tran.clone(),
187 qtot_tran5j: curr.qtot_tran5j.clone(),
188 qtot_cap: curr.qtot_cap.clone(),
189 price_min: low,
190 price_max: high,
191 price_yesterday: yday,
192 price_first: first,
193 });
194 }
195
196 adjusted.reverse();
197
198 AdjustResult {
199 prices: if get_info_only { None } else { Some(adjusted) },
200 info: if should_get_info { Some(info) } else { None },
201 }
202}