From fc23f184201851087b2fbe00dd2e0ea17be5fca3 Mon Sep 17 00:00:00 2001 From: "Mr. Dummy Tester" Date: Fri, 25 Dec 2020 14:39:06 -0600 Subject: [PATCH] feat: Some refactoring - now it all works and is hooked up. Still lots of nice formatting but at least swap is now providing leverage validaiton. --- src/components/Input/numeric.tsx | 5 +- src/constants/labels.ts | 1 + src/models/lending/reserve.ts | 1 + .../marginTrading/newPosition/Breakdown.tsx | 4 +- .../newPosition/NewPositionForm.tsx | 73 +++---------------- src/views/marginTrading/newPosition/index.tsx | 2 +- .../marginTrading/newPosition/interfaces.tsx | 2 +- .../marginTrading/newPosition/leverage.ts | 73 +++++++++++++++++++ 8 files changed, 91 insertions(+), 70 deletions(-) create mode 100644 src/views/marginTrading/newPosition/leverage.ts diff --git a/src/components/Input/numeric.tsx b/src/components/Input/numeric.tsx index 1338a45..a270357 100644 --- a/src/components/Input/numeric.tsx +++ b/src/components/Input/numeric.tsx @@ -14,10 +14,11 @@ export class NumericInput extends React.Component { onBlur = () => { const { value, onBlur, onChange } = this.props; let valueTemp = value; - if (value.charAt(value.length - 1) === '.' || value === '-') { + if (value === undefined || value === null) return; + if (value.charAt && (value.charAt(value.length - 1) === '.' || value === '-')) { valueTemp = value.slice(0, -1); } - if (value.startsWith('.') || value.startsWith('-.')) { + if (value.startsWith && (value.startsWith('.') || value.startsWith('-.'))) { valueTemp = valueTemp.replace('.', '0.'); } onChange?.(valueTemp.replace(/0*(\d+)/, '$1')); diff --git a/src/constants/labels.ts b/src/constants/labels.ts index 4c83fa8..8566999 100644 --- a/src/constants/labels.ts +++ b/src/constants/labels.ts @@ -66,4 +66,5 @@ export const LABELS = { NOT_ENOUGH_MARGIN_MESSAGE: 'Not enough buying power in oyster to make this trade at this leverage.', LEVERAGE_LIMIT_MESSAGE: 'With liquidity pools in their current state, you are not allowed to use leverage at this multiple. You will need more margin to make this trade.', + NO_DEPOSIT_MESSAGE: 'You need to deposit coin of this type into oyster before trading with it on margin.', }; diff --git a/src/models/lending/reserve.ts b/src/models/lending/reserve.ts index 9e1c83f..8be543a 100644 --- a/src/models/lending/reserve.ts +++ b/src/models/lending/reserve.ts @@ -178,6 +178,7 @@ export const collateralExchangeRate = (reserve?: LendingReserve) => { export const collateralToLiquidity = (collateralAmount: BN | number, reserve?: LendingReserve) => { const amount = typeof collateralAmount === 'number' ? collateralAmount : collateralAmount.toNumber(); + console.log('Exchange rate:', collateralExchangeRate(reserve)); return Math.floor(amount / collateralExchangeRate(reserve)); }; diff --git a/src/views/marginTrading/newPosition/Breakdown.tsx b/src/views/marginTrading/newPosition/Breakdown.tsx index 9132f77..a497cf7 100644 --- a/src/views/marginTrading/newPosition/Breakdown.tsx +++ b/src/views/marginTrading/newPosition/Breakdown.tsx @@ -5,8 +5,8 @@ import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'; import tokens from '../../../config/tokens.json'; export function Breakdown({ item }: { item: Position }) { - let myPart = (item.asset?.value || 0) / item.leverage; - const brokeragePart = (item.asset?.value || 0) - myPart; + let myPart = parseFloat(item.asset?.value || '0') / item.leverage; + const brokeragePart = parseFloat(item.asset?.value || '0') - myPart; const brokerageColor = 'brown'; const myColor = 'blue'; const gains = 'green'; diff --git a/src/views/marginTrading/newPosition/NewPositionForm.tsx b/src/views/marginTrading/newPosition/NewPositionForm.tsx index e7ef87f..dc1a709 100644 --- a/src/views/marginTrading/newPosition/NewPositionForm.tsx +++ b/src/views/marginTrading/newPosition/NewPositionForm.tsx @@ -1,17 +1,16 @@ import { Button, Card, Radio } from 'antd'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useState } from 'react'; import { ActionConfirmation } from '../../../components/ActionConfirmation'; import { NumericInput } from '../../../components/Input/numeric'; import { TokenIcon } from '../../../components/TokenIcon'; import { LABELS } from '../../../constants'; import { cache, ParsedAccount } from '../../../contexts/accounts'; -import { collateralToLiquidity, LendingReserve, LendingReserveParser } from '../../../models/lending/reserve'; +import { LendingReserve, LendingReserveParser } from '../../../models/lending/reserve'; import { Position } from './interfaces'; import tokens from '../../../config/tokens.json'; import { CollateralSelector } from '../../../components/CollateralSelector'; import { Breakdown } from './Breakdown'; -import { usePoolForBasket } from '../../../utils/pools'; -import { useEnrichedPools } from '../../../contexts/market'; +import { useLeverage } from './leverage'; interface NewPositionFormProps { lendingReserve: ParsedAccount; @@ -29,64 +28,7 @@ export default function NewPositionForm({ lendingReserve, newPosition, setNewPos }; const [showConfirmation, setShowConfirmation] = useState(false); - const collType = newPosition.collateral; - const desiredType = newPosition.asset.type; - - const pool = usePoolForBasket([ - collType?.info?.liquidityMint?.toBase58(), - desiredType?.info?.liquidityMint?.toBase58(), - ]); - - const enriched = useEnrichedPools(pool ? [pool] : []); - - // Leverage validation - if you choose this leverage, is it allowable, with your buying power and with - // the pool we have to cover you? - useEffect(() => { - if (!collType || !desiredType || !newPosition.asset.value || !enriched || enriched.length == 0) { - return; - } - - const amountDesiredToPurchase = newPosition.asset.value; - const leverageDesired = newPosition.leverage; - console.log('collateral reserve', collType); - const amountAvailableInOysterForMargin = collateralToLiquidity(collType.info.availableLiquidity, desiredType.info); - const amountToDepositOnMargin = amountDesiredToPurchase / leverageDesired; - console.log( - 'Amount desired', - amountDesiredToPurchase, - 'leverage', - leverageDesired, - 'amountAvailable', - amountAvailableInOysterForMargin, - ' amount to deposit on margin', - amountToDepositOnMargin - ); - if (amountToDepositOnMargin > amountAvailableInOysterForMargin) { - setNewPosition({ ...newPosition, error: LABELS.NOT_ENOUGH_MARGIN_MESSAGE }); - return; - } - - const liqA = enriched[0].liquidityA; - const liqB = enriched[0].liquidityB; - const supplyRatio = liqA / liqB; - - console.log('Liq A', liqA, 'liq b', liqB, 'supply ratio', supplyRatio); - - // change in liquidity is amount desired (in units of B) converted to collateral units(A) - const chgLiqA = collateralToLiquidity(amountDesiredToPurchase, collType.info); - const newLiqA = liqA - chgLiqA; - const newLiqB = liqB + amountDesiredToPurchase; - const newSupplyRatio = newLiqA / newLiqB; // 75 / 100 - console.log('chg in liq a', chgLiqA, 'new liq a', newLiqA, 'new supply ratio', newSupplyRatio); - const priceImpact = Math.abs(100 - 100 * (newSupplyRatio / supplyRatio)); // abs(100 - 100*(0.75 / 1)) = 25% - const marginToLeverage = 100 / leverageDesired; - console.log('priceImpact', priceImpact, 'marginToLeverage', marginToLeverage); - if (marginToLeverage > priceImpact) { - // if their marginToLeverage ratio < priceImpact, we say hey ho no go - setNewPosition({ ...newPosition, error: LABELS.LEVERAGE_LIMIT_MESSAGE }); - return; - } - }, [collType, desiredType, newPosition.asset.value, newPosition.leverage, enriched]); + useLeverage({ newPosition, setNewPosition }); return ( @@ -121,8 +63,11 @@ export default function NewPositionForm({ lendingReserve, newPosition, setNewPos borderColor: 'transparent', outline: 'transparent', }} - onChange={(v: number) => { - setNewPosition({ ...newPosition, asset: { ...newPosition.asset, value: v } }); + onChange={(v: string) => { + setNewPosition({ + ...newPosition, + asset: { ...newPosition.asset, value: v }, + }); }} placeholder='0.00' /> diff --git a/src/views/marginTrading/newPosition/index.tsx b/src/views/marginTrading/newPosition/index.tsx index 9869488..5240033 100644 --- a/src/views/marginTrading/newPosition/index.tsx +++ b/src/views/marginTrading/newPosition/index.tsx @@ -15,7 +15,7 @@ export const NewPosition = () => { const [newPosition, setNewPosition] = useState({ id: null, leverage: 1, - asset: { value: 0 }, + asset: { value: '0' }, }); if (!lendingReserve) { diff --git a/src/views/marginTrading/newPosition/interfaces.tsx b/src/views/marginTrading/newPosition/interfaces.tsx index ce02980..fe6ea0b 100644 --- a/src/views/marginTrading/newPosition/interfaces.tsx +++ b/src/views/marginTrading/newPosition/interfaces.tsx @@ -13,7 +13,7 @@ export interface Position { collateral?: ParsedAccount; asset: { type?: ParsedAccount; - value: number; + value: string; // because NumericInput returns strings and I dont want to deal with fixing it right now }; error?: string; } diff --git a/src/views/marginTrading/newPosition/leverage.ts b/src/views/marginTrading/newPosition/leverage.ts new file mode 100644 index 0000000..1fd34b9 --- /dev/null +++ b/src/views/marginTrading/newPosition/leverage.ts @@ -0,0 +1,73 @@ +import { useEffect } from 'react'; +import { LABELS } from '../../../constants'; +import { useEnrichedPools } from '../../../contexts/market'; +import { useUserDeposits } from '../../../hooks'; +import { collateralToLiquidity, liquidityToCollateral } from '../../../models'; +import { usePoolForBasket } from '../../../utils/pools'; +import { Position } from './interfaces'; + +export function useLeverage({ + newPosition, + setNewPosition, +}: { + newPosition: Position; + setNewPosition: (pos: Position) => void; +}) { + const collType = newPosition.collateral; + const desiredType = newPosition.asset.type; + + const pool = usePoolForBasket([ + collType?.info?.liquidityMint?.toBase58(), + desiredType?.info?.liquidityMint?.toBase58(), + ]); + + const userDeposits = useUserDeposits(); + const collateralDeposit = userDeposits.userDeposits.find( + (u) => u.reserve.info.liquidityMint.toBase58() == collType?.info?.liquidityMint?.toBase58() + ); + const enriched = useEnrichedPools(pool ? [pool] : []); + + // Leverage validation - if you choose this leverage, is it allowable, with your buying power and with + // the pool we have to cover you? + useEffect(() => { + if (!collateralDeposit) { + setNewPosition({ ...newPosition, error: LABELS.NO_DEPOSIT_MESSAGE }); + return; + } + + if (!collType || !desiredType || !newPosition.asset.value || !enriched || enriched.length == 0) { + return; + } + + const amountDesiredToPurchase = parseFloat(newPosition.asset.value); + const leverageDesired = newPosition.leverage; + + const amountAvailableInOysterForMargin = collateralToLiquidity(collateralDeposit.info.amount, desiredType.info); + const amountToDepositOnMargin = amountDesiredToPurchase / leverageDesired; + + if (amountToDepositOnMargin > amountAvailableInOysterForMargin) { + setNewPosition({ ...newPosition, error: LABELS.NOT_ENOUGH_MARGIN_MESSAGE }); + return; + } + + const liqA = enriched[0].liquidityA; + const liqB = enriched[0].liquidityB; + const supplyRatio = liqA / liqB; + + // change in liquidity is amount desired (in units of B) converted to collateral units(A) + const chgLiqA = liquidityToCollateral(amountDesiredToPurchase, desiredType.info); + const newLiqA = liqA - chgLiqA; + const newLiqB = liqB + amountDesiredToPurchase; + const newSupplyRatio = newLiqA / newLiqB; + + const priceImpact = Math.abs(100 - 100 * (newSupplyRatio / supplyRatio)); + const marginToLeverage = 100 / leverageDesired; // Would be 20% for 5x + if (marginToLeverage < priceImpact && leverageDesired != 1) { + // Obviously we allow 1x as edge case + // if their marginToLeverage ratio < priceImpact, we say hey ho no go + setNewPosition({ ...newPosition, error: LABELS.LEVERAGE_LIMIT_MESSAGE }); + return; + } + setNewPosition({ ...newPosition, error: '' }); + }, [collType, desiredType, newPosition.asset.value, newPosition.leverage, enriched]); +}