feat: AI agent, signal engine, surge detector, portfolio simulator
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
62
tests/test_portfolio.py
Normal file
62
tests/test_portfolio.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import pytest
|
||||
from engine.portfolio import PortfolioManager
|
||||
|
||||
@pytest.fixture
|
||||
def pm():
|
||||
return PortfolioManager(initial_capital=200.0)
|
||||
|
||||
def test_initial_state(pm):
|
||||
assert pm.cash == 200.0
|
||||
assert pm.positions == {}
|
||||
assert pm.trades == []
|
||||
|
||||
def test_buy(pm):
|
||||
pm.buy("BTCUSDT", price=40000.0, score=85)
|
||||
assert "BTCUSDT" in pm.positions
|
||||
assert pm.cash < 200.0
|
||||
assert len(pm.trades) == 1
|
||||
assert pm.trades[0]["side"] == "BUY"
|
||||
|
||||
def test_buy_size_by_score(pm):
|
||||
pm.buy("SOLUSDT", price=140.0, score=75)
|
||||
assert abs(pm.trades[0]["amount_usd"] - 30.0) < 0.01
|
||||
|
||||
def test_buy_respects_max_positions(pm):
|
||||
for i, coin in enumerate(["A", "B", "C", "D", "E"]):
|
||||
pm.buy(f"{coin}USDT", price=10.0, score=80)
|
||||
pm.buy("FUSDT", price=10.0, score=80)
|
||||
assert len(pm.positions) == 5
|
||||
|
||||
def test_buy_respects_min_position(pm):
|
||||
pm.cash = 10.0
|
||||
pm.buy("BTCUSDT", price=40000.0, score=85)
|
||||
assert "BTCUSDT" not in pm.positions
|
||||
|
||||
def test_sell_full(pm):
|
||||
pm.buy("ETHUSDT", price=3500.0, score=80)
|
||||
invested = pm.positions["ETHUSDT"]["invested_usd"]
|
||||
pm.sell("ETHUSDT", price=3800.0, reason="signal")
|
||||
assert "ETHUSDT" not in pm.positions
|
||||
assert pm.cash > 200.0 - invested
|
||||
|
||||
def test_stop_loss(pm):
|
||||
pm.buy("DOGEUSDT", price=0.10, score=80)
|
||||
pm.check_exit("DOGEUSDT", current_price=0.091)
|
||||
assert "DOGEUSDT" not in pm.positions
|
||||
|
||||
def test_take_profit_partial(pm):
|
||||
pm.buy("SOLUSDT", price=100.0, score=80)
|
||||
qty_before = pm.positions["SOLUSDT"]["quantity"]
|
||||
pm.check_exit("SOLUSDT", current_price=116.0)
|
||||
assert pm.positions["SOLUSDT"]["quantity"] < qty_before
|
||||
|
||||
def test_take_profit_full(pm):
|
||||
pm.buy("SOLUSDT", price=100.0, score=80)
|
||||
pm.check_exit("SOLUSDT", current_price=126.0)
|
||||
assert "SOLUSDT" not in pm.positions
|
||||
|
||||
def test_pnl_calculation(pm):
|
||||
pm.buy("ETHUSDT", price=3500.0, score=80)
|
||||
pnl = pm.get_portfolio_value({"ETHUSDT": 3800.0})
|
||||
assert pnl["total_pnl"] > 0
|
||||
assert pnl["total_value"] > 200.0
|
||||
39
tests/test_signal.py
Normal file
39
tests/test_signal.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import pytest
|
||||
from engine.signal import SignalEngine
|
||||
|
||||
@pytest.fixture
|
||||
def engine():
|
||||
return SignalEngine()
|
||||
|
||||
def test_compute_score_default_weights(engine):
|
||||
score = engine.compute_score(80, 60, 70, 50)
|
||||
assert score == 72.0
|
||||
|
||||
def test_classify_buy(engine):
|
||||
assert engine.classify(75) == "BUY"
|
||||
|
||||
def test_classify_hold(engine):
|
||||
assert engine.classify(55) == "HOLD"
|
||||
|
||||
def test_classify_sell(engine):
|
||||
assert engine.classify(30) == "SELL"
|
||||
|
||||
def test_custom_weights(engine):
|
||||
engine.set_weights({"technical": 0.4, "news": 0.3, "social": 0.2, "ai": 0.1})
|
||||
score = engine.compute_score(80, 60, 70, 50)
|
||||
assert score == 69.0
|
||||
|
||||
def test_weights_must_sum_to_one(engine):
|
||||
with pytest.raises(ValueError):
|
||||
engine.set_weights({"technical": 0.5, "news": 0.3, "social": 0.1, "ai": 0.2})
|
||||
|
||||
def test_rank_coins(engine):
|
||||
coins = {
|
||||
"BTC": {"technical": 80, "news": 70, "social": 60, "ai": 75},
|
||||
"ETH": {"technical": 90, "news": 80, "social": 70, "ai": 85},
|
||||
"DOGE": {"technical": 30, "news": 25, "social": 40, "ai": 20},
|
||||
}
|
||||
ranked = engine.rank_coins(coins)
|
||||
assert ranked[0]["symbol"] == "ETH"
|
||||
assert ranked[-1]["symbol"] == "DOGE"
|
||||
assert ranked[-1]["signal"] == "SELL"
|
||||
21
tests/test_surge.py
Normal file
21
tests/test_surge.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import pytest
|
||||
from engine.surge import SurgeDetector
|
||||
|
||||
def test_detect_surge():
|
||||
detector = SurgeDetector(multiplier=3.0)
|
||||
tickers = [
|
||||
{"symbol": "BTCUSDT", "quoteVolume": "1000000"},
|
||||
{"symbol": "NEWUSDT", "quoteVolume": "5000000"},
|
||||
{"symbol": "ETHUSDT", "quoteVolume": "800000"},
|
||||
]
|
||||
avg_volumes = {"BTCUSDT": 900000, "NEWUSDT": 1000000, "ETHUSDT": 750000}
|
||||
surged = detector.detect(tickers, avg_volumes)
|
||||
assert "NEWUSDT" in surged
|
||||
assert "BTCUSDT" not in surged
|
||||
|
||||
def test_no_surge():
|
||||
detector = SurgeDetector(multiplier=3.0)
|
||||
tickers = [{"symbol": "BTCUSDT", "quoteVolume": "1000000"}]
|
||||
avg_volumes = {"BTCUSDT": 900000}
|
||||
surged = detector.detect(tickers, avg_volumes)
|
||||
assert len(surged) == 0
|
||||
Reference in New Issue
Block a user