Fix opportunity ignore_dust handling
This commit is contained in:
@@ -90,6 +90,7 @@ def get_positions(
|
|||||||
config: dict[str, Any],
|
config: dict[str, Any],
|
||||||
*,
|
*,
|
||||||
spot_client: Any,
|
spot_client: Any,
|
||||||
|
ignore_dust: bool = True,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
quote = str(config.get("market", {}).get("default_quote", "USDT")).upper()
|
quote = str(config.get("market", {}).get("default_quote", "USDT")).upper()
|
||||||
dust = float(config.get("trading", {}).get("dust_usdt_threshold", 0.0))
|
dust = float(config.get("trading", {}).get("dust_usdt_threshold", 0.0))
|
||||||
@@ -102,7 +103,7 @@ def get_positions(
|
|||||||
asset = item["asset"]
|
asset = item["asset"]
|
||||||
mark_price = price_map.get(asset, 1.0 if asset == quote else 0.0)
|
mark_price = price_map.get(asset, 1.0 if asset == quote else 0.0)
|
||||||
notional = quantity * mark_price
|
notional = quantity * mark_price
|
||||||
if notional < dust:
|
if ignore_dust and notional < dust:
|
||||||
continue
|
continue
|
||||||
rows.append(
|
rows.append(
|
||||||
asdict(
|
asdict(
|
||||||
|
|||||||
@@ -48,13 +48,14 @@ def scan_opportunities(
|
|||||||
symbols: list[str] | None = None,
|
symbols: list[str] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
opportunity_config = config.get("opportunity", {})
|
opportunity_config = config.get("opportunity", {})
|
||||||
|
ignore_dust = bool(opportunity_config.get("ignore_dust", True))
|
||||||
signal_weights = get_signal_weights(config)
|
signal_weights = get_signal_weights(config)
|
||||||
interval = get_signal_interval(config)
|
interval = get_signal_interval(config)
|
||||||
thresholds = _opportunity_thresholds(config)
|
thresholds = _opportunity_thresholds(config)
|
||||||
scan_limit = int(opportunity_config.get("scan_limit", 50))
|
scan_limit = int(opportunity_config.get("scan_limit", 50))
|
||||||
top_n = int(opportunity_config.get("top_n", 10))
|
top_n = int(opportunity_config.get("top_n", 10))
|
||||||
quote = str(config.get("market", {}).get("default_quote", "USDT")).upper()
|
quote = str(config.get("market", {}).get("default_quote", "USDT")).upper()
|
||||||
held_positions = get_positions(config, spot_client=spot_client)["positions"]
|
held_positions = get_positions(config, spot_client=spot_client, ignore_dust=ignore_dust)["positions"]
|
||||||
concentration_map = {normalize_symbol(item["symbol"]): float(item["notional_usdt"]) for item in held_positions}
|
concentration_map = {normalize_symbol(item["symbol"]): float(item["notional_usdt"]) for item in held_positions}
|
||||||
total_held = sum(concentration_map.values()) or 1.0
|
total_held = sum(concentration_map.values()) or 1.0
|
||||||
|
|
||||||
|
|||||||
@@ -93,3 +93,14 @@ class AccountMarketServicesTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
universe = market_service.get_scan_universe(config, spot_client=FakeSpotClient())
|
universe = market_service.get_scan_universe(config, spot_client=FakeSpotClient())
|
||||||
self.assertEqual([item["symbol"] for item in universe], ["BTCUSDT", "ETHUSDT"])
|
self.assertEqual([item["symbol"] for item in universe], ["BTCUSDT", "ETHUSDT"])
|
||||||
|
|
||||||
|
def test_get_positions_can_include_dust(self):
|
||||||
|
config = {
|
||||||
|
"market": {"default_quote": "USDT"},
|
||||||
|
"trading": {"dust_usdt_threshold": 10.0},
|
||||||
|
}
|
||||||
|
ignored = account_service.get_positions(config, spot_client=FakeSpotClient())
|
||||||
|
included = account_service.get_positions(config, spot_client=FakeSpotClient(), ignore_dust=False)
|
||||||
|
|
||||||
|
self.assertEqual([item["symbol"] for item in ignored["positions"]], ["USDT", "BTCUSDT"])
|
||||||
|
self.assertEqual([item["symbol"] for item in included["positions"]], ["USDT", "BTCUSDT", "DOGEUSDT"])
|
||||||
|
|||||||
@@ -100,6 +100,39 @@ class FakeSpotClient:
|
|||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
class DustOverlapSpotClient(FakeSpotClient):
|
||||||
|
def account_info(self):
|
||||||
|
return {"balances": [{"asset": "XRP", "free": "5", "locked": "0"}]}
|
||||||
|
|
||||||
|
def ticker_price(self, symbols=None):
|
||||||
|
mapping = {"XRPUSDT": {"symbol": "XRPUSDT", "price": "1.5"}}
|
||||||
|
return [mapping[symbol] for symbol in symbols]
|
||||||
|
|
||||||
|
def ticker_stats(self, symbols=None, *, window="1d"):
|
||||||
|
rows = {
|
||||||
|
"XRPUSDT": {
|
||||||
|
"symbol": "XRPUSDT",
|
||||||
|
"lastPrice": "1.5",
|
||||||
|
"priceChangePercent": "10",
|
||||||
|
"quoteVolume": "5000000",
|
||||||
|
"highPrice": "1.52",
|
||||||
|
"lowPrice": "1.2",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if not symbols:
|
||||||
|
return list(rows.values())
|
||||||
|
return [rows[symbol] for symbol in symbols]
|
||||||
|
|
||||||
|
def exchange_info(self):
|
||||||
|
return {"symbols": [{"symbol": "XRPUSDT", "status": "TRADING"}]}
|
||||||
|
|
||||||
|
def klines(self, symbol, interval, limit):
|
||||||
|
rows = []
|
||||||
|
for index, close in enumerate([1.0, 1.1, 1.2, 1.3, 1.4, 1.45, 1.5][-limit:]):
|
||||||
|
rows.append([index, close * 0.98, close * 1.01, close * 0.97, close, 100 + index * 10, index + 1, close * 100])
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
class OpportunityServiceTestCase(unittest.TestCase):
|
class OpportunityServiceTestCase(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.config = {
|
self.config = {
|
||||||
@@ -149,6 +182,31 @@ class OpportunityServiceTestCase(unittest.TestCase):
|
|||||||
self.assertEqual([item["symbol"] for item in payload["recommendations"]], ["SOLUSDT", "BTCUSDT"])
|
self.assertEqual([item["symbol"] for item in payload["recommendations"]], ["SOLUSDT", "BTCUSDT"])
|
||||||
self.assertEqual([item["action"] for item in payload["recommendations"]], ["enter", "enter"])
|
self.assertEqual([item["action"] for item in payload["recommendations"]], ["enter", "enter"])
|
||||||
|
|
||||||
|
def test_scan_respects_ignore_dust_for_overlap_penalty(self):
|
||||||
|
client = DustOverlapSpotClient()
|
||||||
|
base_config = self.config | {
|
||||||
|
"opportunity": self.config["opportunity"] | {
|
||||||
|
"top_n": 1,
|
||||||
|
"ignore_dust": True,
|
||||||
|
"overlap_penalty": 2.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
with patch.object(opportunity_service, "audit_event", return_value=None):
|
||||||
|
ignored = opportunity_service.scan_opportunities(base_config, spot_client=client, symbols=["XRPUSDT"])
|
||||||
|
included = opportunity_service.scan_opportunities(
|
||||||
|
base_config | {"opportunity": base_config["opportunity"] | {"ignore_dust": False}},
|
||||||
|
spot_client=client,
|
||||||
|
symbols=["XRPUSDT"],
|
||||||
|
)
|
||||||
|
ignored_rec = ignored["recommendations"][0]
|
||||||
|
included_rec = included["recommendations"][0]
|
||||||
|
|
||||||
|
self.assertEqual(ignored_rec["action"], "enter")
|
||||||
|
self.assertEqual(ignored_rec["metrics"]["position_weight"], 0.0)
|
||||||
|
self.assertEqual(included_rec["action"], "skip")
|
||||||
|
self.assertEqual(included_rec["metrics"]["position_weight"], 1.0)
|
||||||
|
self.assertLess(included_rec["score"], ignored_rec["score"])
|
||||||
|
|
||||||
def test_signal_score_handles_empty_klines(self):
|
def test_signal_score_handles_empty_klines(self):
|
||||||
score, metrics = signal_service.score_market_signal([], [], {"price_change_pct": 1.0}, {})
|
score, metrics = signal_service.score_market_signal([], [], {"price_change_pct": 1.0}, {})
|
||||||
self.assertEqual(score, 0.0)
|
self.assertEqual(score, 0.0)
|
||||||
|
|||||||
Reference in New Issue
Block a user