Browse Source

Display price feed in the UI

Display both bid and ask price feed from BitMex.
fix-olivia-event-id
Mariusz Klochowicz 3 years ago
parent
commit
641905e6be
No known key found for this signature in database GPG Key ID: 470C865699C8D4D
  1. 15
      daemon/src/maker.rs
  2. 9
      daemon/src/routes_maker.rs
  3. 9
      daemon/src/routes_taker.rs
  4. 15
      daemon/src/taker.rs
  5. 35
      daemon/src/to_sse_event.rs
  6. 9
      frontend/src/MakerApp.tsx
  7. 5
      frontend/src/TakerApp.tsx
  8. 41
      frontend/src/components/CurrentPrice.tsx
  9. 26
      frontend/src/components/Timestamp.tsx
  10. 6
      frontend/src/components/Types.tsx
  11. 14
      frontend/src/components/Wallet.tsx

15
daemon/src/maker.rs

@ -118,26 +118,15 @@ async fn main() -> Result<()> {
tracing::info!("Listening on {}", local_addr); tracing::info!("Listening on {}", local_addr);
let (task, mut quote_updates) = bitmex_price_feed::new().await?; let (task, quote_updates) = bitmex_price_feed::new().await?;
tokio::spawn(task); tokio::spawn(task);
// dummy usage of quote receiver
tokio::spawn(async move {
loop {
let bitmex_price_feed::Quote { bid, ask, .. } = *quote_updates.borrow();
tracing::info!(%bid, %ask, "BitMex quote updated");
if quote_updates.changed().await.is_err() {
return;
}
}
});
rocket::custom(figment) rocket::custom(figment)
.manage(cfd_feed_receiver) .manage(cfd_feed_receiver)
.manage(order_feed_receiver) .manage(order_feed_receiver)
.manage(wallet_feed_receiver) .manage(wallet_feed_receiver)
.manage(auth_password) .manage(auth_password)
.manage(quote_updates)
.attach(Db::init()) .attach(Db::init())
.attach(AdHoc::try_on_ignite( .attach(AdHoc::try_on_ignite(
"SQL migrations", "SQL migrations",

9
daemon/src/routes_maker.rs

@ -1,4 +1,5 @@
use crate::auth::Authenticated; use crate::auth::Authenticated;
use crate::bitmex_price_feed;
use crate::maker_cfd; use crate::maker_cfd;
use crate::model::cfd::{Cfd, Order, OrderId, Origin}; use crate::model::cfd::{Cfd, Order, OrderId, Origin};
use crate::model::{Usd, WalletInfo}; use crate::model::{Usd, WalletInfo};
@ -23,11 +24,13 @@ pub async fn maker_feed(
rx_cfds: &State<watch::Receiver<Vec<Cfd>>>, rx_cfds: &State<watch::Receiver<Vec<Cfd>>>,
rx_order: &State<watch::Receiver<Option<Order>>>, rx_order: &State<watch::Receiver<Option<Order>>>,
rx_wallet: &State<watch::Receiver<WalletInfo>>, rx_wallet: &State<watch::Receiver<WalletInfo>>,
rx_quote: &State<watch::Receiver<bitmex_price_feed::Quote>>,
_auth: Authenticated, _auth: Authenticated,
) -> EventStream![] { ) -> EventStream![] {
let mut rx_cfds = rx_cfds.inner().clone(); let mut rx_cfds = rx_cfds.inner().clone();
let mut rx_order = rx_order.inner().clone(); let mut rx_order = rx_order.inner().clone();
let mut rx_wallet = rx_wallet.inner().clone(); let mut rx_wallet = rx_wallet.inner().clone();
let mut rx_quote = rx_quote.inner().clone();
EventStream! { EventStream! {
let wallet_info = rx_wallet.borrow().clone(); let wallet_info = rx_wallet.borrow().clone();
@ -38,6 +41,9 @@ pub async fn maker_feed(
let cfds = rx_cfds.borrow().clone(); let cfds = rx_cfds.borrow().clone();
yield cfds.to_sse_event(); yield cfds.to_sse_event();
let quote = rx_quote.borrow().clone();
yield quote.to_sse_event();
loop{ loop{
select! { select! {
@ -52,6 +58,9 @@ pub async fn maker_feed(
Ok(()) = rx_cfds.changed() => { Ok(()) = rx_cfds.changed() => {
let cfds = rx_cfds.borrow().clone(); let cfds = rx_cfds.borrow().clone();
yield cfds.to_sse_event(); yield cfds.to_sse_event();
Ok(()) = rx_quote.changed() => {
let quote = rx_quote.borrow().clone();
yield quote.to_sse_event();
} }
} }
} }

9
daemon/src/routes_taker.rs

@ -1,3 +1,4 @@
use crate::bitmex_price_feed;
use crate::model::cfd::{calculate_buy_margin, Cfd, Order, OrderId}; use crate::model::cfd::{calculate_buy_margin, Cfd, Order, OrderId};
use crate::model::{Leverage, Usd, WalletInfo}; use crate::model::{Leverage, Usd, WalletInfo};
use crate::routes::EmbeddedFileExt; use crate::routes::EmbeddedFileExt;
@ -22,10 +23,12 @@ pub async fn feed(
rx_cfds: &State<watch::Receiver<Vec<Cfd>>>, rx_cfds: &State<watch::Receiver<Vec<Cfd>>>,
rx_order: &State<watch::Receiver<Option<Order>>>, rx_order: &State<watch::Receiver<Option<Order>>>,
rx_wallet: &State<watch::Receiver<WalletInfo>>, rx_wallet: &State<watch::Receiver<WalletInfo>>,
rx_quote: &State<watch::Receiver<bitmex_price_feed::Quote>>,
) -> EventStream![] { ) -> EventStream![] {
let mut rx_cfds = rx_cfds.inner().clone(); let mut rx_cfds = rx_cfds.inner().clone();
let mut rx_order = rx_order.inner().clone(); let mut rx_order = rx_order.inner().clone();
let mut rx_wallet = rx_wallet.inner().clone(); let mut rx_wallet = rx_wallet.inner().clone();
let mut rx_quote = rx_quote.inner().clone();
EventStream! { EventStream! {
let wallet_info = rx_wallet.borrow().clone(); let wallet_info = rx_wallet.borrow().clone();
@ -36,6 +39,9 @@ pub async fn feed(
let cfds = rx_cfds.borrow().clone(); let cfds = rx_cfds.borrow().clone();
yield cfds.to_sse_event(); yield cfds.to_sse_event();
let quote = rx_quote.borrow().clone();
yield quote.to_sse_event();
loop{ loop{
select! { select! {
@ -50,6 +56,9 @@ pub async fn feed(
Ok(()) = rx_cfds.changed() => { Ok(()) = rx_cfds.changed() => {
let cfds = rx_cfds.borrow().clone(); let cfds = rx_cfds.borrow().clone();
yield cfds.to_sse_event(); yield cfds.to_sse_event();
Ok(()) = rx_quote.changed() => {
let quote = rx_quote.borrow().clone();
yield quote.to_sse_event();
} }
} }
} }

15
daemon/src/taker.rs

@ -118,21 +118,9 @@ async fn main() -> Result<()> {
} }
}; };
let (task, mut quote_updates) = bitmex_price_feed::new().await?; let (task, quote_updates) = bitmex_price_feed::new().await?;
tokio::spawn(task); tokio::spawn(task);
// dummy usage of quote receiver
tokio::spawn(async move {
loop {
let bitmex_price_feed::Quote { bid, ask, .. } = *quote_updates.borrow();
tracing::info!(%bid, %ask, "BitMex quote updated");
if quote_updates.changed().await.is_err() {
return;
}
}
});
let figment = rocket::Config::figment() let figment = rocket::Config::figment()
.merge(("databases.taker.url", data_dir.join("taker.sqlite"))) .merge(("databases.taker.url", data_dir.join("taker.sqlite")))
.merge(("port", opts.http_port)); .merge(("port", opts.http_port));
@ -141,6 +129,7 @@ async fn main() -> Result<()> {
.manage(cfd_feed_receiver) .manage(cfd_feed_receiver)
.manage(order_feed_receiver) .manage(order_feed_receiver)
.manage(wallet_feed_receiver) .manage(wallet_feed_receiver)
.manage(quote_updates)
.attach(Db::init()) .attach(Db::init())
.attach(AdHoc::try_on_ignite( .attach(AdHoc::try_on_ignite(
"SQL migrations", "SQL migrations",

35
daemon/src/to_sse_event.rs

@ -1,10 +1,10 @@
use crate::model;
use crate::model::cfd::OrderId; use crate::model::cfd::OrderId;
use crate::model::{Leverage, Position, TradingPair, Usd}; use crate::model::{Leverage, Position, TradingPair, Usd};
use crate::{bitmex_price_feed, model};
use bdk::bitcoin::Amount; use bdk::bitcoin::Amount;
use rocket::response::stream::Event; use rocket::response::stream::Event;
use serde::Serialize; use serde::Serialize;
use std::time::UNIX_EPOCH; use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct Cfd { pub struct Cfd {
@ -127,13 +127,34 @@ impl ToSseEvent for model::WalletInfo {
let wallet_info = WalletInfo { let wallet_info = WalletInfo {
balance: self.balance, balance: self.balance,
address: self.address.to_string(), address: self.address.to_string(),
last_updated_at: self last_updated_at: into_unix_secs(self.last_updated_at),
.last_updated_at
.duration_since(UNIX_EPOCH)
.expect("timestamp to be convertible to duration since epoch")
.as_secs(),
}; };
Event::json(&wallet_info).event("wallet") Event::json(&wallet_info).event("wallet")
} }
} }
#[derive(Debug, Clone, Serialize)]
pub struct Quote {
bid: Usd,
ask: Usd,
last_updated_at: u64,
}
impl ToSseEvent for bitmex_price_feed::Quote {
fn to_sse_event(&self) -> Event {
let quote = Quote {
bid: self.bid,
ask: self.ask,
last_updated_at: into_unix_secs(self.timestamp),
};
Event::json(&quote).event("quote")
}
}
/// Convert to the format expected by the frontend
fn into_unix_secs(time: SystemTime) -> u64 {
time.duration_since(UNIX_EPOCH)
.expect("timestamp to be convertible to duration since epoch")
.as_secs()
}

9
frontend/src/MakerApp.tsx

@ -19,9 +19,10 @@ import { useEventSource } from "react-sse-hooks";
import { CfdTable } from "./components/cfdtables/CfdTable"; import { CfdTable } from "./components/cfdtables/CfdTable";
import { CfdTableMaker } from "./components/cfdtables/CfdTableMaker"; import { CfdTableMaker } from "./components/cfdtables/CfdTableMaker";
import CurrencyInputField from "./components/CurrencyInputField"; import CurrencyInputField from "./components/CurrencyInputField";
import CurrentPrice from "./components/CurrentPrice";
import useLatestEvent from "./components/Hooks"; import useLatestEvent from "./components/Hooks";
import OrderTile from "./components/OrderTile"; import OrderTile from "./components/OrderTile";
import { Cfd, Order, WalletInfo } from "./components/Types"; import { Cfd, Order, PriceInfo, WalletInfo } from "./components/Types";
import Wallet from "./components/Wallet"; import Wallet from "./components/Wallet";
import { CfdSellOrderPayload, postCfdSellOrderRequest } from "./MakerClient"; import { CfdSellOrderPayload, postCfdSellOrderRequest } from "./MakerClient";
@ -35,6 +36,7 @@ export default function App() {
console.log(cfds); console.log(cfds);
const walletInfo = useLatestEvent<WalletInfo>(source, "wallet"); const walletInfo = useLatestEvent<WalletInfo>(source, "wallet");
const priceInfo = useLatestEvent<PriceInfo>(source, "quote");
const toast = useToast(); const toast = useToast();
let [minQuantity, setMinQuantity] = useState<string>("100"); let [minQuantity, setMinQuantity] = useState<string>("100");
@ -80,11 +82,8 @@ export default function App() {
<HStack spacing={5}> <HStack spacing={5}>
<VStack> <VStack>
<Wallet walletInfo={walletInfo} /> <Wallet walletInfo={walletInfo} />
<CurrentPrice priceInfo={priceInfo} />
<VStack spacing={5} shadow={"md"} padding={5} width="100%" align={"stretch"}> <VStack spacing={5} shadow={"md"} padding={5} width="100%" align={"stretch"}>
<HStack>
<Text width={labelWidth} align={"left"}>Current Price:</Text>
<Text>{49000}</Text>
</HStack>
<HStack> <HStack>
<Text width={labelWidth}>Min Quantity:</Text> <Text width={labelWidth}>Min Quantity:</Text>
<CurrencyInputField <CurrencyInputField

5
frontend/src/TakerApp.tsx

@ -18,8 +18,9 @@ import { useAsync } from "react-async";
import { useEventSource } from "react-sse-hooks"; import { useEventSource } from "react-sse-hooks";
import { CfdTable } from "./components/cfdtables/CfdTable"; import { CfdTable } from "./components/cfdtables/CfdTable";
import CurrencyInputField from "./components/CurrencyInputField"; import CurrencyInputField from "./components/CurrencyInputField";
import CurrentPrice from "./components/CurrentPrice";
import useLatestEvent from "./components/Hooks"; import useLatestEvent from "./components/Hooks";
import { Cfd, Order, WalletInfo } from "./components/Types"; import { Cfd, Order, PriceInfo, WalletInfo } from "./components/Types";
import Wallet from "./components/Wallet"; import Wallet from "./components/Wallet";
interface CfdOrderRequestPayload { interface CfdOrderRequestPayload {
@ -62,6 +63,7 @@ export default function App() {
let cfds = cfdsOrUndefined ? cfdsOrUndefined! : []; let cfds = cfdsOrUndefined ? cfdsOrUndefined! : [];
const order = useLatestEvent<Order>(source, "order"); const order = useLatestEvent<Order>(source, "order");
const walletInfo = useLatestEvent<WalletInfo>(source, "wallet"); const walletInfo = useLatestEvent<WalletInfo>(source, "wallet");
const priceInfo = useLatestEvent<PriceInfo>(source, "quote");
const toast = useToast(); const toast = useToast();
let [quantity, setQuantity] = useState("0"); let [quantity, setQuantity] = useState("0");
@ -123,6 +125,7 @@ export default function App() {
<HStack spacing={5}> <HStack spacing={5}>
<VStack> <VStack>
<Wallet walletInfo={walletInfo} /> <Wallet walletInfo={walletInfo} />
<CurrentPrice priceInfo={priceInfo} />
<VStack shadow={"md"} padding={5} align="stretch" spacing={5} width="100%"> <VStack shadow={"md"} padding={5} align="stretch" spacing={5} width="100%">
<HStack> <HStack>
<Text align={"left"} width={labelWidth}>Order Price:</Text> <Text align={"left"} width={labelWidth}>Order Price:</Text>

41
frontend/src/components/CurrentPrice.tsx

@ -0,0 +1,41 @@
import { Box, Center, Divider, HStack, Skeleton, Text } from "@chakra-ui/react";
import React from "react";
import Timestamp from "./Timestamp";
import { PriceInfo } from "./Types";
interface Props {
priceInfo: PriceInfo | null;
}
export default function CurrentPrice(
{
priceInfo,
}: Props,
) {
let bid = <Skeleton height="20px" />;
let ask = <Skeleton height="20px" />;
let timestamp = <Skeleton height="20px" />;
if (priceInfo) {
bid = <Text>{priceInfo.bid} USD</Text>;
ask = <Text>{priceInfo.ask} USD</Text>;
timestamp = <Timestamp timestamp={priceInfo.last_updated_at} />;
}
return (
<Box shadow={"md"} marginBottom={5} padding={5}>
<Center><Text fontWeight={"bold"}>Current Price</Text></Center>
<HStack>
<Text align={"left"}>Bid:</Text>
{bid}
</HStack>
<Divider marginTop={2} marginBottom={2} />
<HStack>
<Text align={"left"}>Ask:</Text>
{ask}
</HStack>
<Divider marginTop={2} marginBottom={2} />
{timestamp}
</Box>
);
}

26
frontend/src/components/Timestamp.tsx

@ -0,0 +1,26 @@
import { Text } from "@chakra-ui/react";
import React from "react";
import { unixTimestampToDate } from "./Types";
interface Props {
timestamp: number;
}
export default function Timestamp(
{
timestamp,
}: Props,
) {
return (
<Text>
Updated: {unixTimestampToDate(timestamp).toLocaleDateString("en-US", {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})}
</Text>
);
}

6
frontend/src/components/Types.tsx

@ -37,6 +37,12 @@ export interface WalletInfo {
last_updated_at: number; last_updated_at: number;
} }
export interface PriceInfo {
bid: number;
ask: number;
last_updated_at: number;
}
export function unixTimestampToDate(unixTimestamp: number): Date { export function unixTimestampToDate(unixTimestamp: number): Date {
return new Date(unixTimestamp * 1000); return new Date(unixTimestamp * 1000);
} }

14
frontend/src/components/Wallet.tsx

@ -1,7 +1,8 @@
import { CheckIcon, CopyIcon } from "@chakra-ui/icons"; import { CheckIcon, CopyIcon } from "@chakra-ui/icons";
import { Box, Center, Divider, HStack, IconButton, Skeleton, Text, useClipboard } from "@chakra-ui/react"; import { Box, Center, Divider, HStack, IconButton, Skeleton, Text, useClipboard } from "@chakra-ui/react";
import React from "react"; import React from "react";
import { unixTimestampToDate, WalletInfo } from "./Types"; import Timestamp from "./Timestamp";
import { WalletInfo } from "./Types";
interface WalletProps { interface WalletProps {
walletInfo: WalletInfo | null; walletInfo: WalletInfo | null;
@ -30,16 +31,7 @@ export default function Wallet(
/> />
</HStack> </HStack>
); );
timestamp = <Text> timestamp = <Timestamp timestamp={walletInfo.last_updated_at} />;
Updated: {unixTimestampToDate(walletInfo.last_updated_at).toLocaleDateString("en-US", {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})}
</Text>;
} }
return ( return (

Loading…
Cancel
Save