Skip to content

Commit

Permalink
Merge branch 'main' of github.com:x-fer/algotrade2024
Browse files Browse the repository at this point in the history
  • Loading branch information
nitko12 committed Feb 29, 2024
2 parents 61d28de + 73031de commit d75bf27
Show file tree
Hide file tree
Showing 45 changed files with 1,516 additions and 561 deletions.
3 changes: 3 additions & 0 deletions backend/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ server:

datasets_path: ./data

redis:
port: 6379

database:
url: postgresql://postgres:postgres@localhost:5432/mydatabase

Expand Down
1 change: 1 addition & 0 deletions backend/db/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .table import Table
from .db import database
from .rate_limit import limiter
20 changes: 11 additions & 9 deletions backend/db/migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,13 @@ async def fill_tables():
not_nat_game_id = await Game.create(
game_name="Stalna igra",
is_contest=False,
bots="dummy:3;resource_bot:1",
dataset_id=dataset_id,
start_time=datetime.now(),
total_ticks=2300,
tick_time=3000)
nat_game_id = await Game.create(
game_name="Natjecanje",
is_contest=True,
bots="dummy:2;resource_bot:1",
dataset_id=dataset_id,
start_time=datetime.now(),
total_ticks=100,
Expand Down Expand Up @@ -59,6 +57,8 @@ async def run_migrations():
team_secret TEXT UNIQUE
)''')

await database.execute('CREATE INDEX CONCURRENTLY team_secret_idx ON teams (team_secret);')

await database.execute('''
CREATE TABLE IF NOT EXISTS datasets (
dataset_id SERIAL PRIMARY KEY,
Expand All @@ -71,8 +71,7 @@ async def run_migrations():
game_id SERIAL PRIMARY KEY,
game_name TEXT,
is_contest BOOLEAN NOT NULL,
bots TEXT,
dataset_id int,
dataset_id INT,
start_time TIMESTAMP NOT NULL,
total_ticks INT NOT NULL,
tick_time INT NOT NULL,
Expand Down Expand Up @@ -129,15 +128,15 @@ async def run_migrations():
order_id SERIAL PRIMARY KEY,
game_id INT NOT NULL,
player_id INT NOT NULL,
order_type INT NOT NULL,
order_side INT NOT NULL,
order_status INT NOT NULL,
order_type TEXT NOT NULL,
order_side TEXT NOT NULL,
order_status TEXT NOT NULL,
price INT NOT NULL,
size INT NOT NULL,
tick INT NOT NULL,
timestamp TIMESTAMP NOT NULL,
expiration_tick INT NOT NULL,
resource INT NOT NULL,
resource TEXT NOT NULL,
filled_size INT NOT NULL DEFAULT 0,
filled_money INT NOT NULL DEFAULT 0,
Expand All @@ -152,14 +151,17 @@ async def run_migrations():
CREATE TABLE IF NOT EXISTS market (
game_id INT,
tick INT,
resource INT,
resource TEXT,
low INT,
high INT,
open INT,
close INT,
market INT,
volume INT,
PRIMARY KEY (game_id, tick, resource)
)''')

await database.execute('CREATE INDEX CONCURRENTLY tick_idx ON market (tick);')

await database.execute('''
CREATE TABLE IF NOT EXISTS dataset_data (
Expand Down
16 changes: 16 additions & 0 deletions backend/db/rate_limit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from fastapi import Request
from slowapi import Limiter
from slowapi.util import get_remote_address
from config import config


def team_secret(request: Request):
param = request.query_params.get("team_secret")
if param is None:
return get_remote_address(request)

return param


limiter = Limiter(key_func=team_secret, default_limits=["3/second"],
storage_uri=f"redis://localhost:{config['redis']['port']}/0")
19 changes: 17 additions & 2 deletions backend/db/table.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from typing import Any
from databases import Database
from dataclasses import fields
from dataclasses import fields, asdict
from .db import database
from enum import Enum
from logger import logger


class Table:
Expand Down Expand Up @@ -36,6 +37,20 @@ async def update(cls, **kwargs) -> int:
kwargs = _transform_kwargs(kwargs)
return await database.fetch_val(query, kwargs)

@classmethod
async def update_many(cls, l: list) -> int:
if len(l) == 0:
return 0

values = [_transform_kwargs(asdict(obj)) for obj in l]

cols = [field.name for field in fields(cls)]
set_query = ', '.join(
f'{col}=:{col}' for col in values[0] if col != cols[0])
query = f"UPDATE {cls.table_name} SET {set_query} WHERE {cols[0]}=:{cols[0]} RETURNING *"

return await database.execute_many(query, values)

@classmethod
async def delete(cls, **kwargs) -> int:
"""
Expand Down Expand Up @@ -73,7 +88,7 @@ async def list(cls, **kwargs):
"""
query, values = cls._select(**kwargs)
result = await database.fetch_all(query, values)
return [cls(**team) for team in result]
return [cls(**obj) for obj in result]

@classmethod
async def count(cls, **kwargs) -> int:
Expand Down
32 changes: 19 additions & 13 deletions backend/game/bots/resource_bot.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from typing import Dict, List
import pandas as pd
from game.tick.tick_data import TickData
from model import Order, Resource, OrderSide, Team, Player
Expand All @@ -18,7 +19,7 @@


class ResourceBot(Bot):
def __init__(self, *args, **kwargs):
def __init__(self, player_id=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.buy_prices = {resource: 50 for resource in Resource}
self.sell_prices = {resource: 50 for resource in Resource}
Expand All @@ -35,11 +36,11 @@ async def run(self, tick_data: TickData):
is_bot=True
)
self.game_id = tick_data.game.game_id

if self.last_tick is not None and tick_data.game.current_tick < self.last_tick + expiration_ticks:
return
self.last_tick = tick_data.game.current_tick

resources_sum = {resource: 0 for resource in Resource}
for resource in Resource:
for player in tick_data.players.values():
Expand All @@ -62,12 +63,15 @@ async def run(self, tick_data: TickData):
buy_volume = clamp(min_volume, max_volume, int(buy_volume))
sell_volume = clamp(min_volume, max_volume, int(sell_volume))

filled_buy_perc, filled_sell_perc = self.get_filled_perc(resource_orders)
filled_buy_perc, filled_sell_perc = self.get_filled_perc(
resource_orders)
buy_price -= price_change_coeff * buy_price * (1-2*filled_buy_perc)
sell_price += price_change_coeff * sell_price * (1-2*filled_sell_perc)
sell_price += price_change_coeff * \
sell_price * (1-2*filled_sell_perc)

if buy_price >= sell_price:
price = (buy_price * buy_volume + sell_price * sell_volume) / (buy_volume + sell_volume)
price = (buy_price * buy_volume + sell_price *
sell_volume) / (buy_volume + sell_volume)
buy_price = price
sell_price = price
buy_price = clamp(min_price, max_price, int(buy_price))
Expand All @@ -80,27 +84,29 @@ async def run(self, tick_data: TickData):
self.buy_prices[resource] = buy_price
self.sell_prices[resource] = sell_price

def get_filled_perc(self, orders: list[Order]):
def get_filled_perc(self, orders: List[Order]):
size = {side: 0 for side in OrderSide}
filled_size = {side: 0 for side in OrderSide}
for order in orders:
size[order.order_side] += order.size
filled_size[order.order_side] += order.filled_size
filled_perc = {side: filled_size[side] / size[side]
filled_perc = {side: filled_size[side] / size[side]
if size[side] > 0 else 0
for side in OrderSide}
return filled_perc[OrderSide.BUY], filled_perc[OrderSide.SELL]

async def get_last_orders(self) -> dict[str, Order]:
if self.last_tick is None: return []
async def get_last_orders(self) -> Dict[str, Order]:
if self.last_tick is None:
return []
orders_list = await Order.list(player_id=self.player_id, tick=self.last_tick)
orders = {resource: [] for resource in Resource}
for order in orders_list:
orders[order.resource].append(order)
return orders

async def create_orders(self, tick, resource, buy_price, sell_price, buy_volume, sell_volume) -> None:
logger.debug(f"({self.game_id}) Bot creating orders {tick=}, {resource=}, {buy_price=}, {sell_price=}, {buy_volume=}, {sell_volume=}")
logger.debug(
f"({self.game_id}) Bot creating orders {tick=}, {resource=}, {buy_price=}, {sell_price=}, {buy_volume=}, {sell_volume=}")
await Order.create(
game_id=self.game_id,
player_id=self.player_id,
Expand All @@ -126,4 +132,4 @@ async def create_orders(self, tick, resource, buy_price, sell_price, buy_volume,


def clamp(_min, _max, x):
return max(_min, min(_max, x))
return max(_min, min(_max, x))
5 changes: 3 additions & 2 deletions backend/game/fixtures/fixtures.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from typing import Dict, List
import pandas as pd
from game.tick import TickData, Ticker
from game.tick.ticker import GameData
Expand Down Expand Up @@ -172,13 +173,13 @@ def get_order(player_id: int, price: int, size: int, order_side: OrderSide, tick
return get_order


def get_player_dict(players: list[Player]) -> dict[int, Player]:
def get_player_dict(players: List[Player]) -> Dict[int, Player]:
return {player.player_id: player for player in players}


@pytest.fixture
def coal_market():
def get_coal_market(players: dict[int, Player] = {}) -> ResourceMarket:
def get_coal_market(players: Dict[int, Player] = {}) -> ResourceMarket:
return ResourceMarket(Resource.coal, players)
return get_coal_market

Expand Down
3 changes: 2 additions & 1 deletion backend/game/market/energy_market.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from typing import Dict
from model import Trade, Resource
from model.order import Order
from config import config
from model.player import Player


class EnergyMarket:
def match(self, players: dict[int, Player], demand: int, max_price: int) -> dict[int, int]:
def match(self, players: Dict[int, Player], demand: int, max_price: int) -> Dict[int, int]:
players_sorted = sorted(players.values(), key=lambda x: x.energy_price)
players_sorted = [
player for player in players_sorted if player.energy_price <= max_price]
Expand Down
4 changes: 2 additions & 2 deletions backend/game/market/resource_market.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from pprint import pprint
from typing import List
from typing import List, Dict
from game.orderbook.orderbook import OrderBook
from game.price_tracker.price_tracker import PriceTracker
from model import Resource, Trade, Order
from model.player import Player


class ResourceMarket:
def __init__(self, resource: Resource, players: dict[int, Player]):
def __init__(self, resource: Resource, players: Dict[int, Player]):
self.resource = resource
self.orderbook = OrderBook()
self.price_tracker = PriceTracker(self.orderbook)
Expand Down
24 changes: 15 additions & 9 deletions backend/game/price_tracker/price_tracker.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from typing import List
from game.orderbook import OrderBook
from model import Trade

Expand All @@ -8,21 +9,22 @@ def __init__(self, orderbook: OrderBook):
self.last_low = 0
self.last_open = 0
self.last_close = 0
self.last_market = 0
self.last_average = 0
self.volume = 0

self.high = None
self.low = None
self.open = None
self.close = None
self.market = None
self.average = None
orderbook.register_callback('on_end_match', self._calculate_low_high)

def _calculate_low_high(self, trades: list[Trade]):
def _calculate_low_high(self, trades: List[Trade]):
self.high = None
self.low = None
self.open = None
self.close = None
self.market = None
self.average = None

money_sum = 0
money_size = 0
Expand All @@ -47,13 +49,14 @@ def _calculate_low_high(self, trades: list[Trade]):
self.low = price

if money_size > 0:
self.market = money_sum / money_size
self.average = money_sum / money_size
self.volume = money_size

self._save_last()

def _save_last(self):
if self.market is not None:
self.last_market = self.market
if self.average is not None:
self.last_average = self.average
self.last_high = self.high
self.last_low = self.low
self.last_open = self.open
Expand All @@ -65,11 +68,14 @@ def get_low(self):
def get_high(self):
return self.high if self.high is not None else self.last_high

def get_market(self):
return self.market if self.market is not None else self.last_market
def get_average(self):
return self.average if self.average is not None else self.last_average

def get_open(self):
return self.open if self.open is not None else self.last_open

def get_close(self):
return self.close if self.close is not None else self.last_close

def get_volume(self):
return self.volume
10 changes: 5 additions & 5 deletions backend/game/price_tracker/test_price_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def test_price_tracker(get_order, get_timestamp):

assert price_tracker.get_high() == 15
assert price_tracker.get_low() == 3
assert price_tracker.get_market() == 8.25
assert price_tracker.get_average() == 8.25
assert price_tracker.get_open() == 5
assert price_tracker.get_close() == 3

Expand All @@ -41,7 +41,7 @@ def test_price_tracker(get_order, get_timestamp):

assert price_tracker.get_high() == 30
assert price_tracker.get_low() == 30
assert price_tracker.get_market() == 30
assert price_tracker.get_average() == 30
assert price_tracker.get_open() == 30
assert price_tracker.get_close() == 30

Expand All @@ -62,7 +62,7 @@ def test_price_tracker_market_weighted(get_order, get_timestamp):

assert price_tracker.get_high() == 25
assert price_tracker.get_low() == 5
assert price_tracker.get_market() == 20
assert price_tracker.get_average() == 20


def test_price_tracker_market_no_trades(get_order, get_timestamp):
Expand All @@ -76,7 +76,7 @@ def test_price_tracker_market_no_trades(get_order, get_timestamp):
orderbook.match(timestamp=get_timestamp(1))

assert len(orderbook.match_trades) == 1
assert price_tracker.get_market() == 5
assert price_tracker.get_average() == 5
orderbook.match(timestamp=get_timestamp(1))
assert len(orderbook.match_trades) == 0
assert price_tracker.get_market() == 5
assert price_tracker.get_average() == 5
Loading

0 comments on commit d75bf27

Please sign in to comment.