package main
import (
"fmt"
"log"
"math/big"
"time"
"src.d10.dev/command"
)
type Lot struct {
name string
date time.Time
inventory Amount
startInventory Amount
startCost Amount
price *big.Rat
}
func NewLot(name string, date time.Time, inventory, basis Amount) *Lot {
if inventory.Sign() < 1 {
log.Panicf("lot must have positive inventory (%s)", inventory.String()) // sanity
}
if basis.Sign() < 0 {
log.Panicf("lot must have non-negative basis (%s)", basis.String()) // sanity
}
price := new(big.Rat).Quo(basis.Rat, inventory.Rat) // price = (total cost) / (how many)
this := &Lot{
name: name,
date: date,
inventory: inventory,
startInventory: inventory,
startCost: basis,
price: price,
}
// sanity
if this.price.Sign() < 0 {
log.Panicf("Calculated new lot (%q) price %s = %s / %s", name, this.price, this.startCost, this.startInventory)
}
return this
}
func (this *Lot) Sell(delta Amount) (actual, basis Amount) {
// sanity
if delta.Sign() > -1 {
log.Panicf("lot.Sell() expects negative amount, got %s", delta)
}
if !delta.Compatible(this.inventory) {
log.Panic("lot.Sell() account/asset mismatch")
}
tmp := new(big.Rat)
tmp.Add(this.inventory.Rat, delta.Rat) // adding negative delta
// tmp is now (inventory - amount to sell)
switch tmp.Sign() {
case -1:
// inventory does not cover delta, actual is limited to inventory amount
actual = this.inventory
this.inventory = this.inventory.ZeroClone() // nothing remains in inventory
case 1:
// inventory has more than enough, put remainder back
this.inventory.Set(tmp)
actual = delta.NegClone()
case 0:
// exact amount, actual is full delta, set inventory to zero
actual = delta.NegClone()
this.inventory = this.inventory.ZeroClone() // nothing remains
}
// calculate basis that corresponds to inventory consumed
basis = this.startCost.ZeroClone()
basis.Mul(this.price, actual.Rat)
basis.Neg(basis.Rat) // convention: amount sold is positive, basis is negative
// sanity
if actual.Sign() < 1 {
log.Panic("lot.Sell() calculated:", actual)
}
if basis.Sign() > 0 { // Note that 0 basis is allowed (i.e. BCH from hard fork)
log.Panic("lot.Sell() basis: ", basis, " from price ", this.price)
}
return actual, basis
}
type LotFIFO []Lot
func (this *LotFIFO) Len() int { return len(*this) }
func (this *LotFIFO) Buy(lot Lot) {
this.sanity(lot.inventory)
*this = append(*this, lot)
}
// Sell consumes inventory and basis from lots.
func (this *LotFIFO) Sell(delta Amount) (lot []Lot, inventory, basis []Amount, err error) {
this.sanity(delta)
command.V(1).Infof("lotFIFO.Sell() %s from queue of %d lots", delta.String(), len(*this)) // troubleshoot
remaining := delta.Clone()
var l Lot
for remaining.Sign() != 0 {
if len(*this) == 0 {
// We haven't consumed original delta, but the queue is empty.
err = fmt.Errorf("failed to sell %s (of %s), no remaining inventory", remaining.String(), delta.String())
return
}
l, *this = (*this)[0], (*this)[1:] // pop front
sold, soldBasis := l.Sell(remaining)
// sanity
if sold.Sign() == -1 || soldBasis.Sign() == 1 { // basis may be zero
log.Panicf("insane sale: sold %s, basis %s", sold, soldBasis)
}
command.V(1).Infof("Sold %s (%s basis) from lot %s", sold, soldBasis, l.name)
lot = append(lot, l)
inventory = append(inventory, sold)
basis = append(basis, soldBasis)
// note that remaining is negative, sold is positive
remaining.Add(remaining.Rat, sold.Rat)
if remaining.Sign() > -1 {
// entire amount has been consumed from inventory
if remaining.Sign() != 0 { // sanity
log.Panic("lotFIFO.Sell() remaining:", remaining) // should never be reached
}
if l.inventory.Sign() > 0 {
// push unsold inventory back to queue
*this = append(LotFIFO{l}, *this...)
}
}
}
command.V(1).Infof("lotFIFO.Sell() sold %s, %d lots remain", delta.String(), len(*this)) // troubleshoot
return lot, inventory, basis, err
}
func (this LotFIFO) sanity(delta Amount) {
if delta.Sign() == 0 {
log.Panic("attempt to buy/sell zero amount")
}
if len(this) == 0 {
if delta.Sign() < 0 {
log.Panicf("attempt to sell (%#v %s) from empty inventory (%#v)", delta, delta.Asset, this)
} else {
return
}
}
// sanity
if delta.Asset != this[0].inventory.Asset {
log.Panicf("currency mismatch: want %q, got %q", delta.Asset, this[0].inventory.Asset)
}
}