Compare commits

..

No commits in common. "9dda0e8b4a01703b6478b1f165a3514e33acf830" and "e0ea4c86fa1d59093e53eefc64c959448f84b129" have entirely different histories.

5 changed files with 82 additions and 202 deletions

View File

@ -3,7 +3,6 @@ from abc import ABC, abstractmethod
from alpaca.common.exceptions import APIError
from glom import glom
from oandapyV20.exceptions import V20Error
from pydantic.error_wrappers import ValidationError
from core.lib import schemas
from core.util import logs
@ -132,12 +131,7 @@ class BaseExchange(ABC):
def validate_response(self, response, method):
schema = self.get_schema(method)
# Return a dict of the validated response
try:
response_valid = schema(**response).dict()
except ValidationError as e:
log.error(f"Error validating {method} response: {response}")
log.error(f"Errors: {e}")
raise GenericAPIError("Error validating response")
response_valid = schema(**response).dict()
return response_valid
def call(self, method, *args, **kwargs):
@ -204,10 +198,6 @@ class BaseExchange(ABC):
def post_trade(self, trade):
pass
@abstractmethod
def close_trade(self, trade_id):
pass
@abstractmethod
def get_trade(self, trade_id):
pass

View File

@ -121,11 +121,6 @@ class AlpacaExchange(BaseExchange):
trade.save()
return order
def close_trade(self, trade_id): # TODO
"""
Close a trade
"""
def get_trade(self, trade_id):
pass # TODO

View File

@ -94,13 +94,6 @@ class OANDAExchange(BaseExchange):
trade.save()
return response
def close_trade(self, trade_id):
"""
Close a trade.
"""
r = trades.TradeClose(accountID=self.account_id, tradeID=trade_id)
return self.call(r)
def get_trade(self, trade_id):
# OANDA is off by one...
r = trades.TradeDetails(accountID=self.account_id, tradeID=trade_id)

View File

