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 | ||||
| # 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 | ||||
| playsound==1.2.2 | ||||
| termcolor==1.1.0 | ||||
| termplotlib==0.3.2 | ||||
| attrs==20.3.0 | ||||
| bidict==0.21.2 | ||||
| bitfinex-api-py==1.1.8 | ||||
| chardet==3.0.4 | ||||
| click==7.1.2 | ||||
| eventemitter==0.2.0 | ||||
| Flask==1.1.2 | ||||
| Flask-SocketIO==5.0.1 | ||||
| idna==2.10 | ||||
| isort==5.6.4 | ||||
| itsdangerous==1.1.0 | ||||
| Jinja2==2.11.2 | ||||
| lazy-object-proxy==1.4.3 | ||||
| MarkupSafe==1.1.1 | ||||
| mccabe==0.6.1 | ||||
| mpmath==1.1.0 | ||||
| multidict==5.1.0 | ||||
| pyee==8.1.0 | ||||
| pylint==2.6.0 | ||||
| python-dotenv==0.15.0 | ||||
| python-engineio==4.0.0 | ||||
| python-socketio==5.0.3 | ||||
| retrying-async==1.2.0 | ||||
| six==1.15.0 | ||||
| sympy==1.7.1 | ||||
| toml==0.10.2 | ||||
| typing-extensions==3.7.4.3 | ||||
| websockets==8.1 | ||||
| Werkzeug==1.0.1 | ||||
| wrapt==1.12.1 | ||||
| yarl==1.6.3 | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										3
									
								
								static/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								static/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| * | ||||
| !.gitignore | ||||
| 
 | ||||
							
								
								
									
										123
									
								
								strategy.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								strategy.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,123 @@ | ||||
| from typing import List, Dict | ||||
| 
 | ||||
| import sympy.abc | ||||
| from bfxapi import Position | ||||
| from sympy import Point, solve | ||||
| 
 | ||||
| from bfxbot.models import Strategy, PositionState, SymbolStatus, Event, EventKind, EventMetadata, PositionWrapper, \ | ||||
|     TAKER_FEE | ||||
| from bfxbot.utils import net_pl_percentage | ||||
| 
 | ||||
| 
 | ||||
| class SquaredTrailingStop: | ||||
|     def __init__(self, p_min: Point, p_max: Point): | ||||
|         a = sympy.abc.a | ||||
|         b = sympy.abc.b | ||||
|         c = sympy.abc.c | ||||
| 
 | ||||
|         self.p_min = p_min | ||||
|         self.p_max = p_max | ||||
| 
 | ||||
|         e1 = 2 * a * (p_max.x + b) | ||||
|         e2 = a * (p_min.x + b) ** 2 + c - p_min.y | ||||
|         e3 = a * (p_max.x + b) ** 2 + c - p_max.y | ||||
| 
 | ||||
|         s = solve([e1, e2, e3])[0] | ||||
| 
 | ||||
|         self.a, self.b, self.c = s[a], s[b], s[c] | ||||
| 
 | ||||
|     def y(self, x): | ||||
|         def inter_y(x): | ||||
|             return self.a * (x + self.b) ** 2 + self.c | ||||
| 
 | ||||
|         if x < self.p_min.x: | ||||
|             return self.p_min.y | ||||
|         elif x > self.p_max.x: | ||||
|             return self.p_max.y | ||||
|         else: | ||||
|             return inter_y(x) | ||||
| 
 | ||||
|     def profit(self, x): | ||||
|         if x < self.p_min.x: | ||||
|             return 0 | ||||
|         return x - self.y(x) | ||||
| 
 | ||||
| 
 | ||||
| class TrailingStopStrategy(Strategy): | ||||
|     BREAK_EVEN_PERC = TAKER_FEE | ||||
|     MIN_PROFIT_PERC = BREAK_EVEN_PERC + 0.3 | ||||
|     GOOD_PROFIT_PERC = MIN_PROFIT_PERC * 2.5 | ||||
|     MAX_LOSS_PERC = -4.0 | ||||
| 
 | ||||
|     TRAILING_STOP = SquaredTrailingStop(Point(MIN_PROFIT_PERC, MIN_PROFIT_PERC / 3 * 2), Point(GOOD_PROFIT_PERC, 0.1)) | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         # position_id : stop percentage | ||||
|         self.stop_percentage: Dict[int, float] = {} | ||||
| 
 | ||||
|     def position_on_new_tick(self, current_position: Position, ss: SymbolStatus) -> (PositionState, List[Event]): | ||||
|         events = [] | ||||
| 
 | ||||
|         pl_perc = net_pl_percentage(current_position.profit_loss_percentage, TAKER_FEE) | ||||
|         prev = ss.previous_pw(current_position.id) | ||||
|         event_metadata = EventMetadata(position_id=current_position.id) | ||||
| 
 | ||||
|         if pl_perc > self.GOOD_PROFIT_PERC: | ||||
|             state = PositionState.PROFIT | ||||
|         elif self.MIN_PROFIT_PERC <= pl_perc < self.GOOD_PROFIT_PERC: | ||||
|             state = PositionState.MINIMUM_PROFIT | ||||
|         elif 0.0 <= pl_perc < self.MIN_PROFIT_PERC: | ||||
|             state = PositionState.BREAK_EVEN | ||||
|         elif self.MAX_LOSS_PERC < pl_perc < 0.0: | ||||
|             state = PositionState.LOSS | ||||
|         else: | ||||
|             events.append(Event(EventKind.CLOSE_POSITION, ss.current_tick, event_metadata)) | ||||
|             state = PositionState.CRITICAL | ||||
| 
 | ||||
|         pw = PositionWrapper(current_position, state=state, net_profit_loss=current_position.profit_loss, | ||||
|                              net_profit_loss_percentage=pl_perc) | ||||
| 
 | ||||
|         if not prev or prev.state() == state: | ||||
|             return pw, events | ||||
| 
 | ||||
|         if state == PositionState.PROFIT: | ||||
|             events.append(Event(EventKind.REACHED_GOOD_PROFIT, ss.current_tick, event_metadata)) | ||||
|         elif state == PositionState.MINIMUM_PROFIT: | ||||
|             events.append(Event(EventKind.REACHED_MIN_PROFIT, ss.current_tick, event_metadata)) | ||||
|         elif state == PositionState.BREAK_EVEN: | ||||
|             events.append(Event(EventKind.REACHED_BREAK_EVEN, ss.current_tick, event_metadata)) | ||||
|         elif state == PositionState.LOSS: | ||||
|             events.append(Event(EventKind.REACHED_LOSS, ss.current_tick, event_metadata)) | ||||
|         else: | ||||
|             events.append(Event(EventKind.REACHED_MAX_LOSS, ss.current_tick, event_metadata)) | ||||
|             events.append(Event(EventKind.CLOSE_POSITION, ss.current_tick, event_metadata)) | ||||
| 
 | ||||
|         return pw, events | ||||
| 
 | ||||
|     async def update_stop_percentage(self, pw: PositionWrapper, ss: SymbolStatus): | ||||
|         current_pl_perc = pw.net_profit_loss_percentage() | ||||
|         pid = pw.position.id | ||||
|         event_metadata = EventMetadata(position_id=pw.position.id) | ||||
| 
 | ||||
|         # if trailing stop not set for this position and state is not profit (we should not set it) | ||||
|         if pid not in self.stop_percentage and pw.state() not in [PositionState.MINIMUM_PROFIT, | ||||
|                                                                   PositionState.PROFIT]: | ||||
|             return | ||||
| 
 | ||||
|         # set stop percentage for first time only if in profit | ||||
|         if pid not in self.stop_percentage: | ||||
|             await ss.add_event(Event(EventKind.TRAILING_STOP_SET, ss.current_tick, event_metadata)) | ||||
| 
 | ||||
|             self.stop_percentage[pid] = current_pl_perc - self.TRAILING_STOP.y(current_pl_perc) | ||||
|             return | ||||
| 
 | ||||
