|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | + |
| 4 | +# MIMIC Conditional Orders - OliverTwist |
| 5 | + |
| 6 | +# Jackrabbit Relay |
| 7 | +# 2021 Copyright © Robert APM Darin |
| 8 | +# All rights reserved unconditionally. |
| 9 | + |
| 10 | +# This is the framework used by OliverTwist to process conditional orders. |
| 11 | + |
| 12 | +# Pull the actual fill price from open trades |
| 13 | + |
| 14 | +# SellAction of Close sets Units to all and closes the entire position. Sell uses the Oanda ticket system. |
| 15 | + |
| 16 | +import sys |
| 17 | +sys.path.append('/home/GitHub/JackrabbitRelay/Base/Library') |
| 18 | +import os |
| 19 | +import json |
| 20 | +import time |
| 21 | + |
| 22 | +import JRRsupport |
| 23 | +import JackrabbitRelay as JRR |
| 24 | + |
| 25 | +# Timeout before Locker auto-deletes this order result |
| 26 | + |
| 27 | +OliverTwistTimeout=(15*60) |
| 28 | + |
| 29 | +# Write the result to Locker memory so parent knows we are finished. |
| 30 | + |
| 31 | +def FinishOrphan(Key,lID,mID,State): |
| 32 | + # Get the lock read and set up the memory key. Locker doesn't care and which class this order belongs to. OliverTwist will |
| 33 | + # match the ID to the right class list, orphan or conditional. |
| 34 | + |
| 35 | + OliverTwistLock=JRRsupport.Locker("OliverTwist",ID=lID) |
| 36 | + Memory=JRRsupport.Locker(Key,ID=mID) |
| 37 | + |
| 38 | + State=State.lower() |
| 39 | + |
| 40 | + if State!='delete': |
| 41 | + NewState='Waiting' |
| 42 | + else: |
| 43 | + NewState='Delete' |
| 44 | + |
| 45 | + OliverTwistLock.Lock() |
| 46 | + Memory.Put(OliverTwistTimeout*100,NewState) |
| 47 | + OliverTwistLock.Unlock() |
| 48 | + |
| 49 | + # We're done. This child has completed its task |
| 50 | + print(Key,NewState) |
| 51 | + sys.stdout.flush() |
| 52 | + sys.exit(0) |
| 53 | + |
| 54 | +# Get the order ID. If there isn't an ID, the order FAILED. |
| 55 | + |
| 56 | +def GetOrderID(res): |
| 57 | + try: |
| 58 | + if res.find('Order Confirmation ID')>-1: |
| 59 | + s=res.find('ID:')+4 |
| 60 | + for e in range(s,len(res)): |
| 61 | + if res[e]=='\n': |
| 62 | + break |
| 63 | + oid=res[s:e] |
| 64 | + |
| 65 | + return oid |
| 66 | + except: |
| 67 | + pass |
| 68 | + return None |
| 69 | + |
| 70 | +# Calculate Proce Exit |
| 71 | + |
| 72 | +def CalculatePriceExit(order,ts,dir,price): |
| 73 | + # Figure out TakeProfit or Stoploss |
| 74 | + if ts=='TakeProfit': |
| 75 | + if '%' in str(order[ts]): |
| 76 | + if dir=='long': |
| 77 | + val=price+((float(order[ts].replace('%','').strip())/100)*price) |
| 78 | + else: |
| 79 | + val=price-((float(order[ts].replace('%','').strip())/100)*price) |
| 80 | + # Pips |
| 81 | + elif 'p' in str(order[ts].lower()): |
| 82 | + if dir=='long': |
| 83 | + val=price+(float(order[ts].lower().replace('p','').strip())*0.0001) |
| 84 | + else: |
| 85 | + val=price-(float(order[ts].lower().replace('p','').strip())*0.0001) |
| 86 | + else: |
| 87 | + val=float(order[ts]) |
| 88 | + elif ts=='StopLoss': |
| 89 | + if '%' in str(order[ts]): |
| 90 | + if dir=='long': |
| 91 | + val=price-((float(order[ts].replace('%','').strip())/100)*price) |
| 92 | + else: |
| 93 | + val=price+((float(order[ts].replace('%','').strip())/100)*price) |
| 94 | + # Pips |
| 95 | + elif 'p' in str(order[ts].lower()): |
| 96 | + if dir=='long': |
| 97 | + val=price-(float(order[ts].lower().replace('p','').strip())*0.0001) |
| 98 | + else: |
| 99 | + val=price+(float(order[ts].lower().replace('p','').strip())*0.0001) |
| 100 | + else: |
| 101 | + val=float(order[ts]) |
| 102 | + |
| 103 | + return val |
| 104 | + |
| 105 | +### |
| 106 | +### Main driver |
| 107 | +### |
| 108 | + |
| 109 | +def main(): |
| 110 | + data=sys.stdin.read().strip() |
| 111 | + Orphan=json.loads(data) |
| 112 | + |
| 113 | +# JRRsupport.WriteFile(f"/tmp/{Orphan['ID']}.txt",json.dumps(Orphan)+'\n') |
| 114 | + |
| 115 | + # Use Relay to process and validate the order, must be a string |
| 116 | + if type(Orphan['Order']) is dict: |
| 117 | + order=json.dumps(Orphan['Order']) |
| 118 | + else: |
| 119 | + order=Orphan['Order'] |
| 120 | + |
| 121 | + relay=JRR.JackrabbitRelay(framework=Orphan['Framework'],payload=order,NoIdentityVerification=True) |
| 122 | + relay.JRLog.SetBaseName('OliverTwist') |
| 123 | + |
| 124 | +###---> |
| 125 | + relay.JRLog.Write(f"{Orphan['Key']}: {Orphan['lID']} {Orphan['mID']}",stdOut=False) |
| 126 | + relay.JRLog.Write(f"{data}",stdOut=False) |
| 127 | + |
| 128 | + try: |
| 129 | + # Check to see if order is still open and return current state |
| 130 | + # Handle OANDa's weird order id sequencing |
| 131 | + id=Orphan['ID'] |
| 132 | + saction=relay.Order['SellAction'].lower() |
| 133 | + if type(Orphan['Response']) is str: |
| 134 | + Orphan['Response']=json.loads(Orphan['Response']) |
| 135 | + oDetail=Orphan['Response'] |
| 136 | + |
| 137 | + # Manage average and close extire position. Average and price are the same. |
| 138 | + price=float(oDetail['Price']) |
| 139 | + amount=float(oDetail['Amount']) |
| 140 | + |
| 141 | + # Process the position |
| 142 | + |
| 143 | + # We need to check TakeProfit and StopLoss. If one of them is hit, we need to build and order and |
| 144 | + # backfeed it in to Relay. It will place a new order. |
| 145 | + |
| 146 | + # Get the direction of the trade, long/short |
| 147 | + dir=relay.Order['Direction'].lower() |
| 148 | + # Get Ticker |
| 149 | + ticker=relay.GetTicker(symbol=relay.Order['Asset']) |
| 150 | + |
| 151 | + # Check to see if we have enough balance, if not then delete this order. Deal with futures as well. |
| 152 | + |
| 153 | + base=relay.Markets[relay.Order['Asset']]['base'].upper() |
| 154 | + bal=relay.GetBalance(Base=base) |
| 155 | + |
| 156 | + # Fsilsafe, in the WORST way possible. Do NOT leave a take profit out of the order. At this stage, the |
| 157 | + # whole thing is an absolute nightmare to fix. The is a very brutal way of dealing with poor user |
| 158 | + # choices. |
| 159 | + |
| 160 | + if 'TakeProfit' not in relay.Order: |
| 161 | + relay.Order['TakeProfit']='2%' |
| 162 | + |
| 163 | + # Calculate Take Profit |
| 164 | + tp=round(CalculatePriceExit(relay.Order,'TakeProfit',dir,price),5) |
| 165 | + |
| 166 | + # Figure out StopLoss, if there is one |
| 167 | + if 'StopLoss' in relay.Order: |
| 168 | + sl=round(CalculatePriceExit(relay.Order,'StopLoss',dir,price),5) |
| 169 | + |
| 170 | + # Get the "strikePrice". This handles both TakeProfit and StopLoss. It doesn't matter which as both are processed |
| 171 | + # the same way. |
| 172 | + |
| 173 | + LogMSG=None |
| 174 | + StrikeHappened=False |
| 175 | + if dir=='long': |
| 176 | + if 'Diagnostics' in relay.Active: |
| 177 | + relay.JRLog.Write(f"{id}: {dir} Price: {price}, Bid: {ticker['Bid']} TP: {tp}/{relay.Order['TakeProfit']}, SL {sl}/{relay.Order['StopLoss']}",stdOut=False) |
| 178 | + |
| 179 | + if ticker['Bid']>tp: |
| 180 | + profit=round((amount*ticker['Bid'])-(amount*price),8) |
| 181 | + LogMSG=f"{id}: TP {dir} hit: {tp}, {amount}: {price:.5f} -> {ticker['Bid']:5f}/{profit}" |
| 182 | + if 'StopLoss' in relay.Order and ticker['Bid']<sl: |
| 183 | + loss=round((amount*price)-(amount*ticker['Bid']),8) |
| 184 | + LogMSG=f"{id}: SL {dir} hit: {sl}, {amount}: {price:.5f} -> {ticker['Bid']:5f}/{loss}" |
| 185 | + |
| 186 | + if ticker['Bid']>tp or ('StopLoss' in relay.Order and ticker['Bid']<sl): |
| 187 | + strikePrice=ticker['Bid'] |
| 188 | + StrikeHappened=True |
| 189 | + else: |
| 190 | + if 'Diagnostics' in relay.Active: |
| 191 | + relay.JRLog.Write(f"{id}: {dir} Price: {price}, Ask: {ticker['Ask']} TP: {tp}/{relay.Order['TakeProfit']}, SL {sl}/{relay.Order['StopLoss']}",stdOut=False) |
| 192 | + |
| 193 | + if ticker['Ask']<tp: |
| 194 | + profit=round((amount*price)-(amount*ticker['Ask']),8) |
| 195 | + LogMSG=f"{id}: TP {dir} hit: {tp}, {amount}: {price:.5f} -> {ticker['Ask']:5f}/{profit}" |
| 196 | + if 'StopLoss' in relay.Order and ticker['Ask']>sl: |
| 197 | + loss=round((amount*ticker['Ask'])-(amounts*price),8) |
| 198 | + LogMSG=f"{id}: SL {dir} hit: {sl}, {amount}: {price:.5f} -> {ticker['Ask']:5f}/{loss}" |
| 199 | + |
| 200 | + if ticker['Ask']<tp or ('StopLoss' in relay.Order and ticker['Ask']>sl): |
| 201 | + strikePrice=ticker['Ask'] |
| 202 | + StrikeHappened=True |
| 203 | + |
| 204 | + if StrikeHappened==True: |
| 205 | + if abs(amount)>bal: |
| 206 | + # Build "strike" order. TakeProfit or StopLoss has been triggered |
| 207 | + newOrder={} |
| 208 | + newOrder['OliverTwist']='Conditional' |
| 209 | + newOrder['Exchange']=relay.Order['Exchange'] |
| 210 | + newOrder['Account']=relay.Order['Account'] |
| 211 | + newOrder['Market']=relay.Order['Market'] |
| 212 | + newOrder['Asset']=relay.Order['Asset'] |
| 213 | + newOrder['Action']=relay.Order['SellAction'] |
| 214 | + newOrder['Price']=str(strikePrice) |
| 215 | + newOrder['Base']=str(amount) |
| 216 | + if 'OrderType' in relay.Order: |
| 217 | + newOrder['OrderType']=relay.Order['OrderType'] |
| 218 | + else: |
| 219 | + newOrder['OrderType']='market' |
| 220 | + |
| 221 | + # relay.JRLog.Write(f"{id}: {json.dumps(newOrder)}",stdOut=False) |
| 222 | + |
| 223 | + newOrder['Identity']=relay.Active['Identity'] |
| 224 | + |
| 225 | + # Feed the new order to Relay |
| 226 | + result=relay.SendWebhook(newOrder) |
| 227 | + oid=GetOrderID(result) |
| 228 | + if oid!=None: |
| 229 | + relay.JRLog.Write(LogMSG,stdOut=False) |
| 230 | + resp=relay.GetOrderDetails(id=oid,symbol=relay.Order['Asset']) |
| 231 | + # Order must be closed as it succedded |
| 232 | + newOrder['ID']=oid |
| 233 | + relay.WriteLedger(Order=newOrder,Response=resp) |
| 234 | + FinishOrphan(Orphan['Key'],Orphan['lID'],Orphan['mID'],'Delete') |
| 235 | + else: |
| 236 | + # Give OliverTwist a response |
| 237 | + relay.JRLog.Write(f"{id}: Order failed",stdOut=False) |
| 238 | + FinishOrphan(Orphan['Key'],Orphan['lID'],Orphan['mID'],Orphan['Status']) |
| 239 | + else: |
| 240 | + # Amount < Balance |
| 241 | + FinishOrphan(Orphan['Key'],Orphan['lID'],Orphan['mID'],Orphan['Status']) |
| 242 | + else: |
| 243 | + # Strike did not happen |
| 244 | + FinishOrphan(Orphan['Key'],Orphan['lID'],Orphan['mID'],Orphan['Status']) |
| 245 | + except Exception as e: |
| 246 | + # Something went wrong |
| 247 | + relay.JRLog.Write(f"{Orphan['Key']}: Code Error - {sys.exc_info()[-1].tb_lineno}/{str(e)}",stdOut=False) |
| 248 | + if 'Diagnostics' in relay.Active: |
| 249 | + relay.JRLog.Write(f"{Orphan['Key']}: {data}",stdOut=False) |
| 250 | + FinishOrphan(Orphan['Key'],Orphan['lID'],Orphan['mID'],Orphan['Status']) |
| 251 | + |
| 252 | +if __name__ == '__main__': |
| 253 | + main() |
0 commit comments