tailwind #9
39
.gitignore
vendored
39
.gitignore
vendored
@ -1,4 +1,43 @@
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/yarn,react
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=yarn,react
|
||||
|
||||
### react ###
|
||||
.DS_*
|
||||
*.log
|
||||
logs
|
||||
**/*.backup.*
|
||||
**/*.back.*
|
||||
|
||||
node_modules
|
||||
bower_components
|
||||
|
||||
*.sublime*
|
||||
|
||||
psd
|
||||
thumb
|
||||
sketch
|
||||
|
||||
### yarn ###
|
||||
# https://yarnpkg.com/advanced/qa#which-files-should-be-gitignored
|
||||
|
||||
.yarn/*
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# if you are NOT using Zero-installs, then:
|
||||
# comment the following lines
|
||||
!.yarn/cache
|
||||
|
||||
# and uncomment the following lines
|
||||
# .pnp.*
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/yarn,react
|
||||
|
||||
.idea/
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode
|
||||
|
||||
|
12
bfxbot.iml
Normal file
12
bfxbot.iml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PackageRequirementsSettings">
|
||||
<option name="removeUnused" value="true" />
|
||||
</component>
|
||||
</module>
|
485
bfxbot.py
485
bfxbot.py
@ -1,485 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
import shutil
|
||||
import time
|
||||
from enum import Enum
|
||||
from time import sleep
|
||||
from typing import Dict, List
|
||||
|
||||
import dotenv
|
||||
import termplotlib
|
||||
from asciimatics.screen import Screen
|
||||
from bfxapi import Client, Order
|
||||
from bfxapi.models.position import Position
|
||||
from playsound import playsound
|
||||
from termcolor import colored
|
||||
|
||||
|
||||
class Ticker:
|
||||
def __init__(self, sec) -> None:
|
||||
self.seconds: int = sec
|
||||
self.start_time = time.time()
|
||||
self.current_tick: int = 1
|
||||
|
||||
|
||||
class EventKind(Enum):
|
||||
NEW_MINIMUM = 1,
|
||||
NEW_MAXIMUM = 2,
|
||||
REACHED_LOSS = 3,
|
||||
REACHED_BREAK_EVEN = 4,
|
||||
REACHED_MIN_PROFIT = 5,
|
||||
REACHED_GOOD_PROFIT = 6,
|
||||
REACHED_MAX_LOSS = 7,
|
||||
CLOSE_POSITION = 8,
|
||||
TRAILING_STOP_SET = 9,
|
||||
TRAILING_STOP_MOVED = 10,
|
||||
ORDER_SUBMITTED = 11,
|
||||
|
||||
|
||||
class Event:
|
||||
def __init__(self, kind: EventKind, tick: int) -> None:
|
||||
self.kind: EventKind = kind
|
||||
self.tick: int = tick
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.kind.name} @ Tick {self.tick}"
|
||||
|
||||
|
||||
class State(Enum):
|
||||
CRITICAL = -1,
|
||||
LOSS = 0,
|
||||
BREAK_EVEN = 1,
|
||||
MINIMUM_PROFIT = 2,
|
||||
PROFIT = 3
|
||||
|
||||
def color(self) -> str:
|
||||
if self == self.LOSS or self == self.CRITICAL:
|
||||
return "red"
|
||||
elif self == self.BREAK_EVEN:
|
||||
return "yellow"
|
||||
else:
|
||||
return "green"
|
||||
|
||||
|
||||
class Printer:
|
||||
def __init__(self, screen: Screen):
|
||||
self.screen: Screen = screen
|
||||
self.current_line: int = 0
|
||||
(self.current_width, self.current_height) = shutil.get_terminal_size()
|
||||
|
||||
def get_current_line(self) -> int:
|
||||
return self.current_line
|
||||
|
||||
def next(self) -> int:
|
||||
line = self.current_line
|
||||
self.current_line += 1
|
||||
return line
|
||||
|
||||
def print_next_line(self, text):
|
||||
for line in text.split("\n"):
|
||||
self.screen.print_at(line, 0, self.next(), 1)
|
||||
self.screen.refresh()
|
||||
|
||||
def reset_current_line(self):
|
||||
self.current_line = 0
|
||||
|
||||
def set_screen(self, screen: Screen):
|
||||
self.screen = screen
|
||||
|
||||
def has_screen_resized(self):
|
||||
return (self.current_width, self.current_height) != shutil.get_terminal_size()
|
||||
|
||||
def to_current_screen_size(self):
|
||||
(self.current_width, self.current_height) = shutil.get_terminal_size()
|
||||
|
||||
|
||||
class Status:
|
||||
def __init__(self, tick_duration, symbol, printer):
|
||||
self.ticker: Ticker = Ticker(tick_duration)
|
||||
self.events: List[Event] = []
|
||||
self.symbol = symbol
|
||||
self.ticks: Dict[int, (float, Position)] = {}
|
||||
self.current_state: State = State.LOSS
|
||||
self.printer: Printer = printer
|
||||
self.stop_percentage: float = None
|
||||
|
||||
async def update(self, position: Position):
|
||||
self.ticks[self.get_current_tick()] = (await get_current_price(self.symbol), position)
|
||||
|
||||
def wait(self):
|
||||
sleep(self.ticker.seconds)
|
||||
self.ticker.current_tick += 1
|
||||
|
||||
def get_current_tick(self) -> int:
|
||||
return self.ticker.current_tick
|
||||
|
||||
def last_events(self, n):
|
||||
return self.events[-n:]
|
||||
|
||||
def last_position(self) -> Position:
|
||||
return self.ticks[self.ticker.current_tick][1]
|
||||
|
||||
async def add_event(self, event: Event):
|
||||
self.events.append(event)
|
||||
await eh.call_event(event, self)
|
||||
|
||||
async def last_price(self) -> float:
|
||||
return await get_current_price(self.symbol)
|
||||
|
||||
async def set_state(self, state: State):
|
||||
if self.current_state != state:
|
||||
event: Event = None
|
||||
|
||||
if state == State.CRITICAL:
|
||||
event = Event(EventKind.REACHED_MAX_LOSS,
|
||||
self.get_current_tick())
|
||||
elif state == State.LOSS:
|
||||
event = Event(EventKind.REACHED_LOSS,
|
||||
self.get_current_tick())
|
||||
elif state == State.BREAK_EVEN:
|
||||
event = Event(EventKind.REACHED_BREAK_EVEN,
|
||||
self.get_current_tick())
|
||||
elif state == State.MINIMUM_PROFIT:
|
||||
event = Event(EventKind.REACHED_MIN_PROFIT,
|
||||
self.get_current_tick())
|
||||
elif state == State.PROFIT:
|
||||
event = Event(EventKind.REACHED_GOOD_PROFIT,
|
||||
self.get_current_tick())
|
||||
|
||||
self.events.append(event)
|
||||
await eh.call_event(event, self)
|
||||
self.current_state = state
|
||||
|
||||
await eh.call_state(self.current_state, self)
|
||||
|
||||
def get_current_state(self) -> State:
|
||||
return self.current_state
|
||||
|
||||
|
||||
class EventHandler:
|
||||
def __init__(self):
|
||||
self.event_handlers = {}
|
||||
self.state_handlers = {}
|
||||
|
||||
async def call_event(self, event: Event, status: Status):
|
||||
value = event.kind.value
|
||||
if value in self.event_handlers:
|
||||
for h in self.event_handlers[value]:
|
||||
if inspect.iscoroutinefunction(h):
|
||||
await h(event, status)
|
||||
else:
|
||||
h(event, status)
|
||||
|
||||
async def call_state(self, state: State, status: Status):
|
||||
if state in self.state_handlers:
|
||||
for h in self.state_handlers[state]:
|
||||
if inspect.iscoroutinefunction(h):
|
||||
await h(status)
|
||||
else:
|
||||
h(status)
|
||||
|
||||
def on_event(self, kind: EventKind):
|
||||
value = kind.value
|
||||
|
||||
def registerhandler(handler):
|
||||
if value in self.event_handlers:
|
||||
self.event_handlers[value].append(handler)
|
||||
else:
|
||||
self.event_handlers[value] = [handler]
|
||||
return handler
|
||||
|
||||
return registerhandler
|
||||
|
||||
def on_state(self, state: State):
|
||||
def registerhandler(handler):
|
||||
if state in self.state_handlers:
|
||||
self.state_handlers[state].append(handler)
|
||||
else:
|
||||
self.state_handlers[state] = [handler]
|
||||
return handler
|
||||
|
||||
return registerhandler
|
||||
|
||||
|
||||
dotenv.load()
|
||||
API_KEY = dotenv.get('API_KEY', default='')
|
||||
API_SECRET = dotenv.get('API_SECRET', default='')
|
||||
|
||||
bfx = Client(
|
||||
API_KEY=API_KEY,
|
||||
API_SECRET=API_SECRET
|
||||
).rest
|
||||
eh = EventHandler()
|
||||
|
||||
TAKER_FEE = 0.2
|
||||
MAKER_FEE = 0.1
|
||||
|
||||
BREAK_EVEN_PERC = TAKER_FEE
|
||||
MIN_PROFIT_PERC = 0.65
|
||||
GOOD_PROFIT_PERC = MIN_PROFIT_PERC * 2.1
|
||||
MAX_LOSS_PERC = -3.75
|
||||
OFFER_PERC = 0.01
|
||||
|
||||
TRAIL_STOP_PERCENTAGES = {
|
||||
State.MINIMUM_PROFIT: 0.2,
|
||||
State.PROFIT: 0.1
|
||||
}
|
||||
|
||||
|
||||
@eh.on_event(EventKind.REACHED_GOOD_PROFIT)
|
||||
def on_good_profit(event: Event, status: Status):
|
||||
playsound("sounds/coin.mp3")
|
||||
|
||||
|
||||
@eh.on_event(EventKind.REACHED_MIN_PROFIT)
|
||||
def on_min_profit(event: Event, status: Status):
|
||||
playsound("sounds/1up.mp3")
|
||||
|
||||
|
||||
@eh.on_event(EventKind.REACHED_MAX_LOSS)
|
||||
def on_critical(event: Event, status: Status):
|
||||
playsound("sounds/gameover.mp3")
|
||||
|
||||
|
||||
@eh.on_state(State.MINIMUM_PROFIT)
|
||||
def on_state_min_profit(status: Status):
|
||||
update_stop_percentage(State.MINIMUM_PROFIT, status)
|
||||
|
||||
current_pl_perc = net_pl_percentage(
|
||||
status.last_position().profit_loss_percentage, TAKER_FEE)
|
||||
|
||||
if current_pl_perc < status.stop_percentage:
|
||||
status.add_event(Event(EventKind.CLOSE_POSITION,
|
||||
status.get_current_tick()))
|
||||
|
||||
|
||||
@eh.on_state(State.CRITICAL)
|
||||
async def on_state_critical(status: Status):
|
||||
await status.add_event(Event(EventKind.CLOSE_POSITION,
|
||||
status.get_current_tick()))
|
||||
|
||||
|
||||
@eh.on_state(State.PROFIT)
|
||||
def on_state_min_profit(status: Status):
|
||||
update_stop_percentage(State.PROFIT, status)
|
||||
|
||||
current_pl_perc = net_pl_percentage(
|
||||
status.last_position().profit_loss_percentage, TAKER_FEE)
|
||||
|
||||
if current_pl_perc < status.stop_percentage:
|
||||
status.add_event(Event(EventKind.CLOSE_POSITION,
|
||||
status.get_current_tick()))
|
||||
|
||||
|
||||
def update_stop_percentage(state: State, status: Status):
|
||||
last_position = status.last_position()
|
||||
last_pl_net_perc = net_pl_percentage(
|
||||
last_position.profit_loss_percentage, TAKER_FEE)
|
||||
|
||||
# set stop percentage for first time
|
||||
if not status.stop_percentage:
|
||||
status.add_event(Event(EventKind.TRAILING_STOP_SET,
|
||||
status.get_current_tick()))
|
||||
status.stop_percentage = last_pl_net_perc - \
|
||||
TRAIL_STOP_PERCENTAGES[state]
|
||||
return
|
||||
|
||||
# moving trailing stop
|
||||
if last_pl_net_perc - TRAIL_STOP_PERCENTAGES[state] > status.stop_percentage:
|
||||
status.add_event(Event(EventKind.TRAILING_STOP_MOVED,
|
||||
status.get_current_tick()))
|
||||
status.stop_percentage = last_pl_net_perc - \
|
||||
TRAIL_STOP_PERCENTAGES[state]
|
||||
|
||||
return
|
||||
|
||||
|
||||
@eh.on_event(EventKind.CLOSE_POSITION)
|
||||
async def on_close_position(event: Event, status: Status):
|
||||
closing_price = await calculate_best_closing_price(status)
|
||||
amount = status.last_position().amount * -1
|
||||
|
||||
await bfx.submit_order(status.symbol, closing_price, amount, Order.Type.LIMIT)
|
||||
await status.add_event(Event(EventKind.ORDER_SUBMITTED, status.get_current_tick()))
|
||||
|
||||
|
||||
@eh.on_event(EventKind.ORDER_SUBMITTED)
|
||||
def on_order_submitted(event: Event, status: Status):
|
||||
status.printer.print_next_line("ORDER SUBMITTED!")
|
||||
return
|
||||
|
||||
|
||||
async def calculate_best_closing_price(status: Status):
|
||||
p: Position = status.last_position()
|
||||
|
||||
is_long_pos = p.amount < 0
|
||||
|
||||
pub_tick = await bfx.get_public_ticker(status.symbol)
|
||||
|
||||
bid_price = pub_tick[0]
|
||||
ask_price = pub_tick[2]
|
||||
|
||||
if is_long_pos:
|
||||
closing_price = bid_price * (1 - OFFER_PERC / 100)
|
||||
else:
|
||||
closing_price = ask_price * (1 + OFFER_PERC / 100)
|
||||
|
||||
return closing_price
|
||||
|
||||
|
||||
def net_pl_percentage(perc: float, reference_fee_perc: float):
|
||||
return perc - reference_fee_perc
|
||||
|
||||
|
||||
async def main(screen: Screen):
|
||||
min_perc = 999.0
|
||||
max_perc = -999.0
|
||||
symbol = "tBTCUSD"
|
||||
|
||||
printer = Printer(screen)
|
||||
status = Status(20, symbol, printer)
|
||||
balance = await get_usd_balance()
|
||||
|
||||
while True:
|
||||
positions = [p for p in await bfx.get_active_position() if p.symbol == status.symbol]
|
||||
orders = await bfx.get_active_orders(symbol)
|
||||
|
||||
current_price = await status.last_price()
|
||||
|
||||
screen.clear()
|
||||
printer.print_next_line(
|
||||
"Balance: ${} | Current {} price: {} | Current tick ({} sec): {}".format(colored_float(balance, "white"),
|
||||
symbol,
|
||||
colored_float(
|
||||
current_price, "white",
|
||||
attrs=["bold"]),
|
||||
status.ticker.seconds,
|
||||
status.get_current_tick(),
|
||||
))
|
||||
|
||||
if positions:
|
||||
printer.print_next_line("")
|
||||
printer.print_next_line("Open {}:".format(
|
||||
colored("POSITIONS", attrs=["underline"])))
|
||||
|
||||
for p in [p for p in positions if p.symbol == status.symbol]:
|
||||
await status.update(p)
|
||||
|
||||
plfees_percentage = net_pl_percentage(
|
||||
p.profit_loss_percentage, TAKER_FEE)
|
||||
|
||||
if plfees_percentage > GOOD_PROFIT_PERC:
|
||||
await status.set_state(State.PROFIT)
|
||||
elif MIN_PROFIT_PERC <= plfees_percentage < GOOD_PROFIT_PERC:
|
||||
await status.set_state(State.MINIMUM_PROFIT)
|
||||
elif 0.0 <= plfees_percentage < MIN_PROFIT_PERC:
|
||||
await status.set_state(State.BREAK_EVEN)
|
||||
elif MAX_LOSS_PERC < plfees_percentage < 0.0:
|
||||
await status.set_state(State.LOSS)
|
||||
else:
|
||||
await status.set_state(State.CRITICAL)
|
||||
|
||||
status_color = status.get_current_state().color()
|
||||
|
||||
#
|
||||
# min / max calculations
|
||||
#
|
||||
if plfees_percentage > max_perc:
|
||||
max_perc = plfees_percentage
|
||||
await status.add_event(Event(EventKind.NEW_MAXIMUM,
|
||||
status.get_current_tick()))
|
||||
if plfees_percentage < min_perc:
|
||||
min_perc = plfees_percentage
|
||||
await status.add_event(Event(EventKind.NEW_MINIMUM,
|
||||
status.get_current_tick()))
|
||||
|
||||
min_perc_colored = colored_percentage(
|
||||
min_perc, "red") if min_perc < 0.0 else colored_percentage(min_perc, "green")
|
||||
max_perc_colored = colored_percentage(
|
||||
max_perc, "red") if max_perc < 0.0 else colored_percentage(max_perc, "green")
|
||||
|
||||
#
|
||||
# current status calculations
|
||||
#
|
||||
current_colored_format = "{} ({})".format(colored_percentage(plfees_percentage, status_color),
|
||||
colored_float(p.profit_loss, status_color))
|
||||
|
||||
#
|
||||
# Status bar
|
||||
#
|
||||
printer.print_next_line("{:1.5f} {} @ {} | {} | min: {}, MAX: {}".format(
|
||||
p.amount,
|
||||
p.symbol,
|
||||
colored_float(p.base_price, "white", attrs=["underline"]),
|
||||
current_colored_format,
|
||||
min_perc_colored,
|
||||
max_perc_colored))
|
||||
|
||||
# Separator
|
||||
printer.print_next_line("")
|
||||
|
||||
if orders:
|
||||
printer.print_next_line("Open {}:".format(
|
||||
colored("ORDERS", attrs=["underline"])))
|
||||
|
||||
print_last_events(status, 10, printer)
|
||||
plot(status, printer)
|
||||
|
||||
printer.reset_current_line()
|
||||
status.wait()
|
||||
|
||||
return
|
||||
|
||||
|
||||
def colored_percentage(perc, color, **kwargs):
|
||||
return "{}".format(colored("{:1.2f}%".format(perc), color=color, **kwargs))
|
||||
|
||||
|
||||
def colored_float(num, color, **kwargs):
|
||||
return "{}".format(colored("{:1.2f}".format(num), color=color, **kwargs))
|
||||
|
||||
|
||||
def print_last_events(status: Status, n: int, printer: Printer):
|
||||
printer.print_next_line(colored(f"Last {n} events:", attrs=["bold"]))
|
||||
|
||||
for e in status.last_events(n):
|
||||
printer.print_next_line(f"- {e}")
|
||||
|
||||
|
||||
def plot(status: Status, printer: Printer):
|
||||
if status.ticks:
|
||||
figure = termplotlib.figure()
|
||||
|
||||
x = range(1, status.get_current_tick() + 1)
|
||||
y = [x[0] for x in status.ticks.values()]
|
||||
|
||||
figure.plot(x, y, width=printer.screen.width,
|
||||
height=printer.screen.height - printer.get_current_line())
|
||||
|
||||
printer.print_next_line(figure.get_string())
|
||||
|
||||
|
||||
async def get_current_price(symbol):
|
||||
tickers = await bfx.get_public_ticker(symbol)
|
||||
return tickers[6]
|
||||
|
||||
|
||||
async def get_usd_balance():
|
||||
balance = 0.0
|
||||
|
||||
wallets = await bfx.get_wallets()
|
||||
|
||||
for w in wallets:
|
||||
if w.currency == "USD":
|
||||
balance += w.balance
|
||||
else:
|
||||
current_price = await get_current_price(f"t{w.currency}USD")
|
||||
balance += current_price * w.balance
|
||||
|
||||
return balance
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(Screen.wrapper(main))
|
1
bfxbot/__init__.py
Normal file
1
bfxbot/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .bfxbot import BfxBot
|
151
bfxbot/bfxbot.py
Normal file
151
bfxbot/bfxbot.py
Normal file
@ -0,0 +1,151 @@
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from bfxapi import Order
|
||||
|
||||
from bfxbot.bfxwrapper import BfxWrapper
|
||||
from bfxbot.currency import TradingPair, Symbol
|
||||
from bfxbot.models import SymbolStatus, Ticker, EventHandler, Strategy, Event, EventKind, OFFER_PERC, PositionWrapper
|
||||
|
||||
|
||||
class BfxBot:
|
||||
def __init__(self, api_key: str, api_secret: str, symbols: List[TradingPair], quote: Symbol,
|
||||
tick_duration: int = 1):
|
||||
if api_key is None:
|
||||
print("API_KEY is not set!")
|
||||
raise ValueError
|
||||
|
||||
if api_secret is None:
|
||||
print("API_SECRET is not set!")
|
||||
raise ValueError
|
||||
|
||||
self.__bfx: BfxWrapper = BfxWrapper(api_key, api_secret)
|
||||
self.__ticker: Ticker = Ticker(tick_duration)
|
||||
self.__status: Dict[TradingPair, SymbolStatus] = {}
|
||||
self.__quote: Symbol = quote
|
||||
self.__account_info = None
|
||||
self.__ledger = None
|
||||
|
||||
if isinstance(symbols, TradingPair):
|
||||
symbols = [symbols]
|
||||
|
||||
self.symbols: List[TradingPair] = symbols
|
||||
|
||||
# init symbol statuses
|
||||
for s in self.symbols:
|
||||
self.__status[s] = SymbolStatus(s)
|
||||
|
||||
def __position_wrapper_from_id(self, position_id) -> Tuple[Optional[PositionWrapper], Optional[SymbolStatus]]:
|
||||
for s in self.__status.values():
|
||||
pw = s.active_position_wrapper_from_id(position_id)
|
||||
|
||||
if pw:
|
||||
return pw, s
|
||||
return None, None
|
||||
|
||||
async def __update_status__(self):
|
||||
active_positions = await self.__bfx.get_active_position()
|
||||
|
||||
for symbol in self.__status:
|
||||
# updating tick
|
||||
self.__status[symbol].__init_tick__(self.__ticker.current_tick)
|
||||
|
||||
# updating last price
|
||||
last_price = await self.__bfx.get_current_prices(symbol)
|
||||
last_price = last_price[0]
|
||||
|
||||
self.__status[symbol].set_tick_price(self.__ticker.current_tick, last_price)
|
||||
|
||||
# updating positions
|
||||
symbol_positions = [x for x in active_positions if x.symbol == str(symbol)]
|
||||
for p in symbol_positions:
|
||||
await self.__status[TradingPair.from_str(p.symbol)].add_position(p)
|
||||
|
||||
# updating orders
|
||||
active_orders = await self.__bfx.get_active_orders(symbol)
|
||||
|
||||
for o in active_orders:
|
||||
self.__status[symbol].add_order(o)
|
||||
|
||||
# emitting new tick event
|
||||
# TODO: handle _on_new_tick() from Strategy
|
||||
await self.__status[symbol].add_event(Event(EventKind.NEW_TICK, self.__ticker.current_tick))
|
||||
|
||||
async def best_position_closing_price(self, position_id: int) -> Optional[float]:
|
||||
pw, _ = self.__position_wrapper_from_id(position_id)
|
||||
|
||||
if not pw:
|
||||
return None
|
||||
|
||||
is_long_pos = pw.position.amount < 0
|
||||
|
||||
pub_tick = await self.__bfx.get_public_ticker(pw.position.symbol)
|
||||
|
||||
bid_price = pub_tick[0]
|
||||
ask_price = pub_tick[2]
|
||||
|
||||
if is_long_pos:
|
||||
closing_price = bid_price * (1 - OFFER_PERC / 100)
|
||||
else:
|
||||
closing_price = ask_price * (1 + OFFER_PERC / 100)
|
||||
|
||||
return closing_price
|
||||
|
||||
def close_order(self, symbol: TradingPair, order_id: int):
|
||||
print(f"I would have closed order {order_id} for {symbol}")
|
||||
|
||||
async def close_position(self, position_id: int):
|
||||
pw, ss = self.__position_wrapper_from_id(position_id)
|
||||
|
||||
if not pw:
|
||||
print("Could not find open position!")
|
||||
return
|
||||
|
||||
closing_price = await self.best_position_closing_price(pw.position.id)
|
||||
|
||||
amount = pw.position.amount * -1
|
||||
|
||||
open_orders = await self.__bfx.get_active_orders(pw.position.symbol)
|
||||
|
||||
if not open_orders:
|
||||
await self.__bfx.submit_order(pw.position.symbol, closing_price, amount, Order.Type.LIMIT)
|
||||
await ss.add_event(Event(EventKind.ORDER_SUBMITTED, ss.current_tick))
|
||||
|
||||
async def get_balances(self):
|
||||
return await self.__bfx.get_current_balances(self.__quote)
|
||||
|
||||
async def get_profit_loss(self, start: int, end: int):
|
||||
return await self.__bfx.profit_loss(start, end, self.__ledger, self.__quote)
|
||||
|
||||
def set_strategy(self, symbol, strategy: Strategy):
|
||||
if symbol in self.__status:
|
||||
self.__status[symbol].strategy = strategy
|
||||
else:
|
||||
self.__status[symbol] = SymbolStatus(symbol, strategy)
|
||||
|
||||
async def start(self):
|
||||
self.__account_info = self.__bfx.get_account_information()
|
||||
self.__ledger = await self.__bfx.ledger_history(0, time.time() * 1000)
|
||||
|
||||
await self.__update_status__()
|
||||
|
||||
def symbol_event_handler(self, symbol) -> Optional[EventHandler]:
|
||||
if symbol not in self.__status:
|
||||
return None
|
||||
|
||||
return self.__status[symbol].eh
|
||||
|
||||
def symbol_status(self, symbol: TradingPair) -> Optional[SymbolStatus]:
|
||||
if symbol not in self.__status:
|
||||
return None
|
||||
|
||||
return self.__status[symbol]
|
||||
|
||||
async def update(self):
|
||||
await asyncio.sleep(self.__ticker.seconds)
|
||||
self.__ticker.inc()
|
||||
await self.__update_status__()
|
||||
|
||||
async def __update_ledger(self):
|
||||
self.__ledger = await self.__bfx.ledger_history(0, time.time() * 1000)
|
212
bfxbot/bfxwrapper.py
Normal file
212
bfxbot/bfxwrapper.py
Normal file
@ -0,0 +1,212 @@
|
||||
from bfxapi.rest.bfx_rest import BfxRest
|
||||
from retrying_async import retry
|
||||
|
||||
from bfxbot.currency import TradingPair, Balance, WalletKind, OrderType, Direction, Currency, BalanceGroup, Symbol
|
||||
from bfxbot.utils import average
|
||||
|
||||
|
||||
class BfxWrapper(BfxRest):
|
||||
# default timeframe (in milliseconds) when retrieving old prices
|
||||
DEFAULT_TIME_DELTA = 5 * 60 * 1000
|
||||
|
||||
def __init__(self, api_key: str, api_secret: str):
|
||||
super().__init__(API_KEY=api_key, API_SECRET=api_secret)
|
||||
|
||||
#######################################
|
||||
# OVERRIDDEN METHODS TO IMPLEMENT RETRY
|
||||
#######################################
|
||||
|
||||
@retry()
|
||||
async def get_public_ticker(self, symbol):
|
||||
if isinstance(symbol, TradingPair):
|
||||
symbol = str(symbol)
|
||||
|
||||
return await super().get_public_ticker(symbol)
|
||||
|
||||
@retry()
|
||||
async def get_active_position(self):
|
||||
return await super().get_active_position()
|
||||
|
||||
@retry()
|
||||
async def get_active_orders(self, symbol):
|
||||
if isinstance(symbol, TradingPair):
|
||||
symbol = str(symbol)
|
||||
|
||||
return await super().get_active_orders(symbol)
|
||||
|
||||
@retry()
|
||||
async def get_trades(self, symbol, start, end):
|
||||
if isinstance(symbol, TradingPair):
|
||||
symbol = str(symbol)
|
||||
|
||||
return await super().get_trades(symbol, start, end)
|
||||
|
||||
@retry()
|
||||
async def post(self, endpoint: str, data=None, params=""):
|
||||
if data is None:
|
||||
data = {}
|
||||
return await super().post(endpoint, data, params)
|
||||
|
||||
################################
|
||||
# NEW METHODS
|
||||
################################
|
||||
|
||||
async def account_movements_between(self, start: int, end: int, ledger, quote: Symbol) -> BalanceGroup:
|
||||
movements = BalanceGroup(quote)
|
||||
|
||||
# TODO: Parallelize this
|
||||
for entry in filter(lambda x: start <= x[3] <= end, ledger):
|
||||
description: str = entry[8]
|
||||
currency = entry[1]
|
||||
amount = entry[5]
|
||||
time = entry[3]
|
||||
|
||||
if not description.lower().startswith("deposit"):
|
||||
continue
|
||||
|
||||
trading_pair = f"t{currency}{quote}"
|
||||
start_time = time - self.DEFAULT_TIME_DELTA
|
||||
end_time = time + self.DEFAULT_TIME_DELTA
|
||||
|
||||
if currency != str(quote):
|
||||
trades = await self.get_public_trades(symbol=trading_pair, start=start_time, end=end_time)
|
||||
currency_price = average(list(map(lambda x: x[3], trades)))
|
||||
|
||||
c = Currency(currency, amount, currency_price)
|
||||
else:
|
||||
c = Currency(currency, amount)
|
||||
|
||||
b = Balance(c, quote)
|
||||
|
||||
movements.add_balance(b)
|
||||
|
||||
return movements
|
||||
|
||||
async def balance_at(self, time: int, ledger, quote: Symbol):
|
||||
bg = BalanceGroup(quote)
|
||||
|
||||
# TODO: Parallelize this
|
||||
for entry in filter(lambda x: x[3] <= time, ledger):
|
||||
currency = entry[1]
|
||||
amount = entry[6]
|
||||
|
||||
if currency in bg.currency_names():
|
||||
continue
|
||||
|
||||
trading_pair = f"t{currency}{quote}"
|
||||
start_time = time - self.DEFAULT_TIME_DELTA
|
||||
end_time = time + self.DEFAULT_TIME_DELTA
|
||||
|
||||
|
||||
if currency != str(quote):
|
||||
trades = await self.get_public_trades(symbol=trading_pair, start=start_time, end=end_time)
|
||||
currency_price = average(list(map(lambda x: x[3], trades)))
|
||||
|
||||
c = Currency(currency, amount, currency_price)
|
||||
else:
|
||||
c = Currency(currency, amount)
|
||||
|
||||
b = Balance(c, quote)
|
||||
|
||||
bg.add_balance(b)
|
||||
|
||||
return bg
|
||||
|
||||
# Calculate the average execution price for Trading or rate for Margin funding.
|
||||
async def calculate_execution_price(self, pair: str, amount: float):
|
||||
api_path = "/calc/trade/avg"
|
||||
|
||||
res = await self.post(api_path, {
|
||||
'symbol': pair,
|
||||
'amount': amount
|
||||
})
|
||||
|
||||
return res[0]
|
||||
|
||||
async def get_account_information(self):
|
||||
api_path = "auth/r/info/user"
|
||||
|
||||
return await self.post(api_path)
|
||||
|
||||
async def get_current_balances(self, quote: Symbol) -> BalanceGroup:
|
||||
bg: BalanceGroup = BalanceGroup(quote)
|
||||
|
||||
wallets = await self.get_wallets()
|
||||
|
||||
for w in wallets:
|
||||
kind = WalletKind.from_str(w.type)
|
||||
|
||||
if not kind:
|
||||
continue
|
||||
|
||||
execution_price = await self.calculate_execution_price(f"t{w.currency}{quote}", w.balance)
|
||||
c = Currency(w.currency, w.balance, execution_price)
|
||||
b = Balance(c, quote, kind)
|
||||
|
||||
bg.add_balance(b)
|
||||
|
||||
return bg
|
||||
|
||||
async def get_current_prices(self, symbol: TradingPair) -> (float, float, float):
|
||||
if isinstance(symbol, TradingPair):
|
||||
symbol = str(symbol)
|
||||
|
||||
tickers = await self.get_public_ticker(symbol)
|
||||
|
||||
bid_price = tickers[0]
|
||||
ask_price = tickers[2]
|
||||
ticker_price = tickers[6]
|
||||
|
||||
return bid_price, ask_price, ticker_price
|
||||
|
||||
async def ledger_history(self, start, end):
|
||||
def chunks(lst):
|
||||
for i in range(len(lst) - 1):
|
||||
yield lst[i:i + 2]
|
||||
|
||||
def get_timeframes(start, end, increments=10):
|
||||
start = int(start)
|
||||
end = int(end)
|
||||
|
||||
delta = int((end - start) / increments)
|
||||
|
||||
return [x for x in range(start, end, delta)]
|
||||
|
||||
api_path = "auth/r/ledgers/hist"
|
||||
|
||||
history = []
|
||||
|
||||
# TODO: Parallelize this
|
||||
for c in chunks(get_timeframes(start, end)):
|
||||
history.extend(await self.post(api_path, {'start': c[0], 'end': c[1], 'limit': 2500}))
|
||||
|
||||
history.sort(key=lambda ledger_entry: ledger_entry[3], reverse=True)
|
||||
|
||||
return history
|
||||
|
||||
async def maximum_order_amount(self, symbol: TradingPair, direction: Direction,
|
||||
order_type: OrderType = OrderType.EXCHANGE,
|
||||
rate: int = 1):
|
||||
api_path = "auth/calc/order/avail"
|
||||
|
||||
return await self.post(api_path,
|
||||
{'symbol': str(symbol), 'type': order_type.value, "dir": direction.value, "rate": rate})
|
||||
|
||||
async def profit_loss(self, start: int, end: int, ledger, quote: Symbol):
|
||||
if start > end:
|
||||
raise ValueError
|
||||
|
||||
start_bg = await self.balance_at(start, ledger, quote)
|
||||
end_bg = await self.balance_at(end, ledger, quote)
|
||||
movements_bg = await self.account_movements_between(start, end, ledger, quote)
|
||||
|
||||
start_quote = start_bg.quote_equivalent()
|
||||
end_quote = end_bg.quote_equivalent()
|
||||
movements_quote = movements_bg.quote_equivalent()
|
||||
|
||||
profit_loss = end_quote - (start_quote + movements_quote)
|
||||
|
||||
profit_loss_percentage = profit_loss / (start_quote + movements_quote) * 100
|
||||
|
||||
|
||||
return profit_loss, profit_loss_percentage
|
161
bfxbot/currency.py
Normal file
161
bfxbot/currency.py
Normal file
@ -0,0 +1,161 @@
|
||||
import re
|
||||
from enum import Enum
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
class Symbol(Enum):
|
||||
XMR = "XMR"
|
||||
BTC = "BTC"
|
||||
ETH = "ETH"
|
||||
USD = "USD"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.value == other.value
|
||||
|
||||
|
||||
class TradingPair(Enum):
|
||||
XMR = "XMR"
|
||||
BTC = "BTC"
|
||||
ETH = "ETH"
|
||||
|
||||
def __repr__(self):
|
||||
return f"t{self.value}USD"
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.value == other.value
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.__repr__())
|
||||
|
||||
@staticmethod
|
||||
def from_str(string: str):
|
||||
match = re.compile("t([a-zA-Z]+)USD").match(string)
|
||||
|
||||
if not match:
|
||||
raise ValueError
|
||||
|
||||
currency = match.group(1).lower()
|
||||
|
||||
if currency in ("xmr"):
|
||||
return TradingPair.XMR
|
||||
elif currency in ("btc"):
|
||||
return TradingPair.BTC
|
||||
elif currency in ("eth"):
|
||||
return TradingPair.ETH
|
||||
else:
|
||||
return NotImplementedError
|
||||
|
||||
|
||||
class Currency:
|
||||
def __init__(self, name: str, amount: float, price: float = None):
|
||||
self.__name: str = name
|
||||
self.__amount: float = amount
|
||||
self.__price: Optional[float] = price
|
||||
|
||||
def __str__(self):
|
||||
if self.__price:
|
||||
return f"{self.__name} {self.__amount} @ {self.__price}"
|
||||
else:
|
||||
return f"{self.__name} {self.__amount}"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def amount(self) -> float:
|
||||
return self.__amount
|
||||
|
||||
def name(self) -> str:
|
||||
return self.__name
|
||||
|
||||
def price(self) -> Optional[float]:
|
||||
return self.__price
|
||||
|
||||
|
||||
class WalletKind(Enum):
|
||||
EXCHANGE = "exchange",
|
||||
MARGIN = "margin"
|
||||
|
||||
@staticmethod
|
||||
def from_str(string: str):
|
||||
string = string.lower()
|
||||
|
||||
if "margin" in string:
|
||||
return WalletKind.MARGIN
|
||||
if "exchange" in string:
|
||||
return WalletKind.EXCHANGE
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class Balance:
|
||||
def __init__(self, currency: Currency, quote: Symbol, wallet: Optional[WalletKind] = None):
|
||||
self.__currency: Currency = currency
|
||||
self.__quote: Symbol = quote
|
||||
self.__quote_equivalent: float = 0.0
|
||||
self.__wallet: Optional[WalletKind] = wallet
|
||||
|
||||
if currency.name() == str(quote):
|
||||
self.__quote_equivalent = currency.amount()
|
||||
else:
|
||||
self.__quote_equivalent = currency.amount() * currency.price()
|
||||
|
||||
def currency(self) -> Currency:
|
||||
return self.__currency
|
||||
|
||||
def quote(self) -> Symbol:
|
||||
return self.__quote
|
||||
|
||||
def quote_equivalent(self) -> float:
|
||||
return self.__quote_equivalent
|
||||
|
||||
def wallet(self) -> Optional[WalletKind]:
|
||||
return self.__wallet
|
||||
|
||||
|
||||
class BalanceGroup:
|
||||
def __init__(self, quote: Symbol, balances: Optional[List[Balance]] = None):
|
||||
if balances is None:
|
||||
balances = []
|
||||
|
||||
self.__quote: Symbol = quote
|
||||
self.__balances: Optional[List[Balance]] = balances
|
||||
self.__quote_equivalent: float = 0.0
|
||||
|
||||
def __iter__(self):
|
||||
return self.__balances.__iter__()
|
||||
|
||||
def add_balance(self, balance: Balance):
|
||||
self.__balances.append(balance)
|
||||
|
||||
self.__quote_equivalent += balance.quote_equivalent()
|
||||
|
||||
def balances(self) -> Optional[List[Balance]]:
|
||||
return self.__balances
|
||||
|
||||
def currency_names(self) -> List[str]:
|
||||
return list(map(lambda x: x.currency().name(), self.balances()))
|
||||
|
||||
def quote(self) -> Symbol:
|
||||
return self.__quote
|
||||
|
||||
def quote_equivalent(self) -> float:
|
||||
return self.__quote_equivalent
|
||||
|
||||
|
||||
class Direction(Enum):
|
||||
UP = 1,
|
||||
DOWN = -1
|
||||
|
||||
|
||||
class OrderType(Enum):
|
||||
EXCHANGE = "EXCHANGE",
|
||||
MARGIN = "MARGIN"
|
295
bfxbot/models.py
Normal file
295
bfxbot/models.py
Normal file
@ -0,0 +1,295 @@
|
||||
import inspect
|
||||
import time
|
||||
from enum import Enum
|
||||
from typing import List, Dict, Tuple, Optional
|
||||
|
||||
from bfxapi import Order, Position
|
||||
|
||||
from bfxbot.currency import TradingPair
|
||||
|
||||
OFFER_PERC = 0.008
|
||||
TAKER_FEE = 0.2
|
||||
MAKER_FEE = 0.1
|
||||
|
||||
|
||||
def __add_to_dict_list__(dictionary: Dict[int, List], k, v) -> Dict[int, List]:
|
||||
if k not in dictionary:
|
||||
dictionary[k] = [v]
|
||||
else:
|
||||
dictionary[k].append(v)
|
||||
|
||||
return dictionary
|
||||
|
||||
|
||||
class EventKind(Enum):
|
||||
NEW_MINIMUM = 1,
|
||||
NEW_MAXIMUM = 2,
|
||||
REACHED_LOSS = 3,
|
||||
REACHED_BREAK_EVEN = 4,
|
||||
REACHED_MIN_PROFIT = 5,
|
||||
REACHED_GOOD_PROFIT = 6,
|
||||
REACHED_MAX_LOSS = 7,
|
||||
CLOSE_POSITION = 8,
|
||||
TRAILING_STOP_SET = 9,
|
||||
TRAILING_STOP_MOVED = 10,
|
||||
ORDER_SUBMITTED = 11,
|
||||
NEW_TICK = 12
|
||||
|
||||
|
||||
class EventMetadata:
|
||||
def __init__(self, position_id: int = None, order_id: int = None):
|
||||
self.position_id: int = position_id
|
||||
self.order_id: int = order_id
|
||||
|
||||
|
||||
class PositionState(Enum):
|
||||
CRITICAL = -1,
|
||||
LOSS = 0,
|
||||
BREAK_EVEN = 1,
|
||||
MINIMUM_PROFIT = 2,
|
||||
PROFIT = 3,
|
||||
UNDEFINED = 4
|
||||
|
||||
def color(self) -> str:
|
||||
if self == self.LOSS or self == self.CRITICAL:
|
||||
return "red"
|
||||
elif self == self.BREAK_EVEN:
|
||||
return "yellow"
|
||||
else:
|
||||
return "green"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name}"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
|
||||
class Ticker:
|
||||
def __init__(self, sec) -> None:
|
||||
self.seconds: int = sec
|
||||
self.start_time = time.time()
|
||||
self.current_tick: int = 1
|
||||
|
||||
def inc(self):
|
||||
self.current_tick += 1
|
||||
|
||||
|
||||
class Event:
|
||||
def __init__(self, kind: EventKind, tick: int, metadata: EventMetadata = None) -> None:
|
||||
self.kind: EventKind = kind
|
||||
self.tick: int = tick
|
||||
self.metadata: EventMetadata = metadata
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.kind.name} @ Tick {self.tick}"
|
||||
|
||||
def has_metadata(self) -> bool:
|
||||
return self.metadata is not None
|
||||
|
||||
|
||||
class PositionWrapper:
|
||||
def __init__(self, position: Position, state: PositionState = PositionState.UNDEFINED,
|
||||
net_profit_loss: float = None,
|
||||
net_profit_loss_percentage: float = None):
|
||||
self.position: Position = position
|
||||
self.__net_profit_loss: float = net_profit_loss
|
||||
self.__net_profit_loss_percentage: float = net_profit_loss_percentage
|
||||
self.__state: PositionState = state
|
||||
|
||||
def net_profit_loss(self) -> float:
|
||||
return self.__net_profit_loss
|
||||
|
||||
def net_profit_loss_percentage(self) -> float:
|
||||
return self.__net_profit_loss_percentage
|
||||
|
||||
def set_state(self, state: PositionState):
|
||||
self.__state = state
|
||||
|
||||
def state(self) -> PositionState:
|
||||
return self.__state
|
||||
|
||||
|
||||
class SymbolStatus:
|
||||
def __init__(self, symbol: TradingPair, strategy=None):
|
||||
self.symbol = symbol
|
||||
self.eh = EventHandler()
|
||||
self.prices: Dict[int, float] = {}
|
||||
self.events: List[Event] = []
|
||||
self.orders: Dict[int, List[Order]] = {}
|
||||
self.positions: Dict[int, List[PositionWrapper]] = {}
|
||||
self.current_tick: int = 1
|
||||
self.strategy: Strategy = strategy
|
||||
|
||||
def __init_tick__(self, tick: int):
|
||||
self.current_tick = tick
|
||||
self.prices[self.current_tick] = None
|
||||
self.orders[self.current_tick] = []
|
||||
self.positions[self.current_tick] = []
|
||||
|
||||
async def add_event(self, event: Event):
|
||||
self.events.append(event)
|
||||
await self.eh.call_event(self, event)
|
||||
|
||||
def add_order(self, order: Order):
|
||||
if self.strategy:
|
||||
self.strategy.order_on_new_tick(order, self)
|
||||
self.orders = __add_to_dict_list__(self.orders, self.current_tick, order)
|
||||
|
||||
# Applies strategy and adds position to list
|
||||
async def add_position(self, position: Position):
|
||||
events = []
|
||||
|
||||
# if a strategy is defined then the strategy takes care of creating a PW for us
|
||||
if not self.strategy:
|
||||
pw = PositionWrapper(position)
|
||||
else:
|
||||
pw, events = await self.__apply_strategy_to_position__(position)
|
||||
self.positions = __add_to_dict_list__(self.positions, self.current_tick, pw)
|
||||
|
||||
# triggering state callbacks
|
||||
await self.__trigger_position_state_callbacks__(pw)
|
||||
|
||||
# triggering events callbacks
|
||||
for e in events:
|
||||
if not isinstance(e, Event):
|
||||
raise ValueError
|
||||
await self.add_event(e)
|
||||
|
||||
def all_prices(self) -> List[float]:
|
||||
return list(map(lambda x: self.prices[x], range(1, self.current_tick + 1)))
|
||||
|
||||
def all_ticks(self) -> List[int]:
|
||||
return [x for x in range(1, self.current_tick + 1)]
|
||||
|
||||
def current_positions(self) -> List[PositionWrapper]:
|
||||
return self.positions[self.current_tick]
|
||||
|
||||
def current_price(self):
|
||||
return self.prices[self.current_tick]
|
||||
|
||||
def previous_pw(self, pid: int) -> Optional[PositionWrapper]:
|
||||
if self.current_tick == 1:
|
||||
return None
|
||||
|
||||
if not self.positions[self.current_tick - 1]:
|
||||
return None
|
||||
|
||||
return next(filter(lambda x: x.position.id == pid, self.positions[self.current_tick - 1]))
|
||||
|
||||
def active_position_wrapper_from_id(self, position_id: int) -> Optional[PositionWrapper]:
|
||||
if self.current_tick in self.positions:
|
||||
for pw in self.positions[self.current_tick]:
|
||||
if pw.position.id == position_id:
|
||||
return pw
|
||||
return None
|
||||
|
||||
def set_tick_price(self, tick, price):
|
||||
self.prices[tick] = price
|
||||
|
||||
async def __apply_strategy_to_position__(self, position: Position) -> Tuple[PositionWrapper, List[Event]]:
|
||||
pw, events = self.strategy.position_on_new_tick(position, self)
|
||||
|
||||
if not isinstance(pw, PositionWrapper):
|
||||
raise ValueError
|
||||
|
||||
if not isinstance(events, list):
|
||||
raise ValueError
|
||||
|
||||
return pw, events
|
||||
|
||||
async def __trigger_position_state_callbacks__(self, pw: PositionWrapper):
|
||||
await self.eh.call_position_state(self, pw)
|
||||
|
||||
|
||||
class Strategy:
|
||||
"""
|
||||
Defines new position state and events after tick.
|
||||
"""
|
||||
|
||||
def position_on_new_tick(self, position: Position, ss: SymbolStatus) -> Tuple[PositionWrapper, List[Event]]:
|
||||
pass
|
||||
|
||||
"""
|
||||
Defines new order state and events after tick.
|
||||
"""
|
||||
|
||||
def order_on_new_tick(self, order: Order, ss: SymbolStatus):
|
||||
pass
|
||||
|
||||
|
||||
class EventHandler:
|
||||
def __init__(self):
|
||||
self.event_handlers = {}
|
||||
self.state_handlers = {}
|
||||
self.any_events = []
|
||||
self.any_state = []
|
||||
|
||||
async def call_event(self, status: SymbolStatus, event: Event):
|
||||
value = event.kind.value
|
||||
|
||||
# print("CALLING EVENT: {}".format(event))
|
||||
if value in self.event_handlers:
|
||||
for h in self.event_handlers[value]:
|
||||
if inspect.iscoroutinefunction(h):
|
||||
await h(event, status)
|
||||
else:
|
||||
h(event, status)
|
||||
|
||||
for h in self.any_events:
|
||||
if inspect.iscoroutinefunction(h):
|
||||
await h(event, status)
|
||||
else:
|
||||
h(event, status)
|
||||
|
||||
async def call_position_state(self, status: SymbolStatus, pw: PositionWrapper):
|
||||
state = pw.state()
|
||||
|
||||
if state in self.state_handlers:
|
||||
for h in self.state_handlers[state]:
|
||||
if inspect.iscoroutinefunction(h):
|
||||
await h(pw, status)
|
||||
else:
|
||||
h(pw, status)
|
||||
|
||||
for h in self.any_state:
|
||||
if inspect.iscoroutinefunction(h):
|
||||
await h(pw, status)
|
||||
else:
|
||||
h(pw, status)
|
||||
|
||||
def on_event(self, kind: EventKind):
|
||||
value = kind.value
|
||||
|
||||
def registerhandler(handler):
|
||||
if value in self.event_handlers:
|
||||
self.event_handlers[value].append(handler)
|
||||
else:
|
||||
self.event_handlers[value] = [handler]
|
||||
return handler
|
||||
|
||||
return registerhandler
|
||||
|
||||
def on_position_state(self, state: PositionState):
|
||||
def registerhandler(handler):
|
||||
if state in self.state_handlers:
|
||||
self.state_handlers[state].append(handler)
|
||||
else:
|
||||
self.state_handlers[state] = [handler]
|
||||
return handler
|
||||
|
||||
return registerhandler
|
||||
|
||||
def on_any_event(self):
|
||||
def registerhandle(handler):
|
||||
self.any_events.append(handler)
|
||||
return handler
|
||||
|
||||
return registerhandle
|
||||
|
||||
def on_any_position_state(self):
|
||||
def registerhandle(handler):
|
||||
self.any_state.append(handler)
|
||||
return handler
|
||||
|
||||
return registerhandle
|
59
bfxbot/utils.py
Normal file
59
bfxbot/utils.py
Normal file
@ -0,0 +1,59 @@
|
||||
import re
|
||||
|
||||
from bfxbot.bfxwrapper import Balance
|
||||
from bfxbot.models import PositionWrapper
|
||||
|
||||
|
||||
class CurrencyPair:
|
||||
def __init__(self, base: str, quote: str):
|
||||
self.base: str = base
|
||||
self.quote: str = quote
|
||||
|
||||
@staticmethod
|
||||
def from_str(string: str):
|
||||
symbol_regex = re.compile("t(?P<base>[a-zA-Z]{3})(?P<quote>[a-zA-Z]{3})")
|
||||
|
||||
match = symbol_regex.match(string)
|
||||
|
||||
if not match:
|
||||
return None
|
||||
|
||||
return CurrencyPair(match.group("base"), match.group("quote"))
|
||||
|
||||
|
||||
def average(a):
|
||||
return sum(a) / len(a)
|
||||
|
||||
|
||||
def balance_to_json(balance: Balance):
|
||||
return {
|
||||
'currency': balance.currency().name(),
|
||||
'amount': balance.currency().amount(),
|
||||
'kind': balance.wallet().value,
|
||||
'quote': balance.quote().value,
|
||||
'quote_equivalent': balance.quote_equivalent()
|
||||
}
|
||||
|
||||
|
||||
def net_pl_percentage(perc: float, reference_fee_perc: float):
|
||||
return perc - reference_fee_perc
|
||||
|
||||
|
||||
def pw_to_posprop(pw: PositionWrapper):
|
||||
pair = CurrencyPair.from_str(pw.position.symbol)
|
||||
|
||||
if not pair:
|
||||
raise ValueError
|
||||
|
||||
return {
|
||||
"id": pw.position.id,
|
||||
"amount": pw.position.amount,
|
||||
"base_price": pw.position.base_price,
|
||||
"state": str(pw.state()),
|
||||
"pair": {
|
||||
"base": pair.base,
|
||||
"quote": pair.quote
|
||||
},
|
||||
"profit_loss": pw.net_profit_loss(),
|
||||
"profit_loss_percentage": pw.net_profit_loss_percentage()
|
||||
}
|
140
main.py
Executable file
140
main.py
Executable file
@ -0,0 +1,140 @@
|
||||
# #!/usr/bin/env python
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import threading
|
||||
from time import sleep
|
||||
from typing import List
|
||||
|
||||
import dotenv
|
||||
from flask import Flask, render_template
|
||||
from flask_socketio import SocketIO
|
||||
|
||||
from bfxbot import BfxBot
|
||||
from bfxbot.bfxwrapper import Balance
|
||||
from bfxbot.currency import TradingPair, Symbol
|
||||
from bfxbot.models import PositionWrapper, SymbolStatus, Event, EventKind
|
||||
from bfxbot.utils import pw_to_posprop, balance_to_json
|
||||
from strategy import TrailingStopStrategy
|
||||
|
||||
|
||||
async def bot_loop():
|
||||
await bot.start()
|
||||
|
||||
while True:
|
||||
await bot.update()
|
||||
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
API_KEY = os.getenv("API_KEY")
|
||||
API_SECRET = os.getenv("API_SECRET")
|
||||
|
||||
app = Flask(__name__)
|
||||
socketio = SocketIO(app, async_mode="threading")
|
||||
bot = BfxBot(api_key=API_KEY, api_secret=API_SECRET,
|
||||
symbols=[TradingPair.BTC], quote=Symbol.USD, tick_duration=20)
|
||||
strategy = TrailingStopStrategy()
|
||||
bot.set_strategy(TradingPair.BTC, strategy)
|
||||
btc_eh = bot.symbol_event_handler(TradingPair.BTC)
|
||||
|
||||
# initializing and starting bot on other thread
|
||||
threading.Thread(target=lambda: asyncio.run(bot_loop())).start()
|
||||
|
||||
|
||||
###################################
|
||||
# Flask callbacks
|
||||
###################################
|
||||
|
||||
@app.route('/')
|
||||
def entry():
|
||||
return render_template('index.html')
|
||||
|
||||
|
||||
###################################
|
||||
# Socker.IO callbacks
|
||||
###################################
|
||||
|
||||
@socketio.on("close_position")
|
||||
def on_close_position(message: dict):
|
||||
position_id = message['position_id']
|
||||
|
||||
loop.run_until_complete(bot.close_position(position_id))
|
||||
|
||||
|
||||
@socketio.on('connect')
|
||||
def on_connect():
|
||||
# sleeping on exception to avoid race condition
|
||||
ticks, prices, positions, balances = [], [], [], []
|
||||
|
||||
while not ticks or not prices:
|
||||
try:
|
||||
ticks = bot.symbol_status(TradingPair.BTC).all_ticks()
|
||||
prices = bot.symbol_status(TradingPair.BTC).all_prices()
|
||||
positions = bot.symbol_status(TradingPair.BTC).current_positions()
|
||||
balances = loop.run_until_complete(bot.get_balances())
|
||||
except KeyError:
|
||||
sleep(1)
|
||||
|
||||
socketio.emit("first_connect",
|
||||
{
|
||||
"ticks": ticks,
|
||||
"prices": prices,
|
||||
"positions": list(map(pw_to_posprop, positions)),
|
||||
"balances": list(map(balance_to_json, balances))
|
||||
})
|
||||
|
||||
|
||||
@socketio.on('get_profit_loss')
|
||||
def on_get_profit_loss(message):
|
||||
start = message['start']
|
||||
end = message['end']
|
||||
|
||||
profit_loss = loop.run_until_complete(bot.get_profit_loss(start, end))
|
||||
|
||||
socketio.emit("put_profit_loss", {
|
||||
"pl": profit_loss[0],
|
||||
"pl_perc": profit_loss[1]
|
||||
})
|
||||
|
||||
###################################
|
||||
# Bot callbacks
|
||||
###################################
|
||||
|
||||
@btc_eh.on_event(EventKind.CLOSE_POSITION)
|
||||
async def on_close_position(event: Event, _):
|
||||
print("CLOSING!")
|
||||
await bot.close_position(event.metadata.position_id)
|
||||
|
||||
|
||||
@btc_eh.on_any_position_state()
|
||||
async def on_any_state(pw: PositionWrapper, ss: SymbolStatus):
|
||||
await strategy.update_stop_percentage(pw, ss)
|
||||
|
||||
|
||||
@btc_eh.on_event(EventKind.NEW_TICK)
|
||||
async def on_new_tick(event: Event, status: SymbolStatus):
|
||||
tick = event.tick
|
||||
price = status.prices[event.tick]
|
||||
|
||||
balances: List[Balance] = await bot.get_balances()
|
||||
positions: List[PositionWrapper] = status.positions[event.tick] if event.tick in status.positions else []
|
||||
|
||||
socketio.emit("new_tick", {"tick": tick,
|
||||
"price": price,
|
||||
"positions": list(map(pw_to_posprop, positions)),
|
||||
"balances": list(map(balance_to_json, balances))})
|
||||
|
||||
|
||||
@btc_eh.on_any_event()
|
||||
def on_any_event(event: Event, _):
|
||||
socketio.emit("new_event", {
|
||||
"tick": event.tick,
|
||||
"kind": event.kind.name
|
||||
})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
socketio.run(app)
|
@ -1,5 +1,36 @@
|
||||
asciimatics==1.12.0
|
||||
aiohttp==3.7.3
|
||||
astroid==2.4.2
|
||||
async-timeout==3.0.1
|
||||
asyncio==3.4.3
|
||||
playsound==1.2.2
|
||||
termcolor==1.1.0
|
||||
termplotlib==0.3.2
|
||||
attrs==20.3.0
|
||||
bidict==0.21.2
|
||||
bitfinex-api-py==1.1.8
|
||||
chardet==3.0.4
|
||||
click==7.1.2
|
||||
eventemitter==0.2.0
|
||||
Flask==1.1.2
|
||||
Flask-SocketIO==5.0.1
|
||||
idna==2.10
|
||||
isort==5.6.4
|
||||
itsdangerous==1.1.0
|
||||
Jinja2==2.11.2
|
||||
lazy-object-proxy==1.4.3
|
||||
MarkupSafe==1.1.1
|
||||
mccabe==0.6.1
|
||||
mpmath==1.1.0
|
||||
multidict==5.1.0
|
||||
pyee==8.1.0
|
||||
pylint==2.6.0
|
||||
python-dotenv==0.15.0
|
||||
python-engineio==4.0.0
|
||||
python-socketio==5.0.3
|
||||
retrying-async==1.2.0
|
||||
six==1.15.0
|
||||
sympy==1.7.1
|
||||
toml==0.10.2
|
||||
typing-extensions==3.7.4.3
|
||||
websockets==8.1
|
||||
Werkzeug==1.0.1
|
||||
wrapt==1.12.1
|
||||
yarl==1.6.3
|
||||
|
||||
|
3
static/.gitignore
vendored
Normal file
3
static/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
*
|
||||
!.gitignore
|
||||
|
123
strategy.py
Normal file
123
strategy.py
Normal file
@ -0,0 +1,123 @@
|
||||
from typing import List, Dict
|
||||
|
||||
import sympy.abc
|
||||
from bfxapi import Position
|
||||
from sympy import Point, solve
|
||||
|
||||
from bfxbot.models import Strategy, PositionState, SymbolStatus, Event, EventKind, EventMetadata, PositionWrapper, \
|
||||
TAKER_FEE
|
||||
from bfxbot.utils import net_pl_percentage
|
||||
|
||||
|
||||
class SquaredTrailingStop:
|
||||
def __init__(self, p_min: Point, p_max: Point):
|
||||
a = sympy.abc.a
|
||||
b = sympy.abc.b
|
||||
c = sympy.abc.c
|
||||
|
||||
self.p_min = p_min
|
||||
self.p_max = p_max
|
||||
|
||||
e1 = 2 * a * (p_max.x + b)
|
||||
e2 = a * (p_min.x + b) ** 2 + c - p_min.y
|
||||
e3 = a * (p_max.x + b) ** 2 + c - p_max.y
|
||||
|
||||
s = solve([e1, e2, e3])[0]
|
||||
|
||||
self.a, self.b, self.c = s[a], s[b], s[c]
|
||||
|
||||
def y(self, x):
|
||||
def inter_y(x):
|
||||
return self.a * (x + self.b) ** 2 + self.c
|
||||
|
||||
if x < self.p_min.x:
|
||||
return self.p_min.y
|
||||
elif x > self.p_max.x:
|
||||
return self.p_max.y
|
||||
else:
|
||||
return inter_y(x)
|
||||
|
||||
def profit(self, x):
|
||||
if x < self.p_min.x:
|
||||
return 0
|
||||
return x - self.y(x)
|
||||
|
||||
|
||||
class TrailingStopStrategy(Strategy):
|
||||
BREAK_EVEN_PERC = TAKER_FEE
|
||||
MIN_PROFIT_PERC = BREAK_EVEN_PERC + 0.3
|
||||
GOOD_PROFIT_PERC = MIN_PROFIT_PERC * 2.5
|
||||
MAX_LOSS_PERC = -4.0
|
||||
|
||||
TRAILING_STOP = SquaredTrailingStop(Point(MIN_PROFIT_PERC, MIN_PROFIT_PERC / 3 * 2), Point(GOOD_PROFIT_PERC, 0.1))
|
||||
|
||||
def __init__(self):
|
||||
# position_id : stop percentage
|
||||
self.stop_percentage: Dict[int, float] = {}
|
||||
|
||||
def position_on_new_tick(self, current_position: Position, ss: SymbolStatus) -> (PositionState, List[Event]):
|
||||
events = []
|
||||
|
||||
pl_perc = net_pl_percentage(current_position.profit_loss_percentage, TAKER_FEE)
|
||||
prev = ss.previous_pw(current_position.id)
|
||||
event_metadata = EventMetadata(position_id=current_position.id)
|
||||
|
||||
if pl_perc > self.GOOD_PROFIT_PERC:
|
||||
state = PositionState.PROFIT
|
||||
elif self.MIN_PROFIT_PERC <= pl_perc < self.GOOD_PROFIT_PERC:
|
||||
state = PositionState.MINIMUM_PROFIT
|
||||
elif 0.0 <= pl_perc < self.MIN_PROFIT_PERC:
|
||||
state = PositionState.BREAK_EVEN
|
||||
elif self.MAX_LOSS_PERC < pl_perc < 0.0:
|
||||
state = PositionState.LOSS
|
||||
else:
|
||||
events.append(Event(EventKind.CLOSE_POSITION, ss.current_tick, event_metadata))
|
||||
state = PositionState.CRITICAL
|
||||
|
||||
pw = PositionWrapper(current_position, state=state, net_profit_loss=current_position.profit_loss,
|
||||
net_profit_loss_percentage=pl_perc)
|
||||
|
||||
if not prev or prev.state() == state:
|
||||
return pw, events
|
||||
|
||||
if state == PositionState.PROFIT:
|
||||
events.append(Event(EventKind.REACHED_GOOD_PROFIT, ss.current_tick, event_metadata))
|
||||
elif state == PositionState.MINIMUM_PROFIT:
|
||||
events.append(Event(EventKind.REACHED_MIN_PROFIT, ss.current_tick, event_metadata))
|
||||
elif state == PositionState.BREAK_EVEN:
|
||||
events.append(Event(EventKind.REACHED_BREAK_EVEN, ss.current_tick, event_metadata))
|
||||
elif state == PositionState.LOSS:
|
||||
events.append(Event(EventKind.REACHED_LOSS, ss.current_tick, event_metadata))
|
||||
else:
|
||||
events.append(Event(EventKind.REACHED_MAX_LOSS, ss.current_tick, event_metadata))
|
||||
events.append(Event(EventKind.CLOSE_POSITION, ss.current_tick, event_metadata))
|
||||
|
||||
return pw, events
|
||||
|
||||
async def update_stop_percentage(self, pw: PositionWrapper, ss: SymbolStatus):
|
||||
current_pl_perc = pw.net_profit_loss_percentage()
|
||||
pid = pw.position.id
|
||||
event_metadata = EventMetadata(position_id=pw.position.id)
|
||||
|
||||
# if trailing stop not set for this position and state is not profit (we should not set it)
|
||||
if pid not in self.stop_percentage and pw.state() not in [PositionState.MINIMUM_PROFIT,
|
||||
PositionState.PROFIT]:
|
||||
return
|
||||
|
||||
# set stop percentage for first time only if in profit
|
||||
if pid not in self.stop_percentage:
|
||||
await ss.add_event(Event(EventKind.TRAILING_STOP_SET, ss.current_tick, event_metadata))
|
||||
|
||||
self.stop_percentage[pid] = current_pl_perc - self.TRAILING_STOP.y(current_pl_perc)
|
||||
return
|
||||
|
||||
# moving trailing stop
|
||||
if current_pl_perc - self.TRAILING_STOP.y(current_pl_perc) > self.stop_percentage[pid]:
|
||||
await ss.add_event(Event(EventKind.TRAILING_STOP_MOVED, ss.current_tick, event_metadata))
|
||||
self.stop_percentage[pid] = current_pl_perc - self.TRAILING_STOP.y(current_pl_perc)
|
||||
|
||||
# close position if current P/L below stop percentage
|
||||
if current_pl_perc < self.stop_percentage[pid]:
|
||||
await ss.add_event(Event(EventKind.CLOSE_POSITION, ss.current_tick, event_metadata))
|
||||
|
||||
return
|
16
templates/index.html
Normal file
16
templates/index.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='index.css') }}">
|
||||
</head>
|
||||
|
||||
<title>Rustico</title>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="{{ url_for('static', filename='index.js') }}"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
1
websrc/.gitignore
vendored
Normal file
1
websrc/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.js
|
131
websrc/components/App.tsx
Normal file
131
websrc/components/App.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import React, {Component} from "react";
|
||||
import {EventProp} from "./Events";
|
||||
import {
|
||||
Balance,
|
||||
CurrencyPair,
|
||||
EventName,
|
||||
FirstConnectMessage,
|
||||
NewEventMessage,
|
||||
NewTickMessage,
|
||||
PositionProp
|
||||
} from "../types";
|
||||
import {socket} from "../index";
|
||||
import {symbolToPair} from "../utils";
|
||||
import {Helmet} from "react-helmet";
|
||||
import {Navbar, Sidebar} from "./Navbars";
|
||||
import {Statusbar} from "./Statusbar";
|
||||
import {PositionsTable} from "./Tables";
|
||||
import RPlot from "./RPlot";
|
||||
|
||||
type AppState = {
|
||||
current_price: number,
|
||||
current_tick: number,
|
||||
last_update: Date,
|
||||
positions: Array<PositionProp>,
|
||||
events: Array<EventProp>,
|
||||
active_pair: CurrencyPair,
|
||||
available_pairs: Array<CurrencyPair>,
|
||||
balances: Array<Balance>
|
||||
}
|
||||
|
||||
class App extends Component<{}, AppState> {
|
||||
event_id = 0;
|
||||
|
||||
state = {
|
||||
current_price: 0,
|
||||
current_tick: 0,
|
||||
last_update: new Date(),
|
||||
positions: [],
|
||||
events: [],
|
||||
balances: [],
|
||||
active_pair: symbolToPair("tBTCUSD"),
|
||||
available_pairs: []
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
socket.on(EventName.FirstConnect, (data: FirstConnectMessage) => {
|
||||
this.setState({
|
||||
current_price: data.prices[data.prices.length - 1],
|
||||
current_tick: data.ticks[data.ticks.length - 1],
|
||||
last_update: new Date(),
|
||||
positions: data.positions,
|
||||
balances: data.balances
|
||||
})
|
||||
})
|
||||
|
||||
socket.on(EventName.NewTick, (data: NewTickMessage) => {
|
||||
this.setState({
|
||||
current_price: data.price,
|
||||
current_tick: data.tick,
|
||||
last_update: new Date(),
|
||||
positions: data.positions,
|
||||
balances: data.balances
|
||||
})
|
||||
})
|
||||
|
||||
socket.on(EventName.NewEvent, (data: NewEventMessage) => {
|
||||
// ignore new tick
|
||||
if (!data.kind.toLowerCase().includes("new_tick")) {
|
||||
const new_event: EventProp = {
|
||||
id: this.event_id,
|
||||
name: data.kind,
|
||||
tick: data.tick
|
||||
}
|
||||
|
||||
this.event_id += 1
|
||||
|
||||
this.setState((state) => ({
|
||||
events: [...state.events, new_event]
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title> Rustico
|
||||
- {String(this.state.current_price.toLocaleString())} {String(this.state.active_pair.base) + "/" + String(this.state.active_pair.quote)} </title>
|
||||
</Helmet>
|
||||
<div className="bg-gray-800">
|
||||
<div className="h-screen max-w-screen flex mx-auto">
|
||||
<Navbar/>
|
||||
|
||||
<main
|
||||
className="my-1 py-2 px-10 flex-1 bg-gray-200 dark:bg-black rounded-l-lg*
|
||||
transition duration-500 ease-in-out overflow-y-auto flex flex-col">
|
||||
<div className="flex justify-center text-2xl my-2">
|
||||
<Statusbar balances={this.state.balances} positions={this.state.positions}
|
||||
price={this.state.current_price}
|
||||
tick={this.state.current_tick}/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col flex-grow my-8 shadow-md hover:shadow-lg">
|
||||
<div
|
||||
className="py-2 flex-grow bg-white min-width dark:bg-gray-600 rounded-lg overflow-hidden">
|
||||
<RPlot/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.state.positions.length > 0 ?
|
||||
<PositionsTable positions={this.state.positions}/> : null}
|
||||
|
||||
<footer className="flex rounded-lg justify-center bg-gray-600 mt-4 border-t text-gray-300">
|
||||
<span className="my-1 mx-1">Made with ❤️ by the Peperone in a scantinato</span>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<Sidebar/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
155
websrc/components/Cards.tsx
Normal file
155
websrc/components/Cards.tsx
Normal file
@ -0,0 +1,155 @@
|
||||
import React, {Component} from 'react';
|
||||
import {Balance, EventName, FirstConnectMessage, NewTickMessage} from "../types";
|
||||
import {socket} from "../index";
|
||||
|
||||
export type CoinBalanceProps = {
|
||||
name: string,
|
||||
amount: number,
|
||||
percentage?: number,
|
||||
quote_equivalent: number,
|
||||
quote_symbol: string,
|
||||
}
|
||||
|
||||
class CoinBalance extends Component<CoinBalanceProps> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
// do not print equivalent if this element is the quote itself
|
||||
const quoteBlock = this.props.name != this.props.quote_symbol ? this.props.quote_symbol.concat(" ").concat(this.props.quote_equivalent.toLocaleString()) : null
|
||||
|
||||
// const accessory = SymbolAccessories.filter((accessory) => {
|
||||
// return accessory.name == this.props.name
|
||||
// })
|
||||
//
|
||||
// const icon = accessory.length > 0 ? accessory.pop().icon : null
|
||||
|
||||
return (
|
||||
<div className="flex-grow flex px-6 py-3 text-gray-800 items-center border-b -mx-4 align-middle">
|
||||
<div className={"w-1/8"}>
|
||||
{/*{icon}*/}
|
||||
</div>
|
||||
<div className="w-2/5 xl:w-1/4 px-4 flex items-center">
|
||||
<span className="text-lg">{this.props.name}</span>
|
||||
</div>
|
||||
<div className="hidden md:flex lg:hidden xl:flex w-1/4 px-1 flex-col">
|
||||
<div className={"italic w-full text-center"}>
|
||||
{Math.trunc(this.props.percentage)}%
|
||||
</div>
|
||||
<div className="w-full bg-transparent mt-2">
|
||||
<div className={"bg-blue-400 rounded-lg text-xs leading-none py-1 text-center text-white"}
|
||||
style={{width: this.props.percentage.toString().concat("%")}}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-3/5 md:w/12">
|
||||
<div className="w-1/2 px-4">
|
||||
<div className="text-right">
|
||||
{this.props.amount.toFixed(5)} {this.props.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-1/2 px-4 my-auto">
|
||||
<div
|
||||
className={"px-2 inline-flex text-center text-xs leading-5 font-semibold rounded-full bg-gray-200 text-gray-800"}>
|
||||
{quoteBlock}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type WalletCardProps = {
|
||||
quote: string,
|
||||
}
|
||||
|
||||
export class WalletCard extends Component
|
||||
<{}, { balances: Array<Balance> }> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
state =
|
||||
{
|
||||
balances: []
|
||||
}
|
||||
|
||||
totalQuoteBalance() {
|
||||
let total = 0
|
||||
|
||||
this.state.balances.forEach((balance: Balance) => {
|
||||
if (balance.currency == balance.quote) {
|
||||
total += balance.amount
|
||||
} else {
|
||||
total += balance.quote_equivalent
|
||||
}
|
||||
})
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
renderCoinBalances() {
|
||||
return (
|
||||
this.state.balances.map((balance: Balance) => {
|
||||
const percentage_amount = balance.quote == balance.currency ? balance.amount : balance.quote_equivalent;
|
||||
|
||||
return (
|
||||
<CoinBalance key={balance.currency.concat(balance.kind)} name={balance.currency}
|
||||
amount={balance.amount} quote_equivalent={balance.quote_equivalent}
|
||||
percentage={percentage_amount / this.totalQuoteBalance() * 100}
|
||||
quote_symbol={balance.quote}/>
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
socket.on(EventName.NewTick, (data: NewTickMessage) => {
|
||||
this.setState({
|
||||
balances: data.balances
|
||||
})
|
||||
})
|
||||
|
||||
socket.on(EventName.FirstConnect, (data: FirstConnectMessage) => {
|
||||
this.setState({
|
||||
balances: data.balances
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="w-full mb-6 lg:mb-0 px-4 flex flex-col">
|
||||
<div
|
||||
className="flex-grow flex bg-white flex-col border-t border-b sm:rounded-lg sm:border shadow overflow-hidden">
|
||||
<div className="border-b bg-gray-50">
|
||||
<div className="flex justify-between px-6 -mb-px">
|
||||
<h3 className="text-blue-700 py-4 font-normal text-lg">Your Wallets</h3>
|
||||
<div className="flex">
|
||||
<button type="button"
|
||||
className="appearance-none py-4 text-blue-700 border-b border-blue-dark mr-3">
|
||||
Margin
|
||||
</button>
|
||||
<button type="button"
|
||||
className="appearance-none py-4 text-gray-600 border-b border-transparent hover:border-grey-dark">
|
||||
Chart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.renderCoinBalances()}
|
||||
|
||||
<div className="px-6 py-4">
|
||||
<div className="text-center text-gray-400">
|
||||
Total Balance ≈ USD {this.totalQuoteBalance().toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
45
websrc/components/Currency.tsx
Normal file
45
websrc/components/Currency.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import {Button, ButtonGroup, Dropdown} from "react-bootstrap";
|
||||
import React, {Component} from "react";
|
||||
import DropdownItem from "react-bootstrap/DropdownItem";
|
||||
import {CurrencyPair} from "../types";
|
||||
|
||||
|
||||
export type CurrencyPairProps = {
|
||||
active_pair: CurrencyPair,
|
||||
pairs: Array<CurrencyPair>
|
||||
}
|
||||
|
||||
export class CurrencyDropdown extends Component<CurrencyPairProps> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
dropdownItems() {
|
||||
return this.props.pairs.map((pair) => {
|
||||
return (
|
||||
<DropdownItem key={String(pair.base) + String(pair.quote)}> {pair.base} / {pair.quote} </DropdownItem>)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Dropdown as={ButtonGroup} className={"mr-3"}>
|
||||
<Button variant="outline-primary"><b>{this.props.active_pair.base} / {this.props.active_pair.quote}</b></Button>
|
||||
|
||||
{this.props.pairs.length > 0 &&
|
||||
|
||||
<>
|
||||
<Dropdown.Toggle split variant="primary" id="dropdown-split-basic"/>
|
||||
|
||||
<Dropdown.Menu className={"mr-3"}>
|
||||
{this.dropdownItems()}
|
||||
</Dropdown.Menu>
|
||||
</>
|
||||
}
|
||||
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
42
websrc/components/Events.tsx
Normal file
42
websrc/components/Events.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React, {Component} from "react";
|
||||
import {Container, ListGroup} from "react-bootstrap";
|
||||
|
||||
|
||||
export type EventProp = {
|
||||
id: number,
|
||||
name: string,
|
||||
tick: number
|
||||
}
|
||||
|
||||
export class Events extends Component<{ events: Array<EventProp> }> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
state = {
|
||||
events: this.props.events
|
||||
}
|
||||
|
||||
mapEvents() {
|
||||
return this.state.events.map((event: EventProp) => {
|
||||
return (
|
||||
<ListGroup.Item action key={event.id}>
|
||||
{event.name} @ Tick {event.tick}
|
||||
</ListGroup.Item>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Container>
|
||||
<div className={"border-bottom mb-2"}>
|
||||
<h2>Events</h2>
|
||||
</div>
|
||||
<ListGroup>
|
||||
{this.mapEvents()}
|
||||
</ListGroup>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
50
websrc/components/Icons.tsx
Normal file
50
websrc/components/Icons.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import React, {Component} from "react";
|
||||
|
||||
type IconProps = {
|
||||
width: number,
|
||||
height: number,
|
||||
}
|
||||
|
||||
class Icon extends Component <IconProps> {
|
||||
private readonly width: string;
|
||||
private readonly height: string;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.width = "w-" + this.props.width.toString();
|
||||
this.height = "h-" + this.props.width.toString();
|
||||
}
|
||||
|
||||
dimensionsClassName() {
|
||||
return (this.height + " " + this.width)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class DollarIcon extends Icon {
|
||||
render() {
|
||||
return (
|
||||
<svg className={this.dimensionsClassName()} xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class ClockIcon extends Icon {
|
||||
render() {
|
||||
return (
|
||||
<svg className={this.dimensionsClassName()} xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
71
websrc/components/Navbars.tsx
Normal file
71
websrc/components/Navbars.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import React, {Component} from "react";
|
||||
import {WalletCard} from "./Cards";
|
||||
|
||||
export class Navbar extends Component<any, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<nav
|
||||
className="ml-1 w-24 flex flex-col items-center bg-gray-700 dark:bg-gray-700 py-4 my-1 rounded-tl rounded-bl">
|
||||
|
||||
<ul className="flex-1 mt-2 text-gray-700 dark:text-gray-400 capitalize">
|
||||
{/* Links */}
|
||||
<li className="mt-3 p-2 text-gray-400 dark:text-blue-300 rounded-lg">
|
||||
<a href="#" className=" flex flex-col items-center">
|
||||
<svg className="fill-current h-5 w-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M19 5v2h-4V5h4M9 5v6H5V5h4m10 8v6h-4v-6h4M9
|
||||
17v2H5v-2h4M21 3h-8v6h8V3M11 3H3v10h8V3m10
|
||||
8h-8v10h8V11m-10 4H3v6h8v-6z"/>
|
||||
</svg>
|
||||
<span className="text-xs mt-2 text-gray-300">Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
<li className="mt-3 p-2 text-gray-400 dark:text-blue-300 rounded-lg">
|
||||
<a href="#" className=" flex flex-col items-center">
|
||||
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/>
|
||||
</svg>
|
||||
<span className="text-xs mt-2 text-gray-300">Reports</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul className="text-gray-700 dark:text-gray-400 capitalize">
|
||||
<li className="mt-auto p-2 text-gray-400 dark:text-blue-300 rounded-lg">
|
||||
<a href="#" className=" flex flex-col items-center">
|
||||
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
<span className="text-xs mt-2 text-gray-300">Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class Sidebar extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<aside
|
||||
className="w-1/4 my-1 mr-1 pr-2 py-4 flex flex-col bg-gray-200 dark:bg-black
|
||||
dark:text-gray-400 rounded-r-lg overflow-y-auto">
|
||||
<WalletCard/>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
}
|
90
websrc/components/Overlays.tsx
Normal file
90
websrc/components/Overlays.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import React, {Component} from "react";
|
||||
import {socket} from "../index";
|
||||
import {EventName} from "../types";
|
||||
|
||||
export type ModalProps = {
|
||||
show: boolean,
|
||||
positionId: number,
|
||||
toggleConfirmation: any
|
||||
}
|
||||
|
||||
export class ClosePositionModal extends Component<ModalProps, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.show) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed z-10 inset-0 overflow-y-auto">
|
||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div className="fixed inset-0 transition-opacity" aria-hidden="true">
|
||||
<div className="absolute inset-0 bg-gray-500 opacity-75"/>
|
||||
</div>
|
||||
|
||||
{/*This element is to trick the browser into centering the modal contents. -->*/}
|
||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen"
|
||||
aria-hidden="true">​</span>
|
||||
|
||||
{/*Modal panel, show/hide based on modal state.*/}
|
||||
|
||||
{/*Entering: "ease-out duration-300"*/}
|
||||
{/* From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"*/}
|
||||
{/* To: "opacity-100 translate-y-0 sm:scale-100"*/}
|
||||
{/*Leaving: "ease-in duration-200"*/}
|
||||
{/* From: "opacity-100 translate-y-0 sm:scale-100"*/}
|
||||
{/* To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"*/}
|
||||
|
||||
<div
|
||||
className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
|
||||
role="dialog" aria-modal="true" aria-labelledby="modal-headline">
|
||||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div
|
||||
className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
{/*Heroicon name: exclamation -->*/}
|
||||
<svg className="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
|
||||
Close position
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
Are you sure you want to close the position? This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button type="button"
|
||||
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
onClick={event => {
|
||||
socket.emit(EventName.ClosePosition, {
|
||||
position_id: this.props.positionId
|
||||
})
|
||||
|
||||
this.props.toggleConfirmation()
|
||||
}}>
|
||||
Close
|
||||
</button>
|
||||
<button type="button"
|
||||
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
onClick={() => this.props.toggleConfirmation()}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
168
websrc/components/RPlot.tsx
Normal file
168
websrc/components/RPlot.tsx
Normal file
@ -0,0 +1,168 @@
|
||||
import React, {Component} from "react"
|
||||
import Plot from "react-plotly.js"
|
||||
|
||||
import {socket} from '../';
|
||||
import {EventName, NewTickMessage} from "../types";
|
||||
|
||||
|
||||
type FirstConnectData = {
|
||||
ticks: Array<number>,
|
||||
prices: Array<number>
|
||||
}
|
||||
|
||||
|
||||
type PriceLine = {
|
||||
x0: number,
|
||||
y0: number,
|
||||
x1: number,
|
||||
y1: number
|
||||
}
|
||||
|
||||
type PlotState = {
|
||||
x: Array<number>,
|
||||
y: Array<number>,
|
||||
current_price_line: PriceLine,
|
||||
positions_price_lines: Array<PriceLine>,
|
||||
}
|
||||
|
||||
class RPlot extends Component<{}, PlotState> {
|
||||
state = {
|
||||
x: [],
|
||||
y: [],
|
||||
current_price_line: {x0: 0, x1: 0, y0: 0, y1: 0},
|
||||
positions_price_lines: []
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
socket.on(EventName.FirstConnect, (data: FirstConnectData) => {
|
||||
const last_tick = data.ticks[data.ticks.length - 1];
|
||||
const last_price = data.prices[data.prices.length - 1];
|
||||
|
||||
this.setState({
|
||||
x: data.ticks,
|
||||
y: data.prices,
|
||||
current_price_line: {
|
||||
x0: 0,
|
||||
y0: last_price,
|
||||
x1: last_tick,
|
||||
y1: last_price
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
socket.on(EventName.NewTick, (data: NewTickMessage) => {
|
||||
const position_price_lines = data.positions.map((pstat): PriceLine => {
|
||||
return {
|
||||
x0: 0,
|
||||
y0: pstat.base_price,
|
||||
x1: data.tick,
|
||||
y1: pstat.base_price
|
||||
}
|
||||
})
|
||||
|
||||
this.setState((state) => ({
|
||||
x: state.x.concat(data.tick),
|
||||
y: state.y.concat(data.price),
|
||||
current_price_line: {
|
||||
x0: 0,
|
||||
y0: data.price,
|
||||
x1: data.tick,
|
||||
y1: data.price
|
||||
},
|
||||
positions_price_lines: position_price_lines
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
let additional_shapes = []
|
||||
|
||||
if (this.state.positions_price_lines.length > 0) {
|
||||
additional_shapes = this.state.positions_price_lines.map((priceline: PriceLine) => {
|
||||
return {
|
||||
type: 'line',
|
||||
x0: priceline.x0,
|
||||
y0: priceline.y0,
|
||||
x1: priceline.x1,
|
||||
y1: priceline.y1,
|
||||
line: {
|
||||
color: 'rgb(1, 1, 1)',
|
||||
width: 1,
|
||||
dash: 'solid'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Plot
|
||||
data={[
|
||||
{
|
||||
x: this.state.x,
|
||||
y: this.state.y,
|
||||
type: 'scatter',
|
||||
mode: 'lines+markers',
|
||||
},
|
||||
]}
|
||||
layout={{
|
||||
margin: {
|
||||
l: 110,
|
||||
r: 20,
|
||||
b: 100,
|
||||
t: 20,
|
||||
pad: 4
|
||||
},
|
||||
dragmode: "pan",
|
||||
shapes: [
|
||||
{
|
||||
type: 'line',
|
||||
x0: this.state.current_price_line.x0,
|
||||
y0: this.state.current_price_line.y0,
|
||||
x1: this.state.current_price_line.x1,
|
||||
y1: this.state.current_price_line.y1,
|
||||
line: {
|
||||
color: 'rgb(50, 171, 96)',
|
||||
width: 2,
|
||||
dash: 'dashdot'
|
||||
}
|
||||
},
|
||||
].concat(additional_shapes),
|
||||
xaxis: {
|
||||
title: {
|
||||
text: 'Tick',
|
||||
font: {
|
||||
family: 'Courier New, monospace',
|
||||
size: 18,
|
||||
color: '#7f7f7f'
|
||||
}
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
title: {
|
||||
text: 'Price',
|
||||
font: {
|
||||
family: 'Courier New, monospace',
|
||||
size: 18,
|
||||
color: '#7f7f7f'
|
||||
}
|
||||
},
|
||||
tickformat: 'r',
|
||||
}
|
||||
}}
|
||||
config={{
|
||||
scrollZoom: true,
|
||||
displayModeBar: false,
|
||||
responsive: true,
|
||||
}}
|
||||
style={{width: '100%', height: '100%'}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default RPlot;
|
48
websrc/components/RToast.tsx
Normal file
48
websrc/components/RToast.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import React, {Component} from 'react';
|
||||
import {Toast} from 'react-bootstrap';
|
||||
import {socket} from "../";
|
||||
|
||||
type ToastProps = {
|
||||
title: string,
|
||||
content?: string,
|
||||
bg?: string
|
||||
}
|
||||
|
||||
type ToastState = {
|
||||
lastUpdated: Date,
|
||||
show: boolean
|
||||
}
|
||||
|
||||
|
||||
export class RToast extends Component<ToastProps, ToastState> {
|
||||
state = {
|
||||
lastUpdated: new Date(),
|
||||
show: false
|
||||
}
|
||||
|
||||
constructor(props: ToastProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
socket.on("connect", () => {
|
||||
this.setState({show: true})
|
||||
})
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.setState({lastUpdated: new Date()})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Toast show={this.state.show} delay={5000} autohide>
|
||||
<Toast.Header>
|
||||
<strong className="mr-auto">{this.props.title}</strong>
|
||||
<small>{this.state.lastUpdated.toLocaleTimeString('en-GB')}</small>
|
||||
</Toast.Header>
|
||||
{this.props.content ? <Toast.Body> {this.props.content}</Toast.Body> : null}
|
||||
</Toast>
|
||||
)
|
||||
}
|
||||
}
|
293
websrc/components/Statusbar.tsx
Normal file
293
websrc/components/Statusbar.tsx
Normal file
@ -0,0 +1,293 @@
|
||||
import React, {Component} from "react";
|
||||
import {EventName, GetProfitLossMessage, NewTickMessage, PutProfitLossMessage} from "../types";
|
||||
import {socket} from "../index";
|
||||
import {DateTime} from "luxon";
|
||||
|
||||
type QuoteStatusProps = {
|
||||
percentage?: boolean,
|
||||
quote_symbol?: string,
|
||||
amount: number,
|
||||
subtitle: string,
|
||||
sign?: boolean
|
||||
}
|
||||
|
||||
export class QuoteStatus extends Component<QuoteStatusProps> {
|
||||
private whole: number;
|
||||
private decimal: number;
|
||||
private sign: string;
|
||||
private signClass: string;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.deriveProps()
|
||||
}
|
||||
|
||||
deriveProps() {
|
||||
this.whole = Math.abs(Math.trunc(this.props.amount))
|
||||
this.decimal = Math.trunc(this.props.amount % 1 * 100)
|
||||
this.sign = this.props.amount > 0 ? "+" : "-"
|
||||
|
||||
this.signClass = this.props.amount > 0 ? "text-green-500" : "text-red-500"
|
||||
}
|
||||
|
||||
renderSign() {
|
||||
if (this.props.sign) {
|
||||
return (
|
||||
<span
|
||||
className={this.signClass}>{this.sign}</span>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
symbolOrPercentageRender() {
|
||||
if (this.props.percentage) {
|
||||
return (
|
||||
<>
|
||||
<span className="text-4xl text-bold align-top">
|
||||
{this.renderSign()}</span>
|
||||
<span className="text-5xl">{Math.abs(this.props.amount).toFixed(2)}</span>
|
||||
<span className="text-3xl align-top">%</span>
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<span className="text-4xl text-bold align-top">{this.renderSign()}{this.props.quote_symbol}</span>
|
||||
<span className="text-5xl">{this.whole.toLocaleString()}</span>
|
||||
<span className="text-3xl align-top">.{Math.abs(this.decimal)}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<div className="text-gray-700 mb-2">
|
||||
{this.symbolOrPercentageRender()}
|
||||
</div>
|
||||
<div className="text-sm uppercase text-gray-300 tracking-wide">
|
||||
{this.props.subtitle}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type DateButtonProps = {
|
||||
label: string,
|
||||
onClick: any,
|
||||
selected_default?: boolean
|
||||
}
|
||||
|
||||
type DateButtonState = {
|
||||
selected: boolean,
|
||||
}
|
||||
|
||||
class DateButton extends Component<DateButtonProps, DateButtonState> {
|
||||
private classSelected: string = "appearance-none py-4 text-blue-600 border-b border-blue-600 mr-3";
|
||||
private classNotSelected: string = "appearance-none py-4 text-gray-600 border-b border-transparent hover:border-gray-800 mr-3";
|
||||
private currentClass: string;
|
||||
|
||||
state = {
|
||||
selected: this.props.selected_default
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.updateClass()
|
||||
}
|
||||
|
||||
onClick() {
|
||||
this.setState({selected: !this.state.selected}, this.updateClass)
|
||||
|
||||
this.props.onClick()
|
||||
}
|
||||
|
||||
updateClass() {
|
||||
this.currentClass = this.state.selected ? this.classSelected : this.classNotSelected
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<button key={this.props.label} type="button"
|
||||
className={this.currentClass} onClick={this.onClick.bind(this)}>
|
||||
{this.props.label}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const PeriodUnit = {
|
||||
SECOND: "second",
|
||||
MINUTE: "minute",
|
||||
HOUR: "hour",
|
||||
DAY: "day",
|
||||
WEEK: "week",
|
||||
MONTH: "month",
|
||||
YEAR: "year"
|
||||
}
|
||||
|
||||
type StatusBarState = {
|
||||
pl_period_unit: string,
|
||||
pl_period_amount: number,
|
||||
pl: number,
|
||||
pl_perc: number
|
||||
}
|
||||
|
||||
export class Statusbar extends Component<NewTickMessage, StatusBarState> {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
pl_period_unit: PeriodUnit.WEEK,
|
||||
pl_period_amount: 1,
|
||||
pl: 0.0,
|
||||
pl_perc: 0.0
|
||||
}
|
||||
|
||||
this.emitGetProfitLoss = this.emitGetProfitLoss.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
socket.on(EventName.PutProfitLoss, (data: PutProfitLossMessage) => {
|
||||
this.setState({
|
||||
pl: data.pl,
|
||||
pl_perc: data.pl_perc
|
||||
})
|
||||
})
|
||||
|
||||
socket.on(EventName.FirstConnect, this.emitGetProfitLoss)
|
||||
socket.on(EventName.NewTick, this.emitGetProfitLoss)
|
||||
}
|
||||
|
||||
|
||||
durationObjectfromStr(str: string) {
|
||||
switch (str) {
|
||||
case PeriodUnit.MINUTE:
|
||||
return {
|
||||
minutes: this.state.pl_period_amount
|
||||
}
|
||||
case PeriodUnit.HOUR:
|
||||
return {
|
||||
hours: this.state.pl_period_amount
|
||||
}
|
||||
case PeriodUnit.DAY:
|
||||
return {
|
||||
days: this.state.pl_period_amount
|
||||
}
|
||||
case PeriodUnit.WEEK:
|
||||
return {
|
||||
weeks: this.state.pl_period_amount
|
||||
}
|
||||
case PeriodUnit.MONTH:
|
||||
return {
|
||||
months: this.state.pl_period_amount
|
||||
}
|
||||
case PeriodUnit.YEAR:
|
||||
return {
|
||||
years: this.state.pl_period_amount
|
||||
}
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
emitGetProfitLoss() {
|
||||
const message: GetProfitLossMessage = {
|
||||
start: DateTime.local().minus(this.durationObjectfromStr(this.state.pl_period_unit)).toMillis(),
|
||||
end: DateTime.local().toMillis()
|
||||
}
|
||||
|
||||
socket.emit(EventName.GetProfitLoss, message)
|
||||
}
|
||||
|
||||
changeProfitLossPeriod(amount: number, unit: string) {
|
||||
this.setState({
|
||||
pl_period_amount: amount,
|
||||
pl_period_unit: unit
|
||||
}, this.emitGetProfitLoss)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="bg-white border-t border-b sm:border-l sm:border-r sm:rounded-lg shadow flex-grow mb-6">
|
||||
<div className="bg-gray-50 rounded-tl-lg rounded-tr-lg border-b px-6">
|
||||
<div className="flex justify-between -mb-px">
|
||||
<div className="lg:hidden text-blue-600 py-4 text-lg">
|
||||
Price Charts
|
||||
</div>
|
||||
<div className="hidden lg:flex">
|
||||
<button type="button"
|
||||
className="appearance-none py-4 text-blue-600 border-b border-blue-dark mr-6">
|
||||
Bitcoin
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex text-sm">
|
||||
<DateButton key={PeriodUnit.MINUTE} label={"1m"}
|
||||
onClick={() => this.changeProfitLossPeriod(1, PeriodUnit.MINUTE)}/>
|
||||
<DateButton key={PeriodUnit.DAY} label={"1D"}
|
||||
onClick={() => this.changeProfitLossPeriod(1, PeriodUnit.DAY)}/>
|
||||
<DateButton key={PeriodUnit.WEEK} label={"1W"} selected_default={true}
|
||||
onClick={() => this.changeProfitLossPeriod(1, PeriodUnit.WEEK)}/>
|
||||
<DateButton key={PeriodUnit.MONTH} label={"1M"}
|
||||
onClick={() => this.changeProfitLossPeriod(1, PeriodUnit.MONTH)}/>
|
||||
<DateButton key={PeriodUnit.YEAR} label={"1Y"}
|
||||
onClick={() => this.changeProfitLossPeriod(1, PeriodUnit.YEAR)}/>
|
||||
{/*<DateButton label={"ALL"} onClick={() => this.changeProfitLossPeriod(1, PeriodUnit.MINUTE)}/>*/}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center px-6 lg:hidden">
|
||||
<div className="flex-grow flex-no-shrink py-6">
|
||||
<div className="text-gray-700 mb-2">
|
||||
<span className="text-3xl align-top">CA$</span>
|
||||
<span className="text-5xl">21,404</span>
|
||||
<span className="text-3xl align-top">.74</span>
|
||||
</div>
|
||||
<div className="text-green-300 text-sm">
|
||||
↑ CA$12,955.35 (154.16%)
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink w-32 inline-block relative">
|
||||
<select
|
||||
className="block appearance-none w-full bg-white border border-grey-light px-4 py-2 pr-8 rounded">
|
||||
<option>BTC</option>
|
||||
</select>
|
||||
<div className="pointer-events-none absolute pin-y pin-r flex items-center px-2 text-gray-300">
|
||||
<svg className="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20">
|
||||
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden lg:flex">
|
||||
<div className="w-1/3 text-center py-8">
|
||||
<div className="border-r">
|
||||
<QuoteStatus key={this.props.price} quote_symbol={"USD"} amount={this.props.price}
|
||||
subtitle={"Bitcoin price"}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-1/3 text-center py-8">
|
||||
<div className="border-r">
|
||||
<QuoteStatus key={this.state.pl} quote_symbol={"USD"} sign={true} amount={this.state.pl}
|
||||
subtitle={"since last ".concat(this.state.pl_period_unit)}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-1/3 text-center py-8">
|
||||
<div>
|
||||
<QuoteStatus key={this.state.pl_perc} percentage={true} sign={true}
|
||||
amount={this.state.pl_perc}
|
||||
subtitle={"since last ".concat(this.state.pl_period_unit)}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
141
websrc/components/Tables.tsx
Normal file
141
websrc/components/Tables.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
import React, {Component} from "react"
|
||||
import {PositionProp} from "../types";
|
||||
import {ClosePositionModal} from "./Overlays";
|
||||
|
||||
type PositionsTableState = {
|
||||
showConfirmation: boolean,
|
||||
positionToClose: number
|
||||
}
|
||||
|
||||
export class PositionsTable extends Component<{ positions: Array<PositionProp> }, PositionsTableState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.toggleConfirmation = this.toggleConfirmation.bind(this)
|
||||
}
|
||||
|
||||
state = {
|
||||
showConfirmation: false,
|
||||
positionToClose: 0,
|
||||
}
|
||||
|
||||
toggleConfirmation() {
|
||||
this.setState((state) => ({
|
||||
showConfirmation: !state.showConfirmation
|
||||
}))
|
||||
}
|
||||
|
||||
stateColor(state: string): string {
|
||||
const lower_state = state.toLowerCase()
|
||||
let res: string
|
||||
|
||||
if (lower_state.includes("profit")) {
|
||||
res = "green"
|
||||
} else if (lower_state.includes("break")) {
|
||||
res = "yellow"
|
||||
} else {
|
||||
res = "red"
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
renderTableHead() {
|
||||
return ["status", "currency pair", "base price", "amount", "Profit/Loss", "actions"].map((entry) => {
|
||||
return (
|
||||
<th scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
{entry}
|
||||
</th>
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
renderTableRows() {
|
||||
return this.props.positions.map((position) => {
|
||||
// TODO: move symbolToPair out of here?
|
||||
const stateBg = "bg-".concat(this.stateColor(position.state)).concat("-100 ")
|
||||
const stateText = "text-".concat(this.stateColor(position.state)).concat("-800 ")
|
||||
const stateClass = "px-2 inline-flex text-xs leading-5 font-semibold rounded-full ".concat(stateBg).concat(stateText)
|
||||
|
||||
return (
|
||||
<tr key={position.id}>
|
||||
{/* Status */}
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
className={stateClass}>
|
||||
{position.state}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td className="px-6 py-1 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-900">{position.pair.base}/{position.pair.quote}</div>
|
||||
{/*<div className="text-sm text-gray-500">{position.}</div>*/}
|
||||
</td>
|
||||
|
||||
<td className="px-6 py-1 whitespace-nowrap">
|
||||
<div
|
||||
className="text-sm text-gray-900">{position.base_price.toLocaleString()} {position.pair.quote}/{position.pair.base}</div>
|
||||
{/*<div className="text-sm text-gray-500">Insert total % here?</div>*/}
|
||||
</td>
|
||||
|
||||
<td className="px-6 py-1 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-900">{position.amount.toFixed(5)} {position.pair.base}</div>
|
||||
<div className="text-sm text-gray-500">Insert total % here?</div>
|
||||
</td>
|
||||
|
||||
<td className="px-6 py-1 whitespace-nowrap">
|
||||
<div
|
||||
className="text-sm text-gray-900 font-semibold">{position.profit_loss.toLocaleString()} {position.pair.quote}</div>
|
||||
<div className={"text-sm ".concat(stateClass)}>{position.profit_loss_percentage.toFixed(2)}%
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-6 py-1 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div className="p-2 md:w-40">
|
||||
<div
|
||||
className="p-4 flex justify-center bg-red-200 rounded-lg shadow-xs cursor-pointer hover:bg-red-500 hover:text-red-100"
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
showConfirmation: true,
|
||||
positionToClose: position.id
|
||||
})
|
||||
}}>
|
||||
<span className="ml-2">Close</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<ClosePositionModal positionId={this.state.positionToClose} toggleConfirmation={this.toggleConfirmation}
|
||||
show={this.state.showConfirmation}/>
|
||||
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
{this.renderTableHead()}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{this.renderTableRows()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
}
|
1
websrc/css/.gitignore
vendored
Normal file
1
websrc/css/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
tailwind.css
|
3
websrc/css/index.css
Normal file
3
websrc/css/index.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
13
websrc/index.tsx
Normal file
13
websrc/index.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import "./css/tailwind.css";
|
||||
import App from "./components/App";
|
||||
import io from "socket.io-client";
|
||||
|
||||
export const socket = io();
|
||||
|
||||
socket.on("connect", function () {
|
||||
console.log("Connected!")
|
||||
})
|
||||
|
||||
ReactDOM.render(<App/>, document.getElementById("root"));
|
40
websrc/package.json
Normal file
40
websrc/package.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "rustico",
|
||||
"version": "1.0.0",
|
||||
"main": "index.tsx",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/luxon": "^1.25.0",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/react-helmet": "^6.1.0",
|
||||
"@types/react-plotly.js": "^2.2.4",
|
||||
"@types/socket.io-client": "^1.4.34",
|
||||
"parcel-bundler": "^1.12.4",
|
||||
"react-plotly.js": "^2.5.1",
|
||||
"typescript": "^4.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/classnames": "^2.2.11",
|
||||
"autoprefixer": "^10.1.0",
|
||||
"classnames": "^2.2.6",
|
||||
"css": "^3.0.0",
|
||||
"luxon": "^1.25.0",
|
||||
"plotly.js": "^1.58.2",
|
||||
"postcss": "^8.2.1",
|
||||
"postcss-cli": "^8.3.1",
|
||||
"react": "^17.0.1",
|
||||
"react-cryptocoins": "^1.0.11",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
"socket.io-client": "~3",
|
||||
"tailwindcss": "^2.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build:tailwind": "tailwindcss build css/index.css -o css/tailwind.css",
|
||||
"watch": "parcel watch index.tsx -d ../static",
|
||||
"prestart": "yarn run build:tailwind",
|
||||
"prebuild": "yarn run build:tailwind",
|
||||
"start": "python ../main.py & yarn run watch"
|
||||
}
|
||||
}
|
12
websrc/tsconfig.json
Normal file
12
websrc/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["esnext", "DOM"],
|
||||
"jsx": "react",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
],
|
||||
}
|
77
websrc/types.ts
Normal file
77
websrc/types.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import {EventProp} from "./components/Events";
|
||||
|
||||
/*******************************
|
||||
* Types
|
||||
*******************************/
|
||||
|
||||
export type Balance = {
|
||||
currency: string,
|
||||
amount: number,
|
||||
// exchange / margin
|
||||
kind: string,
|
||||
quote: string,
|
||||
quote_equivalent: number,
|
||||
}
|
||||
|
||||
export type CurrencyPair = {
|
||||
base: string,
|
||||
quote: string
|
||||
}
|
||||
|
||||
export type FirstConnectMessage = {
|
||||
ticks: Array<number>,
|
||||
prices: Array<number>,
|
||||
positions: Array<PositionProp>,
|
||||
balances: Array<Balance>
|
||||
}
|
||||
|
||||
export type GetProfitLossMessage = {
|
||||
start: number,
|
||||
end: number
|
||||
}
|
||||
|
||||
export type NewEventMessage = {
|
||||
tick: number,
|
||||
kind: string,
|
||||
}
|
||||
|
||||
export type NewTickMessage = {
|
||||
tick: number,
|
||||
price: number,
|
||||
positions: Array<PositionProp>,
|
||||
balances: Array<Balance>
|
||||
}
|
||||
|
||||
export type PositionCloseMessage = {
|
||||
message_name: string,
|
||||
position_id: number,
|
||||
}
|
||||
|
||||
export type PositionProp = {
|
||||
id: number,
|
||||
state: string,
|
||||
base_price: number,
|
||||
amount: number,
|
||||
pair: CurrencyPair,
|
||||
profit_loss: number,
|
||||
profit_loss_percentage: number
|
||||
}
|
||||
|
||||
export type PutProfitLossMessage = {
|
||||
pl: number,
|
||||
pl_perc: number
|
||||
}
|
||||
|
||||
/*******************************
|
||||
* ENUMS
|
||||
*******************************/
|
||||
|
||||
export enum EventName {
|
||||
NewTick = "new_tick",
|
||||
FirstConnect = "first_connect",
|
||||
NewEvent = "new_event",
|
||||
ClosePosition = "close_position",
|
||||
GetProfitLoss = "get_profit_loss",
|
||||
PutProfitLoss = "put_profit_loss"
|
||||
}
|
||||
|
41
websrc/utils.ts
Normal file
41
websrc/utils.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import {CurrencyPair} from "./types";
|
||||
// import * as Icon from 'react-cryptocoins'
|
||||
import {Btc, Eth, Xmr} from 'react-cryptocoins';
|
||||
|
||||
export function symbolToPair(symbol: string): CurrencyPair {
|
||||
const symbol_regex = "t(?<base>[a-zA-Z]{3})(?<quote>[a-zA-Z]{3})"
|
||||
|
||||
const match = symbol.match(symbol_regex)
|
||||
|
||||
return {
|
||||
base: match.groups.base,
|
||||
quote: match.groups.quote
|
||||
}
|
||||
}
|
||||
|
||||
export type SymbolAccesory = {
|
||||
name: string,
|
||||
icon: React.Component,
|
||||
color: string
|
||||
}
|
||||
|
||||
export const SymbolAccessories: Array<SymbolAccesory> = [
|
||||
{
|
||||
name: "BTC",
|
||||
icon: Btc,
|
||||
color: "yellow"
|
||||
},
|
||||
{
|
||||
name: "XMR",
|
||||
icon: Xmr,
|
||||
color: "yellow"
|
||||
},
|
||||
{
|
||||
name: "ETH",
|
||||
icon: Eth,
|
||||
color: "yellow"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
|
8826
websrc/yarn.lock
Normal file
8826
websrc/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user