Compare commits

...

3 Commits

5 changed files with 251 additions and 30 deletions

View File

@ -117,6 +117,16 @@ class OANDAExchange(BaseExchange):
items.append(item) items.append(item)
return items return items
def close_position(self, side, symbol):
data = {
f"{side}Units": "ALL",
}
r = positions.PositionClose(
accountID=self.account_id, instrument=symbol, data=data
)
response = self.call(r)
return response
def close_all_positions(self): def close_all_positions(self):
# all_positions = self.get_all_positions() # all_positions = self.get_all_positions()
@ -124,5 +134,4 @@ class OANDAExchange(BaseExchange):
# print("POS ITER", position) # print("POS ITER", position)
r = positions.PositionClose(accountID=self.account_id) r = positions.PositionClose(accountID=self.account_id)
response = self.call(r) response = self.call(r)
print("CLOSE ALL POSITIONS", response)
return response return response

View File

@ -8,6 +8,37 @@ from core.util import logs
log = logs.get_logger(__name__) log = logs.get_logger(__name__)
def crossfilter(account, symbol, direction, func):
"""
Determine if we are betting against ourselves.
Checks open positions for the account, rejecting the trade if there is one
with an opposite direction to this one.
:param account: Account object
:param symbol: Symbol
:param direction: Direction of the trade
:param func: Whether we are checking entries or exits
:return: dict of action and opposing position, or False
"""
position_info = account.client.get_position_info(symbol)
if direction == "buy":
opposing_side = "short"
elif direction == "sell":
opposing_side = "long"
opposing_position_info = position_info[opposing_side]
if opposing_position_info["units"] != "0":
if func == "entry":
return {"action": "rejected", "positions": opposing_position_info}
elif func == "exit":
# Pass back opposing side so we can close it
return {
"action": "close",
"side": opposing_side,
"positions": opposing_position_info,
}
return False
def get_pair(account, base, quote, invert=False): def get_pair(account, base, quote, invert=False):
""" """
Get the pair for the given account and currencies. Get the pair for the given account and currencies.
@ -258,13 +289,15 @@ def get_price_bound(direction, strategy, price, current_price):
return price_bound return price_bound
def execute_strategy(callback, strategy): def execute_strategy(callback, strategy, func):
""" """
Execute a strategy. Execute a strategy.
:param callback: Callback object :param callback: Callback object
:param strategy: Strategy object :param strategy: Strategy object
""" """
# Only check times for entries. We can always exit trades.
if func == "entry":
# Check if we can trade now! # Check if we can trade now!
now_utc = datetime.utcnow() now_utc = datetime.utcnow()
trading_times = strategy.trading_times.all() trading_times = strategy.trading_times.all()
@ -276,10 +309,6 @@ def execute_strategy(callback, strategy):
log.debug("Not within trading time range") log.debug("Not within trading time range")
return return
# Get the account's balance in the native account currency
cash_balance = strategy.account.client.get_balance()
log.debug(f"Cash balance: {cash_balance}")
# Instruments supported by the account # Instruments supported by the account
if not strategy.account.instruments: if not strategy.account.instruments:
strategy.account.update_info() strategy.account.update_info()
@ -329,12 +358,34 @@ def execute_strategy(callback, strategy):
price = round(D(callback.price), display_precision) price = round(D(callback.price), display_precision)
log.debug(f"Extracted price of quote: {price}") log.debug(f"Extracted price of quote: {price}")
type = strategy.order_type
current_price = get_price(account, direction, symbol) current_price = get_price(account, direction, symbol)
log.debug(f"Callback price: {price}") log.debug(f"Callback price: {price}")
log.debug(f"Current price: {current_price}") log.debug(f"Current price: {current_price}")
# Calculate price bound and round to the display precision
price_bound = get_price_bound(direction, strategy, price, current_price)
if not price_bound:
return
price_bound = round(price_bound, display_precision)
# Callback now verified
if func == "exit":
check_exit = crossfilter(account, symbol, direction, func)
if not check_exit:
log.debug("Exit conditions not met.")
return
if check_exit["action"] == "close":
log.debug(f"Closing position on exit signal: {symbol}")
side = check_exit["side"]
response = account.client.close_position(side, symbol)
log.debug(f"Close position response: {response}")
return
type = strategy.order_type
# Get the account's balance in the native account currency
cash_balance = strategy.account.client.get_balance()
log.debug(f"Cash balance: {cash_balance}")
# Convert the trade size, which is currently in the account's base currency, # Convert the trade size, which is currently in the account's base currency,
# to the base currency of the pair we are trading # to the base currency of the pair we are trading
trade_size_in_base = get_trade_size_in_base( trade_size_in_base = get_trade_size_in_base(
@ -349,12 +400,6 @@ def execute_strategy(callback, strategy):
if "tsl" in protection: if "tsl" in protection:
trailing_stop_loss = protection["tsl"] trailing_stop_loss = protection["tsl"]
# Calculate price bound and round to the display precision
price_bound = get_price_bound(direction, strategy, price, current_price)
if not price_bound:
return
price_bound = round(price_bound, display_precision)
# Create object, note that the amount is rounded to the trade precision # Create object, note that the amount is rounded to the trade precision
new_trade = Trade.objects.create( new_trade = Trade.objects.create(
user=user, user=user,
@ -378,6 +423,19 @@ def execute_strategy(callback, strategy):
round(trailing_stop_loss, display_precision) round(trailing_stop_loss, display_precision)
) )
new_trade.save() new_trade.save()
# Run the crossfilter to ensure we don't trade the same pair in opposite directions
filtered = crossfilter(account, symbol, direction, func)
# TP/SL calculation and get_trade_size_in_base are wasted here, but it's important
# to record the decision in the Trade object. We can only get it after we do those.
# It shows what would be done.
if filtered:
log.debug(f"Trade filtered. Action: {filtered['action']}")
if filtered["action"] == "rejected":
new_trade.status = "rejected"
new_trade.save()
else:
info = new_trade.post() info = new_trade.post()
log.debug(f"Posted trade: {info}") log.debug(f"Posted trade: {info}")
@ -394,7 +452,7 @@ def process_callback(callback):
if callback.hook.user != strategy.user: if callback.hook.user != strategy.user:
log.error("Ownership differs between callback and strategy.") log.error("Ownership differs between callback and strategy.")
continue continue
execute_strategy(callback, strategy) execute_strategy(callback, strategy, func="entry")
# Scan for exit # Scan for exit
log.debug("Scanning for entry strategies...") log.debug("Scanning for entry strategies...")
@ -405,4 +463,4 @@ def process_callback(callback):
if callback.hook.user != strategy.user: if callback.hook.user != strategy.user:
log.error("Ownership differs between callback and strategy.") log.error("Ownership differs between callback and strategy.")
continue continue
execute_strategy(callback, strategy) execute_strategy(callback, strategy, func="exit")

View File

@ -270,7 +270,7 @@ class PositionDetailsNested(BaseModel):
dividendAdjustment: str dividendAdjustment: str
guaranteedExecutionFees: str guaranteedExecutionFees: str
unrealizedPL: str unrealizedPL: str
marginUsed: str marginUsed: str | None
class PositionDetails(BaseModel): class PositionDetails(BaseModel):
@ -459,3 +459,134 @@ PricingInfoSchema = {
], ],
), ),
} }
class LongPositionCloseout(BaseModel):
instrument: str
units: str
class Trade(BaseModel):
tradeID: str
clientTradeID: str
units: str
realizedPL: str
financing: str
baseFinancing: str
price: str
guaranteedExecutionFee: str
quoteGuaranteedExecutionFee: str
halfSpreadCost: str
class HomeConversionFactors(BaseModel):
gainQuoteHome: str
lossQuoteHome: str
gainBaseHome: str
lossBaseHome: str
class LongOrderFillTransaction(BaseModel):
id: str
accountID: str
userID: int
batchID: str
requestID: str
time: str
type: str
orderID: str
instrument: str
units: str
requestedUnits: str
price: str
pl: str
quotePL: str
financing: str
baseFinancing: str
commission: str
accountBalance: str
gainQuoteHomeConversionFactor: str
lossQuoteHomeConversionFactor: str
guaranteedExecutionFee: str
quoteGuaranteedExecutionFee: str
halfSpreadCost: str
fullVWAP: str
reason: str
tradesClosed: list[Trade]
fullPrice: Price
homeConversionFactors: HomeConversionFactors
longPositionCloseout: LongPositionCloseout
class OrderTransation(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
longPositionCloseout: LongPositionCloseout
longOrderFillTransaction: dict
class PositionClose(BaseModel):
longOrderCreateTransaction: OrderTransaction
longOrderFillTransaction: OrderTransaction
longOrderCancelTransaction: OrderTransaction
shortOrderCreateTransaction: OrderTransaction
shortOrderFillTransaction: OrderTransaction
shortOrderCancelTransaction: OrderTransaction
relatedTransactionIDs: list[str]
lastTransactionID: str
PositionCloseSchema = {
"longOrderCreateTransaction": "longOrderCreateTransaction",
"longOrderFillTransaction": "longOrderFillTransaction",
"longOrderCancelTransaction": "longOrderCancelTransaction",
"shortOrderCreateTransaction": "shortOrderCreateTransaction",
"shortOrderFillTransaction": "shortOrderFillTransaction",
"shortOrderCancelTransaction": "shortOrderCancelTransaction",
"relatedTransactionIDs": "relatedTransactionIDs",
"lastTransactionID": "lastTransactionID",
}
class ClientExtensions(BaseModel):
id: str
tag: str
class TradeDetailsTrade(BaseModel):
id: str
instrument: str
price: str
openTime: str
initialUnits: str
initialMarginRequired: str
state: str
currentUnits: str
realizedPL: str
closingTransactionIDs: list[str]
financing: str
dividendAdjustment: str
closeTime: str
averageClosePrice: str
clientExtensions: ClientExtensions
class TradeDetails(BaseModel):
trade: TradeDetailsTrade
lastTransactionID: str
TradeDetailsSchema = {
"trade": "trade",
"lastTransactionID": "lastTransactionID",
}

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.3 on 2022-12-01 19:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0041_alter_strategy_entry_signals_and_more'),
]
operations = [
migrations.AddField(
model_name='trade',
name='information',
field=models.JSONField(blank=True, null=True),
),
]

View File

@ -201,6 +201,7 @@ class Trade(models.Model):
trailing_stop_loss = models.FloatField(null=True, blank=True) trailing_stop_loss = models.FloatField(null=True, blank=True)
take_profit = models.FloatField(null=True, blank=True) take_profit = models.FloatField(null=True, blank=True)
status = models.CharField(max_length=255, null=True, blank=True) status = models.CharField(max_length=255, null=True, blank=True)
information = models.JSONField(null=True, blank=True)
direction = models.CharField(choices=DIRECTION_CHOICES, max_length=255) direction = models.CharField(choices=DIRECTION_CHOICES, max_length=255)
# To populate from the trade # To populate from the trade
@ -213,6 +214,10 @@ class Trade(models.Model):
self._original = self self._original = self
def post(self): def post(self):
if self.status in ["rejected", "close"]:
log.debug(f"Trade {self.id} rejected. Not posting.")
log.debug(f"Trade {self.id} information: {self.information}")
else:
return self.account.client.post_trade(self) return self.account.client.post_trade(self)
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):