aboutsummaryrefslogblamecommitdiff
path: root/api/portfolio.go
blob: 0fef94c7bf041793f367fcd5e48e32e755f4ff00 (plain) (tree)


























































































































































































































                                                                                                                                            
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
}