[a-zA-Z]{3})") - - match = symbol_regex.match(string) - - if not match: - return None - - return CurrencyPair(match.group("base"), match.group("quote")) - - -def average(a): - return sum(a) / len(a) - - -def balance_to_json(balance: Balance): - return { - 'currency': balance.currency().name(), - 'amount': balance.currency().amount(), - 'kind': balance.wallet().value, - 'quote': balance.quote().value, - 'quote_equivalent': balance.quote_equivalent() - } - - -def net_pl_percentage(perc: float, reference_fee_perc: float): - return perc - reference_fee_perc - - -def pw_to_posprop(pw: PositionWrapper): - pair = CurrencyPair.from_str(pw.position.symbol) - - if not pair: - raise ValueError - - return { - "id": pw.position.id, - "amount": pw.position.amount, - "base_price": pw.position.base_price, - "state": str(pw.state()), - "pair": { - "base": pair.base, - "quote": pair.quote - }, - "profit_loss": pw.net_profit_loss(), - "profit_loss_percentage": pw.net_profit_loss_percentage() - } diff --git a/main.py b/main.py deleted file mode 100755 index a0935d9..0000000 --- a/main.py +++ /dev/null @@ -1,140 +0,0 @@ -# #!/usr/bin/env python - -import asyncio -import os -import threading -from time import sleep -from typing import List - -import dotenv -from flask import Flask, render_template -from flask_socketio import SocketIO - -from bfxbot import BfxBot -from bfxbot.bfxwrapper import Balance -from bfxbot.currency import TradingPair, Symbol -from bfxbot.models import PositionWrapper, SymbolStatus, Event, EventKind -from bfxbot.utils import pw_to_posprop, balance_to_json -from strategy import TrailingStopStrategy - - -async def bot_loop(): - await bot.start() - - while True: - await bot.update() - - -loop = asyncio.new_event_loop() - -dotenv.load_dotenv() - -API_KEY = os.getenv("API_KEY") -API_SECRET = os.getenv("API_SECRET") - -app = Flask(__name__) -socketio = SocketIO(app, async_mode="threading") -bot = BfxBot(api_key=API_KEY, api_secret=API_SECRET, - symbols=[TradingPair.BTC], quote=Symbol.USD, tick_duration=20) -strategy = TrailingStopStrategy() -bot.set_strategy(TradingPair.BTC, strategy) -btc_eh = bot.symbol_event_handler(TradingPair.BTC) - -# initializing and starting bot on other thread -threading.Thread(target=lambda: asyncio.run(bot_loop())).start() - - -################################### -# Flask callbacks -################################### - -@app.route('/') -def entry(): - return render_template('index.html') - - -################################### -# Socker.IO callbacks -################################### - -@socketio.on("close_position") -def on_close_position(message: dict): - position_id = message['position_id'] - - loop.run_until_complete(bot.close_position(position_id)) - - -@socketio.on('connect') -def on_connect(): - # sleeping on exception to avoid race condition - ticks, prices, positions, balances = [], [], [], [] - - while not ticks or not prices: - try: - ticks = bot.symbol_status(TradingPair.BTC).all_ticks() - prices = bot.symbol_status(TradingPair.BTC).all_prices() - positions = bot.symbol_status(TradingPair.BTC).current_positions() - balances = loop.run_until_complete(bot.get_balances()) - except KeyError: - sleep(1) - - socketio.emit("first_connect", - { - "ticks": ticks, - "prices": prices, - "positions": list(map(pw_to_posprop, positions)), - "balances": list(map(balance_to_json, balances)) - }) - - -@socketio.on('get_profit_loss') -def on_get_profit_loss(message): - start = message['start'] - end = message['end'] - - profit_loss = loop.run_until_complete(bot.get_profit_loss(start, end)) - - socketio.emit("put_profit_loss", { - "pl": profit_loss[0], - "pl_perc": profit_loss[1] - }) - -################################### -# Bot callbacks -################################### - -@btc_eh.on_event(EventKind.CLOSE_POSITION) -async def on_close_position(event: Event, _): - print("CLOSING!") - await bot.close_position(event.metadata.position_id) - - -@btc_eh.on_any_position_state() -async def on_any_state(pw: PositionWrapper, ss: SymbolStatus): - await strategy.update_stop_percentage(pw, ss) - - -@btc_eh.on_event(EventKind.NEW_TICK) -async def on_new_tick(event: Event, status: SymbolStatus): - tick = event.tick - price = status.prices[event.tick] - - balances: List[Balance] = await bot.get_balances() - positions: List[PositionWrapper] = status.positions[event.tick] if event.tick in status.positions else [] - - socketio.emit("new_tick", {"tick": tick, - "price": price, - "positions": list(map(pw_to_posprop, positions)), - "balances": list(map(balance_to_json, balances))}) - - -@btc_eh.on_any_event() -def on_any_event(event: Event, _): - socketio.emit("new_event", { - "tick": event.tick, - "kind": event.kind.name - }) - - -if __name__ == '__main__': - socketio.run(app) diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index b00bccc..0000000 --- a/requirements.txt +++ /dev/null @@ -1,36 +0,0 @@ -aiohttp==3.7.3 -astroid==2.4.2 -async-timeout==3.0.1 -asyncio==3.4.3 -attrs==20.3.0 -bidict==0.21.2 -bitfinex-api-py==1.1.8 -chardet==3.0.4 -click==7.1.2 -eventemitter==0.2.0 -Flask==1.1.2 -Flask-SocketIO==5.0.1 -idna==2.10 -isort==5.6.4 -itsdangerous==1.1.0 -Jinja2==2.11.2 -lazy-object-proxy==1.4.3 -MarkupSafe==1.1.1 -mccabe==0.6.1 -mpmath==1.1.0 -multidict==5.1.0 -pyee==8.1.0 -pylint==2.6.0 -python-dotenv==0.15.0 -python-engineio==4.0.0 -python-socketio==5.0.3 -retrying-async==1.2.0 -six==1.15.0 -sympy==1.7.1 -toml==0.10.2 -typing-extensions==3.7.4.3 -websockets==8.1 -Werkzeug==1.0.1 -wrapt==1.12.1 -yarl==1.6.3 - diff --git a/sounds/1up.mp3 b/sounds/1up.mp3 deleted file mode 100644 index 14a165d..0000000 Binary files a/sounds/1up.mp3 and /dev/null differ diff --git a/sounds/coin.mp3 b/sounds/coin.mp3 deleted file mode 100644 index db62eca..0000000 Binary files a/sounds/coin.mp3 and /dev/null differ diff --git a/sounds/gameover.mp3 b/sounds/gameover.mp3 deleted file mode 100644 index 2a0905f..0000000 Binary files a/sounds/gameover.mp3 and /dev/null differ diff --git a/sounds/goal.wav b/sounds/goal.wav deleted file mode 100644 index 15144bb..0000000 Binary files a/sounds/goal.wav and /dev/null differ diff --git a/static/.gitignore b/static/.gitignore deleted file mode 100644 index a5baada..0000000 --- a/static/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -* -!.gitignore - diff --git a/strategy.py b/strategy.py deleted file mode 100644 index 920dd85..0000000 --- a/strategy.py +++ /dev/null @@ -1,123 +0,0 @@ -from typing import List, Dict - -import sympy.abc -from bfxapi import Position -from sympy import Point, solve - -from bfxbot.models import Strategy, PositionState, SymbolStatus, Event, EventKind, EventMetadata, PositionWrapper, \ - TAKER_FEE -from bfxbot.utils import net_pl_percentage - - -class SquaredTrailingStop: - def __init__(self, p_min: Point, p_max: Point): - a = sympy.abc.a - b = sympy.abc.b - c = sympy.abc.c - - self.p_min = p_min - self.p_max = p_max - - e1 = 2 * a * (p_max.x + b) - e2 = a * (p_min.x + b) ** 2 + c - p_min.y - e3 = a * (p_max.x + b) ** 2 + c - p_max.y - - s = solve([e1, e2, e3])[0] - - self.a, self.b, self.c = s[a], s[b], s[c] - - def y(self, x): - def inter_y(x): - return self.a * (x + self.b) ** 2 + self.c - - if x < self.p_min.x: - return self.p_min.y - elif x > self.p_max.x: - return self.p_max.y - else: - return inter_y(x) - - def profit(self, x): - if x < self.p_min.x: - return 0 - return x - self.y(x) - - -class TrailingStopStrategy(Strategy): - BREAK_EVEN_PERC = TAKER_FEE - MIN_PROFIT_PERC = BREAK_EVEN_PERC + 0.3 - GOOD_PROFIT_PERC = MIN_PROFIT_PERC * 2.5 - MAX_LOSS_PERC = -4.0 - - TRAILING_STOP = SquaredTrailingStop(Point(MIN_PROFIT_PERC, MIN_PROFIT_PERC / 3 * 2), Point(GOOD_PROFIT_PERC, 0.1)) - - def __init__(self): - # position_id : stop percentage - self.stop_percentage: Dict[int, float] = {} - - def position_on_new_tick(self, current_position: Position, ss: SymbolStatus) -> (PositionState, List[Event]): - events = [] - - pl_perc = net_pl_percentage(current_position.profit_loss_percentage, TAKER_FEE) - prev = ss.previous_pw(current_position.id) - event_metadata = EventMetadata(position_id=current_position.id) - - if pl_perc > self.GOOD_PROFIT_PERC: - state = PositionState.PROFIT - elif self.MIN_PROFIT_PERC <= pl_perc < self.GOOD_PROFIT_PERC: - state = PositionState.MINIMUM_PROFIT - elif 0.0 <= pl_perc < self.MIN_PROFIT_PERC: - state = PositionState.BREAK_EVEN - elif self.MAX_LOSS_PERC < pl_perc < 0.0: - state = PositionState.LOSS - else: - events.append(Event(EventKind.CLOSE_POSITION, ss.current_tick, event_metadata)) - state = PositionState.CRITICAL - - pw = PositionWrapper(current_position, state=state, net_profit_loss=current_position.profit_loss, - net_profit_loss_percentage=pl_perc) - - if not prev or prev.state() == state: - return pw, events - - if state == PositionState.PROFIT: - events.append(Event(EventKind.REACHED_GOOD_PROFIT, ss.current_tick, event_metadata)) - elif state == PositionState.MINIMUM_PROFIT: - events.append(Event(EventKind.REACHED_MIN_PROFIT, ss.current_tick, event_metadata)) - elif state == PositionState.BREAK_EVEN: - events.append(Event(EventKind.REACHED_BREAK_EVEN, ss.current_tick, event_metadata)) - elif state == PositionState.LOSS: - events.append(Event(EventKind.REACHED_LOSS, ss.current_tick, event_metadata)) - else: - events.append(Event(EventKind.REACHED_MAX_LOSS, ss.current_tick, event_metadata)) - events.append(Event(EventKind.CLOSE_POSITION, ss.current_tick, event_metadata)) - - return pw, events - - async def update_stop_percentage(self, pw: PositionWrapper, ss: SymbolStatus): - current_pl_perc = pw.net_profit_loss_percentage() - pid = pw.position.id - event_metadata = EventMetadata(position_id=pw.position.id) - - # if trailing stop not set for this position and state is not profit (we should not set it) - if pid not in self.stop_percentage and pw.state() not in [PositionState.MINIMUM_PROFIT, - PositionState.PROFIT]: - return - - # set stop percentage for first time only if in profit - if pid not in self.stop_percentage: - await ss.add_event(Event(EventKind.TRAILING_STOP_SET, ss.current_tick, event_metadata)) - - self.stop_percentage[pid] = current_pl_perc - self.TRAILING_STOP.y(current_pl_perc) - return - - # moving trailing stop - if current_pl_perc - self.TRAILING_STOP.y(current_pl_perc) > self.stop_percentage[pid]: - await ss.add_event(Event(EventKind.TRAILING_STOP_MOVED, ss.current_tick, event_metadata)) - self.stop_percentage[pid] = current_pl_perc - self.TRAILING_STOP.y(current_pl_perc) - - # close position if current P/L below stop percentage - if current_pl_perc < self.stop_percentage[pid]: - await ss.add_event(Event(EventKind.CLOSE_POSITION, ss.current_tick, event_metadata)) - - return diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index 7940a79..0000000 --- a/templates/index.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - -Rustico - - - - - - - \ No newline at end of file diff --git a/websrc/.eslintcache b/websrc/.eslintcache index f238dc2..d1449c5 100644 --- a/websrc/.eslintcache +++ b/websrc/.eslintcache @@ -1 +1 @@ -[{"/home/giulio/dev/gkaching/websrc/src/components/App.tsx":"1","/home/giulio/dev/gkaching/websrc/src/components/Cards.tsx":"2","/home/giulio/dev/gkaching/websrc/src/components/Overlays.tsx":"3","/home/giulio/dev/gkaching/websrc/src/index.tsx":"4"},{"size":4418,"mtime":1609331626715,"results":"5","hashOfConfig":"6"},{"size":5688,"mtime":1609331022076,"results":"7","hashOfConfig":"6"},{"size":5235,"mtime":1609331232067,"results":"8","hashOfConfig":"6"},{"size":321,"mtime":1609332875579,"results":"9","hashOfConfig":"6"},{"filePath":"10","messages":"11","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"12"},"1ev2e5",{"filePath":"13","messages":"14","errorCount":0,"warningCount":4,"fixableErrorCount":0,"fixableWarningCount":0,"source":"15"},{"filePath":"16","messages":"17","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"18"},{"filePath":"19","messages":"20","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/home/giulio/dev/gkaching/websrc/src/components/App.tsx",["21"],"import React, { Component } from \"react\";\nimport {\n Balance,\n CurrencyPair,\n EventName,\n EventProp,\n FirstConnectMessage,\n NewEventMessage,\n NewTickMessage,\n PositionProp\n} from \"../types\";\nimport { socket } from \"../index\";\nimport { symbolToPair } from \"../utils\";\nimport { Helmet } from \"react-helmet\";\nimport { Navbar, Sidebar } from \"./Navbars\";\nimport { Statusbar } from \"./Statusbar\";\nimport { PositionsTable } from \"./Tables\";\nimport RPlot from \"./RPlot\";\n\ntype AppState = {\n current_price: number,\n current_tick: number,\n last_update: Date,\n positions: Array,\n events: Array ,\n active_pair: CurrencyPair,\n available_pairs: Array ,\n balances: Array \n}\n\nclass App extends Component<{}, AppState> {\n event_id = 0;\n\n state = {\n current_price: 0,\n current_tick: 0,\n last_update: new Date(),\n positions: [],\n events: [],\n balances: [],\n active_pair: symbolToPair(\"tBTCUSD\"),\n available_pairs: []\n }\n\n constructor(props: {}) {\n super(props)\n }\n\n componentDidMount() {\n socket.on(EventName.FirstConnect, (data: FirstConnectMessage) => {\n this.setState({\n current_price: data.prices[data.prices.length - 1],\n current_tick: data.ticks[data.ticks.length - 1],\n last_update: new Date(),\n positions: data.positions,\n balances: data.balances\n })\n })\n\n socket.on(EventName.NewTick, (data: NewTickMessage) => {\n this.setState({\n current_price: data.price,\n current_tick: data.tick,\n last_update: new Date(),\n positions: data.positions,\n balances: data.balances\n })\n })\n\n socket.on(EventName.NewEvent, (data: NewEventMessage) => {\n // ignore new tick\n if (!data.kind.toLowerCase().includes(\"new_tick\")) {\n const new_event: EventProp = {\n id: this.event_id,\n name: data.kind,\n tick: data.tick\n }\n\n this.event_id += 1\n\n this.setState((state) => ({\n events: [...state.events, new_event]\n }))\n }\n })\n }\n\n render() {\n return (\n <>\n \n \nRustico\n - {String(this.state.current_price.toLocaleString())} {String(this.state.active_pair.base) + \"/\" + String(this.state.active_pair.quote)} \n\n\n >\n )\n }\n}\n\nexport default App;","/home/giulio/dev/gkaching/websrc/src/components/Cards.tsx",["22","23","24","25"],"import React, { Component } from 'react';\nimport { Balance, EventName, FirstConnectMessage, NewTickMessage } from \"../types\";\nimport { socket } from \"../index\";\n\nexport type CoinBalanceProps = {\n name: string,\n amount: number,\n percentage: number,\n quote_equivalent: number,\n quote_symbol: string,\n}\n\nclass CoinBalance extends Component\n\n\n\n \n \n\n\n\n\n\n \n\n\n {this.state.positions.length > 0 ?\n\n\n\n : null}\n\n \n \n {\n constructor(props: CoinBalanceProps) {\n super(props);\n }\n\n render() {\n // do not print equivalent if this element is the quote itself\n const quoteBlock = this.props.name != this.props.quote_symbol ? this.props.quote_symbol.concat(\" \").concat(this.props.quote_equivalent.toLocaleString()) : null\n\n // const accessory = SymbolAccessories.filter((accessory) => {\n // return accessory.name == this.props.name\n // })\n //\n // const icon = accessory.length > 0 ? accessory.pop().icon : null\n\n return (\n \n\n )\n }\n\n}\n\nexport type WalletCardProps = {\n quote: string,\n}\n\nexport class WalletCard extends Component\n <{}, { balances: Array\n {/*{icon}*/}\n\n\n {this.props.name}\n\n\n\n\n {Math.trunc(this.props.percentage)}%\n\n\n \n\n\n\n\n\n\n {this.props.amount.toFixed(5)} {this.props.name}\n\n\n\n\n {quoteBlock}\n\n}> {\n // constructor(props) {\n // super(props);\n // }\n\n state =\n {\n balances: []\n }\n\n totalQuoteBalance() {\n let total = 0\n\n this.state.balances.forEach((balance: Balance) => {\n if (balance.currency == balance.quote) {\n total += balance.amount\n } else {\n total += balance.quote_equivalent\n }\n })\n\n return total\n }\n\n renderCoinBalances() {\n return (\n this.state.balances.map((balance: Balance) => {\n const percentage_amount = balance.quote == balance.currency ? balance.amount : balance.quote_equivalent;\n\n return (\n \n )\n })\n )\n }\n\n componentDidMount() {\n socket.on(EventName.NewTick, (data: NewTickMessage) => {\n this.setState({\n balances: data.balances\n })\n })\n\n socket.on(EventName.FirstConnect, (data: FirstConnectMessage) => {\n this.setState({\n balances: data.balances\n })\n })\n }\n\n render() {\n return (\n \n\n )\n }\n}","/home/giulio/dev/gkaching/websrc/src/components/Overlays.tsx",["26"],"import React, {Component} from \"react\";\nimport {socket} from \"../index\";\nimport {EventName} from \"../types\";\n\nexport type ModalProps = {\n show: boolean,\n positionId: number,\n toggleConfirmation: any\n}\n\nexport class ClosePositionModal extends Component\n\n\n\n\n {this.renderCoinBalances()}\n\n\n\nYour Wallets
\n\n \n \n\n\n\n\n Total Balance ≈ USD {this.totalQuoteBalance().toLocaleString()}\n\n{\n constructor(props: ModalProps) {\n super(props);\n }\n\n render() {\n if (!this.props.show) {\n return null\n }\n\n return (\n \n\n )\n }\n}","/home/giulio/dev/gkaching/websrc/src/index.tsx",[],{"ruleId":"27","severity":1,"message":"28","line":45,"column":5,"nodeType":"29","messageId":"30","endLine":47,"endColumn":6},{"ruleId":"27","severity":1,"message":"28","line":14,"column":5,"nodeType":"29","messageId":"30","endLine":16,"endColumn":6},{"ruleId":"31","severity":1,"message":"32","line":20,"column":44,"nodeType":"33","messageId":"34","endLine":20,"endColumn":46},{"ruleId":"31","severity":1,"message":"35","line":83,"column":34,"nodeType":"33","messageId":"34","endLine":83,"endColumn":36},{"ruleId":"31","severity":1,"message":"35","line":96,"column":57,"nodeType":"33","messageId":"34","endLine":96,"endColumn":59},{"ruleId":"27","severity":1,"message":"28","line":12,"column":5,"nodeType":"29","messageId":"30","endLine":14,"endColumn":6},"@typescript-eslint/no-useless-constructor","Useless constructor.","MethodDefinition","noUselessConstructor","eqeqeq","Expected '!==' and instead saw '!='.","BinaryExpression","unexpected","Expected '===' and instead saw '=='."] \ No newline at end of file +[{"/home/giulio/dev/gkaching/websrc/src/components/App.tsx":"1","/home/giulio/dev/gkaching/websrc/src/components/Cards.tsx":"2","/home/giulio/dev/gkaching/websrc/src/components/Overlays.tsx":"3","/home/giulio/dev/gkaching/websrc/src/index.tsx":"4"},{"size":4418,"mtime":1609342346604,"results":"5","hashOfConfig":"6"},{"size":5688,"mtime":1609342346604,"results":"7","hashOfConfig":"6"},{"size":5235,"mtime":1609331232067,"results":"8","hashOfConfig":"6"},{"size":321,"mtime":1609342346604,"results":"9","hashOfConfig":"6"},{"filePath":"10","messages":"11","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},"1ev2e5",{"filePath":"12","messages":"13","errorCount":0,"warningCount":4,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"14","messages":"15","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"16"},{"filePath":"17","messages":"18","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/home/giulio/dev/gkaching/websrc/src/components/App.tsx",["19"],"/home/giulio/dev/gkaching/websrc/src/components/Cards.tsx",["20","21","22","23"],"/home/giulio/dev/gkaching/websrc/src/components/Overlays.tsx",["24"],"import React, {Component} from \"react\";\nimport {socket} from \"../index\";\nimport {EventName} from \"../types\";\n\nexport type ModalProps = {\n show: boolean,\n positionId: number,\n toggleConfirmation: any\n}\n\nexport class ClosePositionModal extends Component\n\n\n \n\n\n {/*This element is to trick the browser into centering the modal contents. -->*/}\n \n\n {/*Modal panel, show/hide based on modal state.*/}\n\n {/*Entering: \"ease-out duration-300\"*/}\n {/* From: \"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95\"*/}\n {/* To: \"opacity-100 translate-y-0 sm:scale-100\"*/}\n {/*Leaving: \"ease-in duration-200\"*/}\n {/* From: \"opacity-100 translate-y-0 sm:scale-100\"*/}\n {/* To: \"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95\"*/}\n\n\n\n\n\n\n\n\n {/*Heroicon name: exclamation -->*/}\n \n\n\n\n\n Close position\n
\n\n\n\n Are you sure you want to close the position? This action cannot be undone.\n
\n\n \n \n\n{\n constructor(props: ModalProps) {\n super(props);\n }\n\n render() {\n if (!this.props.show) {\n return null\n }\n\n return (\n \n\n )\n }\n}","/home/giulio/dev/gkaching/websrc/src/index.tsx",[],{"ruleId":"25","severity":1,"message":"26","line":45,"column":5,"nodeType":"27","messageId":"28","endLine":47,"endColumn":6},{"ruleId":"25","severity":1,"message":"26","line":14,"column":5,"nodeType":"27","messageId":"28","endLine":16,"endColumn":6},{"ruleId":"29","severity":1,"message":"30","line":20,"column":44,"nodeType":"31","messageId":"32","endLine":20,"endColumn":46},{"ruleId":"29","severity":1,"message":"33","line":83,"column":34,"nodeType":"31","messageId":"32","endLine":83,"endColumn":36},{"ruleId":"29","severity":1,"message":"33","line":96,"column":57,"nodeType":"31","messageId":"32","endLine":96,"endColumn":59},{"ruleId":"25","severity":1,"message":"26","line":12,"column":5,"nodeType":"27","messageId":"28","endLine":14,"endColumn":6},"@typescript-eslint/no-useless-constructor","Useless constructor.","MethodDefinition","noUselessConstructor","eqeqeq","Expected '!==' and instead saw '!='.","BinaryExpression","unexpected","Expected '===' and instead saw '=='."] \ No newline at end of file\n\n\n \n\n\n {/*This element is to trick the browser into centering the modal contents. -->*/}\n \n\n {/*Modal panel, show/hide based on modal state.*/}\n\n {/*Entering: \"ease-out duration-300\"*/}\n {/* From: \"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95\"*/}\n {/* To: \"opacity-100 translate-y-0 sm:scale-100\"*/}\n {/*Leaving: \"ease-in duration-200\"*/}\n {/* From: \"opacity-100 translate-y-0 sm:scale-100\"*/}\n {/* To: \"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95\"*/}\n\n\n\n\n\n\n\n\n {/*Heroicon name: exclamation -->*/}\n \n\n\n\n\n Close position\n
\n\n\n\n Are you sure you want to close the position? This action cannot be undone.\n
\n\n \n \n\n