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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user