Skip to content

Commit

Permalink
Support Subscribable Rights Issue corporate action (#67, #64)
Browse files Browse the repository at this point in the history
  • Loading branch information
KonishchevDmitry committed Apr 29, 2022
1 parent 511d84c commit 52ee902
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 47 deletions.
71 changes: 39 additions & 32 deletions src/broker_statement/corporate_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,17 @@ impl CorporateAction {
#[cfg_attr(test, derive(PartialEq))]
#[serde(tag = "type", rename_all="kebab-case")]
pub enum CorporateActionType {
StockSplit {
ratio: StockSplitRatio,

#[serde(skip)]
from_change: Option<Decimal>,
// See https://github.com/KonishchevDmitry/investments/issues/29 for details
Rename {
new_symbol: String,
},

#[serde(skip)]
to_change: Option<Decimal>,
// See https://github.com/KonishchevDmitry/investments/issues/20 for details
#[serde(skip)]
Spinoff {
symbol: String,
quantity: Decimal,
currency: String,
},

// There are two types of stock dividend (see https://github.com/KonishchevDmitry/investments/issues/27#issuecomment-802212517)
Expand All @@ -65,18 +68,20 @@ pub enum CorporateActionType {
quantity: Decimal,
},

// See https://github.com/KonishchevDmitry/investments/issues/29 for details
Rename {
new_symbol: String,
StockSplit {
ratio: StockSplitRatio,

#[serde(skip)]
from_change: Option<Decimal>,

#[serde(skip)]
to_change: Option<Decimal>,
},

// See https://github.com/KonishchevDmitry/investments/issues/20 for details
// Allows existing shareholders to purchase shares of a secondary offering, usually at a
// discounted price. Doesn't affects anything, so can be ignored.
#[serde(skip)]
Spinoff {
symbol: String,
quantity: Decimal,
currency: String,
}
SubscribableRightsIssue,
}

#[derive(Clone, Copy, PartialEq, Debug)]
Expand Down Expand Up @@ -199,37 +204,39 @@ pub fn process_corporate_actions(statement: &mut BrokerStatement) -> EmptyResult

fn process_corporate_action(statement: &mut BrokerStatement, action: CorporateAction) -> EmptyResult {
match action.action {
CorporateActionType::StockSplit {ratio, from_change, to_change} => {
process_stock_split(
statement, action.time, &action.symbol,
ratio, from_change, to_change,
).map_err(|e| format!(
"Failed to process {} stock split from {}: {}",
action.symbol, format_date(action.time), e,
))?;
CorporateActionType::Rename {ref new_symbol} => {
statement.rename_symbol(&action.symbol, new_symbol, Some(action.time)).map_err(|e| format!(
"Failed to process {} -> {} rename corporate action: {}",
action.symbol, new_symbol, e))?;
},

CorporateActionType::StockDividend {quantity} => {
CorporateActionType::Spinoff {ref symbol, quantity, ..} => {
statement.stock_buys.push(StockBuy::new_corporate_action(
&action.symbol, quantity, PurchaseTotalCost::new(),
symbol, quantity, PurchaseTotalCost::new(),
action.time, action.execution_date(),
));
statement.sort_and_validate_stock_buys()?;
},

CorporateActionType::Spinoff {ref symbol, quantity, ..} => {
CorporateActionType::StockDividend {quantity} => {
statement.stock_buys.push(StockBuy::new_corporate_action(
symbol, quantity, PurchaseTotalCost::new(),
&action.symbol, quantity, PurchaseTotalCost::new(),
action.time, action.execution_date(),
));
statement.sort_and_validate_stock_buys()?;
},

CorporateActionType::Rename {ref new_symbol} => {
statement.rename_symbol(&action.symbol, new_symbol, Some(action.time)).map_err(|e| format!(
"Failed to process {} -> {} rename corporate action: {}",
action.symbol, new_symbol, e))?;
CorporateActionType::StockSplit {ratio, from_change, to_change} => {
process_stock_split(
statement, action.time, &action.symbol,
ratio, from_change, to_change,
).map_err(|e| format!(
"Failed to process {} stock split from {}: {}",
action.symbol, format_date(action.time), e,
))?;
},

CorporateActionType::SubscribableRightsIssue {} => {},
};

statement.corporate_actions.push(action);
Expand Down
61 changes: 47 additions & 14 deletions src/broker_statement/ib/corporate_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,17 @@ fn parse(record: &Record) -> GenericResult<CorporateAction> {

lazy_static! {
static ref REGEX: Regex = Regex::new(&format!(concat!(
r"^(?P<symbol>{symbol}) ?\({id}\) (?P<action>Split|Stock Dividend|Spinoff) ",
r"^(?P<symbol>{symbol}) ?\({id}\) ",
r"(?P<action>Spinoff|Split|Stock Dividend|Subscribable Rights Issue) ",
r"(?:{id} )?(?P<to>[1-9]\d*) for (?P<from>[1-9]\d*) ",
r"\((?P<other_symbol>{symbol})(?:{old_suffix})?, [^,)]+, {id}\)$"),

// The secondary symbol and its name may be suffixed with the following abbreviations:
// * RT, RTS - rights (subscribable rights)
// * WI, W/I - when issued
//
// See the examples in tests below.
r"\((?P<other_symbol>{symbol})(?:{old_suffix})?(?: (?:RT|WI))*, [^,)]+, {id}\)$"),

symbol=common::STOCK_SYMBOL_REGEX, old_suffix=regex::escape(common::OLD_SYMBOL_SUFFIX),
id=SecurityID::REGEX)).unwrap();
}
Expand All @@ -97,6 +105,16 @@ fn parse(record: &Record) -> GenericResult<CorporateAction> {
let other_symbol = parse_symbol(captures.name("other_symbol").unwrap().as_str())?;

let action = match captures.name("action").unwrap().as_str() {
"Spinoff" => {
let quantity = record.parse_quantity("Quantity", DecimalRestrictions::StrictlyPositive)?;
let currency = record.get_value("Currency")?.to_owned();

CorporateActionType::Spinoff {
symbol: other_symbol,
quantity, currency,
}
},

"Split" => {
if other_symbol != symbol {
return error();
Expand Down Expand Up @@ -125,15 +143,7 @@ fn parse(record: &Record) -> GenericResult<CorporateAction> {
CorporateActionType::StockDividend {quantity}
},

"Spinoff" => {
let quantity = record.parse_quantity("Quantity", DecimalRestrictions::StrictlyPositive)?;
let currency = record.get_value("Currency")?.to_owned();

CorporateActionType::Spinoff {
symbol: other_symbol,
quantity, currency,
}
},
"Subscribable Rights Issue" => CorporateActionType::SubscribableRightsIssue,

_ => unreachable!(),
};
Expand Down Expand Up @@ -220,7 +230,7 @@ mod tests {
"13.3333", "0", "0", "0", "",
], "VISL", date_time!(2020, 7, 31, 20, 25, 00), date!(2020, 8, 3), 1, 6, None, Some(dec!(13.3333))),
)]
fn stock_split_parsing(
fn stock_split(
record: &[&str], symbol: &str, time: DateTime, report_date: Date, to: u32, from: u32,
from_change: Option<Decimal>, to_change: Option<Decimal>,
) {
Expand All @@ -237,7 +247,7 @@ mod tests {
}

#[test]
fn stock_dividend_parsing() {
fn stock_dividend() {
test_parsing(&[
"Stocks", "USD", "2020-07-17", "2020-07-17, 20:20:00",
"TEF (US8793822086) Stock Dividend US8793822086 416666667 for 10000000000 (TEF, TELEFONICA SA-SPON ADR, US8793822086)",
Expand All @@ -252,7 +262,7 @@ mod tests {
}

#[test]
fn spinoff_parsing() {
fn spinoff() {
test_parsing(&[
"Stocks", "USD", "2020-11-17", "2020-11-16, 20:25:00",
"PFE(US7170811035) Spinoff 124079 for 1000000 (VTRS, VIATRIS INC-W/I, US92556V1061)",
Expand All @@ -270,6 +280,29 @@ mod tests {
});
}

#[rstest(record, symbol, time, report_date,
case(&[
"Stocks", "USD", "2021-06-18", "2021-06-16, 20:25:00",
"BST(US09258G1040) Subscribable Rights Issue 1 for 1 (BST RT WI, BLACKROCK SCIENCE -RTS W/I, US09258G1123)",
"6", "0", "0", "0", "",
], "BST", date_time!(2021, 6, 16, 20, 25, 00), date!(2021, 6, 18)),
case(&[
"Stocks", "HKD", "2021-08-12", "2021-08-11, 20:25:00",
"698(KYG8917X1218) Subscribable Rights Issue 1 for 2 (2965, TONGDA GROUP HOLDINGS LTD - RIGHTS, KYG8917X1RTS)",
"25000", "0", "0", "0",
], "698", date_time!(2021, 8, 11, 20, 25, 00), date!(2021, 8, 12)),
)]
fn subscribable_rights_issue(record: &[&str], symbol: &str, time: DateTime, report_date: Date) {
test_parsing(record, CorporateAction {
time: time.into(),
report_date: Some(report_date),

symbol: symbol.to_owned(),
action: CorporateActionType::SubscribableRightsIssue,
});
}

fn test_parsing(record: &[&str], expected: CorporateAction) {
let fields =
"Asset Category,Currency,Report Date,Date/Time,Description,Quantity,Proceeds,Value,Realized P/L,Code"
Expand Down
2 changes: 1 addition & 1 deletion testdata
Submodule testdata updated from cb079f to a0fd5d

0 comments on commit 52ee902

Please sign in to comment.