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
}