diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 92b46683..a6b3d46b 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -44,3 +44,4 @@ jobs: name: harvest-code-coverage fail_ci_if_error: true verbose: true + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index e4317c2f..8cdf7c56 100644 --- a/.gitignore +++ b/.gitignore @@ -27,5 +27,5 @@ package-lock.json .DS_Store *.log -env +.env save diff --git a/.python-version b/.python-version index 77caa0f0..a5c4c763 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.9.9 \ No newline at end of file +3.9.0 diff --git a/.trunk/configs/.isort.cfg b/.trunk/configs/.isort.cfg new file mode 100644 index 00000000..b9fb3f3e --- /dev/null +++ b/.trunk/configs/.isort.cfg @@ -0,0 +1,2 @@ +[settings] +profile=black diff --git a/.vscode/settings.json b/.vscode/settings.json index c520b28d..2765887f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,4 +3,7 @@ "files.trimTrailingWhitespace": true, "editor.formatOnSave": true, "editor.defaultFormatter": "trunk.io", + "cSpell.words": [ + "Robinhood" + ], } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1e82ea55..e760d4a8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,50 +1,50 @@ # Contributing -Thank you for helping out coding Harvest :). Your help is greatly appreciated. +Thank you for helping out coding Harvest :). Your help is greatly appreciated. ## Workflow The coding process is relatively straight-forward: 1. Choose a task to work on from [open issues](https://github.com/tfukaza/harvest/issues). Alternatively, you can create your own task by [filing a bug report](https://github.com/tfukaza/harvest/issues/new?assignees=&labels=bug&template=bug_report.md&title=%5B%F0%9F%AA%B0BUG%5D) or [submitting a feature suggestion](https://github.com/tfukaza/harvest/issues/new?assignees=&labels=enhancement%2C+question&template=feature-request.md&title=%5B%F0%9F%92%A1Feature+Request%5D). 2. When working on an issue, notify others you are doing so, so other people are aware of who is working on what. 3. Clone the repo, and write your code in your own branch. -4. Run unit tests (as described in a following section). +4. Run unit tests (as described in a following section). 5. Lint your code using [Black](https://github.com/psf/black) -6. Push your code and make a PR to merge your code to main branch. Currently this project requires the approval of at least one contributor to merge the code. +6. Push your code and make a PR to merge your code to main branch. Currently this project requires the approval of at least one contributor to merge the code. # Developer Guide -Read through the following guides to understand how to properly set up your development environment. +Read through the following guides to understand how to properly set up your development environment. ## Harvest Harvest requires a Python version of 3.9 or greater, and has a lot of dependencies, so it is highly recommended you use tools like Anaconda or VirtualEnv. ### Installing a Local Build -Run the following in the root of the project directory to install local changes you made. +Run the following in the root of the project directory to install local changes you made. ```bash pip install . ``` ### Unit Testing After any modifications to the code, conduct unit tests by running: ```bash -python -m unittest discover -s tests +python -m unittest discover -s tests/unittest ``` from the project's root directory. This will run the tests defined in the `tests` directory. ### Real-Time Testing -Unit testing does not cover all possible situations Harvest might encounter. Whenever possible, run the program as if you are a user on your own machine to test the code in real-life environments. This is especially true for codes for specific brokerages, which automated unit tests cannot cover. +Unit testing does not cover all possible situations Harvest might encounter. Whenever possible, run the program as if you are a user on your own machine to test the code in real-life environments. This is especially true for codes for specific brokerages, which automated unit tests cannot cover. **Make sure you don't accidentally `git push` secret keys of the brokerage you are using.** ## Web Interface -The web interface of Harvest is made with the Svelte framework. +The web interface of Harvest is made with the Svelte framework. ### Running a Dev Server Move to the `/gui` directory (not `/harvest/gui`) and run: ```bash npm run dev ``` -This will start the dev server. Any edits you make in `/gui/src` will automatically be built and saved to `/harvest/gui`. +This will start the dev server. Any edits you make in `/gui/src` will automatically be built and saved to `/harvest/gui`. # Coding Practices -We want to make sure our code is stable and reliable - a good way to do that is to write clean, well-documented code. +We want to make sure our code is stable and reliable - a good way to do that is to write clean, well-documented code. ### Linting This project uses the [Black](https://github.com/psf/black) linter to format the code. Before pushing any code, run the linter on every file you edited. This can usually be done by running: @@ -69,7 +69,7 @@ Good logs and debug messages can not only help users, but other developers under * Log error if an API call failed. * `raise Exception`: Something really bad happened and the entire system must be shutdown because there is no way to recover. The main difference between raising an exception and logging an error is because if the logged error is not addressed by the user the entire program will still be able to run while raising an exception requires the user to edit their code. For example: * Errors if a call to a function that should return something without a solid null case. For example returning an empty list is a fine null case but an empty dictionary or None isn't (since no one checks for the None case). - * Errors if the user tried to get a particular stock position from a broker that only supports crypto. The user expects a dictionary but Harvest has no way of providing this. + * Errors if the user tried to get a particular stock position from a broker that only supports crypto. The user expects a dictionary but Harvest has no way of providing this. ### Documenting -Every method, no matter how trivial, should be documented. This project uses the [reST format](https://stackabuse.com/python-docstrings/) +Every method, no matter how trivial, should be documented. This project uses the [reST format](https://stackabuse.com/python-docstrings/) diff --git a/examples/crossover.py b/examples/crossover.py index 2221ab40..f6d51be4 100644 --- a/examples/crossover.py +++ b/examples/crossover.py @@ -1,6 +1,6 @@ # HARVEST_SKIP from harvest.algo import BaseAlgo -from harvest.trader import LiveTrader +from harvest.trader import BrokerHub class Crossover(BaseAlgo): @@ -21,6 +21,6 @@ def main(self): if __name__ == "__main__": - t = LiveTrader() + t = BrokerHub() t.set_algo(Crossover()) t.start() diff --git a/examples/crypto.py b/examples/crypto.py index 8a2dbf35..ac4e49c2 100644 --- a/examples/crypto.py +++ b/examples/crypto.py @@ -1,8 +1,8 @@ # HARVEST_SKIP from harvest.algo import BaseAlgo -from harvest.trader import LiveTrader -from harvest.api.robinhood import Robinhood -from harvest.api.paper import PaperBroker +from harvest.broker.paper import PaperBroker +from harvest.broker.robinhood import RobinhoodBroker +from harvest.trader import BrokerHub """This algorithm trades Dogecoin. It also demonstrates some built-in functions. @@ -100,6 +100,6 @@ def sell_eval(self, ret): if __name__ == "__main__": - t = LiveTrader(Robinhood(), PaperBroker()) + t = BrokerHub(RobinhoodBroker(), PaperBroker()) t.set_algo(Crypto()) t.start() diff --git a/examples/em_alpaca.py b/examples/em_alpaca.py index 81117cf7..4cf0b09d 100644 --- a/examples/em_alpaca.py +++ b/examples/em_alpaca.py @@ -1,17 +1,18 @@ # HARVEST_SKIP # Builtin imports -import logging import datetime as dt +import logging -# Harvest imports -from harvest.algo import BaseAlgo -from harvest.trader import LiveTrader -from harvest.api.alpaca import Alpaca -from harvest.storage.csv_storage import CSVStorage +import mplfinance as mpf # Third-party imports import pandas as pd -import mplfinance as mpf + +# Harvest imports +from harvest.algo import BaseAlgo +from harvest.broker.alpaca import AlpacaBroker +from harvest.storage.csv_storage import CSVStorage +from harvest.trader import BrokerHub class EMAlgo(BaseAlgo): @@ -30,10 +31,8 @@ def main(self): now = dt.datetime.now() logging.info(f"EMAlgo.main ran at: {now}") - if now - now.replace(hour=0, minute=0, second=0, microsecond=0) <= dt.timedelta( - seconds=60 - ): - logger.info(f"It's a new day! Clearning OHLC caches!") + if now - now.replace(hour=0, minute=0, second=0, microsecond=0) <= dt.timedelta(seconds=60): + logging.info("It's a new day! Cleaning OHLC caches!") for ticker_value in self.tickers.values(): ticker_value["ohlc"] = pd.DataFrame() @@ -66,11 +65,9 @@ def process_ticker(self, ticker, ticker_data, current_price, current_ohlc): # Store the OHLC data in a folder called `em_storage` with each file stored as a csv document csv_storage = CSVStorage(save_dir="em_storage") # Our streamer and broker will be Alpaca. My secret keys are stored in `alpaca_secret.yaml` - alpaca = Alpaca( - path="accounts/alpaca-secret.yaml", is_basic_account=True, paper_trader=True - ) + alpaca = AlpacaBroker(path="accounts/alpaca-secret.yaml", is_basic_account=True, paper_trader=True) em_algo = EMAlgo() - trader = LiveTrader(streamer=alpaca, broker=alpaca, storage=csv_storage, debug=True) + trader = BrokerHub(streamer=alpaca, broker=alpaca, storage=csv_storage, debug=True) # Watch for Apple and Microsoft trader.set_symbol("AAPL") diff --git a/examples/em_kraken.py b/examples/em_kraken.py deleted file mode 100644 index 3ac92857..00000000 --- a/examples/em_kraken.py +++ /dev/null @@ -1,101 +0,0 @@ -# HARVEST_SKIP -# Builtin imports -import logging -import datetime as dt - -# Harvest imports -from harvest.algo import BaseAlgo -from harvest.trader import LiveTrader -from harvest.api.kraken import Kraken -from harvest.storage.csv_storage import CSVStorage - -# Third-party imports -import pandas as pd -import matplotlib.pyplot as plt -import mplfinance as mpf - - -class EMAlgo(BaseAlgo): - def setup(self): - now = dt.datetime.now() - logging.info(f"EMAlgo.setup ran at: {now}") - - def init_ticker(ticker): - fig = mpf.figure() - ax1 = fig.add_subplot(2, 1, 1) - ax2 = fig.add_subplot(3, 1, 3) - - return { - ticker: { - "initial_price": None, - "ohlc": pd.DataFrame(), - "fig": fig, - "ax1": ax1, - "ax2": ax2, - } - } - - self.tickers = {} - # self.tickers.update(init_ticker("@BTC")) - self.tickers.update(init_ticker("@DOGE")) - - def main(self): - now = dt.datetime.now() - logging.info(f"EMAlgo.main ran at: {now}") - - if now - now.replace(hour=0, minute=0, second=0, microsecond=0) <= dt.timedelta( - seconds=60 - ): - logger.info(f"It's a new day! Clearning OHLC caches!") - for ticker_value in self.tickers.values(): - ticker_value["ohlc"] = pd.DataFrame() - - for ticker, ticker_value in self.tickers.items(): - current_price = self.get_asset_price(ticker) - current_ohlc = self.get_asset_candle(ticker) - if ticker_value["initial_price"] is None: - ticker_value["initial_price"] = current_price - - if current_ohlc.empty: - logging.warn( - f"{ticker}'s get_asset_candle_list returned an empty list." - ) - return - - ticker_value["ohlc"] = ticker_value["ohlc"].append(current_ohlc) - - self.process_ticker(ticker, ticker_value, current_price) - - def process_ticker(self, ticker, ticker_data, current_price): - initial_price = ticker_data["initial_price"] - ohlc = ticker_data["ohlc"] - - # Calculate the price change - delta_price = current_price - initial_price - - # Print stock info - logging.info(f"{ticker} current price: ${current_price}") - logging.info(f"{ticker} price change: ${delta_price}") - - # Update the OHLC graph - ticker_data["ax1"].clear() - ticker_data["ax2"].clear() - mpf.plot(ohlc, ax=ticker_data["ax1"], volume=ticker_data["ax2"], type="candle") - plt.pause(3) - - -if __name__ == "__main__": - # Store the OHLC data in a folder called `em_storage` with each file stored as a csv document - csv_storage = CSVStorage(save_dir="em_storage") - # Our streamer and broker will be Alpaca. My secret keys are stored in `alpaca_secret.yaml` - kraken = Kraken(path="accounts/kraken-secret.yaml") - em_algo = EMAlgo() - trader = LiveTrader(streamer=kraken, broker=kraken, storage=csv_storage, debug=True) - - # trader.set_symbol("@BTC") - trader.set_symbol("@DOGE") - trader.set_algo(em_algo) - mpf.show() - - # Update every minute - trader.start("1MIN", all_history=False) diff --git a/examples/em_polygon.py b/examples/em_polygon.py index 18c5cd6b..29ea543a 100644 --- a/examples/em_polygon.py +++ b/examples/em_polygon.py @@ -1,19 +1,20 @@ # HARVEST_SKIP # Builtin imports -import logging import datetime as dt +import logging -# Harvest imports -from harvest.algo import BaseAlgo -from harvest.trader import LiveTrader -from harvest.api.polygon import PolygonStreamer -from harvest.api.paper import PaperBroker -from harvest.storage.csv_storage import CSVStorage +import matplotlib.pyplot as plt +import mplfinance as mpf # Third-party imports import pandas as pd -import matplotlib.pyplot as plt -import mplfinance as mpf + +# Harvest imports +from harvest.algo import BaseAlgo +from harvest.broker.paper import PaperBroker +from harvest.broker.polygon import PolygonBroker +from harvest.storage.csv_storage import CSVStorage +from harvest.trader import BrokerHub class EMAlgo(BaseAlgo): @@ -37,10 +38,8 @@ def main(self): logging.info("*" * 20) logging.info(f"EMAlgo.main ran at: {now}") - if now - now.replace(hour=0, minute=0, second=0, microsecond=0) <= dt.timedelta( - seconds=60 - ): - logger.info(f"It's a new day! Clearning OHLC caches!") + if now - now.replace(hour=0, minute=0, second=0, microsecond=0) <= dt.timedelta(seconds=60): + logging.info("It's a new day! Cleaning OHLC caches!") for ticker_value in self.tickers.values(): ticker_value["ohlc"] = pd.DataFrame( columns=["open", "high", "low", "close", "volume"], @@ -54,9 +53,7 @@ def main(self): logging.warn("No ohlc returned!") return ticker_value["ohlc"] = ticker_value["ohlc"].append(current_ohlc) - ticker_value["ohlc"] = ticker_value["ohlc"][ - ~ticker_value["ohlc"].index.duplicated(keep="first") - ] + ticker_value["ohlc"] = ticker_value["ohlc"][~ticker_value["ohlc"].index.duplicated(keep="first")] if ticker_value["initial_price"] is None: ticker_value["initial_price"] = current_price @@ -90,12 +87,10 @@ def process_ticker(self, ticker, ticker_data, current_price): # Store the OHLC data in a folder called `em_storage` with each file stored as a csv document csv_storage = CSVStorage(save_dir="em-polygon-storage") # Our streamer will be Polygon and the broker will be Harvest's paper trader. My secret keys are stored in `polygon-secret.yaml` - polygon = PolygonStreamer( - path="accounts/polygon-secret.yaml", is_basic_account=True - ) + polygon = PolygonBroker(path="accounts/polygon-secret.yaml", is_basic_account=True) paper = PaperBroker() em_algo = EMAlgo() - trader = LiveTrader(streamer=polygon, broker=paper, storage=csv_storage, debug=True) + trader = BrokerHub(streamer=polygon, broker=paper, storage=csv_storage, debug=True) trader.set_algo(em_algo) diff --git a/examples/options.py b/examples/options.py index a0da405a..cb296285 100644 --- a/examples/options.py +++ b/examples/options.py @@ -1,9 +1,9 @@ # HARVEST_SKIP +import datetime as dt + from harvest.algo import BaseAlgo +from harvest.broker.robinhood import RobinhoodBroker from harvest.trader import Trader -from harvest.api.robinhood import Robinhood - -import datetime as dt """This algorithm trades options every 5 minutes. To keep things simple, the logic is very basic, with emphasis on @@ -22,7 +22,6 @@ def setup(self): self.buy_qty = 0 def main(self): - price = self.get_asset_price() if not self.hold: @@ -40,9 +39,7 @@ def eval_buy(self, price): # Sort so the earliest expiration date is first dates.sort() # Filter out expiration dates that within 5 days (since they are VERY risky) - dates = filter( - lambda x: x > self.timestamp.date() + dt.timedelta(days=5), dates - ) + dates = filter(lambda x: x > self.timestamp.date() + dt.timedelta(days=5), dates) # Get the option chain chain = self.get_option_chain("TWTR", dates[0]) # Strike price should be greater than current price @@ -67,7 +64,7 @@ def eval_buy(self, price): if __name__ == "__main__": - t = Trader(Robinhood()) + t = Trader(RobinhoodBroker()) t.set_algo(Option()) t.start() diff --git a/examples/simulation.py b/examples/simulation.py index c5d4272e..f414e813 100644 --- a/examples/simulation.py +++ b/examples/simulation.py @@ -1,8 +1,6 @@ # HARVEST_SKIP from harvest.algo import BaseAlgo from harvest.trader import PaperTrader -from harvest.api.robinhood import Robinhood -from harvest.api.paper import PaperBroker """ """ diff --git a/examples/storage.py b/examples/storage.py index 39680518..71d628a7 100644 --- a/examples/storage.py +++ b/examples/storage.py @@ -2,11 +2,12 @@ """ This code uses the pickle storage class in action. """ + from harvest.algo import BaseAlgo -from harvest.trader import Trader -from harvest.api.yahoo import YahooStreamer -from harvest.api.paper import PaperBroker +from harvest.broker.paper import PaperBroker +from harvest.broker.yahoo import YahooBroker from harvest.storage import PickleStorage +from harvest.trader import Trader class Watch(BaseAlgo): @@ -15,9 +16,8 @@ def main(self): if __name__ == "__main__": - # Fetch data from yfinance package. - streamer = YahooStreamer() + streamer = YahooBroker() # A fake broker that simulates buying and selling assets. broker = PaperBroker() diff --git a/harvest/algo.py b/harvest/algo.py index c44cc0f3..689edbc6 100644 --- a/harvest/algo.py +++ b/harvest/algo.py @@ -1,17 +1,20 @@ -# Builtins -from datetime import timedelta, timezone -import datetime as dt -from typing import Any, List, Tuple import math +from datetime import timezone +from typing import List, Tuple -# External libraries -from finta import TA import numpy as np import pandas as pd +from finta import TA -from harvest.definitions import * -from harvest.utils import * +from harvest.enum import Interval from harvest.plugin._base import Plugin +from harvest.util.date import convert_input_to_datetime, datetime_utc_to_local, pandas_timestamp_to_local +from harvest.util.helper import ( + debugger, + interval_string_to_enum, + mark_up, + symbol_type, +) """ Algo class is the main interface between users and the program. @@ -43,7 +46,7 @@ def config(self): - aggregations: A List of strings specifying the intervals to aggregate data. Choose from "1MIN", "5MIN", "15MIN", "30MIN", "1HR", "1DAY". - watchlist: A List of strings specifying the stock/crypto assets this algorithm tracks. Crypto assets must be prepended with a '@' symbol. - Any parameters set to None or an empty List will fall back to respective paramters set in the Trader class. + Any parameters set to None or an empty List will fall back to respective parameters set in the Trader class. Example ```python @@ -77,9 +80,7 @@ def add_plugin(self, plugin: Plugin): if value is None: setattr(self, plugin.name, plugin) else: - debugger.error( - f"Plugin name is already in use! {plugin.name} points to {value}." - ) + debugger.error(f"Plugin name is already in use! {plugin.name} points to {value}.") ############ Functions interfacing with broker through the trader ################# @@ -334,9 +335,7 @@ def rsi( list to perform calculations and ignore other parameters. defaults to None :returns: A list in numpy format, containing RSI values """ - symbol, interval, ref, prices = self._default_param( - symbol, interval, ref, prices - ) + symbol, interval, ref, prices = self._default_param(symbol, interval, ref, prices) if len(prices) < period: debugger.warning("Not enough data to calculate RSI, returning None") @@ -370,9 +369,7 @@ def sma( list to perform calculations and ignore other parameters. defaults to None :returns: A list in numpy format, containing SMA values """ - symbol, interval, ref, prices = self._default_param( - symbol, interval, ref, prices - ) + symbol, interval, ref, prices = self._default_param(symbol, interval, ref, prices) if len(prices) < period: debugger.warning("Not enough data to calculate SMA, returning None") @@ -406,9 +403,7 @@ def ema( list to perform calculations and ignore other parameters. defaults to None :returns: A list in numpy format, containing EMA values """ - symbol, interval, ref, prices = self._default_param( - symbol, interval, ref, prices - ) + symbol, interval, ref, prices = self._default_param(symbol, interval, ref, prices) if len(prices) < period: debugger.warning("Not enough data to calculate EMA, returning None") @@ -444,9 +439,7 @@ def bbands( list to perform calculations and ignore other parameters. defaults to None :returns: A tuple of numpy lists, each a list of BBand top, average, and bottom values """ - symbol, interval, ref, prices = self._default_param( - symbol, interval, ref, prices - ) + symbol, interval, ref, prices = self._default_param(symbol, interval, ref, prices) if len(prices) < period: debugger.warning("Not enough data to calculate BBands, returning None") @@ -461,9 +454,7 @@ def bbands( } ) - t, m, b = TA.BBANDS( - ohlc, period=period, std_multiplier=dev, MA=TA.SMA(ohlc, period) - ).T.to_numpy() + t, m, b = TA.BBANDS(ohlc, period=period, std_multiplier=dev, MA=TA.SMA(ohlc, period)).T.to_numpy() return t, m, b def crossover(self, prices_0, prices_1): @@ -476,16 +467,12 @@ def crossover(self, prices_0, prices_1): :raises Exception: If either or both price list has less than 2 values """ if len(prices_0) < 2 or len(prices_1) < 2: - raise Exception( - "There must be at least 2 datapoints to calculate crossover" - ) + raise Exception("There must be at least 2 datapoints to calculate crossover") return prices_0[-2] < prices_1[-2] and prices_0[-1] > prices_1[-1] ############### Getters for Trader properties ################# - def get_asset_quantity( - self, symbol: str = None, include_pending_buy=True, include_pending_sell=False - ) -> float: + def get_asset_quantity(self, symbol: str = None, include_pending_buy=True, include_pending_sell=False) -> float: """Returns the quantity owned of a specified asset. :param str? symbol: Symbol of asset. defaults to first symbol in watchlist @@ -497,9 +484,7 @@ def get_asset_quantity( if symbol is None: symbol = self.watchlist[0] - return self.func.get_asset_quantity( - symbol, include_pending_buy, include_pending_sell - ) + return self.func.get_asset_quantity(symbol, include_pending_buy, include_pending_sell) def get_asset_avg_cost(self, symbol: str = None) -> float: """Returns the average cost of a specified asset. @@ -533,9 +518,7 @@ def get_asset_current_price(self, symbol: str = None) -> float: return p.current_price * p.multiplier return self.func.fetch_option_market_data(symbol)["price"] * 100 - def get_asset_price_list( - self, symbol: str = None, interval: str = None, ref: str = "close" - ): + def get_asset_price_list(self, symbol: str = None, interval: str = None, ref: str = "close"): """Returns a list of recent prices for an asset. This function is not compatible with options. @@ -583,9 +566,7 @@ def get_asset_current_candle(self, symbol: str, interval=None) -> pd.DataFrame() debugger.warning("Candles not available for options") return None - def get_asset_candle_list( - self, symbol: str = None, interval=None - ) -> pd.DataFrame(): + def get_asset_candle_list(self, symbol: str = None, interval=None) -> pd.DataFrame(): """Returns the candles of an asset as a pandas DataFrame This function is not compatible with options. diff --git a/harvest/api/kraken.py b/harvest/api/kraken.py deleted file mode 100644 index b4f21c32..00000000 --- a/harvest/api/kraken.py +++ /dev/null @@ -1,463 +0,0 @@ -# Builtins -import yaml -import datetime as dt -from typing import Any, Dict, List, Tuple - -# External libraries -import krakenex -import pandas as pd - -# Submodule imports -from harvest.api._base import API -from harvest.utils import * -from harvest.definitions import * - - -class Kraken(API): - - interval_list = [Interval.MIN_1, Interval.MIN_5, Interval.HR_1, Interval.DAY_1] - req_keys = ["kraken_api_key", "kraken_secret_key"] - - crypto_ticker_to_kraken_names = { - "BTC": "XXBTZ", - "ETH": "XETH", - "ADA": "ADA", - "USDT": "USDT", - "XRP": "XXRP", - "SOL": "SOL", - "DOGE": "XDG", - "DOT": "DOT", - "USDC": "USDC", - "UNI": "UNI", - "LTC": "XLTC", - "LINK": "LINK", - "BCH": "BCH", - "FIL": "FIL", - "MATIC": "MATIC", - "WBTC": "WBTC", - "ETC": "XETC", - "XLM": "XXLM", - "TRX": "TRX", - "DAI": "DAI", - "EOS": "EOS", - "ATOM": "ATOM", - "AAVE": "AAVE", - "XMR": "XXMR", - "AXS": "AXS", - "GRT": "GRT", - "XTZ": "XXTZ", - "ALGO": "ALGO", - "MKR": "MKR", - "KSM": "KSM", - "WAVE": "WAVE", - "COMP": "COMP", - "DASH": "DASH", - "CHZ": "CHZ", - "ZEC": "XZEC", - "MANA": "MANA", - "ENJ": "ENJ", - "SUSHI": "SUSHI", - "YFI": "YFI", - "QTUM": "QTUM", - "FLOW": "FLOW", - "SNX": "SNX", - "BAT": "BAT", - "SC": "SC", - "ICX": "ICX", - "PERP": "PERP", - "BNT": "BNT", - "OMG": "OMG", - "CRV": "CRV", - "ZRX": "ZRX", - "NANO": "NANO", - "ANKR": "ANKR", - "SAND": "SAND", - "REN": "REN", - "KAVA": "KAVA", - "MINA": "MINA", - "1INCH": "1INCH", - "GHST": "GHST", - "ANT": "ANT", - "REP": "XREP", - "REPV2": "XREPV2", - "BADGER": "BADGER", - "BAL": "BAL", - "BAND": "BAND", - "CTSI": "CTSI", - "CQT": "CQT", - "EWT": "EWT", - "MLN": "XMLN", - "ETH2": "ETH2", - "GNO": "GNO", - "INJ": "INJ", - "KAR": "KAR", - "KEEP": "KEEP", - "KNC": "KNC", - "LSK": "LSK", - "LTP": "LTP", - "LRC": "LRC", - "MIR": "MIR", - "OCEAN": "OCEAN", - "PAXG": "PAXG", - "RARI": "RARI", - "REN": "REN", - "XRP": "XXRP", - "SRM": "SRM", - "STORJ": "STORJ", - "TBTC": "TBTC", - "OGN": "OGN", - "OXT": "OXT", - } - - kraken_names_to_crypto_ticker = { - v: k for k, v in crypto_ticker_to_kraken_names.items() - } - - def __init__(self, path: str = None) -> None: - super().__init__(path) - - if self.config is None: - raise Exception( - f"Account credentials not found! Expected file path: {path}" - ) - - self.api = krakenex.API( - self.config["kraken_api_key"], self.config["kraken_secret_key"] - ) - - def setup( - self, stats: Stats, account: Account, trader_main: Callable = None - ) -> None: - super().setup(stats, account, trader_main) - self.watch_crypto = [] - for sym in self.stats.watchlist_cfg: - if is_crypto(sym): - self.watch_crypto.append(sym) - else: - debugger.warning(f"Kraken does not support stocks. Ignoring {sym}.") - - self.option_cache = {} - - def main(self) -> None: - df_dict = {} - df_dict.update(self._fetch_latest_crypto_price()) - - self.trader_main(df_dict) - - def exit(self) -> None: - self.option_cache = {} - - # -------------- Streamer methods -------------- # - - @API._exception_handler - def get_current_time(self) -> dt.datetime: - ret = self._get_result(self.api.query_public("Time")) - return dt.datetime.fromtimestamp(ret["unixtime"], dt.timezone.utc) - - @API._exception_handler - def fetch_price_history( - self, - symbol: str, - interval: Interval, - start: Union[str, dt.datetime] = None, - end: Union[str, dt.datetime] = None, - ) -> pd.DataFrame: - - debugger.debug(f"Fetching {symbol} {interval} price history") - - start = convert_input_to_datetime(start) - end = convert_input_to_datetime(end) - - if start is None: - start = now() - dt.timedelta(hours=12) - if end is None: - end = now() - - if start >= end: - return pd.DataFrame() - - if interval not in self.interval_list: - raise Exception( - f"Interval {interval} not in interval list. Possible options are: {self.interval_list}" - ) - val, unit = expand_interval(interval) - df = self._get_data_from_kraken(symbol, val, unit, start, end) - - return df - - def fetch_chain_info(self, symbol: str) -> None: - raise NotImplementedError("Kraken does not support options.") - - def fetch_chain_data(self, symbol: str, date: dt.datetime) -> None: - raise NotImplementedError("Kraken does not support options.") - - def fetch_option_market_data(self, occ_symbol: str) -> None: - raise NotImplementedError("Kraken does not support options.") - - def fetch_market_hours(self, date: datetime.date) -> None: - # Crypto markets are always open. - ret = self._get_result(self.api.query_public("SystemStatus")) - return { - "is_open": ret["status"] == "online", - "open_at": None, - "close_at": None, - } - - # ------------- Broker methods ------------- # - - @API._exception_handler - def fetch_stock_positions(self) -> List: - debugger.error("Kraken does not support stocks. Returning an empty list.") - return [] - - @API._exception_handler - def fetch_option_positions(self) -> List: - debugger.error("Kraken does not support options") - return [] - - @API._exception_handler - def fetch_crypto_positions(self) -> List[Dict[str, Any]]: - positions = self._get_result(self.api.query_private("OpenPositions")) - - def fmt(crypto: Dict[str, Any]): - # Remove the currency - symbol = crypto["pair"][:-4] - # Convert from kraken name to crypto currency ticker - symbol = kraken_name_to_crypto_ticker.get(symbol) - return { - "symbol": "@" + symbol, - "avg_price": float(crypto["cost"]) / float(crypto["vol"]), - "quantity": float(crypto["vol"]), - "kraken": crypto, - } - - return [fmt(pos) for pos in positions] - - def update_option_positions(self, positions: List[Any]): - debugger.error("Kraken does not support options. Doing nothing.") - - @API._exception_handler - def fetch_account(self) -> Dict[str, Any]: - account = self._get_result(self.api.query_private("Balance")) - if account is None: - equity = 0 - cash = 0 - else: - equity = sum(float(v) for k, v in account.items() if k != "ZUSD") - cash = account.get("ZUSD", 0) - return { - "equity": equity, - "cash": cash, - "buying_power": equity + cash, - "multiplier": 1, - "kraken": account, - } - - def fetch_stock_order_status(self, order_id: str) -> None: - return NotImplementedError("Kraken does not support stocks.") - - def fetch_option_order_status(self, order_id: str) -> None: - raise Exception("Kraken does not support options.") - - @API._exception_handler - def fetch_crypto_order_status(self, order_id: str) -> Dict[str, Any]: - order = self.api.query_private("QueryOrders", {"txid": order_id}) - symbol = kraken_names_to_crypto_ticker.get(crypto["descr"]["pair"][:-4]) - return { - "type": "CRYPTO", - "symbol": "@" + symbol, - "id": crypto.key(), - "quantity": float(crypto["vol"]), - "filled_quantity": float(crypto["vol_exec"]), - "side": crypto["descr"]["type"], - "time_in_force": None, - "status": crypto["status"], - "kraken": crypto, - } - - # --------------- Methods for Trading --------------- # - - @API._exception_handler - def fetch_order_queue(self) -> Dict[str, Any]: - open_orders = self._get_result(self.api.query_private("OpenOrders")) - open_orders = open_orders["open"] - - def fmt(crypto: Dict[str, Any]): - symbol = kraken_names_to_crypto_ticker.get(crypto["descr"]["pair"][:-4]) - return { - "type": "CRYPTO", - "symbol": "@" + symbol, - "id": crypto.key(), - "quantity": float(crypto["vol"]), - "filled_quantity": float(crypto["vol_exec"]), - "side": crypto["descr"]["type"], - "time_in_force": None, - "status": crypto["status"], - "kraken": crypto, - } - - return [fmt(order) for order in open_orders] - - def order_crypto_limit( - self, - side: str, - symbol: str, - quantity: float, - limit_price: float, - in_force: str = "gtc", - extended: bool = False, - ) -> Dict[str, Any]: - - self._validate_order(side, quantity, limit_price) - - kraken_symbol = self._ticker_to_kraken(symbol) - order = self._get_result( - self.api.query_private( - "AddOrder", - { - "ordertype": "limit", - "type": side, - "volume": quantity, - "price": limit_price, - "pair": "XXBTZUSD", # symbol, - }, - ) - ) - - return { - "type": "CRYPTO", - "id": order["txid"], - "symbol": symbol, - "kraken": order, - } - - def cancel_crypto_order(self, order_id) -> None: - self.api.query_private("CancelOrder", {"txid": order_id}) - - # ------------- Helper methods ------------- # - - def create_secret(self) -> Dict[str, str]: - import harvest.wizard as wizard - - w = wizard.Wizard() - - w.println("Hmm, looks like you haven't set up an api key for Kraken.") - should_setup = w.get_bool("Do you want to set it up now?", default="y") - - if not should_setup: - w.println("You can't use Kraken without an API key.") - w.println( - "You can set up the credentials manually, or use other streamers." - ) - return False - - w.println("Alright! Let's get started") - - have_account = w.get_bool("Do you have an Kraken account?", default="y") - if not have_account: - w.println( - "In that case you'll first need to make an account. This takes a few steps." - ) - w.println( - "First visit: https://www.kraken.com/sign-up and sign up. Hit Enter or Return for the next step." - ) - w.wait_for_input() - w.println( - "Create an account, go to your account dropdown > Security > API and create an API key." - ) - w.wait_for_input() - - api_key_id = w.get_string("Enter your API key ID") - secret_key = w.get_password("Enter your API secret key") - - return {"kraken_api_key": f"{api_key_id}", "kraken_secret_key": f"{secret_key}"} - - def _get_data_from_kraken( - self, - symbol: str, - multiplier: int, - timespan: str, - start: dt.datetime, - end: dt.datetime, - ) -> pd.DataFrame: - if timespan == "MIN": - multiplier *= 1 - elif timespan == "HR": - multiplier *= 60 - elif timespan == "DAY": - multiplier *= 1440 - - if is_crypto(symbol): - temp_symbol = self._ticker_to_kraken(symbol) - else: - raise Exception("Kraken does not support stocks.") - bars = self._get_result( - self.api.query_public( - "OHLC", - {"pair": temp_symbol, "interval": multiplier, "since": 0}, - ) - ) - - df = pd.DataFrame( - bars[temp_symbol], - columns=[ - "timestamp", - "open", - "high", - "low", - "close", - "vwap", - "volume", - "count", - ], - ) - - df = self._format_df(df, symbol) - df = df.loc[start:end] - return df - - def _format_df(self, df: pd.DataFrame, symbol: str) -> pd.DataFrame: - df = df[["timestamp", "open", "high", "low", "close", "volume"]].astype(float) - df.index = pd.DatetimeIndex( - pd.to_datetime(df["timestamp"].astype(int), unit="s", utc=True), - tz=dt.timezone.utc, - ) - df = df.drop(columns=["timestamp"]) - df.columns = pd.MultiIndex.from_product([[symbol], df.columns]) - - return df.dropna() - - def _ticker_to_kraken(self, ticker: str) -> str: - if not is_crypto(ticker): - raise Exception("Kraken does not support stocks.") - - if ticker[1:] in self.crypto_ticker_to_kraken_names: - # Currently Harvest supports trades for USD and not other currencies. - kraken_ticker = self.crypto_ticker_to_kraken_names.get(ticker[1:]) + "USD" - asset_pairs = self._get_result(self.api.query_public("AssetPairs")).keys() - if kraken_ticker in asset_pairs: - return kraken_ticker - else: - raise Exception(f"{kraken_ticker} is not a valid asset pair.") - else: - raise Exception(f"Kraken does not support ticker {ticker}.") - - def _get_result(self, response: Dict[str, Any]) -> Dict[str, Any]: - """Given a kraken response from an endpoint, either raise an error if an - error exists or return the data in the results key. - """ - if len(response["error"]) > 0: - raise Exception("\n".join(response["error"])) - return response.get("result", None) - - @API._exception_handler - def _fetch_latest_crypto_price(self) -> Dict[str, pd.DataFrame]: - dfs = {} - for symbol in self.watch_crypto: - dfs[symbol] = self.fetch_price_history( - symbol, - self.interval[symbol]["interval"], - now() - dt.timedelta(days=7), - now(), - ).iloc[[-1]] - return dfs diff --git a/harvest/api/__init__.py b/harvest/broker/__init__.py similarity index 100% rename from harvest/api/__init__.py rename to harvest/broker/__init__.py diff --git a/harvest/api/_base.py b/harvest/broker/_base.py similarity index 69% rename from harvest/api/_base.py rename to harvest/broker/_base.py index 3ad9ed9d..0c5cfdf4 100644 --- a/harvest/api/_base.py +++ b/harvest/broker/_base.py @@ -1,31 +1,38 @@ -# Builtins import datetime as dt -import time -from pathlib import Path -import yaml -import traceback +import inspect import threading -from typing import Any, Callable, Dict, List, Tuple, Union +import time from os.path import exists +from typing import Any, Callable, Dict, List, Tuple, Union -# External libraries import pandas as pd +import yaml from rich.status import Status -# Submodule imports -from harvest.utils import * -from harvest.definitions import * - - -class API: +from harvest.definitions import Account, Stats +from harvest.enum import Interval +from harvest.util.helper import ( + check_interval, + data_to_occ, + debugger, + expand_interval, + interval_enum_to_string, + interval_to_timedelta, + occ_to_data, + symbol_type, + utc_current_time, +) + + +class Broker: """ - The API class communicates with various API endpoints to perform the - necessary operations. The Base class defines the interface for all API classes to - extend and implement. + The Broker defines the interface for all brokers. + Broker classes communicate with various API endpoints to perform operations like + fetching historical data and placing orders. Attributes - :interval_list: A list of supported intervals. - :exchange: The market the API trades on. Ignored if the API is not a broker. + :interval_list: A list of intervals that the broker supports. + :exchange: The market the API trades on. Ignored if the API cannot place orders. """ # List of supported intervals @@ -39,7 +46,7 @@ class API: ] # Name of the exchange this API trades on exchange = "" - # List of attributes that are required to be in the secret file + # List of attributes that are required to be in the secret file, e.g. 'api_key' req_keys = [] def __init__(self, path: str = None) -> None: @@ -47,11 +54,9 @@ def __init__(self, path: str = None) -> None: Performs initializations of the class, such as setting the timestamp and loading credentials. - There are three API class types, 'streamer', 'broker', and 'both'. A - 'streamer' is responsible for fetching data and interacting with - the queue to store data. A 'broker' is used solely for buying and - selling stocks, cryptos and options. Finally, 'both' is used to - indicate that the broker fetch data and buy and sell stocks. + A broker can retrieve stock/account data, place orders, or both. + Usually a broker can do both, but some brokers may only be able to + place orders (such as PaperBroker), or only retrieve data. All subclass implementations should call this __init__ method using `super().__init__(path)`. @@ -63,11 +68,11 @@ def __init__(self, path: str = None) -> None: if path is None: path = "./secret.yaml" + # Check if file exists. If not, create a secret file if not exists(path): config = self.create_secret() else: - # Open file with open(path, "r") as stream: config = yaml.safe_load(stream) # Check if the file contains all the required parameters @@ -79,40 +84,40 @@ def __init__(self, path: str = None) -> None: self.config = config - def setup( - self, stats: Stats, account: Account, trader_main: Callable = None - ) -> None: + def setup(self, stats: Stats, account: Account, broker_hub_cb: Callable = None) -> None: """ This function is called right before the algorithm begins, and initializes several runtime parameters like the symbols to watch and what interval data is needed. - :trader_main: A callback function to the trader which will pass the data to the algorithms. + :stats: The Stats object that contains the watchlist and other configurations. + :account: The Account object that contains the user's account information. + :broker_hub_cb: The callback function that the broker calls every time it fetches new data. """ - self.trader_main = trader_main + self.broker_hub_cb = broker_hub_cb self.stats = stats - self.stats.timestamp = now() + self.stats.timestamp = utc_current_time() self.account = account min_interval = None for sym in stats.watchlist_cfg: inter = stats.watchlist_cfg[sym]["interval"] - # If the specified interval is not supported on this API, raise Exception if inter < self.interval_list[0]: raise Exception(f"Specified interval {inter} is not supported.") - # If the exact inteval is not supported but it can be recreated by aggregating + # If the exact interval is not supported, see if it can be recreated by aggregating # candles from a more granular interval if inter not in self.interval_list: granular_int = [i for i in self.interval_list if i < inter] + if not granular_int: + raise Exception( + f"Specified interval {inter} is not supported, and cannot be recreated by aggregating from a more granular interval either." + ) new_inter = granular_int[-1] stats.watchlist_cfg[sym]["aggregations"].append(inter) stats.watchlist_cfg[sym]["interval"] = new_inter - if ( - min_interval is None - or stats.watchlist_cfg[sym]["interval"] < min_interval - ): + if min_interval is None or stats.watchlist_cfg[sym]["interval"] < min_interval: min_interval = stats.watchlist_cfg[sym]["interval"] self.poll_interval = min_interval @@ -122,79 +127,71 @@ def setup( def _poll_sec(self, interval_sec) -> None: """ - This function is called by the main thread to poll the API for - new data. + This function is called by the main thread to poll the Broker every second. """ - status = Status( - f"Waiting for next interval... ({val} {unit})", spinner="material" - ) + status = Status(f"Waiting for next interval... ({self.poll_interval})", spinner="material") status.start() cur_sec = -1 while 1: - cur = now() + cur = utc_current_time() sec = cur.second if sec % interval_sec == 0 and sec != cur_sec: cur_sec = sec self.stats.timestamp = cur status.stop() - self.main() + self.step() status.start() def _poll_min(self, interval_min): """ - This function is called by the main thread to poll the API for - new data. + This function is called by the main thread to poll the Broker for new data every minute. """ - status = Status( - f"Waiting for next interval... ({val} {unit})", spinner="material" - ) + status = Status(f"Waiting for next interval... ({self.poll_interval})", spinner="material") status.start() cur_min = -1 sleep = interval_min * 60 - 10 while 1: - cur = now() + cur = utc_current_time() minute = cur.minute if minute % interval_min == 0 and minute != cur_min: self.stats.timestamp = cur status.stop() - self.main() + self.step() status.start() time.sleep(sleep) cur_min = minute def _poll_hr(self, interval_hr): """ - This function is called by the main thread to poll the API for - new data. + This function is called by the main thread to poll the Broker for new data every hour. """ - status = Status( - f"Waiting for next interval... ({interval_hr} HR)", spinner="material" - ) + status = Status(f"Waiting for next interval... ({self.poll_interval})", spinner="material") status.start() cur_min = -1 sleep = interval_hr * 3600 - 60 while 1: - cur = now() + cur = utc_current_time() minutes = cur.minute hours = cur.hour if hours % interval_hr == 0 and minutes == 0 and minutes != cur_min: self.stats.timestamp = cur status.stop() - self.main() + self.step() status.start() time.sleep(sleep) cur_min = minutes def _poll_day(self, interval_day): - status = Status( - f"Waiting for next interval... ({interval_hr} HR)", spinner="material" - ) + """ + This function is called by the main thread to poll the Broker for new data every day. + """ + status = Status(f"Waiting for next interval... ({self.poll_interval})", spinner="material") status.start() cur_min = -1 cur_day = -1 # market_data = self.fetch_market_hours(now()) while 1: - cur = now() + cur = utc_current_time() minutes = cur.minute hours = cur.hour day = cur.day @@ -207,28 +204,23 @@ def _poll_day(self, interval_day): closes_min = closes_at.minute is_open = market_data["is_open"] - if ( - is_open - and hours == closes_hr - and minutes == closes_min - and minutes != cur_min - ): + if is_open and hours == closes_hr and minutes == closes_min and minutes != cur_min: self.stats.timestamp = cur status.stop() - self.main() + self.step() status.start() time.sleep(80000) - market_data = self.fetch_market_hours(now()) + market_data = self.fetch_market_hours(utc_current_time()) cur_min = minutes def start(self) -> None: """ - This method begins streaming data from the API. + This method begins streaming data from the Broker. The default implementation below is for polling the API. - If your brokerage provides a streaming API, you should override - this method and configure it to use that API. In that case, - make sure to set the callback function to self.main(). + If a brokerage provides a streaming API, this method should be overridden + to use the streaming API. + Make sure to call self.step() in the overridden method. """ val, unit = expand_interval(self.poll_interval) debugger.debug(f"{type(self).__name__} started...") @@ -244,7 +236,7 @@ def start(self) -> None: else: raise Exception(f"Unsupported interval {self.poll_interval}.") - def main(self) -> None: + def step(self) -> None: """ This method is called at the interval specified by the user. It should create a dictionary where each key is the symbol for an asset, @@ -256,38 +248,34 @@ def main(self) -> None: timestamp should be an offset-aware datetime object in UTC timezone. - The dictionary should be passed to the trader by calling `self.trader_main(dict)` + The dictionary should be passed to the trader by calling `self.broker_hub_cb()` """ # Iterate through securities in the watchlist. For those that have # intervals that needs to be called now, fetch the latest data - df_dict = {} for sym in self.stats.watchlist_cfg: inter = self.stats.watchlist_cfg[sym]["interval"] - if is_freq(self.stats.timestamp, inter): + if check_interval(self.stats.timestamp, inter): n = self.stats.timestamp - latest = self.fetch_price_history( - sym, inter, n - interval_to_timedelta(inter) * 2, n - ) + latest = self.fetch_price_history(sym, inter, n - interval_to_timedelta(inter) * 2, n) debugger.debug(f"{sym} price fetch returned: {latest}") if latest is None or latest.empty: continue df_dict[sym] = latest.iloc[[-1]] - self.trader_main(df_dict) + self.broker_hub_cb(df_dict) def exit(self) -> None: """ - This function is called after every invocation of algo's handler. - The intended purpose is for brokers to clear any cache it may have created. + Exit the broker. """ debugger.debug(f"{type(self).__name__} exited") def create_secret(self) -> Dict[str, str]: """ - This method is called when the yaml file with credentials - is not found. It returns a dictionary containing the necessary credentials. + This method is called when the yaml file with credentials is not found. + Each broker should implement a wizard to instruct users on how to create the necessary credentials. """ debugger.warning("Assuming API does not need account information.") @@ -298,10 +286,11 @@ def refresh_cred(self) -> None: """ debugger.info(f"Refreshing credentials for {type(self).__name__}.") - # -------------- Streamer methods -------------- # - def get_current_time(self) -> dt.datetime: - return now() + """ + Returns the current time in UTC timezone, accurate to the minute. + """ + return utc_current_time() def fetch_price_history( self, @@ -322,10 +311,15 @@ def fetch_price_history( :returns: A pandas dataframe, same format as main() """ raise NotImplementedError( - f"{type(self).__name__} does not support this streamer method: `fetch_price_history`." + f"{type(self).__name__} class does not support the method {inspect.currentframe().f_code.co_name}." ) def fetch_latest_price(self, symbol: str) -> float: + """ + Fetches the latest price of the specified asset. + + :param symbol: The stock/crypto to get data for. Note options are not supported. + """ interval = self.poll_interval end = self.get_current_time() start = end - interval_to_timedelta(interval) * 12 @@ -343,12 +337,10 @@ def fetch_chain_info(self, symbol: str) -> Dict[str, Any]: - multiplier: Multiplier of the option, usually 100 """ raise NotImplementedError( - f"{type(self).__name__} does not support this streamer method: `fetch_chain_info`." + f"{type(self).__name__} class does not support the method {inspect.currentframe().f_code.co_name}." ) - def fetch_chain_data( - self, symbol: str, date: Union[str, dt.datetime] - ) -> pd.DataFrame: + def fetch_chain_data(self, symbol: str, date: Union[str, dt.datetime]) -> pd.DataFrame: """ Returns the option chain for the specified symbol. @@ -362,7 +354,7 @@ def fetch_chain_data( exp_date should be a timezone-aware datetime object localized to UTC """ raise NotImplementedError( - f"{type(self).__name__} does not support this streamer method: `fetch_chain_data`." + f"{type(self).__name__} class does not support the method {inspect.currentframe().f_code.co_name}." ) def fetch_option_market_data(self, symbol: str) -> Dict[str, Any]: @@ -376,7 +368,7 @@ def fetch_option_market_data(self, symbol: str) -> Dict[str, Any]: - bid: bid price """ raise NotImplementedError( - f"{type(self).__name__} does not support this streamer method: `fetch_option_market_data`." + f"{type(self).__name__} class does not support the method {inspect.currentframe().f_code.co_name}." ) def fetch_market_hours(self, date: dt.date) -> Dict[str, Any]: @@ -389,9 +381,6 @@ def fetch_market_hours(self, date: dt.date) -> Dict[str, Any]: - open_at: Time the market opens in UTC timezone. - close_at: Time the market closes in UTC timezone. """ - # raise NotImplementedError( - # f"{type(self).__name__} does not support this broker method: `fetch_market_hours`." - # ) return {"is_open": True, "open_at": None, "close_at": None} # ------------- Broker methods ------------- # @@ -406,7 +395,7 @@ def fetch_stock_positions(self) -> List[Dict[str, Any]]: - quantity: Quantity owned """ debugger.error( - f"{type(self).__name__} does not support this broker method: `fetch_stock_positions`. Returning an empty list." + f"{type(self).__name__} class does not support the method {inspect.currentframe().f_code.co_name}. Returning an empty list." ) return [] @@ -425,7 +414,7 @@ def fetch_option_positions(self) -> List[Dict[str, Any]]: - type: 'call' or 'put' """ debugger.error( - f"{type(self).__name__} does not support this broker method: `fetch_option_positions`. Returning an empty list." + f"{type(self).__name__} class does not support the method {inspect.currentframe().f_code.co_name}. Returning an empty list." ) return [] @@ -439,23 +428,10 @@ def fetch_crypto_positions(self) -> List[Dict[str, Any]]: - quantity: Quantity owned """ debugger.error( - f"{type(self).__name__} does not support this broker method: `fetch_crypto_positions`. Returning an empty list." + f"{type(self).__name__} class does not support the method {inspect.currentframe().f_code.co_name}. Returning an empty list." ) return [] - # def update_option_positions(self, positions: List[Any]): - # """ - # Updates entries in option_positions list with the latest option price. - # This is needed as options are priced based on various metrics, - # and cannot be easily calculated from stock prices. - - # :positions: The option_positions list in the Trader class. - # :returns: Nothing - # """ - # debugger.error( - # f"{type(self).__name__} does not support this broker method: `update_option_positions`. Doing nothing." - # ) - def fetch_account(self) -> Dict[str, float]: """ Returns current account information from the brokerage. @@ -467,7 +443,7 @@ def fetch_account(self) -> Dict[str, float]: - multiplier: Scale of leverage, if leveraging """ raise NotImplementedError( - f"{type(self).__name__} does not support this broker method: `fetch_account`." + f"{type(self).__name__} class does not support the method {inspect.currentframe().f_code.co_name}." ) def fetch_stock_order_status(self, id) -> Dict[str, Any]: @@ -488,7 +464,7 @@ def fetch_stock_order_status(self, id) -> Dict[str, Any]: - filled_price: Price the order was filled at """ raise NotImplementedError( - f"{type(self).__name__} does not support this broker method: `fetch_stock_order_status`." + f"{type(self).__name__} class does not support the method {inspect.currentframe().f_code.co_name}." ) def fetch_option_order_status(self, id) -> Dict[str, Any]: @@ -509,7 +485,7 @@ def fetch_option_order_status(self, id) -> Dict[str, Any]: - filled_price: Price the order was filled at """ raise NotImplementedError( - f"{type(self).__name__} does not support this broker method: `fetch_option_order_status`." + f"{type(self).__name__} class does not support the method {inspect.currentframe().f_code.co_name}." ) def fetch_crypto_order_status(self, id) -> Dict[str, Any]: @@ -530,7 +506,7 @@ def fetch_crypto_order_status(self, id) -> Dict[str, Any]: - filled_price: Price the order was filled at """ raise NotImplementedError( - f"{type(self).__name__} does not support this broker method: `fetch_crypto_order_status`." + f"{type(self).__name__} class does not support the method {inspect.currentframe().f_code.co_name}." ) def fetch_order_queue(self) -> List[Dict[str, Any]]: @@ -595,7 +571,7 @@ def order_stock_limit( Raises an exception if order fails. """ raise NotImplementedError( - f"{type(self).__name__} does not support this broker method: `order_stock_limit`." + f"{type(self).__name__} class does not support the method {inspect.currentframe().f_code.co_name}." ) def order_crypto_limit( @@ -623,7 +599,7 @@ def order_crypto_limit( Raises an exception if order fails. """ raise NotImplementedError( - f"{type(self).__name__} does not support this broker method: `order_crypto_limit`." + f"{type(self).__name__} class does not support the method {inspect.currentframe().f_code.co_name}." ) def order_option_limit( @@ -655,22 +631,22 @@ def order_option_limit( Raises an exception if order fails. """ raise NotImplementedError( - f"{type(self).__name__} does not support this broker method: `order_option_limit`." + f"{type(self).__name__} class does not support the method {inspect.currentframe().f_code.co_name}." ) def cancel_stock_order(self, order_id) -> None: raise NotImplementedError( - f"{type(self).__name__} does not support this broker method: `cancel_stock_order`." + f"{type(self).__name__} class does not support the method {inspect.currentframe().f_code.co_name}." ) def cancel_crypto_order(self, order_id) -> None: raise NotImplementedError( - f"{type(self).__name__} does not support this broker method: `cancel_crypto_order`." + f"{type(self).__name__} class does not support the method {inspect.currentframe().f_code.co_name}." ) def cancel_option_order(self, order_id) -> None: raise NotImplementedError( - f"{type(self).__name__} does not support this broker method: `cancel_option_order`." + f"{type(self).__name__} class does not support the method {inspect.currentframe().f_code.co_name}." ) # -------------- Built-in methods -------------- # @@ -699,13 +675,9 @@ def buy( debugger.debug(f"{type(self).__name__} ordered a buy of {quantity} {symbol}") typ = symbol_type(symbol) if typ == "STOCK": - return self.order_stock_limit( - "buy", symbol, quantity, limit_price, in_force, extended - ) + return self.order_stock_limit("buy", symbol, quantity, limit_price, in_force, extended) elif typ == "CRYPTO": - return self.order_crypto_limit( - "buy", symbol[1:], quantity, limit_price, in_force, extended - ) + return self.order_crypto_limit("buy", symbol[1:], quantity, limit_price, in_force, extended) elif typ == "OPTION": sym, exp_date, option_type, strike = self.occ_to_data(symbol) return self.order_option_limit( @@ -744,13 +716,9 @@ def sell( typ = symbol_type(symbol) if typ == "STOCK": - return self.order_stock_limit( - "sell", symbol, quantity, limit_price, in_force, extended - ) + return self.order_stock_limit("sell", symbol, quantity, limit_price, in_force, extended) elif typ == "CRYPTO": - return self.order_crypto_limit( - "sell", symbol[1:], quantity, limit_price, in_force, extended - ) + return self.order_crypto_limit("sell", symbol[1:], quantity, limit_price, in_force, extended) elif typ == "OPTION": sym, exp_date, option_type, strike = self.occ_to_data(symbol) return self.order_option_limit( @@ -777,102 +745,21 @@ def cancel(self, order_id) -> None: elif asset_type == "OPTION": self.cancel_option_order(order_id) - # def buy_option(self, symbol: str, quantity: int = 0, in_force: str = "gtc"): - # """ - # Buys the specified option. - - # :symbol: Symbol of the asset to buy, in OCC format. - # :quantity: Quantity of asset to buy - # :in_force: Duration the order is in force - - # :returns: The result of order_option_limit(). Returns None if there is an issue with the parameters. - # """ - # if quantity <= 0.0: - # debugger.error( - # f"Quantity cannot be less than or equal to 0: was given {quantity}" - # ) - # return None - # if self.trader is None: - # buy_power = self.fetch_account()["buying_power"] - # price = self.streamer.fetch_option_market_data(symbol)["price"] - # else: - # buy_power = self.trader.account["buying_power"] - # price = self.trader.streamer.fetch_option_market_data(symbol)["price"] - - # limit_price = mark_up(price) - # total_price = limit_price * quantity - - # if total_price >= buy_power: - # debugger.warning( - # "Not enough buying power.\n" + - # f"Total price ({price} * {quantity} * 1.05 = {limit_price*quantity}) exceeds buying power {buy_power}.\n" + - # "Reduce purchase quantity or increase buying power." - # ) - - # sym, date, option_type, strike = self.occ_to_data(symbol) - # return self.order_option_limit( - # "buy", - # sym, - # quantity, - # limit_price, - # option_type, - # date, - # strike, - # in_force=in_force, - # ) - - # def sell_option(self, symbol: str, quantity: int = 0, in_force: str = "gtc"): - # """ - # Sells the specified option. - - # :symbol: Symbol of the asset to buy, in OCC format. - # :quantity: Quantity of asset to buy - # :in_force: Duration the order is in force - - # :returns: The result of order_option_limit(). Returns None if there is an issue with the parameters. - # """ - # if quantity <= 0.0: - # debugger.error( - # f"Quantity cannot be less than or equal to 0: was given {quantity}" - # ) - # return None - # if self.trader is None: - # price = self.streamer.fetch_option_market_data(symbol)["price"] - # else: - # price = self.trader.streamer.fetch_option_market_data(symbol)["price"] - - # limit_price = mark_down(price) - - # debugger.debug(f"{type(self).__name__} ordered a sell of {quantity} {symbol}") - # sym, date, option_type, strike = self.occ_to_data(symbol) - # return self.order_option_limit( - # "sell", - # sym, - # quantity, - # limit_price, - # option_type, - # date, - # strike, - # in_force=in_force, - # ) - # -------------- Helper methods -------------- # def has_interval(self, interval: Interval) -> bool: return interval in self.interval_list - def data_to_occ( - self, symbol: str, date: dt.datetime, option_type: str, price: float - ) -> str: + def data_to_occ(self, symbol: str, date: dt.datetime, option_type: str, price: float) -> str: return data_to_occ(symbol, date, option_type, price) def occ_to_data(self, symbol: str) -> Tuple[str, dt.datetime, str, float]: return occ_to_data(symbol) def current_timestamp(self) -> dt.datetime: - return now() + return utc_current_time() - def _exception_handler(func: Callable) -> Callable: + def _exception_handler(self: Callable) -> Callable: """ Wrapper to handle unexpected errors in the wrapped function. Most functions should be wrapped with this to properly handle errors, such as @@ -886,40 +773,21 @@ def wrapper(*args, **kwargs): tries = 3 while tries > 0: try: - return func(*args, **kwargs) + return self(*args, **kwargs) except Exception as e: from rich.console import Console c = Console() c.print_exception(show_locals=True) # self = args[0] - # debugger.error(f"Error: {e}") + debugger.error(f"Error: {e}") # traceback.print_exc() debugger.error("Logging out and back in...") args[0].refresh_cred() tries -= 1 debugger.error("Retrying...") continue - raise Exception(f"Failed to run {func.__name__}") - - return wrapper - - def _run_once(func: Callable) -> Callable: - """ - Wrapper to only allows wrapped functions to be run once. - - :func: Function to wrap. - :returns: The return of the inputted function if it has not been run before and None otherwise. - """ - - ran = False - - def wrapper(*args, **kwargs): - nonlocal ran - if not ran: - ran = True - return func(*args, **kwargs) - return None + raise Exception(f"Failed to run {self.__name__}") return wrapper @@ -929,10 +797,18 @@ def _validate_order(self, side: str, quantity: float, limit_price: float) -> Non assert limit_price >= 0, "Limit price must be nonnegative" -class StreamAPI(API): - """ """ +class StreamBroker(Broker): + """ + Class for brokers that support streaming APIs. + Whenever possible, it is preferred to use a streaming API over polling as it helps offload + interval handling to the server. + """ def __init__(self, path: str = None) -> None: + """ + Streaming APIs often return data asynchronously, so this class additionally defines a lock to + prevent race conditions in case different data arrives close to each other. + """ super().__init__(path) # Lock for streams that receive data asynchronously. @@ -941,37 +817,40 @@ def __init__(self, path: str = None) -> None: self.first = True def start(self) -> None: + """ + Called when the broker is started. + The streaming API should be initialized here. + """ debugger.debug(f"{type(self).__name__} started...") - def main(self, df_dict: Dict[str, Any]) -> None: + def step(self, df_dict: Dict[str, Any]) -> None: """ - Streaming is event driven, so sometimes not all data comes in at once. - StreamAPI class + Called at the interval specified by the user. + This method is more complicated for streaming APIs, as data can arrive asynchronously. """ - self.block_lock.acquire() - got = [k for k in df_dict] - # First, identify which symbols need to have data fetched - # for this timestamp + self.block_lock.acquire() # Obtain lock to prevent race conditions when data arrives asynchronously + + # First, identify which symbols need to have data fetched for this timestamp + got = list(df_dict) if self.first: self.needed = [ sym for sym in self.stats.watchlist_cfg - if is_freq(now(), self.stats.watchlist_cfg[sym]["interval"]) + if check_interval(utc_current_time(), self.stats.watchlist_cfg[sym]["interval"]) ] self.stats.timestamp = df_dict[got[0]].index[0] - - debugger.debug(f"Needs: {self.needed}") - debugger.debug(f"Got data for: {got}") missing = list(set(self.needed) - set(got)) - debugger.debug(f"Still need data for: {missing}") + + debugger.debug(f"Awaiting data for: {self.needed}") + debugger.debug(f"Received data for: {got}") + debugger.debug(f"Missing data for: {missing}") self.block_queue.update(df_dict) - # debugger.debug(self.block_queue) # If all data has been received, pass on the data if len(missing) == 0: debugger.debug("All data received") - self.trader_main(self.block_queue) + self.broker_hub_cb(self.block_queue) self.block_queue = {} self.all_recv = True self.first = True @@ -990,13 +869,20 @@ def main(self, df_dict: Dict[str, Any]) -> None: self.block_lock.release() def timeout(self) -> None: + """ + Starts a timer after the first data is received for the current timestamp. + """ debugger.debug("Begin timeout timer") - time.sleep(1) + time.sleep(1) # TODO: Make it configurable if not self.all_recv: debugger.debug("Force flush") self.flush() def flush(self) -> None: + """ + Called when the timeout timer expires. + Forces data to be returned for the current timestamp. + """ # For missing data, return a OHLC with all zeroes. self.block_lock.acquire() for n in self.needed: @@ -1008,5 +894,5 @@ def flush(self) -> None: data.columns = pd.MultiIndex.from_product([[n], data.columns]) self.block_queue[n] = data self.block_lock.release() - self.trader_main(self.block_queue) + self.broker_hub_cb(self.block_queue) self.block_queue = {} diff --git a/harvest/api/alpaca.py b/harvest/broker/alpaca.py similarity index 78% rename from harvest/api/alpaca.py rename to harvest/broker/alpaca.py index 684b6387..f3c304fa 100644 --- a/harvest/api/alpaca.py +++ b/harvest/broker/alpaca.py @@ -1,23 +1,30 @@ -# Builtins -import yaml import asyncio -import threading import datetime as dt -from typing import Any, Dict, List, Tuple +import threading +from typing import Any, Callable, Dict, List, Union -# External libraries import pandas as pd -from alpaca_trade_api.rest import REST, TimeFrame, URL -from alpaca_trade_api.entity import Bar from alpaca_trade_api import Stream - -# Submodule imports -from harvest.api._base import StreamAPI, API -from harvest.utils import * -from harvest.definitions import * - - -class Alpaca(StreamAPI): +from alpaca_trade_api.entity import Bar +from alpaca_trade_api.rest import REST, URL, TimeFrame + +from harvest.broker._base import Broker, StreamBroker +from harvest.definitions import Account, Interval, Stats +from harvest.util.helper import ( + aggregate_df, + convert_input_to_datetime, + debugger, + expand_interval, + is_crypto, + utc_current_time, +) + + +class AlpacaBroker(StreamBroker): + """ + Class for the Alpaca Broker (alpaca.markets) + For live trading, Harvest will use the streaming API provided by Alpaca to get real-time data. + """ interval_list = [ Interval.MIN_1, @@ -30,23 +37,22 @@ def __init__( is_basic_account: bool = False, paper_trader: bool = False, ) -> None: + """ + Initialize the Alpaca Broker. + + :param path: Path to the account credentials file. + :param is_basic_account: The free (basic) plan uses iex for data feed, while the unlimited plan uses sip. (alpaca.markets/support/data-provider-alpaca) + :param paper_trader: Whether to use the paper trading API. + """ super().__init__(path) if self.config is None: - raise Exception( - f"Account credentials not found! Expected file path: {path}" - ) + raise Exception(f"Account credentials not found! Expected file path: {path}") self.basic = is_basic_account - endpoint = ( - "https://paper-api.alpaca.markets" - if paper_trader - else "https://api.alpaca.markets" - ) - self.api = REST( - self.config["alpaca_api_key"], self.config["alpaca_secret_key"], endpoint - ) + endpoint = "https://paper-api.alpaca.markets" if paper_trader else "https://api.alpaca.markets" + self.api = REST(self.config["alpaca_api_key"], self.config["alpaca_secret_key"], endpoint) data_feed = "iex" if self.basic else "sip" self.stream = Stream( @@ -58,9 +64,7 @@ def __init__( self.data_lock = threading.Lock() self.data = {} - def setup( - self, stats: Stats, account: Account, trader_main: Callable = None - ) -> None: + def setup(self, stats: Stats, account: Account, trader_main: Callable = None) -> None: super().setup(stats, account, trader_main) self.watch_stock = [] @@ -87,7 +91,7 @@ def exit(self) -> None: # -------------- Streamer methods -------------- # - @API._exception_handler + @Broker._exception_handler def get_current_time(self) -> dt.datetime: ret = self.api.get_clock().__dict__["_raw"] # Convert to ISO 8601 by removing nanoseconds @@ -95,7 +99,7 @@ def get_current_time(self) -> dt.datetime: timestamp = ret["timestamp"][:index] + ret["timestamp"][index + 9 :] return dt.datetime.fromisoformat(timestamp) - @API._exception_handler + @Broker._exception_handler def fetch_price_history( self, symbol: str, @@ -103,36 +107,35 @@ def fetch_price_history( start: Union[str, dt.datetime] = None, end: Union[str, dt.datetime] = None, ) -> pd.DataFrame: - debugger.debug(f"Fetching {symbol} {interval} price history") start = convert_input_to_datetime(start) end = convert_input_to_datetime(end) if start is None: - start = now() - dt.timedelta(days=365 * 5) + start = utc_current_time() - dt.timedelta(days=365 * 5) if end is None: - end = now() + end = utc_current_time() if start >= end: return pd.DataFrame() return self._get_data_from_alpaca(symbol, interval, start, end) - @API._exception_handler + @Broker._exception_handler def fetch_chain_info(self, symbol: str) -> None: raise NotImplementedError("Alpaca does not support options.") - @API._exception_handler + @Broker._exception_handler def fetch_chain_data(self, symbol: str, date: dt.datetime) -> None: raise NotImplementedError("Alpaca does not support options.") - @API._exception_handler + @Broker._exception_handler def fetch_option_market_data(self, occ_symbol: str) -> None: raise NotImplementedError("Alpaca does not support options.") - @API._exception_handler - def fetch_market_hours(self, date: datetime.date) -> Dict[str, Any]: + @Broker._exception_handler + def fetch_market_hours(self, date: dt.datetime.date) -> Dict[str, Any]: ret = self.api.get_clock().__dict__["_raw"] return { "is_open": ret["is_open"], @@ -142,7 +145,7 @@ def fetch_market_hours(self, date: datetime.date) -> Dict[str, Any]: # ------------- Broker methods ------------- # - @API._exception_handler + @Broker._exception_handler def fetch_stock_positions(self) -> List[Dict[str, Any]]: def fmt(stock: Dict[str, Any]) -> Dict[str, Any]: return { @@ -152,44 +155,34 @@ def fmt(stock: Dict[str, Any]) -> Dict[str, Any]: "alpaca": stock, } - return [ - fmt(pos.__dict__["_raw"]) - for pos in self.api.list_positions() - if pos.asset_class != "crypto" - ] + return [fmt(pos.__dict__["_raw"]) for pos in self.api.list_positions() if pos.asset_class != "crypto"] - @API._exception_handler + @Broker._exception_handler def fetch_option_positions(self) -> List: debugger.error("Alpaca does not support options. Returning an empty list.") return [] - @API._exception_handler + @Broker._exception_handler def fetch_crypto_positions(self) -> List: if self.basic: - debugger.error( - "Alpaca basic accounts do not support crypto. Returning an empty list." - ) + debugger.error("Alpaca basic accounts do not support crypto. Returning an empty list.") return [] def fmt(crypto: Dict[str, Any]) -> Dict[str, Any]: return { "symbol": "@" + crypto["symbol"], "avg_price": float(crypto["avg_entry_price"]), - "quantity": float(stock["qty"]), + "quantity": float(crypto["qty"]), "alpaca": crypto, } - return [ - fmt(pos.__dict__["_raw"]) - for pos in self.api.list_positions() - if pos.asset_class == "crypto" - ] + return [fmt(pos.__dict__["_raw"]) for pos in self.api.list_positions() if pos.asset_class == "crypto"] - @API._exception_handler + @Broker._exception_handler def update_option_positions(self, positions: List[Any]) -> None: debugger.error("Alpaca does not support options. Doing nothing.") - @API._exception_handler + @Broker._exception_handler def fetch_account(self) -> Dict[str, Any]: account = self.api.get_account().__dict__["_raw"] return { @@ -200,26 +193,23 @@ def fetch_account(self) -> Dict[str, Any]: "alpaca": account, } - @API._exception_handler + @Broker._exception_handler def fetch_stock_order_status(self, order_id: str) -> Dict[str, Any]: return self.api.get_order(order_id).__dict__["_raw"] - @API._exception_handler + @Broker._exception_handler def fetch_option_order_status(self, order_id: str) -> None: raise NotImplementedError("Alpaca does not support options.") - @API._exception_handler + @Broker._exception_handler def fetch_crypto_order_status(self, order_id: str) -> Dict[str, Any]: if self.basic: raise Exception("Alpaca basic accounts do not support crypto.") return self.api.get_order(order_id).__dict__["_raw"] - @API._exception_handler + @Broker._exception_handler def fetch_order_queue(self) -> List[Dict[str, Any]]: - return [ - self._format_order_status(pos.__dict__["_raw"]) - for pos in self.api.list_orders() - ] + return [self._format_order_status(pos.__dict__["_raw"]) for pos in self.api.list_orders()] # --------------- Methods for Trading --------------- # @@ -232,7 +222,6 @@ def order_stock_limit( in_force: str = "gtc", extended: bool = False, ) -> Dict[str, Any]: - self._validate_order(side, quantity, limit_price) order = self.api.submit_order( @@ -299,7 +288,7 @@ def order_crypto_limit( } def cancel_stock_order(self, order_id: str) -> Dict[str, Any]: - ret = self.api.cancel_order(order_id) + self.api.cancel_order(order_id) return {"id": order_id} def cancel_option_order(self, order_id: str) -> None: @@ -308,7 +297,7 @@ def cancel_option_order(self, order_id: str) -> None: def cancel_crypto_order(self, order_id: str) -> Dict[str, Any]: if self.basic: raise Exception("Alpaca basic accounts do not support crypto.") - ret = self.api.cancel_order(order_id) + self.api.cancel_order(order_id) return {"id": order_id} # ------------- Helper methods ------------- # @@ -323,21 +312,15 @@ def create_secret(self) -> Dict[str, str]: if not should_setup: w.println("You can't use Alpaca without an API key.") - w.println( - "You can set up the credentials manually, or use other streamers." - ) + w.println("You can set up the credentials manually, or use other streamers.") return False w.println("Alright! Let's get started") have_account = w.get_bool("Do you have an Alpaca account?", default="y") if not have_account: - w.println( - "In that case you'll first need to make an account. This takes a few steps." - ) - w.println( - "First visit: https://alpaca.markets/ and sign up. Hit Enter or Return for the next step." - ) + w.println("In that case you'll first need to make an account. This takes a few steps.") + w.println("First visit: https://alpaca.markets/ and sign up. Hit Enter or Return for the next step.") w.wait_for_input() w.println("Follow the setups to make an individual or buisness account.") w.wait_for_input() @@ -355,9 +338,7 @@ def create_secret(self) -> Dict[str, str]: return {"alpaca_api_key": f"{api_key_id}", "alpaca_secret_key": f"{secret_key}"} - def _format_order_status( - self, order: Dict[str, Any], is_stock: bool = True - ) -> Dict[str, Any]: + def _format_order_status(self, order: Dict[str, Any], is_stock: bool = True) -> Dict[str, Any]: return { "type": "STOCK" if is_stock else "CRYPTO", "id": order["id"], @@ -378,12 +359,10 @@ def _get_data_from_alpaca( end: dt.datetime, ) -> pd.DataFrame: if self.basic and is_crypto(symbol): - debugger.error( - "Alpaca basic accounts do not support crypto. Returning empty dataframe" - ) + debugger.error("Alpaca basic accounts do not support crypto. Returning empty dataframe") return pd.DataFrame() - current_time = now() + current_time = utc_current_time() if self.basic and start < current_time - dt.timedelta(days=365 * 5): debugger.warning( "Start time is over five years old! Only data from the past five years will be returned for basic accounts." @@ -408,9 +387,7 @@ def _get_data_from_alpaca( end_str = end.isoformat() temp_symbol = symbol[1:] if is_crypto(symbol) else symbol - bars = self.api.get_bars( - temp_symbol, TimeFrame(timespan), start_str, end_str, adjustment="raw" - ) + bars = self.api.get_bars(temp_symbol, TimeFrame(timespan), start_str, end_str, adjustment="raw") df = pd.DataFrame((bar.__dict__["_raw"] for bar in bars)) df = self._format_df(df, symbol) df = aggregate_df(df, interval) @@ -430,9 +407,7 @@ def _format_df(self, df: pd.DataFrame, symbol: str) -> pd.DataFrame: inplace=True, ) - df.index = pd.DatetimeIndex( - pd.to_datetime(df["timestamp"], utc=True), tz=dt.timezone.utc - ) + df.index = pd.DatetimeIndex(pd.to_datetime(df["timestamp"], utc=True), tz=dt.timezone.utc) df.drop(columns=["timestamp"], inplace=True) df.columns = pd.MultiIndex.from_product([[symbol], df.columns]) return df.dropna() @@ -463,6 +438,6 @@ async def _update_data(self, bar: Bar) -> None: data = self.data self.data = {} self.data_lock.release() - self.trader_main(data) + self.broker_hub_cb(data) else: self.data_lock.release() diff --git a/harvest/api/dummy.py b/harvest/broker/dummy.py similarity index 69% rename from harvest/api/dummy.py rename to harvest/broker/dummy.py index e0cd8807..2e702d6a 100644 --- a/harvest/api/dummy.py +++ b/harvest/broker/dummy.py @@ -1,23 +1,27 @@ -# Builtins import datetime as dt -from typing import Any, Dict, List, Tuple, Union, Callable -import hashlib +import time +from typing import Callable, Dict, Union -# External libraries -import pytz -import pandas as pd import numpy as np +import pandas as pd -# Submodule imports -from harvest.api._base import API -from harvest.utils import * -from harvest.definitions import * - - -class DummyStreamer(API): - """DummyStreamer, as its name implies, is a dummy broker class that can - be useful for testing algorithms. When used as a streamer, it will return - randomly generated prices. +from harvest.broker._base import Broker +from harvest.definitions import Account, Stats +from harvest.enum import Interval +from harvest.util.date import convert_input_to_datetime +from harvest.util.helper import ( + aggregate_df, + data_to_occ, + debugger, + expand_interval, + interval_to_timedelta, + utc_current_time, +) + + +class DummyDataBroker(Broker): + """ + A dummy broker designed to generate fake data for testing purposes. """ interval_list = [ @@ -35,7 +39,6 @@ def __init__( stock_market_times: bool = False, realistic_simulation: bool = True, ) -> None: - # Whether or not to include time outside of the typical time that US stock market operates. self.stock_market_times = stock_market_times @@ -45,10 +48,10 @@ def __init__( # The current_time is used to let users go back in time. self.current_time = convert_input_to_datetime(current_time) if self.current_time is None: - self.current_time = now() + self.current_time = utc_current_time() # Fake the epoch so the difference between the time Harvest starts and the epoch is fixed - self.epoch = now() - dt.timedelta(days=365 * 30) + self.epoch = utc_current_time() - dt.timedelta(days=365 * 30) # Store random values and generates for each asset to make `fetch_price_history` fixed self.randomness = {} @@ -56,9 +59,7 @@ def __init__( # Set a default poll interval in case `setup` is not called. self.poll_interval = Interval.MIN_1 - def setup( - self, stats: Stats, account: Account, trader_main: Callable = None - ) -> None: + def setup(self, stats: Stats, account: Account, trader_main: Callable = None) -> None: super().setup(stats, account, trader_main) # Override the time set in the base class with the user specified time self.stats.timestamp = self.get_current_time() @@ -73,18 +74,18 @@ def start(self) -> None: elif unit == "HR": sleep = val * 3600 elif unit == "DAY": - sleep == val * 86400 + sleep = val * 86400 self.stats.timestamp = self.get_current_time() - self.main() + self.step() if self.realistic_simulation: # Simulate realistic wait times time.sleep(sleep) - def main(self) -> None: + def step(self) -> None: self.tick() df_dict = self.fetch_latest_ohlc() - self.trader_main(df_dict) + self.broker_hub_cb(df_dict) # -------------- Streamer methods -------------- # @@ -98,7 +99,6 @@ def fetch_price_history( start: Union[str, dt.datetime] = None, end: Union[str, dt.datetime] = None, ) -> pd.DataFrame: - start = convert_input_to_datetime(start) end = convert_input_to_datetime(end) @@ -121,41 +121,19 @@ def fetch_price_history( results = self._generate_history(symbol, interval, start, end) if self.stock_market_times: - debugger.debug( - "Dummy Broker excluding information when the stock market is closed." - ) + debugger.debug("Dummy Broker excluding information when the stock market is closed.") open_time = dt.time(hour=13, minute=30) close_time = dt.time(hour=20) - # Removes datapoints when the stock marked is closed. Does not handle holidays. - results = results.loc[ - (open_time < results.index.time) & (results.index.time < close_time) - ] - results = results[ - (results.index.dayofweek != 5) & (results.index.dayofweek != 6) - ] + # Removes data points when the stock marked is closed. Does not handle holidays. + results = results.loc[(open_time < results.index.time) & (results.index.time < close_time)] + results = results[(results.index.dayofweek != 5) & (results.index.dayofweek != 6)] return results - # TODO: Generate dummy option data - def fetch_option_market_data(self, symbol: str) -> Dict[str, float]: - # This is a placeholder so Trader doesn't crash - # message = hashlib.sha256() - # message.update(symbol.encode("utf-8")) - # message.update(str(self.get_current_time()).encode("utf-8")) - # hsh = message.digest() - # price = int.from_bytes(hsh[:4], "big") / (2 ** 32) - # price = (price + 1) * 1.5 - price = ( - self.fetch_price_history(symbol, self.poll_interval)[symbol].iloc[-1][ - "close" - ] - / 100 - ) - debugger.debug( - f"Dummy Streamer fake fetch option market data price for {symbol}: {price}" - ) + price = self.fetch_price_history(symbol, self.poll_interval)[symbol].iloc[-1]["close"] / 100 + debugger.debug(f"Dummy Streamer fake fetch option market data price for {symbol}: {price}") return { "price": price, @@ -163,13 +141,45 @@ def fetch_option_market_data(self, symbol: str) -> Dict[str, float]: "bid": price * 0.95, } - # Not implemented functions: - # fetch_chain_info - # fetch_chain_data + def fetch_chain_data(self, symbol: str, date: Union[str, dt.datetime]) -> pd.DataFrame: + price = self.fetch_price_history(symbol, self.poll_interval)[symbol].iloc[-1]["close"] / 100 + + # Types = call, put + types = ["call", "put"] + # Strike prices are price +- 200% + strikes = np.linspace(price * 0.2, price * 2.0, 10) + # Expirations are the next day, next week, and next month + expirations = [date + dt.timedelta(days=1), date + dt.timedelta(days=7), date + dt.timedelta(days=30)] + + # Create a permutation of all the data + data = [] + for t in types: + for s in strikes: + for e in expirations: + data.append([symbol, e, t, s]) + + # Create a DataFrame from the data + # Columns are exp_date, strike, and type, with the index being the OCC symbol + df = pd.DataFrame(data, columns=["exp_date", "strike", "type"]) + df.index = [data_to_occ(*l) for l in data] + + return df + + def fetch_chain_info(self, symbol: str) -> Dict[str, str]: + cur_date = self.get_current_time().date() + return { + "chain_id": "123456", + "exp_dates": [ + cur_date + dt.timedelta(days=1), + cur_date + dt.timedelta(days=7), + cur_date + dt.timedelta(days=30), + ], + "multiplier": 100, + } # ------------- Broker methods ------------- # - # Not implemented functions: + # Not implemented: # fetch_stock_positions # fetch_option_positions # fetch_crypto_positions @@ -182,7 +192,7 @@ def fetch_option_market_data(self, symbol: str) -> Dict[str, float]: # --------------- Methods for Trading --------------- # - # Not implemented functions: + # Not implemented: # order_stock_limit # order_crypto_limit # order_option_limit @@ -204,53 +214,39 @@ def fetch_latest_ohlc(self) -> Dict[str, pd.DataFrame]: def tick(self) -> None: self.current_time += interval_to_timedelta(self.poll_interval) - def _generate_history( - self, symbol: str, interval: Interval, start: dt.datetime, end: dt.datetime - ) -> pd.DataFrame: + def _generate_history(self, symbol: str, interval: Interval, start: dt.datetime, end: dt.datetime) -> pd.DataFrame: # Convert datetime to indices - start_index = int((start - self.epoch).total_seconds() // 60) end_index = 1 + int((end - self.epoch).total_seconds() // 60) if symbol in self.randomness: # If we already generated data from this asset - num_of_random = 1 + end_index if len(self.randomness[symbol]) < num_of_random: # If the new end index is greater than the data we have - # Get the rng for this symbol rng = self.randomness[symbol + "_rng"] - # Update the number of random data points num_of_random -= len(self.randomness[symbol]) - returns = rng.normal(loc=1e-12, scale=1e-12, size=num_of_random) - # Calculate the change in price since the first price new_price_changes = np.append(self.randomness[symbol], returns).cumsum() - # Store the new prices - self.randomness[symbol] = np.append( - self.randomness[symbol], new_price_changes - ) + self.randomness[symbol] = np.append(self.randomness[symbol], new_price_changes) else: # If there is no information about the asset - # Create an rng using the asset's symbol as a seed rng = np.random.default_rng(int.from_bytes(symbol.encode("ascii"), "big")) num_of_random = 1 + end_index - # Generate a bunch of random numbers using Geometric Brownian Motion returns = rng.normal(loc=1e-12, scale=1e-12, size=num_of_random) - # Store the price change since the first price self.randomness[symbol] = returns.cumsum() self.randomness[symbol + "_rng"] = rng - # The inital price is arbitarly calculated from the first change in price + # The initial price is arbitrarily calculated from the first change in price start_price = 100 * (self.randomness[symbol][0] + 1) times = [] @@ -261,7 +257,7 @@ def _generate_history( # Prevent prices from going negative prices[prices < 0] = 0.01 - # Calculate olhcv from the prices + # Calculate ohlc from the prices open_s = prices - 50 low = prices - 100 high = prices + 100 diff --git a/harvest/api/paper.py b/harvest/broker/paper.py similarity index 83% rename from harvest/api/paper.py rename to harvest/broker/paper.py index 1849fbe1..9c195128 100644 --- a/harvest/api/paper.py +++ b/harvest/broker/paper.py @@ -1,27 +1,24 @@ -# Builtins -import re import datetime as dt -from typing import Any, Dict, List, Tuple -from pathlib import Path -import pickle import os +import pickle +import re +from pathlib import Path +from typing import Any, Callable, Dict, List, Union -# External libraries -import pandas as pd -import yaml - -# Submodule imports -from harvest.api._base import API -from harvest.api.dummy import DummyStreamer -from harvest.utils import * -from harvest.definitions import * +from harvest.broker._base import Broker +from harvest.definitions import OPTION_QTY_MULTIPLIER, Account, Stats +from harvest.enum import DataBrokerType, Interval from harvest.storage import BaseStorage +from harvest.util.factory import load_broker +from harvest.util.helper import data_to_occ, debugger, is_crypto -class PaperBroker(API): - """DummyBroker, as its name implies, is a dummy broker class that can - be useful for testing algorithms. When used as a streamer, it will return - randomly generated prices. When used as a broker, it paper trades. +class PaperBroker(Broker): + """ + PaperBroker is a broker class that simulates buying and selling of assets. + It does this by keeping track of orders and assets in a local database. + It does not have the ability to retrieve real data from the market, + so it must be used in conjunction with another broker that can provide data. """ interval_list = [ @@ -36,13 +33,13 @@ class PaperBroker(API): def __init__( self, path: str = None, - streamer: API = None, + data_source_broker: DataBrokerType = DataBrokerType.DUMMY, commission_fee: Union[float, str, Dict[str, Any]] = 0, save: bool = False, ) -> None: """ :path: Path to a configuration file holding account information for the user. - :streamer: A streamer to get asset prices and the current time. + :data_source_broker: A broker that can provide data to the PaperBroker. :commission_fee: When this is a number it is assumed to be a flat price on all buys and sells of assets. When this is a string formatted as 'XX%' then it is assumed that commission fees are that percent of the @@ -50,6 +47,7 @@ def __init__( with the keys 'buy' and 'sell' you can specify different commission fees when buying and selling assets. The values must be numbers or strings formatted as 'XX%'. + :save: Whether or not to save the state of the broker to a file. """ super().__init__(path) @@ -62,7 +60,7 @@ def __init__( self.commission_fee = commission_fee self.save = save - self.streamer = None + self.data_broker_ref = load_broker(data_source_broker)() if path is None: self.save_path = "./save" @@ -135,12 +133,10 @@ def _delete_account(self) -> None: try: os.remove(self.save_path) debugger.debug("Removed saved account file.") - except: + except OSError: debugger.warning("Saved account file does not exists.") - def setup( - self, stats: Stats, account: Account, trader_main: Callable = None - ) -> None: + def setup(self, stats: Stats, account: Account, trader_main: Callable = None) -> None: super().setup(stats, account, trader_main) self.backtest = False @@ -167,15 +163,6 @@ def fetch_option_positions(self) -> List[Dict[str, Any]]: def fetch_crypto_positions(self) -> List[Dict[str, Any]]: return self.cryptos - # def update_option_positions(self, positions) -> List[Dict[str, Any]]: - # for r in self.options: - # occ_sym = r["symbol"] - # price = self.streamer.fetch_option_market_data(occ_sym)["price"] - - # r["current_price"] = price - # r["market_value"] = price * r["quantity"] * 100 - # r["cost_basis"] = r["avg_price"] * r["quantity"] * 100 - def fetch_account(self) -> Dict[str, Any]: self.equity = self._calc_equity() self._save_account() @@ -194,15 +181,13 @@ def fetch_stock_order_status(self, order_id: int) -> Dict[str, Any]: debugger.debug(f"Backtest: {self.backtest}") if self.backtest: - price = self.storage.load(sym, self.stats.watchlist_cfg[sym]["interval"])[ - sym - ]["close"][-1] + price = self.storage.load(sym, self.stats.watchlist_cfg[sym]["interval"])[sym]["close"][-1] else: - price = self.streamer.fetch_price_history( + price = self.data_broker_ref.fetch_price_history( sym, self.stats.watchlist_cfg[sym]["interval"], - self.streamer.get_current_time() - dt.timedelta(days=7), - self.streamer.get_current_time(), + self.data_broker_ref.get_current_time() - dt.timedelta(days=7), + self.data_broker_ref.get_current_time(), )[sym]["close"][-1] debugger.debug(f"Price of {sym} is {price}") @@ -216,9 +201,7 @@ def fetch_stock_order_status(self, order_id: int) -> Dict[str, Any]: if ret["side"] == "buy": # Check to see if user has enough funds to buy the stock debugger.debug(f"Original price: {original_price}") - actual_price = self.apply_commission( - original_price, self.commission_fee, "buy" - ) + actual_price = self.apply_commission(original_price, self.commission_fee, "buy") # Check if user has enough buying power if self.buying_power + ret["limit_price"] * qty < actual_price: debugger.error( @@ -226,17 +209,13 @@ def fetch_stock_order_status(self, order_id: int) -> Dict[str, Any]: ) elif ret["limit_price"] < price: limit_price = ret["limit_price"] - debugger.info( - f"Limit price for {sym} is less than current price ({limit_price} < {price})." - ) + debugger.info(f"Limit price for {sym} is less than current price ({limit_price} < {price}).") else: # If asset already exists, buy more. If not, add a new entry if pos is None: lst.append({"symbol": sym, "avg_price": price, "quantity": qty}) else: - pos["avg_price"] = ( - pos["avg_price"] * pos["quantity"] + price * qty - ) / (qty + pos["quantity"]) + pos["avg_price"] = (pos["avg_price"] * pos["quantity"] + price * qty) / (qty + pos["quantity"]) pos["quantity"] = pos["quantity"] + qty self.cash -= actual_price @@ -248,7 +227,7 @@ def fetch_stock_order_status(self, order_id: int) -> Dict[str, Any]: self.orders.remove(ret) ret = ret_1 ret["status"] = "filled" - ret["filled_time"] = self.streamer.get_current_time() + ret["filled_time"] = self.data_broker_ref.get_current_time() ret["filled_price"] = price else: if pos is None: @@ -257,16 +236,14 @@ def fetch_stock_order_status(self, order_id: int) -> Dict[str, Any]: pos["quantity"] = pos["quantity"] - qty if pos["quantity"] < 1e-8: lst.remove(pos) - actual_worth = self.apply_commission( - original_price, self.commission_fee, "sell" - ) + actual_worth = self.apply_commission(original_price, self.commission_fee, "sell") self.cash += actual_worth self.buying_power += actual_worth ret_1 = ret.copy() self.orders.remove(ret) ret = ret_1 ret["status"] = "filled" - ret["filled_time"] = self.streamer.get_current_time() + ret["filled_time"] = self.data_broker_ref.get_current_time() ret["filled_price"] = price self.equity = self._calc_equity() @@ -287,7 +264,7 @@ def fetch_option_order_status(self, order_id: int) -> Dict[str, Any]: sym = ret["base_symbol"] occ_sym = ret["symbol"] - price = self.streamer.fetch_option_market_data(occ_sym)["price"] + price = self.data_broker_ref.fetch_option_market_data(occ_sym)["price"] qty = ret["quantity"] original_price = price * qty * OPTION_QTY_MULTIPLIER @@ -296,9 +273,7 @@ def fetch_option_order_status(self, order_id: int) -> Dict[str, Any]: pos = next((r for r in self.options if r["symbol"] == occ_sym), None) if ret["side"] == "buy": # Check to see if user has enough funds to buy the stock - actual_price = self.apply_commission( - original_price, self.commission_fee, "buy" - ) + actual_price = self.apply_commission(original_price, self.commission_fee, "buy") if self.buying_power < actual_price: debugger.error( f"""Not enough buying power.\n Total price ({actual_price}) exceeds buying power {self.buying_power}.\n Reduce purchase quantity or increase buying power.""" @@ -325,18 +300,14 @@ def fetch_option_order_status(self, order_id: int) -> Dict[str, Any]: } ) else: - pos["avg_price"] = ( - pos["avg_price"] * pos["quantity"] + price * qty - ) / (qty + pos["quantity"]) + pos["avg_price"] = (pos["avg_price"] * pos["quantity"] + price * qty) / (qty + pos["quantity"]) pos["quantity"] = pos["quantity"] + qty self.cash -= actual_price - self.buying_power += ( - ret["limit_price"] * qty * OPTION_QTY_MULTIPLIER - ) + self.buying_power += ret["limit_price"] * qty * OPTION_QTY_MULTIPLIER self.buying_power -= actual_price ret["status"] = "filled" - ret["filled_time"] = self.streamer.get_current_time() + ret["filled_time"] = self.data_broker_ref.get_current_time() ret["filled_price"] = price debugger.debug(f"After BUY: {self.buying_power}") ret_1 = ret.copy() @@ -347,18 +318,14 @@ def fetch_option_order_status(self, order_id: int) -> Dict[str, Any]: raise Exception(f"Cannot sell {sym}, is not owned") pos["quantity"] = pos["quantity"] - qty debugger.debug(f"current:{self.buying_power}") - actual_price = self.apply_commission( - original_price, self.commission_fee, "sell" - ) + actual_price = self.apply_commission(original_price, self.commission_fee, "sell") self.cash += actual_price self.buying_power += actual_price - debugger.debug( - f"Made {sym} {occ_sym} {qty} {price}: {self.buying_power}" - ) + debugger.debug(f"Made {sym} {occ_sym} {qty} {price}: {self.buying_power}") if pos["quantity"] < 1e-8: self.options.remove(pos) ret["status"] = "filled" - ret["filled_time"] = self.streamer.get_current_time() + ret["filled_time"] = self.data_broker_ref.get_current_time() ret["filled_price"] = price ret_1 = ret.copy() self.orders.remove(ret) @@ -389,7 +356,6 @@ def order_stock_limit( in_force: str = "gtc", extended: bool = False, ) -> Dict[str, Any]: - self._validate_order(side, quantity, limit_price) data = { @@ -422,7 +388,6 @@ def order_crypto_limit( in_force: str = "gtc", extended: bool = False, ) -> Dict[str, Any]: - self._validate_order(side, quantity, limit_price) data = { @@ -457,7 +422,6 @@ def order_option_limit( strike: float, in_force: str = "gtc", ) -> Dict[str, Any]: - self._validate_order(side, quantity, limit_price) data = { @@ -517,8 +481,6 @@ def apply_commission( if match is not None: commission_fee = inital_price * 0.01 * float(match.group(1)) return f(inital_price, commission_fee) - raise Exception( - f"`commission_fee` {commission_fee} not valid must match this regex expression: {pattern}" - ) + raise Exception(f"`commission_fee` {commission_fee} not valid must match this regex expression: {pattern}") elif type(commission_fee) is dict: return self.apply_commission(inital_price, commission_fee[side], side) diff --git a/harvest/api/polygon.py b/harvest/broker/polygon.py similarity index 85% rename from harvest/api/polygon.py rename to harvest/broker/polygon.py index 0e88ef0f..976d2946 100644 --- a/harvest/api/polygon.py +++ b/harvest/broker/polygon.py @@ -1,21 +1,25 @@ -# Builtins -import json +import datetime import datetime as dt -from typing import Any, Dict, List, Tuple, Union -import requests +import re +from typing import Any, Dict, Union +from zoneinfo import ZoneInfo -# External libraries import pandas as pd -import pytz - -# Submodule imports -from harvest.api._base import API -from harvest.utils import * -from harvest.definitions import * +import requests +from harvest.broker._base import Broker +from harvest.enum import Interval +from harvest.util.helper import ( + convert_input_to_datetime, + debugger, + expand_interval, + is_crypto, + occ_to_data, + utc_current_time, +) -class PolygonStreamer(API): +class PolygonBroker(Broker): interval_list = [Interval.MIN_1, Interval.MIN_5, Interval.HR_1, Interval.DAY_1] req_keys = ["polygon_api_key"] @@ -23,14 +27,12 @@ def __init__(self, path: str = None, is_basic_account: bool = False) -> None: super().__init__(path) if self.config is None: - raise Exception( - f"Account credentials not found! Expected file path: {path}" - ) + raise Exception(f"Account credentials not found! Expected file path: {path}") self.basic = is_basic_account self.option_cache = {} - def main(self) -> None: + def step(self) -> None: df_dict = {} combo = self.stats.watchlist_cfg.keys() if self.basic and len(combo) > 5: @@ -41,25 +43,28 @@ def main(self) -> None: for s in combo: df = self.fetch_price_history( - s, Interval.MIN_1, now() - dt.timedelta(days=3), now() + s, + Interval.MIN_1, + utc_current_time() - dt.timedelta(days=3), + utc_current_time(), ).iloc[[-1]] df_dict[s] = df debugger.debug(df) - self.trader_main(df_dict) + self.broker_hub_cb(df_dict) def exit(self) -> None: self.option_cache = {} # -------------- Streamer methods -------------- # - @API._exception_handler + @Broker._exception_handler def get_current_time(self) -> dt.datetime: key = self.config["polygon_api_key"] request = f"https://api.polygon.io/v1/marketstatus/now?apiKey={key}" server_time = requests.get(request).json().get("serverTime") return dt.datetime.fromisoformat(server_time) - @API._exception_handler + @Broker._exception_handler def fetch_price_history( self, symbol: str, @@ -67,16 +72,15 @@ def fetch_price_history( start: Union[str, dt.datetime] = None, end: Union[str, dt.datetime] = None, ) -> pd.DataFrame: - debugger.debug(f"Fetching {symbol} {interval} price history") start = convert_input_to_datetime(start) end = convert_input_to_datetime(end) if start is None: - start = now() - dt.timedelta(days=365 * 2) + start = utc_current_time() - dt.timedelta(days=365 * 2) if end is None: - end = now() + end = utc_current_time() if start >= end: return pd.DataFrame() @@ -84,7 +88,7 @@ def fetch_price_history( val, unit = expand_interval(interval) return self._get_data_from_polygon(symbol, val, unit, start, end) - @API._exception_handler + @Broker._exception_handler def fetch_chain_info(self, symbol: str) -> Dict[str, Any]: key = self.config["polygon_api_key"] request = f"https://api.polygon.io/v3/reference/options/contracts?underlying_ticker={symbol}&apiKey={key}" @@ -92,24 +96,16 @@ def fetch_chain_info(self, symbol: str) -> Dict[str, Any]: if response is None: raise Exception(f"Failed to fech chain info for {symbol}.") - dates = { - dt.datetime.strptime(contract["expiration_date"], "%Y-%m-%d") - for contract in response - } + dates = {dt.datetime.strptime(contract["expiration_date"], "%Y-%m-%d") for contract in response} return { "id": "n/a", "exp_dates": list(dates), "multiplier": 100, } - @API._exception_handler + @Broker._exception_handler def fetch_chain_data(self, symbol: str, date: dt.datetime) -> pd.DataFrame: - - if ( - bool(self.option_cache) - and symbol in self.option_cache - and date in self.option_cache[symbol] - ): + if bool(self.option_cache) and symbol in self.option_cache and date in self.option_cache[symbol]: return self.option_cache[symbol][date] exp_date = date.strftime("%Y-%m-%d") @@ -117,9 +113,7 @@ def fetch_chain_data(self, symbol: str, date: dt.datetime) -> pd.DataFrame: request = f"https://api.polygon.io/v3/reference/options/contracts?underlying_ticker={symbol}&expiration_date={exp_date}&apiKey={key}" response = self._handle_request_response(request) if response is None: - debugger.error( - f"Failed to get chain data for {symbol} at {date}. Returning an empty dataframe." - ) + debugger.error(f"Failed to get chain data for {symbol} at {date}. Returning an empty dataframe.") return pd.DataFrame() df = pd.DataFrame.from_dict(response) @@ -143,7 +137,7 @@ def fetch_chain_data(self, symbol: str, date: dt.datetime) -> pd.DataFrame: return df - @API._exception_handler + @Broker._exception_handler def fetch_option_market_data(self, occ_symbol: str) -> Dict[str, Any]: if self.basic: raise Exception("Basic accounts do not have access to options.") @@ -161,7 +155,7 @@ def fetch_option_market_data(self, occ_symbol: str) -> Dict[str, Any]: "bid": response["last_quote"]["bid"], } - @API._exception_handler + @Broker._exception_handler def fetch_market_hours(self, date: datetime.date) -> Dict[str, Any]: # Polygon does not support getting market hours, # so use the free Tradier API instead. @@ -227,9 +221,7 @@ def create_secret(self) -> Dict[str, str]: if not should_setup: w.println("You can't use Polygon without an API key.") - w.println( - "You can set up the credentials manually, or use other streamers." - ) + w.println("You can set up the credentials manually, or use other streamers.") return False w.println("Alright! Let's get started") @@ -257,7 +249,7 @@ def _get_data_from_polygon( start: dt.datetime, end: dt.datetime, ) -> pd.DataFrame: - if self.basic and start < now() - dt.timedelta(days=365 * 2): + if self.basic and start < utc_current_time() - dt.timedelta(days=365 * 2): debugger.warning( "Start time is over two years old! Only data from the past two years will be returned for basic accounts." ) @@ -280,7 +272,7 @@ def _get_data_from_polygon( response = self._handle_request_response(request) if response is None: - debugger.error(f"Request error! Returning empty dataframe.") + debugger.error("Request error! Returning empty dataframe.") return pd.DataFrame() df = pd.DataFrame(response) @@ -302,9 +294,7 @@ def _format_df(self, df: pd.DataFrame, symbol: str) -> pd.DataFrame: ) df = df[["timestamp", "open", "high", "low", "close", "volume"]].astype(float) - df.index = pd.DatetimeIndex( - pd.to_datetime(df["timestamp"], unit="ms", utc=True), tz=dt.timezone.utc - ) + df.index = pd.DatetimeIndex(pd.to_datetime(df["timestamp"], unit="ms", utc=True), tz=dt.timezone.utc) df.drop(columns=["timestamp"], inplace=True) df.columns = pd.MultiIndex.from_product([[symbol], df.columns]) diff --git a/harvest/api/robinhood.py b/harvest/broker/robinhood.py similarity index 80% rename from harvest/api/robinhood.py rename to harvest/broker/robinhood.py index ce5a88f2..65b004c8 100644 --- a/harvest/api/robinhood.py +++ b/harvest/broker/robinhood.py @@ -1,22 +1,34 @@ -# Builtins +import datetime import datetime as dt -from logging import debug -from typing import Any, Dict, List, Tuple +from typing import List -# External libraries import pandas as pd -import robin_stocks.robinhood as rh -import pytz import pyotp -import yaml +import pytz +import pytz as tz +import robin_stocks.robinhood as rh -# Submodule imports -from harvest.api._base import API -from harvest.definitions import * -from harvest.utils import * +from harvest.broker._base import Broker +from harvest.enum import Interval +from harvest.util.helper import ( + aggregate_df, + convert_input_to_datetime, + data_to_occ, + date_to_str, + debugger, + is_crypto, + str_to_date, + symbol_type, +) -class Robinhood(API): +class RobinhoodBroker(Broker): + """ + The Robinhood broker class that interacts with the Robinhood Brokerage (robinhood.com) + + Robinhood is not an API-first brokerage, so this class uses the robin_stocks package + to interact with the brokerage. + """ interval_list = [Interval.SEC_15, Interval.MIN_5, Interval.HR_1, Interval.DAY_1] exchange = "NASDAQ" @@ -26,13 +38,15 @@ def __init__(self, path=None): super().__init__(path) if self.config is None: - raise Exception( - f"Account credentials not found! Expected file path: {path}" - ) + raise Exception(f"Account credentials not found! Expected file path: {path}") self.login() def login(self): + """ + Log in to Robinhood using the credentials provided in the secret.yml file. + Robinhood also requires 2FA to be enabled, so the MFA code is also required. + """ debugger.debug("Logging into Robinhood...") totp = pyotp.TOTP(self.config["robin_mfa"]).now() rh.login( @@ -43,78 +57,30 @@ def login(self): ) def refresh_cred(self): + """ + Robinhood sessions expire after a certain amount of time, so this method is used to refresh the session. + """ super().refresh_cred() debugger.debug("Logging out of Robinhood...") rh.authentication.logout() self.login() debugger.debug("Logged into Robinhood...") - # @API._run_once def setup(self, stats, account, trader_main=None): - super().setup(stats, account, trader_main) # Robinhood only supports 15SEC, 1MIN interval for crypto for sym in self.stats.watchlist_cfg: - if ( - not is_crypto(sym) - and self.stats.watchlist_cfg[sym]["interval"] < Interval.MIN_5 - ): - raise Exception( - f'Interval {self.stats.watchlist_cfg[sym]["interval"]} is only supported for crypto' - ) - - # self.__watch_stock = [] - # self.__watch_crypto = [] - # self.__watch_crypto_fmt = [] - - # for s in interval: - # if is_crypto(s): - # self.__watch_crypto_fmt.append(s[1:]) - # self.__watch_crypto.append(s) - # else: - # self.__watch_stock.append(s) - + if not is_crypto(sym) and self.stats.watchlist_cfg[sym]["interval"] < Interval.MIN_5: + raise Exception(f'Interval {self.stats.watchlist_cfg[sym]["interval"]} is only supported for crypto') self.__option_cache = {} def exit(self): self.__option_cache = {} - # @API._exception_handler - # def fetch_latest_stock_price(self): - # df={} - # for s in self.__watch_stock: - # ret = rh.get_stock_historicals( - # s, - # interval=self.__interval_fmt, - # span='day', - # ) - # if 'error' in ret or ret == None or (type(ret) == list and len(ret) == 0): - # continue - # df_tmp = pd.DataFrame.from_dict(ret) - # df_tmp = self._format_df(df_tmp, [s], self.stats.watchlist_cfg).iloc[[-1]] - # df[s] = df_tmp - - # return df - - # @API._exception_handler - # def fetch_latest_crypto_price(self): - # df={} - # for s in self.__watch_crypto_fmt: - # ret = rh.get_crypto_historicals( - # s, - # interval=self.__interval_fmt, - # span='hour', - # ) - # df_tmp = pd.DataFrame.from_dict(ret) - # df_tmp = self._format_df(df_tmp, ['@'+s], self.stats.watchlist_cfg).iloc[[-1]] - # df['@'+s] = df_tmp - - # return df - # -------------- Streamer methods -------------- # - @API._exception_handler + @Broker._exception_handler def fetch_price_history( self, symbol: str, @@ -122,7 +88,6 @@ def fetch_price_history( start: dt.datetime = None, end: dt.datetime = None, ): - if start is None: start = dt.datetime(1970, 1, 1) if end is None: @@ -203,7 +168,7 @@ def fetch_price_history( return df - @API._exception_handler + @Broker._exception_handler def fetch_chain_info(self, symbol: str): ret = rh.get_chains(symbol) return { @@ -212,14 +177,9 @@ def fetch_chain_info(self, symbol: str): "multiplier": ret["trade_value_multiplier"], } - @API._exception_handler + @Broker._exception_handler def fetch_chain_data(self, symbol: str, date: dt.datetime): - - if ( - bool(self.__option_cache) - and symbol in self.__option_cache - and date in self.__option_cache[symbol] - ): + if bool(self.__option_cache) and symbol in self.__option_cache and date in self.__option_cache[symbol]: return self.__option_cache[symbol][date] ret = rh.find_tradable_options(symbol, date_to_str(date)) @@ -257,13 +217,10 @@ def fetch_chain_data(self, symbol: str, date: dt.datetime): return df - @API._exception_handler + @Broker._exception_handler def fetch_option_market_data(self, symbol: str): - - sym, date, type, price = self.occ_to_data(symbol) - ret = rh.get_option_market_data( - sym, date.strftime("%Y-%m-%d"), str(price), type - ) + sym, date, opt_type, price = self.occ_to_data(symbol) + ret = rh.get_option_market_data(sym, date.strftime("%Y-%m-%d"), str(price), opt_type) ret = ret[0][0] return { "price": float(ret["adjusted_mark_price"]), @@ -271,7 +228,7 @@ def fetch_option_market_data(self, symbol: str): "bid": float(ret["bid_price"]), } - @API._exception_handler + @Broker._exception_handler def fetch_market_hours(self, date: datetime.date): ret = rh.get_market_hours("XNAS", date.strftime("%Y-%m-%d")) is_open = ret["is_open"] @@ -298,7 +255,7 @@ def fetch_market_hours(self, date: datetime.date): # ------------- Broker methods ------------- # - @API._exception_handler + @Broker._exception_handler def fetch_stock_positions(self): ret = rh.get_open_stock_positions() pos = [] @@ -316,7 +273,7 @@ def fetch_stock_positions(self): ) return pos - @API._exception_handler + @Broker._exception_handler def fetch_option_positions(self): ret = rh.get_open_option_positions() pos = [] @@ -327,8 +284,7 @@ def fetch_option_positions(self): pos.append( { "base_symbol": r["chain_symbol"], - "avg_price": float(r["average_price"]) - / float(r["trade_value_multiplier"]), + "avg_price": float(r["average_price"]) / float(r["trade_value_multiplier"]), "quantity": float(r["quantity"]), "multiplier": float(r["trade_value_multiplier"]), "exp_date": data["expiration_date"], @@ -338,13 +294,11 @@ def fetch_option_positions(self): ) date = data["expiration_date"] date = dt.datetime.strptime(date, "%Y-%m-%d") - pos[-1]["symbol"] = self.data_to_occ( - r["chain_symbol"], date, data["type"], float(data["strike_price"]) - ) + pos[-1]["symbol"] = self.data_to_occ(r["chain_symbol"], date, data["type"], float(data["strike_price"])) return pos - @API._exception_handler + @Broker._exception_handler def fetch_crypto_positions(self, key=None): ret = rh.get_crypto_positions() pos = [] @@ -366,19 +320,7 @@ def fetch_crypto_positions(self, key=None): ) return pos - # @API._exception_handler - # def update_option_positions(self, positions: List[Any]): - # for r in positions: - # sym, date, type, price = self.occ_to_data(r["symbol"]) - # upd = rh.get_option_market_data( - # sym, date.strftime("%Y-%m-%d"), str(price), type - # ) - # upd = upd[0][0] - # r["current_price"] = float(upd["adjusted_mark_price"]) - # r["market_value"] = float(upd["adjusted_mark_price"]) * r["quantity"] - # r["cost_basis"] = r["avg_price"] * r["quantity"] - - @API._exception_handler + @Broker._exception_handler def fetch_account(self): ret = rh.load_phoenix_account() ret = { @@ -389,7 +331,7 @@ def fetch_account(self): } return ret - @API._exception_handler + @Broker._exception_handler def fetch_stock_order_status(self, id): ret = rh.get_stock_order_info(id) # Check if any of the orders were executed @@ -413,7 +355,7 @@ def fetch_stock_order_status(self, id): "filled_price": filled_price, } - @API._exception_handler + @Broker._exception_handler def fetch_option_order_status(self, id): ret = rh.get_option_order_info(id) debugger.debug(ret) @@ -438,7 +380,7 @@ def fetch_option_order_status(self, id): "filled_price": filled_price, } - @API._exception_handler + @Broker._exception_handler def fetch_crypto_order_status(self, id): ret = rh.get_crypto_order_info(id) debugger.debug(ret) @@ -464,7 +406,7 @@ def fetch_crypto_order_status(self, id): "filled_price": filled_price, } - @API._exception_handler + @Broker._exception_handler def fetch_order_queue(self): queue = [] ret = rh.get_all_open_stock_orders() @@ -567,9 +509,8 @@ def order_stock_limit( "order_id": ret["id"], "symbol": symbol, } - except: + except Exception: debugger.error("Error while placing order.\nReturned: {ret}", exc_info=True) - raise Exception("Error while placing order.") def order_crypto_limit( self, @@ -600,11 +541,8 @@ def order_crypto_limit( "order_id": ret["id"], "symbol": "@" + symbol, } - except: - debugger.error( - f"Error while placing order.\nReturned: {ret}", exc_info=True - ) - raise Exception(f"Error while placing order.") + except Exception: + debugger.error(f"Error while placing order.\nReturned: {ret}", exc_info=True) def order_option_limit( self, @@ -649,11 +587,8 @@ def order_option_limit( "order_id": ret["id"], "symbol": symbol, } - except: - debugger.error( - f"Error while placing order.\nReturned: {ret}", exc_info=True - ) - raise Exception("Error while placing order") + except Exception: + debugger.error(f"Error while placing order.\nReturned: {ret}", exc_info=True) def cancel_stock_order(self, order_id): ret = rh.cancel_stock_order(order_id) @@ -667,9 +602,7 @@ def cancel_crypto_order(self, order_id): ret = rh.cancel_crypto_order(order_id) debugger.debug(ret) - def _format_df( - self, df: pd.DataFrame, watch: List[str], interval: str, latest: bool = False - ): + def _format_df(self, df: pd.DataFrame, watch: List[str], interval: str, latest: bool = False): # Robinhood returns offset-aware timestamps based on timezone GMT-0, or UTC df["timestamp"] = pd.to_datetime(df["begins_at"]) df = df.set_index(["timestamp"]) @@ -706,9 +639,7 @@ def create_secret(self): w = wizard.Wizard() - w.println( - "Hmm, looks like you haven't set up login credentials for Robinhood yet." - ) + w.println("Hmm, looks like you haven't set up login credentials for Robinhood yet.") should_setup = w.get_bool("Do you want to set it up now?", default="y") if not should_setup: @@ -725,23 +656,15 @@ def create_secret(self): ) w.wait_for_input() - have_mfa = w.get_bool( - "Do you have Two Factor Authentication enabled?", default="y" - ) + have_mfa = w.get_bool("Do you have Two Factor Authentication enabled?", default="y") if not have_mfa: - w.println( - "Robinhood (and Harvest) requires users to have 2FA enabled, so we'll turn that on next." - ) + w.println("Robinhood (and Harvest) requires users to have 2FA enabled, so we'll turn that on next.") else: - w.println( - "We'll need to reconfigure 2FA to use Harvest, so temporarily disable 2FA" - ) + w.println("We'll need to reconfigure 2FA to use Harvest, so temporarily disable 2FA") w.wait_for_input() - w.println( - "Enable 2FA. Robinhood should ask you what authentication method you want to use." - ) + w.println("Enable 2FA. Robinhood should ask you what authentication method you want to use.") w.wait_for_input() w.println("Select 'Authenticator App'.") w.wait_for_input() @@ -755,7 +678,7 @@ def create_secret(self): while True: try: totp = pyotp.TOTP(mfa).now() - except: + except ValueError: mfa = w.get_string( "Woah😮 Something went wrong. Make sure you typed in the code correctly.", pattern=r"[\d\w]+", @@ -763,18 +686,14 @@ def create_secret(self): continue break + w.print(f"Good! Robinhood should now be asking you for a 6-digit passcode. Type in: {totp}") w.print( - f"Good! Robinhood should now be asking you for a 6-digit passcode. Type in: {totp}" - ) - w.print( - f"⚠️ Beware, this passcode expires in a few seconds! If you couldn't type it in time, it should be regenerated." + "⚠️ Beware, this passcode expires in a few seconds! If you couldn't type it in time, it should be regenerated." ) new_passcode = True while new_passcode: - new_passcode = w.get_bool( - "Do you want to generate a new passcode?", default="n" - ) + new_passcode = w.get_bool("Do you want to generate a new passcode?", default="n") if new_passcode: totp = pyotp.TOTP(mfa).now() w.print(f"New passcode: {totp}") @@ -794,12 +713,12 @@ def create_secret(self): ) w.wait_for_input() - w.println(f"Almost there! Type in your username and password for Robinhood") + w.println("Almost there! Type in your username and password for Robinhood") username = w.get_string("Username: ") password = w.get_password("Password: ") - w.println(f"All steps are complete now 🎉. Generating secret.yml...") + w.println("All steps are complete now 🎉. Generating secret.yml...") return { "robin_mfa": f"{mfa}", diff --git a/harvest/api/webull.py b/harvest/broker/webull.py similarity index 78% rename from harvest/api/webull.py rename to harvest/broker/webull.py index 8cded9da..b464cd5e 100644 --- a/harvest/api/webull.py +++ b/harvest/broker/webull.py @@ -1,21 +1,16 @@ -# Builtins import datetime as dt -from typing import Any, Dict, List, Tuple import os.path +from typing import List -# External libraries import pandas as pd -from webull import webull, paper_webull -import yaml +from webull import paper_webull, webull -# Submodule imports -from harvest.api._base import API -from harvest.definitions import * -from harvest.utils import * +from harvest.broker._base import Broker +from harvest.enum import Interval +from harvest.util.helper import date_to_str, debugger, expand_interval, is_crypto, str_to_date, utc_current_time -class Webull(API): - +class WebullBroker(Broker): interval_list = [Interval.MIN_1, Interval.MIN_5, Interval.HR_1, Interval.DAY_1] exchange = "NASDAQ" req_keys = ["wb_username", "wb_password", "wb_trade_pin"] @@ -24,13 +19,11 @@ def __init__(self, path: str = None, paper_trader: bool = False): super().__init__(path) if self.config is None: - raise Exception( - f"Account credentials not found! Expected file path: {path}" - ) + raise Exception(f"Account credentials not found! Expected file path: {path}") self.paper = paper_trader self.wb_tokens = None - self.timestamp = now() + self.timestamp = utc_current_time() wb_filename = "webull_credentials.json" if os.path.isfile(wb_filename): self.wb_tokens = pd.read_pickle(wb_filename) @@ -48,23 +41,21 @@ def login(self): if wb_tokens: debugger.debug("Trying token login") try: - ret = self.api.api_login( + self.api.api_login( access_token=wb_tokens["accessToken"], refresh_token=wb_tokens["refreshToken"], token_expire=wb_tokens["tokenExpireTime"], uuid=wb_tokens["uuid"], ) if not self.api.is_logged_in(): - debugger.debug(f"Token login failed. \n{e}") + debugger.debug("Token login failed.") wb_tokens = None except Exception as e: - debugger.debug(f"Token login failed. \n{e}") + debugger.debug(f"Token login failed: {e}") wb_tokens = None if not wb_tokens and hasattr(self, "config"): debugger.debug("Trying interactive login.") - self.api.login( - self.config["wb_username"], self.config["wb_password"], save_token=True - ) + self.api.login(self.config["wb_username"], self.config["wb_password"], save_token=True) debugger.debug(f"Logged-in?: {self.api.is_logged_in()}, Paper: {self.paper}") def refresh_cred(self): @@ -104,40 +95,9 @@ def setup(self, stats, account, trader_main=None): def exit(self): pass - # def main(self): - # df_dict = {} - # df_dict.update(self.fetch_latest_stock_price()) - # df_dict.update(self.fetch_latest_crypto_price()) - - # self.trader_main(df_dict) - - # @API._exception_handler - # def fetch_latest_stock_price(self): - # df = {} - # for s in self.watch_stock: - # ret = self.api.get_bars(stock=s, interval=self.poll_interval_fmt) - # if len(ret) == 0: - # continue - # df_tmp = pd.DataFrame.from_dict(ret) - # df_tmp = self._format_df(df_tmp, [s], self.poll_interval).iloc[[-1]] - # df[s] = df_tmp - - # return df - - # @API._exception_handler - # def fetch_latest_crypto_price(self): - # df = {} - # for s in self.watch_crypto_fmt: - # ret = self.api.get_bars(stock=s, interval=self.poll_interval_fmt) - # df_tmp = pd.DataFrame.from_dict(ret) - # df_tmp = self._format_df(df_tmp, ["@" + s], self.poll_interval).iloc[[-1]] - # df["@" + s] = df_tmp - - # return df - # -------------- Streamer methods -------------- # - @API._exception_handler + @Broker._exception_handler def fetch_price_history( self, symbol: str, @@ -145,7 +105,6 @@ def fetch_price_history( start: dt.datetime = None, end: dt.datetime = None, ): - if start is None: start = dt.datetime(1970, 1, 1) if end is None: @@ -202,7 +161,7 @@ def fetch_price_history( return df - @API._exception_handler + @Broker._exception_handler def fetch_chain_info(self, symbol: str): ret = self.api.get_options_expiration_dates(symbol) return { @@ -211,14 +170,9 @@ def fetch_chain_info(self, symbol: str): "multiplier": 100, } - @API._exception_handler + @Broker._exception_handler def fetch_chain_data(self, symbol: str, date: dt.datetime): - - if ( - bool(self.__option_cache) - and symbol in self.__option_cache - and date in self.__option_cache[symbol] - ): + if bool(self.__option_cache) and symbol in self.__option_cache and date in self.__option_cache[symbol]: df = self.__option_cache[symbol][date] df.drop("id", axis=1, inplace=True) return df @@ -263,7 +217,7 @@ def fetch_chain_data(self, symbol: str, date: dt.datetime): return df.drop(["id"], axis=1) - @API._exception_handler + @Broker._exception_handler def fetch_option_market_data(self, symbol: str): sym, date, _, price = self.occ_to_data(symbol) date = str_to_date(date_to_str(date)) @@ -271,9 +225,7 @@ def fetch_option_market_data(self, symbol: str): if sym not in self.__option_cache or date not in self.__option_cache[sym]: self.fetch_chain_data(sym, date) - option_df = self.__option_cache[sym][date][ - self.__option_cache[sym][date].index == symbol - ] + option_df = self.__option_cache[sym][date][self.__option_cache[sym][date].index == symbol] oc_id = option_df["id"][0] @@ -289,11 +241,8 @@ def fetch_option_market_data(self, symbol: str): raise Exception(err_msg) try: price = float(ret["data"][0]["close"]) - except: - price = ( - float(ret["data"][0]["askList"][0]["price"]) - + float(ret["data"][0]["bidList"][0]["price"]) - ) / 2 + except Exception: + price = (float(ret["data"][0]["askList"][0]["price"]) + float(ret["data"][0]["bidList"][0]["price"])) / 2 return { "price": price, @@ -303,7 +252,7 @@ def fetch_option_market_data(self, symbol: str): # ------------- Broker methods ------------- # - @API._exception_handler + @Broker._exception_handler def fetch_stock_positions(self): ret = self.api.get_positions() pos = [] @@ -319,7 +268,7 @@ def fetch_stock_positions(self): ) return pos - @API._exception_handler + @Broker._exception_handler def fetch_option_positions(self): ret = self.api.get_positions() pos = [] @@ -327,14 +276,11 @@ def fetch_option_positions(self): if not r.get("assetType") or r.get("assetType") != "OPTION": continue # Get option data such as expiration date - data = self.api.get_option_quote( - stock=r["ticker"]["tickerId"], optionId=r["tickerId"] - ) + data = self.api.get_option_quote(stock=r["ticker"]["tickerId"], optionId=r["tickerId"]) pos.append( { "base_symbol": r["ticker"]["symbol"], - "avg_price": float(r["cost"]) - / float(data["data"][0]["quoteMultiplier"]), + "avg_price": float(r["cost"]) / float(data["data"][0]["quoteMultiplier"]), "quantity": float(r["position"]), "multiplier": float(data["data"][0]["quoteMultiplier"]), "exp_date": data["data"][0]["expireDate"], @@ -346,7 +292,7 @@ def fetch_option_positions(self): return pos - @API._exception_handler + @Broker._exception_handler def fetch_crypto_positions(self, key=None): ret = self.api.get_positions() pos = [] @@ -364,44 +310,35 @@ def fetch_crypto_positions(self, key=None): ) return pos - # @API._exception_handler - # def update_option_positions(self, positions: List[Any]): - # for r in positions: - # sym, date, _, price = self.occ_to_data(r["occ_symbol"]) - # oc_id = self.__option_cache[sym][date][ - # self.__option_cache[sym][date].index == symbol - # ].id[0] - # ret = self.api.get_option_quote(stock=sym, optionId=oc_id) - - # r["current_price"] = float(ret["data"][0]["close"]) - # r["market_value"] = float(ret["data"][0]["close"]) * r["quantity"] - # r["cost_basis"] = r["avg_price"] * r["quantity"] - def fmt_fetch_account(self, val, data): for line in data: if line["key"] == val: return line["value"] return -1 - @API._exception_handler + @Broker._exception_handler def fetch_account(self): if not self.api.is_logged_in(): return None ret = self.api.get_account()["accountMembers"] return { "equity": float(self.fmt_fetch_account("totalMarketValue", ret)), - "cash": float(self.fmt_fetch_account("cashBalance", ret)) - if not self.paper - else float(self.fmt_fetch_account("usableCash", ret)), - "buying_power": float(self.fmt_fetch_account("dayBuyingPower", ret)) - if not self.paper - else float(self.fmt_fetch_account("usableCash", ret)), + "cash": ( + float(self.fmt_fetch_account("cashBalance", ret)) + if not self.paper + else float(self.fmt_fetch_account("usableCash", ret)) + ), + "buying_power": ( + float(self.fmt_fetch_account("dayBuyingPower", ret)) + if not self.paper + else float(self.fmt_fetch_account("usableCash", ret)) + ), "bp_options": float(self.fmt_fetch_account("optionBuyingPower", ret)), "bp_crypto": float(self.fmt_fetch_account("cryptoBuyingPower", ret)), "multiplier": float(-1), } - @API._exception_handler + @Broker._exception_handler def fetch_stock_order_status(self, id): ret = self.api.get_history_orders(status="All") for r in ret: @@ -419,7 +356,7 @@ def fetch_stock_order_status(self, id): "status": r["status"].lower(), } - @API._exception_handler + @Broker._exception_handler def fetch_option_order_status(self, id): ret = self.api.get_history_orders(status="All") for r in ret: @@ -442,7 +379,7 @@ def fetch_option_order_status(self, id): "status": r["status"].lower(), } - @API._exception_handler + @Broker._exception_handler def fetch_crypto_order_status(self, id): ret = self.api.get_history_orders(status="All") for r in ret: @@ -453,16 +390,14 @@ def fetch_crypto_order_status(self, id): "symbol": f"@{r['orders'][0]['ticker']['symbol'].replace('USD', '')}", "qty": float(r["quantity"]), "filled_qty": float(r["cumulative_quantity"]), - "filled_price": float(r["executions"][0]["effective_price"]) - if len(r["executions"]) - else 0, + "filled_price": (float(r["executions"][0]["effective_price"]) if len(r["executions"]) else 0), "filled_cost": float(r["rounded_executed_notional"]), "side": r["side"], "time_in_force": r["timeInForce"], "status": r["status"].lower(), } - @API._exception_handler + @Broker._exception_handler def fetch_order_queue(self): queue = [] ret = self.api.get_current_orders() @@ -515,11 +450,8 @@ def order_stock_limit( debugger.error(f"Error while placing order.\nReturned: {ret}") raise Exception("Error while placing order.") return {"type": typ, "id": ret["data"]["orderId"], "symbol": symbol} - except Exception as e: - debugger.error( - f"Error while placing order.\nReturned: {ret}", exc_info=True - ) - raise Exception("Error while placing order.") + except TimeoutError: + debugger.error(f"Error while placing order.\nReturned: {ret}", exc_info=True) def order_crypto_limit( self, @@ -552,11 +484,8 @@ def order_crypto_limit( debugger.error(f"Error while placing order.\nReturned: {ret}") raise Exception("Error while placing order.") return {"type": typ, "id": ret["data"]["orderId"], "symbol": symbol} - except Exception as e: - debugger.error( - f"Error while placing order.\nReturned: {ret}", exc_info=True - ) - raise Exception("Error while placing order.") + except Exception: + debugger.error(f"Error while placing order.\nReturned: {ret}", exc_info=True) def order_option_limit( self, @@ -573,14 +502,10 @@ def order_option_limit( ret = None sym = self.data_to_occ(symbol, exp_date, side, strike) date = str_to_date(date_to_str(exp_date)) - oc_id = self.__option_cache[symbol][date][ - self.__option_cache[symbol][date].index == sym - ]["id"][0].item() + oc_id = self.__option_cache[symbol][date][self.__option_cache[symbol][date].index == sym]["id"][0].item() if not isinstance(oc_id, int): - debugger.error( - "Error while placing order_option_limit. Can't find optionId." - ) + debugger.error("Error while placing order_option_limit. Can't find optionId.") raise Exception("Error while placing order.") try: ret = self.api.place_order_option( @@ -598,15 +523,10 @@ def order_option_limit( raise Exception("Error while placing order.") return {"type": "OPTION", "id": ret["orderId"], "symbol": symbol} - except: - debugger.error( - f"Error while placing order.\nReturned: {ret}", exc_info=True - ) - raise Exception("Error while placing order") + except Exception: + debugger.error(f"Error while placing order.\nReturned: {ret}", exc_info=True) - def _format_df( - self, df: pd.DataFrame, watch: List[str], interval: str, latest: bool = False - ): + def _format_df(self, df: pd.DataFrame, watch: List[str], interval: str, latest: bool = False): df = df[["open", "close", "high", "low", "volume"]].astype(float) df.columns = pd.MultiIndex.from_product([watch, df.columns]) return df.dropna() @@ -616,9 +536,7 @@ def create_secret(self, path): w = wizard.Wizard() - w.println( - "Hmm, looks like you haven't set up login credentials for Webull yet." - ) + w.println("Hmm, looks like you haven't set up login credentials for Webull yet.") should_setup = w.get_bool("Do you want to set it up now?", default="y") if not should_setup: @@ -638,7 +556,7 @@ def create_secret(self, path): username = w.get_string("Username: ") password = w.get_password("Password: ") device = w.get_string("Device Name (e.g. Harvester): ") - w.println(f"Getting security question.") + w.println("Getting security question.") from webull import webull wb = webull() @@ -675,7 +593,7 @@ def create_secret(self, path): else: break - w.println(f"Requesting MFA code via email.") + w.println("Requesting MFA code via email.") wb.get_mfa(username) mfa = w.get_password("MFA Code: ") @@ -694,12 +612,10 @@ def create_secret(self, path): return False if isinstance(pin, int) and not wb.get_trade_token(pin): - w.println( - f"Trade PIN verification failed... check your info and try again.\nReason: {ret}" - ) + w.println(f"Trade PIN verification failed... check your info and try again.\nReason: {ret}") return False wb.logout() - w.println(f"All steps are complete now 🎉. Generating secret.yml...") + w.println("All steps are complete now 🎉. Generating secret.yml...") return {"wb_username": username, "wb_password": password, "wb_trade_pin": pin} diff --git a/harvest/api/yahoo.py b/harvest/broker/yahoo.py similarity index 85% rename from harvest/api/yahoo.py rename to harvest/broker/yahoo.py index 8d1e74d8..89e2cca0 100644 --- a/harvest/api/yahoo.py +++ b/harvest/broker/yahoo.py @@ -1,23 +1,27 @@ -# Builtins +import datetime import datetime as dt -from typing import Any, Dict, List, Tuple -import requests import re +from typing import Any, Callable, Dict, Union from zoneinfo import ZoneInfo -# External libraries -import tzlocal import pandas as pd +import requests import yfinance as yf -# Submodule imports -from harvest.api._base import API -from harvest.utils import * -from harvest.definitions import * - +from harvest.broker._base import Broker +from harvest.definitions import Account, Stats +from harvest.enum import Interval +from harvest.util.date import convert_input_to_datetime, date_to_str, utc_current_time, utc_epoch_zero +from harvest.util.helper import ( + check_interval, + debugger, + expand_interval, + interval_string_to_enum, + is_crypto, +) -class YahooStreamer(API): +class YahooBroker(Broker): interval_list = [ Interval.MIN_1, Interval.MIN_5, @@ -63,22 +67,18 @@ def unfmt_symbol(self, symbol: str) -> str: def exit(self) -> None: self.option_cache = {} - def main(self) -> None: + def step(self) -> None: df_dict = {} combo = [ self.fmt_symbol(sym) for sym in self.stats.watchlist_cfg - if is_freq(self.stats.timestamp, self.stats.watchlist_cfg[sym]["interval"]) + if check_interval(self.stats.timestamp, self.stats.watchlist_cfg[sym]["interval"]) ] if len(combo) == 1: s = combo[0] - interval_fmt = self.fmt_interval( - self.stats.watchlist_cfg[self.unfmt_symbol(s)]["interval"] - ) - df = yf.download( - s, period="1d", interval=interval_fmt, prepost=True, progress=False - ) + interval_fmt = self.fmt_interval(self.stats.watchlist_cfg[self.unfmt_symbol(s)]["interval"]) + df = yf.download(s, period="1d", interval=interval_fmt, prepost=True, progress=False) debugger.debug(f"From yfinance got: {df}") if len(df.index) == 0: return @@ -122,11 +122,11 @@ def main(self) -> None: df_dict[s] = df_tmp debugger.debug(f"From yfinance dict: {df_dict}") - self.trader_main(df_dict) + self.broker_hub_cb(df_dict) # -------------- Streamer methods -------------- # - @API._exception_handler + @Broker._exception_handler def fetch_price_history( self, symbol: str, @@ -134,7 +134,6 @@ def fetch_price_history( start: Union[str, dt.datetime] = None, end: Union[str, dt.datetime] = None, ) -> pd.DataFrame: - debugger.debug(f"Fetching {symbol} {interval} price history") if isinstance(interval, str): interval = interval_string_to_enum(interval) @@ -143,9 +142,9 @@ def fetch_price_history( end = convert_input_to_datetime(end) if start is None: - start = epoch_zero() + start = utc_epoch_zero() if end is None: - end = now() + end = utc_current_time() df = pd.DataFrame() @@ -172,9 +171,7 @@ def fetch_price_history( symbol = symbol[1:] + "-USD" crypto = True - df = yf.download( - symbol, period=period, interval=get_fmt, prepost=True, progress=False - ) + df = yf.download(symbol, period=period, interval=get_fmt, prepost=True, progress=False) if crypto: symbol = "@" + symbol[:-4] df = self._format_df(df, symbol) @@ -182,30 +179,26 @@ def fetch_price_history( debugger.debug(f"From yfinance got: {df}") return df - @API._exception_handler + @Broker._exception_handler def fetch_chain_info(self, symbol: str) -> Dict[str, Any]: option_list = self.watch_ticker[symbol].options return { "id": "n/a", - "exp_dates": [ - convert_input_to_datetime(s, no_tz=True) for s in option_list - ], + "exp_dates": [convert_input_to_datetime(s, no_tz=True) for s in option_list], "multiplier": 100, } - @API._exception_handler + @Broker._exception_handler def fetch_chain_data(self, symbol: str, date: dt.datetime) -> pd.DataFrame: - - if ( - bool(self.option_cache) - and symbol in self.option_cache - and date in self.option_cache[symbol] - ): + if bool(self.option_cache) and symbol in self.option_cache and date in self.option_cache[symbol]: return self.option_cache[symbol][date] df = pd.DataFrame(columns=["contractSymbol", "exp_date", "strike", "type"]) chain = self.watch_ticker[symbol].option_chain(date_to_str(date)) + + print(f"From yfinance got: {chain}") + puts = chain.puts puts["type"] = "put" calls = chain.calls @@ -214,9 +207,7 @@ def fetch_chain_data(self, symbol: str, date: dt.datetime) -> pd.DataFrame: df = df.append(calls) df = df.rename(columns={"contractSymbol": "occ_symbol"}) - df["exp_date"] = df.apply( - lambda x: self.occ_to_data(x["occ_symbol"])[1], axis=1 - ) + df["exp_date"] = df.apply(lambda x: self.occ_to_data(x["occ_symbol"])[1], axis=1) df = df[["occ_symbol", "exp_date", "strike", "type"]] df.set_index("occ_symbol", inplace=True) @@ -226,7 +217,7 @@ def fetch_chain_data(self, symbol: str, date: dt.datetime) -> pd.DataFrame: return df - @API._exception_handler + @Broker._exception_handler def fetch_option_market_data(self, occ_symbol: str) -> Dict[str, Any]: occ_symbol = occ_symbol.replace(" ", "") symbol, date, typ, _ = self.occ_to_data(occ_symbol) @@ -241,7 +232,7 @@ def fetch_option_market_data(self, occ_symbol: str) -> Dict[str, Any]: "bid": float(df["bid"].iloc[0]), } - @API._exception_handler + @Broker._exception_handler def fetch_market_hours(self, date: datetime.date) -> Dict[str, Any]: # yfinance does not support getting market hours, # so use the free Tradier API instead. diff --git a/harvest/cli.py b/harvest/cli.py index 534cbfe9..f1daf2e9 100644 --- a/harvest/cli.py +++ b/harvest/cli.py @@ -1,14 +1,15 @@ +import argparse +import inspect import os import sys -import inspect -import argparse -from importlib.util import spec_from_file_location, module_from_spec -from importlib import import_module - +from importlib.util import module_from_spec, spec_from_file_location from os import listdir from os.path import isfile, join from typing import Callable +from harvest.util.helper import str_to_data_broker_type, str_to_storage_type, str_to_trade_broker_type + + # Lambda functions cannot raise exceptions so using higher order functions. def _raise(e) -> Callable: def raise_helper(): @@ -17,9 +18,9 @@ def raise_helper(): return raise_helper -from harvest.utils import debugger -from harvest.util.factory import storages, streamers, brokers from harvest.algo import BaseAlgo +from harvest.enum import DataBrokerType, StorageType, TradeBrokerType +from harvest.util.helper import debugger parser = argparse.ArgumentParser(description="Harvest CLI") subparsers = parser.add_subparsers(dest="command") @@ -31,21 +32,21 @@ def raise_helper(): "--storage", default="base", help="the way to store asset data", - choices=list(storages.keys()), + choices=StorageType.list(), ) start_parser.add_argument( "-s", "--streamer", default="yahoo", help="fetches asset data", - choices=list(streamers.keys()), + choices=DataBrokerType.list(), ) start_parser.add_argument( "-b", "--broker", default="streamer", help="buys and sells assets on your behalf", - choices=list(brokers.keys()), + choices=TradeBrokerType.list(), ) # Directory with algos that you want to run, default is the current working directory. @@ -55,9 +56,7 @@ def raise_helper(): default=".", help="directory where algorithms are located", ) -start_parser.add_argument( - "--debug", default=False, action=argparse.BooleanOptionalAction -) +start_parser.add_argument("--debug", default=False, action=argparse.BooleanOptionalAction) # Parser for visualing data @@ -65,8 +64,8 @@ def raise_helper(): visualize_parser.add_argument("path", help="path to harvest generated data file") from rich.console import Console -from rich.tree import Tree from rich.padding import Padding +from rich.tree import Tree def main() -> None: @@ -98,15 +97,18 @@ def start(args: argparse.Namespace, test: bool = False) -> None: broker = args.broker debug = args.debug - from harvest.trader import LiveTrader + storage = str_to_storage_type(storage) + streamer = str_to_data_broker_type(streamer) + broker = str_to_trade_broker_type(broker) - trader = LiveTrader(streamer=streamer, broker=broker, storage=storage, debug=debug) + from harvest.trader import BrokerHub - console = Console() - console.print(f"> [bold green]Welcome to Harvest[/bold green]") + trader = BrokerHub(streamer, broker, storage, debug) - with console.status("[bold green] Loading... [/bold green]") as status: + console = Console() + console.print("> [bold green]Welcome to Harvest[/bold green]") + with console.status("[bold green] Loading... [/bold green]") as _: # Get the directories. directory = args.directory console.print(f"- Searching directory [bold cyan]{directory}[/bold cyan] 🔎") @@ -145,11 +147,9 @@ def start(args: argparse.Namespace, test: bool = False) -> None: dir_pad = Padding(dir_tree, (0, 4)) console.print(dir_pad) - console.print( - f"- Found {len(trader.algo)} algo{'' if algo_count == 1 else 's'} 🎉" - ) + console.print(f"- Found {len(trader.algo)} algo{'' if algo_count == 1 else 's'} 🎉") # status.stop() - console.print(f"> [bold green]Finished loading algorithms[/bold green]") + console.print("> [bold green]Finished loading algorithms[/bold green]") if not test: # console.print(f"Starting trader") @@ -162,8 +162,9 @@ def visualize(args: argparse.Namespace) -> None: :args: A Namespace object containing parsed user arguments. """ import re - import pandas as pd + import mplfinance as mpf + import pandas as pd # Open the file using the appropriate parser. if args.path.endswith(".csv"): @@ -182,7 +183,8 @@ def visualize(args: argparse.Namespace) -> None: path = os.path.basename(args.path) # File names are asset {ticker name}@{interval}.{file format} - file_search = re.search("^(@?[\w]+)@([\w]+).(csv|pickle)$", path) + # file_search = re.search("^(@?[\w]+)@([\w]+).(csv|pickle)$", path) TODO Fix linter error + file_search = re.search("^(@?[]+)@([]+).(csv|pickle)$", path) # Temporary fix symbol, interval = file_search.group(1), file_search.group(2) open_price = df.iloc[0]["open"] close_price = df.iloc[-1]["close"] diff --git a/harvest/definitions.py b/harvest/definitions.py index 72353dd4..ec9e1418 100644 --- a/harvest/definitions.py +++ b/harvest/definitions.py @@ -1,12 +1,13 @@ import datetime as dt from typing import Any, Callable, Dict, Iterable, List -from harvest.utils import symbol_type, occ_to_data, OPTION_QTY_MULTIPLIER + +from harvest.util.helper import occ_to_data, symbol_type + +OPTION_QTY_MULTIPLIER = 100 class Stats: - def __init__( - self, timestamp: dt.datetime = None, timezone=None, watchlist_cfg=None - ) -> None: + def __init__(self, timestamp: dt.datetime = None, timezone=None, watchlist_cfg=None) -> None: self._timestamp = timestamp self._timezone = timezone self._watchlist_cfg = watchlist_cfg @@ -150,7 +151,7 @@ def __init__( def __str__(self) -> str: return f""" - order_id: {self._order_id} + order_id: {self._order_id} symbol: {self._symbol} type: {self._type} side: {self._side} @@ -251,9 +252,7 @@ def get_order(self, order_id: Any) -> Order: if o.order_id == order_id: return o - def add_new_order( - self, symbol: str, order_id: Any, side: str, quantity: float, time_in_force - ) -> None: + def add_new_order(self, symbol: str, order_id: Any, side: str, quantity: float, time_in_force) -> None: if symbol_type(symbol) == "OPTION": base_symbol, _, _, _ = occ_to_data(symbol) self._orders.append( @@ -341,9 +340,7 @@ def update(self, current_price: float): # self._profit_percent = self._profit / (self._avg_price * self._quantity) def buy(self, quantity, price): - self._avg_price = (self._avg_price * self._quantity + price * quantity) / ( - self._quantity + quantity - ) + self._avg_price = (self._avg_price * self._quantity + price * quantity) / (self._quantity + quantity) self._quantity += quantity def sell(self, quantity, price): @@ -403,7 +400,13 @@ def __str__(self): class Positions: - def __init__(self, stock=[], option=[], crypto=[]): + def __init__(self, stock=None, option=None, crypto=None): + if stock is None: + stock = [] + if option is None: + option = [] + if crypto is None: + crypto = [] self._stock = stock self._option = option self._crypto = crypto @@ -471,9 +474,7 @@ def __str__(self): class OptionPosition(Position): - def __init__( - self, symbol, quantity, avg_price, strike, expiration, option_type, multiplier - ): + def __init__(self, symbol, quantity, avg_price, strike, expiration, option_type, multiplier): super().__init__(symbol, quantity, avg_price) self._base_symbol = occ_to_data(symbol)[0] self._strike = strike diff --git a/harvest/enum.py b/harvest/enum.py new file mode 100644 index 00000000..cad9b6e5 --- /dev/null +++ b/harvest/enum.py @@ -0,0 +1,99 @@ +import datetime as dt +from enum import Enum, IntEnum, auto + +from harvest.util.date import str_to_datetime + + +class Interval(IntEnum): + SEC_15 = auto() + MIN_1 = auto() + MIN_5 = auto() + MIN_15 = auto() + MIN_30 = auto() + HR_1 = auto() + DAY_1 = auto() + + +class EnumList(Enum): + @classmethod + def list(cls): + return list(map(lambda c: c.value, cls)) + + +class DataBrokerType(EnumList): + DUMMY = "dummy" + YAHOO = "yahoo" + POLYGON = "polygon" + ROBINHOOD = "robinhood" + ALPACA = "alpaca" + WEBULL = "webull" + + +class TradeBrokerType(EnumList): + PAPER = "paper" + ROBINHOOD = "robinhood" + ALPACA = "alpaca" + WEBULL = "webull" + + +class BrokerType(EnumList): + # Combine both DataBrokerType and TradeBrokerType + DUMMY = "dummy" + YAHOO = "yahoo" + POLYGON = "polygon" + ROBINHOOD = "robinhood" + ALPACA = "alpaca" + WEBULL = "webull" + PAPER = "paper" + BASE_STREAMER = "base_streamer" + + +class StorageType(EnumList): + BASE = "base" + CSV = "csv" + PICKLE = "pickle" + DB = "db" + + +class Timestamp: + """ + A class that represents a timestamp. It can be initialized with a string or a datetime object. + If using a string, it must be in the format "YYYY-MM-DD hh:mm". + If using a datetime object either: + - Pass in a datetime object + - Pass in a series of integers that represent the year, month, day, hour, minute, second, and microsecond. + """ + + def __init__(self, *args) -> None: + if len(args) == 1: + timestamp = args[0] + if isinstance(timestamp, str): + self.timestamp = str_to_datetime(timestamp) + elif isinstance(timestamp, dt.datetime): + self.timestamp = timestamp + else: + raise ValueError(f"Invalid timestamp type {type(timestamp)}") + elif len(args) > 1: + self.timestamp = dt.datetime(*args) + + def __sub__(self, other): + return TimeRange(self.timestamp - other.timestamp) + + +class TimeRange: + """ + A wrapper around a timedelta object that represents a range of time. + It can be initialized with a timedelta object or a series of integers that represent the number of days, hours, and minutes. + """ + + def __init__(self, *args) -> None: + if len(args) == 1: + timerange = args[1] + if isinstance(timerange, dt.timedelta): + self.timerange = timerange + else: + raise ValueError(f"Invalid timestamp type {type(timerange)}") + elif len(args) > 1: + range_list = ["days", "hours", "minutes"] + range_dict = {range_list[i]: arg for i, arg in enumerate(args)} + self.timerange = dt.timedelta(**range_dict) diff --git a/harvest/gui/build/bundle.js b/harvest/gui/build/bundle.js index ce55d1aa..2b269bea 100644 --- a/harvest/gui/build/bundle.js +++ b/harvest/gui/build/bundle.js @@ -4,8 +4,10 @@ var app = (function () { 'use strict'; function noop() { } + // Adapted from https://github.com/then/is-promise/blob/master/index.js + // Distributed under MIT License https://github.com/then/is-promise/blob/master/LICENSE function is_promise(value) { - return value && typeof value === 'object' && typeof value.then === 'function'; + return !!value && (typeof value === 'object' || typeof value === 'function') && typeof value.then === 'function'; } function add_location(element, file, line, column, char) { element.__svelte_meta = { @@ -37,7 +39,9 @@ var app = (function () { target.insertBefore(node, anchor || null); } function detach(node) { - node.parentNode.removeChild(node); + if (node.parentNode) { + node.parentNode.removeChild(node); + } } function destroy_each(iterations, detaching) { for (let i = 0; i < iterations.length; i += 1) { @@ -63,9 +67,9 @@ var app = (function () { function children(element) { return Array.from(element.childNodes); } - function custom_event(type, detail, bubbles = false) { + function custom_event(type, detail, { bubbles = false, cancelable = false } = {}) { const e = document.createEvent('CustomEvent'); - e.initCustomEvent(type, bubbles, false, detail); + e.initCustomEvent(type, bubbles, cancelable, detail); return e; } @@ -81,9 +85,9 @@ var app = (function () { const dirty_components = []; const binding_callbacks = []; - const render_callbacks = []; + let render_callbacks = []; const flush_callbacks = []; - const resolved_promise = Promise.resolve(); + const resolved_promise = /* @__PURE__ */ Promise.resolve(); let update_scheduled = false; function schedule_update() { if (!update_scheduled) { @@ -94,22 +98,54 @@ var app = (function () { function add_render_callback(fn) { render_callbacks.push(fn); } - let flushing = false; + // flush() calls callbacks in this order: + // 1. All beforeUpdate callbacks, in order: parents before children + // 2. All bind:this callbacks, in reverse order: children before parents. + // 3. All afterUpdate callbacks, in order: parents before children. EXCEPT + // for afterUpdates called during the initial onMount, which are called in + // reverse order: children before parents. + // Since callbacks might update component values, which could trigger another + // call to flush(), the following steps guard against this: + // 1. During beforeUpdate, any updated components will be added to the + // dirty_components array and will cause a reentrant call to flush(). Because + // the flush index is kept outside the function, the reentrant call will pick + // up where the earlier call left off and go through all dirty components. The + // current_component value is saved and restored so that the reentrant call will + // not interfere with the "parent" flush() call. + // 2. bind:this callbacks cannot trigger new flush() calls. + // 3. During afterUpdate, any updated components will NOT have their afterUpdate + // callback called a second time; the seen_callbacks set, outside the flush() + // function, guarantees this behavior. const seen_callbacks = new Set(); + let flushidx = 0; // Do *not* move this inside the flush() function function flush() { - if (flushing) + // Do not reenter flush while dirty components are updated, as this can + // result in an infinite loop. Instead, let the inner flush handle it. + // Reentrancy is ok afterwards for bindings etc. + if (flushidx !== 0) { return; - flushing = true; + } + const saved_component = current_component; do { // first, call beforeUpdate functions // and update components - for (let i = 0; i < dirty_components.length; i += 1) { - const component = dirty_components[i]; - set_current_component(component); - update(component.$$); + try { + while (flushidx < dirty_components.length) { + const component = dirty_components[flushidx]; + flushidx++; + set_current_component(component); + update(component.$$); + } + } + catch (e) { + // reset dirty state to not end up in a deadlocked state and then rethrow + dirty_components.length = 0; + flushidx = 0; + throw e; } set_current_component(null); dirty_components.length = 0; + flushidx = 0; while (binding_callbacks.length) binding_callbacks.pop()(); // then, once components are updated, call @@ -129,8 +165,8 @@ var app = (function () { flush_callbacks.pop()(); } update_scheduled = false; - flushing = false; seen_callbacks.clear(); + set_current_component(saved_component); } function update($$) { if ($$.fragment !== null) { @@ -142,6 +178,16 @@ var app = (function () { $$.after_update.forEach(add_render_callback); } } + /** + * Useful for example to execute remaining `afterUpdate` callbacks before executing `destroy`. + */ + function flush_render_callbacks(fns) { + const filtered = []; + const targets = []; + render_callbacks.forEach((c) => fns.indexOf(c) === -1 ? filtered.push(c) : targets.push(c)); + targets.forEach((c) => c()); + render_callbacks = filtered; + } const outroing = new Set(); let outros; function group_outros() { @@ -178,6 +224,9 @@ var app = (function () { }); block.o(local); } + else if (callback) { + callback(); + } } function handle_promise(promise, info) { @@ -262,14 +311,17 @@ var app = (function () { info.block.p(child_ctx, dirty); } function mount_component(component, target, anchor, customElement) { - const { fragment, on_mount, on_destroy, after_update } = component.$$; + const { fragment, after_update } = component.$$; fragment && fragment.m(target, anchor); if (!customElement) { // onMount happens before the initial afterUpdate add_render_callback(() => { - const new_on_destroy = on_mount.map(run).filter(is_function); - if (on_destroy) { - on_destroy.push(...new_on_destroy); + const new_on_destroy = component.$$.on_mount.map(run).filter(is_function); + // if the component was destroyed immediately + // it will update the `$$.on_destroy` reference to `null`. + // the destructured on_destroy may still reference to the old array + if (component.$$.on_destroy) { + component.$$.on_destroy.push(...new_on_destroy); } else { // Edge case - component was destroyed immediately, @@ -284,6 +336,7 @@ var app = (function () { function destroy_component(component, detaching) { const $$ = component.$$; if ($$.fragment !== null) { + flush_render_callbacks($$.after_update); run_all($$.on_destroy); $$.fragment && $$.fragment.d(detaching); // TODO null out other refs, including component.$$ (but need to @@ -305,7 +358,7 @@ var app = (function () { set_current_component(component); const $$ = component.$$ = { fragment: null, - ctx: null, + ctx: [], // state props, update: noop, @@ -317,7 +370,7 @@ var app = (function () { on_disconnect: [], before_update: [], after_update: [], - context: new Map(parent_component ? parent_component.$$.context : options.context || []), + context: new Map(options.context || (parent_component ? parent_component.$$.context : [])), // everything else callbacks: blank_object(), dirty, @@ -370,6 +423,9 @@ var app = (function () { this.$destroy = noop; } $on(type, callback) { + if (!is_function(callback)) { + return noop; + } const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = [])); callbacks.push(callback); return () => { @@ -388,7 +444,7 @@ var app = (function () { } function dispatch_dev(type, detail) { - document.dispatchEvent(custom_event(type, Object.assign({ version: '3.42.2' }, detail), true)); + document.dispatchEvent(custom_event(type, Object.assign({ version: '3.59.2' }, detail), { bubbles: true })); } function append_dev(target, node) { dispatch_dev('SvelteDOMInsert', { target, node }); @@ -411,7 +467,7 @@ var app = (function () { } function set_data_dev(text, data) { data = '' + data; - if (text.wholeText === data) + if (text.data === data) return; dispatch_dev('SvelteDOMSetData', { node: text, data }); text.data = data; @@ -452,7 +508,7 @@ var app = (function () { $inject_state() { } } - /* src/App.svelte generated by Svelte v3.42.2 */ + /* src/App.svelte generated by Svelte v3.59.2 */ const file = "src/App.svelte"; @@ -523,7 +579,9 @@ var app = (function () { insert_dev(target, ul, anchor); for (let i = 0; i < each_blocks.length; i += 1) { - each_blocks[i].m(ul, null); + if (each_blocks[i]) { + each_blocks[i].m(ul, null); + } } }, p: function update(ctx, dirty) { @@ -725,6 +783,12 @@ var app = (function () { return ret; })(); + $$self.$$.on_mount.push(function () { + if (name === undefined && !('name' in $$props || $$self.$$.bound[$$self.$$.props['name']])) { + console.warn(" was created without expected prop 'name'"); + } + }); + const writable_props = ['name']; Object.keys($$props).forEach(key => { @@ -759,13 +823,6 @@ var app = (function () { options, id: create_fragment.name }); - - const { ctx } = this.$$; - const props = options.props || {}; - - if (/*name*/ ctx[0] === undefined && !('name' in props)) { - console.warn(" was created without expected prop 'name'"); - } } get name() { @@ -786,5 +843,5 @@ var app = (function () { return app; -}()); +})(); //# sourceMappingURL=bundle.js.map diff --git a/harvest/gui/build/bundle.js.map b/harvest/gui/build/bundle.js.map index 8061b4dd..fbf63423 100644 --- a/harvest/gui/build/bundle.js.map +++ b/harvest/gui/build/bundle.js.map @@ -1 +1 @@ -{"version":3,"file":"bundle.js","sources":["../../../gui/node_modules/svelte/internal/index.mjs","../../../gui/src/App.svelte","../../../gui/src/main.js"],"sourcesContent":["function noop() { }\nconst identity = x => x;\nfunction assign(tar, src) {\n // @ts-ignore\n for (const k in src)\n tar[k] = src[k];\n return tar;\n}\nfunction is_promise(value) {\n return value && typeof value === 'object' && typeof value.then === 'function';\n}\nfunction add_location(element, file, line, column, char) {\n element.__svelte_meta = {\n loc: { file, line, column, char }\n };\n}\nfunction run(fn) {\n return fn();\n}\nfunction blank_object() {\n return Object.create(null);\n}\nfunction run_all(fns) {\n fns.forEach(run);\n}\nfunction is_function(thing) {\n return typeof thing === 'function';\n}\nfunction safe_not_equal(a, b) {\n return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function');\n}\nlet src_url_equal_anchor;\nfunction src_url_equal(element_src, url) {\n if (!src_url_equal_anchor) {\n src_url_equal_anchor = document.createElement('a');\n }\n src_url_equal_anchor.href = url;\n return element_src === src_url_equal_anchor.href;\n}\nfunction not_equal(a, b) {\n return a != a ? b == b : a !== b;\n}\nfunction is_empty(obj) {\n return Object.keys(obj).length === 0;\n}\nfunction validate_store(store, name) {\n if (store != null && typeof store.subscribe !== 'function') {\n throw new Error(`'${name}' is not a store with a 'subscribe' method`);\n }\n}\nfunction subscribe(store, ...callbacks) {\n if (store == null) {\n return noop;\n }\n const unsub = store.subscribe(...callbacks);\n return unsub.unsubscribe ? () => unsub.unsubscribe() : unsub;\n}\nfunction get_store_value(store) {\n let value;\n subscribe(store, _ => value = _)();\n return value;\n}\nfunction component_subscribe(component, store, callback) {\n component.$$.on_destroy.push(subscribe(store, callback));\n}\nfunction create_slot(definition, ctx, $$scope, fn) {\n if (definition) {\n const slot_ctx = get_slot_context(definition, ctx, $$scope, fn);\n return definition[0](slot_ctx);\n }\n}\nfunction get_slot_context(definition, ctx, $$scope, fn) {\n return definition[1] && fn\n ? assign($$scope.ctx.slice(), definition[1](fn(ctx)))\n : $$scope.ctx;\n}\nfunction get_slot_changes(definition, $$scope, dirty, fn) {\n if (definition[2] && fn) {\n const lets = definition[2](fn(dirty));\n if ($$scope.dirty === undefined) {\n return lets;\n }\n if (typeof lets === 'object') {\n const merged = [];\n const len = Math.max($$scope.dirty.length, lets.length);\n for (let i = 0; i < len; i += 1) {\n merged[i] = $$scope.dirty[i] | lets[i];\n }\n return merged;\n }\n return $$scope.dirty | lets;\n }\n return $$scope.dirty;\n}\nfunction update_slot_base(slot, slot_definition, ctx, $$scope, slot_changes, get_slot_context_fn) {\n if (slot_changes) {\n const slot_context = get_slot_context(slot_definition, ctx, $$scope, get_slot_context_fn);\n slot.p(slot_context, slot_changes);\n }\n}\nfunction update_slot(slot, slot_definition, ctx, $$scope, dirty, get_slot_changes_fn, get_slot_context_fn) {\n const slot_changes = get_slot_changes(slot_definition, $$scope, dirty, get_slot_changes_fn);\n update_slot_base(slot, slot_definition, ctx, $$scope, slot_changes, get_slot_context_fn);\n}\nfunction get_all_dirty_from_scope($$scope) {\n if ($$scope.ctx.length > 32) {\n const dirty = [];\n const length = $$scope.ctx.length / 32;\n for (let i = 0; i < length; i++) {\n dirty[i] = -1;\n }\n return dirty;\n }\n return -1;\n}\nfunction exclude_internal_props(props) {\n const result = {};\n for (const k in props)\n if (k[0] !== '$')\n result[k] = props[k];\n return result;\n}\nfunction compute_rest_props(props, keys) {\n const rest = {};\n keys = new Set(keys);\n for (const k in props)\n if (!keys.has(k) && k[0] !== '$')\n rest[k] = props[k];\n return rest;\n}\nfunction compute_slots(slots) {\n const result = {};\n for (const key in slots) {\n result[key] = true;\n }\n return result;\n}\nfunction once(fn) {\n let ran = false;\n return function (...args) {\n if (ran)\n return;\n ran = true;\n fn.call(this, ...args);\n };\n}\nfunction null_to_empty(value) {\n return value == null ? '' : value;\n}\nfunction set_store_value(store, ret, value) {\n store.set(value);\n return ret;\n}\nconst has_prop = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);\nfunction action_destroyer(action_result) {\n return action_result && is_function(action_result.destroy) ? action_result.destroy : noop;\n}\n\nconst is_client = typeof window !== 'undefined';\nlet now = is_client\n ? () => window.performance.now()\n : () => Date.now();\nlet raf = is_client ? cb => requestAnimationFrame(cb) : noop;\n// used internally for testing\nfunction set_now(fn) {\n now = fn;\n}\nfunction set_raf(fn) {\n raf = fn;\n}\n\nconst tasks = new Set();\nfunction run_tasks(now) {\n tasks.forEach(task => {\n if (!task.c(now)) {\n tasks.delete(task);\n task.f();\n }\n });\n if (tasks.size !== 0)\n raf(run_tasks);\n}\n/**\n * For testing purposes only!\n */\nfunction clear_loops() {\n tasks.clear();\n}\n/**\n * Creates a new task that runs on each raf frame\n * until it returns a falsy value or is aborted\n */\nfunction loop(callback) {\n let task;\n if (tasks.size === 0)\n raf(run_tasks);\n return {\n promise: new Promise(fulfill => {\n tasks.add(task = { c: callback, f: fulfill });\n }),\n abort() {\n tasks.delete(task);\n }\n };\n}\n\n// Track which nodes are claimed during hydration. Unclaimed nodes can then be removed from the DOM\n// at the end of hydration without touching the remaining nodes.\nlet is_hydrating = false;\nfunction start_hydrating() {\n is_hydrating = true;\n}\nfunction end_hydrating() {\n is_hydrating = false;\n}\nfunction upper_bound(low, high, key, value) {\n // Return first index of value larger than input value in the range [low, high)\n while (low < high) {\n const mid = low + ((high - low) >> 1);\n if (key(mid) <= value) {\n low = mid + 1;\n }\n else {\n high = mid;\n }\n }\n return low;\n}\nfunction init_hydrate(target) {\n if (target.hydrate_init)\n return;\n target.hydrate_init = true;\n // We know that all children have claim_order values since the unclaimed have been detached if target is not \n let children = target.childNodes;\n // If target is , there may be children without claim_order\n if (target.nodeName === 'HEAD') {\n const myChildren = [];\n for (let i = 0; i < children.length; i++) {\n const node = children[i];\n if (node.claim_order !== undefined) {\n myChildren.push(node);\n }\n }\n children = myChildren;\n }\n /*\n * Reorder claimed children optimally.\n * We can reorder claimed children optimally by finding the longest subsequence of\n * nodes that are already claimed in order and only moving the rest. The longest\n * subsequence subsequence of nodes that are claimed in order can be found by\n * computing the longest increasing subsequence of .claim_order values.\n *\n * This algorithm is optimal in generating the least amount of reorder operations\n * possible.\n *\n * Proof:\n * We know that, given a set of reordering operations, the nodes that do not move\n * always form an increasing subsequence, since they do not move among each other\n * meaning that they must be already ordered among each other. Thus, the maximal\n * set of nodes that do not move form a longest increasing subsequence.\n */\n // Compute longest increasing subsequence\n // m: subsequence length j => index k of smallest value that ends an increasing subsequence of length j\n const m = new Int32Array(children.length + 1);\n // Predecessor indices + 1\n const p = new Int32Array(children.length);\n m[0] = -1;\n let longest = 0;\n for (let i = 0; i < children.length; i++) {\n const current = children[i].claim_order;\n // Find the largest subsequence length such that it ends in a value less than our current value\n // upper_bound returns first greater value, so we subtract one\n // with fast path for when we are on the current longest subsequence\n const seqLen = ((longest > 0 && children[m[longest]].claim_order <= current) ? longest + 1 : upper_bound(1, longest, idx => children[m[idx]].claim_order, current)) - 1;\n p[i] = m[seqLen] + 1;\n const newLen = seqLen + 1;\n // We can guarantee that current is the smallest value. Otherwise, we would have generated a longer sequence.\n m[newLen] = i;\n longest = Math.max(newLen, longest);\n }\n // The longest increasing subsequence of nodes (initially reversed)\n const lis = [];\n // The rest of the nodes, nodes that will be moved\n const toMove = [];\n let last = children.length - 1;\n for (let cur = m[longest] + 1; cur != 0; cur = p[cur - 1]) {\n lis.push(children[cur - 1]);\n for (; last >= cur; last--) {\n toMove.push(children[last]);\n }\n last--;\n }\n for (; last >= 0; last--) {\n toMove.push(children[last]);\n }\n lis.reverse();\n // We sort the nodes being moved to guarantee that their insertion order matches the claim order\n toMove.sort((a, b) => a.claim_order - b.claim_order);\n // Finally, we move the nodes\n for (let i = 0, j = 0; i < toMove.length; i++) {\n while (j < lis.length && toMove[i].claim_order >= lis[j].claim_order) {\n j++;\n }\n const anchor = j < lis.length ? lis[j] : null;\n target.insertBefore(toMove[i], anchor);\n }\n}\nfunction append(target, node) {\n target.appendChild(node);\n}\nfunction append_styles(target, style_sheet_id, styles) {\n const append_styles_to = get_root_for_style(target);\n if (!append_styles_to.getElementById(style_sheet_id)) {\n const style = element('style');\n style.id = style_sheet_id;\n style.textContent = styles;\n append_stylesheet(append_styles_to, style);\n }\n}\nfunction get_root_for_style(node) {\n if (!node)\n return document;\n const root = node.getRootNode ? node.getRootNode() : node.ownerDocument;\n if (root.host) {\n return root;\n }\n return document;\n}\nfunction append_empty_stylesheet(node) {\n const style_element = element('style');\n append_stylesheet(get_root_for_style(node), style_element);\n return style_element;\n}\nfunction append_stylesheet(node, style) {\n append(node.head || node, style);\n}\nfunction append_hydration(target, node) {\n if (is_hydrating) {\n init_hydrate(target);\n if ((target.actual_end_child === undefined) || ((target.actual_end_child !== null) && (target.actual_end_child.parentElement !== target))) {\n target.actual_end_child = target.firstChild;\n }\n // Skip nodes of undefined ordering\n while ((target.actual_end_child !== null) && (target.actual_end_child.claim_order === undefined)) {\n target.actual_end_child = target.actual_end_child.nextSibling;\n }\n if (node !== target.actual_end_child) {\n // We only insert if the ordering of this node should be modified or the parent node is not target\n if (node.claim_order !== undefined || node.parentNode !== target) {\n target.insertBefore(node, target.actual_end_child);\n }\n }\n else {\n target.actual_end_child = node.nextSibling;\n }\n }\n else if (node.parentNode !== target || node.nextSibling !== null) {\n target.appendChild(node);\n }\n}\nfunction insert(target, node, anchor) {\n target.insertBefore(node, anchor || null);\n}\nfunction insert_hydration(target, node, anchor) {\n if (is_hydrating && !anchor) {\n append_hydration(target, node);\n }\n else if (node.parentNode !== target || node.nextSibling != anchor) {\n target.insertBefore(node, anchor || null);\n }\n}\nfunction detach(node) {\n node.parentNode.removeChild(node);\n}\nfunction destroy_each(iterations, detaching) {\n for (let i = 0; i < iterations.length; i += 1) {\n if (iterations[i])\n iterations[i].d(detaching);\n }\n}\nfunction element(name) {\n return document.createElement(name);\n}\nfunction element_is(name, is) {\n return document.createElement(name, { is });\n}\nfunction object_without_properties(obj, exclude) {\n const target = {};\n for (const k in obj) {\n if (has_prop(obj, k)\n // @ts-ignore\n && exclude.indexOf(k) === -1) {\n // @ts-ignore\n target[k] = obj[k];\n }\n }\n return target;\n}\nfunction svg_element(name) {\n return document.createElementNS('http://www.w3.org/2000/svg', name);\n}\nfunction text(data) {\n return document.createTextNode(data);\n}\nfunction space() {\n return text(' ');\n}\nfunction empty() {\n return text('');\n}\nfunction listen(node, event, handler, options) {\n node.addEventListener(event, handler, options);\n return () => node.removeEventListener(event, handler, options);\n}\nfunction prevent_default(fn) {\n return function (event) {\n event.preventDefault();\n // @ts-ignore\n return fn.call(this, event);\n };\n}\nfunction stop_propagation(fn) {\n return function (event) {\n event.stopPropagation();\n // @ts-ignore\n return fn.call(this, event);\n };\n}\nfunction self(fn) {\n return function (event) {\n // @ts-ignore\n if (event.target === this)\n fn.call(this, event);\n };\n}\nfunction trusted(fn) {\n return function (event) {\n // @ts-ignore\n if (event.isTrusted)\n fn.call(this, event);\n };\n}\nfunction attr(node, attribute, value) {\n if (value == null)\n node.removeAttribute(attribute);\n else if (node.getAttribute(attribute) !== value)\n node.setAttribute(attribute, value);\n}\nfunction set_attributes(node, attributes) {\n // @ts-ignore\n const descriptors = Object.getOwnPropertyDescriptors(node.__proto__);\n for (const key in attributes) {\n if (attributes[key] == null) {\n node.removeAttribute(key);\n }\n else if (key === 'style') {\n node.style.cssText = attributes[key];\n }\n else if (key === '__value') {\n node.value = node[key] = attributes[key];\n }\n else if (descriptors[key] && descriptors[key].set) {\n node[key] = attributes[key];\n }\n else {\n attr(node, key, attributes[key]);\n }\n }\n}\nfunction set_svg_attributes(node, attributes) {\n for (const key in attributes) {\n attr(node, key, attributes[key]);\n }\n}\nfunction set_custom_element_data(node, prop, value) {\n if (prop in node) {\n node[prop] = typeof node[prop] === 'boolean' && value === '' ? true : value;\n }\n else {\n attr(node, prop, value);\n }\n}\nfunction xlink_attr(node, attribute, value) {\n node.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);\n}\nfunction get_binding_group_value(group, __value, checked) {\n const value = new Set();\n for (let i = 0; i < group.length; i += 1) {\n if (group[i].checked)\n value.add(group[i].__value);\n }\n if (!checked) {\n value.delete(__value);\n }\n return Array.from(value);\n}\nfunction to_number(value) {\n return value === '' ? null : +value;\n}\nfunction time_ranges_to_array(ranges) {\n const array = [];\n for (let i = 0; i < ranges.length; i += 1) {\n array.push({ start: ranges.start(i), end: ranges.end(i) });\n }\n return array;\n}\nfunction children(element) {\n return Array.from(element.childNodes);\n}\nfunction init_claim_info(nodes) {\n if (nodes.claim_info === undefined) {\n nodes.claim_info = { last_index: 0, total_claimed: 0 };\n }\n}\nfunction claim_node(nodes, predicate, processNode, createNode, dontUpdateLastIndex = false) {\n // Try to find nodes in an order such that we lengthen the longest increasing subsequence\n init_claim_info(nodes);\n const resultNode = (() => {\n // We first try to find an element after the previous one\n for (let i = nodes.claim_info.last_index; i < nodes.length; i++) {\n const node = nodes[i];\n if (predicate(node)) {\n const replacement = processNode(node);\n if (replacement === undefined) {\n nodes.splice(i, 1);\n }\n else {\n nodes[i] = replacement;\n }\n if (!dontUpdateLastIndex) {\n nodes.claim_info.last_index = i;\n }\n return node;\n }\n }\n // Otherwise, we try to find one before\n // We iterate in reverse so that we don't go too far back\n for (let i = nodes.claim_info.last_index - 1; i >= 0; i--) {\n const node = nodes[i];\n if (predicate(node)) {\n const replacement = processNode(node);\n if (replacement === undefined) {\n nodes.splice(i, 1);\n }\n else {\n nodes[i] = replacement;\n }\n if (!dontUpdateLastIndex) {\n nodes.claim_info.last_index = i;\n }\n else if (replacement === undefined) {\n // Since we spliced before the last_index, we decrease it\n nodes.claim_info.last_index--;\n }\n return node;\n }\n }\n // If we can't find any matching node, we create a new one\n return createNode();\n })();\n resultNode.claim_order = nodes.claim_info.total_claimed;\n nodes.claim_info.total_claimed += 1;\n return resultNode;\n}\nfunction claim_element_base(nodes, name, attributes, create_element) {\n return claim_node(nodes, (node) => node.nodeName === name, (node) => {\n const remove = [];\n for (let j = 0; j < node.attributes.length; j++) {\n const attribute = node.attributes[j];\n if (!attributes[attribute.name]) {\n remove.push(attribute.name);\n }\n }\n remove.forEach(v => node.removeAttribute(v));\n return undefined;\n }, () => create_element(name));\n}\nfunction claim_element(nodes, name, attributes) {\n return claim_element_base(nodes, name, attributes, element);\n}\nfunction claim_svg_element(nodes, name, attributes) {\n return claim_element_base(nodes, name, attributes, svg_element);\n}\nfunction claim_text(nodes, data) {\n return claim_node(nodes, (node) => node.nodeType === 3, (node) => {\n const dataStr = '' + data;\n if (node.data.startsWith(dataStr)) {\n if (node.data.length !== dataStr.length) {\n return node.splitText(dataStr.length);\n }\n }\n else {\n node.data = dataStr;\n }\n }, () => text(data), true // Text nodes should not update last index since it is likely not worth it to eliminate an increasing subsequence of actual elements\n );\n}\nfunction claim_space(nodes) {\n return claim_text(nodes, ' ');\n}\nfunction find_comment(nodes, text, start) {\n for (let i = start; i < nodes.length; i += 1) {\n const node = nodes[i];\n if (node.nodeType === 8 /* comment node */ && node.textContent.trim() === text) {\n return i;\n }\n }\n return nodes.length;\n}\nfunction claim_html_tag(nodes) {\n // find html opening tag\n const start_index = find_comment(nodes, 'HTML_TAG_START', 0);\n const end_index = find_comment(nodes, 'HTML_TAG_END', start_index);\n if (start_index === end_index) {\n return new HtmlTagHydration();\n }\n init_claim_info(nodes);\n const html_tag_nodes = nodes.splice(start_index, end_index + 1);\n detach(html_tag_nodes[0]);\n detach(html_tag_nodes[html_tag_nodes.length - 1]);\n const claimed_nodes = html_tag_nodes.slice(1, html_tag_nodes.length - 1);\n for (const n of claimed_nodes) {\n n.claim_order = nodes.claim_info.total_claimed;\n nodes.claim_info.total_claimed += 1;\n }\n return new HtmlTagHydration(claimed_nodes);\n}\nfunction set_data(text, data) {\n data = '' + data;\n if (text.wholeText !== data)\n text.data = data;\n}\nfunction set_input_value(input, value) {\n input.value = value == null ? '' : value;\n}\nfunction set_input_type(input, type) {\n try {\n input.type = type;\n }\n catch (e) {\n // do nothing\n }\n}\nfunction set_style(node, key, value, important) {\n node.style.setProperty(key, value, important ? 'important' : '');\n}\nfunction select_option(select, value) {\n for (let i = 0; i < select.options.length; i += 1) {\n const option = select.options[i];\n if (option.__value === value) {\n option.selected = true;\n return;\n }\n }\n select.selectedIndex = -1; // no option should be selected\n}\nfunction select_options(select, value) {\n for (let i = 0; i < select.options.length; i += 1) {\n const option = select.options[i];\n option.selected = ~value.indexOf(option.__value);\n }\n}\nfunction select_value(select) {\n const selected_option = select.querySelector(':checked') || select.options[0];\n return selected_option && selected_option.__value;\n}\nfunction select_multiple_value(select) {\n return [].map.call(select.querySelectorAll(':checked'), option => option.__value);\n}\n// unfortunately this can't be a constant as that wouldn't be tree-shakeable\n// so we cache the result instead\nlet crossorigin;\nfunction is_crossorigin() {\n if (crossorigin === undefined) {\n crossorigin = false;\n try {\n if (typeof window !== 'undefined' && window.parent) {\n void window.parent.document;\n }\n }\n catch (error) {\n crossorigin = true;\n }\n }\n return crossorigin;\n}\nfunction add_resize_listener(node, fn) {\n const computed_style = getComputedStyle(node);\n if (computed_style.position === 'static') {\n node.style.position = 'relative';\n }\n const iframe = element('iframe');\n iframe.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; width: 100%; height: 100%; ' +\n 'overflow: hidden; border: 0; opacity: 0; pointer-events: none; z-index: -1;');\n iframe.setAttribute('aria-hidden', 'true');\n iframe.tabIndex = -1;\n const crossorigin = is_crossorigin();\n let unsubscribe;\n if (crossorigin) {\n iframe.src = \"data:text/html,\";\n unsubscribe = listen(window, 'message', (event) => {\n if (event.source === iframe.contentWindow)\n fn();\n });\n }\n else {\n iframe.src = 'about:blank';\n iframe.onload = () => {\n unsubscribe = listen(iframe.contentWindow, 'resize', fn);\n };\n }\n append(node, iframe);\n return () => {\n if (crossorigin) {\n unsubscribe();\n }\n else if (unsubscribe && iframe.contentWindow) {\n unsubscribe();\n }\n detach(iframe);\n };\n}\nfunction toggle_class(element, name, toggle) {\n element.classList[toggle ? 'add' : 'remove'](name);\n}\nfunction custom_event(type, detail, bubbles = false) {\n const e = document.createEvent('CustomEvent');\n e.initCustomEvent(type, bubbles, false, detail);\n return e;\n}\nfunction query_selector_all(selector, parent = document.body) {\n return Array.from(parent.querySelectorAll(selector));\n}\nclass HtmlTag {\n constructor() {\n this.e = this.n = null;\n }\n c(html) {\n this.h(html);\n }\n m(html, target, anchor = null) {\n if (!this.e) {\n this.e = element(target.nodeName);\n this.t = target;\n this.c(html);\n }\n this.i(anchor);\n }\n h(html) {\n this.e.innerHTML = html;\n this.n = Array.from(this.e.childNodes);\n }\n i(anchor) {\n for (let i = 0; i < this.n.length; i += 1) {\n insert(this.t, this.n[i], anchor);\n }\n }\n p(html) {\n this.d();\n this.h(html);\n this.i(this.a);\n }\n d() {\n this.n.forEach(detach);\n }\n}\nclass HtmlTagHydration extends HtmlTag {\n constructor(claimed_nodes) {\n super();\n this.e = this.n = null;\n this.l = claimed_nodes;\n }\n c(html) {\n if (this.l) {\n this.n = this.l;\n }\n else {\n super.c(html);\n }\n }\n i(anchor) {\n for (let i = 0; i < this.n.length; i += 1) {\n insert_hydration(this.t, this.n[i], anchor);\n }\n }\n}\nfunction attribute_to_object(attributes) {\n const result = {};\n for (const attribute of attributes) {\n result[attribute.name] = attribute.value;\n }\n return result;\n}\nfunction get_custom_elements_slots(element) {\n const result = {};\n element.childNodes.forEach((node) => {\n result[node.slot || 'default'] = true;\n });\n return result;\n}\n\nconst active_docs = new Set();\nlet active = 0;\n// https://github.com/darkskyapp/string-hash/blob/master/index.js\nfunction hash(str) {\n let hash = 5381;\n let i = str.length;\n while (i--)\n hash = ((hash << 5) - hash) ^ str.charCodeAt(i);\n return hash >>> 0;\n}\nfunction create_rule(node, a, b, duration, delay, ease, fn, uid = 0) {\n const step = 16.666 / duration;\n let keyframes = '{\\n';\n for (let p = 0; p <= 1; p += step) {\n const t = a + (b - a) * ease(p);\n keyframes += p * 100 + `%{${fn(t, 1 - t)}}\\n`;\n }\n const rule = keyframes + `100% {${fn(b, 1 - b)}}\\n}`;\n const name = `__svelte_${hash(rule)}_${uid}`;\n const doc = get_root_for_style(node);\n active_docs.add(doc);\n const stylesheet = doc.__svelte_stylesheet || (doc.__svelte_stylesheet = append_empty_stylesheet(node).sheet);\n const current_rules = doc.__svelte_rules || (doc.__svelte_rules = {});\n if (!current_rules[name]) {\n current_rules[name] = true;\n stylesheet.insertRule(`@keyframes ${name} ${rule}`, stylesheet.cssRules.length);\n }\n const animation = node.style.animation || '';\n node.style.animation = `${animation ? `${animation}, ` : ''}${name} ${duration}ms linear ${delay}ms 1 both`;\n active += 1;\n return name;\n}\nfunction delete_rule(node, name) {\n const previous = (node.style.animation || '').split(', ');\n const next = previous.filter(name\n ? anim => anim.indexOf(name) < 0 // remove specific animation\n : anim => anim.indexOf('__svelte') === -1 // remove all Svelte animations\n );\n const deleted = previous.length - next.length;\n if (deleted) {\n node.style.animation = next.join(', ');\n active -= deleted;\n if (!active)\n clear_rules();\n }\n}\nfunction clear_rules() {\n raf(() => {\n if (active)\n return;\n active_docs.forEach(doc => {\n const stylesheet = doc.__svelte_stylesheet;\n let i = stylesheet.cssRules.length;\n while (i--)\n stylesheet.deleteRule(i);\n doc.__svelte_rules = {};\n });\n active_docs.clear();\n });\n}\n\nfunction create_animation(node, from, fn, params) {\n if (!from)\n return noop;\n const to = node.getBoundingClientRect();\n if (from.left === to.left && from.right === to.right && from.top === to.top && from.bottom === to.bottom)\n return noop;\n const { delay = 0, duration = 300, easing = identity, \n // @ts-ignore todo: should this be separated from destructuring? Or start/end added to public api and documentation?\n start: start_time = now() + delay, \n // @ts-ignore todo:\n end = start_time + duration, tick = noop, css } = fn(node, { from, to }, params);\n let running = true;\n let started = false;\n let name;\n function start() {\n if (css) {\n name = create_rule(node, 0, 1, duration, delay, easing, css);\n }\n if (!delay) {\n started = true;\n }\n }\n function stop() {\n if (css)\n delete_rule(node, name);\n running = false;\n }\n loop(now => {\n if (!started && now >= start_time) {\n started = true;\n }\n if (started && now >= end) {\n tick(1, 0);\n stop();\n }\n if (!running) {\n return false;\n }\n if (started) {\n const p = now - start_time;\n const t = 0 + 1 * easing(p / duration);\n tick(t, 1 - t);\n }\n return true;\n });\n start();\n tick(0, 1);\n return stop;\n}\nfunction fix_position(node) {\n const style = getComputedStyle(node);\n if (style.position !== 'absolute' && style.position !== 'fixed') {\n const { width, height } = style;\n const a = node.getBoundingClientRect();\n node.style.position = 'absolute';\n node.style.width = width;\n node.style.height = height;\n add_transform(node, a);\n }\n}\nfunction add_transform(node, a) {\n const b = node.getBoundingClientRect();\n if (a.left !== b.left || a.top !== b.top) {\n const style = getComputedStyle(node);\n const transform = style.transform === 'none' ? '' : style.transform;\n node.style.transform = `${transform} translate(${a.left - b.left}px, ${a.top - b.top}px)`;\n }\n}\n\nlet current_component;\nfunction set_current_component(component) {\n current_component = component;\n}\nfunction get_current_component() {\n if (!current_component)\n throw new Error('Function called outside component initialization');\n return current_component;\n}\nfunction beforeUpdate(fn) {\n get_current_component().$$.before_update.push(fn);\n}\nfunction onMount(fn) {\n get_current_component().$$.on_mount.push(fn);\n}\nfunction afterUpdate(fn) {\n get_current_component().$$.after_update.push(fn);\n}\nfunction onDestroy(fn) {\n get_current_component().$$.on_destroy.push(fn);\n}\nfunction createEventDispatcher() {\n const component = get_current_component();\n return (type, detail) => {\n const callbacks = component.$$.callbacks[type];\n if (callbacks) {\n // TODO are there situations where events could be dispatched\n // in a server (non-DOM) environment?\n const event = custom_event(type, detail);\n callbacks.slice().forEach(fn => {\n fn.call(component, event);\n });\n }\n };\n}\nfunction setContext(key, context) {\n get_current_component().$$.context.set(key, context);\n}\nfunction getContext(key) {\n return get_current_component().$$.context.get(key);\n}\nfunction getAllContexts() {\n return get_current_component().$$.context;\n}\nfunction hasContext(key) {\n return get_current_component().$$.context.has(key);\n}\n// TODO figure out if we still want to support\n// shorthand events, or if we want to implement\n// a real bubbling mechanism\nfunction bubble(component, event) {\n const callbacks = component.$$.callbacks[event.type];\n if (callbacks) {\n // @ts-ignore\n callbacks.slice().forEach(fn => fn.call(this, event));\n }\n}\n\nconst dirty_components = [];\nconst intros = { enabled: false };\nconst binding_callbacks = [];\nconst render_callbacks = [];\nconst flush_callbacks = [];\nconst resolved_promise = Promise.resolve();\nlet update_scheduled = false;\nfunction schedule_update() {\n if (!update_scheduled) {\n update_scheduled = true;\n resolved_promise.then(flush);\n }\n}\nfunction tick() {\n schedule_update();\n return resolved_promise;\n}\nfunction add_render_callback(fn) {\n render_callbacks.push(fn);\n}\nfunction add_flush_callback(fn) {\n flush_callbacks.push(fn);\n}\nlet flushing = false;\nconst seen_callbacks = new Set();\nfunction flush() {\n if (flushing)\n return;\n flushing = true;\n do {\n // first, call beforeUpdate functions\n // and update components\n for (let i = 0; i < dirty_components.length; i += 1) {\n const component = dirty_components[i];\n set_current_component(component);\n update(component.$$);\n }\n set_current_component(null);\n dirty_components.length = 0;\n while (binding_callbacks.length)\n binding_callbacks.pop()();\n // then, once components are updated, call\n // afterUpdate functions. This may cause\n // subsequent updates...\n for (let i = 0; i < render_callbacks.length; i += 1) {\n const callback = render_callbacks[i];\n if (!seen_callbacks.has(callback)) {\n // ...so guard against infinite loops\n seen_callbacks.add(callback);\n callback();\n }\n }\n render_callbacks.length = 0;\n } while (dirty_components.length);\n while (flush_callbacks.length) {\n flush_callbacks.pop()();\n }\n update_scheduled = false;\n flushing = false;\n seen_callbacks.clear();\n}\nfunction update($$) {\n if ($$.fragment !== null) {\n $$.update();\n run_all($$.before_update);\n const dirty = $$.dirty;\n $$.dirty = [-1];\n $$.fragment && $$.fragment.p($$.ctx, dirty);\n $$.after_update.forEach(add_render_callback);\n }\n}\n\nlet promise;\nfunction wait() {\n if (!promise) {\n promise = Promise.resolve();\n promise.then(() => {\n promise = null;\n });\n }\n return promise;\n}\nfunction dispatch(node, direction, kind) {\n node.dispatchEvent(custom_event(`${direction ? 'intro' : 'outro'}${kind}`));\n}\nconst outroing = new Set();\nlet outros;\nfunction group_outros() {\n outros = {\n r: 0,\n c: [],\n p: outros // parent group\n };\n}\nfunction check_outros() {\n if (!outros.r) {\n run_all(outros.c);\n }\n outros = outros.p;\n}\nfunction transition_in(block, local) {\n if (block && block.i) {\n outroing.delete(block);\n block.i(local);\n }\n}\nfunction transition_out(block, local, detach, callback) {\n if (block && block.o) {\n if (outroing.has(block))\n return;\n outroing.add(block);\n outros.c.push(() => {\n outroing.delete(block);\n if (callback) {\n if (detach)\n block.d(1);\n callback();\n }\n });\n block.o(local);\n }\n}\nconst null_transition = { duration: 0 };\nfunction create_in_transition(node, fn, params) {\n let config = fn(node, params);\n let running = false;\n let animation_name;\n let task;\n let uid = 0;\n function cleanup() {\n if (animation_name)\n delete_rule(node, animation_name);\n }\n function go() {\n const { delay = 0, duration = 300, easing = identity, tick = noop, css } = config || null_transition;\n if (css)\n animation_name = create_rule(node, 0, 1, duration, delay, easing, css, uid++);\n tick(0, 1);\n const start_time = now() + delay;\n const end_time = start_time + duration;\n if (task)\n task.abort();\n running = true;\n add_render_callback(() => dispatch(node, true, 'start'));\n task = loop(now => {\n if (running) {\n if (now >= end_time) {\n tick(1, 0);\n dispatch(node, true, 'end');\n cleanup();\n return running = false;\n }\n if (now >= start_time) {\n const t = easing((now - start_time) / duration);\n tick(t, 1 - t);\n }\n }\n return running;\n });\n }\n let started = false;\n return {\n start() {\n if (started)\n return;\n started = true;\n delete_rule(node);\n if (is_function(config)) {\n config = config();\n wait().then(go);\n }\n else {\n go();\n }\n },\n invalidate() {\n started = false;\n },\n end() {\n if (running) {\n cleanup();\n running = false;\n }\n }\n };\n}\nfunction create_out_transition(node, fn, params) {\n let config = fn(node, params);\n let running = true;\n let animation_name;\n const group = outros;\n group.r += 1;\n function go() {\n const { delay = 0, duration = 300, easing = identity, tick = noop, css } = config || null_transition;\n if (css)\n animation_name = create_rule(node, 1, 0, duration, delay, easing, css);\n const start_time = now() + delay;\n const end_time = start_time + duration;\n add_render_callback(() => dispatch(node, false, 'start'));\n loop(now => {\n if (running) {\n if (now >= end_time) {\n tick(0, 1);\n dispatch(node, false, 'end');\n if (!--group.r) {\n // this will result in `end()` being called,\n // so we don't need to clean up here\n run_all(group.c);\n }\n return false;\n }\n if (now >= start_time) {\n const t = easing((now - start_time) / duration);\n tick(1 - t, t);\n }\n }\n return running;\n });\n }\n if (is_function(config)) {\n wait().then(() => {\n // @ts-ignore\n config = config();\n go();\n });\n }\n else {\n go();\n }\n return {\n end(reset) {\n if (reset && config.tick) {\n config.tick(1, 0);\n }\n if (running) {\n if (animation_name)\n delete_rule(node, animation_name);\n running = false;\n }\n }\n };\n}\nfunction create_bidirectional_transition(node, fn, params, intro) {\n let config = fn(node, params);\n let t = intro ? 0 : 1;\n let running_program = null;\n let pending_program = null;\n let animation_name = null;\n function clear_animation() {\n if (animation_name)\n delete_rule(node, animation_name);\n }\n function init(program, duration) {\n const d = (program.b - t);\n duration *= Math.abs(d);\n return {\n a: t,\n b: program.b,\n d,\n duration,\n start: program.start,\n end: program.start + duration,\n group: program.group\n };\n }\n function go(b) {\n const { delay = 0, duration = 300, easing = identity, tick = noop, css } = config || null_transition;\n const program = {\n start: now() + delay,\n b\n };\n if (!b) {\n // @ts-ignore todo: improve typings\n program.group = outros;\n outros.r += 1;\n }\n if (running_program || pending_program) {\n pending_program = program;\n }\n else {\n // if this is an intro, and there's a delay, we need to do\n // an initial tick and/or apply CSS animation immediately\n if (css) {\n clear_animation();\n animation_name = create_rule(node, t, b, duration, delay, easing, css);\n }\n if (b)\n tick(0, 1);\n running_program = init(program, duration);\n add_render_callback(() => dispatch(node, b, 'start'));\n loop(now => {\n if (pending_program && now > pending_program.start) {\n running_program = init(pending_program, duration);\n pending_program = null;\n dispatch(node, running_program.b, 'start');\n if (css) {\n clear_animation();\n animation_name = create_rule(node, t, running_program.b, running_program.duration, 0, easing, config.css);\n }\n }\n if (running_program) {\n if (now >= running_program.end) {\n tick(t = running_program.b, 1 - t);\n dispatch(node, running_program.b, 'end');\n if (!pending_program) {\n // we're done\n if (running_program.b) {\n // intro — we can tidy up immediately\n clear_animation();\n }\n else {\n // outro — needs to be coordinated\n if (!--running_program.group.r)\n run_all(running_program.group.c);\n }\n }\n running_program = null;\n }\n else if (now >= running_program.start) {\n const p = now - running_program.start;\n t = running_program.a + running_program.d * easing(p / running_program.duration);\n tick(t, 1 - t);\n }\n }\n return !!(running_program || pending_program);\n });\n }\n }\n return {\n run(b) {\n if (is_function(config)) {\n wait().then(() => {\n // @ts-ignore\n config = config();\n go(b);\n });\n }\n else {\n go(b);\n }\n },\n end() {\n clear_animation();\n running_program = pending_program = null;\n }\n };\n}\n\nfunction handle_promise(promise, info) {\n const token = info.token = {};\n function update(type, index, key, value) {\n if (info.token !== token)\n return;\n info.resolved = value;\n let child_ctx = info.ctx;\n if (key !== undefined) {\n child_ctx = child_ctx.slice();\n child_ctx[key] = value;\n }\n const block = type && (info.current = type)(child_ctx);\n let needs_flush = false;\n if (info.block) {\n if (info.blocks) {\n info.blocks.forEach((block, i) => {\n if (i !== index && block) {\n group_outros();\n transition_out(block, 1, 1, () => {\n if (info.blocks[i] === block) {\n info.blocks[i] = null;\n }\n });\n check_outros();\n }\n });\n }\n else {\n info.block.d(1);\n }\n block.c();\n transition_in(block, 1);\n block.m(info.mount(), info.anchor);\n needs_flush = true;\n }\n info.block = block;\n if (info.blocks)\n info.blocks[index] = block;\n if (needs_flush) {\n flush();\n }\n }\n if (is_promise(promise)) {\n const current_component = get_current_component();\n promise.then(value => {\n set_current_component(current_component);\n update(info.then, 1, info.value, value);\n set_current_component(null);\n }, error => {\n set_current_component(current_component);\n update(info.catch, 2, info.error, error);\n set_current_component(null);\n if (!info.hasCatch) {\n throw error;\n }\n });\n // if we previously had a then/catch block, destroy it\n if (info.current !== info.pending) {\n update(info.pending, 0);\n return true;\n }\n }\n else {\n if (info.current !== info.then) {\n update(info.then, 1, info.value, promise);\n return true;\n }\n info.resolved = promise;\n }\n}\nfunction update_await_block_branch(info, ctx, dirty) {\n const child_ctx = ctx.slice();\n const { resolved } = info;\n if (info.current === info.then) {\n child_ctx[info.value] = resolved;\n }\n if (info.current === info.catch) {\n child_ctx[info.error] = resolved;\n }\n info.block.p(child_ctx, dirty);\n}\n\nconst globals = (typeof window !== 'undefined'\n ? window\n : typeof globalThis !== 'undefined'\n ? globalThis\n : global);\n\nfunction destroy_block(block, lookup) {\n block.d(1);\n lookup.delete(block.key);\n}\nfunction outro_and_destroy_block(block, lookup) {\n transition_out(block, 1, 1, () => {\n lookup.delete(block.key);\n });\n}\nfunction fix_and_destroy_block(block, lookup) {\n block.f();\n destroy_block(block, lookup);\n}\nfunction fix_and_outro_and_destroy_block(block, lookup) {\n block.f();\n outro_and_destroy_block(block, lookup);\n}\nfunction update_keyed_each(old_blocks, dirty, get_key, dynamic, ctx, list, lookup, node, destroy, create_each_block, next, get_context) {\n let o = old_blocks.length;\n let n = list.length;\n let i = o;\n const old_indexes = {};\n while (i--)\n old_indexes[old_blocks[i].key] = i;\n const new_blocks = [];\n const new_lookup = new Map();\n const deltas = new Map();\n i = n;\n while (i--) {\n const child_ctx = get_context(ctx, list, i);\n const key = get_key(child_ctx);\n let block = lookup.get(key);\n if (!block) {\n block = create_each_block(key, child_ctx);\n block.c();\n }\n else if (dynamic) {\n block.p(child_ctx, dirty);\n }\n new_lookup.set(key, new_blocks[i] = block);\n if (key in old_indexes)\n deltas.set(key, Math.abs(i - old_indexes[key]));\n }\n const will_move = new Set();\n const did_move = new Set();\n function insert(block) {\n transition_in(block, 1);\n block.m(node, next);\n lookup.set(block.key, block);\n next = block.first;\n n--;\n }\n while (o && n) {\n const new_block = new_blocks[n - 1];\n const old_block = old_blocks[o - 1];\n const new_key = new_block.key;\n const old_key = old_block.key;\n if (new_block === old_block) {\n // do nothing\n next = new_block.first;\n o--;\n n--;\n }\n else if (!new_lookup.has(old_key)) {\n // remove old block\n destroy(old_block, lookup);\n o--;\n }\n else if (!lookup.has(new_key) || will_move.has(new_key)) {\n insert(new_block);\n }\n else if (did_move.has(old_key)) {\n o--;\n }\n else if (deltas.get(new_key) > deltas.get(old_key)) {\n did_move.add(new_key);\n insert(new_block);\n }\n else {\n will_move.add(old_key);\n o--;\n }\n }\n while (o--) {\n const old_block = old_blocks[o];\n if (!new_lookup.has(old_block.key))\n destroy(old_block, lookup);\n }\n while (n)\n insert(new_blocks[n - 1]);\n return new_blocks;\n}\nfunction validate_each_keys(ctx, list, get_context, get_key) {\n const keys = new Set();\n for (let i = 0; i < list.length; i++) {\n const key = get_key(get_context(ctx, list, i));\n if (keys.has(key)) {\n throw new Error('Cannot have duplicate keys in a keyed each');\n }\n keys.add(key);\n }\n}\n\nfunction get_spread_update(levels, updates) {\n const update = {};\n const to_null_out = {};\n const accounted_for = { $$scope: 1 };\n let i = levels.length;\n while (i--) {\n const o = levels[i];\n const n = updates[i];\n if (n) {\n for (const key in o) {\n if (!(key in n))\n to_null_out[key] = 1;\n }\n for (const key in n) {\n if (!accounted_for[key]) {\n update[key] = n[key];\n accounted_for[key] = 1;\n }\n }\n levels[i] = n;\n }\n else {\n for (const key in o) {\n accounted_for[key] = 1;\n }\n }\n }\n for (const key in to_null_out) {\n if (!(key in update))\n update[key] = undefined;\n }\n return update;\n}\nfunction get_spread_object(spread_props) {\n return typeof spread_props === 'object' && spread_props !== null ? spread_props : {};\n}\n\n// source: https://html.spec.whatwg.org/multipage/indices.html\nconst boolean_attributes = new Set([\n 'allowfullscreen',\n 'allowpaymentrequest',\n 'async',\n 'autofocus',\n 'autoplay',\n 'checked',\n 'controls',\n 'default',\n 'defer',\n 'disabled',\n 'formnovalidate',\n 'hidden',\n 'ismap',\n 'loop',\n 'multiple',\n 'muted',\n 'nomodule',\n 'novalidate',\n 'open',\n 'playsinline',\n 'readonly',\n 'required',\n 'reversed',\n 'selected'\n]);\n\nconst invalid_attribute_name_character = /[\\s'\">/=\\u{FDD0}-\\u{FDEF}\\u{FFFE}\\u{FFFF}\\u{1FFFE}\\u{1FFFF}\\u{2FFFE}\\u{2FFFF}\\u{3FFFE}\\u{3FFFF}\\u{4FFFE}\\u{4FFFF}\\u{5FFFE}\\u{5FFFF}\\u{6FFFE}\\u{6FFFF}\\u{7FFFE}\\u{7FFFF}\\u{8FFFE}\\u{8FFFF}\\u{9FFFE}\\u{9FFFF}\\u{AFFFE}\\u{AFFFF}\\u{BFFFE}\\u{BFFFF}\\u{CFFFE}\\u{CFFFF}\\u{DFFFE}\\u{DFFFF}\\u{EFFFE}\\u{EFFFF}\\u{FFFFE}\\u{FFFFF}\\u{10FFFE}\\u{10FFFF}]/u;\n// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2\n// https://infra.spec.whatwg.org/#noncharacter\nfunction spread(args, classes_to_add) {\n const attributes = Object.assign({}, ...args);\n if (classes_to_add) {\n if (attributes.class == null) {\n attributes.class = classes_to_add;\n }\n else {\n attributes.class += ' ' + classes_to_add;\n }\n }\n let str = '';\n Object.keys(attributes).forEach(name => {\n if (invalid_attribute_name_character.test(name))\n return;\n const value = attributes[name];\n if (value === true)\n str += ' ' + name;\n else if (boolean_attributes.has(name.toLowerCase())) {\n if (value)\n str += ' ' + name;\n }\n else if (value != null) {\n str += ` ${name}=\"${value}\"`;\n }\n });\n return str;\n}\nconst escaped = {\n '\"': '"',\n \"'\": ''',\n '&': '&',\n '<': '<',\n '>': '>'\n};\nfunction escape(html) {\n return String(html).replace(/[\"'&<>]/g, match => escaped[match]);\n}\nfunction escape_attribute_value(value) {\n return typeof value === 'string' ? escape(value) : value;\n}\nfunction escape_object(obj) {\n const result = {};\n for (const key in obj) {\n result[key] = escape_attribute_value(obj[key]);\n }\n return result;\n}\nfunction each(items, fn) {\n let str = '';\n for (let i = 0; i < items.length; i += 1) {\n str += fn(items[i], i);\n }\n return str;\n}\nconst missing_component = {\n $$render: () => ''\n};\nfunction validate_component(component, name) {\n if (!component || !component.$$render) {\n if (name === 'svelte:component')\n name += ' this={...}';\n throw new Error(`<${name}> is not a valid SSR component. You may need to review your build config to ensure that dependencies are compiled, rather than imported as pre-compiled modules`);\n }\n return component;\n}\nfunction debug(file, line, column, values) {\n console.log(`{@debug} ${file ? file + ' ' : ''}(${line}:${column})`); // eslint-disable-line no-console\n console.log(values); // eslint-disable-line no-console\n return '';\n}\nlet on_destroy;\nfunction create_ssr_component(fn) {\n function $$render(result, props, bindings, slots, context) {\n const parent_component = current_component;\n const $$ = {\n on_destroy,\n context: new Map(parent_component ? parent_component.$$.context : context || []),\n // these will be immediately discarded\n on_mount: [],\n before_update: [],\n after_update: [],\n callbacks: blank_object()\n };\n set_current_component({ $$ });\n const html = fn(result, props, bindings, slots);\n set_current_component(parent_component);\n return html;\n }\n return {\n render: (props = {}, { $$slots = {}, context = new Map() } = {}) => {\n on_destroy = [];\n const result = { title: '', head: '', css: new Set() };\n const html = $$render(result, props, {}, $$slots, context);\n run_all(on_destroy);\n return {\n html,\n css: {\n code: Array.from(result.css).map(css => css.code).join('\\n'),\n map: null // TODO\n },\n head: result.title + result.head\n };\n },\n $$render\n };\n}\nfunction add_attribute(name, value, boolean) {\n if (value == null || (boolean && !value))\n return '';\n return ` ${name}${value === true ? '' : `=${typeof value === 'string' ? JSON.stringify(escape(value)) : `\"${value}\"`}`}`;\n}\nfunction add_classes(classes) {\n return classes ? ` class=\"${classes}\"` : '';\n}\n\nfunction bind(component, name, callback) {\n const index = component.$$.props[name];\n if (index !== undefined) {\n component.$$.bound[index] = callback;\n callback(component.$$.ctx[index]);\n }\n}\nfunction create_component(block) {\n block && block.c();\n}\nfunction claim_component(block, parent_nodes) {\n block && block.l(parent_nodes);\n}\nfunction mount_component(component, target, anchor, customElement) {\n const { fragment, on_mount, on_destroy, after_update } = component.$$;\n fragment && fragment.m(target, anchor);\n if (!customElement) {\n // onMount happens before the initial afterUpdate\n add_render_callback(() => {\n const new_on_destroy = on_mount.map(run).filter(is_function);\n if (on_destroy) {\n on_destroy.push(...new_on_destroy);\n }\n else {\n // Edge case - component was destroyed immediately,\n // most likely as a result of a binding initialising\n run_all(new_on_destroy);\n }\n component.$$.on_mount = [];\n });\n }\n after_update.forEach(add_render_callback);\n}\nfunction destroy_component(component, detaching) {\n const $$ = component.$$;\n if ($$.fragment !== null) {\n run_all($$.on_destroy);\n $$.fragment && $$.fragment.d(detaching);\n // TODO null out other refs, including component.$$ (but need to\n // preserve final state?)\n $$.on_destroy = $$.fragment = null;\n $$.ctx = [];\n }\n}\nfunction make_dirty(component, i) {\n if (component.$$.dirty[0] === -1) {\n dirty_components.push(component);\n schedule_update();\n component.$$.dirty.fill(0);\n }\n component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));\n}\nfunction init(component, options, instance, create_fragment, not_equal, props, append_styles, dirty = [-1]) {\n const parent_component = current_component;\n set_current_component(component);\n const $$ = component.$$ = {\n fragment: null,\n ctx: null,\n // state\n props,\n update: noop,\n not_equal,\n bound: blank_object(),\n // lifecycle\n on_mount: [],\n on_destroy: [],\n on_disconnect: [],\n before_update: [],\n after_update: [],\n context: new Map(parent_component ? parent_component.$$.context : options.context || []),\n // everything else\n callbacks: blank_object(),\n dirty,\n skip_bound: false,\n root: options.target || parent_component.$$.root\n };\n append_styles && append_styles($$.root);\n let ready = false;\n $$.ctx = instance\n ? instance(component, options.props || {}, (i, ret, ...rest) => {\n const value = rest.length ? rest[0] : ret;\n if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {\n if (!$$.skip_bound && $$.bound[i])\n $$.bound[i](value);\n if (ready)\n make_dirty(component, i);\n }\n return ret;\n })\n : [];\n $$.update();\n ready = true;\n run_all($$.before_update);\n // `false` as a special case of no DOM component\n $$.fragment = create_fragment ? create_fragment($$.ctx) : false;\n if (options.target) {\n if (options.hydrate) {\n start_hydrating();\n const nodes = children(options.target);\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n $$.fragment && $$.fragment.l(nodes);\n nodes.forEach(detach);\n }\n else {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n $$.fragment && $$.fragment.c();\n }\n if (options.intro)\n transition_in(component.$$.fragment);\n mount_component(component, options.target, options.anchor, options.customElement);\n end_hydrating();\n flush();\n }\n set_current_component(parent_component);\n}\nlet SvelteElement;\nif (typeof HTMLElement === 'function') {\n SvelteElement = class extends HTMLElement {\n constructor() {\n super();\n this.attachShadow({ mode: 'open' });\n }\n connectedCallback() {\n const { on_mount } = this.$$;\n this.$$.on_disconnect = on_mount.map(run).filter(is_function);\n // @ts-ignore todo: improve typings\n for (const key in this.$$.slotted) {\n // @ts-ignore todo: improve typings\n this.appendChild(this.$$.slotted[key]);\n }\n }\n attributeChangedCallback(attr, _oldValue, newValue) {\n this[attr] = newValue;\n }\n disconnectedCallback() {\n run_all(this.$$.on_disconnect);\n }\n $destroy() {\n destroy_component(this, 1);\n this.$destroy = noop;\n }\n $on(type, callback) {\n // TODO should this delegate to addEventListener?\n const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));\n callbacks.push(callback);\n return () => {\n const index = callbacks.indexOf(callback);\n if (index !== -1)\n callbacks.splice(index, 1);\n };\n }\n $set($$props) {\n if (this.$$set && !is_empty($$props)) {\n this.$$.skip_bound = true;\n this.$$set($$props);\n this.$$.skip_bound = false;\n }\n }\n };\n}\n/**\n * Base class for Svelte components. Used when dev=false.\n */\nclass SvelteComponent {\n $destroy() {\n destroy_component(this, 1);\n this.$destroy = noop;\n }\n $on(type, callback) {\n const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));\n callbacks.push(callback);\n return () => {\n const index = callbacks.indexOf(callback);\n if (index !== -1)\n callbacks.splice(index, 1);\n };\n }\n $set($$props) {\n if (this.$$set && !is_empty($$props)) {\n this.$$.skip_bound = true;\n this.$$set($$props);\n this.$$.skip_bound = false;\n }\n }\n}\n\nfunction dispatch_dev(type, detail) {\n document.dispatchEvent(custom_event(type, Object.assign({ version: '3.42.2' }, detail), true));\n}\nfunction append_dev(target, node) {\n dispatch_dev('SvelteDOMInsert', { target, node });\n append(target, node);\n}\nfunction append_hydration_dev(target, node) {\n dispatch_dev('SvelteDOMInsert', { target, node });\n append_hydration(target, node);\n}\nfunction insert_dev(target, node, anchor) {\n dispatch_dev('SvelteDOMInsert', { target, node, anchor });\n insert(target, node, anchor);\n}\nfunction insert_hydration_dev(target, node, anchor) {\n dispatch_dev('SvelteDOMInsert', { target, node, anchor });\n insert_hydration(target, node, anchor);\n}\nfunction detach_dev(node) {\n dispatch_dev('SvelteDOMRemove', { node });\n detach(node);\n}\nfunction detach_between_dev(before, after) {\n while (before.nextSibling && before.nextSibling !== after) {\n detach_dev(before.nextSibling);\n }\n}\nfunction detach_before_dev(after) {\n while (after.previousSibling) {\n detach_dev(after.previousSibling);\n }\n}\nfunction detach_after_dev(before) {\n while (before.nextSibling) {\n detach_dev(before.nextSibling);\n }\n}\nfunction listen_dev(node, event, handler, options, has_prevent_default, has_stop_propagation) {\n const modifiers = options === true ? ['capture'] : options ? Array.from(Object.keys(options)) : [];\n if (has_prevent_default)\n modifiers.push('preventDefault');\n if (has_stop_propagation)\n modifiers.push('stopPropagation');\n dispatch_dev('SvelteDOMAddEventListener', { node, event, handler, modifiers });\n const dispose = listen(node, event, handler, options);\n return () => {\n dispatch_dev('SvelteDOMRemoveEventListener', { node, event, handler, modifiers });\n dispose();\n };\n}\nfunction attr_dev(node, attribute, value) {\n attr(node, attribute, value);\n if (value == null)\n dispatch_dev('SvelteDOMRemoveAttribute', { node, attribute });\n else\n dispatch_dev('SvelteDOMSetAttribute', { node, attribute, value });\n}\nfunction prop_dev(node, property, value) {\n node[property] = value;\n dispatch_dev('SvelteDOMSetProperty', { node, property, value });\n}\nfunction dataset_dev(node, property, value) {\n node.dataset[property] = value;\n dispatch_dev('SvelteDOMSetDataset', { node, property, value });\n}\nfunction set_data_dev(text, data) {\n data = '' + data;\n if (text.wholeText === data)\n return;\n dispatch_dev('SvelteDOMSetData', { node: text, data });\n text.data = data;\n}\nfunction validate_each_argument(arg) {\n if (typeof arg !== 'string' && !(arg && typeof arg === 'object' && 'length' in arg)) {\n let msg = '{#each} only iterates over array-like objects.';\n if (typeof Symbol === 'function' && arg && Symbol.iterator in arg) {\n msg += ' You can use a spread to convert this iterable into an array.';\n }\n throw new Error(msg);\n }\n}\nfunction validate_slots(name, slot, keys) {\n for (const slot_key of Object.keys(slot)) {\n if (!~keys.indexOf(slot_key)) {\n console.warn(`<${name}> received an unexpected slot \"${slot_key}\".`);\n }\n }\n}\n/**\n * Base class for Svelte components with some minor dev-enhancements. Used when dev=true.\n */\nclass SvelteComponentDev extends SvelteComponent {\n constructor(options) {\n if (!options || (!options.target && !options.$$inline)) {\n throw new Error(\"'target' is a required option\");\n }\n super();\n }\n $destroy() {\n super.$destroy();\n this.$destroy = () => {\n console.warn('Component was already destroyed'); // eslint-disable-line no-console\n };\n }\n $capture_state() { }\n $inject_state() { }\n}\n/**\n * Base class to create strongly typed Svelte components.\n * This only exists for typing purposes and should be used in `.d.ts` files.\n *\n * ### Example:\n *\n * You have component library on npm called `component-library`, from which\n * you export a component called `MyComponent`. For Svelte+TypeScript users,\n * you want to provide typings. Therefore you create a `index.d.ts`:\n * ```ts\n * import { SvelteComponentTyped } from \"svelte\";\n * export class MyComponent extends SvelteComponentTyped<{foo: string}> {}\n * ```\n * Typing this makes it possible for IDEs like VS Code with the Svelte extension\n * to provide intellisense and to use the component like this in a Svelte file\n * with TypeScript:\n * ```svelte\n * \n * \n * ```\n *\n * #### Why not make this part of `SvelteComponent(Dev)`?\n * Because\n * ```ts\n * class ASubclassOfSvelteComponent extends SvelteComponent<{foo: string}> {}\n * const component: typeof SvelteComponent = ASubclassOfSvelteComponent;\n * ```\n * will throw a type error, so we need to separate the more strictly typed class.\n */\nclass SvelteComponentTyped extends SvelteComponentDev {\n constructor(options) {\n super(options);\n }\n}\nfunction loop_guard(timeout) {\n const start = Date.now();\n return () => {\n if (Date.now() - start > timeout) {\n throw new Error('Infinite loop detected');\n }\n };\n}\n\nexport { HtmlTag, HtmlTagHydration, SvelteComponent, SvelteComponentDev, SvelteComponentTyped, SvelteElement, action_destroyer, add_attribute, add_classes, add_flush_callback, add_location, add_render_callback, add_resize_listener, add_transform, afterUpdate, append, append_dev, append_empty_stylesheet, append_hydration, append_hydration_dev, append_styles, assign, attr, attr_dev, attribute_to_object, beforeUpdate, bind, binding_callbacks, blank_object, bubble, check_outros, children, claim_component, claim_element, claim_html_tag, claim_space, claim_svg_element, claim_text, clear_loops, component_subscribe, compute_rest_props, compute_slots, createEventDispatcher, create_animation, create_bidirectional_transition, create_component, create_in_transition, create_out_transition, create_slot, create_ssr_component, current_component, custom_event, dataset_dev, debug, destroy_block, destroy_component, destroy_each, detach, detach_after_dev, detach_before_dev, detach_between_dev, detach_dev, dirty_components, dispatch_dev, each, element, element_is, empty, end_hydrating, escape, escape_attribute_value, escape_object, escaped, exclude_internal_props, fix_and_destroy_block, fix_and_outro_and_destroy_block, fix_position, flush, getAllContexts, getContext, get_all_dirty_from_scope, get_binding_group_value, get_current_component, get_custom_elements_slots, get_root_for_style, get_slot_changes, get_spread_object, get_spread_update, get_store_value, globals, group_outros, handle_promise, hasContext, has_prop, identity, init, insert, insert_dev, insert_hydration, insert_hydration_dev, intros, invalid_attribute_name_character, is_client, is_crossorigin, is_empty, is_function, is_promise, listen, listen_dev, loop, loop_guard, missing_component, mount_component, noop, not_equal, now, null_to_empty, object_without_properties, onDestroy, onMount, once, outro_and_destroy_block, prevent_default, prop_dev, query_selector_all, raf, run, run_all, safe_not_equal, schedule_update, select_multiple_value, select_option, select_options, select_value, self, setContext, set_attributes, set_current_component, set_custom_element_data, set_data, set_data_dev, set_input_type, set_input_value, set_now, set_raf, set_store_value, set_style, set_svg_attributes, space, spread, src_url_equal, start_hydrating, stop_propagation, subscribe, svg_element, text, tick, time_ranges_to_array, to_number, toggle_class, transition_in, transition_out, trusted, update_await_block_branch, update_keyed_each, update_slot, update_slot_base, validate_component, validate_each_argument, validate_each_keys, validate_slots, validate_store, xlink_attr };\n","\n\n
\n\t

