merge tailwind
This commit is contained in:
commit
1294274951
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
|
# Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode
|
||||||
# Edit at https://www.toptal.com/developers/gitignore?templates=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>
|
514
bfxbot.py
514
bfxbot.py
@ -1,514 +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 os
|
|
||||||
|
|
||||||
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 = {}
|
|
||||||
self.any_events = []
|
|
||||||
self.any_state = []
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
for h in self.any_events:
|
|
||||||
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)
|
|
||||||
|
|
||||||
for h in self.any_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
|
|
||||||
|
|
||||||
def on_any_event(self):
|
|
||||||
def registerhandle(handler):
|
|
||||||
self.any_events.append(handler)
|
|
||||||
return handler
|
|
||||||
return registerhandle
|
|
||||||
|
|
||||||
def on_any_state(self):
|
|
||||||
def registerhandle(handler):
|
|
||||||
self.any_state.append(handler)
|
|
||||||
return handler
|
|
||||||
return registerhandle
|
|
||||||
|
|
||||||
dotenv.load_dotenv()
|
|
||||||
|
|
||||||
API_KEY = os.getenv('API_KEY', default='')
|
|
||||||
API_SECRET = os.getenv('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.7
|
|
||||||
GOOD_PROFIT_PERC = MIN_PROFIT_PERC * 2.1
|
|
||||||
MAX_LOSS_PERC = -3.75
|
|
||||||
OFFER_PERC = 0.01
|
|
||||||
|
|
||||||
TRAIL_STOP_PERCENTAGES = {
|
|
||||||
State.MINIMUM_PROFIT: 0.25,
|
|
||||||
State.PROFIT: 0.125
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@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_any_state()
|
|
||||||
async def call_close_on_percentage(status):
|
|
||||||
if status.stop_percentage:
|
|
||||||
current_pl_perc = net_pl_percentage(
|
|
||||||
status.last_position().profit_loss_percentage, TAKER_FEE)
|
|
||||||
|
|
||||||
if current_pl_perc < status.stop_percentage:
|
|
||||||
await status.add_event(Event(EventKind.CLOSE_POSITION,
|
|
||||||
status.get_current_tick()))
|
|
||||||
|
|
||||||
@eh.on_state(State.MINIMUM_PROFIT)
|
|
||||||
async def on_state_min_profit(status: Status):
|
|
||||||
await update_stop_percentage(State.MINIMUM_PROFIT, status)
|
|
||||||
|
|
||||||
|
|
||||||
@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)
|
|
||||||
async def on_state_min_profit(status: Status):
|
|
||||||
await update_stop_percentage(State.PROFIT, status)
|
|
||||||
|
|
||||||
|
|
||||||
async 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:
|
|
||||||
await 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:
|
|
||||||
await 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
|
|
||||||
|
|
||||||
open_orders = await bfx.get_active_orders(status.symbol)
|
|
||||||
|
|
||||||
if not open_orders:
|
|
||||||
await bfx.submit_order(status.symbol, closing_price, amount, Order.Type.LIMIT)
|
|
||||||
await status.add_event(Event(EventKind.ORDER_SUBMITTED, status.get_current_tick()))
|
|
||||||
playsound("sounds/goal.wav")
|
|
||||||
|
|
||||||
@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"])))
|
|
||||||
|
|
||||||
if status.stop_percentage:
|
|
||||||
printer.print_next_line("Trailing stop percentage: {}".format(colored_percentage(status.stop_percentage, "yellow")))
|
|
||||||
|
|
||||||
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
|
asyncio==3.4.3
|
||||||
playsound==1.2.2
|
attrs==20.3.0
|
||||||
termcolor==1.1.0
|
bidict==0.21.2
|
||||||
termplotlib==0.3.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