diff --git a/.eslintrc b/.eslintrc index 5dfd6363d2..5ec2210563 100644 --- a/.eslintrc +++ b/.eslintrc @@ -19,7 +19,7 @@ "rules": { "no-unused-vars": ["error", { "vars": "all", "args": "after-used" }], "comma-dangle": 0, - "indent": ["error", "tab", {SwitchCase: 1}], + "indent": ["error", 4, {SwitchCase: 1}], "quotes": [2, "double", "avoid-escape"], "semi": [2, "always"], "camelcase": [0], diff --git a/web/app/components/Exchange/Exchange.jsx b/web/app/components/Exchange/Exchange.jsx index 1308716ea0..168c231cc7 100644 --- a/web/app/components/Exchange/Exchange.jsx +++ b/web/app/components/Exchange/Exchange.jsx @@ -28,1262 +28,1262 @@ import ExchangeHeader from "./ExchangeHeader"; import Translate from "react-translate-component"; Highcharts.setOptions({ - global: { - useUTC: false - } + global: { + useUTC: false + } }); class Exchange extends React.Component { - constructor(props) { - super(); - - this.state = this._initialState(props); - - this._getWindowSize = debounce(this._getWindowSize.bind(this), 150); - } - - _initialState(props) { - let ws = props.viewSettings; - let bid = { - forSaleText: "", - toReceiveText: "", - priceText: "", - for_sale: new Asset({ - asset_id: props.baseAsset.get("id"), - precision: props.baseAsset.get("precision") - }), - to_receive: new Asset({ - asset_id: props.quoteAsset.get("id"), - precision: props.quoteAsset.get("precision") - }) - }; - bid.price = new Price({base: bid.for_sale, quote: bid.to_receive}); - let ask = { - forSaleText: "", - toReceiveText: "", - priceText: "", - for_sale: new Asset({ - asset_id: props.quoteAsset.get("id"), - precision: props.quoteAsset.get("precision") - }), - to_receive: new Asset({ - asset_id: props.baseAsset.get("id"), - precision: props.baseAsset.get("precision") - }) - }; - ask.price = new Price({base: ask.for_sale, quote: ask.to_receive}); - - /* Make sure the indicators objects only contains the current indicators */ - let savedIndicators = ws.get("indicators", {}); - let indicators = {}; - [["sma", true], ["ema1", false], ["ema2", false], ["smaVolume", true], ["macd", false], ["bb", false]].forEach(i => { - indicators[i[0]] = savedIndicators[i[0]] || i[1]; - }); - - let savedIndicatorsSettings = ws.get("indicatorSettings", {}); - let indicatorSettings = {}; - [["sma", 7], ["ema1", 20], ["ema2", 50], ["smaVolume", 30]].forEach(i => { - indicatorSettings[i[0]] = savedIndicatorsSettings[i[0]] || i[1]; - }); - - return { - history: [], - buySellOpen: ws.get("buySellOpen", true), - bid, - ask, - flipBuySell: ws.get("flipBuySell", false), - favorite: false, - showDepthChart: ws.get("showDepthChart", false), - leftOrderBook: ws.get("leftOrderBook", false), - buyDiff: false, - sellDiff: false, - indicators, - buySellTop: ws.get("buySellTop", true), - buyFeeAssetIdx: ws.get("buyFeeAssetIdx", 0), - sellFeeAssetIdx: ws.get("sellFeeAssetIdx", 0), - indicatorSettings, - tools: { - fib: false, - trendline: false - }, - height: window.innerHeight, - width: window.innerWidth, - chartHeight: ws.get("chartHeight", 425), - currentPeriod: ws.get("currentPeriod", 3600* 24 * 30 * 3) // 3 months - }; - } - - static propTypes = { - marketCallOrders: PropTypes.object.isRequired, - activeMarketHistory: PropTypes.object.isRequired, - viewSettings: PropTypes.object.isRequired, - priceData: PropTypes.array.isRequired, - volumeData: PropTypes.array.isRequired - }; - - static defaultProps = { - marketCallOrders: [], - activeMarketHistory: {}, - viewSettings: {}, - priceData: [], - volumeData: [] - }; - - componentDidMount() { - let centerContainer = this.refs.center; - if (centerContainer) { - Ps.initialize(centerContainer); - } - SettingsActions.changeViewSetting.defer({ - lastMarket: this.props.quoteAsset.get("symbol") + "_" + this.props.baseAsset.get("symbol") - }); - - window.addEventListener("resize", this._getWindowSize, false); - } - - shouldComponentUpdate(nextProps) { - if (!nextProps.marketReady && !this.props.marketReady) { - return false; - } - return true; - }; - - _getWindowSize() { - let { innerHeight, innerWidth } = window; - if (innerHeight !== this.state.height || innerWidth !== this.state.width) { - this.setState({ - height: innerHeight, - width: innerWidth - }); - } - } - - componentWillReceiveProps(nextProps) { - if (nextProps.quoteAsset.get("symbol") !== this.props.quoteAsset.get("symbol") || nextProps.baseAsset.get("symbol") !== this.props.baseAsset.get("symbol")) { - this.setState(this._initialState(nextProps)); - - return SettingsActions.changeViewSetting({ - lastMarket: nextProps.quoteAsset.get("symbol") + "_" + nextProps.baseAsset.get("symbol") - }); - } - - if (this.props.sub && nextProps.bucketSize !== this.props.bucketSize) { - return this._changeBucketSize(nextProps.bucketSize); - } - } - - componentWillUnmount() { - window.removeEventListener("resize", this._getWindowSize, false); - } - - _getFee(asset) { - let fee = utils.estimateFee("limit_order_create", [], ChainStore.getObject("2.0.0")) || 0; - const coreFee = new Asset({ - amount: fee - }); - if (!asset || asset.get("id") === "1.3.0") return coreFee; - - const cer = asset.getIn(["options", "core_exchange_rate"]).toJS(); - - const cerBase = new Asset({ - asset_id: cer.base.asset_id, - amount: cer.base.amount, - precision: ChainStore.getAsset(cer.base.asset_id).get("precision") - }); - const cerQuote = new Asset({ - asset_id: cer.quote.asset_id, - amount: cer.quote.amount, - precision: ChainStore.getAsset(cer.quote.asset_id).get("precision") - }); - const cerPrice = new Price({ - base: cerBase, quote: cerQuote - }); - const convertedFee = coreFee.times(cerPrice); - - return convertedFee; - } - - _verifyFee(fee, sellAmount, sellBalance, coreBalance) { - let coreFee = this._getFee(); - - let sellSum = fee.getAmount() + sellAmount; - if (fee.asset_id === "1.3.0") { - if (coreFee.getAmount() <= coreBalance) { - return "1.3.0"; - } else { - return null; - } - } else { - if (sellSum <= sellBalance) { // Sufficient balance in asset to pay fee - return fee.asset_id; - } else if (coreFee.getAmount() <= coreBalance && fee.asset_id !== "1.3.0") { // Sufficient balance in core asset to pay fee - return "1.3.0"; - } else { - return null; // Unable to pay fee in either asset - } - } - } - - _createLimitOrderConfirm(buyAsset, sellAsset, sellBalance, coreBalance, feeAsset, type, short = true, e) { - e.preventDefault(); - let {highestBid, lowestAsk} = this.props.marketData; - let current = this.state[type === "sell" ? "ask" : "bid"]; - - sellBalance = current.for_sale.clone(sellBalance ? parseInt(ChainStore.getObject(sellBalance).toJS().balance, 10) : 0); - coreBalance = new Asset({ - amount: coreBalance ? parseInt(ChainStore.getObject(coreBalance).toJS().balance, 10) : 0 - }); - - let fee = this._getFee(feeAsset); - - let feeID = this._verifyFee(fee, current.for_sale.getAmount(), sellBalance.getAmount(), coreBalance.getAmount()); - if (!feeID) { - return notify.addNotification({ - message: "Insufficient funds to pay fees", - level: "error" - }); - } - - if (type === "buy" && lowestAsk) { - let diff = this.state.bid.price.toReal() / lowestAsk.getPrice(); - if (diff > 1.20) { - this.refs.buy.show(); - return this.setState({ - buyDiff: diff - }); - } - } else if (type === "sell" && highestBid) { - let diff = 1 / (this.state.ask.price.toReal() / highestBid.getPrice()); - if (diff > 1.20) { - this.refs.sell.show(); - return this.setState({ - sellDiff: diff - }); - } - } - - let isPredictionMarket = sellAsset.getIn(["bitasset", "is_prediction_market"]); - - if (current.for_sale.gt(sellBalance) && !isPredictionMarket) { - return notify.addNotification({ - message: "Insufficient funds to place current. Required: " + current.for_sale.getAmount() + " " + sellAsset.get("symbol"), - level: "error" - }); - } - // - if (!(current.for_sale.getAmount() > 0 && current.to_receive.getAmount() > 0)) { - return notify.addNotification({ - message: "Please enter a valid amount and price", - level: "error" - }); - } - // - if (type === "sell" && isPredictionMarket && short) { - return this._createPredictionShort(feeID); - } - - - - this._createLimitOrder(type, feeID); - } - - _createLimitOrder(type, feeID) { - let current = this.state[type === "sell" ? "ask" : "bid"]; - const order = new LimitOrderCreate({ - for_sale: current.for_sale, - to_receive: current.to_receive, - seller: this.props.currentAccount.get("id"), - fee: { - asset_id: feeID, - amount: 0 - } - }); - - console.log("order:", JSON.stringify(order.toObject())); - return MarketsActions.createLimitOrder2(order).then((result) => { - if (result.error) { - if (result.error.message !== "wallet locked") - notify.addNotification({ - message: "Unknown error. Failed to place order for " + current.to_receive.getAmount({real: true}) + " " + current.to_receive.asset_id, - level: "error" - }); - } - // console.log("order success"); - }).catch(e => { - console.log("order failed:", e); - }); - } - - _createPredictionShort(feeID) { - let current = this.state.ask; - const order = new LimitOrderCreate({ - for_sale: current.for_sale, - to_receive: current.to_receive, - seller: this.props.currentAccount.get("id"), - fee: { - asset_id: feeID, - amount: 0 - } - }); - - Promise.all([ - FetchChain("getAsset", this.props.quoteAsset.getIn(["bitasset", "options", "short_backing_asset"])) - ]).then(assets => { - let [backingAsset] = assets; - let collateral = new Asset({ - amount: order.amount_for_sale.getAmount(), - asset_id: backingAsset.get("id"), - precision: backingAsset.get("precision") - }); - - MarketsActions.createPredictionShort( - order, - collateral - ).then(result => { - if (result.error) { - if (result.error.message !== "wallet locked") - notify.addNotification({ - message: "Unknown error. Failed to place order for " + buyAssetAmount + " " + buyAsset.symbol, - level: "error" - }); - } - }); - }); - } - - _forceBuy(type, feeAsset, sellBalance, coreBalance) { - let current = this.state[type === "sell" ? "ask" : "bid"]; - // Convert fee to relevant asset fee and check if user has sufficient balance - sellBalance = current.for_sale.clone(sellBalance ? parseInt(ChainStore.getObject(sellBalance).get("balance"), 10) : 0); - coreBalance = new Asset({ - amount: coreBalance ? parseInt(ChainStore.getObject(coreBalance).toJS().balance, 10) : 0 - }); - let fee = this._getFee(feeAsset); - let feeID = this._verifyFee(fee, current.for_sale.getAmount(), sellBalance.getAmount(), coreBalance.getAmount()); - - if (feeID) { - this._createLimitOrder(type, feeID); - } else { - console.error("Unable to pay fees, aborting limit order creation"); - } - } - - _forceSell(type, feeAsset, sellBalance, coreBalance) { - let current = this.state[type === "sell" ? "ask" : "bid"]; - // Convert fee to relevant asset fee and check if user has sufficient balance - sellBalance = current.for_sale.clone(sellBalance ? parseInt(ChainStore.getObject(sellBalance).get("balance"), 10) : 0); - coreBalance = new Asset({ - amount: coreBalance ? parseInt(ChainStore.getObject(coreBalance).toJS().balance, 10) : 0 - }); - let fee = this._getFee(feeAsset); - let feeID = this._verifyFee(fee, current.for_sale.getAmount(), sellBalance.getAmount(), coreBalance.getAmount()); - - if (feeID) { - this._createLimitOrder(type, feeID); - } else { - console.error("Unable to pay fees, aborting limit order creation"); - } - } - - _cancelLimitOrder(orderID, e) { - e.preventDefault(); - let { currentAccount } = this.props; - MarketsActions.cancelLimitOrder( - currentAccount.get("id"), - orderID // order id to cancel - ); - } - - _changeBucketSize(size, e) { - if (e) e.preventDefault(); - if (size !== this.props.bucketSize) { - MarketsActions.changeBucketSize.defer(size); - let currentSub = this.props.sub.split("_"); - MarketsActions.unSubscribeMarket.defer(currentSub[0], currentSub[1]); - this.props.subToMarket(this.props, size); - } - } - - _changeZoomPeriod(size, e) { - e.preventDefault(); - if (size !== this.state.currentPeriod) { - this.setState({ - currentPeriod: size - }); - SettingsActions.changeViewSetting({ - currentPeriod: size - }); - } - } - - _depthChartClick(base, quote, power, e) { - e.preventDefault(); - let {bid, ask} = this.state; - - bid.price = new Price({ - base: this.state.bid.for_sale, - quote: this.state.bid.to_receive, - real: e.xAxis[0].value / power - }); - bid.priceText = bid.price.toReal(); - - ask.price = new Price({ - base: this.state.ask.to_receive, - quote: this.state.ask.for_sale, - real: e.xAxis[0].value / power - }); - ask.priceText = ask.price.toReal(); - let newState = { - bid, - ask, - depthLine: bid.price.toReal() - }; - - this._setForSale(bid, true) || this._setReceive(bid, true); - this._setReceive(ask) || this._setForSale(ask); - - this.setState(newState); - } - - _flipBuySell() { - SettingsActions.changeViewSetting({ - flipBuySell: !this.state.flipBuySell - }); - - this.setState({ flipBuySell: !this.state.flipBuySell }); - } - - _toggleOpenBuySell() { - SettingsActions.changeViewSetting({ - buySellOpen: !this.state.buySellOpen - }); - - this.setState({ buySellOpen: !this.state.buySellOpen }); - } - - _toggleCharts() { - SettingsActions.changeViewSetting({ - showDepthChart: !this.state.showDepthChart - }); - - this.setState({ showDepthChart: !this.state.showDepthChart }); - } - - _moveOrderBook() { - SettingsActions.changeViewSetting({ - leftOrderBook: !this.state.leftOrderBook - }); - - this.setState({ leftOrderBook: !this.state.leftOrderBook }); - } - - _currentPriceClick(type, price) { - const isBid = type === "bid"; - let current = this.state[type]; - current.price = price[(isBid) ? "invert" : "clone"](); - current.priceText = current.price.toReal(); - if (isBid) { - this._setForSale(current, isBid) || this._setReceive(current, isBid); - } else { - this._setReceive(current, isBid) || this._setForSale(current, isBid); - } - this.forceUpdate(); - } - - _orderbookClick(order) { - const isBid = order.isBid(); - /* - * Because we are using a bid order to construct an ask and vice versa, - * totalToReceive becomes forSale, and totalForSale becomes toReceive - */ - let forSale = order.totalToReceive({noCache: true}); - // let toReceive = order.totalForSale({noCache: true}); - let toReceive = forSale.times(order.sellPrice()); - - let newPrice = new Price({ - base: isBid ? toReceive : forSale, - quote: isBid ? forSale : toReceive - }); - - let current = this.state[isBid ? "bid" : "ask"]; - current.price = newPrice; - current.priceText = newPrice.toReal(); - - let newState = { // If isBid is true, newState defines a new ask order and vice versa - [isBid ? "ask" : "bid"]: { - for_sale: forSale, - forSaleText: forSale.getAmount({real: true}), - to_receive: toReceive, - toReceiveText: toReceive.getAmount({real: true}), - price: newPrice, - priceText: newPrice.toReal() - } - }; - - if (isBid) { - this._setForSale(current, isBid) || this._setReceive(current, isBid); - } else { - this._setReceive(current, isBid) || this._setForSale(current, isBid); - } - this.setState(newState); - } - - _borrowQuote() { - this.refs.borrowQuote.show(); - } - - _borrowBase() { - this.refs.borrowBase.show(); - } - - _onSelectIndicators() { - this.refs.indicators.show(); - } - - _getSettlementInfo() { - let {lowestCallPrice, feedPrice, quoteAsset} = this.props; - - let showCallLimit = false; - if (feedPrice) { - if (feedPrice.inverted) { - showCallLimit = lowestCallPrice <= feedPrice.toReal(); - } else { - showCallLimit = lowestCallPrice >= feedPrice.toReal(); - } - } - return !!(showCallLimit && lowestCallPrice && !quoteAsset.getIn(["bitasset", "is_prediction_market"])); - } - - _changeIndicator(key) { - let indicators = cloneDeep(this.state.indicators); - indicators[key] = !indicators[key]; - this.setState({ - indicators - }); - - SettingsActions.changeViewSetting({ - indicators - }); - } - - _changeIndicatorSetting(key, e) { - e.preventDefault(); - let indicatorSettings = cloneDeep(this.state.indicatorSettings); - let value = parseInt(e.target.value, 10); - if (isNaN(value)) { - value = 1; - } - indicatorSettings[key] = value; - - this.setState({ - indicatorSettings: indicatorSettings - }); - - SettingsActions.changeViewSetting({ - indicatorSettings: indicatorSettings - }); - } - - onChangeFeeAsset(type, e) { - e.preventDefault(); - if (type === "buy") { - this.setState({ - buyFeeAssetIdx: e.target.value - }); - - SettingsActions.changeViewSetting({ - "buyFeeAssetIdx": e.target.value - }); - } else { - this.setState({ - sellFeeAssetIdx: e.target.value - }); - - SettingsActions.changeViewSetting({ - "sellFeeAssetIdx": e.target.value - }); - } - } - - onChangeChartHeight({value, increase}) { - let newHeight = value ? value : this.state.chartHeight + (increase ? 20 : -20); - console.log("newHeight", newHeight); - this.setState({ - chartHeight: newHeight - }); - - SettingsActions.changeViewSetting({ - "chartHeight": newHeight - }); - } - - _getFeeAssets(quote, base, coreAsset) { - let { currentAccount } = this.props; - - function addMissingAsset(target, asset) { - if (target.indexOf(asset) === -1) { - target.push(asset); - } - } - - let sellFeeAssets = [coreAsset, quote === coreAsset ? base : quote]; - addMissingAsset(sellFeeAssets, quote); - addMissingAsset(sellFeeAssets, base); - let sellFeeAsset; - - let buyFeeAssets = [coreAsset, base === coreAsset ? quote : base]; - addMissingAsset(buyFeeAssets, quote); - addMissingAsset(buyFeeAssets, base); - let buyFeeAsset; - - let balances = {}; - - currentAccount.get("balances", []).filter((balance, id) => { - return (["1.3.0", quote.get("id"), base.get("id")].indexOf(id) >= 0); - }).forEach((balance, id) => { - let balanceObject = ChainStore.getObject(balance); - balances[id] = { - balance: balanceObject ? parseInt(balanceObject.get("balance"), 10) : 0, - fee: this._getFee(ChainStore.getAsset(id)) - }; - }); - - // Sell asset fee - sellFeeAssets = sellFeeAssets.filter(a => { - if (!balances[a.get("id")]) { - return false; - }; - return balances[a.get("id")].balance > balances[a.get("id")].fee.getAmount(); - }); - - if (!sellFeeAssets.length) { - sellFeeAsset = coreAsset; - sellFeeAssets.push(coreAsset); - } else { - sellFeeAsset = sellFeeAssets[Math.min(sellFeeAssets.length - 1, this.state.sellFeeAssetIdx)]; - } - - // Buy asset fee - buyFeeAssets = buyFeeAssets.filter(a => { - if (!balances[a.get("id")]) { - return false; - }; - return balances[a.get("id")].balance > balances[a.get("id")].fee.getAmount(); - }); - - if (!buyFeeAssets.length) { - buyFeeAsset = coreAsset; - buyFeeAssets.push(coreAsset); - } else { - buyFeeAsset = buyFeeAssets[Math.min(buyFeeAssets.length - 1, this.state.buyFeeAssetIdx)]; - } - - let sellFee = this._getFee(sellFeeAsset); - let buyFee = this._getFee(buyFeeAsset); - - return { - sellFeeAsset, - sellFeeAssets, - sellFee, - buyFeeAsset, - buyFeeAssets, - buyFee - }; - } - - _toggleBuySellPosition() { - this.setState({ - buySellTop: !this.state.buySellTop - }); - - SettingsActions.changeViewSetting({ - buySellTop: !this.state.buySellTop - }); - } - - _setReceive(state, isBid) { - if (state.price.isValid() && state.for_sale.hasAmount()) { - state.to_receive = state.for_sale.times(state.price, isBid); - state.toReceiveText = state.to_receive.getAmount({real: true}).toString(); - return true; - } - return false; - } - - _setForSale(state, isBid) { - if (state.price.isValid() && state.to_receive.hasAmount()) { - state.for_sale = state.to_receive.times(state.price, isBid); - state.forSaleText = state.for_sale.getAmount({real: true}).toString(); - return true; - } - return false; - } - - _setPrice(state) { - if (state.for_sale.hasAmount() && state.to_receive.hasAmount()) { - state.price = new Price({ - base: state.for_sale, - quote: state.to_receive - }); - state.priceText = state.price.toReal().toString(); - return true; - } - return false; - } - - _onInputPrice(type, e) { - let current = this.state[type]; - const isBid = type === "bid"; - current.price = new Price({ - base: current[isBid ? "for_sale" : "to_receive"], - quote: current[isBid ? "to_receive" : "for_sale"], - real: parseFloat(e.target.value) || 0 - }); - - if (isBid) { - this._setForSale(current, isBid) || this._setReceive(current, isBid); - } else { - this._setReceive(current, isBid) || this._setForSale(current, isBid); - } - - current.priceText = e.target.value; - this.forceUpdate(); - } - - _onInputSell(type, e) { - let current = this.state[type]; - const isBid = type === "bid"; - current.for_sale.setAmount({real: parseFloat(e.target.value) || 0}); - - if (current.price.isValid()) { - this._setReceive(current, isBid); - } else { - this._setPrice(current); - } - - current.forSaleText = e.target.value; - this.forceUpdate(); - } - - _onInputReceive(type, e) { - let current = this.state[type]; - const isBid = type === "bid"; - current.to_receive.setAmount({real: parseFloat(e.target.value) || 0}); - - if (current.price.isValid()) { - this._setForSale(current, isBid); - } else { - this._setPrice(current); - } - - current.toReceiveText = e.target.value; - this.forceUpdate(); - } - - isMarketFrozen() { - let {baseAsset, quoteAsset} = this.props; - - let baseWhiteList = baseAsset.getIn(["options", "whitelist_markets"]).toJS(); - let quoteWhiteList = quoteAsset.getIn(["options", "whitelist_markets"]).toJS(); - let baseBlackList = baseAsset.getIn(["options", "blacklist_markets"]).toJS(); - let quoteBlackList = quoteAsset.getIn(["options", "blacklist_markets"]).toJS(); - - if (quoteWhiteList.length && quoteWhiteList.indexOf(baseAsset.get("id") === -1)) { - return {isFrozen: true, frozenAsset: quoteAsset.get("symbol")}; - } - if (baseWhiteList.length && baseWhiteList.indexOf(quoteAsset.get("id") === -1)) { - return {isFrozen: true, frozenAsset: baseAsset.get("symbol")}; - } - - if (quoteBlackList.length && quoteBlackList.indexOf(baseAsset.get("id") !== -1)) { - return {isFrozen: true, frozenAsset: quoteAsset.get("symbol")}; - } - if (baseBlackList.length && baseBlackList.indexOf(quoteAsset.get("id") !== -1)) { - return {isFrozen: true, frozenAsset: baseAsset.get("symbol")}; - } - - return {isFrozen: false}; - } - - render() { - let { currentAccount, marketLimitOrders, marketCallOrders, marketData, activeMarketHistory, - invertedCalls, starredMarkets, quoteAsset, baseAsset, lowestCallPrice, - marketStats, marketReady, marketSettleOrders, bucketSize, totals, - feedPrice, buckets } = this.props; - - const {combinedBids, combinedAsks, lowestAsk, highestBid, - flatBids, flatAsks, flatCalls, flatSettles} = marketData; - - let {bid, ask, leftOrderBook, showDepthChart, tools, chartHeight, - buyDiff, sellDiff, indicators, indicatorSettings, width, buySellTop} = this.state; - const {isFrozen, frozenAsset} = this.isMarketFrozen(); - - let base = null, quote = null, accountBalance = null, quoteBalance = null, - baseBalance = null, coreBalance = null, quoteSymbol, baseSymbol, - showCallLimit = false, latestPrice, changeClass; - - - let isNullAccount = currentAccount.get("id") === "1.2.3"; - - const showVolumeChart = this.props.viewSettings.get("showVolumeChart", true); - - if (quoteAsset.size && baseAsset.size && currentAccount.size) { - base = baseAsset; - quote = quoteAsset; - baseSymbol = base.get("symbol"); - quoteSymbol = quote.get("symbol"); - - accountBalance = currentAccount.get("balances").toJS(); - - if (accountBalance) { - for (let id in accountBalance) { - if (id === quote.get("id")) { - quoteBalance = accountBalance[id]; - } - if (id === base.get("id")) { - baseBalance = accountBalance[id]; - } - if (id === "1.3.0") { - coreBalance = accountBalance[id]; - } - } - } - - showCallLimit = this._getSettlementInfo(); - } - - let quoteIsBitAsset = quoteAsset.get("bitasset_data_id") ? true : false; - let baseIsBitAsset = baseAsset.get("bitasset_data_id") ? true : false; - - let spread = (lowestAsk && highestBid) ? lowestAsk.getPrice() - highestBid.getPrice() : 0; - - // Latest price - if (activeMarketHistory.size) { - // Orders come in pairs, first is driver. Third entry is first of second pair. - let latest_two = activeMarketHistory.take(3); - let latest = latest_two.first(); - let second_latest = latest_two.last(); - let paysAsset, receivesAsset, isAsk = false; - if (latest.pays.asset_id === base.get("id")) { - paysAsset = base; - receivesAsset = quote; - isAsk = true; - } else { - paysAsset = quote; - receivesAsset = base; - } - let flipped = base.get("id").split(".")[2] > quote.get("id").split(".")[2]; - latestPrice = market_utils.parse_order_history(latest, paysAsset, receivesAsset, isAsk, flipped); - - isAsk = false; - if (second_latest) { - if (second_latest.pays.asset_id === base.get("id")) { - paysAsset = base; - receivesAsset = quote; - isAsk = true; - } else { - paysAsset = quote; - receivesAsset = base; - } - - let oldPrice = market_utils.parse_order_history(second_latest, paysAsset, receivesAsset, isAsk, flipped); - changeClass = latestPrice.full === oldPrice.full ? "" : latestPrice.full - oldPrice.full > 0 ? "change-up" : "change-down"; - } - } - - // Fees - let coreAsset = ChainStore.getAsset("1.3.0"); - if (!coreAsset) { - return null; - } - - let { - sellFeeAsset, - sellFeeAssets, - sellFee, - buyFeeAsset, - buyFeeAssets, - buyFee - } = this._getFeeAssets(quote, base, coreAsset); - - // Decimals - let hasPrediction = base.getIn(["bitasset", "is_prediction_market"]) || quote.getIn(["bitasset", "is_prediction_market"]); - - let description = null; - - if (hasPrediction) { - description = quoteAsset.getIn(["options", "description"]); - description = assetUtils.parseDescription(description).main; - } - - let smallScreen = false; - if (width < 1000) { - smallScreen = true; - leftOrderBook = false; - } - - let orderMultiplier = leftOrderBook ? 2 : 1; - - let buyForm = isFrozen ? null : ( - - ); - - let sellForm = isFrozen ? null : ( - - ); - - let orderBook = ( - - ); - - return ( -
- - {/* Main vertical block with content */} - - {/* Left Column - Open Orders */} - {leftOrderBook ? ( -
- {orderBook} -
) : null} - - {/* Center Column */} -
- - {/* Top bar with info */} - { - let tools = cloneDeep(this.state.tools); - for (let k in tools) { - if (k === key) { - tools[k] = !tools[k]; - } else { - tools[k] = false; - } - } - this.setState({tools}, () => { - this.setState({tools: {fib: false, trendline: false}}); - }); - }} - onChangeChartHeight={this.onChangeChartHeight.bind(this)} - chartHeight={chartHeight} - showVolumeChart={showVolumeChart} - onToggleVolume={() => {SettingsActions.changeViewSetting({showVolumeChart: !showVolumeChart});}} - onChangeIndicatorSetting={this._changeIndicatorSetting.bind(this)} - indicatorSettings={indicatorSettings} - /> - -
- {!showDepthChart ? ( -
- {/* Price history chart */} - 1100 ? chartHeight : chartHeight - 125} - leftOrderBook={leftOrderBook} - marketReady={marketReady} - indicators={indicators} - indicatorSettings={indicatorSettings} - bucketSize={bucketSize} - latest={latestPrice} - verticalOrderbook={leftOrderBook} - theme={this.props.settings.get("themes")} - zoom={this.state.currentPeriod} - tools={tools} - showVolumeChart={showVolumeChart} - /> -
) : ( -
- 1100 ? this.state.chartHeight : this.state.chartHeight - 125} - onClick={this._depthChartClick.bind(this, base, quote)} - settlementPrice={(!hasPrediction && feedPrice) && feedPrice.toReal()} - spread={spread} - LCP={showCallLimit ? lowestCallPrice : null} - leftOrderBook={leftOrderBook} - hasPrediction={hasPrediction} - noFrame={false} - verticalOrderbook={leftOrderBook} - theme={this.props.settings.get("themes")} - /> -
)} - -
- {hasPrediction ?

{description}

: null} - - {isFrozen ?
: null} - {buyForm} - {sellForm} - - - - {!leftOrderBook ? orderBook : null} - - - - - - {marketLimitOrders.size > 0 && base && quote ? ( - ) : null} -
- - - {/* Settle Orders */} - - {(base.get("id") === "1.3.0" || quote.get("id") === "1.3.0") ? ( - ) : null} - - -
{ /* end CenterContent */} - - -
{/* End of Main Content Column */} - - {/* Right Column - Market History */} -
- {/* Market History */} -
- -
- {/*
- -
*/} -
- - {!isNullAccount && quoteIsBitAsset ? - : null} - {!isNullAccount && baseIsBitAsset ? - : null} - {/* End of Second Vertical Block */} -
- ); - } + constructor(props) { + super(); + + this.state = this._initialState(props); + + this._getWindowSize = debounce(this._getWindowSize.bind(this), 150); + } + + _initialState(props) { + let ws = props.viewSettings; + let bid = { + forSaleText: "", + toReceiveText: "", + priceText: "", + for_sale: new Asset({ + asset_id: props.baseAsset.get("id"), + precision: props.baseAsset.get("precision") + }), + to_receive: new Asset({ + asset_id: props.quoteAsset.get("id"), + precision: props.quoteAsset.get("precision") + }) + }; + bid.price = new Price({base: bid.for_sale, quote: bid.to_receive}); + let ask = { + forSaleText: "", + toReceiveText: "", + priceText: "", + for_sale: new Asset({ + asset_id: props.quoteAsset.get("id"), + precision: props.quoteAsset.get("precision") + }), + to_receive: new Asset({ + asset_id: props.baseAsset.get("id"), + precision: props.baseAsset.get("precision") + }) + }; + ask.price = new Price({base: ask.for_sale, quote: ask.to_receive}); + + /* Make sure the indicators objects only contains the current indicators */ + let savedIndicators = ws.get("indicators", {}); + let indicators = {}; + [["sma", true], ["ema1", false], ["ema2", false], ["smaVolume", true], ["macd", false], ["bb", false]].forEach(i => { + indicators[i[0]] = savedIndicators[i[0]] || i[1]; + }); + + let savedIndicatorsSettings = ws.get("indicatorSettings", {}); + let indicatorSettings = {}; + [["sma", 7], ["ema1", 20], ["ema2", 50], ["smaVolume", 30]].forEach(i => { + indicatorSettings[i[0]] = savedIndicatorsSettings[i[0]] || i[1]; + }); + + return { + history: [], + buySellOpen: ws.get("buySellOpen", true), + bid, + ask, + flipBuySell: ws.get("flipBuySell", false), + favorite: false, + showDepthChart: ws.get("showDepthChart", false), + leftOrderBook: ws.get("leftOrderBook", false), + buyDiff: false, + sellDiff: false, + indicators, + buySellTop: ws.get("buySellTop", true), + buyFeeAssetIdx: ws.get("buyFeeAssetIdx", 0), + sellFeeAssetIdx: ws.get("sellFeeAssetIdx", 0), + indicatorSettings, + tools: { + fib: false, + trendline: false + }, + height: window.innerHeight, + width: window.innerWidth, + chartHeight: ws.get("chartHeight", 425), + currentPeriod: ws.get("currentPeriod", 3600* 24 * 30 * 3) // 3 months + }; + } + + static propTypes = { + marketCallOrders: PropTypes.object.isRequired, + activeMarketHistory: PropTypes.object.isRequired, + viewSettings: PropTypes.object.isRequired, + priceData: PropTypes.array.isRequired, + volumeData: PropTypes.array.isRequired + }; + + static defaultProps = { + marketCallOrders: [], + activeMarketHistory: {}, + viewSettings: {}, + priceData: [], + volumeData: [] + }; + + componentDidMount() { + let centerContainer = this.refs.center; + if (centerContainer) { + Ps.initialize(centerContainer); + } + SettingsActions.changeViewSetting.defer({ + lastMarket: this.props.quoteAsset.get("symbol") + "_" + this.props.baseAsset.get("symbol") + }); + + window.addEventListener("resize", this._getWindowSize, false); + } + + shouldComponentUpdate(nextProps) { + if (!nextProps.marketReady && !this.props.marketReady) { + return false; + } + return true; + }; + + _getWindowSize() { + let { innerHeight, innerWidth } = window; + if (innerHeight !== this.state.height || innerWidth !== this.state.width) { + this.setState({ + height: innerHeight, + width: innerWidth + }); + } + } + + componentWillReceiveProps(nextProps) { + if (nextProps.quoteAsset.get("symbol") !== this.props.quoteAsset.get("symbol") || nextProps.baseAsset.get("symbol") !== this.props.baseAsset.get("symbol")) { + this.setState(this._initialState(nextProps)); + + return SettingsActions.changeViewSetting({ + lastMarket: nextProps.quoteAsset.get("symbol") + "_" + nextProps.baseAsset.get("symbol") + }); + } + + if (this.props.sub && nextProps.bucketSize !== this.props.bucketSize) { + return this._changeBucketSize(nextProps.bucketSize); + } + } + + componentWillUnmount() { + window.removeEventListener("resize", this._getWindowSize, false); + } + + _getFee(asset) { + let fee = utils.estimateFee("limit_order_create", [], ChainStore.getObject("2.0.0")) || 0; + const coreFee = new Asset({ + amount: fee + }); + if (!asset || asset.get("id") === "1.3.0") return coreFee; + + const cer = asset.getIn(["options", "core_exchange_rate"]).toJS(); + + const cerBase = new Asset({ + asset_id: cer.base.asset_id, + amount: cer.base.amount, + precision: ChainStore.getAsset(cer.base.asset_id).get("precision") + }); + const cerQuote = new Asset({ + asset_id: cer.quote.asset_id, + amount: cer.quote.amount, + precision: ChainStore.getAsset(cer.quote.asset_id).get("precision") + }); + const cerPrice = new Price({ + base: cerBase, quote: cerQuote + }); + const convertedFee = coreFee.times(cerPrice); + + return convertedFee; + } + + _verifyFee(fee, sellAmount, sellBalance, coreBalance) { + let coreFee = this._getFee(); + + let sellSum = fee.getAmount() + sellAmount; + if (fee.asset_id === "1.3.0") { + if (coreFee.getAmount() <= coreBalance) { + return "1.3.0"; + } else { + return null; + } + } else { + if (sellSum <= sellBalance) { // Sufficient balance in asset to pay fee + return fee.asset_id; + } else if (coreFee.getAmount() <= coreBalance && fee.asset_id !== "1.3.0") { // Sufficient balance in core asset to pay fee + return "1.3.0"; + } else { + return null; // Unable to pay fee in either asset + } + } + } + + _createLimitOrderConfirm(buyAsset, sellAsset, sellBalance, coreBalance, feeAsset, type, short = true, e) { + e.preventDefault(); + let {highestBid, lowestAsk} = this.props.marketData; + let current = this.state[type === "sell" ? "ask" : "bid"]; + + sellBalance = current.for_sale.clone(sellBalance ? parseInt(ChainStore.getObject(sellBalance).toJS().balance, 10) : 0); + coreBalance = new Asset({ + amount: coreBalance ? parseInt(ChainStore.getObject(coreBalance).toJS().balance, 10) : 0 + }); + + let fee = this._getFee(feeAsset); + + let feeID = this._verifyFee(fee, current.for_sale.getAmount(), sellBalance.getAmount(), coreBalance.getAmount()); + if (!feeID) { + return notify.addNotification({ + message: "Insufficient funds to pay fees", + level: "error" + }); + } + + if (type === "buy" && lowestAsk) { + let diff = this.state.bid.price.toReal() / lowestAsk.getPrice(); + if (diff > 1.20) { + this.refs.buy.show(); + return this.setState({ + buyDiff: diff + }); + } + } else if (type === "sell" && highestBid) { + let diff = 1 / (this.state.ask.price.toReal() / highestBid.getPrice()); + if (diff > 1.20) { + this.refs.sell.show(); + return this.setState({ + sellDiff: diff + }); + } + } + + let isPredictionMarket = sellAsset.getIn(["bitasset", "is_prediction_market"]); + + if (current.for_sale.gt(sellBalance) && !isPredictionMarket) { + return notify.addNotification({ + message: "Insufficient funds to place current. Required: " + current.for_sale.getAmount() + " " + sellAsset.get("symbol"), + level: "error" + }); + } + // + if (!(current.for_sale.getAmount() > 0 && current.to_receive.getAmount() > 0)) { + return notify.addNotification({ + message: "Please enter a valid amount and price", + level: "error" + }); + } + // + if (type === "sell" && isPredictionMarket && short) { + return this._createPredictionShort(feeID); + } + + + + this._createLimitOrder(type, feeID); + } + + _createLimitOrder(type, feeID) { + let current = this.state[type === "sell" ? "ask" : "bid"]; + const order = new LimitOrderCreate({ + for_sale: current.for_sale, + to_receive: current.to_receive, + seller: this.props.currentAccount.get("id"), + fee: { + asset_id: feeID, + amount: 0 + } + }); + + console.log("order:", JSON.stringify(order.toObject())); + return MarketsActions.createLimitOrder2(order).then((result) => { + if (result.error) { + if (result.error.message !== "wallet locked") + notify.addNotification({ + message: "Unknown error. Failed to place order for " + current.to_receive.getAmount({real: true}) + " " + current.to_receive.asset_id, + level: "error" + }); + } + // console.log("order success"); + }).catch(e => { + console.log("order failed:", e); + }); + } + + _createPredictionShort(feeID) { + let current = this.state.ask; + const order = new LimitOrderCreate({ + for_sale: current.for_sale, + to_receive: current.to_receive, + seller: this.props.currentAccount.get("id"), + fee: { + asset_id: feeID, + amount: 0 + } + }); + + Promise.all([ + FetchChain("getAsset", this.props.quoteAsset.getIn(["bitasset", "options", "short_backing_asset"])) + ]).then(assets => { + let [backingAsset] = assets; + let collateral = new Asset({ + amount: order.amount_for_sale.getAmount(), + asset_id: backingAsset.get("id"), + precision: backingAsset.get("precision") + }); + + MarketsActions.createPredictionShort( + order, + collateral + ).then(result => { + if (result.error) { + if (result.error.message !== "wallet locked") + notify.addNotification({ + message: "Unknown error. Failed to place order for " + buyAssetAmount + " " + buyAsset.symbol, + level: "error" + }); + } + }); + }); + } + + _forceBuy(type, feeAsset, sellBalance, coreBalance) { + let current = this.state[type === "sell" ? "ask" : "bid"]; + // Convert fee to relevant asset fee and check if user has sufficient balance + sellBalance = current.for_sale.clone(sellBalance ? parseInt(ChainStore.getObject(sellBalance).get("balance"), 10) : 0); + coreBalance = new Asset({ + amount: coreBalance ? parseInt(ChainStore.getObject(coreBalance).toJS().balance, 10) : 0 + }); + let fee = this._getFee(feeAsset); + let feeID = this._verifyFee(fee, current.for_sale.getAmount(), sellBalance.getAmount(), coreBalance.getAmount()); + + if (feeID) { + this._createLimitOrder(type, feeID); + } else { + console.error("Unable to pay fees, aborting limit order creation"); + } + } + + _forceSell(type, feeAsset, sellBalance, coreBalance) { + let current = this.state[type === "sell" ? "ask" : "bid"]; + // Convert fee to relevant asset fee and check if user has sufficient balance + sellBalance = current.for_sale.clone(sellBalance ? parseInt(ChainStore.getObject(sellBalance).get("balance"), 10) : 0); + coreBalance = new Asset({ + amount: coreBalance ? parseInt(ChainStore.getObject(coreBalance).toJS().balance, 10) : 0 + }); + let fee = this._getFee(feeAsset); + let feeID = this._verifyFee(fee, current.for_sale.getAmount(), sellBalance.getAmount(), coreBalance.getAmount()); + + if (feeID) { + this._createLimitOrder(type, feeID); + } else { + console.error("Unable to pay fees, aborting limit order creation"); + } + } + + _cancelLimitOrder(orderID, e) { + e.preventDefault(); + let { currentAccount } = this.props; + MarketsActions.cancelLimitOrder( + currentAccount.get("id"), + orderID // order id to cancel + ); + } + + _changeBucketSize(size, e) { + if (e) e.preventDefault(); + if (size !== this.props.bucketSize) { + MarketsActions.changeBucketSize.defer(size); + let currentSub = this.props.sub.split("_"); + MarketsActions.unSubscribeMarket.defer(currentSub[0], currentSub[1]); + this.props.subToMarket(this.props, size); + } + } + + _changeZoomPeriod(size, e) { + e.preventDefault(); + if (size !== this.state.currentPeriod) { + this.setState({ + currentPeriod: size + }); + SettingsActions.changeViewSetting({ + currentPeriod: size + }); + } + } + + _depthChartClick(base, quote, power, e) { + e.preventDefault(); + let {bid, ask} = this.state; + + bid.price = new Price({ + base: this.state.bid.for_sale, + quote: this.state.bid.to_receive, + real: e.xAxis[0].value / power + }); + bid.priceText = bid.price.toReal(); + + ask.price = new Price({ + base: this.state.ask.to_receive, + quote: this.state.ask.for_sale, + real: e.xAxis[0].value / power + }); + ask.priceText = ask.price.toReal(); + let newState = { + bid, + ask, + depthLine: bid.price.toReal() + }; + + this._setForSale(bid, true) || this._setReceive(bid, true); + this._setReceive(ask) || this._setForSale(ask); + + this.setState(newState); + } + + _flipBuySell() { + SettingsActions.changeViewSetting({ + flipBuySell: !this.state.flipBuySell + }); + + this.setState({ flipBuySell: !this.state.flipBuySell }); + } + + _toggleOpenBuySell() { + SettingsActions.changeViewSetting({ + buySellOpen: !this.state.buySellOpen + }); + + this.setState({ buySellOpen: !this.state.buySellOpen }); + } + + _toggleCharts() { + SettingsActions.changeViewSetting({ + showDepthChart: !this.state.showDepthChart + }); + + this.setState({ showDepthChart: !this.state.showDepthChart }); + } + + _moveOrderBook() { + SettingsActions.changeViewSetting({ + leftOrderBook: !this.state.leftOrderBook + }); + + this.setState({ leftOrderBook: !this.state.leftOrderBook }); + } + + _currentPriceClick(type, price) { + const isBid = type === "bid"; + let current = this.state[type]; + current.price = price[(isBid) ? "invert" : "clone"](); + current.priceText = current.price.toReal(); + if (isBid) { + this._setForSale(current, isBid) || this._setReceive(current, isBid); + } else { + this._setReceive(current, isBid) || this._setForSale(current, isBid); + } + this.forceUpdate(); + } + + _orderbookClick(order) { + const isBid = order.isBid(); + /* + * Because we are using a bid order to construct an ask and vice versa, + * totalToReceive becomes forSale, and totalForSale becomes toReceive + */ + let forSale = order.totalToReceive({noCache: true}); + // let toReceive = order.totalForSale({noCache: true}); + let toReceive = forSale.times(order.sellPrice()); + + let newPrice = new Price({ + base: isBid ? toReceive : forSale, + quote: isBid ? forSale : toReceive + }); + + let current = this.state[isBid ? "bid" : "ask"]; + current.price = newPrice; + current.priceText = newPrice.toReal(); + + let newState = { // If isBid is true, newState defines a new ask order and vice versa + [isBid ? "ask" : "bid"]: { + for_sale: forSale, + forSaleText: forSale.getAmount({real: true}), + to_receive: toReceive, + toReceiveText: toReceive.getAmount({real: true}), + price: newPrice, + priceText: newPrice.toReal() + } + }; + + if (isBid) { + this._setForSale(current, isBid) || this._setReceive(current, isBid); + } else { + this._setReceive(current, isBid) || this._setForSale(current, isBid); + } + this.setState(newState); + } + + _borrowQuote() { + this.refs.borrowQuote.show(); + } + + _borrowBase() { + this.refs.borrowBase.show(); + } + + _onSelectIndicators() { + this.refs.indicators.show(); + } + + _getSettlementInfo() { + let {lowestCallPrice, feedPrice, quoteAsset} = this.props; + + let showCallLimit = false; + if (feedPrice) { + if (feedPrice.inverted) { + showCallLimit = lowestCallPrice <= feedPrice.toReal(); + } else { + showCallLimit = lowestCallPrice >= feedPrice.toReal(); + } + } + return !!(showCallLimit && lowestCallPrice && !quoteAsset.getIn(["bitasset", "is_prediction_market"])); + } + + _changeIndicator(key) { + let indicators = cloneDeep(this.state.indicators); + indicators[key] = !indicators[key]; + this.setState({ + indicators + }); + + SettingsActions.changeViewSetting({ + indicators + }); + } + + _changeIndicatorSetting(key, e) { + e.preventDefault(); + let indicatorSettings = cloneDeep(this.state.indicatorSettings); + let value = parseInt(e.target.value, 10); + if (isNaN(value)) { + value = 1; + } + indicatorSettings[key] = value; + + this.setState({ + indicatorSettings: indicatorSettings + }); + + SettingsActions.changeViewSetting({ + indicatorSettings: indicatorSettings + }); + } + + onChangeFeeAsset(type, e) { + e.preventDefault(); + if (type === "buy") { + this.setState({ + buyFeeAssetIdx: e.target.value + }); + + SettingsActions.changeViewSetting({ + "buyFeeAssetIdx": e.target.value + }); + } else { + this.setState({ + sellFeeAssetIdx: e.target.value + }); + + SettingsActions.changeViewSetting({ + "sellFeeAssetIdx": e.target.value + }); + } + } + + onChangeChartHeight({value, increase}) { + let newHeight = value ? value : this.state.chartHeight + (increase ? 20 : -20); + console.log("newHeight", newHeight); + this.setState({ + chartHeight: newHeight + }); + + SettingsActions.changeViewSetting({ + "chartHeight": newHeight + }); + } + + _getFeeAssets(quote, base, coreAsset) { + let { currentAccount } = this.props; + + function addMissingAsset(target, asset) { + if (target.indexOf(asset) === -1) { + target.push(asset); + } + } + + let sellFeeAssets = [coreAsset, quote === coreAsset ? base : quote]; + addMissingAsset(sellFeeAssets, quote); + addMissingAsset(sellFeeAssets, base); + let sellFeeAsset; + + let buyFeeAssets = [coreAsset, base === coreAsset ? quote : base]; + addMissingAsset(buyFeeAssets, quote); + addMissingAsset(buyFeeAssets, base); + let buyFeeAsset; + + let balances = {}; + + currentAccount.get("balances", []).filter((balance, id) => { + return (["1.3.0", quote.get("id"), base.get("id")].indexOf(id) >= 0); + }).forEach((balance, id) => { + let balanceObject = ChainStore.getObject(balance); + balances[id] = { + balance: balanceObject ? parseInt(balanceObject.get("balance"), 10) : 0, + fee: this._getFee(ChainStore.getAsset(id)) + }; + }); + + // Sell asset fee + sellFeeAssets = sellFeeAssets.filter(a => { + if (!balances[a.get("id")]) { + return false; + }; + return balances[a.get("id")].balance > balances[a.get("id")].fee.getAmount(); + }); + + if (!sellFeeAssets.length) { + sellFeeAsset = coreAsset; + sellFeeAssets.push(coreAsset); + } else { + sellFeeAsset = sellFeeAssets[Math.min(sellFeeAssets.length - 1, this.state.sellFeeAssetIdx)]; + } + + // Buy asset fee + buyFeeAssets = buyFeeAssets.filter(a => { + if (!balances[a.get("id")]) { + return false; + }; + return balances[a.get("id")].balance > balances[a.get("id")].fee.getAmount(); + }); + + if (!buyFeeAssets.length) { + buyFeeAsset = coreAsset; + buyFeeAssets.push(coreAsset); + } else { + buyFeeAsset = buyFeeAssets[Math.min(buyFeeAssets.length - 1, this.state.buyFeeAssetIdx)]; + } + + let sellFee = this._getFee(sellFeeAsset); + let buyFee = this._getFee(buyFeeAsset); + + return { + sellFeeAsset, + sellFeeAssets, + sellFee, + buyFeeAsset, + buyFeeAssets, + buyFee + }; + } + + _toggleBuySellPosition() { + this.setState({ + buySellTop: !this.state.buySellTop + }); + + SettingsActions.changeViewSetting({ + buySellTop: !this.state.buySellTop + }); + } + + _setReceive(state, isBid) { + if (state.price.isValid() && state.for_sale.hasAmount()) { + state.to_receive = state.for_sale.times(state.price, isBid); + state.toReceiveText = state.to_receive.getAmount({real: true}).toString(); + return true; + } + return false; + } + + _setForSale(state, isBid) { + if (state.price.isValid() && state.to_receive.hasAmount()) { + state.for_sale = state.to_receive.times(state.price, isBid); + state.forSaleText = state.for_sale.getAmount({real: true}).toString(); + return true; + } + return false; + } + + _setPrice(state) { + if (state.for_sale.hasAmount() && state.to_receive.hasAmount()) { + state.price = new Price({ + base: state.for_sale, + quote: state.to_receive + }); + state.priceText = state.price.toReal().toString(); + return true; + } + return false; + } + + _onInputPrice(type, e) { + let current = this.state[type]; + const isBid = type === "bid"; + current.price = new Price({ + base: current[isBid ? "for_sale" : "to_receive"], + quote: current[isBid ? "to_receive" : "for_sale"], + real: parseFloat(e.target.value) || 0 + }); + + if (isBid) { + this._setForSale(current, isBid) || this._setReceive(current, isBid); + } else { + this._setReceive(current, isBid) || this._setForSale(current, isBid); + } + + current.priceText = e.target.value; + this.forceUpdate(); + } + + _onInputSell(type, e) { + let current = this.state[type]; + const isBid = type === "bid"; + current.for_sale.setAmount({real: parseFloat(e.target.value) || 0}); + + if (current.price.isValid()) { + this._setReceive(current, isBid); + } else { + this._setPrice(current); + } + + current.forSaleText = e.target.value; + this.forceUpdate(); + } + + _onInputReceive(type, e) { + let current = this.state[type]; + const isBid = type === "bid"; + current.to_receive.setAmount({real: parseFloat(e.target.value) || 0}); + + if (current.price.isValid()) { + this._setForSale(current, isBid); + } else { + this._setPrice(current); + } + + current.toReceiveText = e.target.value; + this.forceUpdate(); + } + + isMarketFrozen() { + let {baseAsset, quoteAsset} = this.props; + + let baseWhiteList = baseAsset.getIn(["options", "whitelist_markets"]).toJS(); + let quoteWhiteList = quoteAsset.getIn(["options", "whitelist_markets"]).toJS(); + let baseBlackList = baseAsset.getIn(["options", "blacklist_markets"]).toJS(); + let quoteBlackList = quoteAsset.getIn(["options", "blacklist_markets"]).toJS(); + + if (quoteWhiteList.length && quoteWhiteList.indexOf(baseAsset.get("id") === -1)) { + return {isFrozen: true, frozenAsset: quoteAsset.get("symbol")}; + } + if (baseWhiteList.length && baseWhiteList.indexOf(quoteAsset.get("id") === -1)) { + return {isFrozen: true, frozenAsset: baseAsset.get("symbol")}; + } + + if (quoteBlackList.length && quoteBlackList.indexOf(baseAsset.get("id") !== -1)) { + return {isFrozen: true, frozenAsset: quoteAsset.get("symbol")}; + } + if (baseBlackList.length && baseBlackList.indexOf(quoteAsset.get("id") !== -1)) { + return {isFrozen: true, frozenAsset: baseAsset.get("symbol")}; + } + + return {isFrozen: false}; + } + + render() { + let { currentAccount, marketLimitOrders, marketCallOrders, marketData, activeMarketHistory, + invertedCalls, starredMarkets, quoteAsset, baseAsset, lowestCallPrice, + marketStats, marketReady, marketSettleOrders, bucketSize, totals, + feedPrice, buckets } = this.props; + + const {combinedBids, combinedAsks, lowestAsk, highestBid, + flatBids, flatAsks, flatCalls, flatSettles} = marketData; + + let {bid, ask, leftOrderBook, showDepthChart, tools, chartHeight, + buyDiff, sellDiff, indicators, indicatorSettings, width, buySellTop} = this.state; + const {isFrozen, frozenAsset} = this.isMarketFrozen(); + + let base = null, quote = null, accountBalance = null, quoteBalance = null, + baseBalance = null, coreBalance = null, quoteSymbol, baseSymbol, + showCallLimit = false, latestPrice, changeClass; + + + let isNullAccount = currentAccount.get("id") === "1.2.3"; + + const showVolumeChart = this.props.viewSettings.get("showVolumeChart", true); + + if (quoteAsset.size && baseAsset.size && currentAccount.size) { + base = baseAsset; + quote = quoteAsset; + baseSymbol = base.get("symbol"); + quoteSymbol = quote.get("symbol"); + + accountBalance = currentAccount.get("balances").toJS(); + + if (accountBalance) { + for (let id in accountBalance) { + if (id === quote.get("id")) { + quoteBalance = accountBalance[id]; + } + if (id === base.get("id")) { + baseBalance = accountBalance[id]; + } + if (id === "1.3.0") { + coreBalance = accountBalance[id]; + } + } + } + + showCallLimit = this._getSettlementInfo(); + } + + let quoteIsBitAsset = quoteAsset.get("bitasset_data_id") ? true : false; + let baseIsBitAsset = baseAsset.get("bitasset_data_id") ? true : false; + + let spread = (lowestAsk && highestBid) ? lowestAsk.getPrice() - highestBid.getPrice() : 0; + + // Latest price + if (activeMarketHistory.size) { + // Orders come in pairs, first is driver. Third entry is first of second pair. + let latest_two = activeMarketHistory.take(3); + let latest = latest_two.first(); + let second_latest = latest_two.last(); + let paysAsset, receivesAsset, isAsk = false; + if (latest.pays.asset_id === base.get("id")) { + paysAsset = base; + receivesAsset = quote; + isAsk = true; + } else { + paysAsset = quote; + receivesAsset = base; + } + let flipped = base.get("id").split(".")[2] > quote.get("id").split(".")[2]; + latestPrice = market_utils.parse_order_history(latest, paysAsset, receivesAsset, isAsk, flipped); + + isAsk = false; + if (second_latest) { + if (second_latest.pays.asset_id === base.get("id")) { + paysAsset = base; + receivesAsset = quote; + isAsk = true; + } else { + paysAsset = quote; + receivesAsset = base; + } + + let oldPrice = market_utils.parse_order_history(second_latest, paysAsset, receivesAsset, isAsk, flipped); + changeClass = latestPrice.full === oldPrice.full ? "" : latestPrice.full - oldPrice.full > 0 ? "change-up" : "change-down"; + } + } + + // Fees + let coreAsset = ChainStore.getAsset("1.3.0"); + if (!coreAsset) { + return null; + } + + let { + sellFeeAsset, + sellFeeAssets, + sellFee, + buyFeeAsset, + buyFeeAssets, + buyFee + } = this._getFeeAssets(quote, base, coreAsset); + + // Decimals + let hasPrediction = base.getIn(["bitasset", "is_prediction_market"]) || quote.getIn(["bitasset", "is_prediction_market"]); + + let description = null; + + if (hasPrediction) { + description = quoteAsset.getIn(["options", "description"]); + description = assetUtils.parseDescription(description).main; + } + + let smallScreen = false; + if (width < 1000) { + smallScreen = true; + leftOrderBook = false; + } + + let orderMultiplier = leftOrderBook ? 2 : 1; + + let buyForm = isFrozen ? null : ( + + ); + + let sellForm = isFrozen ? null : ( + + ); + + let orderBook = ( + + ); + + return ( +
+ + {/* Main vertical block with content */} + + {/* Left Column - Open Orders */} + {leftOrderBook ? ( +
+ {orderBook} +
) : null} + + {/* Center Column */} +
+ + {/* Top bar with info */} + { + let tools = cloneDeep(this.state.tools); + for (let k in tools) { + if (k === key) { + tools[k] = !tools[k]; + } else { + tools[k] = false; + } + } + this.setState({tools}, () => { + this.setState({tools: {fib: false, trendline: false}}); + }); + }} + onChangeChartHeight={this.onChangeChartHeight.bind(this)} + chartHeight={chartHeight} + showVolumeChart={showVolumeChart} + onToggleVolume={() => {SettingsActions.changeViewSetting({showVolumeChart: !showVolumeChart});}} + onChangeIndicatorSetting={this._changeIndicatorSetting.bind(this)} + indicatorSettings={indicatorSettings} + /> + +
+ {!showDepthChart ? ( +
+ {/* Price history chart */} + 1100 ? chartHeight : chartHeight - 125} + leftOrderBook={leftOrderBook} + marketReady={marketReady} + indicators={indicators} + indicatorSettings={indicatorSettings} + bucketSize={bucketSize} + latest={latestPrice} + verticalOrderbook={leftOrderBook} + theme={this.props.settings.get("themes")} + zoom={this.state.currentPeriod} + tools={tools} + showVolumeChart={showVolumeChart} + /> +
) : ( +
+ 1100 ? this.state.chartHeight : this.state.chartHeight - 125} + onClick={this._depthChartClick.bind(this, base, quote)} + settlementPrice={(!hasPrediction && feedPrice) && feedPrice.toReal()} + spread={spread} + LCP={showCallLimit ? lowestCallPrice : null} + leftOrderBook={leftOrderBook} + hasPrediction={hasPrediction} + noFrame={false} + verticalOrderbook={leftOrderBook} + theme={this.props.settings.get("themes")} + /> +
)} + +
+ {hasPrediction ?

{description}

: null} + + {isFrozen ?
: null} + {buyForm} + {sellForm} + + + + {!leftOrderBook ? orderBook : null} + + + + + + {marketLimitOrders.size > 0 && base && quote ? ( + ) : null} +
+ + + {/* Settle Orders */} + + {(base.get("id") === "1.3.0" || quote.get("id") === "1.3.0") ? ( + ) : null} + + +
{ /* end CenterContent */} + + +
{/* End of Main Content Column */} + + {/* Right Column - Market History */} +
+ {/* Market History */} +
+ +
+ {/*
+ +
*/} +
+ + {!isNullAccount && quoteIsBitAsset ? + : null} + {!isNullAccount && baseIsBitAsset ? + : null} + {/* End of Second Vertical Block */} +
+ ); + } } export default Exchange; diff --git a/web/app/components/Exchange/PriceChartD3.jsx b/web/app/components/Exchange/PriceChartD3.jsx index 8438430e40..9256447095 100644 --- a/web/app/components/Exchange/PriceChartD3.jsx +++ b/web/app/components/Exchange/PriceChartD3.jsx @@ -3,10 +3,10 @@ import { format } from "d3-format"; import { timeFormat } from "d3-time-format"; import { ChartCanvas, Chart, series, scale, coordinates, tooltip, axes, - indicator, helper, interactive } from "react-stockcharts"; + indicator, helper, interactive } from "react-stockcharts"; const { CandlestickSeries, BarSeries, LineSeries, AreaSeries, BollingerSeries, - MACDSeries } = series; + MACDSeries } = series; const { XAxis, YAxis } = axes; const { fitWidth } = helper; const { discontinuousTimeScaleProvider } = scale; @@ -21,393 +21,393 @@ import { cloneDeep } from "lodash"; import utils from "common/utils"; class CandleStickChartWithZoomPan extends React.Component { - constructor(props) { - super(); - - const pricePrecision = props.base.get("precision"); - const volumePrecision = props.quote.get("precision"); - - const priceFormat = format(`.${pricePrecision}f`); - const timeFormatter = timeFormat("%Y-%m-%d %H:%M"); - const volumeFormat = format(`.${volumePrecision}r`); - - this.state = { - enableTrendLine: false, - enableFib: false, - tools: [], - priceFormat, - timeFormatter, - volumeFormat, - margin: {left: 75, right: 75, top:20, bottom: 30}, - calculators: this._getCalculators(props) - }; - - this.onTrendLineComplete = this.onTrendLineComplete.bind(this); - this.onFibComplete = this.onFibComplete.bind(this); - this.onKeyPress = this.onKeyPress.bind(this); - } - - componentDidMount() { - document.addEventListener("keyup", this.onKeyPress); - } - componentWillUnmount() { - document.removeEventListener("keyup", this.onKeyPress); - } - - onTrendLineComplete() { - this.setState({ - enableTrendLine: false - }); - } - - onFibComplete() { - this.setState({ - enableFib: false - }); - } - - onKeyPress(e) { - const tools = cloneDeep(this.state.tools); - const ref = this.refs[tools[tools.length - 1]]; - var keyCode = e.which; - switch (keyCode) { - case 46: { // DEL - if (ref) ref.removeLast(); - tools.pop(); - this.setState({tools}); - break; - } - case 27: { // ESC - if (ref) ref.terminate(); - this.setState({ - [enableFib]: false - }); - break; - } - } - } - - componentWillReceiveProps(np) { - let tools = cloneDeep(this.state.tools); - if (np.tools && np.tools.trendline) { - this.setState({enableTrendLine: true}); - tools.push("enableTrendLine"); - }; - if (np.tools && np.tools.fib) { - this.setState({enableFib: true}); - tools.push("enableFib"); - }; - this.setState({tools}); - - if (!utils.are_equal_shallow(np.indicators, this.props.indicators) || - !utils.are_equal_shallow(np.indicatorSettings, this.props.indicatorSettings)) { - this.setState({calculators: this._getCalculators(np)}); - } - } - - _getThemeColors(props = this.props) { - return colors[props.theme]; - } - - _getCalculators(props = this.props) { - const { positiveColor, negativeColor } = this._getThemeColors(props); - const { indicatorSettings } = props; - const calculators = {}; - - calculators.sma = sma() - .windowSize(parseInt(indicatorSettings["sma"], 10)) - .sourcePath("close") - .stroke("#1f77b4") - .fill("#1f77b4") - .merge((d, c) => {d.sma = c;}) - .accessor(d => d.sma); - - calculators.ema1 = ema() - .windowSize(parseInt(indicatorSettings["ema1"], 10)) - .merge((d, c) => {d.ema1 = c;}) - .accessor(d => d.ema1); - - calculators.ema2 = ema() - .windowSize(parseInt(indicatorSettings["ema2"], 10)) - .merge((d, c) => {d.ema2 = c;}) - .accessor(d => d.ema2); - - calculators.smaVolume = sma() - .windowSize(parseInt(indicatorSettings["smaVolume"], 10)) - .sourcePath("volume") - .merge((d, c) => {d.smaVolume = c;}) - .stroke("#1f77b4") - .fill("#1f77b4") - .accessor(d => d.smaVolume); - - calculators.bb = bollingerBand() - .merge((d, c) => {d.bb = c;}) - .accessor(d => d.bb); - - calculators.macd = macd() - .fast(12) - .slow(26) - .signal(9) - .stroke({macd: negativeColor, signal: positiveColor}) - .merge((d, c) => {d.macd = c;}) - .accessor(d => d.macd); - - return calculators; - } - - _renderVolumeChart(chartMultiplier) { - const { height, indicators } = this.props; - const { timeFormatter, volumeFormat, calculators } = this.state; - const { axisLineColor, volumeColor, indicatorLineColor } = this._getThemeColors(); - - return d.volume, calculators.smaVolume.accessor()]} - height={height * 0.2} - origin={(w, h) => [0, h - (chartMultiplier * height * 0.2)]} - > - - {indicators.macd ? null : } - - - {indicators.macd ? null : } - - - - - d.volume} fill={volumeColor} /> - {indicators.smaVolume ? : null} - - {indicators.smaVolume ? : null} - d.volume} fill={volumeColor} /> - - d.volume} displayFormat={volumeFormat} fill="#0F0F0F"/> - d.volume} displayFormat={volumeFormat} fill="#0F0F0F"/> - {indicators.smaVolume ? : null} - {indicators.smaVolume ? : null} - ; - } - - _renderCandleStickChart(chartMultiplier, maCalcs) { - const { height, width, showVolumeChart, indicators } = this.props; - const { timeFormatter, volumeFormat, priceFormat, margin, enableTrendLine, - enableFib, calculators } = this.state; - const { positiveColor, negativeColor, axisLineColor, indicatorLineColor } = this._getThemeColors(); - - let gridHeight = height - margin.top - margin.bottom; - let gridWidth = width - margin.left - margin.right; - - let showGrid = false; - let yGrid = showGrid ? { innerTickSize: -1 * gridWidth, tickStrokeOpacity: 0.1 } : {}; - let xGrid = showGrid ? { innerTickSize: -1 * gridHeight, tickStrokeOpacity: 0.1 } : {}; - - return [d.high, d.low], calculators.ema1.accessor(), calculators.ema2.accessor(), calculators.sma.accessor()]} - padding={{ top: 10, bottom: 20 }} - > - {indicators.macd || showVolumeChart ? null : - } - - - {indicators.macd || showVolumeChart ? null : - } - - - - - - d.close > d.open ? positiveColor : negativeColor} - fill={d => d.close > d.open ? positiveColor : negativeColor} - opacity={0.8} - /> - {indicators.bb ? : null} - - {indicators.sma ? : null} - {indicators.ema1 ? : null} - {indicators.ema2 ? : null} - - {indicators.sma ? : null} - {indicators.ema1 ? : null} - {indicators.ema2 ? : null} - - d.close} displayFormat={priceFormat} fill={d => d.close > d.open ? positiveColor : negativeColor} - /> - d.close} displayFormat={priceFormat} fill={d => d.close > d.open ? positiveColor : negativeColor} - /> - - - - {maCalcs.length ? - : null} - - {indicators.bb ? : null} - - [d.high, d.low]} - onComplete={this.onTrendLineComplete} - stroke={axisLineColor} - fontStroke={axisLineColor} - /> - - - ; - } - - render() { - const { width, priceData, height, ratio, theme, zoom, - indicators, showVolumeChart } = this.props; - const { timeFormatter, enableFib, enableTrendLine, margin, calculators } = this.state; - const themeColors = colors[theme]; - const { axisLineColor, indicatorLineColor} = themeColors; - - let chartMultiplier = showVolumeChart ? 1 : 0; // Used to adjust the height of the charts and their positioning - // if (indicators.bb) calc.push(bb); - - // Indicator calculators - let calc = [], maCalcs = [], tooltipIncludes = ["sma", "ema1", "ema2", "smaVolume"]; - - // if (showVolumeChart) maCalcs.push(calculators["smaVolume"]); - - for (let i in indicators) { - if (indicators[i]) { - // Don't add volume indicators if the volume chart is off - if (i.toLowerCase().indexOf("volume") !== -1 && !showVolumeChart) continue; - // Add active calculators - calc.push(calculators[i]); - // Add calculators needing tooltips - if (tooltipIncludes.indexOf(i) !== -1) maCalcs.push(calculators[i]); - } - }; - if (indicators["macd"]) chartMultiplier++; - - const filterDate = new Date((new Date).getTime() - zoom * 1000); - const filteredData = zoom === "all" ? priceData : priceData.filter(a => { - return a.date > filterDate; - }); - - return ( - d.date} xScaleProvider={discontinuousTimeScaleProvider} - xExtents={[filteredData[0].date, filteredData[filteredData.length - 1].date]} - type="hybrid" - className="ps-child no-overflow Stockcharts__wrapper ps-must-propagate" - drawMode={enableTrendLine || enableFib}> - > - {showVolumeChart ? this._renderVolumeChart(chartMultiplier) - : } - - {this._renderCandleStickChart(chartMultiplier, maCalcs)} - - {indicators.macd ? - [0, h - ((chartMultiplier - (showVolumeChart ? 1 : 0) ) * height * 0.2)]} - padding={{ top: 40, bottom: 10 }} > - - - - - - - - - : /* Need to return an empty element here, null triggers an error */} - - - - ); - } + constructor(props) { + super(); + + const pricePrecision = props.base.get("precision"); + const volumePrecision = props.quote.get("precision"); + + const priceFormat = format(`.${pricePrecision}f`); + const timeFormatter = timeFormat("%Y-%m-%d %H:%M"); + const volumeFormat = format(`.${volumePrecision}r`); + + this.state = { + enableTrendLine: false, + enableFib: false, + tools: [], + priceFormat, + timeFormatter, + volumeFormat, + margin: {left: 75, right: 75, top:20, bottom: 30}, + calculators: this._getCalculators(props) + }; + + this.onTrendLineComplete = this.onTrendLineComplete.bind(this); + this.onFibComplete = this.onFibComplete.bind(this); + this.onKeyPress = this.onKeyPress.bind(this); + } + + componentDidMount() { + document.addEventListener("keyup", this.onKeyPress); + } + componentWillUnmount() { + document.removeEventListener("keyup", this.onKeyPress); + } + + onTrendLineComplete() { + this.setState({ + enableTrendLine: false + }); + } + + onFibComplete() { + this.setState({ + enableFib: false + }); + } + + onKeyPress(e) { + const tools = cloneDeep(this.state.tools); + const ref = this.refs[tools[tools.length - 1]]; + var keyCode = e.which; + switch (keyCode) { + case 46: { // DEL + if (ref) ref.removeLast(); + tools.pop(); + this.setState({tools}); + break; + } + case 27: { // ESC + if (ref) ref.terminate(); + this.setState({ + [enableFib]: false + }); + break; + } + } + } + + componentWillReceiveProps(np) { + let tools = cloneDeep(this.state.tools); + if (np.tools && np.tools.trendline) { + this.setState({enableTrendLine: true}); + tools.push("enableTrendLine"); + }; + if (np.tools && np.tools.fib) { + this.setState({enableFib: true}); + tools.push("enableFib"); + }; + this.setState({tools}); + + if (!utils.are_equal_shallow(np.indicators, this.props.indicators) || + !utils.are_equal_shallow(np.indicatorSettings, this.props.indicatorSettings)) { + this.setState({calculators: this._getCalculators(np)}); + } + } + + _getThemeColors(props = this.props) { + return colors[props.theme]; + } + + _getCalculators(props = this.props) { + const { positiveColor, negativeColor } = this._getThemeColors(props); + const { indicatorSettings } = props; + const calculators = {}; + + calculators.sma = sma() + .windowSize(parseInt(indicatorSettings["sma"], 10)) + .sourcePath("close") + .stroke("#1f77b4") + .fill("#1f77b4") + .merge((d, c) => {d.sma = c;}) + .accessor(d => d.sma); + + calculators.ema1 = ema() + .windowSize(parseInt(indicatorSettings["ema1"], 10)) + .merge((d, c) => {d.ema1 = c;}) + .accessor(d => d.ema1); + + calculators.ema2 = ema() + .windowSize(parseInt(indicatorSettings["ema2"], 10)) + .merge((d, c) => {d.ema2 = c;}) + .accessor(d => d.ema2); + + calculators.smaVolume = sma() + .windowSize(parseInt(indicatorSettings["smaVolume"], 10)) + .sourcePath("volume") + .merge((d, c) => {d.smaVolume = c;}) + .stroke("#1f77b4") + .fill("#1f77b4") + .accessor(d => d.smaVolume); + + calculators.bb = bollingerBand() + .merge((d, c) => {d.bb = c;}) + .accessor(d => d.bb); + + calculators.macd = macd() + .fast(12) + .slow(26) + .signal(9) + .stroke({macd: negativeColor, signal: positiveColor}) + .merge((d, c) => {d.macd = c;}) + .accessor(d => d.macd); + + return calculators; + } + + _renderVolumeChart(chartMultiplier) { + const { height, indicators } = this.props; + const { timeFormatter, volumeFormat, calculators } = this.state; + const { axisLineColor, volumeColor, indicatorLineColor } = this._getThemeColors(); + + return d.volume, calculators.smaVolume.accessor()]} + height={height * 0.2} + origin={(w, h) => [0, h - (chartMultiplier * height * 0.2)]} + > + + {indicators.macd ? null : } + + + {indicators.macd ? null : } + + + + + d.volume} fill={volumeColor} /> + {indicators.smaVolume ? : null} + + {indicators.smaVolume ? : null} + d.volume} fill={volumeColor} /> + + d.volume} displayFormat={volumeFormat} fill="#0F0F0F"/> + d.volume} displayFormat={volumeFormat} fill="#0F0F0F"/> + {indicators.smaVolume ? : null} + {indicators.smaVolume ? : null} + ; + } + + _renderCandleStickChart(chartMultiplier, maCalcs) { + const { height, width, showVolumeChart, indicators } = this.props; + const { timeFormatter, volumeFormat, priceFormat, margin, enableTrendLine, + enableFib, calculators } = this.state; + const { positiveColor, negativeColor, axisLineColor, indicatorLineColor } = this._getThemeColors(); + + let gridHeight = height - margin.top - margin.bottom; + let gridWidth = width - margin.left - margin.right; + + let showGrid = false; + let yGrid = showGrid ? { innerTickSize: -1 * gridWidth, tickStrokeOpacity: 0.1 } : {}; + let xGrid = showGrid ? { innerTickSize: -1 * gridHeight, tickStrokeOpacity: 0.1 } : {}; + + return [d.high, d.low], calculators.ema1.accessor(), calculators.ema2.accessor(), calculators.sma.accessor()]} + padding={{ top: 10, bottom: 20 }} + > + {indicators.macd || showVolumeChart ? null : + } + + + {indicators.macd || showVolumeChart ? null : + } + + + + + + d.close > d.open ? positiveColor : negativeColor} + fill={d => d.close > d.open ? positiveColor : negativeColor} + opacity={0.8} + /> + {indicators.bb ? : null} + + {indicators.sma ? : null} + {indicators.ema1 ? : null} + {indicators.ema2 ? : null} + + {indicators.sma ? : null} + {indicators.ema1 ? : null} + {indicators.ema2 ? : null} + + d.close} displayFormat={priceFormat} fill={d => d.close > d.open ? positiveColor : negativeColor} + /> + d.close} displayFormat={priceFormat} fill={d => d.close > d.open ? positiveColor : negativeColor} + /> + + + + {maCalcs.length ? + : null} + + {indicators.bb ? : null} + + [d.high, d.low]} + onComplete={this.onTrendLineComplete} + stroke={axisLineColor} + fontStroke={axisLineColor} + /> + + + ; + } + + render() { + const { width, priceData, height, ratio, theme, zoom, + indicators, showVolumeChart } = this.props; + const { timeFormatter, enableFib, enableTrendLine, margin, calculators } = this.state; + const themeColors = colors[theme]; + const { axisLineColor, indicatorLineColor} = themeColors; + + let chartMultiplier = showVolumeChart ? 1 : 0; // Used to adjust the height of the charts and their positioning + // if (indicators.bb) calc.push(bb); + + // Indicator calculators + let calc = [], maCalcs = [], tooltipIncludes = ["sma", "ema1", "ema2", "smaVolume"]; + + // if (showVolumeChart) maCalcs.push(calculators["smaVolume"]); + + for (let i in indicators) { + if (indicators[i]) { + // Don't add volume indicators if the volume chart is off + if (i.toLowerCase().indexOf("volume") !== -1 && !showVolumeChart) continue; + // Add active calculators + calc.push(calculators[i]); + // Add calculators needing tooltips + if (tooltipIncludes.indexOf(i) !== -1) maCalcs.push(calculators[i]); + } + }; + if (indicators["macd"]) chartMultiplier++; + + const filterDate = new Date((new Date).getTime() - zoom * 1000); + const filteredData = zoom === "all" ? priceData : priceData.filter(a => { + return a.date > filterDate; + }); + + return ( + d.date} xScaleProvider={discontinuousTimeScaleProvider} + xExtents={[filteredData[0].date, filteredData[filteredData.length - 1].date]} + type="hybrid" + className="ps-child no-overflow Stockcharts__wrapper ps-must-propagate" + drawMode={enableTrendLine || enableFib}> + > + {showVolumeChart ? this._renderVolumeChart(chartMultiplier) + : } + + {this._renderCandleStickChart(chartMultiplier, maCalcs)} + + {indicators.macd ? + [0, h - ((chartMultiplier - (showVolumeChart ? 1 : 0) ) * height * 0.2)]} + padding={{ top: 40, bottom: 10 }} > + + + + + + + + + : /* Need to return an empty element here, null triggers an error */} + + + + ); + } } CandleStickChartWithZoomPan = fitWidth(CandleStickChartWithZoomPan); export default class Wrapper extends React.Component { - shouldComponentUpdate(np) { - if (!np.priceData.length) { - return false; - } - return ( - !utils.are_equal_shallow(np.priceData, this.props.priceData) || - !utils.are_equal_shallow(np.indicators, this.props.indicators) || - !utils.are_equal_shallow(np.indicatorSettings, this.props.indicatorSettings) || - !utils.are_equal_shallow(np.tools, this.props.tools) || - np.height !== this.props.height || - np.zoom !== this.props.zoom || - np.showVolumeChart !== this.props.showVolumeChart - ); - } - - render() { - if (!this.props.priceData.length) { - return null; - } - - return ( - - ); - } + shouldComponentUpdate(np) { + if (!np.priceData.length) { + return false; + } + return ( + !utils.are_equal_shallow(np.priceData, this.props.priceData) || + !utils.are_equal_shallow(np.indicators, this.props.indicators) || + !utils.are_equal_shallow(np.indicatorSettings, this.props.indicatorSettings) || + !utils.are_equal_shallow(np.tools, this.props.tools) || + np.height !== this.props.height || + np.zoom !== this.props.zoom || + np.showVolumeChart !== this.props.showVolumeChart + ); + } + + render() { + if (!this.props.priceData.length) { + return null; + } + + return ( + + ); + } }