Hello {name}!!!

\n\t{#await positions}\n\t\t

Loading...

\n\t{:then data}\n\t\t
    \n\t\t{#each data as position}\n\t\t\t
  • {position.symbol}
  • \n\t\t{/each}\n\t\t
\n\t{:catch error}\n\t\t

Error: {error}

\n\t{/await}\n\t

\n
\n\n","import App from './App.svelte';\n\nconst app = new App({\n\ttarget: document.body,\n\tprops: {\n\t\tname: 'world'\n\t}\n});\n\nexport default app;"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"bundle.js","sources":["../../../gui/node_modules/svelte/internal/index.mjs","../../../gui/src/App.svelte","../../../gui/src/main.js"],"sourcesContent":["function noop() { }\nconst identity = x => x;\nfunction assign(tar, src) {\n // @ts-ignore\n for (const k in src)\n tar[k] = src[k];\n return tar;\n}\n// Adapted from https://github.com/then/is-promise/blob/master/index.js\n// Distributed under MIT License https://github.com/then/is-promise/blob/master/LICENSE\nfunction is_promise(value) {\n return !!value && (typeof value === 'object' || typeof value === 'function') && typeof value.then === 'function';\n}\nfunction add_location(element, file, line, column, char) {\n element.__svelte_meta = {\n loc: { file, line, column, char }\n };\n}\nfunction run(fn) {\n return fn();\n}\nfunction blank_object() {\n return Object.create(null);\n}\nfunction run_all(fns) {\n fns.forEach(run);\n}\nfunction is_function(thing) {\n return typeof thing === 'function';\n}\nfunction safe_not_equal(a, b) {\n return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function');\n}\nlet src_url_equal_anchor;\nfunction src_url_equal(element_src, url) {\n if (!src_url_equal_anchor) {\n src_url_equal_anchor = document.createElement('a');\n }\n src_url_equal_anchor.href = url;\n return element_src === src_url_equal_anchor.href;\n}\nfunction not_equal(a, b) {\n return a != a ? b == b : a !== b;\n}\nfunction is_empty(obj) {\n return Object.keys(obj).length === 0;\n}\nfunction validate_store(store, name) {\n if (store != null && typeof store.subscribe !== 'function') {\n throw new Error(`'${name}' is not a store with a 'subscribe' method`);\n }\n}\nfunction subscribe(store, ...callbacks) {\n if (store == null) {\n return noop;\n }\n const unsub = store.subscribe(...callbacks);\n return unsub.unsubscribe ? () => unsub.unsubscribe() : unsub;\n}\nfunction get_store_value(store) {\n let value;\n subscribe(store, _ => value = _)();\n return value;\n}\nfunction component_subscribe(component, store, callback) {\n component.$$.on_destroy.push(subscribe(store, callback));\n}\nfunction create_slot(definition, ctx, $$scope, fn) {\n if (definition) {\n const slot_ctx = get_slot_context(definition, ctx, $$scope, fn);\n return definition[0](slot_ctx);\n }\n}\nfunction get_slot_context(definition, ctx, $$scope, fn) {\n return definition[1] && fn\n ? assign($$scope.ctx.slice(), definition[1](fn(ctx)))\n : $$scope.ctx;\n}\nfunction get_slot_changes(definition, $$scope, dirty, fn) {\n if (definition[2] && fn) {\n const lets = definition[2](fn(dirty));\n if ($$scope.dirty === undefined) {\n return lets;\n }\n if (typeof lets === 'object') {\n const merged = [];\n const len = Math.max($$scope.dirty.length, lets.length);\n for (let i = 0; i < len; i += 1) {\n merged[i] = $$scope.dirty[i] | lets[i];\n }\n return merged;\n }\n return $$scope.dirty | lets;\n }\n return $$scope.dirty;\n}\nfunction update_slot_base(slot, slot_definition, ctx, $$scope, slot_changes, get_slot_context_fn) {\n if (slot_changes) {\n const slot_context = get_slot_context(slot_definition, ctx, $$scope, get_slot_context_fn);\n slot.p(slot_context, slot_changes);\n }\n}\nfunction update_slot(slot, slot_definition, ctx, $$scope, dirty, get_slot_changes_fn, get_slot_context_fn) {\n const slot_changes = get_slot_changes(slot_definition, $$scope, dirty, get_slot_changes_fn);\n update_slot_base(slot, slot_definition, ctx, $$scope, slot_changes, get_slot_context_fn);\n}\nfunction get_all_dirty_from_scope($$scope) {\n if ($$scope.ctx.length > 32) {\n const dirty = [];\n const length = $$scope.ctx.length / 32;\n for (let i = 0; i < length; i++) {\n dirty[i] = -1;\n }\n return dirty;\n }\n return -1;\n}\nfunction exclude_internal_props(props) {\n const result = {};\n for (const k in props)\n if (k[0] !== '$')\n result[k] = props[k];\n return result;\n}\nfunction compute_rest_props(props, keys) {\n const rest = {};\n keys = new Set(keys);\n for (const k in props)\n if (!keys.has(k) && k[0] !== '$')\n rest[k] = props[k];\n return rest;\n}\nfunction compute_slots(slots) {\n const result = {};\n for (const key in slots) {\n result[key] = true;\n }\n return result;\n}\nfunction once(fn) {\n let ran = false;\n return function (...args) {\n if (ran)\n return;\n ran = true;\n fn.call(this, ...args);\n };\n}\nfunction null_to_empty(value) {\n return value == null ? '' : value;\n}\nfunction set_store_value(store, ret, value) {\n store.set(value);\n return ret;\n}\nconst has_prop = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);\nfunction action_destroyer(action_result) {\n return action_result && is_function(action_result.destroy) ? action_result.destroy : noop;\n}\nfunction split_css_unit(value) {\n const split = typeof value === 'string' && value.match(/^\\s*(-?[\\d.]+)([^\\s]*)\\s*$/);\n return split ? [parseFloat(split[1]), split[2] || 'px'] : [value, 'px'];\n}\nconst contenteditable_truthy_values = ['', true, 1, 'true', 'contenteditable'];\n\nconst is_client = typeof window !== 'undefined';\nlet now = is_client\n ? () => window.performance.now()\n : () => Date.now();\nlet raf = is_client ? cb => requestAnimationFrame(cb) : noop;\n// used internally for testing\nfunction set_now(fn) {\n now = fn;\n}\nfunction set_raf(fn) {\n raf = fn;\n}\n\nconst tasks = new Set();\nfunction run_tasks(now) {\n tasks.forEach(task => {\n if (!task.c(now)) {\n tasks.delete(task);\n task.f();\n }\n });\n if (tasks.size !== 0)\n raf(run_tasks);\n}\n/**\n * For testing purposes only!\n */\nfunction clear_loops() {\n tasks.clear();\n}\n/**\n * Creates a new task that runs on each raf frame\n * until it returns a falsy value or is aborted\n */\nfunction loop(callback) {\n let task;\n if (tasks.size === 0)\n raf(run_tasks);\n return {\n promise: new Promise(fulfill => {\n tasks.add(task = { c: callback, f: fulfill });\n }),\n abort() {\n tasks.delete(task);\n }\n };\n}\n\nconst globals = (typeof window !== 'undefined'\n ? window\n : typeof globalThis !== 'undefined'\n ? globalThis\n : global);\n\n/**\n * Resize observer singleton.\n * One listener per element only!\n * https://groups.google.com/a/chromium.org/g/blink-dev/c/z6ienONUb5A/m/F5-VcUZtBAAJ\n */\nclass ResizeObserverSingleton {\n constructor(options) {\n this.options = options;\n this._listeners = 'WeakMap' in globals ? new WeakMap() : undefined;\n }\n observe(element, listener) {\n this._listeners.set(element, listener);\n this._getObserver().observe(element, this.options);\n return () => {\n this._listeners.delete(element);\n this._observer.unobserve(element); // this line can probably be removed\n };\n }\n _getObserver() {\n var _a;\n return (_a = this._observer) !== null && _a !== void 0 ? _a : (this._observer = new ResizeObserver((entries) => {\n var _a;\n for (const entry of entries) {\n ResizeObserverSingleton.entries.set(entry.target, entry);\n (_a = this._listeners.get(entry.target)) === null || _a === void 0 ? void 0 : _a(entry);\n }\n }));\n }\n}\n// Needs to be written like this to pass the tree-shake-test\nResizeObserverSingleton.entries = 'WeakMap' in globals ? new WeakMap() : undefined;\n\n// Track which nodes are claimed during hydration. Unclaimed nodes can then be removed from the DOM\n// at the end of hydration without touching the remaining nodes.\nlet is_hydrating = false;\nfunction start_hydrating() {\n is_hydrating = true;\n}\nfunction end_hydrating() {\n is_hydrating = false;\n}\nfunction upper_bound(low, high, key, value) {\n // Return first index of value larger than input value in the range [low, high)\n while (low < high) {\n const mid = low + ((high - low) >> 1);\n if (key(mid) <= value) {\n low = mid + 1;\n }\n else {\n high = mid;\n }\n }\n return low;\n}\nfunction init_hydrate(target) {\n if (target.hydrate_init)\n return;\n target.hydrate_init = true;\n // We know that all children have claim_order values since the unclaimed have been detached if target is not \n let children = target.childNodes;\n // If target is , there may be children without claim_order\n if (target.nodeName === 'HEAD') {\n const myChildren = [];\n for (let i = 0; i < children.length; i++) {\n const node = children[i];\n if (node.claim_order !== undefined) {\n myChildren.push(node);\n }\n }\n children = myChildren;\n }\n /*\n * Reorder claimed children optimally.\n * We can reorder claimed children optimally by finding the longest subsequence of\n * nodes that are already claimed in order and only moving the rest. The longest\n * subsequence of nodes that are claimed in order can be found by\n * computing the longest increasing subsequence of .claim_order values.\n *\n * This algorithm is optimal in generating the least amount of reorder operations\n * possible.\n *\n * Proof:\n * We know that, given a set of reordering operations, the nodes that do not move\n * always form an increasing subsequence, since they do not move among each other\n * meaning that they must be already ordered among each other. Thus, the maximal\n * set of nodes that do not move form a longest increasing subsequence.\n */\n // Compute longest increasing subsequence\n // m: subsequence length j => index k of smallest value that ends an increasing subsequence of length j\n const m = new Int32Array(children.length + 1);\n // Predecessor indices + 1\n const p = new Int32Array(children.length);\n m[0] = -1;\n let longest = 0;\n for (let i = 0; i < children.length; i++) {\n const current = children[i].claim_order;\n // Find the largest subsequence length such that it ends in a value less than our current value\n // upper_bound returns first greater value, so we subtract one\n // with fast path for when we are on the current longest subsequence\n const seqLen = ((longest > 0 && children[m[longest]].claim_order <= current) ? longest + 1 : upper_bound(1, longest, idx => children[m[idx]].claim_order, current)) - 1;\n p[i] = m[seqLen] + 1;\n const newLen = seqLen + 1;\n // We can guarantee that current is the smallest value. Otherwise, we would have generated a longer sequence.\n m[newLen] = i;\n longest = Math.max(newLen, longest);\n }\n // The longest increasing subsequence of nodes (initially reversed)\n const lis = [];\n // The rest of the nodes, nodes that will be moved\n const toMove = [];\n let last = children.length - 1;\n for (let cur = m[longest] + 1; cur != 0; cur = p[cur - 1]) {\n lis.push(children[cur - 1]);\n for (; last >= cur; last--) {\n toMove.push(children[last]);\n }\n last--;\n }\n for (; last >= 0; last--) {\n toMove.push(children[last]);\n }\n lis.reverse();\n // We sort the nodes being moved to guarantee that their insertion order matches the claim order\n toMove.sort((a, b) => a.claim_order - b.claim_order);\n // Finally, we move the nodes\n for (let i = 0, j = 0; i < toMove.length; i++) {\n while (j < lis.length && toMove[i].claim_order >= lis[j].claim_order) {\n j++;\n }\n const anchor = j < lis.length ? lis[j] : null;\n target.insertBefore(toMove[i], anchor);\n }\n}\nfunction append(target, node) {\n target.appendChild(node);\n}\nfunction append_styles(target, style_sheet_id, styles) {\n const append_styles_to = get_root_for_style(target);\n if (!append_styles_to.getElementById(style_sheet_id)) {\n const style = element('style');\n style.id = style_sheet_id;\n style.textContent = styles;\n append_stylesheet(append_styles_to, style);\n }\n}\nfunction get_root_for_style(node) {\n if (!node)\n return document;\n const root = node.getRootNode ? node.getRootNode() : node.ownerDocument;\n if (root && root.host) {\n return root;\n }\n return node.ownerDocument;\n}\nfunction append_empty_stylesheet(node) {\n const style_element = element('style');\n append_stylesheet(get_root_for_style(node), style_element);\n return style_element.sheet;\n}\nfunction append_stylesheet(node, style) {\n append(node.head || node, style);\n return style.sheet;\n}\nfunction append_hydration(target, node) {\n if (is_hydrating) {\n init_hydrate(target);\n if ((target.actual_end_child === undefined) || ((target.actual_end_child !== null) && (target.actual_end_child.parentNode !== target))) {\n target.actual_end_child = target.firstChild;\n }\n // Skip nodes of undefined ordering\n while ((target.actual_end_child !== null) && (target.actual_end_child.claim_order === undefined)) {\n target.actual_end_child = target.actual_end_child.nextSibling;\n }\n if (node !== target.actual_end_child) {\n // We only insert if the ordering of this node should be modified or the parent node is not target\n if (node.claim_order !== undefined || node.parentNode !== target) {\n target.insertBefore(node, target.actual_end_child);\n }\n }\n else {\n target.actual_end_child = node.nextSibling;\n }\n }\n else if (node.parentNode !== target || node.nextSibling !== null) {\n target.appendChild(node);\n }\n}\nfunction insert(target, node, anchor) {\n target.insertBefore(node, anchor || null);\n}\nfunction insert_hydration(target, node, anchor) {\n if (is_hydrating && !anchor) {\n append_hydration(target, node);\n }\n else if (node.parentNode !== target || node.nextSibling != anchor) {\n target.insertBefore(node, anchor || null);\n }\n}\nfunction detach(node) {\n if (node.parentNode) {\n node.parentNode.removeChild(node);\n }\n}\nfunction destroy_each(iterations, detaching) {\n for (let i = 0; i < iterations.length; i += 1) {\n if (iterations[i])\n iterations[i].d(detaching);\n }\n}\nfunction element(name) {\n return document.createElement(name);\n}\nfunction element_is(name, is) {\n return document.createElement(name, { is });\n}\nfunction object_without_properties(obj, exclude) {\n const target = {};\n for (const k in obj) {\n if (has_prop(obj, k)\n // @ts-ignore\n && exclude.indexOf(k) === -1) {\n // @ts-ignore\n target[k] = obj[k];\n }\n }\n return target;\n}\nfunction svg_element(name) {\n return document.createElementNS('http://www.w3.org/2000/svg', name);\n}\nfunction text(data) {\n return document.createTextNode(data);\n}\nfunction space() {\n return text(' ');\n}\nfunction empty() {\n return text('');\n}\nfunction comment(content) {\n return document.createComment(content);\n}\nfunction listen(node, event, handler, options) {\n node.addEventListener(event, handler, options);\n return () => node.removeEventListener(event, handler, options);\n}\nfunction prevent_default(fn) {\n return function (event) {\n event.preventDefault();\n // @ts-ignore\n return fn.call(this, event);\n };\n}\nfunction stop_propagation(fn) {\n return function (event) {\n event.stopPropagation();\n // @ts-ignore\n return fn.call(this, event);\n };\n}\nfunction stop_immediate_propagation(fn) {\n return function (event) {\n event.stopImmediatePropagation();\n // @ts-ignore\n return fn.call(this, event);\n };\n}\nfunction self(fn) {\n return function (event) {\n // @ts-ignore\n if (event.target === this)\n fn.call(this, event);\n };\n}\nfunction trusted(fn) {\n return function (event) {\n // @ts-ignore\n if (event.isTrusted)\n fn.call(this, event);\n };\n}\nfunction attr(node, attribute, value) {\n if (value == null)\n node.removeAttribute(attribute);\n else if (node.getAttribute(attribute) !== value)\n node.setAttribute(attribute, value);\n}\n/**\n * List of attributes that should always be set through the attr method,\n * because updating them through the property setter doesn't work reliably.\n * In the example of `width`/`height`, the problem is that the setter only\n * accepts numeric values, but the attribute can also be set to a string like `50%`.\n * If this list becomes too big, rethink this approach.\n */\nconst always_set_through_set_attribute = ['width', 'height'];\nfunction set_attributes(node, attributes) {\n // @ts-ignore\n const descriptors = Object.getOwnPropertyDescriptors(node.__proto__);\n for (const key in attributes) {\n if (attributes[key] == null) {\n node.removeAttribute(key);\n }\n else if (key === 'style') {\n node.style.cssText = attributes[key];\n }\n else if (key === '__value') {\n node.value = node[key] = attributes[key];\n }\n else if (descriptors[key] && descriptors[key].set && always_set_through_set_attribute.indexOf(key) === -1) {\n node[key] = attributes[key];\n }\n else {\n attr(node, key, attributes[key]);\n }\n }\n}\nfunction set_svg_attributes(node, attributes) {\n for (const key in attributes) {\n attr(node, key, attributes[key]);\n }\n}\nfunction set_custom_element_data_map(node, data_map) {\n Object.keys(data_map).forEach((key) => {\n set_custom_element_data(node, key, data_map[key]);\n });\n}\nfunction set_custom_element_data(node, prop, value) {\n if (prop in node) {\n node[prop] = typeof node[prop] === 'boolean' && value === '' ? true : value;\n }\n else {\n attr(node, prop, value);\n }\n}\nfunction set_dynamic_element_data(tag) {\n return (/-/.test(tag)) ? set_custom_element_data_map : set_attributes;\n}\nfunction xlink_attr(node, attribute, value) {\n node.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);\n}\nfunction get_binding_group_value(group, __value, checked) {\n const value = new Set();\n for (let i = 0; i < group.length; i += 1) {\n if (group[i].checked)\n value.add(group[i].__value);\n }\n if (!checked) {\n value.delete(__value);\n }\n return Array.from(value);\n}\nfunction init_binding_group(group) {\n let _inputs;\n return {\n /* push */ p(...inputs) {\n _inputs = inputs;\n _inputs.forEach(input => group.push(input));\n },\n /* remove */ r() {\n _inputs.forEach(input => group.splice(group.indexOf(input), 1));\n }\n };\n}\nfunction init_binding_group_dynamic(group, indexes) {\n let _group = get_binding_group(group);\n let _inputs;\n function get_binding_group(group) {\n for (let i = 0; i < indexes.length; i++) {\n group = group[indexes[i]] = group[indexes[i]] || [];\n }\n return group;\n }\n function push() {\n _inputs.forEach(input => _group.push(input));\n }\n function remove() {\n _inputs.forEach(input => _group.splice(_group.indexOf(input), 1));\n }\n return {\n /* update */ u(new_indexes) {\n indexes = new_indexes;\n const new_group = get_binding_group(group);\n if (new_group !== _group) {\n remove();\n _group = new_group;\n push();\n }\n },\n /* push */ p(...inputs) {\n _inputs = inputs;\n push();\n },\n /* remove */ r: remove\n };\n}\nfunction to_number(value) {\n return value === '' ? null : +value;\n}\nfunction time_ranges_to_array(ranges) {\n const array = [];\n for (let i = 0; i < ranges.length; i += 1) {\n array.push({ start: ranges.start(i), end: ranges.end(i) });\n }\n return array;\n}\nfunction children(element) {\n return Array.from(element.childNodes);\n}\nfunction init_claim_info(nodes) {\n if (nodes.claim_info === undefined) {\n nodes.claim_info = { last_index: 0, total_claimed: 0 };\n }\n}\nfunction claim_node(nodes, predicate, processNode, createNode, dontUpdateLastIndex = false) {\n // Try to find nodes in an order such that we lengthen the longest increasing subsequence\n init_claim_info(nodes);\n const resultNode = (() => {\n // We first try to find an element after the previous one\n for (let i = nodes.claim_info.last_index; i < nodes.length; i++) {\n const node = nodes[i];\n if (predicate(node)) {\n const replacement = processNode(node);\n if (replacement === undefined) {\n nodes.splice(i, 1);\n }\n else {\n nodes[i] = replacement;\n }\n if (!dontUpdateLastIndex) {\n nodes.claim_info.last_index = i;\n }\n return node;\n }\n }\n // Otherwise, we try to find one before\n // We iterate in reverse so that we don't go too far back\n for (let i = nodes.claim_info.last_index - 1; i >= 0; i--) {\n const node = nodes[i];\n if (predicate(node)) {\n const replacement = processNode(node);\n if (replacement === undefined) {\n nodes.splice(i, 1);\n }\n else {\n nodes[i] = replacement;\n }\n if (!dontUpdateLastIndex) {\n nodes.claim_info.last_index = i;\n }\n else if (replacement === undefined) {\n // Since we spliced before the last_index, we decrease it\n nodes.claim_info.last_index--;\n }\n return node;\n }\n }\n // If we can't find any matching node, we create a new one\n return createNode();\n })();\n resultNode.claim_order = nodes.claim_info.total_claimed;\n nodes.claim_info.total_claimed += 1;\n return resultNode;\n}\nfunction claim_element_base(nodes, name, attributes, create_element) {\n return claim_node(nodes, (node) => node.nodeName === name, (node) => {\n const remove = [];\n for (let j = 0; j < node.attributes.length; j++) {\n const attribute = node.attributes[j];\n if (!attributes[attribute.name]) {\n remove.push(attribute.name);\n }\n }\n remove.forEach(v => node.removeAttribute(v));\n return undefined;\n }, () => create_element(name));\n}\nfunction claim_element(nodes, name, attributes) {\n return claim_element_base(nodes, name, attributes, element);\n}\nfunction claim_svg_element(nodes, name, attributes) {\n return claim_element_base(nodes, name, attributes, svg_element);\n}\nfunction claim_text(nodes, data) {\n return claim_node(nodes, (node) => node.nodeType === 3, (node) => {\n const dataStr = '' + data;\n if (node.data.startsWith(dataStr)) {\n if (node.data.length !== dataStr.length) {\n return node.splitText(dataStr.length);\n }\n }\n else {\n node.data = dataStr;\n }\n }, () => text(data), true // Text nodes should not update last index since it is likely not worth it to eliminate an increasing subsequence of actual elements\n );\n}\nfunction claim_space(nodes) {\n return claim_text(nodes, ' ');\n}\nfunction claim_comment(nodes, data) {\n return claim_node(nodes, (node) => node.nodeType === 8, (node) => {\n node.data = '' + data;\n return undefined;\n }, () => comment(data), true);\n}\nfunction find_comment(nodes, text, start) {\n for (let i = start; i < nodes.length; i += 1) {\n const node = nodes[i];\n if (node.nodeType === 8 /* comment node */ && node.textContent.trim() === text) {\n return i;\n }\n }\n return nodes.length;\n}\nfunction claim_html_tag(nodes, is_svg) {\n // find html opening tag\n const start_index = find_comment(nodes, 'HTML_TAG_START', 0);\n const end_index = find_comment(nodes, 'HTML_TAG_END', start_index);\n if (start_index === end_index) {\n return new HtmlTagHydration(undefined, is_svg);\n }\n init_claim_info(nodes);\n const html_tag_nodes = nodes.splice(start_index, end_index - start_index + 1);\n detach(html_tag_nodes[0]);\n detach(html_tag_nodes[html_tag_nodes.length - 1]);\n const claimed_nodes = html_tag_nodes.slice(1, html_tag_nodes.length - 1);\n for (const n of claimed_nodes) {\n n.claim_order = nodes.claim_info.total_claimed;\n nodes.claim_info.total_claimed += 1;\n }\n return new HtmlTagHydration(claimed_nodes, is_svg);\n}\nfunction set_data(text, data) {\n data = '' + data;\n if (text.data === data)\n return;\n text.data = data;\n}\nfunction set_data_contenteditable(text, data) {\n data = '' + data;\n if (text.wholeText === data)\n return;\n text.data = data;\n}\nfunction set_data_maybe_contenteditable(text, data, attr_value) {\n if (~contenteditable_truthy_values.indexOf(attr_value)) {\n set_data_contenteditable(text, data);\n }\n else {\n set_data(text, data);\n }\n}\nfunction set_input_value(input, value) {\n input.value = value == null ? '' : value;\n}\nfunction set_input_type(input, type) {\n try {\n input.type = type;\n }\n catch (e) {\n // do nothing\n }\n}\nfunction set_style(node, key, value, important) {\n if (value == null) {\n node.style.removeProperty(key);\n }\n else {\n node.style.setProperty(key, value, important ? 'important' : '');\n }\n}\nfunction select_option(select, value, mounting) {\n for (let i = 0; i < select.options.length; i += 1) {\n const option = select.options[i];\n if (option.__value === value) {\n option.selected = true;\n return;\n }\n }\n if (!mounting || value !== undefined) {\n select.selectedIndex = -1; // no option should be selected\n }\n}\nfunction select_options(select, value) {\n for (let i = 0; i < select.options.length; i += 1) {\n const option = select.options[i];\n option.selected = ~value.indexOf(option.__value);\n }\n}\nfunction select_value(select) {\n const selected_option = select.querySelector(':checked');\n return selected_option && selected_option.__value;\n}\nfunction select_multiple_value(select) {\n return [].map.call(select.querySelectorAll(':checked'), option => option.__value);\n}\n// unfortunately this can't be a constant as that wouldn't be tree-shakeable\n// so we cache the result instead\nlet crossorigin;\nfunction is_crossorigin() {\n if (crossorigin === undefined) {\n crossorigin = false;\n try {\n if (typeof window !== 'undefined' && window.parent) {\n void window.parent.document;\n }\n }\n catch (error) {\n crossorigin = true;\n }\n }\n return crossorigin;\n}\nfunction add_iframe_resize_listener(node, fn) {\n const computed_style = getComputedStyle(node);\n if (computed_style.position === 'static') {\n node.style.position = 'relative';\n }\n const iframe = element('iframe');\n iframe.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; width: 100%; height: 100%; ' +\n 'overflow: hidden; border: 0; opacity: 0; pointer-events: none; z-index: -1;');\n iframe.setAttribute('aria-hidden', 'true');\n iframe.tabIndex = -1;\n const crossorigin = is_crossorigin();\n let unsubscribe;\n if (crossorigin) {\n iframe.src = \"data:text/html,\";\n unsubscribe = listen(window, 'message', (event) => {\n if (event.source === iframe.contentWindow)\n fn();\n });\n }\n else {\n iframe.src = 'about:blank';\n iframe.onload = () => {\n unsubscribe = listen(iframe.contentWindow, 'resize', fn);\n // make sure an initial resize event is fired _after_ the iframe is loaded (which is asynchronous)\n // see https://github.com/sveltejs/svelte/issues/4233\n fn();\n };\n }\n append(node, iframe);\n return () => {\n if (crossorigin) {\n unsubscribe();\n }\n else if (unsubscribe && iframe.contentWindow) {\n unsubscribe();\n }\n detach(iframe);\n };\n}\nconst resize_observer_content_box = /* @__PURE__ */ new ResizeObserverSingleton({ box: 'content-box' });\nconst resize_observer_border_box = /* @__PURE__ */ new ResizeObserverSingleton({ box: 'border-box' });\nconst resize_observer_device_pixel_content_box = /* @__PURE__ */ new ResizeObserverSingleton({ box: 'device-pixel-content-box' });\nfunction toggle_class(element, name, toggle) {\n element.classList[toggle ? 'add' : 'remove'](name);\n}\nfunction custom_event(type, detail, { bubbles = false, cancelable = false } = {}) {\n const e = document.createEvent('CustomEvent');\n e.initCustomEvent(type, bubbles, cancelable, detail);\n return e;\n}\nfunction query_selector_all(selector, parent = document.body) {\n return Array.from(parent.querySelectorAll(selector));\n}\nfunction head_selector(nodeId, head) {\n const result = [];\n let started = 0;\n for (const node of head.childNodes) {\n if (node.nodeType === 8 /* comment node */) {\n const comment = node.textContent.trim();\n if (comment === `HEAD_${nodeId}_END`) {\n started -= 1;\n result.push(node);\n }\n else if (comment === `HEAD_${nodeId}_START`) {\n started += 1;\n result.push(node);\n }\n }\n else if (started > 0) {\n result.push(node);\n }\n }\n return result;\n}\nclass HtmlTag {\n constructor(is_svg = false) {\n this.is_svg = false;\n this.is_svg = is_svg;\n this.e = this.n = null;\n }\n c(html) {\n this.h(html);\n }\n m(html, target, anchor = null) {\n if (!this.e) {\n if (this.is_svg)\n this.e = svg_element(target.nodeName);\n /** #7364 target for