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}
}
type Status uint32
type ErrorCode uint32
-const EXTERNAL_SERVICE_TIMEOUT_SECONDS = 10
+const EXTERNAL_SERVICE_TIMEOUT_SECONDS = 20
const (
OK Status = iota
"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 {
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)}
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
})
return nil, resultErr
}
- return &result, nil
+ return nil, nil
}
type UpdateMarketConfigQuery struct {
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
+}
--- /dev/null
+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
+}
[]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"},
},
},
}
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)
}
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)
}
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') {
}
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);
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': {
--- /dev/null
+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};
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 = () => {
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;
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>
}
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>
);
}
}
"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",
margin-top: 5px;
}
+.performance-up {
+ color: green;
+}
+
+.performance-down {
+ color: red;
+}
+
.sign-in .form-control {
margin-bottom: 20px;
border-radius: 2px;
.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;
}
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;
}
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"
--- /dev/null
+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
+}
}
}
+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