]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front.git/commitdiff
Refactor Portfolio balance.
authorjloup <jeanloup.jamet@gmail.com>
Wed, 9 May 2018 17:44:17 +0000 (19:44 +0200)
committerjloup <jeanloup.jamet@gmail.com>
Wed, 9 May 2018 17:44:17 +0000 (19:44 +0200)
15 files changed:
api/api.go
api/const.go
api/market_config.go
api/portfolio.go [new file with mode: 0644]
api/routes.go
cmd/web/Makefile
cmd/web/js/account.jsx
cmd/web/js/api.js
cmd/web/js/balance.jsx [new file with mode: 0644]
cmd/web/js/poloniex.jsx
cmd/web/package.json
cmd/web/static/style.css
cmd/web/yarn.lock
db/report_lines.go [new file with mode: 0644]
markets/poloniex.go

index 42b9923986484cf896d7587efaa8c508f2bde379..ece2a26aee852a63727a56bac5fd3faac9da4ccf 100644 (file)
@@ -45,6 +45,18 @@ func (e Error) Error() string {
        return ""
 }
 
+func ErrorIs(err error, code ErrorCode) bool {
+       if err == nil {
+               return false
+       }
+
+       if apiError, ok := err.(*Error); !ok {
+               return false
+       } else {
+               return apiError.Code == code
+       }
+}
+
 func NewInternalError(err error) *Error {
        return &Error{InternalError, "internal error", err}
 }
index d3e5f42a485a27ced0feff2a89611686e1a23f59..1f15c6ec35176fdd71e361879500b7bcc1fec9ec 100644 (file)
@@ -6,7 +6,7 @@ import "net/http"
 type Status uint32
 type ErrorCode uint32
 