|         # moving trailing stop | ||||
|         if current_pl_perc - self.TRAILING_STOP.y(current_pl_perc) > self.stop_percentage[pid]: | ||||
|             await ss.add_event(Event(EventKind.TRAILING_STOP_MOVED, ss.current_tick, event_metadata)) | ||||
|             self.stop_percentage[pid] = current_pl_perc - self.TRAILING_STOP.y(current_pl_perc) | ||||
| 
 | ||||
|         # close position if current P/L below stop percentage | ||||
|         if current_pl_perc < self.stop_percentage[pid]: | ||||
|             await ss.add_event(Event(EventKind.CLOSE_POSITION, ss.current_tick, event_metadata)) | ||||
| 
 | ||||
|         return | ||||
							
								
								
									
										16
									
								
								templates/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								templates/index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| <!doctype html> | ||||
| <html> | ||||
| 
 | ||||
| <head> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
|     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='index.css') }}"> | ||||
| </head> | ||||
| 
 | ||||
| <title>Rustico</title> | ||||
| 
 | ||||
| <body> | ||||
| <div id="root"></div> | ||||
| <script src="{{ url_for('static', filename='index.js') }}"></script> | ||||
| </body> | ||||
| 
 | ||||
| </html> | ||||
							
								
								
									
										1
									
								
								websrc/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								websrc/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| *.js | ||||
							
								
								
									
										131
									
								
								websrc/components/App.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								websrc/components/App.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,131 @@ | ||||
| import React, {Component} from "react"; | ||||
| import {EventProp} from "./Events"; | ||||
| import { | ||||
|     Balance, | ||||
|     CurrencyPair, | ||||
|     EventName, | ||||
|     FirstConnectMessage, | ||||
|     NewEventMessage, | ||||
|     NewTickMessage, | ||||
|     PositionProp | ||||
| } from "../types"; | ||||
| import {socket} from "../index"; | ||||
| import {symbolToPair} from "../utils"; | ||||
| import {Helmet} from "react-helmet"; | ||||
| import {Navbar, Sidebar} from "./Navbars"; | ||||
| import {Statusbar} from "./Statusbar"; | ||||
| import {PositionsTable} from "./Tables"; | ||||
| import RPlot from "./RPlot"; | ||||
| 
 | ||||
| type AppState = { | ||||
|     current_price: number, | ||||
|     current_tick: number, | ||||
|     last_update: Date, | ||||
|     positions: Array<PositionProp>, | ||||
|     events: Array<EventProp>, | ||||
|     active_pair: CurrencyPair, | ||||
|     available_pairs: Array<CurrencyPair>, | ||||
|     balances: Array<Balance> | ||||
| } | ||||
| 
 | ||||
