From a0e2c146e4662256f5371ae1cc02d91191b4c67d Mon Sep 17 00:00:00 2001 From: Aaron Lindsay Date: Mon, 20 Mar 2017 21:07:58 -0400 Subject: [PATCH] Add requesting/parsing security list --- banking.go | 6 -- common.go | 6 ++ request.go | 5 +- response.go | 9 +- securities.go | 260 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 277 insertions(+), 9 deletions(-) create mode 100644 securities.go diff --git a/banking.go b/banking.go index 1c3ce51..48f12e9 100644 --- a/banking.go +++ b/banking.go @@ -131,12 +131,6 @@ type PendingTransactionList struct { Transactions []PendingTransaction `xml:"STMTTRNP,omitempty"` } -type Currency struct { - XMLName xml.Name // CURRENCY or ORIGCURRENCY - CurRate Amount `xml:"CURRATE"` // Ratio of currency to currency - CurSym String `xml:"CURSYM"` // ISO-4217 3-character currency identifier -} - type Balance struct { XMLName xml.Name `xml:"BAL"` Name String `xml:"NAME"` diff --git a/common.go b/common.go index 85131b5..ea114ae 100644 --- a/common.go +++ b/common.go @@ -48,3 +48,9 @@ type InvAcct struct { BrokerId String `xml:"BROKERID"` AcctId String `xml:"ACCTID"` } + +type Currency struct { + XMLName xml.Name // CURRENCY or ORIGCURRENCY + CurRate Amount `xml:"CURRATE"` // Ratio of currency to currency + CurSym String `xml:"CURSYM"` // ISO-4217 3-character currency identifier +} diff --git a/request.go b/request.go index 0425b67..c1c402d 100644 --- a/request.go +++ b/request.go @@ -19,7 +19,7 @@ type Request struct { // // // - // + Securities []Message // // // Profile []Message // @@ -109,6 +109,9 @@ NEWFILEUID:NONE if err := marshalMessageSet(encoder, oq.Investments, "INVSTMTMSGSRQV1"); err != nil { return nil, err } + if err := marshalMessageSet(encoder, oq.Securities, "SECLISTMSGSRQV1"); err != nil { + return nil, err + } if err := marshalMessageSet(encoder, oq.Profile, "PROFMSGSRQV1"); err != nil { return nil, err } diff --git a/response.go b/response.go index a8d5224..9268f6b 100644 --- a/response.go +++ b/response.go @@ -21,7 +21,7 @@ type Response struct { // // // - // + Securities []Message // // // Profile []Message // @@ -281,7 +281,12 @@ func ParseResponse(reader io.Reader) (*Response, error) { //case "WIREXFERMSGSRSV1": //case "BILLPAYMSGSRSV1": //case "EMAILMSGSRSV1": - //case "SECLISTMSGSRSV1": + case "SECLISTMSGSRSV1": + msgs, err := DecodeSecuritiesMessageSet(decoder, start) + if err != nil { + return nil, err + } + or.Securities = msgs //case "PRESDIRMSGSRSV1": //case "PRESDLVMSGSRSV1": case "PROFMSGSRSV1": diff --git a/securities.go b/securities.go new file mode 100644 index 0000000..0a9812c --- /dev/null +++ b/securities.go @@ -0,0 +1,260 @@ +package ofxgo + +import ( + "errors" + "github.com/golang/go/src/encoding/xml" +) + +type SecurityId struct { + XMLName xml.Name `xml:"SECID"` + UniqueId String `xml:"UNIQUEID"` // CUSIP for US FI's + UniqueIdType String `xml:"UNIQUEIDTYPE"` // Should always be "CUSIP" for US FI's +} + +type SecurityRequest struct { + XMLName xml.Name `xml:"SECRQ"` + // Only one of the next three should be present + SecId SecurityId `xml:"SECID,omitempty"` + Ticker String `xml:"TICKER,omitempty"` + FiId String `xml:"FIID,omitempty"` +} + +type SecListRequest struct { + XMLName xml.Name `xml:"SECLISTTRNRQ"` + TrnUID UID `xml:"TRNUID"` + CltCookie String `xml:"CLTCOOKIE,omitempty"` + TAN String `xml:"TAN,omitempty"` // Transaction authorization number + Securities []SecurityRequest `xml:"SECLISTRQ>SECRQ,omitempty"` +} + +func (r *SecListRequest) Name() string { + return "SECLISTTRNRQ" +} + +func (r *SecListRequest) Valid() (bool, error) { + if ok, err := r.TrnUID.Valid(); !ok { + return false, err + } + return true, nil +} + +type SecListResponse struct { + XMLName xml.Name `xml:"SECLISTTRNRS"` + TrnUID UID `xml:"TRNUID"` + Status Status `xml:"STATUS"` + CltCookie String `xml:"CLTCOOKIE,omitempty"` + // SECLISTRS is always empty, so we don't parse it here. The actual securities list will be in a top-level element parallel to SECLISTTRNRS +} + +func (r SecListResponse) Name() string { + return "SECLISTTRNRS" +} + +func (r SecListResponse) Valid() (bool, error) { + if ok, err := r.TrnUID.Valid(); !ok { + return false, err + } + return true, nil +} + +type Security interface { + SecurityType() string +} + +type SecInfo struct { + XMLName xml.Name `xml:"SECINFO"` + SecId SecurityId `xml:"SECID"` + SecName String `xml:"SECNAME"` // Full name of security + Ticker String `xml:"TICKER,omitempty"` // Ticker symbol + FiId String `xml:"FIID,omitempty"` + Rating String `xml:"RATING,omitempty"` + UnitPrice Amount `xml:"UNITPRICE,omitempty"` // Current price, as of DTASOF + DtAsOf Date `xml:"DTASOF,omitempty"` // Date UNITPRICE was for + Currency Currency `xml:"CURRENCY,omitempty"` // Overriding currency for UNITPRICE + Memo String `xml:"MEMO,omitempty"` +} + +type DebtInfo struct { + XMLName xml.Name `xml:"DEBTINFO"` + SecInfo SecInfo `xml:"SECINFO"` + ParValue Amount `xml:"PARVALUE"` + DebtType String `xml:"DEBTTYPE"` // One of COUPON, ZERO (zero coupon) + DebtClass String `xml:"DEBTCLASS,omitempty"` // One of TREASURY, MUNICIPAL, CORPORATE, OTHER + CouponRate Amount `xml:"COUPONRT,omitempty"` // Bond coupon rate for next closest call date + DtCoupon Date `xml:"DTCOUPON,omitempty"` // Maturity date for next coupon + CouponFreq String `xml:"COUPONFREQ,omitempty"` // When coupons mature - one of MONTHLY, QUARTERLY, SEMIANNUAL, ANNUAL, or OTHER + CallPrice Amount `xml:"CALLPRICE,omitempty"` // Bond call price + YieldToCall Amount `xml:"YIELDTOCALL,omitempty"` // Yield to next call + DtCall Date `xml:"DTCALL,omitempty"` // Next call date + CallType String `xml:"CALLTYPE,omitempt"` // Type of next call. One of CALL, PUT, PREFUND, MATURITY + YieldToMat Amount `xml:"YIELDTOMAT,omitempty"` // Yield to maturity + DtMat Date `xml:"DTMAT,omitempty"` // Debt maturity date + AssetClass String `xml:"ASSETCLASS,omitempty"` // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER + FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class +} + +func (i DebtInfo) SecurityType() string { + return "DEBTINFO" +} + +type AssetPortion struct { + XMLName xml.Name `xml:"PORTION"` + AssetClass String `xml:"ASSETCLASS"` // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER + Percent Amount `xml:"PERCENT"` // Percentage of the fund that falls under this asset class +} + +type FiAssetPortion struct { + XMLName xml.Name `xml:"FIPORTION"` + FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class + Percent Amount `xml:"PERCENT"` // Percentage of the fund that falls under this asset class +} + +type MFInfo struct { + XMLName xml.Name `xml:"MFINFO"` + SecInfo SecInfo `xml:"SECINFO"` + MfType String `xml:"MFTYPE"` // One of OPEN, END, CLOSEEND, OTHER + Yield Amount `xml:"YIELD,omitempty"` // Current yield reported as the dividend expressed as a portion of the current stock price + DtYieldAsOf Date `xml:"DTYIELDASOF,omitempty"` // Date YIELD is valid for + AssetClasses []AssetPortion `xml:"MFASSETCLASS>PORTION"` + FiAssetClasses []FiAssetPortion `xml:"FIMFASSETCLASS>FIPORTION"` +} + +func (i MFInfo) SecurityType() string { + return "MFINFO" +} + +type OptInfo struct { + XMLName xml.Name `xml:"OPTINFO"` + SecInfo SecInfo `xml:"SECINFO"` + OptType String `xml:"OPTTYPE"` // One of PUT, CALL + StrikePrice Amount `xml:"STRIKEPRICE"` + DtExpire Date `xml"DTEXPIRE"` // Expiration date + ShPerCtrct Int `xml:"SHPERCTRCT"` // Shares per contract + SecId SecurityId `xml:"SECID,omitempty"` // Security ID of the underlying security + AssetClass String `xml:"ASSETCLASS,omitempty"` // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER + FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class +} + +func (i OptInfo) SecurityType() string { + return "OPTINFO" +} + +type OtherInfo struct { + XMLName xml.Name `xml:"OTHERINFO"` + SecInfo SecInfo `xml:"SECINFO"` + TypeDesc String `xml:"TYPEDESC,omitempty"` // Description of security type + AssetClass String `xml:"ASSETCLASS,omitempty"` // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER + FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class +} + +func (i OtherInfo) SecurityType() string { + return "OTHERINFO" +} + +type StockInfo struct { + XMLName xml.Name `xml:"STOCKINFO"` + SecInfo SecInfo `xml:"SECINFO"` + StockType String `xml:"STOCKTYPE,omitempty"` // One of COMMON, PREFERRED, CONVERTIBLE, OTHER + Yield Amount `xml:"YIELD,omitempty"` // Current yield reported as the dividend expressed as a portion of the current stock price + DtYieldAsOf Date `xml:"DTYIELDASOF,omitempty"` // Date YIELD is valid for + AssetClass String `xml:"ASSETCLASS,omitempty"` // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER + FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class +} + +func (i StockInfo) SecurityType() string { + return "STOCKINFO" +} + +type SecurityList struct { + Securities []Security +} + +func (r SecurityList) Name() string { + return "SECLIST" +} + +func (r SecurityList) Valid() (bool, error) { + // TODO implement + return true, nil +} + +func (r *SecurityList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + for { + tok, err := nextNonWhitespaceToken(d) + if err != nil { + return err + } else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local { + // If we found the end of our starting element, we're done parsing + return nil + } else if startElement, ok := tok.(xml.StartElement); ok { + switch startElement.Name.Local { + case "DEBTINFO": + var info DebtInfo + if err := d.DecodeElement(&info, &startElement); err != nil { + return err + } + r.Securities = append(r.Securities, Security(info)) + case "MFINFO": + var info MFInfo + if err := d.DecodeElement(&info, &startElement); err != nil { + return err + } + r.Securities = append(r.Securities, Security(info)) + case "OPTINFO": + var info OptInfo + if err := d.DecodeElement(&info, &startElement); err != nil { + return err + } + r.Securities = append(r.Securities, Security(info)) + case "OTHERINFO": + var info OtherInfo + if err := d.DecodeElement(&info, &startElement); err != nil { + return err + } + r.Securities = append(r.Securities, Security(info)) + case "STOCKINFO": + var info StockInfo + if err := d.DecodeElement(&info, &startElement); err != nil { + return err + } + r.Securities = append(r.Securities, Security(info)) + default: + return errors.New("Invalid SECLIST child tag: " + startElement.Name.Local) + } + } else { + return errors.New("Didn't find an opening element") + } + } +} + +func DecodeSecuritiesMessageSet(d *xml.Decoder, start xml.StartElement) ([]Message, error) { + var msgs []Message + for { + tok, err := nextNonWhitespaceToken(d) + if err != nil { + return nil, err + } else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local { + // If we found the end of our starting element, we're done parsing + return msgs, nil + } else if startElement, ok := tok.(xml.StartElement); ok { + switch startElement.Name.Local { + case "SECLISTTRNRS": + var info SecListResponse + if err := d.DecodeElement(&info, &startElement); err != nil { + return nil, err + } + msgs = append(msgs, Message(info)) + case "SECLIST": + var info SecurityList + if err := d.DecodeElement(&info, &startElement); err != nil { + return nil, err + } + msgs = append(msgs, Message(info)) + default: + return nil, errors.New("Unsupported securities response tag: " + startElement.Name.Local) + } + } else { + return nil, errors.New("Didn't find an opening element") + } + } +}