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);
let (task, mut quote_updates) = bitmex_price_feed::new().await?;
let (task, quote_updates) = bitmex_price_feed::new().await?;
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)
.manage(cfd_feed_receiver)
.manage(order_feed_receiver)
.manage(wallet_feed_receiver)
.manage(auth_password)
.manage(quote_updates)
.attach(Db::init())
.attach(AdHoc::try_on_ignite(
"SQL migrations",

9
daemon/src/routes_maker.rs

@ -1,4 +1,5 @@
use crate::auth::Authenticated;
use crate::bitmex_price_feed;
use crate::maker_cfd;
use crate::model::cfd::{Cfd, Order, OrderId, Origin};
use crate::model::{Usd, WalletInfo};
@ -23,11 +24,13 @@ pub async fn maker_feed(
rx_cfds: &State<watch::Receiver<Vec<Cfd>>>,
rx_order: &State<watch::Receiver<Option<Order>>>,
rx_wallet: &State<watch::Receiver<WalletInfo>>,
rx_quote: &State<watch::Receiver<bitmex_price_feed::Quote>>,
_auth: Authenticated,
) -> EventStream![] {
let mut rx_cfds = rx_cfds.inner().clone();
let mut rx_order = rx_order.inner().clone();
let mut rx_wallet = rx_wallet.inner().clone();
let mut rx_quote = rx_quote.inner().clone();
EventStream! {
let wallet_info = rx_wallet.borrow().clone();
@ -38,6 +41,9 @@ pub async fn maker_feed(
let cfds = rx_cfds.borrow().clone();
yield cfds.to_sse_event();
let quote = rx_quote.borrow().clone();
yield quote.to_sse_event();
loop{
select! {
@ -52,6 +58,9 @@ pub async fn maker_feed(
Ok(()) = rx_cfds.changed() => {
let cfds = rx_cfds.borrow().clone();
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::{Leverage, Usd, WalletInfo};
use crate::routes::EmbeddedFileExt;
@ -22,10 +23,12 @@ pub async fn feed(
rx_cfds: &State<watch::Receiver<Vec<Cfd>>>,
rx_order: &State<watch::Receiver<Option<Order>>>,
rx_wallet: &State<watch::Receiver<WalletInfo>>,
rx_quote: &State<watch::Receiver<bitmex_price_feed::Quote>>,
) -> EventStream![] {
let mut rx_cfds = rx_cfds.inner().clone();
let mut rx_order = rx_order.inner().clone();
let mut rx_wallet = rx_wallet.inner().clone();
let mut rx_quote = rx_quote.inner().clone();
EventStream! {
let wallet_info = rx_wallet.borrow().clone();
@ -36,6 +39,9 @@ pub async fn feed(
let cfds = rx_cfds.borrow().clone();
yield cfds.to_sse_event();
let quote = rx_quote.borrow().clone();
yield quote.to_sse_event();
loop{
select! {
@ -50,6 +56,9 @@ pub async fn feed(
Ok(()) = rx_cfds.changed() => {
let cfds = rx_cfds.borrow().clone();
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);
// 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()
.merge(("databases.taker.url", data_dir.join("taker.sqlite")))
.merge(("port", opts.http_port));
@ -141,6 +129,7 @@ async fn main() -> Result<()> {
.manage(cfd_feed_receiver)
.manage(order_feed_receiver)
.manage(wallet_feed_receiver)
.manage(quote_updates)
.attach(Db::init())
.attach(AdHoc::try_on_ignite(
"SQL migrations",

35
daemon/src/to_sse_event.rs

@ -1,10 +1,10 @@
use crate::model;
use crate::model::cfd::OrderId;
use crate::model::{Leverage, Position, TradingPair, Usd};
use crate::{bitmex_price_feed, model};
use bdk::bitcoin::Amount;
use rocket::response::stream::Event;
use serde::Serialize;
use std::time::UNIX_EPOCH;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Serialize)]
pub struct Cfd {
@ -127,13 +127,34 @@ impl ToSseEvent for model::WalletInfo {
let wallet_info = WalletInfo {
balance: self.balance,
address: self.address.to_string(),
last_updated_at: self
.last_updated_at
.duration_since(UNIX_EPOCH)
.expect("timestamp to be convertible to duration since epoch")
.as_secs(),
last_updated_at: into_unix_secs(self.last_updated_at),
};
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 { CfdTableMaker } from "./components/cfdtables/CfdTableMaker";
import CurrencyInputField from "./components/CurrencyInputField";
import CurrentPrice from "./components/CurrentPrice";
import useLatestEvent from "./components/Hooks";
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 { CfdSellOrderPayload, postCfdSellOrderRequest } from "./MakerClient";
@ -35,6 +36,7 @@ export default function App() {
console.log(cfds);
const walletInfo = useLatestEvent<WalletInfo>(source, "wallet");
const priceInfo = useLatestEvent<PriceInfo>(source, "quote");
const toast = useToast();
let [minQuantity, setMinQuantity] = useState<string>("100");
@ -80,11 +82,8 @@ export default function App() {
<HStack spacing={5}>
<VStack>
<Wallet walletInfo={walletInfo} />
<CurrentPrice priceInfo={priceInfo} />
<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>
<Text width={labelWidth}>Min Quantity:</Text>
<CurrencyInputField

5
frontend/src/TakerApp.tsx

@ -18,8 +18,9 @@ import { useAsync } from "react-async";
import { useEventSource } from "react-sse-hooks";
import { CfdTable } from "./components/cfdtables/CfdTable";
import CurrencyInputField from "./components/CurrencyInputField";
import CurrentPrice from "./components/CurrentPrice";
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";
interface CfdOrderRequestPayload {
@ -62,6 +63,7 @@ export default function App() {
let cfds = cfdsOrUndefined ? cfdsOrUndefined! : [];
const order = useLatestEvent<Order>(source, "order");
const walletInfo = useLatestEvent<WalletInfo>(source, "wallet");
const priceInfo = useLatestEvent<PriceInfo>(source, "quote");
const toast = useToast();
let [quantity, setQuantity] = useState("0");
@ -123,6 +125,7 @@ export default function App() {
<HStack spacing={5}>
<VStack>
<Wallet walletInfo={walletInfo} />
<CurrentPrice priceInfo={priceInfo} />
<VStack shadow={"md"} padding={5} align="stretch" spacing={5} width="100%">
<HStack>
<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;
}
export interface PriceInfo {
bid: number;
ask: number;
last_updated_at: number;
}
export function unixTimestampToDate(unixTimestamp: number): Date {
return new Date(unixTimestamp * 1000);
}

14
frontend/src/components/Wallet.tsx

@ -1,7 +1,8 @@
import { CheckIcon, CopyIcon } from "@chakra-ui/icons";
import { Box, Center, Divider, HStack, IconButton, Skeleton, Text, useClipboard } from "@chakra-ui/react";
import React from "react";
import { unixTimestampToDate, WalletInfo } from "./Types";
import Timestamp from "./Timestamp";
import { WalletInfo } from "./Types";
interface WalletProps {
walletInfo: WalletInfo | null;
@ -30,16 +31,7 @@ export default function Wallet(
/>
</HStack>
);
timestamp = <Text>
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>;
timestamp = <Timestamp timestamp={walletInfo.last_updated_at} />;
}
return (

Loading…
Cancel
Save