Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions qlib/backtest/exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,11 @@ def _get_buy_amount_by_cash_limit(self, trade_price: float, cash: float, cost_ra
"""
max_trade_amount = 0.0
if cash >= self.min_cost:
if cost_ratio <= 0:
# Without a proportional fee the service fee is always `min_cost`,
# so there is no critical price to compare against.
max_trade_amount = (cash - self.min_cost) / trade_price
return max_trade_amount
# critical_price means the stock transaction price when the service fee is equal to min_cost.
critical_price = self.min_cost / cost_ratio + self.min_cost
if cash >= critical_price:
Expand Down
60 changes: 60 additions & 0 deletions tests/backtest/test_exchange_cost.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

import unittest

from qlib.backtest.exchange import Exchange


def make_exchange(min_cost: float) -> Exchange:
"""Build an Exchange for testing `_get_buy_amount_by_cash_limit` only.

The method is pure arithmetic over `min_cost`, so the instance is created
without `__init__` to keep the test free of the data layer.
"""
exchange = object.__new__(Exchange)
exchange.min_cost = min_cost
return exchange


class TestBuyAmountByCashLimit(unittest.TestCase):
"""`_get_buy_amount_by_cash_limit` must handle a zero proportional fee."""

TRADE_PRICE = 10.0
CASH = 1000.0

def test_default_cost_ratio(self):
"""With the default fees, the min_cost branch applies at this cash level."""
exchange = make_exchange(min_cost=5.0)
# critical_price = 5 / 0.0015 + 5 = 3338.3 > cash, so the fee is min_cost.
amount = exchange._get_buy_amount_by_cash_limit(self.TRADE_PRICE, self.CASH, cost_ratio=0.0015)
self.assertAlmostEqual(amount, (self.CASH - 5.0) / self.TRADE_PRICE)

def test_cost_ratio_above_critical_price(self):
"""Above the critical price the proportional fee applies."""
exchange = make_exchange(min_cost=5.0)
cash = 10_000.0
amount = exchange._get_buy_amount_by_cash_limit(self.TRADE_PRICE, cash, cost_ratio=0.0015)
self.assertAlmostEqual(amount, cash / 1.0015 / self.TRADE_PRICE)

def test_zero_cost_ratio_with_min_cost(self):
"""A zero proportional fee must not divide by zero; min_cost still applies."""
exchange = make_exchange(min_cost=5.0)
amount = exchange._get_buy_amount_by_cash_limit(self.TRADE_PRICE, self.CASH, cost_ratio=0.0)
self.assertAlmostEqual(amount, (self.CASH - 5.0) / self.TRADE_PRICE)

def test_frictionless(self):
"""With no fee at all the whole cash balance is investable."""
exchange = make_exchange(min_cost=0.0)
amount = exchange._get_buy_amount_by_cash_limit(self.TRADE_PRICE, self.CASH, cost_ratio=0.0)
self.assertAlmostEqual(amount, self.CASH / self.TRADE_PRICE)

def test_cash_below_min_cost(self):
"""Cash that cannot even cover the minimum fee buys nothing."""
exchange = make_exchange(min_cost=5.0)
amount = exchange._get_buy_amount_by_cash_limit(self.TRADE_PRICE, 1.0, cost_ratio=0.0)
self.assertEqual(amount, 0.0)


if __name__ == "__main__":
unittest.main()