package api import ( "fmt" "time" "github.com/shopspring/decimal" "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/db" ) func init() { decimal.MarshalJSONWithoutQuotes = true } const MARGIN_POSITION_SECURED_RATIO = 1.0 const BTC_DIGITS = 3 var MINIMAL_BTC_VALUE_TRESHOLD decimal.Decimal = decimal.NewFromFloat(10.0).Pow(decimal.NewFromFloat(-3.0)) type ValuePerformance struct { Value decimal.Decimal `json:"value"` Variation decimal.Decimal `json:"variation"` VariationP decimal.Decimal `json:"variationP"` } func NewValuePerformance(fromValue, toValue decimal.Decimal) ValuePerformance { variation := toValue.Sub(fromValue) return ValuePerformance{ Value: toValue, Variation: variation, VariationP: variation.Div(fromValue).Mul(decimal.NewFromFloat(100.0)), } } type PositionType string const POSITION_SHORT PositionType = "short" const POSITION_LONG PositionType = "long" type PortfolioBalance struct { PositionType PositionType `json:"positionType"` Quantity decimal.Decimal `json:"quantity"` QuantityLocked decimal.Decimal `json:"quantityLocked"` BTCValue decimal.Decimal `json:"BTCValue"` PositionPerformanceP decimal.Decimal `json:"positionPerformanceP"` Weight decimal.Decimal `json:"weight"` } type Portfolio struct { PeriodStart time.Time `json:"periodStart"` PeriodEnd time.Time `json:"periodEnd"` Balances map[string]PortfolioBalance `json:"balances"` Value decimal.Decimal `json:"value"` Performance ValuePerformance `json:"performance"` } func (p Portfolio) Round() Portfolio { p.Value = p.Value.Round(BTC_DIGITS) for currency := range p.Balances { balance := p.Balances[currency] balance.Quantity = balance.Quantity.Round(2) balance.BTCValue = balance.BTCValue.Round(BTC_DIGITS) balance.Weight = balance.Weight.Round(1) balance.PositionPerformanceP = balance.PositionPerformanceP.Round(1) p.Balances[currency] = balance } p.Performance.VariationP = p.Performance.VariationP.Round(1) p.Performance.Variation = p.Performance.Variation.Round(BTC_DIGITS) p.Performance.Value = p.Performance.Value.Round(BTC_DIGITS) return p } func GetCurrenciesPerformance(from, to db.ReportTickers) map[string]ValuePerformance { performances := make(map[string]ValuePerformance) currencies := make(map[string]struct{}) for currency := range to.Balances { if to.Balances[currency].Abs().LessThan(MINIMAL_BTC_VALUE_TRESHOLD) { continue } currencies[currency] = struct{}{} } for currency := range currencies { performances[currency] = NewValuePerformance(from.GetBTCRate(currency), to.GetBTCRate(currency)) } return performances } func UserMovements(from db.Report, to db.Report) (decimal.Decimal, error) { if from.Tag != db.BUY_END || (to.Tag != db.SELL_BEGIN && to.Tag != db.INTERMADIATE_STATE) { return decimal.Zero, fmt.Errorf("cannot compare reports: '%s' -> '%s'", from.Tag, to.Tag) } var deltaBTC decimal.Decimal currencies := make(map[string]struct{}) for currency := range to.Balances { currencies[currency] = struct{}{} } for currency := range from.Balances { currencies[currency] = struct{}{} } for currency := range currencies { balanceFrom := from.Balances[currency] balanceTo := to.Balances[currency] delta := balanceTo.Total.Sub(balanceFrom.Total) if !delta.Equals(decimal.Zero) { deltaBTC = deltaBTC.Add(delta.Mul(to.Tickers.GetBTCRate(currency)).Neg()) } } return deltaBTC, nil } // Computes plus-value, ignoring positions took by users. func ComputePlusValue(from db.Report, to db.Report) (decimal.Decimal, error) { if from.Tag != db.BUY_END || (to.Tag != db.SELL_BEGIN && to.Tag != db.INTERMADIATE_STATE) { return decimal.Zero, fmt.Errorf("cannot compare reports: '%s' -> '%s'", from.Tag, to.Tag) } diff, err := UserMovements(from, to) if err != nil { return decimal.Zero, err } return to.Tickers.Total.Sub(from.Tickers.Total).Add(diff), nil } func ComputeWeights(report db.Report) map[string]decimal.Decimal { weights := make(map[string]decimal.Decimal) for currency := range report.Balances { if report.Tickers.Balances[currency].Abs().LessThan(MINIMAL_BTC_VALUE_TRESHOLD) { continue } quantityBlocked := report.Balances[currency].MarginInPosition.Mul(decimal.NewFromFloat(1.0 + MARGIN_POSITION_SECURED_RATIO)) weights[currency] = report.Tickers.Balances[currency]. Sub(quantityBlocked.Mul(report.Tickers.GetBTCRate(currency))). Abs(). Div(report.Tickers.Total). Mul(decimal.NewFromFloat(100)) } return weights } func GetWeekPortfolio(marketConfig db.MarketConfig) (Portfolio, error) { portfolio := Portfolio{ Balances: make(map[string]PortfolioBalance), } report, err := db.GetLastPortfolioBegin(marketConfig) if err != nil { return portfolio, err } if report == nil { return portfolio, &Error{NotFound, "no report", fmt.Errorf("no reports for marketConfigId '%v'", marketConfig.Id)} } liveReport, err := db.GetLatestReport(marketConfig) if err != nil { return portfolio, err } weights := ComputeWeights(*report) currenciesPerformances := GetCurrenciesPerformance(report.Tickers, liveReport.Tickers) portfolio.PeriodStart = report.Date.Truncate(time.Second).UTC() portfolio.PeriodEnd = liveReport.Date.Truncate(time.Second).UTC() for currency := range liveReport.Balances { balance := liveReport.Balances[currency] btcTicker := liveReport.Tickers.Balances[currency] if btcTicker.Abs().LessThan(MINIMAL_BTC_VALUE_TRESHOLD) { continue } var positionType PositionType var perfMul decimal.Decimal if balance.Total.LessThan(decimal.Zero) { positionType = POSITION_SHORT perfMul = decimal.NewFromFloat(-1.0) } else { positionType = POSITION_LONG perfMul = decimal.NewFromFloat(1.0) } portfolio.Balances[currency] = PortfolioBalance{ PositionType: positionType, Quantity: balance.Total, BTCValue: btcTicker, QuantityLocked: balance.MarginInPosition.Mul(decimal.NewFromFloat(1.0 + MARGIN_POSITION_SECURED_RATIO)), Weight: weights[currency], PositionPerformanceP: currenciesPerformances[currency].VariationP.Mul(perfMul), } } portfolio.Value = liveReport.Tickers.Total plusValue, err := ComputePlusValue(*report, liveReport) if err != nil { return portfolio, err } portfolio.Performance = NewValuePerformance(liveReport.Tickers.Total.Sub(plusValue), liveReport.Tickers.Total) return portfolio, nil }