diff --git a/investments.go b/investments.go index dfb27b1..c1cccb3 100644 --- a/investments.go +++ b/investments.go @@ -42,10 +42,133 @@ type InvBankTransaction struct { } type InvTransactionList struct { - XMLName xml.Name `xml:"INVTRANLIST"` - DtStart Date `xml:"DTSTART"` - DtEnd Date `xml:"DTEND"` - Transactions []InvBankTransaction `xml:"INVBANKTRAN,omitempty"` + XMLName xml.Name `xml:"INVTRANLIST"` + DtStart Date `xml:"DTSTART"` + DtEnd Date `xml:"DTEND"` + BankTransactions []InvBankTransaction `xml:"INVBANKTRAN,omitempty"` +} + +type InvPosition struct { + XMLName xml.Name `xml:"INVPOS"` + SecId SecurityId `xml:"SECID"` + HeldInAcct String `xml:"HELDINACCT"` // Sub-account type, one of CASH, MARGIN, SHORT, OTHER + PosType String `xml:"POSTYPE"` // SHORT = Writer for options, Short for all others; LONG = Holder for options, Long for all others. + Units Amount `xml:"UNITS"` // For stocks, MFs, other, number of shares held. Bonds = face value. Options = number of contracts + UnitPrice Amount `xml:"UNITPRICE"` // For stocks, MFs, other, price per share. Bonds = percentage of par. Option = premium per share of underlying security + MktVal Amount `xml:"MKTVAL"` // Market value of this position + AvgCostBasis Amount `xml:"AVGCOSTBASIS,omitempty"` // + DtPriceAsOf Date `xml:"DTPRICEASOF"` // Date and time of unit price and market value, and cost basis. If this date is unknown, use 19900101 as the placeholder; do not use 0, + Currency Currency `xml:"CURRENCY,omitempty"` // Overriding currency for UNITPRICE + Memo String `xml:"MEMO,omitempty"` + Inv401kSource String `xml:"INV401KSOURCE,omitempty"` // One of PRETAX, AFTERTAX, MATCH, PROFITSHARING, ROLLOVER, OTHERVEST, OTHERNONVEST for 401(k) accounts. Default if not present is OTHERNONVEST. The following cash source types are subject to vesting: MATCH, PROFITSHARING, and OTHERVEST +} + +type Position interface { + PositionType() string +} + +type DebtPosition struct { + XMLName xml.Name `xml:"POSDEBT"` + InvPos InvPosition `xml:"INVPOS"` +} + +func (p DebtPosition) PositionType() string { + return "POSDEBT" +} + +type MFPosition struct { + XMLName xml.Name `xml:"POSMF"` + InvPos InvPosition `xml:"INVPOS"` + UnitsStreet Amount `xml:"UNITSSTREET,omitempty"` // Units in the FI’s street name + UnitsUser Amount `xml:"UNITSUSER,omitempty"` // Units in the user's name directly + ReinvDiv Boolean `xml:"REINVDIV,omitempty"` // Reinvest dividends + ReinCG Boolean `xml:"REINVCG,omitempty"` // Reinvest capital gains +} + +func (p MFPosition) PositionType() string { + return "POSMF" +} + +type OptPosition struct { + XMLName xml.Name `xml:"POSOPT"` + InvPos InvPosition `xml:"INVPOS"` + Secured String `xml:"SECURED,omitempty"` // One of NAKED, COVERED +} + +func (p OptPosition) PositionType() string { + return "POSOPT" +} + +type OtherPosition struct { + XMLName xml.Name `xml:"POSOTHER"` + InvPos InvPosition `xml:"INVPOS"` +} + +func (p OtherPosition) PositionType() string { + return "POSOTHER" +} + +type StockPosition struct { + XMLName xml.Name `xml:"POSSTOCK"` + InvPos InvPosition `xml:"INVPOS"` + UnitsStreet Amount `xml:"UNITSSTREET,omitempty"` // Units in the FI’s street name + UnitsUser Amount `xml:"UNITSUSER,omitempty"` // Units in the user's name directly + ReinvDiv Boolean `xml:"REINVDIV,omitempty"` // Reinvest dividends +} + +func (p StockPosition) PositionType() string { + return "POSSTOCK" +} + +type PositionList []Position + +func (p PositionList) 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 "POSDEBT": + var position DebtPosition + if err := d.DecodeElement(&position, &startElement); err != nil { + return err + } + p = append(p, Position(position)) + case "POSMF": + var position MFPosition + if err := d.DecodeElement(&position, &startElement); err != nil { + return err + } + p = append(p, Position(position)) + case "POSOPT": + var position OptPosition + if err := d.DecodeElement(&position, &startElement); err != nil { + return err + } + p = append(p, Position(position)) + case "POSOTHER": + var position OtherPosition + if err := d.DecodeElement(&position, &startElement); err != nil { + return err + } + p = append(p, Position(position)) + case "POSSTOCK": + var position StockPosition + if err := d.DecodeElement(&position, &startElement); err != nil { + return err + } + p = append(p, Position(position)) + default: + return errors.New("Invalid INVPOSLIST child tag: " + startElement.Name.Local) + } + } else { + return errors.New("Didn't find an opening element") + } + } } type InvBalance struct { @@ -67,8 +190,8 @@ type InvStatementResponse struct { CurDef String `xml:"INVSTMTRS>CURDEF"` InvAcctFrom InvAcct `xml:"INVSTMTRS>INVACCTFROM"` InvTranList InvTransactionList `xml:"INVSTMTRS>INVTRANLIST,omitempty"` - // TODO INVPOSLIST - InvBal InvBalance `xml:"INVSTMTRS>INVBAL,omitempty"` + InvPosList PositionList `xml:"INVSTMTRS>INVPOSLIST,omitempty"` + InvBal InvBalance `xml:"INVSTMTRS>INVBAL,omitempty"` // TODO INVOOLIST MktgInfo String `xml:"INVSTMTRS>MKTGINFO,omitempty"` // Marketing information // TODO INV401K