committed by
GitHub
9 changed files with 690 additions and 281 deletions
@ -1,42 +0,0 @@ |
|||
.App { |
|||
text-align: center; |
|||
} |
|||
|
|||
.App-logo { |
|||
height: 40vmin; |
|||
pointer-events: none; |
|||
} |
|||
|
|||
@media (prefers-reduced-motion: no-preference) { |
|||
.App-logo { |
|||
animation: App-logo-spin infinite 20s linear; |
|||
} |
|||
} |
|||
|
|||
.App-header { |
|||
background-color: #282c34; |
|||
min-height: 100vh; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
font-size: calc(10px + 2vmin); |
|||
color: white; |
|||
} |
|||
|
|||
.App-link { |
|||
color: #61dafb; |
|||
} |
|||
|
|||
@keyframes App-logo-spin { |
|||
from { |
|||
transform: rotate(0deg); |
|||
} |
|||
to { |
|||
transform: rotate(360deg); |
|||
} |
|||
} |
|||
|
|||
button { |
|||
font-size: calc(10px + 2vmin); |
|||
} |
@ -1,138 +0,0 @@ |
|||
import { Box, Button, HStack, SimpleGrid, Text, useToast, VStack } from "@chakra-ui/react"; |
|||
import React from "react"; |
|||
import { useAsync } from "react-async"; |
|||
import { postAcceptOrder, postRejectOrder } from "../MakerClient"; |
|||
import { Cfd, unixTimestampToDate } from "./Types"; |
|||
|
|||
interface CfdTileProps { |
|||
index: number; |
|||
cfd: Cfd; |
|||
} |
|||
|
|||
export default function CfdTile( |
|||
{ |
|||
index, |
|||
cfd, |
|||
}: CfdTileProps, |
|||
) { |
|||
const toast = useToast(); |
|||
|
|||
let { run: acceptOrder, isLoading: isAccepting } = useAsync({ |
|||
deferFn: async ([args]: any[]) => { |
|||
try { |
|||
let payload = { |
|||
order_id: args.order_id, |
|||
}; |
|||
await postAcceptOrder(payload); |
|||
} catch (e) { |
|||
const description = typeof e === "string" ? e : JSON.stringify(e); |
|||
|
|||
toast({ |
|||
title: "Error", |
|||
description, |
|||
status: "error", |
|||
duration: 9000, |
|||
isClosable: true, |
|||
}); |
|||
} |
|||
}, |
|||
}); |
|||
|
|||
let { run: rejectOrder, isLoading: isRejecting } = useAsync({ |
|||
deferFn: async ([args]: any[]) => { |
|||
try { |
|||
let payload = { |
|||
order_id: args.order_id, |
|||
}; |
|||
await postRejectOrder(payload); |
|||
} catch (e) { |
|||
const description = typeof e === "string" ? e : JSON.stringify(e); |
|||
|
|||
toast({ |
|||
title: "Error", |
|||
description, |
|||
status: "error", |
|||
duration: 9000, |
|||
isClosable: true, |
|||
}); |
|||
} |
|||
}, |
|||
}); |
|||
|
|||
let actionButtons; |
|||
if (cfd.state === "Open") { |
|||
actionButtons = <Box paddingBottom={5}> |
|||
<Button colorScheme="blue" variant="solid"> |
|||
Close |
|||
</Button> |
|||
</Box>; |
|||
} else if (cfd.state == "Requested") { |
|||
actionButtons = ( |
|||
<HStack> |
|||
<Box paddingBottom={5}> |
|||
<Button |
|||
colorScheme="blue" |
|||
variant="solid" |
|||
onClick={async () => acceptOrder(cfd)} |
|||
isLoading={isAccepting} |
|||
> |
|||
Accept |
|||
</Button> |
|||
</Box> |
|||
<Box paddingBottom={5}> |
|||
<Button |
|||
colorScheme="blue" |
|||
variant="solid" |
|||
onClick={async () => rejectOrder(cfd)} |
|||
isLoading={isRejecting} |
|||
> |
|||
Reject |
|||
</Button> |
|||
</Box> |
|||
</HStack> |
|||
); |
|||
} |
|||
|
|||
return ( |
|||
<Box borderRadius={"md"} borderColor={"blue.800"} borderWidth={2} bg={"gray.50"}> |
|||
<VStack> |
|||
<Box bg="blue.800" w="100%"> |
|||
<Text padding={2} color={"white"} fontWeight={"bold"}>CFD #{index}</Text> |
|||
</Box> |
|||
<SimpleGrid padding={5} columns={2} spacing={5}> |
|||
<Text>Trading Pair</Text> |
|||
<Text>{cfd.trading_pair}</Text> |
|||
<Text>Position</Text> |
|||
<Text>{cfd.position}</Text> |
|||
<Text>CFD Price</Text> |
|||
<Text>{cfd.initial_price}</Text> |
|||
<Text>Leverage</Text> |
|||
<Text>{cfd.leverage}</Text> |
|||
<Text>Quantity</Text> |
|||
<Text>{cfd.quantity_usd}</Text> |
|||
<Text>Margin</Text> |
|||
<Text>{cfd.margin}</Text> |
|||
<Text>Liquidation Price</Text> |
|||
<Text |
|||
overflow="hidden" |
|||
textOverflow="ellipsis" |
|||
whiteSpace="nowrap" |
|||
_hover={{ overflow: "visible" }} |
|||
> |
|||
{cfd.liquidation_price} |
|||
</Text> |
|||
<Text>Profit</Text> |
|||
<Text>{cfd.profit_usd}</Text> |
|||
<Text>Open since</Text> |
|||
{/* TODO: Format date in a more compact way */} |
|||
<Text> |
|||
{unixTimestampToDate(cfd.state_transition_timestamp).toString()} |
|||
</Text> |
|||
<Text>Status</Text> |
|||
<Text>{cfd.state}</Text> |
|||
</SimpleGrid> |
|||
{actionButtons} |
|||
</VStack> |
|||
</Box> |
|||
); |
|||
} |
@ -0,0 +1,271 @@ |
|||
import { ChevronRightIcon, ChevronUpIcon, TriangleDownIcon, TriangleUpIcon } from "@chakra-ui/icons"; |
|||
import { Badge, Box, chakra, HStack, IconButton, Table as CUITable, Tbody, Td, Th, Thead, Tr } from "@chakra-ui/react"; |
|||
import React from "react"; |
|||
import { Column, Row, useExpanded, useSortBy, useTable } from "react-table"; |
|||
import { Cfd } from "../Types"; |
|||
|
|||
interface CfdTableProps { |
|||
data: Cfd[]; |
|||
} |
|||
|
|||
export function CfdTable( |
|||
{ data }: CfdTableProps, |
|||
) { |
|||
const tableData = React.useMemo( |
|||
() => data, |
|||
[data], |
|||
); |
|||
|
|||
const columns: Array<Column<Cfd>> = React.useMemo( |
|||
() => [ |
|||
{ |
|||
id: "expander", |
|||
Header: () => null, |
|||
Cell: ({ row }: any) => ( |
|||
<span {...row.getToggleRowExpandedProps()}> |
|||
{row.isExpanded |
|||
? <IconButton |
|||
aria-label="Reduce" |
|||
icon={<ChevronUpIcon />} |
|||
onClick={() => { |
|||
row.toggleRowExpanded(); |
|||
}} |
|||
/> |
|||
: <IconButton |
|||
aria-label="Expand" |
|||
icon={<ChevronRightIcon />} |
|||
onClick={() => { |
|||
row.toggleRowExpanded(); |
|||
}} |
|||
/>} |
|||
</span> |
|||
), |
|||
}, |
|||
{ |
|||
Header: "OrderId", |
|||
accessor: "order_id", // accessor is the "key" in the data
|
|||
}, |
|||
{ |
|||
Header: "Position", |
|||
accessor: ({ position }) => { |
|||
let colorScheme = "green"; |
|||
if (position.toLocaleLowerCase() === "buy") { |
|||
colorScheme = "purple"; |
|||
} |
|||
return ( |
|||
<Badge colorScheme={colorScheme}>{position}</Badge> |
|||
); |
|||
}, |
|||
isNumeric: true, |
|||
}, |
|||
|
|||
{ |
|||
Header: "Quantity", |
|||
accessor: ({ quantity_usd }) => { |
|||
return (<Dollars amount={quantity_usd} />); |
|||
}, |
|||
isNumeric: true, |
|||
}, |
|||
{ |
|||
Header: "Leverage", |
|||
accessor: "leverage", |
|||
isNumeric: true, |
|||
}, |
|||
{ |
|||
Header: "Margin", |
|||
accessor: "margin", |
|||
isNumeric: true, |
|||
}, |
|||
{ |
|||
Header: "Initial Price", |
|||
accessor: ({ initial_price }) => { |
|||
return (<Dollars amount={initial_price} />); |
|||
}, |
|||
isNumeric: true, |
|||
}, |
|||
{ |
|||
Header: "Liquidation Price", |
|||
isNumeric: true, |
|||
accessor: ({ liquidation_price }) => { |
|||
return (<Dollars amount={liquidation_price} />); |
|||
}, |
|||
}, |
|||
{ |
|||
Header: "Unrealized P/L", |
|||
accessor: ({ profit_usd }) => { |
|||
return (<Dollars amount={profit_usd} />); |
|||
}, |
|||
isNumeric: true, |
|||
}, |
|||
{ |
|||
Header: "Timestamp", |
|||
accessor: "state_transition_timestamp", |
|||
}, |
|||
{ |
|||
Header: "State", |
|||
accessor: ({ state }) => { |
|||
let colorScheme = "gray"; |
|||
if (state.toLowerCase() === "rejected") { |
|||
colorScheme = "red"; |
|||
} |
|||
if (state.toLowerCase() === "contract setup") { |
|||
colorScheme = "green"; |
|||
} |
|||
return ( |
|||
<Badge colorScheme={colorScheme}>{state}</Badge> |
|||
); |
|||
}, |
|||
}, |
|||
], |
|||
[], |
|||
); |
|||
|
|||
// if we mark certain columns only as hidden, they are still around and we can render them in the sub-row
|
|||
const hiddenColumns = ["order_id", "leverage", "state_transition_timestamp"]; |
|||
|
|||
return ( |
|||
<Table |
|||
tableData={tableData} |
|||
columns={columns} |
|||
hiddenColumns={hiddenColumns} |
|||
renderDetails={renderRowSubComponent} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function renderRowSubComponent(row: Row<Cfd>) { |
|||
// TODO: I would show additional information here such as txids, timestamps, actions
|
|||
let cells = row.allCells |
|||
.filter((cell) => { |
|||
return ["state_transition_timestamp"].includes(cell.column.id); |
|||
}) |
|||
.map((cell) => { |
|||
return cell; |
|||
}); |
|||
|
|||
return ( |
|||
<> |
|||
Showing some more information here... |
|||
<HStack> |
|||
{cells.map(cell => ( |
|||
<Box key={cell.column.id}> |
|||
{cell.column.id} = {cell.render("Cell")} |
|||
</Box> |
|||
))} |
|||
</HStack> |
|||
</> |
|||
); |
|||
} |
|||
|
|||
interface DollarsProps { |
|||
amount: number; |
|||
} |
|||
function Dollars({ amount }: DollarsProps) { |
|||
const price = Math.floor(amount * 100.0) / 100.0; |
|||
return ( |
|||
<> |
|||
$ {price} |
|||
</> |
|||
); |
|||
} |
|||
|
|||
interface TableProps { |
|||
columns: Array<Column<Cfd>>; |
|||
tableData: Cfd[]; |
|||
hiddenColumns: string[]; |
|||
renderDetails: any; |
|||
} |
|||
|
|||
export function Table({ columns, tableData, hiddenColumns, renderDetails }: TableProps) { |
|||
const { |
|||
getTableProps, |
|||
getTableBodyProps, |
|||
headerGroups, |
|||
rows, |
|||
prepareRow, |
|||
visibleColumns, |
|||
} = useTable( |
|||
{ |
|||
columns, |
|||
data: tableData, |
|||
initialState: { |
|||
hiddenColumns, |
|||
}, |
|||
}, |
|||
useSortBy, |
|||
useExpanded, |
|||
); |
|||
|
|||
return ( |
|||
<> |
|||
<CUITable {...getTableProps()} colorScheme="blue"> |
|||
<Thead> |
|||
{headerGroups.map((headerGroup) => ( |
|||
<Tr {...headerGroup.getHeaderGroupProps()}> |
|||
{headerGroup.headers.map((column) => ( |
|||
<Th |
|||
// @ts-ignore
|
|||
{...column.getHeaderProps(column.getSortByToggleProps())} |
|||
// @ts-ignore
|
|||
isNumeric={column.isNumeric} |
|||
> |
|||
{column.render("Header")} |
|||
<chakra.span pl="4"> |
|||
{// @ts-ignore
|
|||
column.isSorted |
|||
? ( |
|||
// @ts-ignore
|
|||
column.isSortedDesc |
|||
? ( |
|||
<TriangleDownIcon aria-label="sorted descending" /> |
|||
) |
|||
: ( |
|||
<TriangleUpIcon aria-label="sorted ascending" /> |
|||
) |
|||
) |
|||
: null} |
|||
</chakra.span> |
|||
</Th> |
|||
))} |
|||
</Tr> |
|||
))} |
|||
</Thead> |
|||
<Tbody {...getTableBodyProps()}> |
|||
{rows.map((row) => { |
|||
prepareRow(row); |
|||
return ( |
|||
<React.Fragment key={row.id}> |
|||
<Tr |
|||
{...row.getRowProps()} |
|||
onClick={() => |
|||
// @ts-ignore
|
|||
row.toggleRowExpanded()} |
|||
> |
|||
{row.cells.map((cell) => ( |
|||
// @ts-ignore
|
|||
<Td {...cell.getCellProps()} isNumeric={cell.column.isNumeric}> |
|||
{cell.render("Cell")} |
|||
</Td> |
|||
))} |
|||
</Tr> |
|||
|
|||
{// @ts-ignore
|
|||
row.isExpanded |
|||
? ( |
|||
<Tr> |
|||
<Td> |
|||
</Td> |
|||
<Td colSpan={visibleColumns.length - 1}> |
|||
{renderDetails(row)} |
|||
</Td> |
|||
</Tr> |
|||
) |
|||
: null} |
|||
</React.Fragment> |
|||
); |
|||
})} |
|||
</Tbody> |
|||
</CUITable> |
|||
</> |
|||
); |
|||
} |
@ -0,0 +1,236 @@ |
|||
import { CheckIcon, ChevronRightIcon, ChevronUpIcon, CloseIcon } from "@chakra-ui/icons"; |
|||
import { Badge, Box, HStack, IconButton, useToast } from "@chakra-ui/react"; |
|||
import React from "react"; |
|||
import { useAsync } from "react-async"; |
|||
import { Column, Row } from "react-table"; |
|||
import { postAcceptOrder, postRejectOrder } from "../../MakerClient"; |
|||
import { Cfd } from "../Types"; |
|||
import { Table } from "./CfdTable"; |
|||
|
|||
interface CfdMableTakerProps { |
|||
data: Cfd[]; |
|||
} |
|||
|
|||
export function CfdTableMaker( |
|||
{ data }: CfdMableTakerProps, |
|||
) { |
|||
const tableData = React.useMemo( |
|||
() => data, |
|||
[data], |
|||
); |
|||
|
|||
const toast = useToast(); |
|||
|
|||
let { run: acceptOrder, isLoading: isAccepting } = useAsync({ |
|||
deferFn: async ([orderId]: any[]) => { |
|||
try { |
|||
let payload = { |
|||
order_id: orderId, |
|||
}; |
|||
await postAcceptOrder(payload); |
|||
} catch (e) { |
|||
const description = typeof e === "string" ? e : JSON.stringify(e); |
|||
|
|||
toast({ |
|||
title: "Error", |
|||
description, |
|||
status: "error", |
|||
duration: 9000, |
|||
isClosable: true, |
|||
}); |
|||
} |
|||
}, |
|||
}); |
|||
|
|||
let { run: rejectOrder, isLoading: isRejecting } = useAsync({ |
|||
deferFn: async ([orderId]: any[]) => { |
|||
try { |
|||
let payload = { |
|||
order_id: orderId, |
|||
}; |
|||
await postRejectOrder(payload); |
|||
} catch (e) { |
|||
const description = typeof e === "string" ? e : JSON.stringify(e); |
|||
|
|||
toast({ |
|||
title: "Error", |
|||
description, |
|||
status: "error", |
|||
duration: 9000, |
|||
isClosable: true, |
|||
}); |
|||
} |
|||
}, |
|||
}); |
|||
|
|||
const columns: Array<Column<Cfd>> = React.useMemo( |
|||
() => [ |
|||
{ |
|||
id: "expander", |
|||
Header: () => null, |
|||
Cell: ({ row }: any) => ( |
|||
<span {...row.getToggleRowExpandedProps()}> |
|||
{row.isExpanded |
|||
? <IconButton |
|||
aria-label="Reduce" |
|||
icon={<ChevronUpIcon />} |
|||
onClick={() => { |
|||
row.toggleRowExpanded(); |
|||
}} |
|||
/> |
|||
: <IconButton |
|||
aria-label="Expand" |
|||
icon={<ChevronRightIcon />} |
|||
onClick={() => { |
|||
row.toggleRowExpanded(); |
|||
}} |
|||
/>} |
|||
</span> |
|||
), |
|||
}, |
|||
{ |
|||
Header: "OrderId", |
|||
accessor: "order_id", |
|||
}, |
|||
{ |
|||
Header: "Position", |
|||
accessor: ({ position }) => { |
|||
let colorScheme = "green"; |
|||
if (position.toLocaleLowerCase() === "buy") { |
|||
colorScheme = "purple"; |
|||
} |
|||
return ( |
|||
<Badge colorScheme={colorScheme}>{position}</Badge> |
|||
); |
|||
}, |
|||
isNumeric: true, |
|||
}, |
|||
|
|||
{ |
|||
Header: "Quantity", |
|||
accessor: ({ quantity_usd }) => { |
|||
return (<Dollars amount={quantity_usd} />); |
|||
}, |
|||
isNumeric: true, |
|||
}, |
|||
{ |
|||
Header: "Leverage", |
|||
accessor: "leverage", |
|||
isNumeric: true, |
|||
}, |
|||
{ |
|||
Header: "Margin", |
|||
accessor: "margin", |
|||
isNumeric: true, |
|||
}, |
|||
{ |
|||
Header: "Initial Price", |
|||
accessor: ({ initial_price }) => { |
|||
return (<Dollars amount={initial_price} />); |
|||
}, |
|||
isNumeric: true, |
|||
}, |
|||
{ |
|||
Header: "Liquidation Price", |
|||
isNumeric: true, |
|||
accessor: ({ liquidation_price }) => { |
|||
return (<Dollars amount={liquidation_price} />); |
|||
}, |
|||
}, |
|||
{ |
|||
Header: "Unrealized P/L", |
|||
accessor: ({ profit_usd }) => { |
|||
return (<Dollars amount={profit_usd} />); |
|||
}, |
|||
isNumeric: true, |
|||
}, |
|||
{ |
|||
Header: "Timestamp", |
|||
accessor: "state_transition_timestamp", |
|||
}, |
|||
{ |
|||
Header: "Action", |
|||
accessor: ({ state, order_id }) => { |
|||
if (state.toLowerCase() === "requested") { |
|||
return (<HStack> |
|||
<IconButton |
|||
colorScheme="green" |
|||
aria-label="Accept" |
|||
icon={<CheckIcon />} |
|||
onClick={async () => acceptOrder(order_id)} |
|||
isLoading={isAccepting} |
|||
/> |
|||
<IconButton |
|||
colorScheme="red" |
|||
aria-label="Reject" |
|||
icon={<CloseIcon />} |
|||
onClick={async () => rejectOrder(order_id)} |
|||
isLoading={isRejecting} |
|||
/> |
|||
</HStack>); |
|||
} |
|||
|
|||
let colorScheme = "gray"; |
|||
if (state.toLowerCase() === "rejected") { |
|||
colorScheme = "red"; |
|||
} |
|||
if (state.toLowerCase() === "contract setup") { |
|||
colorScheme = "green"; |
|||
} |
|||
return ( |
|||
<Badge colorScheme={colorScheme}>{state}</Badge> |
|||
); |
|||
}, |
|||
}, |
|||
], |
|||
[], |
|||
); |
|||
|
|||
// if we mark certain columns only as hidden, they are still around and we can render them in the sub-row
|
|||
const hiddenColumns = ["order_id", "leverage", "Unrealized P/L", "state_transition_timestamp"]; |
|||
|
|||
return ( |
|||
<Table |
|||
tableData={tableData} |
|||
columns={columns} |
|||
hiddenColumns={hiddenColumns} |
|||
renderDetails={renderRowSubComponent} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function renderRowSubComponent(row: Row<Cfd>) { |
|||
// TODO: I would show additional information here such as txids, timestamps, actions
|
|||
let cells = row.allCells |
|||
.filter((cell) => { |
|||
return ["state_transition_timestamp"].includes(cell.column.id); |
|||
}) |
|||
.map((cell) => { |
|||
return cell; |
|||
}); |
|||
|
|||
return ( |
|||
<> |
|||
Showing some more information here... |
|||
<HStack> |
|||
{cells.map(cell => ( |
|||
<Box key={cell.column.id}> |
|||
{cell.column.id} = {cell.render("Cell")} |
|||
</Box> |
|||
))} |
|||
</HStack> |
|||
</> |
|||
); |
|||
} |
|||
|
|||
interface DollarsProps { |
|||
amount: number; |
|||
} |
|||
function Dollars({ amount }: DollarsProps) { |
|||
const price = Math.floor(amount * 100.0) / 100.0; |
|||
return ( |
|||
<> |
|||
$ {price} |
|||
</> |
|||
); |
|||
} |
Loading…
Reference in new issue