удалить лимиты
This commit is contained in:
@@ -5,14 +5,13 @@ from zoneinfo import ZoneInfo
|
||||
|
||||
import pytest
|
||||
|
||||
from llm_trade_bot.agent import AgentTools, _has_existing_idea
|
||||
from llm_trade_bot.agent import AgentTools
|
||||
from llm_trade_bot.config import (
|
||||
AppConfig,
|
||||
BraveConfig,
|
||||
BybitConfig,
|
||||
ZaiConfig,
|
||||
PromptsConfig,
|
||||
RiskConfig,
|
||||
ScannerConfig,
|
||||
ScheduleConfig,
|
||||
StorageConfig,
|
||||
@@ -55,28 +54,27 @@ def make_config(database_path: Path) -> AppConfig:
|
||||
PromptsConfig(database_path.parent / "prompts.md"),
|
||||
ScheduleConfig(ZoneInfo("Europe/Warsaw"), (time(9), time(21))),
|
||||
ScannerConfig(),
|
||||
RiskConfig(),
|
||||
StorageConfig(database_path),
|
||||
)
|
||||
|
||||
|
||||
async def test_open_trade_is_risk_sized_and_deduplicated_within_run(tmp_path):
|
||||
async def test_open_trade_uses_all_available_buying_power_and_allows_repeated_entries(tmp_path):
|
||||
database = Database(tmp_path / "state.db")
|
||||
await database.open()
|
||||
bybit = FakeBybit()
|
||||
tools = AgentTools(make_config(database.path), database, bybit) # type: ignore[arg-type]
|
||||
run_id = await database.start_run("test")
|
||||
context = {"run_id": run_id, "candidate_keys": {"linear:BTCUSDT"}}
|
||||
context = {"run_id": run_id}
|
||||
try:
|
||||
first = await tools.open_trade(
|
||||
"linear", "BTCUSDT", "Buy", "Market", 90, 2, "test", take_profit=120, leverage=3, context=context
|
||||
"linear", "BTCUSDT", "Buy", "Market", 90, "test", take_profit=120, leverage=3, context=context
|
||||
)
|
||||
second = await tools.open_trade(
|
||||
"linear", "BTCUSDT", "Buy", "Market", 90, 2, "test", take_profit=120, leverage=3, context=context
|
||||
"linear", "BTCUSDT", "Buy", "Market", 90, "test", take_profit=120, leverage=3, context=context
|
||||
)
|
||||
assert first["quantity"] == "20"
|
||||
assert second["deduplicated"] is True
|
||||
assert len(bybit.orders) == 1
|
||||
assert first["quantity"] == "300"
|
||||
assert second["quantity"] == "300"
|
||||
assert len(bybit.orders) == 2
|
||||
assert bybit.orders[0]["stopLoss"] == "90"
|
||||
finally:
|
||||
await database.close()
|
||||
@@ -95,29 +93,9 @@ async def test_pause_blocks_entry_from_running_session(tmp_path):
|
||||
"Buy",
|
||||
"Market",
|
||||
90,
|
||||
2,
|
||||
"test",
|
||||
take_profit=120,
|
||||
context={"run_id": 1, "candidate_keys": {"linear:BTCUSDT"}},
|
||||
context={"run_id": 1},
|
||||
)
|
||||
finally:
|
||||
await database.close()
|
||||
|
||||
|
||||
def test_seeded_spot_balance_does_not_block_new_agent_entry():
|
||||
state = {
|
||||
"wallets": {"spot": {"balances": [{"asset": "SOL", "free": "4", "locked": "0"}]}},
|
||||
"positions": [],
|
||||
"open_orders": [],
|
||||
}
|
||||
assert _has_existing_idea(state, "spot", "SOLUSDT") is False
|
||||
|
||||
state["open_orders"] = [
|
||||
{
|
||||
"category": "spot",
|
||||
"symbol": "SOLUSDT",
|
||||
"protective": True,
|
||||
"client_id": "ltb-12-abcd-tp",
|
||||
}
|
||||
]
|
||||
assert _has_existing_idea(state, "spot", "SOLUSDT") is True
|
||||
|
||||
@@ -73,7 +73,6 @@ def request(category: Category, **changes: Any) -> OpenTradeRequest:
|
||||
"stop_loss": Decimal(90),
|
||||
"take_profit": Decimal(120),
|
||||
"trailing_distance": None,
|
||||
"risk_pct": Decimal(2),
|
||||
"leverage": Decimal(3),
|
||||
"rationale": "test",
|
||||
}
|
||||
|
||||
@@ -24,11 +24,6 @@ timezone = "Europe/Warsaw"
|
||||
times = ["09:00", "21:00"]
|
||||
[scanner]
|
||||
categories = ["spot", "linear"]
|
||||
[risk]
|
||||
max_risk_per_trade_pct = 2
|
||||
max_total_exposure_pct = 40
|
||||
max_positions = 5
|
||||
max_leverage = 3
|
||||
[storage]
|
||||
database_path = "state.db"
|
||||
"""
|
||||
@@ -57,12 +52,6 @@ def test_loads_and_resolves_database_path(tmp_path: Path):
|
||||
assert config.prompts.file_path == tmp_path / "prompts.md"
|
||||
|
||||
|
||||
def test_rejects_risk_above_hard_limit(tmp_path: Path):
|
||||
path = write_config(tmp_path, CONFIG.replace("max_leverage = 3", "max_leverage = 4"))
|
||||
with pytest.raises(ConfigError, match="max_leverage"):
|
||||
load_config(path)
|
||||
|
||||
|
||||
def test_rejects_non_official_zai_endpoint(tmp_path: Path):
|
||||
changed = CONFIG.replace("api_key = \"zai-key\"", 'api_key = "zai-key"\nbase_url = "https://example.com"', 1)
|
||||
path = write_config(tmp_path, changed)
|
||||
|
||||
@@ -2,7 +2,6 @@ from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from llm_trade_bot.config import RiskConfig
|
||||
from llm_trade_bot.models import (
|
||||
AccountSnapshot,
|
||||
Category,
|
||||
@@ -11,12 +10,12 @@ from llm_trade_bot.models import (
|
||||
OrderType,
|
||||
Side,
|
||||
)
|
||||
from llm_trade_bot.risk import RiskEngine, RiskRejected
|
||||
from llm_trade_bot.risk import OrderRejected, OrderValidator
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def engine() -> RiskEngine:
|
||||
return RiskEngine(RiskConfig())
|
||||
def engine() -> OrderValidator:
|
||||
return OrderValidator()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -40,7 +39,6 @@ def request(**overrides) -> OpenTradeRequest:
|
||||
"stop_loss": Decimal(90),
|
||||
"take_profit": Decimal(120),
|
||||
"trailing_distance": None,
|
||||
"risk_pct": Decimal(2),
|
||||
"leverage": Decimal(3),
|
||||
"rationale": "test",
|
||||
}
|
||||
@@ -48,36 +46,28 @@ def request(**overrides) -> OpenTradeRequest:
|
||||
return OpenTradeRequest(**values)
|
||||
|
||||
|
||||
def test_sizes_by_risk_and_exposure(engine, account, rules):
|
||||
def test_uses_all_available_buying_power(engine, account, rules):
|
||||
result = engine.validate_open(request(), account, rules)
|
||||
assert result.quantity == Decimal(20)
|
||||
assert result.notional == Decimal(2000)
|
||||
assert result.quantity == Decimal(300)
|
||||
assert result.notional == Decimal(30000)
|
||||
|
||||
|
||||
def test_remaining_exposure_caps_position(engine, rules):
|
||||
def test_existing_exposure_does_not_cap_position(engine, rules):
|
||||
account = AccountSnapshot(Decimal(10000), Decimal(10000), Decimal(3900), 2)
|
||||
result = engine.validate_open(request(), account, rules)
|
||||
assert result.quantity == Decimal(1)
|
||||
assert result.notional == Decimal(100)
|
||||
assert result.quantity == Decimal(300)
|
||||
assert result.notional == Decimal(30000)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"overrides, message",
|
||||
[
|
||||
({"risk_pct": Decimal("2.1")}, "risk_pct"),
|
||||
({"leverage": Decimal(4)}, "Leverage"),
|
||||
({"leverage": Decimal(101)}, "Leverage"),
|
||||
({"stop_loss": Decimal(101)}, "stop-loss"),
|
||||
({"take_profit": None, "trailing_distance": None}, "take-profit or trailing"),
|
||||
({"category": Category.SPOT, "order_type": OrderType.MARKET}, "Spot entries"),
|
||||
({"category": Category.SPOT, "order_type": OrderType.LIMIT, "limit_price": Decimal(100), "take_profit": None}, "Spot entry requires"),
|
||||
],
|
||||
)
|
||||
def test_rejects_unsafe_requests(engine, account, rules, overrides, message):
|
||||
with pytest.raises(RiskRejected, match=message):
|
||||
with pytest.raises(OrderRejected, match=message):
|
||||
engine.validate_open(request(**overrides), account, rules)
|
||||
|
||||
|
||||
def test_rejects_max_positions(engine, rules):
|
||||
account = AccountSnapshot(Decimal(10000), Decimal(10000), Decimal(0), 5)
|
||||
with pytest.raises(RiskRejected, match="Maximum"):
|
||||
engine.validate_open(request(), account, rules)
|
||||
|
||||
@@ -13,10 +13,10 @@ def test_serializes_glm_tool_round_in_chat_completions_format():
|
||||
]
|
||||
)
|
||||
|
||||
messages = ZaiLLM._serialize_conversation(conversation, "Follow the risk limits.")
|
||||
messages = ZaiLLM._serialize_conversation(conversation, "Use the available buying power.")
|
||||
|
||||
assert messages == [
|
||||
{"role": "system", "content": "Follow the risk limits."},
|
||||
{"role": "system", "content": "Use the available buying power."},
|
||||
{"role": "user", "content": "analyse BTC"},
|
||||
{
|
||||
"role": "assistant",
|
||||
|
||||
Reference in New Issue
Block a user