@ -386,6 +386,41 @@ AccountInstrumentsSchema = {
}
class OrderTransaction(BaseModel):
id: str
accountID: str
userID: int
batchID: str
requestID: str
time: str
type: str
instrument: str
units: str
timeInForce: str
positionFill: str
reason: str
class OrderCreate(BaseModel):
orderCreateTransaction: OrderTransaction
OrderCreateSchema = {
"id": "orderCreateTransaction.id",
"accountID": "orderCreateTransaction.accountID",
"userID": "orderCreateTransaction.userID",
"batchID": "orderCreateTransaction.batchID",
"requestID": "orderCreateTransaction.requestID",
"time": "orderCreateTransaction.time",
"type": "orderCreateTransaction.type",
"symbol": "orderCreateTransaction.instrument",
"units": "orderCreateTransaction.units",
"timeInForce": "orderCreateTransaction.timeInForce",
"positionFill": "orderCreateTransaction.positionFill",
"reason": "orderCreateTransaction.reason",
}
class PriceBid(BaseModel):
price: str
liquidity: int
@ -441,6 +476,11 @@ PricingInfoSchema = {
}
class LongPositionCloseout(BaseModel):
instrument: str
units: str
class Trade(BaseModel):
tradeID: str
clientTradeID: str
@ -458,52 +498,30 @@ class Trade(BaseModel):
trailingStopLossOrder: dict | None
class SideCarOrder(BaseModel):
id: str
createTime: str
state: str
price: str | None
timeInForce: str
gtdTime: str | None
clientExtensions: dict | None
tradeID: str
clientTradeID: str | None
type: str
time: str | None
priceBound: str | None
positionFill: str | None
reason: str | None
orderFillTransactionID: str | None
tradeOpenedID: str | None
tradeReducedID: str | None
tradeClosedIDs: list[str] | None
cancellingTransactionID: str | None
replacesOrderID: str | None
replacedByOrderID: str | None
class OpenTradesTrade(BaseModel):
id: str
instrument: str
price: str
openTime: str
initialUnits: str
initialMarginRequired: str
state: str
currentUnits: str
realizedPL: str
financing: str
dividendAdjustment: str
unrealizedPL: str
marginUsed: str
takeProfitOrder: SideCarOrder | None
stopLossOrder: SideCarOrder | None
trailingStopLossOrder: SideCarOrder | None
trailingStopValue: dict | None
{
"trades": [
{
"id": "14480",
"instrument": "EUR_USD",
"price": "1.06345",
"openTime": "2022-12-22T08:57:10.459593310Z",
"initialUnits": "100",
"initialMarginRequired": "2.9226",
"state": "OPEN",
"currentUnits": "100",
"realizedPL": "0.0000",
"financing": "0.0000",
"dividendAdjustment": "0.0000",
"unrealizedPL": "-0.0158",
"marginUsed": "2.9228",
}
],
"lastTransactionID": "14480",
}
class OpenTrades(BaseModel):
trades: list[OpenTradesTrade]
trades: list[Trade]
lastTransactionID: str
@ -512,8 +530,8 @@ OpenTradesSchema = {
"trades",
[
{
"id": "id",
"symbol": "instrument",
"id": "tradeID",
"instrument": "instrument",
"price": "price",
"openTime": "openTime",
"initialUnits": "initialUnits",
@ -528,7 +546,6 @@ OpenTradesSchema = {
"takeProfitOrder": "takeProfitOrder",
"stopLossOrder": "stopLossOrder",
"trailingStopLossOrder": "trailingStopLossOrder",
"trailingStopValue": "trailingStopValue",
}
],
),
@ -543,48 +560,6 @@ class HomeConversionFactors(BaseModel):
lossBaseHome: str
class LongPositionCloseout(BaseModel):
instrument: str
units: str
class OrderTransaction(BaseModel):
id: str
accountID: str
userID: int
batchID: str
requestID: str
time: str
type: str
instrument: str | None
units: str | None
timeInForce: str | None
positionFill: str | None
reason: str
longPositionCloseout: LongPositionCloseout | None
longOrderFillTransaction: dict | None
class OrderCreate(BaseModel):
orderCreateTransaction: OrderTransaction
OrderCreateSchema = {
"id": "orderCreateTransaction.id",
"accountID": "orderCreateTransaction.accountID",
"userID": "orderCreateTransaction.userID",
"batchID": "orderCreateTransaction.batchID",
"requestID": "orderCreateTransaction.requestID",
"time": "orderCreateTransaction.time",
"type": "orderCreateTransaction.type",
"symbol": "orderCreateTransaction.instrument",
"units": "orderCreateTransaction.units",
"timeInForce": "orderCreateTransaction.timeInForce",
"positionFill": "orderCreateTransaction.positionFill",
"reason": "orderCreateTransaction.reason",
}
class LongOrderFillTransaction(BaseModel):
id: str
accountID: str
@ -617,6 +592,23 @@ class LongOrderFillTransaction(BaseModel):
longPositionCloseout: LongPositionCloseout
class OrderTransaction(BaseModel):
id: str
accountID: str
userID: int
batchID: str
requestID: str
time: str
type: str
instrument: str | None
units: str | None
timeInForce: str | None
positionFill: str | None
reason: str
longPositionCloseout: LongPositionCloseout | None
longOrderFillTransaction: dict | None
class PositionClose(BaseModel):
longOrderCreateTransaction: OrderTransaction | None
longOrderFillTransaction: OrderTransaction | None
@ -686,25 +678,3 @@ TradeDetailsSchema = {
"clientExtensions": "trade.clientExtensions",
"lastTransactionID": "lastTransactionID",
}
class TradeClose(BaseModel):
orderCreateTransaction: OrderTransaction
TradeCloseSchema = {
"id": "orderCreateTransaction.id",
"accountID": "orderCreateTransaction.accountID",
"userID": "orderCreateTransaction.userID",
"batchID": "orderCreateTransaction.batchID",
"requestID": "orderCreateTransaction.requestID",
"time": "orderCreateTransaction.time",
"type": "orderCreateTransaction.type",
"symbol": "orderCreateTransaction.instrument",
"units": "orderCreateTransaction.units",
"timeInForce": "orderCreateTransaction.timeInForce",
"positionFill": "orderCreateTransaction.positionFill",
"reason": "orderCreateTransaction.reason",
"longPositionCloseout": "orderCreateTransaction.longPositionCloseout",
"longOrderFillTransaction": "orderCreateTransaction.longOrderFillTransaction",
}

View File

@ -1,80 +1,12 @@
from django.test import TestCase
from core.models import Trade
from core.tests.helpers import ElasticMock, LiveBase
class LiveTradingTestCase(ElasticMock, LiveBase, TestCase):
def setUp(self):
super(LiveTradingTestCase, self).setUp()
self.trade = Trade.objects.create(
user=self.user,
account=self.account,
symbol="EUR_USD",
time_in_force="FOK",
type="market",
amount=10,
direction="buy",
)
def test_account_functional(self):
"""
Test that the account is functional.
"""
balance = self.account.client.get_balance()
# We need some money to place trades
self.assertTrue(balance > 1000)
def open_trade(self):
posted = self.trade.post()
# Check the opened trade
self.assertEqual(posted["type"], "MARKET_ORDER")
self.assertEqual(posted["symbol"], "EUR_USD")
self.assertEqual(posted["units"], "10")
self.assertEqual(posted["timeInForce"], "FOK")
return posted
def close_trade(self):
# refresh the trade to get the trade id
self.trade.refresh_from_db()
closed = self.account.client.close_trade(self.trade.order_id)
# Check the feedback from closing the trade
self.assertEqual(closed["type"], "MARKET_ORDER")
self.assertEqual(closed["symbol"], "EUR_USD")
self.assertEqual(closed["units"], "-10")
self.assertEqual(closed["timeInForce"], "FOK")
self.assertEqual(closed["reason"], "TRADE_CLOSE")
return closed
def test_place_close_trade(self):
"""
Test placing a trade.
"""
self.open_trade()
self.close_trade()
def test_get_all_open_trades(self):
"""
Test getting all open trades.
"""
self.open_trade()
trades = self.account.client.get_all_open_trades()
self.trade.refresh_from_db()
found = False
for trade in trades["itemlist"]:
if trade["id"] == self.trade.order_id:
self.assertEqual(trade["symbol"], "EUR_USD")
self.assertEqual(trade["currentUnits"], "10")
self.assertEqual(trade["initialUnits"], "10")
self.assertEqual(trade["state"], "OPEN")
found = True
break
self.close_trade()
if not found:
self.fail("Could not find the trade in the list of open trades")
self.assertTrue(balance > 0)