Refactor opportunity scoring model

This commit is contained in:
2026-04-21 11:25:38 +08:00
parent 4761067c30
commit 50402e4aa7
7 changed files with 269 additions and 42 deletions

View File

@@ -128,11 +128,111 @@ class DustOverlapSpotClient(FakeSpotClient):
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:]):
setup_curve = [
1.4151,
1.4858,
1.3868,
1.5,
1.4009,
1.5142,
1.4151,
1.5,
1.4292,
1.4858,
1.4434,
1.4717,
1.4505,
1.4575,
1.4547,
1.4604,
1.4575,
1.4632,
1.4599,
1.466,
1.4618,
1.4698,
1.4745,
1.5,
]
for index, close in enumerate(setup_curve[-limit:]):
rows.append([index, close * 0.98, close * 1.01, close * 0.97, close, 100 + index * 10, index + 1, close * 100])
return rows
class OpportunityPatternSpotClient:
def account_info(self):
return {"balances": [{"asset": "USDT", "free": "100", "locked": "0"}]}
def ticker_price(self, symbols=None):
return []
def ticker_stats(self, symbols=None, *, window="1d"):
rows = {
"SETUPUSDT": {
"symbol": "SETUPUSDT",
"lastPrice": "106",
"priceChangePercent": "4",
"quoteVolume": "10000000",
"highPrice": "107",
"lowPrice": "98",
},
"CHASEUSDT": {
"symbol": "CHASEUSDT",
"lastPrice": "150",
"priceChangePercent": "18",
"quoteVolume": "9000000",
"highPrice": "152",
"lowPrice": "120",
},
}
if not symbols:
return list(rows.values())
return [rows[symbol] for symbol in symbols]
def exchange_info(self):
return {
"symbols": [
{"symbol": "SETUPUSDT", "status": "TRADING"},
{"symbol": "CHASEUSDT", "status": "TRADING"},
]
}
def klines(self, symbol, interval, limit):
curves = {
"SETUPUSDT": [
100,
105,
98,
106,
99,
107,
100,
106,
101,
105,
102,
104,
102.5,
103,
102.8,
103.2,
103.0,
103.4,
103.1,
103.6,
103.3,
103.8,
104.2,
106,
],
"CHASEUSDT": [120, 125, 130, 135, 140, 145, 150],
}[symbol]
rows = []
for index, close in enumerate(curves[-limit:]):
rows.append([index, close * 0.98, close * 1.01, close * 0.97, close, 100 + index * 20, index + 1, close * 100])
return rows
class OpportunityServiceTestCase(unittest.TestCase):
def setUp(self):
self.config = {
@@ -177,10 +277,13 @@ class OpportunityServiceTestCase(unittest.TestCase):
def test_scan_is_deterministic(self):
with patch.object(opportunity_service, "audit_event", return_value=None):
payload = opportunity_service.scan_opportunities(
self.config | {"opportunity": self.config["opportunity"] | {"top_n": 2}}, spot_client=FakeSpotClient()
self.config | {"opportunity": self.config["opportunity"] | {"top_n": 2}},
spot_client=OpportunityPatternSpotClient(),
)
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["symbol"] for item in payload["recommendations"]], ["SETUPUSDT", "CHASEUSDT"])
self.assertEqual([item["action"] for item in payload["recommendations"]], ["trigger", "chase"])
self.assertGreater(payload["recommendations"][0]["metrics"]["setup_score"], 0.6)
self.assertGreater(payload["recommendations"][1]["metrics"]["extension_penalty"], 1.0)
def test_scan_respects_ignore_dust_for_overlap_penalty(self):
client = DustOverlapSpotClient()
@@ -201,7 +304,7 @@ class OpportunityServiceTestCase(unittest.TestCase):
ignored_rec = ignored["recommendations"][0]
included_rec = included["recommendations"][0]
self.assertEqual(ignored_rec["action"], "enter")
self.assertEqual(ignored_rec["action"], "trigger")
self.assertEqual(ignored_rec["metrics"]["position_weight"], 0.0)
self.assertEqual(included_rec["action"], "skip")
self.assertEqual(included_rec["metrics"]["position_weight"], 1.0)