[wip] add trigger order components

This commit is contained in:
Maximilian Schneider 2021-09-21 20:04:23 +02:00
parent 5da4887736
commit 8abbce0238
5 changed files with 183 additions and 41 deletions

View File

@ -11,6 +11,7 @@ import { floorToDecimal } from '../utils/index'
import useMangoStore from '../stores/useMangoStore'
import Button from './Button'
import TradeType from './TradeType'
import TriggerType from './TriggerType'
import Input from './Input'
import Switch from './Switch'
import { Market } from '@project-serum/serum'
@ -35,9 +36,21 @@ export default function TradeForm() {
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const mangoClient = useMangoStore((s) => s.connection.client)
const market = useMangoStore((s) => s.selectedMarket.current)
const { side, baseSize, quoteSize, price, tradeType } = useMangoStore(
(s) => s.tradeForm
)
const isPerpMarket = market instanceof PerpMarket
const {
side,
baseSize,
quoteSize,
price,
tradeType,
triggerPrice,
triggerType,
} = useMangoStore((s) => s.tradeForm)
const isLimitOrder = ['Limit', 'Trigger Limit'].includes(tradeType)
const isMarketOrder = ['Market', 'Trigger Market'].includes(tradeType)
const isTriggerOrder = ['Trigger Limit', 'Trigger Market'].includes(tradeType)
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
@ -102,6 +115,24 @@ export default function TradeForm() {
s.tradeForm.tradeType = type
})
const setTriggerPrice = (price) => {
set((s) => {
if (!Number.isNaN(parseFloat(price))) {
s.tradeForm.triggerPrice = parseFloat(price)
} else {
s.tradeForm.triggerPrice = price
}
})
if (isMarketOrder) {
onSetPrice(price)
}
}
const setTriggerType = (type) =>
set((s) => {
s.tradeForm.triggerType = type
})
const markPriceRef = useRef(useMangoStore.getState().selectedMarket.markPrice)
const markPrice = markPriceRef.current
useEffect(
@ -131,7 +162,7 @@ export default function TradeForm() {
let tickSize = 1
if (market instanceof Market) {
tickSize = market.tickSize
} else if (market instanceof PerpMarket) {
} else if (isPerpMarket) {
const baseDecimals = getTokenBySymbol(
groupConfig,
marketConfig.baseSymbol
@ -179,7 +210,7 @@ export default function TradeForm() {
return
}
if (!Number(price) && tradeType === 'Limit') {
if (!Number(price) && isLimitOrder) {
setBaseSize('')
return
}
@ -191,9 +222,11 @@ export default function TradeForm() {
const onTradeTypeChange = (tradeType) => {
setTradeType(tradeType)
if (tradeType === 'Market') {
if (isMarketOrder) {
setIoc(true)
setPrice('')
if (isTriggerOrder) {
setPrice(triggerPrice)
}
} else {
const priceOnBook = side === 'buy' ? orderbook?.asks : orderbook?.bids
if (priceOnBook && priceOnBook.length > 0 && priceOnBook[0].length > 0) {
@ -217,7 +250,7 @@ export default function TradeForm() {
}
async function onSubmit() {
if (!price && tradeType === 'Limit') {
if (!price && isLimitOrder) {
notify({
title: 'Missing price',
type: 'error',
@ -245,7 +278,8 @@ export default function TradeForm() {
orderbook,
baseSize,
side,
price
price,
triggerPrice
)
if (!orderPrice) {
@ -305,7 +339,7 @@ export default function TradeForm() {
}
const disabledTradeButton =
(!price && tradeType === 'Limit') ||
(!price && isLimitOrder) ||
!baseSize ||
!connected ||
submitting ||
@ -353,7 +387,7 @@ export default function TradeForm() {
step={tickSize}
onChange={(e) => onSetPrice(e.target.value)}
value={price}
disabled={tradeType === 'Market'}
disabled={isMarketOrder}
prefix={'Price'}
suffix={groupConfig.quoteSymbol}
className="rounded-r-none"
@ -362,6 +396,7 @@ export default function TradeForm() {
<TradeType
onChange={onTradeTypeChange}
value={tradeType}
offerTriggers={isPerpMarket}
className="hover:border-th-primary flex-grow"
/>
</Input.Group>
@ -402,10 +437,11 @@ export default function TradeForm() {
orderbook,
baseSize ? baseSize : 0,
side,
price
price,
triggerPrice
)}
/>
{tradeType !== 'Market' ? (
{isLimitOrder && (
<div className="flex mt-2">
<Switch checked={postOnly} onChange={postOnChange}>
POST
@ -416,7 +452,30 @@ export default function TradeForm() {
</Switch>
</div>
</div>
) : null}
)}
{isTriggerOrder && (
<Input.Group className="mt-4">
<TriggerType
onChange={setTriggerType}
value={triggerType}
className="hover:border-th-primary flex-grow"
/>
<Input
type="number"
min="0"
step={tickSize}
onChange={(e) => setTriggerPrice(e.target.value)}
value={triggerPrice}
prefix={'Price'}
suffix={groupConfig.quoteSymbol}
className="rounded-l-none"
wrapperClassName="rounded-l-none w-3/5"
/>
</Input.Group>
)}
<div className={`flex py-4`}>
{ipAllowed ? (
side === 'buy' ? (
@ -435,9 +494,7 @@ export default function TradeForm() {
</div>
) : (
`${baseSize > 0 ? 'Buy ' + baseSize : 'Buy '} ${
marketConfig.name.includes('PERP')
? marketConfig.name
: marketConfig.baseSymbol
isPerpMarket ? marketConfig.name : marketConfig.baseSymbol
}`
)}
</Button>
@ -457,9 +514,7 @@ export default function TradeForm() {
</div>
) : (
`${baseSize > 0 ? 'Sell ' + baseSize : 'Sell '} ${
marketConfig.name.includes('PERP')
? marketConfig.name
: marketConfig.baseSymbol
isPerpMarket ? marketConfig.name : marketConfig.baseSymbol
}`
)}
</Button>
@ -582,7 +637,8 @@ export default function TradeForm() {
orderbook,
baseSize ? baseSize : 0,
side,
price
price,
triggerPrice
)}
/>
{tradeType !== 'Market' ? (

View File

@ -8,11 +8,16 @@ const StyledListbox = styled(Listbox.Button)`
border-left: 1px solid transparent;
`
const TRADE_TYPES = ['Limit', 'Market']
const TradeType = ({ value, onChange, className = '' }) => {
const TradeType = ({ value, onChange, offerTriggers = false, className = '' }) => {
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const TRADE_TYPES = ['Limit', 'Market']
if (offerTriggers)
TRADE_TYPES.push('Trigger Market', 'Trigger Limit')
return (
<div className={`relative ${className}`}>
{!isMobile ? (
@ -60,30 +65,20 @@ const TradeType = ({ value, onChange, className = '' }) => {
</Listbox>
) : (
<div className="flex">
<div
{ TRADE_TYPES.map( tradeType => {
<div
className={`px-2 py-1 ml-2 rounded-md cursor-pointer default-transition bg-th-bkg-4
${
value === 'Limit'
value === tradeType
? `ring-1 ring-inset ring-th-primary text-th-primary`
: `text-th-fgd-1 opacity-50 hover:opacity-100`
}
`}
onClick={() => onChange('Limit')}
onClick={() => onChange(tradeType)}
>
Limit
</div>
<div
className={`px-2 py-1 ml-2 rounded-md cursor-pointer default-transition bg-th-bkg-4
${
value === 'Market'
? `ring-1 ring-inset ring-th-primary text-th-primary`
: `text-th-fgd-1 opacity-50 hover:opacity-100`
}
`}
onClick={() => onChange('Market')}
>
Market
{tradeType}
</div>
})}
</div>
)}
</div>

View File

@ -0,0 +1,84 @@
import { Listbox } from '@headlessui/react'
import styled from '@emotion/styled'
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid'
import { useViewport } from '../hooks/useViewport'
import { breakpoints } from './TradePageGrid'
const StyledListbox = styled(Listbox.Button)`
border-right: 1px solid transparent;
`
const TriggerType = ({ value, onChange, className = '' }) => {
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const TRIGGER_TYPES = ['Above', 'Below']
return (
<div className={`relative ${className}`}>
{!isMobile ? (
<Listbox value={value} onChange={onChange}>
{({ open }) => (
<>
<StyledListbox
className={`font-normal h-full w-full bg-th-bkg-1 border border-th-fgd-4 hover:border-th-primary rounded rounded-r-none focus:outline-none focus:border-th-primary`}
>
<div
className={`flex items-center justify-between space-x-4 pl-2 pr-1`}
>
<span>{value}</span>
{open ? (
<ChevronUpIcon className={`h-5 w-5 mr-1 text-th-primary`} />
) : (
<ChevronDownIcon
className={`h-5 w-5 mr-1 text-th-primary`}
/>
)}
</div>
</StyledListbox>
{open ? (
<Listbox.Options
static
className={`z-20 w-full p-1 absolute left-0 mt-1 bg-th-bkg-1 origin-top-left divide-y divide-th-bkg-3 shadow-lg outline-none rounded-md`}
>
{TRIGGER_TYPES.map((type) => (
<Listbox.Option key={type} value={type}>
{({ selected }) => (
<div
className={`p-2 hover:bg-th-bkg-2 hover:cursor-pointer tracking-wider ${
selected && `text-th-primary`
}`}
>
{type}
</div>
)}
</Listbox.Option>
))}
</Listbox.Options>
) : null}
</>
)}
</Listbox>
) : (
<div className="flex">
{ TRIGGER_TYPES.map( triggerType => {
<div
className={`px-2 py-1 ml-2 rounded-md cursor-pointer default-transition bg-th-bkg-4
${
value === triggerType
? `ring-1 ring-inset ring-th-primary text-th-primary`
: `text-th-fgd-1 opacity-50 hover:opacity-100`
}
`}
onClick={() => onChange(triggerType)}
>
{triggerType}
</div>
})}
</div>
)}
</div>
)
}
export default TriggerType

View File

@ -144,7 +144,9 @@ interface MangoStore extends State {
price: number | ''
baseSize: number | ''
quoteSize: number | ''
tradeType: 'Market' | 'Limit'
tradeType: 'Market' | 'Limit' | 'Trigger Market' | 'Trigger Limit'
triggerPrice: number | ''
triggerType: 'Above' | 'Below'
}
wallet: {
providerUrl: string
@ -215,6 +217,8 @@ const useMangoStore = create<MangoStore>((set, get) => {
quoteSize: '',
tradeType: 'Limit',
price: '',
triggerPrice: '',
triggerType: 'Above',
},
wallet: INITIAL_STATE.WALLET,
settings: {

View File

@ -101,10 +101,13 @@ export function calculateTradePrice(
orderBook: Orderbook,
baseSize: number,
side: 'buy' | 'sell',
price: string | number
price: string | number,
triggerPrice?: string | number
): number {
if (tradeType === 'Market') {
return calculateMarketPrice(orderBook, baseSize, side)
} else if (tradeType === 'Trigger Market') {
return Number(triggerPrice)
}
return Number(price)
}