Răsfoiți Sursa

market: estimate profit/time

raylu 2 săptămâni în urmă
părinte
comite
26aba9cdac
2 a modificat fișierele cu 70 adăugiri și 30 ștergeri
  1. 66 27
      market.py
  2. 4 3
      sell.py

+ 66 - 27
market.py

@@ -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()

+ 4 - 3
sell.py

@@ -25,9 +25,10 @@ def main() -> None:
 			print(mat, 'has no bid/ask')
 			continue
 		spread = (price['Ask'] - price['Bid']) / price['Ask']
-		bids_filled, asks_filled = market.estimate_filled_orders(mat + '.IC1', (price['Bid'] + price['Ask']) / 2)
-		ask_fill_ratio = asks_filled / (bids_filled + asks_filled)
-		materials.append(Material(mat, spread=spread, bids_filled=bids_filled, asks_filled=asks_filled, score=spread * ask_fill_ratio))
+		chart_analysis = market.analyze_price_chart(mat + '.IC1', (price['Bid'] + price['Ask']) / 2)
+		ask_fill_ratio = chart_analysis.asks_filled / (chart_analysis.bids_filled + chart_analysis.asks_filled)
+		materials.append(Material(mat, spread=spread, bids_filled=chart_analysis.bids_filled,
+				asks_filled=chart_analysis.asks_filled, score=spread * ask_fill_ratio))
 	materials.sort()
 
 	print(f'{"mat":^4} spread  bids filled  asks filled')