|
|
@@ -12,8 +12,8 @@ def main() -> None:
|
|
|
if len(sys.argv) > 1:
|
|
|
exchange_tickers = sys.argv[1:]
|
|
|
for ticker in exchange_tickers:
|
|
|
- bids_filled, asks_filled = estimate_filled_orders(ticker, 0)
|
|
|
- print(f'{ticker}: {bids_filled=}, {asks_filled=}')
|
|
|
+ a = analyze_price_chart(ticker, 0)
|
|
|
+ print(f'{ticker}: bids filled = {a.bids_filled:6}, asks filled = {a.asks_filled:6}, profit per interval = {a.profit_per_interval:10}')
|
|
|
return
|
|
|
|
|
|
check_cxos()
|
|
|
@@ -32,19 +32,17 @@ def main() -> None:
|
|
|
spread = (price['Ask'] - price['Bid']) / price['Ask']
|
|
|
if spread < 0.15:
|
|
|
continue
|
|
|
- bids_filled, asks_filled = estimate_filled_orders(price['FullTicker'], (price['Bid'] + price['Ask']) / 2)
|
|
|
- bid_fill_ratio = bids_filled / (bids_filled + asks_filled)
|
|
|
- if bid_fill_ratio < 0.05 or bid_fill_ratio > 0.95:
|
|
|
- continue
|
|
|
- max_profit = (price['Ask'] - price['Bid']) * min(bids_filled, asks_filled)
|
|
|
+ chart_analysis = analyze_price_chart(price['FullTicker'], (price['Bid'] + price['Ask']) / 2)
|
|
|
+ max_profit = (price['Ask'] - price['Bid']) * min(chart_analysis.bids_filled, chart_analysis.asks_filled)
|
|
|
markets[price['ExchangeCode']].append(Market(price['FullTicker'], bid=price['Bid'], ask=price['Ask'],
|
|
|
- spread=spread, traded=traded, bids_filled=bids_filled, asks_filled=asks_filled, max_profit=max_profit))
|
|
|
+ spread=spread, max_profit=max_profit, chart_analysis=chart_analysis))
|
|
|
|
|
|
- print(f'{"mat":^8} {"bid":^5} {"ask":^5} spread {"traded":^7} bids filled asks filled max profit')
|
|
|
+ print(' mat bid ask spread bids filled asks filled profit/time max profit')
|
|
|
for commodities in markets.values():
|
|
|
commodities.sort(reverse=True)
|
|
|
for m in commodities:
|
|
|
- print(f'{m.full_ticker:>8} {m.bid:5} {m.ask:5} {m.spread*100: 5.0f}% {m.traded:7.0f} {m.bids_filled:10.0f} {m.asks_filled:10.0f} {m.max_profit:10.0f}')
|
|
|
+ print(f'{m.full_ticker:>8} {m.bid:5} {m.ask:5} {m.spread*100: 5.0f}% {m.chart_analysis.bids_filled:12.0f} '
|
|
|
+ f'{m.chart_analysis.asks_filled:12.0f} {m.chart_analysis.profit_per_interval:12.0f} {m.max_profit:11.0f}')
|
|
|
print()
|
|
|
|
|
|
def check_cxos() -> None:
|
|
|
@@ -75,29 +73,59 @@ def check_cxos() -> None:
|
|
|
print('warehouse', warehouse['LocationNaturalId'], 'is not empty')
|
|
|
print()
|
|
|
|
|
|
-def estimate_filled_orders(exchange_ticker: str, midpoint: float) -> tuple[float, float]:
|
|
|
- '''use price chart to estimate how many bids and asks were filled'''
|
|
|
- bids = asks = 0
|
|
|
- for hist in cache.get('https://rest.fnar.net/exchange/cxpc/' + exchange_ticker):
|
|
|
- if hist['Interval'] != 'MINUTE_FIVE':
|
|
|
- continue
|
|
|
+def analyze_price_chart(exchange_ticker: str, midpoint: float) -> ChartAnalysis:
|
|
|
+ '''use price chart to estimate how long it takes to fill a bid and then an ask'''
|
|
|
+ pcpoints: list[PriceChartPoint] = [p for p in cache.get('https://rest.fnar.net/exchange/cxpc/' + exchange_ticker)
|
|
|
+ if p['Interval'] == 'MINUTE_FIVE']
|
|
|
+ pcpoints.reverse()
|
|
|
+ five_min = 5 * 60 * 1000
|
|
|
+ asks_filled: list[AskFilled] = []
|
|
|
+ r = ChartAnalysis(bids_filled=0, asks_filled=0, profit_per_interval=0)
|
|
|
+ for hist in pcpoints:
|
|
|
+ time = hist['DateEpochMs'] // five_min
|
|
|
+ bids = asks = 0
|
|
|
if hist['Low'] > midpoint:
|
|
|
- asks += hist['Traded']
|
|
|
+ asks = hist['Traded']
|
|
|
+ r.asks_filled += asks
|
|
|
elif hist['High'] < midpoint:
|
|
|
- bids += hist['Traded']
|
|
|
- elif hist['High'] == hist['Low']:
|
|
|
+ bids = hist['Traded']
|
|
|
+ r.bids_filled += bids
|
|
|
+ elif hist['High'] == hist['Low']: # all trades right at midpoint
|
|
|
assert hist['High'] == midpoint
|
|
|
else:
|
|
|
interval_bids = (hist['High'] * hist['Traded'] - hist['Volume']) / (hist['High'] - hist['Low'])
|
|
|
interval_asks = hist['Traded'] - interval_bids
|
|
|
- bids += interval_bids
|
|
|
- asks += interval_asks
|
|
|
- return bids, asks
|
|
|
+ r.bids_filled += interval_bids
|
|
|
+ r.asks_filled += interval_asks
|
|
|
+ bids = int(interval_bids)
|
|
|
+ asks = int(interval_asks)
|
|
|
+ if bids and asks:
|
|
|
+ intra_interval_trades = min(bids, asks)
|
|
|
+ r.profit_per_interval += (hist['High'] - hist['Low']) * intra_interval_trades
|
|
|
+ bids -= intra_interval_trades
|
|
|
+ asks -= intra_interval_trades
|
|
|
+ assert bids == 0 or asks == 0
|
|
|
+ while bids > 0 and len(asks_filled) > 0:
|
|
|
+ profit_per_unit_per_interval = (asks_filled[-1].price - hist['Low']) / (asks_filled[-1].time - time)
|
|
|
+ if asks_filled[-1].amount >= bids:
|
|
|
+ r.profit_per_interval += bids * profit_per_unit_per_interval
|
|
|
+ asks_filled[-1].amount -= bids
|
|
|
+ if asks_filled[-1].amount == 0:
|
|
|
+ asks_filled.pop()
|
|
|
+ bids = 0
|
|
|
+ else:
|
|
|
+ r.profit_per_interval += asks_filled[-1].amount * profit_per_unit_per_interval
|
|
|
+ bids -= asks_filled[-1].amount
|
|
|
+ asks_filled.pop()
|
|
|
+ if asks:
|
|
|
+ asks_filled.append(AskFilled(price=hist['High'], amount=asks, time=time))
|
|
|
+ return r
|
|
|
|
|
|
class ExchangeOrder(typing.TypedDict):
|
|
|
MaterialTicker: str
|
|
|
ExchangeCode: str
|
|
|
OrderType: typing.Literal['SELLING'] | typing.Literal['BUYING']
|
|
|
+ Status: typing.Literal['FILLED'] | typing.Literal['PARTIALLY_FILLED'] | typing.Literal['FILLED']
|
|
|
Limit: float
|
|
|
|
|
|
class ExchangeSummary(typing.TypedDict):
|
|
|
@@ -127,24 +155,35 @@ class RawPrice(typing.TypedDict):
|
|
|
|
|
|
class PriceChartPoint(typing.TypedDict):
|
|
|
Interval: typing.Literal['MINUTE_FIVE'] | typing.Literal['MINUTE_FIFTEEN'] | typing.Literal['MINUTE_THIRTY'] | typing.Literal['HOUR_ONE'] | typing.Literal['HOUR_TWO'] | typing.Literal['HOUR_FOUR'] | typing.Literal['HOUR_SIX'] | typing.Literal['HOUR_TWELVE'] | typing.Literal['DAY_ONE'] | typing.Literal['DAY_THREE']
|
|
|
+ DateEpochMs: int
|
|
|
High: float
|
|
|
Low: float
|
|
|
Volume: float
|
|
|
Traded: int
|
|
|
|
|
|
@dataclasses.dataclass(eq=False, slots=True)
|
|
|
+class AskFilled:
|
|
|
+ price: float
|
|
|
+ amount: int
|
|
|
+ time: int
|
|
|
+
|
|
|
+@dataclasses.dataclass(eq=False, slots=True)
|
|
|
+class ChartAnalysis:
|
|
|
+ bids_filled: float
|
|
|
+ asks_filled: float
|
|
|
+ profit_per_interval: float
|
|
|
+
|
|
|
+@dataclasses.dataclass(eq=False, frozen=True, slots=True)
|
|
|
class Market:
|
|
|
full_ticker: str
|
|
|
bid: float
|
|
|
ask: float
|
|
|
spread: float
|
|
|
- traded: float
|
|
|
- bids_filled: float
|
|
|
- asks_filled: float
|
|
|
max_profit: float
|
|
|
+ chart_analysis: ChartAnalysis
|
|
|
|
|
|
- def __lt__(self, o) -> bool:
|
|
|
- return self.max_profit < o.max_profit
|
|
|
+ def __lt__(self, o: Market) -> bool:
|
|
|
+ return self.chart_analysis.profit_per_interval < o.chart_analysis.profit_per_interval
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
main()
|