| class App extends Component<{}, AppState> { | ||||
|     event_id = 0; | ||||
| 
 | ||||
|     state = { | ||||
|         current_price: 0, | ||||
|         current_tick: 0, | ||||
|         last_update: new Date(), | ||||
|         positions: [], | ||||
|         events: [], | ||||
|         balances: [], | ||||
|         active_pair: symbolToPair("tBTCUSD"), | ||||
|         available_pairs: [] | ||||
|     } | ||||
| 
 | ||||
|     constructor(props) { | ||||
|         super(props) | ||||
|     } | ||||
| 
 | ||||
|     componentDidMount() { | ||||
|         socket.on(EventName.FirstConnect, (data: FirstConnectMessage) => { | ||||
|             this.setState({ | ||||
|                 current_price: data.prices[data.prices.length - 1], | ||||
|                 current_tick: data.ticks[data.ticks.length - 1], | ||||
|                 last_update: new Date(), | ||||
|                 positions: data.positions, | ||||
|                 balances: data.balances | ||||
|             }) | ||||
|         }) | ||||
| 
 | ||||
|         socket.on(EventName.NewTick, (data: NewTickMessage) => { | ||||
|             this.setState({ | ||||
|                 current_price: data.price, | ||||
|                 current_tick: data.tick, | ||||
|                 last_update: new Date(), | ||||
|                 positions: data.positions, | ||||
|                 balances: data.balances | ||||
|             }) | ||||
|         }) | ||||
| 
 | ||||
|         socket.on(EventName.NewEvent, (data: NewEventMessage) => { | ||||
|             // ignore new tick
 | ||||
|             if (!data.kind.toLowerCase().includes("new_tick")) { | ||||
|                 const new_event: EventProp = { | ||||
|                     id: this.event_id, | ||||
|                     name: data.kind, | ||||
|                     tick: data.tick | ||||
|                 } | ||||
| 
 | ||||
|                 this.event_id += 1 | ||||
| 
 | ||||
|                 this.setState((state) => ({ | ||||
|                     events: [...state.events, new_event] | ||||
|                 })) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         return ( | ||||
|             <> | ||||
|                 <Helmet> | ||||
|                     <title> Rustico | ||||
|                         - {String(this.state.current_price.toLocaleString())} {String(this.state.active_pair.base) + "/" + String(this.state.active_pair.quote)} </title> | ||||
|                 </Helmet> | ||||
|                 <div className="bg-gray-800"> | ||||
|                     <div className="h-screen max-w-screen flex mx-auto"> | ||||
|                         <Navbar/> | ||||
| 
 | ||||
|                         <main | ||||
|                             className="my-1 py-2 px-10 flex-1 bg-gray-200 dark:bg-black rounded-l-lg* | ||||
|                     transition duration-500 ease-in-out overflow-y-auto flex flex-col"> | ||||
|                             <div className="flex justify-center text-2xl my-2"> | ||||
|                                 <Statusbar balances={this.state.balances} positions={this.state.positions} | ||||
|                                            price={this.state.current_price} | ||||
|                                            tick={this.state.current_tick}/> | ||||
|                             </div> | ||||
| 
 | ||||
|                             <div className="flex flex-col flex-grow my-8 shadow-md hover:shadow-lg"> | ||||
|                                 <div | ||||
|                                     className="py-2 flex-grow bg-white min-width dark:bg-gray-600 rounded-lg overflow-hidden"> | ||||
|                                     <RPlot/> | ||||
|                                 </div> | ||||
|                             </div> | ||||
| 
 | ||||
|                             {this.state.positions.length > 0 ? | ||||
|                                 <PositionsTable positions={this.state.positions}/> : null} | ||||
| 
 | ||||
|                             <footer className="flex rounded-lg justify-center bg-gray-600 mt-4 border-t text-gray-300"> | ||||
|                                 <span className="my-1 mx-1">Made with ❤️ by the Peperone in a scantinato</span> | ||||
|                             </footer> | ||||
|                         </main> | ||||
| 
 | ||||
|                         <Sidebar/> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </> | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default App; | ||||
							
								
								
									
										155
									
								
								websrc/components/Cards.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								websrc/components/Cards.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,155 @@ | ||||
| import React, {Component} from 'react'; | ||||
| import {Balance, EventName, FirstConnectMessage, NewTickMessage} from "../types"; | ||||
| import {socket} from "../index"; | ||||
| 
 | ||||
| export type CoinBalanceProps = { | ||||
|     name: string, | ||||
|     amount: number, | ||||
|     percentage?: number, | ||||
|     quote_equivalent: number, | ||||
|     quote_symbol: string, | ||||
| } | ||||
| 
 | ||||
| class CoinBalance extends Component<CoinBalanceProps> { | ||||
|     constructor(props) { | ||||
|         super(props); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         // do not print equivalent if this element is the quote itself
 | ||||
|         const quoteBlock = this.props.name != this.props.quote_symbol ? this.props.quote_symbol.concat(" ").concat(this.props.quote_equivalent.toLocaleString()) : null | ||||
| 
 | ||||
|         // const accessory = SymbolAccessories.filter((accessory) => {
 | ||||
|         //     return accessory.name == this.props.name
 | ||||
|         // })
 | ||||
|         //
 | ||||
|         // const icon = accessory.length > 0 ? accessory.pop().icon : null
 | ||||
| 
 | ||||
|         return ( | ||||
|             <div className="flex-grow flex px-6 py-3 text-gray-800 items-center border-b -mx-4 align-middle"> | ||||
|                 <div className={"w-1/8"}> | ||||
|                     {/*{icon}*/} | ||||
|                 </div> | ||||
|                 <div className="w-2/5 xl:w-1/4 px-4 flex items-center"> | ||||
|                     <span className="text-lg">{this.props.name}</span> | ||||
|                 </div> | ||||
|                 <div className="hidden md:flex lg:hidden xl:flex w-1/4 px-1 flex-col"> | ||||
|                     <div className={"italic w-full text-center"}> | ||||
|                         {Math.trunc(this.props.percentage)}% | ||||
|                     </div> | ||||
|                     <div className="w-full bg-transparent mt-2"> | ||||
|                         <div className={"bg-blue-400 rounded-lg text-xs leading-none py-1 text-center text-white"} | ||||
|                              style={{width: this.props.percentage.toString().concat("%")}}/> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div className="flex w-3/5 md:w/12"> | ||||
|                     <div className="w-1/2 px-4"> | ||||
|                         <div className="text-right"> | ||||
|                             {this.props.amount.toFixed(5)} {this.props.name} | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div className="w-1/2 px-4 my-auto"> | ||||
|                         <div | ||||
|                             className={"px-2 inline-flex text-center text-xs leading-5 font-semibold rounded-full bg-gray-200 text-gray-800"}> | ||||
|                             {quoteBlock} | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export type WalletCardProps = { | ||||
|     quote: string, | ||||
| } | ||||
| 
 | ||||
| export class WalletCard extends Component | ||||
|     <{}, { balances: Array<Balance> }> { | ||||
|     constructor(props) { | ||||
|         super(props); | ||||
|     } | ||||
| 
 | ||||
|     state = | ||||
|         { | ||||
|             balances: [] | ||||
|         } | ||||
| 
 | ||||
|     totalQuoteBalance() { | ||||
|         let total = 0 | ||||
| 
 | ||||
|         this.state.balances.forEach((balance: Balance) => { | ||||
|             if (balance.currency == balance.quote) { | ||||
|                 total += balance.amount | ||||
|             } else { | ||||
|                 total += balance.quote_equivalent | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|         return total | ||||
|     } | ||||
| 
 | ||||
|     renderCoinBalances() { | ||||
|         return ( | ||||
|             this.state.balances.map((balance: Balance) => { | ||||
|                 const percentage_amount = balance.quote == balance.currency ? balance.amount : balance.quote_equivalent; | ||||
| 
 | ||||
|                 return ( | ||||
|                     <CoinBalance key={balance.currency.concat(balance.kind)} name={balance.currency} | ||||
|                                  amount={balance.amount} quote_equivalent={balance.quote_equivalent} | ||||
|                                  percentage={percentage_amount / this.totalQuoteBalance() * 100} | ||||
|                                  quote_symbol={balance.quote}/> | ||||
|                 ) | ||||
|             }) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     componentDidMount() { | ||||
|         socket.on(EventName.NewTick, (data: NewTickMessage) => { | ||||
|             this.setState({ | ||||
|                 balances: data.balances | ||||
|             }) | ||||
|         }) | ||||
| 
 | ||||
|         socket.on(EventName.FirstConnect, (data: FirstConnectMessage) => { | ||||
|             this.setState({ | ||||
|                 balances: data.balances | ||||
|             }) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         return ( | ||||
|             <div className="w-full mb-6 lg:mb-0 px-4 flex flex-col"> | ||||
|                 <div | ||||
|                     className="flex-grow flex bg-white flex-col border-t border-b sm:rounded-lg sm:border shadow overflow-hidden"> | ||||
|                     <div className="border-b bg-gray-50"> | ||||
|                         <div className="flex justify-between px-6 -mb-px"> | ||||
|                             <h3 className="text-blue-700 py-4 font-normal text-lg">Your Wallets</h3> | ||||
|                             <div className="flex"> | ||||
|                                 <button type="button" | ||||
|                                         className="appearance-none py-4 text-blue-700 border-b border-blue-dark mr-3"> | ||||
|                                     Margin | ||||
|                                 </button> | ||||
|                                 <button type="button" | ||||
|                                         className="appearance-none py-4 text-gray-600 border-b border-transparent hover:border-grey-dark"> | ||||
|                                     Chart | ||||
|                                 </button> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
| 
 | ||||
|                     {this.renderCoinBalances()} | ||||
| 
 | ||||
|                     <div className="px-6 py-4"> | ||||
|                         <div className="text-center text-gray-400"> | ||||
|                             Total Balance ≈ USD {this.totalQuoteBalance().toLocaleString()} | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         ) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										45
									
								
								websrc/components/Currency.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								websrc/components/Currency.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | ||||
| import {Button, ButtonGroup, Dropdown} from "react-bootstrap"; | ||||
| import React, {Component} from "react"; | ||||
| import DropdownItem from "react-bootstrap/DropdownItem"; | ||||
| import {CurrencyPair} from "../types"; | ||||
| 
 | ||||
| 
 | ||||
| export type CurrencyPairProps = { | ||||
|     active_pair: CurrencyPair, | ||||
|     pairs: Array<CurrencyPair> | ||||
| } | ||||
| 
 | ||||
| export class CurrencyDropdown extends Component<CurrencyPairProps> { | ||||
|     constructor(props) { | ||||
|         super(props); | ||||
|     } | ||||
| 
 | ||||
|     dropdownItems() { | ||||
|         return this.props.pairs.map((pair) => { | ||||
|             return ( | ||||
|                 <DropdownItem key={String(pair.base) + String(pair.quote)}> {pair.base} / {pair.quote} </DropdownItem>) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         return ( | ||||
|             <Dropdown as={ButtonGroup} className={"mr-3"}> | ||||
|                 <Button variant="outline-primary"><b>{this.props.active_pair.base} / {this.props.active_pair.quote}</b></Button> | ||||
| 
 | ||||
|                 {this.props.pairs.length > 0 && | ||||
| 
 | ||||
|                 <> | ||||
|                     <Dropdown.Toggle split variant="primary" id="dropdown-split-basic"/> | ||||
| 
 | ||||
|                     <Dropdown.Menu className={"mr-3"}> | ||||
|                         {this.dropdownItems()} | ||||
|                     </Dropdown.Menu> | ||||
|                 </> | ||||
|                 } | ||||
| 
 | ||||
|             </Dropdown> | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										42
									
								
								websrc/components/Events.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								websrc/components/Events.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | ||||
| import React, {Component} from "react"; | ||||
| import {Container, ListGroup} from "react-bootstrap"; | ||||
| 
 | ||||
| 
 | ||||
| export type EventProp = { | ||||
|     id: number, | ||||
|     name: string, | ||||
|     tick: number | ||||
| } | ||||
| 
 | ||||
| export class Events extends Component<{ events: Array<EventProp> }> { | ||||
|     constructor(props) { | ||||
|         super(props); | ||||
|     } | ||||
| 
 | ||||
|     state = { | ||||
|         events: this.props.events | ||||
|     } | ||||
| 
 | ||||
|     mapEvents() { | ||||
|         return this.state.events.map((event: EventProp) => { | ||||
|             return ( | ||||
|                 <ListGroup.Item action key={event.id}> | ||||
|                     {event.name} @ Tick {event.tick} | ||||
|                 </ListGroup.Item> | ||||
|             ) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         return ( | ||||
|             <Container> | ||||
|                 <div className={"border-bottom mb-2"}> | ||||
|                     <h2>Events</h2> | ||||
|                 </div> | ||||
|                 <ListGroup> | ||||
|                     {this.mapEvents()} | ||||
|                 </ListGroup> | ||||
|             </Container> | ||||
|         ) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										50
									
								
								websrc/components/Icons.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								websrc/components/Icons.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,50 @@ | ||||
| import React, {Component} from "react"; | ||||
| 
 | ||||
| type IconProps = { | ||||
|     width: number, | ||||
|     height: number, | ||||
| } | ||||
| 
 | ||||
| class Icon extends Component <IconProps> { | ||||
|     private readonly width: string; | ||||
|     private readonly height: string; | ||||
| 
 | ||||
|     constructor(props) { | ||||
|         super(props); | ||||
| 
 | ||||
|         this.width = "w-" + this.props.width.toString(); | ||||
|         this.height = "h-" + this.props.width.toString(); | ||||
|     } | ||||
| 
 | ||||
|     dimensionsClassName() { | ||||
|         return (this.height + " " + this.width) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| export class DollarIcon extends Icon { | ||||
|     render() { | ||||
|         return ( | ||||
|             <svg className={this.dimensionsClassName()} xmlns="http://www.w3.org/2000/svg" fill="none" | ||||
|                  viewBox="0 0 24 24" | ||||
|                  stroke="currentColor"> | ||||
|                 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} | ||||
|                       d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/> | ||||
|             </svg> | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class ClockIcon extends Icon { | ||||
|     render() { | ||||
|         return ( | ||||
|             <svg className={this.dimensionsClassName()} xmlns="http://www.w3.org/2000/svg" fill="none" | ||||
|                  viewBox="0 0 24 24" | ||||
|                  stroke="currentColor"> | ||||
|                 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} | ||||
|                       d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/> | ||||
|             </svg> | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										71
									
								
								websrc/components/Navbars.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								websrc/components/Navbars.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,71 @@ | ||||
| import React, {Component} from "react"; | ||||
| import {WalletCard} from "./Cards"; | ||||
| 
 | ||||
| export class Navbar extends Component<any, any> { | ||||
|     constructor(props) { | ||||
|         super(props); | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         return ( | ||||
|             <nav | ||||
|                 className="ml-1 w-24 flex flex-col items-center bg-gray-700 dark:bg-gray-700 py-4 my-1 rounded-tl rounded-bl"> | ||||
| 
 | ||||
|                 <ul className="flex-1 mt-2 text-gray-700 dark:text-gray-400 capitalize"> | ||||
|                     {/* Links */} | ||||
|                     <li className="mt-3 p-2 text-gray-400 dark:text-blue-300 rounded-lg"> | ||||
|                         <a href="#" className=" flex flex-col items-center"> | ||||
|                             <svg className="fill-current h-5 w-5" viewBox="0 0 24 24"> | ||||
|                                 <path | ||||
|                                     d="M19 5v2h-4V5h4M9 5v6H5V5h4m10 8v6h-4v-6h4M9 | ||||
| 							17v2H5v-2h4M21 3h-8v6h8V3M11 3H3v10h8V3m10 | ||||
| 							8h-8v10h8V11m-10 4H3v6h8v-6z"/> | ||||
|                             </svg> | ||||
|                             <span className="text-xs mt-2 text-gray-300">Dashboard</span> | ||||
|                         </a> | ||||
|                     </li> | ||||
|                     <li className="mt-3 p-2 text-gray-400 dark:text-blue-300 rounded-lg"> | ||||
|                         <a href="#" className=" flex flex-col items-center"> | ||||
|                             <svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" | ||||
|                                  stroke="currentColor"> | ||||
|                                 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} | ||||
|                                       d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/> | ||||
|                             </svg> | ||||
|                             <span className="text-xs mt-2 text-gray-300">Reports</span> | ||||
|                         </a> | ||||
|                     </li> | ||||
|                 </ul> | ||||
|                 <ul className="text-gray-700 dark:text-gray-400 capitalize"> | ||||
|                     <li className="mt-auto p-2 text-gray-400 dark:text-blue-300 rounded-lg"> | ||||
|                         <a href="#" className=" flex flex-col items-center"> | ||||
|                             <svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" | ||||
|                                  stroke="currentColor"> | ||||
|                                 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} | ||||
|                                       d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/> | ||||
|                                 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} | ||||
|                                       d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/> | ||||
|                             </svg> | ||||
|                             <span className="text-xs mt-2 text-gray-300">Settings</span> | ||||
|                         </a> | ||||
|                     </li> | ||||
|                 </ul> | ||||
|             </nav> | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class Sidebar extends Component { | ||||
|     constructor(props) { | ||||
|         super(props); | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         return ( | ||||
|             <aside | ||||
|                 className="w-1/4 my-1 mr-1 pr-2 py-4 flex flex-col bg-gray-200 dark:bg-black | ||||
| 		dark:text-gray-400 rounded-r-lg overflow-y-auto"> | ||||
|                 <WalletCard/> | ||||
|             </aside> | ||||
|         ) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										90
									
								
								websrc/components/Overlays.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								websrc/components/Overlays.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,90 @@ | ||||
| import React, {Component} from "react"; | ||||
| import {socket} from "../index"; | ||||
| import {EventName} from "../types"; | ||||
| 
 | ||||
| export type ModalProps = { | ||||
|     show: boolean, | ||||
|     positionId: number, | ||||
|     toggleConfirmation: any | ||||
| } | ||||
| 
 | ||||
| export class ClosePositionModal extends Component<ModalProps, any> { | ||||
|     constructor(props) { | ||||
|         super(props); | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         if (!this.props.show) { | ||||
|             return null | ||||
|         } | ||||
| 
 | ||||
|         return ( | ||||
|             <div className="fixed z-10 inset-0 overflow-y-auto"> | ||||
|                 <div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> | ||||
|                     <div className="fixed inset-0 transition-opacity" aria-hidden="true"> | ||||
|                         <div className="absolute inset-0 bg-gray-500 opacity-75"/> | ||||
|                     </div> | ||||
| 
 | ||||
|                     {/*This element is to trick the browser into centering the modal contents. -->*/} | ||||
|                     <span className="hidden sm:inline-block sm:align-middle sm:h-screen" | ||||
|                           aria-hidden="true">​</span> | ||||
| 
 | ||||
|                       {/*Modal panel, show/hide based on modal state.*/} | ||||
| 
 | ||||
|                       {/*Entering: "ease-out duration-300"*/} | ||||
|                       {/*  From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"*/} | ||||
|                       {/*  To: "opacity-100 translate-y-0 sm:scale-100"*/} | ||||
|                       {/*Leaving: "ease-in duration-200"*/} | ||||
|                       {/*  From: "opacity-100 translate-y-0 sm:scale-100"*/} | ||||
|                       {/*  To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"*/} | ||||
| 
 | ||||
|                     <div | ||||
|                         className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full" | ||||
|                         role="dialog" aria-modal="true" aria-labelledby="modal-headline"> | ||||
|                         <div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> | ||||
|                             <div className="sm:flex sm:items-start"> | ||||
|                                 <div | ||||
|                                     className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10"> | ||||
|                                      {/*Heroicon name: exclamation -->*/} | ||||
|                                     <svg className="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" | ||||
|                                          viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> | ||||
|                                         <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" | ||||
|                                               d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/> | ||||
|                                     </svg> | ||||
|                                 </div> | ||||
|                                 <div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> | ||||
|                                     <h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-headline"> | ||||
|                                         Close position | ||||
|                                     </h3> | ||||
|                                     <div className="mt-2"> | ||||
|                                         <p className="text-sm text-gray-500"> | ||||
|                                             Are you sure you want to close the position? This action cannot be undone. | ||||
|                                         </p> | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"> | ||||
|                             <button type="button" | ||||
|                                     className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm" | ||||
|                                     onClick={event => { | ||||
|                                         socket.emit(EventName.ClosePosition, { | ||||
|                                             position_id: this.props.positionId | ||||
|                                         }) | ||||
| 
 | ||||
|                                         this.props.toggleConfirmation() | ||||
|                                     }}> | ||||
|                                 Close | ||||
|                             </button> | ||||
|                             <button type="button" | ||||
|                                     className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" | ||||
|                                     onClick={() => this.props.toggleConfirmation()}> | ||||
|                                 Cancel | ||||
|                             </button> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         ) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										168
									
								
								websrc/components/RPlot.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								websrc/components/RPlot.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,168 @@ | ||||
| import React, {Component} from "react" | ||||
| import Plot from "react-plotly.js" | ||||
| 
 | ||||
| import {socket} from '../'; | ||||
| import {EventName, NewTickMessage} from "../types"; | ||||
| 
 | ||||
| 
 | ||||
| type FirstConnectData = { | ||||
|     ticks: Array<number>, | ||||
|     prices: Array<number> | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| type PriceLine = { | ||||
|     x0: number, | ||||
|     y0: number, | ||||
|     x1: number, | ||||
|     y1: number | ||||
| } | ||||
| 
 | ||||
| type PlotState = { | ||||
|     x: Array<number>, | ||||
|     y: Array<number>, | ||||
|     current_price_line: PriceLine, | ||||
|     positions_price_lines: Array<PriceLine>, | ||||
| } | ||||
| 
 | ||||
| class RPlot extends Component<{}, PlotState> { | ||||
|     state = { | ||||
|         x: [], | ||||
|         y: [], | ||||
|         current_price_line: {x0: 0, x1: 0, y0: 0, y1: 0}, | ||||
|         positions_price_lines: [] | ||||
|     } | ||||
| 
 | ||||
|     constructor(props) { | ||||
|         super(props) | ||||
|     } | ||||
| 
 | ||||
|     componentDidMount() { | ||||
|         socket.on(EventName.FirstConnect, (data: FirstConnectData) => { | ||||
|             const last_tick = data.ticks[data.ticks.length - 1]; | ||||
|             const last_price = data.prices[data.prices.length - 1]; | ||||
| 
 | ||||
|             this.setState({ | ||||
|                 x: data.ticks, | ||||
|                 y: data.prices, | ||||
|                 current_price_line: { | ||||
|                     x0: 0, | ||||
|                     y0: last_price, | ||||
|                     x1: last_tick, | ||||
|                     y1: last_price | ||||
|                 }, | ||||
|             }) | ||||
|         }) | ||||
| 
 | ||||
|         socket.on(EventName.NewTick, (data: NewTickMessage) => { | ||||
|             const position_price_lines = data.positions.map((pstat): PriceLine => { | ||||
|                 return { | ||||
|                     x0: 0, | ||||
|                     y0: pstat.base_price, | ||||
|                     x1: data.tick, | ||||
|                     y1: pstat.base_price | ||||
|                 } | ||||
|             }) | ||||
| 
 | ||||
|             this.setState((state) => ({ | ||||
|                 x: state.x.concat(data.tick), | ||||
|                 y: state.y.concat(data.price), | ||||
|                 current_price_line: { | ||||
|                     x0: 0, | ||||
|                     y0: data.price, | ||||
|                     x1: data.tick, | ||||
|                     y1: data.price | ||||
|                 }, | ||||
|                 positions_price_lines: position_price_lines | ||||
|             })) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         let additional_shapes = [] | ||||
| 
 | ||||
|         if (this.state.positions_price_lines.length > 0) { | ||||
|             additional_shapes = this.state.positions_price_lines.map((priceline: PriceLine) => { | ||||
|                 return { | ||||
|                     type: 'line', | ||||
|                     x0: priceline.x0, | ||||
|                     y0: priceline.y0, | ||||
|                     x1: priceline.x1, | ||||
|                     y1: priceline.y1, | ||||
|                     line: { | ||||
|                         color: 'rgb(1, 1, 1)', | ||||
|                         width: 1, | ||||
|                         dash: 'solid' | ||||
|                     } | ||||
|                 } | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         return ( | ||||
|             <Plot | ||||
|                 data={[ | ||||
|                     { | ||||
|                         x: this.state.x, | ||||
|                         y: this.state.y, | ||||
|                         type: 'scatter', | ||||
|                         mode: 'lines+markers', | ||||
|                     }, | ||||
|                 ]} | ||||
|                 layout={{ | ||||
|                     margin: { | ||||
|                         l: 110, | ||||
|                         r: 20, | ||||
|                         b: 100, | ||||
|                         t: 20, | ||||
|                         pad: 4 | ||||
|                     }, | ||||
|                     dragmode: "pan", | ||||
|                     shapes: [ | ||||
|                         { | ||||
|                             type: 'line', | ||||
|                             x0: this.state.current_price_line.x0, | ||||
|                             y0: this.state.current_price_line.y0, | ||||
|                             x1: this.state.current_price_line.x1, | ||||
|                             y1: this.state.current_price_line.y1, | ||||
|                             line: { | ||||
|                                 color: 'rgb(50, 171, 96)', | ||||
|                                 width: 2, | ||||
|                                 dash: 'dashdot' | ||||
|                             } | ||||
|                         }, | ||||
|                     ].concat(additional_shapes), | ||||
|                     xaxis: { | ||||
|                         title: { | ||||
|                             text: 'Tick', | ||||
|                             font: { | ||||
|                                 family: 'Courier New, monospace', | ||||
|                                 size: 18, | ||||
|                                 color: '#7f7f7f' | ||||
|                             } | ||||
|                         }, | ||||
|                     }, | ||||
|                     yaxis: { | ||||
|                         title: { | ||||
|                             text: 'Price', | ||||
|                             font: { | ||||
|                                 family: 'Courier New, monospace', | ||||
|                                 size: 18, | ||||
|                                 color: '#7f7f7f' | ||||
|                             } | ||||
|                         }, | ||||
|                         tickformat: 'r', | ||||
|                     } | ||||
|                 }} | ||||
|                 config={{ | ||||
|                     scrollZoom: true, | ||||
|                     displayModeBar: false, | ||||
|                     responsive: true, | ||||
|                 }} | ||||
|                 style={{width: '100%', height: '100%'}} | ||||
|             /> | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export default RPlot; | ||||
							
								
								
									
										48
									
								
								websrc/components/RToast.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								websrc/components/RToast.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | ||||
| import React, {Component} from 'react'; | ||||
| import {Toast} from 'react-bootstrap'; | ||||
| import {socket} from "../"; | ||||
| 
 | ||||
| type ToastProps = { | ||||
|     title: string, | ||||
|     content?: string, | ||||
|     bg?: string | ||||
| } | ||||
| 
 | ||||
| type ToastState = { | ||||
|     lastUpdated: Date, | ||||
|     show: boolean | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| export class RToast extends Component<ToastProps, ToastState> { | ||||
|     state = { | ||||
|         lastUpdated: new Date(), | ||||
|         show: false | ||||
|     } | ||||
| 
 | ||||
|     constructor(props: ToastProps) { | ||||
|         super(props) | ||||
|     } | ||||
| 
 | ||||
|     componentDidMount() { | ||||
|         socket.on("connect", () => { | ||||
|             this.setState({show: true}) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     tick() { | ||||
|         this.setState({lastUpdated: new Date()}) | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         return ( | ||||
|             <Toast show={this.state.show} delay={5000} autohide> | ||||
|                 <Toast.Header> | ||||
|                     <strong className="mr-auto">{this.props.title}</strong> | ||||
|                     <small>{this.state.lastUpdated.toLocaleTimeString('en-GB')}</small> | ||||
|                 </Toast.Header> | ||||
|                 {this.props.content ? <Toast.Body> {this.props.content}</Toast.Body> : null} | ||||
|             </Toast> | ||||
|         ) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										293
									
								
								websrc/components/Statusbar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										293
									
								
								websrc/components/Statusbar.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,293 @@ | ||||
| import React, {Component} from "react"; | ||||
| import {EventName, GetProfitLossMessage, NewTickMessage, PutProfitLossMessage} from "../types"; | ||||
| import {socket} from "../index"; | ||||
| import {DateTime} from "luxon"; | ||||
| 
 | ||||
| type QuoteStatusProps = { | ||||
|     percentage?: boolean, | ||||
|     quote_symbol?: string, | ||||
|     amount: number, | ||||
|     subtitle: string, | ||||
|     sign?: boolean | ||||
| } | ||||
| 
 | ||||
| export class QuoteStatus extends Component<QuoteStatusProps> { | ||||
|     private whole: number; | ||||
|     private decimal: number; | ||||
|     private sign: string; | ||||
|     private signClass: string; | ||||
| 
 | ||||
|     constructor(props) { | ||||
|         super(props); | ||||
| 
 | ||||
|         this.deriveProps() | ||||
|     } | ||||
| 
 | ||||
|     deriveProps() { | ||||
|         this.whole = Math.abs(Math.trunc(this.props.amount)) | ||||
|         this.decimal = Math.trunc(this.props.amount % 1 * 100) | ||||
|         this.sign = this.props.amount > 0 ? "+" : "-" | ||||
| 
 | ||||
|         this.signClass = this.props.amount > 0 ? "text-green-500" : "text-red-500" | ||||
|     } | ||||
| 
 | ||||
|     renderSign() { | ||||
|         if (this.props.sign) { | ||||
|             return ( | ||||
|                 <span | ||||
|                     className={this.signClass}>{this.sign}</span> | ||||
|             ) | ||||
|         } | ||||
|         return null | ||||
|     } | ||||
| 
 | ||||
|     symbolOrPercentageRender() { | ||||
|         if (this.props.percentage) { | ||||
|             return ( | ||||
|                 <> | ||||
|                 <span className="text-4xl text-bold align-top"> | ||||
|                     {this.renderSign()}</span> | ||||
|                     <span className="text-5xl">{Math.abs(this.props.amount).toFixed(2)}</span> | ||||
|                     <span className="text-3xl align-top">%</span> | ||||
|                 </> | ||||
|             ) | ||||
|         } else { | ||||
|             return ( | ||||
|                 <> | ||||
|                     <span className="text-4xl text-bold align-top">{this.renderSign()}{this.props.quote_symbol}</span> | ||||
|                     <span className="text-5xl">{this.whole.toLocaleString()}</span> | ||||
|                     <span className="text-3xl align-top">.{Math.abs(this.decimal)}</span> | ||||
|                 </> | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         return ( | ||||
|             <> | ||||
|                 <div className="text-gray-700 mb-2"> | ||||
|                     {this.symbolOrPercentageRender()} | ||||
|                 </div> | ||||
|                 <div className="text-sm uppercase text-gray-300 tracking-wide"> | ||||
|                     {this.props.subtitle} | ||||
|                 </div> | ||||
|             </> | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| type DateButtonProps = { | ||||
|     label: string, | ||||
|     onClick: any, | ||||
|     selected_default?: boolean | ||||
| } | ||||
| 
 | ||||
| type DateButtonState = { | ||||
|     selected: boolean, | ||||
| } | ||||
| 
 | ||||
| class DateButton extends Component<DateButtonProps, DateButtonState> { | ||||
|     private classSelected: string = "appearance-none py-4 text-blue-600 border-b border-blue-600 mr-3"; | ||||
|     private classNotSelected: string = "appearance-none py-4 text-gray-600 border-b border-transparent hover:border-gray-800 mr-3"; | ||||
|     private currentClass: string; | ||||
| 
 | ||||
|     state = { | ||||
|         selected: this.props.selected_default | ||||
|     } | ||||
| 
 | ||||
|     constructor(props) { | ||||
|         super(props); | ||||
| 
 | ||||
|         this.updateClass() | ||||
|     } | ||||
| 
 | ||||
|     onClick() { | ||||
|         this.setState({selected: !this.state.selected}, this.updateClass) | ||||
| 
 | ||||
|         this.props.onClick() | ||||
|     } | ||||
| 
 | ||||
|     updateClass() { | ||||
|         this.currentClass = this.state.selected ? this.classSelected : this.classNotSelected | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         return ( | ||||
|             <button key={this.props.label} type="button" | ||||
|                     className={this.currentClass} onClick={this.onClick.bind(this)}> | ||||
|                 {this.props.label} | ||||
|             </button> | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| const PeriodUnit = { | ||||
|     SECOND: "second", | ||||
|     MINUTE: "minute", | ||||
|     HOUR: "hour", | ||||
|     DAY: "day", | ||||
|     WEEK: "week", | ||||
|     MONTH: "month", | ||||
|     YEAR: "year" | ||||
| } | ||||
| 
 | ||||
| type StatusBarState = { | ||||
|     pl_period_unit: string, | ||||
|     pl_period_amount: number, | ||||
|     pl: number, | ||||
|     pl_perc: number | ||||
| } | ||||
| 
 | ||||
| export class Statusbar extends Component<NewTickMessage, StatusBarState> { | ||||
| 
 | ||||
|     constructor(props) { | ||||
|         super(props); | ||||
| 
 | ||||
|         this.state = { | ||||
|             pl_period_unit: PeriodUnit.WEEK, | ||||
|             pl_period_amount: 1, | ||||
|             pl: 0.0, | ||||
|             pl_perc: 0.0 | ||||
|         } | ||||
| 
 | ||||
|         this.emitGetProfitLoss = this.emitGetProfitLoss.bind(this) | ||||
|     } | ||||
| 
 | ||||
|     componentDidMount() { | ||||
|         socket.on(EventName.PutProfitLoss, (data: PutProfitLossMessage) => { | ||||
|             this.setState({ | ||||
|                 pl: data.pl, | ||||
|                 pl_perc: data.pl_perc | ||||
|             }) | ||||
|         }) | ||||
| 
 | ||||
|         socket.on(EventName.FirstConnect, this.emitGetProfitLoss) | ||||
|         socket.on(EventName.NewTick, this.emitGetProfitLoss) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     durationObjectfromStr(str: string) { | ||||
|         switch (str) { | ||||
|             case PeriodUnit.MINUTE: | ||||
|                 return { | ||||
|                     minutes: this.state.pl_period_amount | ||||
|                 } | ||||
|             case PeriodUnit.HOUR: | ||||
|                 return { | ||||
|                     hours: this.state.pl_period_amount | ||||
|                 } | ||||
|             case PeriodUnit.DAY: | ||||
|                 return { | ||||
|                     days: this.state.pl_period_amount | ||||
|                 } | ||||
|             case PeriodUnit.WEEK: | ||||
|                 return { | ||||
|                     weeks: this.state.pl_period_amount | ||||
|                 } | ||||
|             case PeriodUnit.MONTH: | ||||
|                 return { | ||||
|                     months: this.state.pl_period_amount | ||||
|                 } | ||||
|             case PeriodUnit.YEAR: | ||||
|                 return { | ||||
|                     years: this.state.pl_period_amount | ||||
|                 } | ||||
|             default: | ||||
|                 return {} | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     emitGetProfitLoss() { | ||||
|         const message: GetProfitLossMessage = { | ||||
|             start: DateTime.local().minus(this.durationObjectfromStr(this.state.pl_period_unit)).toMillis(), | ||||
|             end: DateTime.local().toMillis() | ||||
|         } | ||||
| 
 | ||||
|         socket.emit(EventName.GetProfitLoss, message) | ||||
|     } | ||||
| 
 | ||||
|     changeProfitLossPeriod(amount: number, unit: string) { | ||||
|         this.setState({ | ||||
|             pl_period_amount: amount, | ||||
|             pl_period_unit: unit | ||||
|         }, this.emitGetProfitLoss) | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         return ( | ||||
|             <div className="bg-white border-t border-b sm:border-l sm:border-r sm:rounded-lg shadow flex-grow mb-6"> | ||||
|                 <div className="bg-gray-50 rounded-tl-lg rounded-tr-lg border-b px-6"> | ||||
|                     <div className="flex justify-between -mb-px"> | ||||
|                         <div className="lg:hidden text-blue-600 py-4 text-lg"> | ||||
|                             Price Charts | ||||
|                         </div> | ||||
|                         <div className="hidden lg:flex"> | ||||
|                             <button type="button" | ||||
|                                     className="appearance-none py-4 text-blue-600 border-b border-blue-dark mr-6"> | ||||
|                                 Bitcoin | ||||
|                             </button> | ||||
|                         </div> | ||||
|                         <div className="flex text-sm"> | ||||
|                             <DateButton key={PeriodUnit.MINUTE} label={"1m"} | ||||
|                                         onClick={() => this.changeProfitLossPeriod(1, PeriodUnit.MINUTE)}/> | ||||
|                             <DateButton key={PeriodUnit.DAY} label={"1D"} | ||||
|                                         onClick={() => this.changeProfitLossPeriod(1, PeriodUnit.DAY)}/> | ||||
|                             <DateButton key={PeriodUnit.WEEK} label={"1W"} selected_default={true} | ||||
|                                         onClick={() => this.changeProfitLossPeriod(1, PeriodUnit.WEEK)}/> | ||||
|                             <DateButton key={PeriodUnit.MONTH} label={"1M"} | ||||
|                                         onClick={() => this.changeProfitLossPeriod(1, PeriodUnit.MONTH)}/> | ||||
|                             <DateButton key={PeriodUnit.YEAR} label={"1Y"} | ||||
|                                         onClick={() => this.changeProfitLossPeriod(1, PeriodUnit.YEAR)}/> | ||||
|                             {/*<DateButton label={"ALL"} onClick={() => this.changeProfitLossPeriod(1, PeriodUnit.MINUTE)}/>*/} | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div className="flex items-center px-6 lg:hidden"> | ||||
|                     <div className="flex-grow flex-no-shrink py-6"> | ||||
|                         <div className="text-gray-700 mb-2"> | ||||
|                             <span className="text-3xl align-top">CA$</span> | ||||
|                             <span className="text-5xl">21,404</span> | ||||
|                             <span className="text-3xl align-top">.74</span> | ||||
|                         </div> | ||||
|                         <div className="text-green-300 text-sm"> | ||||
|                             ↑ CA$12,955.35 (154.16%) | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div className="flex-shrink w-32 inline-block relative"> | ||||
|                         <select | ||||
|                             className="block appearance-none w-full bg-white border border-grey-light px-4 py-2 pr-8 rounded"> | ||||
|                             <option>BTC</option> | ||||
|                         </select> | ||||
|                         <div className="pointer-events-none absolute pin-y pin-r flex items-center px-2 text-gray-300"> | ||||
|                             <svg className="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" | ||||
|                                  viewBox="0 0 20 20"> | ||||
|                                 <path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"/> | ||||
|                             </svg> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div className="hidden lg:flex"> | ||||
|                     <div className="w-1/3 text-center py-8"> | ||||
|                         <div className="border-r"> | ||||
|                             <QuoteStatus key={this.props.price} quote_symbol={"USD"} amount={this.props.price} | ||||
|                                          subtitle={"Bitcoin price"}/> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div className="w-1/3 text-center py-8"> | ||||
|                         <div className="border-r"> | ||||
|                             <QuoteStatus key={this.state.pl} quote_symbol={"USD"} sign={true} amount={this.state.pl} | ||||
|                                          subtitle={"since last ".concat(this.state.pl_period_unit)}/> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div className="w-1/3 text-center py-8"> | ||||
|                         <div> | ||||
|                             <QuoteStatus key={this.state.pl_perc} percentage={true} sign={true} | ||||
|                                          amount={this.state.pl_perc} | ||||
|                                          subtitle={"since last ".concat(this.state.pl_period_unit)}/> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         ) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										141
									
								
								websrc/components/Tables.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								websrc/components/Tables.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,141 @@ | ||||
| import React, {Component} from "react" | ||||
| import {PositionProp} from "../types"; | ||||
| import {ClosePositionModal} from "./Overlays"; | ||||
| 
 | ||||
| type PositionsTableState = { | ||||
|     showConfirmation: boolean, | ||||
|     positionToClose: number | ||||
| } | ||||
| 
 | ||||
| export class PositionsTable extends Component<{ positions: Array<PositionProp> }, PositionsTableState> { | ||||
|     constructor(props) { | ||||
|         super(props); | ||||
|         this.toggleConfirmation = this.toggleConfirmation.bind(this) | ||||
|     } | ||||
| 
 | ||||
|     state = { | ||||
|         showConfirmation: false, | ||||
|         positionToClose: 0, | ||||
|     } | ||||
| 
 | ||||
|     toggleConfirmation() { | ||||
|         this.setState((state) => ({ | ||||
|             showConfirmation: !state.showConfirmation | ||||
|         })) | ||||
|     } | ||||
| 
 | ||||
|     stateColor(state: string): string { | ||||
|         const lower_state = state.toLowerCase() | ||||
|         let res: string | ||||
| 
 | ||||
|         if (lower_state.includes("profit")) { | ||||
|             res = "green" | ||||
|         } else if (lower_state.includes("break")) { | ||||
|             res = "yellow" | ||||
|         } else { | ||||
|             res = "red" | ||||
|         } | ||||
| 
 | ||||
|         return res | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     renderTableHead() { | ||||
|         return ["status", "currency pair", "base price", "amount", "Profit/Loss", "actions"].map((entry) => { | ||||
|                 return ( | ||||
|                     <th scope="col" | ||||
|                         className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> | ||||
|                         {entry} | ||||
|                     </th> | ||||
|                 ) | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     renderTableRows() { | ||||
|         return this.props.positions.map((position) => { | ||||
|             // TODO: move symbolToPair out of here?
 | ||||
|             const stateBg = "bg-".concat(this.stateColor(position.state)).concat("-100 ") | ||||
|             const stateText = "text-".concat(this.stateColor(position.state)).concat("-800 ") | ||||
|             const stateClass = "px-2 inline-flex text-xs leading-5 font-semibold rounded-full ".concat(stateBg).concat(stateText) | ||||
| 
 | ||||
|             return ( | ||||
|                 <tr key={position.id}> | ||||
|                     {/* Status */} | ||||
|                     <td className="px-6 py-4 whitespace-nowrap"> | ||||
|                         <span | ||||
|                             className={stateClass}> | ||||
|                           {position.state} | ||||
|                         </span> | ||||
|                     </td> | ||||
| 
 | ||||
|                     <td className="px-6 py-1 whitespace-nowrap"> | ||||
|                         <div className="text-sm text-gray-900">{position.pair.base}/{position.pair.quote}</div> | ||||
|                         {/*<div className="text-sm text-gray-500">{position.}</div>*/} | ||||
|                     </td> | ||||
| 
 | ||||
|                     <td className="px-6 py-1 whitespace-nowrap"> | ||||
|                         <div | ||||
|                             className="text-sm text-gray-900">{position.base_price.toLocaleString()} {position.pair.quote}/{position.pair.base}</div> | ||||
|                         {/*<div className="text-sm text-gray-500">Insert total % here?</div>*/} | ||||
|                     </td> | ||||
| 
 | ||||
|                     <td className="px-6 py-1 whitespace-nowrap"> | ||||
|                         <div className="text-sm text-gray-900">{position.amount.toFixed(5)} {position.pair.base}</div> | ||||
|                         <div className="text-sm text-gray-500">Insert total % here?</div> | ||||
|                     </td> | ||||
| 
 | ||||
|                     <td className="px-6 py-1 whitespace-nowrap"> | ||||
|                         <div | ||||
|                             className="text-sm text-gray-900 font-semibold">{position.profit_loss.toLocaleString()} {position.pair.quote}</div> | ||||
|                         <div className={"text-sm ".concat(stateClass)}>{position.profit_loss_percentage.toFixed(2)}% | ||||
|                         </div> | ||||
|                     </td> | ||||
| 
 | ||||
|                     <td className="px-6 py-1 whitespace-nowrap text-right text-sm font-medium"> | ||||
|                         <div className="p-2 md:w-40"> | ||||
|                             <div | ||||
|                                 className="p-4 flex justify-center bg-red-200 rounded-lg shadow-xs cursor-pointer hover:bg-red-500 hover:text-red-100" | ||||
|                                 onClick={() => { | ||||
|                                     this.setState({ | ||||
|                                         showConfirmation: true, | ||||
|                                         positionToClose: position.id | ||||
|                                     }) | ||||
|                                 }}> | ||||
|                                 <span className="ml-2">Close</span> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </td> | ||||
|                 </tr> | ||||
|             ) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         return ( | ||||
|             <div className="flex flex-col"> | ||||
|                 <ClosePositionModal positionId={this.state.positionToClose} toggleConfirmation={this.toggleConfirmation} | ||||
|                                     show={this.state.showConfirmation}/> | ||||
|                 <div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> | ||||
|                     <div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"> | ||||
|                         <div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"> | ||||
|                             <table className="min-w-full divide-y divide-gray-200"> | ||||
| 
 | ||||
|                                 <thead className="bg-gray-50"> | ||||
|                                 <tr> | ||||
|                                     {this.renderTableHead()} | ||||
|                                 </tr> | ||||
|                                 </thead> | ||||
| 
 | ||||
|                                 <tbody className="bg-white divide-y divide-gray-200"> | ||||
|                                 {this.renderTableRows()} | ||||
|                                 </tbody> | ||||
|                             </table> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
| 
 | ||||
|         ) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										1
									
								
								websrc/css/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								websrc/css/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| tailwind.css | ||||
							
								
								
									
										3
									
								
								websrc/css/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								websrc/css/index.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| @tailwind base; | ||||
| @tailwind components; | ||||
| @tailwind utilities; | ||||
							
								
								
									
										13
									
								
								websrc/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								websrc/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| import React from "react"; | ||||
| import ReactDOM from "react-dom"; | ||||
| import "./css/tailwind.css"; | ||||
| import App from "./components/App"; | ||||
| import io from "socket.io-client"; | ||||
| 
 | ||||
| export const socket = io(); | ||||
| 
 | ||||
| socket.on("connect", function () { | ||||
|     console.log("Connected!") | ||||
| }) | ||||
| 
 | ||||
| ReactDOM.render(<App/>, document.getElementById("root")); | ||||
							
								
								
									
										40
									
								
								websrc/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								websrc/package.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| { | ||||
|   "name": "rustico", | ||||
|   "version": "1.0.0", | ||||
|   "main": "index.tsx", | ||||
|   "license": "MIT", | ||||
|   "devDependencies": { | ||||
|     "@types/luxon": "^1.25.0", | ||||
|     "@types/react": "^17.0.0", | ||||
|     "@types/react-dom": "^17.0.0", | ||||
|     "@types/react-helmet": "^6.1.0", | ||||
|     "@types/react-plotly.js": "^2.2.4", | ||||
|     "@types/socket.io-client": "^1.4.34", | ||||
|     "parcel-bundler": "^1.12.4", | ||||
|     "react-plotly.js": "^2.5.1", | ||||
|     "typescript": "^4.1.2" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@types/classnames": "^2.2.11", | ||||
|     "autoprefixer": "^10.1.0", | ||||
|     "classnames": "^2.2.6", | ||||
|     "css": "^3.0.0", | ||||
|     "luxon": "^1.25.0", | ||||
|     "plotly.js": "^1.58.2", | ||||
|     "postcss": "^8.2.1", | ||||
|     "postcss-cli": "^8.3.1", | ||||
|     "react": "^17.0.1", | ||||
|     "react-cryptocoins": "^1.0.11", | ||||
|     "react-dom": "^17.0.1", | ||||
|     "react-helmet": "^6.1.0", | ||||
|     "socket.io-client": "~3", | ||||
|     "tailwindcss": "^2.0.2" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "build:tailwind": "tailwindcss build css/index.css -o css/tailwind.css", | ||||
|     "watch": "parcel watch index.tsx -d ../static", | ||||
|     "prestart": "yarn run build:tailwind", | ||||
|     "prebuild": "yarn run build:tailwind", | ||||
|     "start": "python ../main.py & yarn run watch" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										12
									
								
								websrc/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								websrc/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| { | ||||
|     "compilerOptions": { | ||||
|         "lib": ["esnext", "DOM"], | ||||
|         "jsx": "react", | ||||
|         "moduleResolution": "node", | ||||
|         "allowSyntheticDefaultImports": true, | ||||
|         "esModuleInterop": true, | ||||
|     }, | ||||
|     "exclude": [ | ||||
|         "node_modules" | ||||
|     ], | ||||
| } | ||||
							
								
								
									
										77
									
								
								websrc/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								websrc/types.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,77 @@ | ||||
| import {EventProp} from "./components/Events"; | ||||
| 
 | ||||
| /******************************* | ||||
|  * Types | ||||
|  *******************************/ | ||||
| 
 | ||||
| export type Balance = { | ||||
|     currency: string, | ||||
|     amount: number, | ||||
|     // exchange / margin
 | ||||
|     kind: string, | ||||
|     quote: string, | ||||
|     quote_equivalent: number, | ||||
| } | ||||
| 
 | ||||
| export type CurrencyPair = { | ||||
|     base: string, | ||||
|     quote: string | ||||
| } | ||||
| 
 | ||||
| export type FirstConnectMessage = { | ||||
|     ticks: Array<number>, | ||||
|     prices: Array<number>, | ||||
|     positions: Array<PositionProp>, | ||||
|     balances: Array<Balance> | ||||
| } | ||||
| 
 | ||||
| export type GetProfitLossMessage = { | ||||
|     start: number, | ||||
|     end: number | ||||
| } | ||||
| 
 | ||||
| export type NewEventMessage = { | ||||
|     tick: number, | ||||
|     kind: string, | ||||
| } | ||||
| 
 | ||||
| export type NewTickMessage = { | ||||
|     tick: number, | ||||
|     price: number, | ||||
|     positions: Array<PositionProp>, | ||||
|     balances: Array<Balance> | ||||
| } | ||||
| 
 | ||||
| export type PositionCloseMessage = { | ||||
|     message_name: string, | ||||
|     position_id: number, | ||||
| } | ||||
| 
 | ||||
| export type PositionProp = { | ||||
|     id: number, | ||||
|     state: string, | ||||
|     base_price: number, | ||||
|     amount: number, | ||||
|     pair: CurrencyPair, | ||||
|     profit_loss: number, | ||||
|     profit_loss_percentage: number | ||||
| } | ||||
| 
 | ||||
| export type PutProfitLossMessage = { | ||||
|     pl: number, | ||||
|     pl_perc: number | ||||
| } | ||||
| 
 | ||||
| /******************************* | ||||
| * ENUMS | ||||
| *******************************/ | ||||
| 
 | ||||
| export enum EventName { | ||||
|     NewTick = "new_tick", | ||||
|     FirstConnect = "first_connect", | ||||
|     NewEvent = "new_event", | ||||
|     ClosePosition = "close_position", | ||||
|     GetProfitLoss = "get_profit_loss", | ||||
|     PutProfitLoss = "put_profit_loss" | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										41
									
								
								websrc/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								websrc/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| import {CurrencyPair} from "./types"; | ||||
| // import * as Icon from 'react-cryptocoins'
 | ||||
| import {Btc, Eth, Xmr} from 'react-cryptocoins'; | ||||
| 
 | ||||
| export function symbolToPair(symbol: string): CurrencyPair { | ||||
|     const symbol_regex = "t(?<base>[a-zA-Z]{3})(?<quote>[a-zA-Z]{3})" | ||||
| 
 | ||||
|     const match = symbol.match(symbol_regex) | ||||
| 
 | ||||
|     return { | ||||
|         base: match.groups.base, | ||||
|         quote: match.groups.quote | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export type SymbolAccesory = { | ||||
|     name: string, | ||||
|     icon: React.Component, | ||||
|     color: string | ||||
| } | ||||
| 
 | ||||
| export const SymbolAccessories: Array<SymbolAccesory> = [ | ||||
|     { | ||||
|         name: "BTC", | ||||
|         icon: Btc, | ||||
|         color: "yellow" | ||||
|     }, | ||||
|     { | ||||
|         name: "XMR", | ||||
|         icon: Xmr, | ||||
|         color: "yellow" | ||||
|     }, | ||||
|     { | ||||
|         name: "ETH", | ||||
|         icon: Eth, | ||||
|         color: "yellow" | ||||
|     } | ||||
| ]; | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
							
								
								
									
										8826
									
								
								websrc/yarn.lock
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8826
									
								
								websrc/yarn.lock
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user