mirror of
https://github.com/aclindsa/moneygo.git
synced 2024-10-31 16:00:05 -04:00
Add security prices
* Import them from Gnucash's pricedb * Add support for querying prices from lua for reports * Add documentation for lua reports
This commit is contained in:
parent
594555b0c4
commit
f213e1061c
1
db.go
1
db.go
@ -22,6 +22,7 @@ func initDB() *gorp.DbMap {
|
||||
dbmap.AddTableWithName(Security{}, "securities").SetKeys(true, "SecurityId")
|
||||
dbmap.AddTableWithName(Transaction{}, "transactions").SetKeys(true, "TransactionId")
|
||||
dbmap.AddTableWithName(Split{}, "splits").SetKeys(true, "SplitId")
|
||||
dbmap.AddTableWithName(Price{}, "prices").SetKeys(true, "PriceId")
|
||||
dbmap.AddTableWithName(Report{}, "reports").SetKeys(true, "ReportId")
|
||||
|
||||
err = dbmap.CreateTablesIfNotExists()
|
||||
|
@ -136,9 +136,37 @@ has several fields describing it:
|
||||
* `s.Type` returns an int constant which represents what type of security it is
|
||||
(i.e. stock or currency)
|
||||
|
||||
Securities support a ClosestPrice function that allows you to fetch the price of
|
||||
the current security in a given currency that is closest to the supplied date.
|
||||
For example, to print the price in the user's default currency for each security
|
||||
in the user's account:
|
||||
|
||||
```
|
||||
default_currency = get_default_currency()
|
||||
for id, security in pairs(get_securities()) do
|
||||
price = security.price(default_currency, date.now())
|
||||
if price ~= nil then
|
||||
print(tostring(security) .. ": " security.Symbol .. " " .. price.Value)
|
||||
else
|
||||
print("Failed to fetch price for " .. tostring(security))
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
You can also query for an account's default currency using the global
|
||||
`get_default_currency()` function.
|
||||
|
||||
### Prices
|
||||
|
||||
Price objects can be queried from Security objects. Price objects contain the
|
||||
following fields:
|
||||
|
||||
* `p.PriceId`
|
||||
* `p.Security` returns the security object the price is for
|
||||
* `p.Currency` returns the currency that the price is in
|
||||
* `p.Value` returns the price of one unit of 'security' in 'currency', as a
|
||||
float
|
||||
|
||||
### Dates
|
||||
|
||||
In order to make it easier to do operations like finding account balances for a
|
||||
|
63
gnucash.go
63
gnucash.go
@ -72,6 +72,20 @@ type GnucashDate struct {
|
||||
Date GnucashTime `xml:"http://www.gnucash.org/XML/ts date"`
|
||||
}
|
||||
|
||||
type GnucashPrice struct {
|
||||
Id string `xml:"http://www.gnucash.org/XML/price id"`
|
||||
Commodity GnucashCommodity `xml:"http://www.gnucash.org/XML/price commodity"`
|
||||
Currency GnucashCommodity `xml:"http://www.gnucash.org/XML/price currency"`
|
||||
Date GnucashDate `xml:"http://www.gnucash.org/XML/price time"`
|
||||
Source string `xml:"http://www.gnucash.org/XML/price source"`
|
||||
Type string `xml:"http://www.gnucash.org/XML/price type"`
|
||||
Value string `xml:"http://www.gnucash.org/XML/price value"`
|
||||
}
|
||||
|
||||
type GnucashPriceDB struct {
|
||||
Prices []GnucashPrice `xml:"price"`
|
||||
}
|
||||
|
||||
type GnucashAccount struct {
|
||||
Version string `xml:"version,attr"`
|
||||
accountid int64 // Used to map Gnucash guid's to integer ones
|
||||
@ -105,6 +119,7 @@ type GnucashSplit struct {
|
||||
type GnucashXMLImport struct {
|
||||
XMLName xml.Name `xml:"gnc-v2"`
|
||||
Commodities []GnucashCommodity `xml:"http://www.gnucash.org/XML/gnc book>commodity"`
|
||||
PriceDB GnucashPriceDB `xml:"http://www.gnucash.org/XML/gnc book>pricedb"`
|
||||
Accounts []GnucashAccount `xml:"http://www.gnucash.org/XML/gnc book>account"`
|
||||
Transactions []GnucashTransaction `xml:"http://www.gnucash.org/XML/gnc book>transaction"`
|
||||
}
|
||||
@ -113,6 +128,7 @@ type GnucashImport struct {
|
||||
Securities []Security
|
||||
Accounts []Account
|
||||
Transactions []Transaction
|
||||
Prices []Price
|
||||
}
|
||||
|
||||
func ImportGnucash(r io.Reader) (*GnucashImport, error) {
|
||||
@ -141,6 +157,38 @@ func ImportGnucash(r io.Reader) (*GnucashImport, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Create prices, setting security and currency IDs from securityMap
|
||||
for i := range gncxml.PriceDB.Prices {
|
||||
price := gncxml.PriceDB.Prices[i]
|
||||
var p Price
|
||||
security, ok := securityMap[price.Commodity.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Unable to find commodity '%s' for price '%s'", price.Commodity.Name, price.Id)
|
||||
}
|
||||
currency, ok := securityMap[price.Currency.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Unable to find currency '%s' for price '%s'", price.Currency.Name, price.Id)
|
||||
}
|
||||
if currency.Type != Currency {
|
||||
return nil, fmt.Errorf("Currency for imported price isn't actually a currency\n")
|
||||
}
|
||||
p.PriceId = int64(i + 1)
|
||||
p.SecurityId = security.SecurityId
|
||||
p.CurrencyId = currency.SecurityId
|
||||
p.Date = price.Date.Date.Time
|
||||
|
||||
var r big.Rat
|
||||
_, ok = r.SetString(price.Value)
|
||||
if ok {
|
||||
p.Value = r.FloatString(currency.Precision)
|
||||
} else {
|
||||
return nil, fmt.Errorf("Can't set price value: %s", price.Value)
|
||||
}
|
||||
|
||||
p.RemoteId = "gnucash:" + price.Id
|
||||
gncimport.Prices = append(gncimport.Prices, p)
|
||||
}
|
||||
|
||||
//find root account, while simultaneously creating map of GUID's to
|
||||
//accounts
|
||||
var rootAccount GnucashAccount
|
||||
@ -340,6 +388,21 @@ func GnucashImportHandler(w http.ResponseWriter, r *http.Request) {
|
||||
securityMap[securityId] = s.SecurityId
|
||||
}
|
||||
|
||||
// Import prices, setting security and currency IDs from securityMap
|
||||
for _, price := range gnucashImport.Prices {
|
||||
price.SecurityId = securityMap[price.SecurityId]
|
||||
price.CurrencyId = securityMap[price.CurrencyId]
|
||||
price.PriceId = 0
|
||||
|
||||
err := CreatePriceIfNotExist(sqltransaction, &price)
|
||||
if err != nil {
|
||||
sqltransaction.Rollback()
|
||||
WriteError(w, 6 /*Import Error*/)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get/create accounts in the database, building a map from Gnucash account
|
||||
// IDs to our internal IDs as we go
|
||||
accountMap := make(map[int64]int64)
|
||||
|
122
prices.go
Normal file
122
prices.go
Normal file
@ -0,0 +1,122 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/FlashBoys/go-finance"
|
||||
"gopkg.in/gorp.v1"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Price struct {
|
||||
PriceId int64
|
||||
SecurityId int64
|
||||
CurrencyId int64
|
||||
Date time.Time
|
||||
Value string // String representation of decimal price of Security in Currency units, suitable for passing to big.Rat.SetString()
|
||||
RemoteId string // unique ID from source, for detecting duplicates
|
||||
}
|
||||
|
||||
func InsertPriceTx(transaction *gorp.Transaction, p *Price) error {
|
||||
err := transaction.Insert(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreatePriceIfNotExist(transaction *gorp.Transaction, price *Price) error {
|
||||
if len(price.RemoteId) == 0 {
|
||||
// Always create a new price if we can't match on the RemoteId
|
||||
err := InsertPriceTx(transaction, price)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var prices []*Price
|
||||
|
||||
_, err := transaction.Select(&prices, "SELECT * from prices where SecurityId=? AND CurrencyId=? AND Date=? AND Value=?", price.SecurityId, price.CurrencyId, price.Date, price.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(prices) > 0 {
|
||||
return nil // price already exists
|
||||
}
|
||||
|
||||
err = InsertPriceTx(transaction, price)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return the latest price for security in currency units before date
|
||||
func GetLatestPrice(transaction *gorp.Transaction, security, currency *Security, date *time.Time) (*Price, error) {
|
||||
var p Price
|
||||
err := transaction.SelectOne(&p, "SELECT * from prices where SecurityId=? AND CurrencyId=? AND Date <= ? ORDER BY Date DESC LIMIT 1", security.SecurityId, currency.SecurityId, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// Return the earliest price for security in currency units after date
|
||||
func GetEarliestPrice(transaction *gorp.Transaction, security, currency *Security, date *time.Time) (*Price, error) {
|
||||
var p Price
|
||||
err := transaction.SelectOne(&p, "SELECT * from prices where SecurityId=? AND CurrencyId=? AND Date >= ? ORDER BY Date ASC LIMIT 1", security.SecurityId, currency.SecurityId, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// Return the price for security in currency closest to date
|
||||
func GetClosestPriceTx(transaction *gorp.Transaction, security, currency *Security, date *time.Time) (*Price, error) {
|
||||
earliest, _ := GetEarliestPrice(transaction, security, currency, date)
|
||||
latest, err := GetLatestPrice(transaction, security, currency, date)
|
||||
|
||||
// Return early if either earliest or latest are invalid
|
||||
if earliest == nil {
|
||||
return latest, err
|
||||
} else if err != nil {
|
||||
return earliest, nil
|
||||
}
|
||||
|
||||
howlate := earliest.Date.Sub(*date)
|
||||
howearly := date.Sub(latest.Date)
|
||||
if howearly < howlate {
|
||||
return latest, nil
|
||||
} else {
|
||||
return earliest, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetClosestPrice(security, currency *Security, date *time.Time) (*Price, error) {
|
||||
transaction, err := DB.Begin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
price, err := GetClosestPriceTx(transaction, security, currency, date)
|
||||
if err != nil {
|
||||
transaction.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = transaction.Commit()
|
||||
if err != nil {
|
||||
transaction.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return price, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
q, err := finance.GetQuote("BRK-A")
|
||||
if err == nil {
|
||||
fmt.Printf("%+v", q)
|
||||
}
|
||||
}
|
91
prices_lua.go
Normal file
91
prices_lua.go
Normal file
@ -0,0 +1,91 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
const luaPriceTypeName = "price"
|
||||
|
||||
func luaRegisterPrices(L *lua.LState) {
|
||||
mt := L.NewTypeMetatable(luaPriceTypeName)
|
||||
L.SetGlobal("price", mt)
|
||||
L.SetField(mt, "__index", L.NewFunction(luaPrice__index))
|
||||
L.SetField(mt, "__tostring", L.NewFunction(luaPrice__tostring))
|
||||
L.SetField(mt, "__metatable", lua.LString("protected"))
|
||||
}
|
||||
|
||||
func PriceToLua(L *lua.LState, price *Price) *lua.LUserData {
|
||||
ud := L.NewUserData()
|
||||
ud.Value = price
|
||||
L.SetMetatable(ud, L.GetTypeMetatable(luaPriceTypeName))
|
||||
return ud
|
||||
}
|
||||
|
||||
// Checks whether the first lua argument is a *LUserData with *Price and returns this *Price.
|
||||
func luaCheckPrice(L *lua.LState, n int) *Price {
|
||||
ud := L.CheckUserData(n)
|
||||
if price, ok := ud.Value.(*Price); ok {
|
||||
return price
|
||||
}
|
||||
L.ArgError(n, "price expected")
|
||||
return nil
|
||||
}
|
||||
|
||||
func luaPrice__index(L *lua.LState) int {
|
||||
p := luaCheckPrice(L, 1)
|
||||
field := L.CheckString(2)
|
||||
|
||||
switch field {
|
||||
case "PriceId", "priceid":
|
||||
L.Push(lua.LNumber(float64(p.PriceId)))
|
||||
case "Security", "security":
|
||||
security_map, err := luaContextGetSecurities(L)
|
||||
if err != nil {
|
||||
panic("luaContextGetSecurities couldn't fetch securities")
|
||||
}
|
||||
s, ok := security_map[p.SecurityId]
|
||||
if !ok {
|
||||
panic("Price's security not found for user")
|
||||
}
|
||||
L.Push(SecurityToLua(L, s))
|
||||
case "Currency", "currency":
|
||||
security_map, err := luaContextGetSecurities(L)
|
||||
if err != nil {
|
||||
panic("luaContextGetSecurities couldn't fetch securities")
|
||||
}
|
||||
c, ok := security_map[p.CurrencyId]
|
||||
if !ok {
|
||||
panic("Price's currency not found for user")
|
||||
}
|
||||
L.Push(SecurityToLua(L, c))
|
||||
case "Value", "value":
|
||||
amt, err := GetBigAmount(p.Value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
float, _ := amt.Float64()
|
||||
L.Push(lua.LNumber(float))
|
||||
default:
|
||||
L.ArgError(2, "unexpected price attribute: "+field)
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func luaPrice__tostring(L *lua.LState) int {
|
||||
p := luaCheckPrice(L, 1)
|
||||
|
||||
security_map, err := luaContextGetSecurities(L)
|
||||
if err != nil {
|
||||
panic("luaContextGetSecurities couldn't fetch securities")
|
||||
}
|
||||
s, ok1 := security_map[p.SecurityId]
|
||||
c, ok2 := security_map[p.CurrencyId]
|
||||
if !ok1 || !ok2 {
|
||||
panic("Price's currency or security not found for user")
|
||||
}
|
||||
|
||||
L.Push(lua.LString(p.Value + " " + c.Symbol + " (" + s.Symbol + ")"))
|
||||
|
||||
return 1
|
||||
}
|
@ -161,6 +161,7 @@ func runReport(user *User, report *Report) (*Tabulation, error) {
|
||||
luaRegisterBalances(L)
|
||||
luaRegisterDates(L)
|
||||
luaRegisterTabulations(L)
|
||||
luaRegisterPrices(L)
|
||||
|
||||
err := L.DoString(report.Lua)
|
||||
|
||||
|
@ -135,6 +135,8 @@ func luaSecurity__index(L *lua.LState) int {
|
||||
L.Push(lua.LNumber(float64(a.Precision)))
|
||||
case "Type", "type":
|
||||
L.Push(lua.LNumber(float64(a.Type)))
|
||||
case "ClosestPrice", "closestprice":
|
||||
L.Push(L.NewFunction(luaClosestPrice))
|
||||
default:
|
||||
L.ArgError(2, "unexpected security attribute: "+field)
|
||||
}
|
||||
@ -142,6 +144,21 @@ func luaSecurity__index(L *lua.LState) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func luaClosestPrice(L *lua.LState) int {
|
||||
s := luaCheckSecurity(L, 1)
|
||||
c := luaCheckSecurity(L, 2)
|
||||
date := luaCheckTime(L, 3)
|
||||
|
||||
p, err := GetClosestPrice(s, c, date)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
} else {
|
||||
L.Push(PriceToLua(L, p))
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func luaSecurity__tostring(L *lua.LState) int {
|
||||
s := luaCheckSecurity(L, 1)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user