-const EXTERNAL_SERVICE_TIMEOUT_SECONDS = 10
+const EXTERNAL_SERVICE_TIMEOUT_SECONDS = 20
 
 const (
        OK Status = iota
index c2248b354e876403361f7a67169ae845ab7e2e90..81a92d1e6d35fb476b9ddd79ff97e6f487347122 100644 (file)
@@ -5,11 +5,9 @@ import (
        "strings"
        "time"
 
-       "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/markets"
-
        "github.com/jloup/utils"
-       "github.com/shopspring/decimal"
        "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/db"
+       "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/markets"
 )
 
 type MarketConfigQuery struct {
@@ -56,45 +54,33 @@ func (q MarketConfigQuery) Run() (interface{}, *Error) {
        return config.Config, nil
 }
 
-type MarketBalanceQuery struct {
+type TestMarketCredentialsQuery struct {
        In struct {
-               User     db.User
-               Market   string
-               Currency string
+               User   db.User
+               Market string
        }
 }
 
-func (q MarketBalanceQuery) ValidateParams() *Error {
+func (q TestMarketCredentialsQuery) ValidateParams() *Error {
        if q.In.Market != "poloniex" {
                return &Error{BadRequest, "invalid market name", fmt.Errorf("'%v' is not a valid market name", q.In.Market)}
        }
 
-       // TODO: we should request market for available currencies.
-       if q.In.Currency != "BTC" && q.In.Currency != "USDT" && q.In.Currency != "ETH" {
-               return &Error{BadRequest, "invalid currency, accept [BTC, USDT, ETH]", fmt.Errorf("'%v' is not a valid currency", q.In.Currency)}
-       }
-
        return nil
 }
 
-func (q MarketBalanceQuery) Run() (interface{}, *Error) {
+func (q TestMarketCredentialsQuery) Run() (interface{}, *Error) {
        config, err := db.GetUserMarketConfig(q.In.User.Id, q.In.Market)
        if err != nil {
                return nil, NewInternalError(err)
        }
 
        if config.Config["key"] == "" || config.Config["secret"] == "" {
-               return nil, &Error{InvalidMarketCredentials, "your credentials for this market are not setup", fmt.Errorf("'%v' credentials are not setup", q.In.Market)}
+               return nil, &Error{InvalidMarketCredentials, "no market credentials", fmt.Errorf("market credentials are empty for marketId '%v'", q.In.Market)}
        }
 
-       result := struct {
-               Value         decimal.Decimal            `json:"value"`
-               ValueCurrency string                     `json:"valueCurrency"`
-               Balance       map[string]markets.Balance `json:"balance"`
-       }{}
-
        resultErr := CallExternalService(fmt.Sprintf("'%s' GetBalanceValue", q.In.Market), EXTERNAL_SERVICE_TIMEOUT_SECONDS*time.Second, func() *Error {
-               balance, err := Poloniex.GetBalance(config.Config["key"], config.Config["secret"])
+               err := Poloniex.TestCredentials(config.Config["key"], config.Config["secret"])
 
                if utils.ErrIs(err, markets.InvalidCredentials) {
                        return &Error{InvalidMarketCredentials, "wrong market credentials", fmt.Errorf("wrong '%v' market credentials", q.In.Market)}
@@ -108,16 +94,6 @@ func (q MarketBalanceQuery) Run() (interface{}, *Error) {
                        return NewInternalError(err)
                }
 
-               for currency, value := range balance.Balances {
-                       if value.BTCValue.Abs().LessThan(decimal.NewFromFloat(0.0001)) {
-                               delete(balance.Balances, currency)
-                       }
-               }
-
-               result.Balance = balance.Balances
-               result.ValueCurrency = "BTC"
-               result.Value = balance.BTCValue.Round(8)
-
                return nil
        })
 
@@ -125,7 +101,7 @@ func (q MarketBalanceQuery) Run() (interface{}, *Error) {
                return nil, resultErr
        }
 
-       return &result, nil
+       return nil, nil
 }
 
 type UpdateMarketConfigQuery struct {
@@ -164,3 +140,36 @@ func (q UpdateMarketConfigQuery) Run() (interface{}, *Error) {
 
        return nil, nil
 }
+
+type GetPortfolioQuery struct {
+       In struct {
+               User   db.User
+               Market string
+       }
+}
+
+func (q GetPortfolioQuery) ValidateParams() *Error {
+       if q.In.Market != "poloniex" {
+               return &Error{BadRequest, "invalid market name", fmt.Errorf("'%v' is not a valid market name", q.In.Market)}
+       }
+
+       return nil
+}
+
+func (q GetPortfolioQuery) Run() (interface{}, *Error) {
+       marketConfig, err := db.GetUserMarketConfig(q.In.User.Id, q.In.Market)
+       if err != nil {
+               return nil, NewInternalError(err)
+       }
+
+       report, err := GetWeekPortfolio(*marketConfig)
+       if ErrorIs(err, NotFound) {
+               return nil, err.(*Error)
+       }
+
+       if err != nil {
+               return nil, NewInternalError(err)
+       }
+
+       return report.Round(), nil
+}
diff --git a/api/portfolio.go b/api/portfolio.go
new file mode 100644 (file)
index 0000000..0fef94c
--- /dev/null
@@ -0,0 +1,219 @@
+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
+}
index 22af0e728c9e0c6fe3c365592466512ad42886c1..d7b316d885517e4a720aa3b1bdeb4ffa1037e7e0 100644 (file)
@@ -43,7 +43,8 @@ var Groups = []Group{
                []Route{
                        {"GET", []gin.HandlerFunc{GetMarketConfig}, "/:name"},
                        {"POST", []gin.HandlerFunc{UpdateMarketConfig}, "/:name/update"},
-                       {"GET", []gin.HandlerFunc{GetMarketBalance}, "/:name/balance"},
+                       {"GET", []gin.HandlerFunc{TestMarketCredentials}, "/:name/test-credentials"},
+                       {"GET", []gin.HandlerFunc{GetPortfolio}, "/:name/portfolio"},
                },
        },
 }
@@ -114,12 +115,20 @@ func GetMarketConfig(c *gin.Context) {
        RunQuery(query, c)
 }
 
-func GetMarketBalance(c *gin.Context) {
-       query := &MarketBalanceQuery{}
+func TestMarketCredentials(c *gin.Context) {
+       query := &TestMarketCredentialsQuery{}
+
+       query.In.User = GetUser(c)
+       query.In.Market = c.Param("name")
+
+       RunQuery(query, c)
+}
+
+func GetPortfolio(c *gin.Context) {
+       query := &GetPortfolioQuery{}
 
        query.In.User = GetUser(c)
        query.In.Market = c.Param("name")
-       query.In.Currency = c.Query("currency")
 
        RunQuery(query, c)
 }
index 52e2289a9adc4b27b5738a653adca28a6725e34d..7315d5ec17a9ff28b3cf6e591db6d95e93625c44 100644 (file)
@@ -4,7 +4,7 @@ export PATH := $(PATH):./node_modules/.bin
 
 SRC_DIR=js
 BUILD_DIR=build/js
-JSX_SRC= header_footer.jsx main.jsx signup.jsx signin.jsx otp.jsx poloniex.jsx password_reset.jsx change_password.jsx account.jsx
+JSX_SRC= header_footer.jsx main.jsx signup.jsx signin.jsx otp.jsx poloniex.jsx password_reset.jsx change_password.jsx account.jsx balance.jsx
 JS_SRC= cookies.js app.js api.js
 STATIC_FILES= index.html style.css fontello.css
 STATIC_FILES+=$(addprefix fonts/, fontello.eot fontello.svg fontello.ttf fontello.woff fontello.woff2)
index 43e7083efdef93d45f14ee0cf4cb0584143b1dd4..d30abe7e229a157a627aab99a8c55f55c33cd329 100644 (file)
@@ -8,7 +8,7 @@ class PoloniexConfiguration extends React.Component {
   }
 
   checkCredentials = () => {
-    Api.Call('MARKET_BALANCE', {'name': 'poloniex', 'currency': 'BTC'}, function(err, status, data) {
+    Api.Call('MARKET_TEST_CREDENTIALS', {'name': 'poloniex'}, function(err, status, data) {
       if (err) {
         console.error(err, data);
         if (err.code === 'invalid_market_credentials') {
@@ -28,13 +28,6 @@ class PoloniexConfiguration extends React.Component {
   }
 
   handleCredentialsSubmit = () => {
-
-    /*
-     *If (!this.state.apiKey || !this.state.apiSecret) {
-     *  return;
-     *}
-     */
-
     Api.Call('UPDATE_MARKET', {'key': this.state.apiKey, 'secret': this.state.apiSecret, 'name': 'poloniex'}, function(err, status, data) {
       if (err) {
         console.error(err, data);
index c9b4ef5eda1d87bbecb2d3f93d2ce253f31b7e83..f892a6b3affa49a5463c0165cab2b5593802ce65 100644 (file)
@@ -74,15 +74,24 @@ var ApiEndpoints = {
       return '/market/' + params.name;
     }
   },
-  'MARKET_BALANCE': {
+  'MARKET_TEST_CREDENTIALS': {
     'type': 'GET',
     'auth': true,
     'parameters': [
-      {'name': 'name',     'mandatory': true, 'inquery': false},
-      {'name': 'currency', 'mandatory': true, 'inquery': true},
+      {'name': 'name', 'mandatory': true, 'inquery': false},
+    ],
+    'buildUrl': function(params) {
+      return '/market/' + params.name + '/test-credentials';
+    }
+  },
+  'MARKET_GET_PORTFOLIO': {
+    'type': 'GET',
+    'auth': true,
+    'parameters': [
+      {'name': 'name', 'mandatory': true, 'inquery': false},
     ],
     'buildUrl': function(params) {
-      return '/market/' + params.name + '/balance';
+      return '/market/' + params.name + '/portfolio';
     }
   },
   'UPDATE_MARKET': {
diff --git a/cmd/web/js/balance.jsx b/cmd/web/js/balance.jsx
new file mode 100644 (file)
index 0000000..d141aa8
--- /dev/null
@@ -0,0 +1,99 @@
+import React from 'react';
+import moment from 'moment';
+
+class CurrencyLogo extends React.Component {
+    render = () => {
+      return <div className="d-inline-block h-100">
+               <img className="currency-logo align-top"
+                  src={'/public/icons/black/' + this.props.currency.toLowerCase() + '.svg' }
+                  title={this.props.currency}
+                  alt={this.props.currency} />
+             </div>;
+    }
+ }
+
+ var formatVariation = (variation) => {
+  var variationAbs = Math.abs(variation);
+  if (variation === 0.0) {
+    return <span>{variationAbs}%</span>;
+  } else if (variation > 0) {
+    return <span className="performance-up">+{variationAbs}%</span>;
+  }
+  return <span className="performance-down">-{variationAbs}%</span>;
+};
+
+
+class CurrencyRateHeader extends React.Component {
+    render = () => {
+        return <React.Fragment>
+              <div className="row text-center">
+                <div className="d-inline col-2">Asset</div>
+                <div className="d-inline col-2">Position</div>
+                <div className="d-inline col-2">Qty</div>
+                <div className="d-inline col-2">Value (BTC)</div>
+                <div className="d-inline col-2">Weight</div>
+                <div className="d-inline col-2">Perf %</div>
+              </div>
+            </React.Fragment>;
+    }
+}
+
+class CurrencyRate extends React.Component {
+    render = () => {
+        return <React.Fragment>
+              <div className="row text-center">
+                <div className="d-inline col-2 text-left"><CurrencyLogo currency={this.props.currency} /><span>{this.props.currency}</span></div>
+                <div className="d-inline col-2">{this.props.positionType}</div>
+                <div className="d-inline col-2">{this.props.quantity}</div>
+                <div className="d-inline col-2">{this.props.BTCValue}</div>
+                <div className="d-inline col-2">{this.props.weight}%</div>
+                <div className="d-inline col-2">{formatVariation(this.props.positionPerformanceP)}</div>
+              </div>
+            </React.Fragment>;
+    }
+}
+
+class Assets extends React.Component {
+    render = () => {
+        var currencies = Object.keys(this.props.balances).map(function(currency) {
+            var balance = this.props.balances[currency];
+            balance.currency = currency;
+            return <div className="row" key={currency}>
+                    <div className="col-12"><CurrencyRate {...balance} /></div>
+                  </div>;
+        }.bind(this));
+
+        return <div className="assets">
+          <CurrencyRateHeader />
+          {currencies}
+        </div>;
+    }
+}
+
+class PFBalance extends React.Component {
+    render = () => {
+        var date = moment(this.props.periodStart).format('MMM Do, h:mma');
+        return <React.Fragment>
+            <div className="h-100 align-self-center balance">
+              <div className="row">
+                <div className="col-8">
+                   Current balance
+                </div>
+                <div className="col-4 text-center h-100 btcValue">
+                  <CurrencyLogo currency="BTC" /> <span className="h-100 align-middle"><strong>{this.props.balance}</strong></span>
+                </div>
+              </div>
+              <div className="row">
+                <div className="col-8">
+                  <em>since {date}</em>
+                </div>
+                <div className="col-4 variation text-center">
+                   <strong>{formatVariation(this.props.variationP)}</strong>
+                </div>
+              </div>
+            </div>
+        </React.Fragment>;
+    }
+}
+
+export {PFBalance, Assets};
index edac368e84d07f02d2060d838481d7853cab04a3..6019ef8cb59402ac143e1fdc4cf9d1668360b7fa 100644 (file)
@@ -1,30 +1,45 @@
 import Api from './api.js';
 import React from 'react';
+import {PFBalance, Assets} from './balance.js';
 
 class PoloniexController extends React.Component {
   constructor(props) {
     super(props);
-    this.state = {'flag': 'loading', 'valueCurrency': null, 'balanceValue': null, 'balance': null};
+    this.state = {'flag': 'loading', 'periodStart': null, 'variationP': null, 'balance': null, 'balances': null};
   }
 
-  loadBalance = () => {
-    Api.Call('MARKET_BALANCE', {'name': 'poloniex', 'currency': 'BTC'}, function(err, status, data) {
+  testCredentials = () => {
+    Api.Call('MARKET_TEST_CREDENTIALS', {'name': 'poloniex'}, function(err, status, data) {
       if (err) {
         console.error(err, data);
         if (err.code === 'invalid_market_credentials') {
-          this.setState({'flag': 'invalidCredentials', 'valueCurrency': null, 'balanceValue': null, 'balance': null});
+          this.setState({'flag': 'invalidCredentials', 'variationP': null, 'balance': null, 'balances': null, 'periodStart': null});
         } else if (err.code === 'ip_restricted_api_key') {
-          this.setState({'flag': 'ipRestricted', 'valueCurrency': null, 'balanceValue': null, 'balance': null});
+          this.setState({'flag': 'ipRestricted', 'variationP': null, 'balance': null, 'balances': null, 'periodStart': null});
         }
         return;
       }
 
-      this.setState({'flag': 'ok', 'valueCurrency': data.valueCurrency, 'balanceValue': data.value, 'balance': data.balance});
+      this.loadPortfolio();
+    }.bind(this));
+  }
+
+  loadPortfolio = () => {
+    Api.Call('MARKET_GET_PORTFOLIO', {'name': 'poloniex'}, function(err, status, data) {
+      if (err) {
+        console.error(err, data);
+        if (err.code === 'not_found') {
+          this.setState({'flag': 'noReport', 'variationP': null, 'balance': null, 'balances': null, 'periodStart': null});
+        }
+        return;
+      }
+
+      this.setState({'flag': 'ok', 'variationP': data.performance.variationP, 'balance': data.value, 'balances': data.balances, 'periodStart': data.periodStart});
     }.bind(this));
   }
 
   componentDidMount = () => {
-    this.loadBalance();
+    this.testCredentials();
   }
 
   render = () => {
@@ -33,10 +48,13 @@ class PoloniexController extends React.Component {
       case 'loading':
         displayText = 'Loading data from poloniex...';
         break;
+      case 'noReport':
+        displayText = 'Your account is setup ! Reporting will start next Monday !';
+        break;
       case 'invalidCredentials':
       case 'ipRestricted':
       case 'emptyCredentials':
-        displayText = <div>Please provide poloniex credentials in <a href="/account">Account</a> page.</div>;
+        displayText = <div>Please provide poloniex credentials in <a href="/account"><u>Account</u></a> page.</div>;
         break;
       default:
         displayText = null;
@@ -44,59 +62,55 @@ class PoloniexController extends React.Component {
     return (
       <div>
         <PoloniexBalance  balanceCurrency={this.state.valueCurrency}
-                          balanceValue={this.state.balanceValue}
+                          variationP={this.state.variationP}
+                          periodStart={this.state.periodStart}
                           balance={this.state.balance}
+                          balances={this.state.balances}
                           displayText={displayText}/>
       </div>
     );
   }
 }
 
-class CurrencyLogo extends React.Component {
+class Panel extends React.Component {
   render = () => {
-    return <img className="currency-logo"
-                src={'/public/icons/black/' + this.props.currency.toLowerCase() + '.svg' }
-                title={this.props.currency}
-                alt={this.props.currency} />;
+    if (this.props.component === null) {
+      return <div></div>;
+    }
+
+    return (
+      <div className="row">
+        <div className="box col-12">
+          <div className="row">
+            <div className="col-4">{this.props.title}</div>
+          </div>
+          <hr/>
+          {this.props.component}
+        </div>
+      </div>);
   }
 }
 
 class PoloniexBalance extends React.Component {
-  constructor(props) {
-    super(props);
-    this.state = {'hideMsg': true, 'msg': '', 'msgOk': false};
-  }
-
-  computeCurrencyRatio = (currency) => {
-    return (parseFloat(this.props.balance[currency].btcValue) / parseFloat(this.props.balanceValue) * 100.0).toFixed(1);
-  }
 
   render = () => {
-    var dashboard = null;
-
-    if (this.props.balanceValue !== null) {
+    var balancePanel = null;
+    var assetsPanel  = null;
+    var messagePanel = null;
 
-      var balance = Object.keys(this.props.balance).map(function(currency) {
-        return <div key={currency}>
-                 <div>
-                 <CurrencyLogo currency={currency} /> {this.props.balance[currency].amount} {currency} ({this.computeCurrencyRatio(currency)}%)
-                 </div>
-               </div>;
-      }.bind(this));
-
-      dashboard =
+    if (this.props.variationP !== null) {
+      balancePanel =
         <div className="row">
-          <div className="col-6 align-self-center h-100 balances">
-              {balance}
-          </div>
-          <div className="offset-1 col-5 h-100 align-self-center">
-            <div className="text-center">
-              Balance ({this.props.balanceCurrency}): <span>{this.props.balanceValue}</span><CurrencyLogo currency={this.props.balanceCurrency} />
-            </div>
+          <div className="col-12 offset-md-1 col-md-10 align-self-center h-100 balances">
+            <PFBalance variationP={this.props.variationP} balance={this.props.balance} periodStart={this.props.periodStart}/>
           </div>
         </div>;
+
+      assetsPanel =
+        <Assets balances={this.props.balances} />;
+
     } else {
-      dashboard =
+      messagePanel =
         <div className="row">
           <div className="col-12 text-center">
            <span>{this.props.displayText}</span>
@@ -105,15 +119,11 @@ class PoloniexBalance extends React.Component {
     }
 
     return (
-      <div className="row">
-        <div className="box offset-2 col-8 portfolio">
-          <div className="row">
-            <div className="col-4">Portfolio</div>
-          </div>
-          <hr/>
-          {dashboard}
-        </div>
-      </div>
+      <React.Fragment>
+        <Panel title="Balance" component={balancePanel}/>
+        <Panel title="Assets"  component={assetsPanel}/>
+        <Panel title="Balance" component={messagePanel}/>
+      </React.Fragment>
     );
   }
 }
index a86731336baf186f5a7d2eeffbb555fa0d551fc4..0bfb4e9a0f06efd983e1f567240d378ee1c2509e 100644 (file)
@@ -12,6 +12,7 @@
     "classnames": "^2.2.5",
     "debowerify": "^1.3.1",
     "localenvify": "^1.0.1",
+    "moment": "^2.22.1",
     "page": "^1.8.3",
     "path-to-regexp": "^1.2.1",
     "qs": "^6.5.1",
index 2212168befe603a5528f2d62775d723f2f0d0dc3..e4da17680a2f2590133691917501b5c7cf4d5cc2 100644 (file)
@@ -54,6 +54,14 @@ h1 {
   margin-top: 5px;
 }
 
+.performance-up {
+  color: green;
+}
+
+.performance-down {
+  color: red;
+}
+
 .sign-in .form-control {
   margin-bottom: 20px;
   border-radius: 2px;
@@ -63,16 +71,12 @@ h1 {
 
 .config-status {
   margin-bottom: 10px;
-  font-size: 0.9em;
 }
 
-.config-status .icon-cancel-circled {
-  color: red;
-}
+.config-status .icon-cancel-circled {}
+
+.config-status .icon-ok-circled {}
 
-.config-status .icon-ok-circled {
-  color: green;
-}
 .config-status i {
   font-size: 1.2em;
 }
@@ -123,13 +127,27 @@ h1 {
   display: inline-block;
   margin-left: 5px;
   margin-right: 5px;
-}
-
-.portfolio .currency-logo {
+  
   height: 24px;
   width: 24px;
 }
 
+.balance > .btcValue {
+  font-size: 1.2em;
+}
+
+.balance > .variation {
+  font-size: 1.2em;
+}
+
+.balance > .row {
+  margin-bottom: 10px;
+}
+
+.assets > .row {
+  margin-bottom: 10px;
+}
+
 .balances > div {
   margin-bottom: 5px;
 }
index b4a18f4a6e5adf39218e93bf606fc46dd249011a..366391a7262c7c3756e0855a026d6cdc7eb96776 100644 (file)
@@ -3078,6 +3078,10 @@ module-deps@^5.0.1:
     through2 "^2.0.0"
     xtend "^4.0.0"
 
+moment@^2.22.1:
+  version "2.22.1"
+  resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.1.tgz#529a2e9bf973f259c9643d237fda84de3a26e8ad"
+
 mout@~0.9.0:
   version "0.9.1"
   resolved "https://registry.yarnpkg.com/mout/-/mout-0.9.1.tgz#84f0f3fd6acc7317f63de2affdcc0cee009b0477"
diff --git a/db/report_lines.go b/db/report_lines.go
new file mode 100644 (file)
index 0000000..274be57
--- /dev/null
@@ -0,0 +1,140 @@
+package db
+
+import (
+       "encoding/json"
+       "fmt"
+       "time"
+
+       "github.com/go-redis/redis"
+       "github.com/shopspring/decimal"
+)
+
+type PortfolioStep string
+
+const SELL_BEGIN PortfolioStep = "process_sell_all__1_all_sell_begin"
+const SELL_END PortfolioStep = "process_sell_all__1_all_sell_end"
+const BUY_BEGIN PortfolioStep = "process_sell_all__3_all_buy_begin"
+const BUY_END PortfolioStep = "process_sell_all__3_all_buy_end"
+const INTERMADIATE_STATE PortfolioStep = "process_print_balances__1_print_balances_begin"
+
+type ViewBalances struct {
+       Id       int64
+       Date     time.Time
+       ReportId int64
+       MarketId int64
+       Payload  struct {
+               Report
+       }
+}
+
+type Report struct {
+       Tag      PortfolioStep
+       Date     time.Time
+       Balances ReportBalances
+       Tickers  ReportTickers
+}
+
+type ReportTickers struct {
+       Currency string
+       Balances map[string]decimal.Decimal
+       Rates    BTCRates
+       Total    decimal.Decimal
+}
+
+func (r ReportTickers) GetBTCRate(currency string) decimal.Decimal {
+       if currency == "BTC" {
+               return decimal.NewFromFloat(1.0)
+       }
+
+       return r.Rates[currency]
+}
+
+type BTCRates map[string]decimal.Decimal
+
+func (b BTCRates) Rate(currency string) decimal.Decimal {
+       if currency == "BTC" {
+               return decimal.NewFromFloat(1.0)
+       }
+
+       return b[currency]
+}
+
+type ReportBalances map[string]struct {
+       Total            decimal.Decimal
+       MarginTotal      decimal.Decimal `json:"margin_total"`
+       MarginInPosition decimal.Decimal `json:"margin_in_position"`
+       MarginAvailable  decimal.Decimal `json:"margin_available"`
+}
+
+func RedisReportKey(marketConfigId int64, timestamp, entity string) string {
+       return fmt.Sprintf("/cryptoportfolio/%v/%v/%v", marketConfigId, timestamp, entity)
+}
+
+func GetLatestReport(marketConfig MarketConfig) (Report, error) {
+       var reportPayload Report
+       var err error
+       var key string
+
+       // Get balance.
+       key = RedisReportKey(marketConfig.Id, "latest", "balance")
+
+       payload, err := Redis.Get(key).Bytes()
+       if err == redis.Nil {
+               return Report{}, fmt.Errorf("cannot find '%s' redis key", key)
+       } else if err != nil {
+               return Report{}, err
+       }
+
+       // Get date.
+       key = RedisReportKey(marketConfig.Id, "latest", "date")
+       dateString, err := Redis.Get(key).Result()
+
+       if err == redis.Nil {
+               return Report{}, fmt.Errorf("cannot find '%s' redis key", key)
+       } else if err != nil {
+               return Report{}, err
+       }
+
+       reportPayload.Date, err = time.Parse("2006-01-02T15:04:05", dateString)
+       if err != nil {
+               return Report{}, err
+       }
+
+       err = json.Unmarshal(payload, &reportPayload)
+
+       return reportPayload, err
+}
+
+func GetPortfolioMilestones(marketConfig MarketConfig, step PortfolioStep, limit int) ([]Report, error) {
+       viewBalances := make([]ViewBalances, 0)
+       reports := make([]Report, 0)
+
+       err := DB.
+               Model(&viewBalances).
+               Where("market_id = ?", marketConfig.Id).
+               Where("payload @> ?", fmt.Sprintf(`{"tag": "%s", "checkpoint": "%s"}`, step, "begin")).
+               OrderExpr("date DESC").Limit(limit).Select()
+       if err != nil {
+               return nil, err
+       }
+
+       for _, reportLine := range viewBalances {
+               reportLine.Payload.Report.Date = reportLine.Date
+               reports = append(reports, reportLine.Payload.Report)
+       }
+
+       return reports, nil
+}
+
+func GetLastPortfolioBegin(marketConfig MarketConfig) (*Report, error) {
+       reports, err := GetPortfolioMilestones(marketConfig, BUY_END, 1)
+       if err != nil {
+               return nil, nil
+       }
+
+       if len(reports) == 0 {
+               return nil, nil
+       }
+
+       return &reports[0], nil
+}
index 9aaeafbfcb1cedc3f6812bb0cd34d61a97ff160d..58645b2b2a23750c4adae2633e7c7297bfb71d19 100644 (file)
@@ -52,6 +52,22 @@ func NewPoloniex() *Poloniex {
        }
 }
 
+func (p *Poloniex) TestCredentials(apiKey, apiSecret string) error {
+       client, _ := poloniex.NewClient(apiKey, apiSecret)
+
+       _, err := client.TradeReturnDepositAdresses()
+
+       if poloniexInvalidCredentialsError(err) {
+               return utils.Error{InvalidCredentials, "invalid poloniex credentials"}
+       }
+
+       if poloniexRestrictedIPError(err) {
+               return utils.Error{IPRestricted, "IP restricted api key"}
+       }
+
+       return nil
+}
+
 func (p *Poloniex) GetBalance(apiKey, apiSecret string) (Summary, error) {
        client, _ := poloniex.NewClient(apiKey, apiSecret)
        var summary Summary