+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
+}