summaryrefslogtreecommitdiff
path: root/backend/src/arbitrage.py
blob: 0991bbcb7502a5af6fef2dd6caad65b16ffdd7c7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import logging
from dataclasses import dataclass
from typing import Optional, List

from market import Order

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

TAX_RATE = 0.08
PROFIT_THRESHOLD = 100_000
MAX_CAPITAL = 100_000_000
SNIPE_PROFIT_THRESHOLD = 20_000_000

@dataclass
class Trade:
    from_station: int
    to_station: int
    from_order_id: int
    to_order_id: int
    item_name: str
    type_id: int
    item_cargo_volume: float
    from_price: float
    to_price: float
    quantity: int
    cargo: float
    gross_profit: float
    net_profit: Optional[float] = None

def arbitrage(orders: List[Order], cargo_space: int) -> List[Trade]:
    sell_orders = sorted(
        (order for order in orders if not order.is_buy_order),
        key=lambda x: x.price
    )
    buy_orders = sorted(
        (order for order in orders if order.is_buy_order),
        key=lambda x: x.price, reverse=True
    )

    logger.info("Initial sell orders: %d, buy orders: %d", len(sell_orders), len(buy_orders))

    sell_orders = filter_orders_for_cargo(sell_orders, cargo_space)
    buy_orders = filter_orders_for_cargo(buy_orders, cargo_space)
    logger.info("After cargo filter - sell orders: %d, buy orders: %d", len(sell_orders), len(buy_orders))

    sell_orders = filter_orders_for_capital_risk(sell_orders, MAX_CAPITAL)
    buy_orders = filter_orders_for_capital_risk(buy_orders, MAX_CAPITAL)
    logger.info("After capital risk filter - sell orders: %d, buy orders: %d", len(sell_orders), len(buy_orders))

    trades = create_trades(sell_orders, buy_orders)
    return trades

def create_trades(sell_orders: List[Order], buy_orders: List[Order]) -> List[Trade]:
    trades = []
    grouped_sell_orders = {}
    grouped_buy_orders = {}

    for order in sell_orders:
        grouped_sell_orders.setdefault(order.type_id, []).append(order)
    for order in buy_orders:
        grouped_buy_orders.setdefault(order.type_id, []).append(order)

    for type_id in grouped_sell_orders.keys() & grouped_buy_orders.keys():
        for sell_order in grouped_sell_orders[type_id]:
            for buy_order in grouped_buy_orders[type_id]:
                effective_buy_price = buy_order.price * (1 - TAX_RATE)
                if effective_buy_price < sell_order.price:
                    continue

                quantity = min(sell_order.volume_remain, buy_order.volume_remain)
                gross_profit = quantity * (effective_buy_price - sell_order.price)

                if gross_profit < PROFIT_THRESHOLD:
                    continue

                cargo_volume = quantity * sell_order.item_cargo_volume

                trade = Trade(
                    from_station=sell_order.location_id,
                    to_station=buy_order.location_id,
                    from_order_id=sell_order.order_id,
                    to_order_id=buy_order.order_id,
                    item_name=sell_order.item_name,
                    type_id=sell_order.type_id,
                    item_cargo_volume=sell_order.item_cargo_volume,
                    from_price=sell_order.price,
                    to_price=buy_order.price,
                    quantity=quantity,
                    cargo=cargo_volume,
                    gross_profit=gross_profit
                )
                trades.append(trade)
    return trades

def filter_orders_for_cargo(orders: List[Order], cargo_space: int) -> List[Order]:
    filtered_orders = []
    for order in orders:
        item_volume = order.item_cargo_volume
        if item_volume == 0:
            continue
        max_quantity = int(cargo_space // item_volume)
        volume_remain = min(order.volume_remain, max_quantity)
        if volume_remain > 0:
            order = Order(**vars(order))
            order.volume_remain = volume_remain
            filtered_orders.append(order)
    return filtered_orders

def filter_orders_for_capital_risk(orders: List[Order], max_amount: int) -> List[Order]:
    filtered_orders = []
    for order in orders:
        price = order.price
        if price == 0:
            continue
        max_quantity = int(max_amount // price)
        volume_remain = min(order.volume_remain, max_quantity)
        if volume_remain > 0:
            order = Order(**vars(order))
            order.volume_remain = volume_remain
            filtered_orders.append(order)
    return filtered_orders

def snipe(orders: List[Order], cargo_space: int, current_region: int) -> Optional[Trade]:
    trades = arbitrage(orders, cargo_space)

    order_regions = {order.order_id: order.region_id for order in orders}

    region_trades = [
        trade for trade in trades
        if order_regions.get(trade.from_order_id) == current_region
    ]

    region_trades.sort(key=lambda x: x.gross_profit, reverse=True)

    logger.info("Reviewing potential snipes for region %d: %d trades found", current_region, len(region_trades))

    if region_trades and region_trades[0].gross_profit > SNIPE_PROFIT_THRESHOLD:
        return region_trades[0]
    else:
        return None