From 1a758df642753d7fd20737be48ba7191b543cf63 Mon Sep 17 00:00:00 2001 From: Harald Wolff Date: Thu, 23 Nov 2017 13:02:43 +0100 Subject: [PATCH] Initial Commit --- .gitignore | 40 ++++ BasicBotSetup.cs | 14 ++ Properties/AssemblyInfo.cs | 26 +++ TradeBotBalance.cs | 25 +++ TradeBotLoader.cs | 125 ++++++++++++ TradingBot.cs | 377 +++++++++++++++++++++++++++++++++++++ TradingBotEnvironment.cs | 115 +++++++++++ logging/FileLogger.cs | 26 +++ logging/Logger.cs | 30 +++ sharp.tradebot.csproj | 60 ++++++ 10 files changed, 838 insertions(+) create mode 100644 .gitignore create mode 100644 BasicBotSetup.cs create mode 100644 Properties/AssemblyInfo.cs create mode 100644 TradeBotBalance.cs create mode 100644 TradeBotLoader.cs create mode 100644 TradingBot.cs create mode 100644 TradingBotEnvironment.cs create mode 100644 logging/FileLogger.cs create mode 100644 logging/Logger.cs create mode 100644 sharp.tradebot.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4e82d27 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Autosave files +*~ + +# build +[Oo]bj/ +[Bb]in/ +packages/ +TestResults/ + +# globs +Makefile.in +*.DS_Store +*.sln.cache +*.suo +*.cache +*.pidb +*.userprefs +*.usertasks +config.log +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.user +*.tar.gz +tarballs/ +test-results/ +Thumbs.db + +# Mac bundle stuff +*.dmg +*.app + +# resharper +*_Resharper.* +*.Resharper + +# dotCover +*.dotCover diff --git a/BasicBotSetup.cs b/BasicBotSetup.cs new file mode 100644 index 0000000..d7033e8 --- /dev/null +++ b/BasicBotSetup.cs @@ -0,0 +1,14 @@ +using System; +namespace sharp.tradebot +{ + public class BasicBotSetup + { + public bool Enabled; + + public string BaseSymbol = ""; + public string MarketSymbol = ""; + + public double ChargeBaseBalance; + public double ChargeMarketBalance; + } +} diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..50fdbbb --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle("sharp.tradebot")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion("1.0.*")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] diff --git a/TradeBotBalance.cs b/TradeBotBalance.cs new file mode 100644 index 0000000..46e33cc --- /dev/null +++ b/TradeBotBalance.cs @@ -0,0 +1,25 @@ +using System; +namespace sharp.tradebot +{ + public class TradeBotBalance + { + public readonly string Currency; + public double CurrentBalance; + + public TradeBotBalance() + { + } + + public TradeBotBalance(string currency) + { + this.Currency = currency; + } + + + public override string ToString() + { + return string.Format("[TradeBotBalance: Currency={0,5}, CurrentBalance={1,12:#####0.00000000}]", Currency, CurrentBalance); + } + + } +} diff --git a/TradeBotLoader.cs b/TradeBotLoader.cs new file mode 100644 index 0000000..b730d45 --- /dev/null +++ b/TradeBotLoader.cs @@ -0,0 +1,125 @@ +using System; +using System.Reflection; +using sharp.trading.logging; +using System.IO; +using System.Collections.Generic; +namespace sharp.tradebot +{ + public class TradeBotLoader : MarshalByRefObject + { + public String DllPath { get; set; } + public String ClassName { get; set; } + public Guid UID { get; set; } + + private AppDomain _appDomain; + private TradingBot _instance; + + TradingBotEnvironment environment; + + Dictionary pWorkers; + + struct pworker { + public PeriodicWorker worker; + + public pworker(PeriodicWorker worker){ + this.worker = worker; + } + + public void call(){ + worker(); + } + } + + public TradeBotLoader() + { + pWorkers = new Dictionary(); + } + + public void RegisterPeriodic(PeriodicWorker worker, int timeout) + { + environment.RegisterPeriodic(worker,timeout); + } + public void UnregisterPeriodic(PeriodicWorker worker) + { + environment.UnregisterPeriodic(worker); + } + +/* + public void RegisterPeriodic(PeriodicWorker worker, int timeout) { + Guid guid = Guid.NewGuid(); + pworker pw = new pworker(worker); + pWorkers.Add(guid, pw ); + environment.RegisterPeriodic( pWorkers[guid].call, timeout ); + } + public void UnregisterPeriodic(PeriodicWorker worker){ + foreach (Guid guid in pWorkers.Keys){ + if (pWorkers[guid].worker == worker){ + environment.UnregisterPeriodic(pWorkers[guid].call); + pWorkers.Remove(guid); + break; + } + } + } +*/ + + + public void Load(TradingBotEnvironment environment) + { + this.environment = environment; + + AppDomainSetup ads = new AppDomainSetup(); + // ads.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory; + + + this._appDomain = AppDomain.CreateDomain(String.Format("{0}:{1}:{2}:{3}", DllPath, ClassName, UID, Guid.NewGuid().ToString()), AppDomain.CurrentDomain.Evidence, ads); +/* + Assembly assembly = this._appDomain.Load(File.ReadAllBytes(DllPath)); + Type type = assembly.GetType(ClassName); + this._instance = (TradingBot)Activator.CreateInstance(type); +*/ + this._instance = (TradingBot)this._appDomain.CreateInstanceFromAndUnwrap(DllPath, ClassName); + this._instance.AfterLoad(environment, UID, new FileLogger(Path.Combine(environment.BaseDataDirectory, "logs", String.Format("{0}.log", UID.ToString()))),this); + } + + public void Unload() + { + if (this._instance != null) + { + lock (this._instance) + { + if (this._instance.IsPrepared) + { + this._instance.Unprepare(); + } + this._instance.Logger.Close(); + } + this._instance = null; + AppDomain.Unload(this._appDomain); + this._appDomain = null; + } + } + + public void Prepare() + { + if (this._instance != null) + { + lock (this._instance) + { + this._instance.Prepare(); + } + } + } + + public void Unprepare() + { + if (this._instance != null) + { + lock (this._instance) + { + this._instance.Unprepare(); + } + } + } + + } +} diff --git a/TradingBot.cs b/TradingBot.cs new file mode 100644 index 0000000..797a98b --- /dev/null +++ b/TradingBot.cs @@ -0,0 +1,377 @@ +using System; +using System.Collections.Generic; +using sharp.trading; +using System.IO; +using sharp.json; +using System.Linq; +using sharp.extensions; +using System.Diagnostics; +using sharp.trading.services; +using sharp.trading.logging; +using sharp.json.attributes; + +namespace sharp.tradebot +{ + public abstract class TradingBot : TradingBot where T:BasicBotSetup + { + public T BasicSetup { + get { return this.setup.CurrentValue; } + set { this.setup.CurrentValue = value; } + } + + protected override BasicBotSetup BasicBotSetup + { + get { return BasicSetup; } + set { this.BasicSetup = (T)value; } + } + + protected FileBackedJSONValue setup; + + protected override void EnvironmentAssigned(TradingBotEnvironment env){ + this.setup = new FileBackedJSONValue(Path.Combine(BaseDataPath,"botsetup.json")); + if (this.setup.CurrentValue.IsNull()){ + this.setup.CurrentValue = Activator.CreateInstance(); + } + } + + public override void Save() + { + setup.Save(); + + base.Save(); + } + } + + [JSONClassPolicy( Policy = JSONPolicy.ATTRIBUTED)] + public abstract class TradingBot : MarshalByRefObject + { + public TradingBotEnvironment TradingBotEnvironment { get; private set; } + public TradeBotLoader TradeBotLoader { get; private set; } + + public Guid UID { get; private set; } + public Logger Logger { get; private set; } + + public string BaseDataPath { get; private set; } + + List orders = new List(); + List balances = new List(); + Market botMarket; + + TextWriter balanceWriter; + + protected abstract BasicBotSetup BasicBotSetup { get; set; } + protected abstract void EnvironmentAssigned(TradingBotEnvironment env); + + [JSONField] + TradeBotBalance[] __balances { + get { + return balances.ToArray(); + } + set + { + this.balances.Clear(); + if (value != null) + { + this.balances.AddRange(value); + } + } + } + + [JSONField] + Order[] __orders + { + get { + return orders.ToArray(); + } + set + { + this.orders.Clear(); + if (value != null) + { + this.orders.AddRange(value); + } + } + } + + public bool IsPrepared { get; private set; } + + public TradingBot() + { + } + + public Market BotMarket { + get + { + if (this.botMarket.IsNull() && !this.BasicBotSetup.BaseSymbol.Equals("") && !this.BasicBotSetup.BaseSymbol.Equals("")) + { + botMarket = TradingBotEnvironment.TradingConnection.openMarket(BasicBotSetup.MarketSymbol, BasicBotSetup.BaseSymbol); + } + return this.botMarket; + } + } + + public void AfterLoad(TradingBotEnvironment environment,Guid uid,Logger logger,TradeBotLoader loader){ + this.TradingBotEnvironment = environment; + this.UID = uid; + this.Logger = logger; + this.BaseDataPath = Path.Combine(environment.BaseDataDirectory, "bots", uid.ToString()); + + if (!Directory.Exists(DataDirectory)){ + Directory.CreateDirectory(DataDirectory); + } + + EnvironmentAssigned(environment); + + Log("TradingBot [{0}] loaded to AppDomain [{1}]",GetType().Name,AppDomain.CurrentDomain.FriendlyName); + } + + public virtual void Prepare() + { + + //if (File.Exists(Path.Combine(DataDirectory,"orders.json"))){ + // JSON jsonorders = JSON.ReadFrom(Path.Combine(DataDirectory,"orders.json")); + // string[] orderids = jsonorders.To(); + // foreach (string oid in orderids){ + // this.orders.Add(TradingEnvironment.DefaultConnection.getOrder(oid)); + // } + //} + + //if (File.Exists(Path.Combine(DataDirectory,"balances.json"))){ + // JSON jsonbalances = JSON.ReadFrom(Path.Combine(DataDirectory,"balances.json")); + // this.balances.Clear(); + // this.balances.AddRange( jsonbalances.To()); + //} + + if (File.Exists(Path.Combine(DataDirectory,"botsetup.json"))){ + JSON jbasicsetup = JSON.ReadFrom(Path.Combine(DataDirectory, "botsetup.json")); + this.BasicBotSetup = (BasicBotSetup)JSONConverter.To(BasicBotSetup.GetType(),jbasicsetup); + } + + JSON jstate = JSON.ReadFrom(Path.Combine(BaseDataPath, "state.json")); + if (jstate != null){ + JSONConverter.ApplyObject(jstate, this); + } + balanceWriter = new StreamWriter(new FileStream(Path.Combine(BaseDataPath, "balance.log"),FileMode.Append)); + + this.TradingBotEnvironment.RegisterPeriodic(Balancing,10); + + IsPrepared = true; + } + + public virtual void Unprepare() + { + IsPrepared = false; + this.TradingBotEnvironment.UnregisterPeriodic(Balancing); + balanceWriter.Close(); + + Save(); + } + + public virtual void Save(){ + JSON.From(this).WriteTo(Path.Combine(BaseDataPath, "state.json"),true); + } + + public Order[] Orders { + get { return this.orders.ToArray(); } + } + + public Order getOrder(string orderID){ + foreach (Order o in this.orders){ + if (o.OrderID.Equals(orderID)){ + return o; + } + } + + Order order = TradingEnvironment.DefaultConnection.getOrder(orderID); + return order; + } + + public void cancelOrder(Order order){ + TradingEnvironment.DefaultConnection.cancelOrder(order); + } + + public void cancelOrder(string orderID) + { + cancelOrder(getOrder(orderID)); + } + + public Order createLimitOrder(OrderType orderType,string marketCurrency,string baseCurrency,double quantity,double limit){ + + TradeBotBalance bBase = getBalance(baseCurrency); + TradeBotBalance bMarket = getBalance(marketCurrency); + + Market orderMarket = TradingEnvironment.DefaultConnection.openMarket(marketCurrency, baseCurrency); + + if (orderMarket.MinimumTradeVolume > quantity){ + Log("Refusing to create order below minimum trading volume for market! Volume: {0:#0.000000} {1} < {2:#0.000000} {1}", quantity, marketCurrency, orderMarket.MinimumTradeVolume); + return null; + } + + if (orderType == OrderType.BUY){ + if (bBase.CurrentBalance < (limit * quantity)){ + Log("Refusing order to buy {0} @{1} with balance of {2}", quantity, limit, bBase.CurrentBalance); + return null; + } + } else { + if (bMarket.CurrentBalance < (quantity)){ + Log("Refusing order to sell {0} @{1} with balance of {2}", quantity, limit, bMarket.CurrentBalance); + return null; + } + } + + Log("createLimitOrder({0},{1},{2},{3},{4})", orderType, marketCurrency, baseCurrency, quantity, limit); + + try + { + Order o = TradingEnvironment.DefaultConnection.createOrder(orderType, OrderTarget.LIMIT, marketCurrency, baseCurrency, quantity, limit); + this.orders.Add(o); + return o; + + } catch (Exception e){ + Log("createLimitOrder(): threw Exception: {0}", e); + } + return null; + } + + public void RemoveOrder(Order order){ + this.orders.Remove(order); + } + + public VolumeRate getVolumeOnOrders(){ + VolumeRate vrate = new VolumeRate(); + + foreach (Order order in this.orders){ + if (order.OrderType == OrderType.BUY){ + vrate.Volume += order.OrderVolume; + vrate.Price += order.LimitPrice * order.OrderVolume; + } else { + vrate.Volume -= order.OrderVolume; + vrate.Price -= order.LimitPrice * order.OrderVolume; + } + } + + vrate.Price /= vrate.Volume; + + return vrate; + } + + /* Balances */ + + public TradeBotBalance getBalance(string symbol){ + if (symbol.IsNull() || symbol.Equals("")) + { + return null; + } + + foreach (TradeBotBalance b in balances){ + if (b.Currency.Equals(symbol)){ + return b; + } + } + + TradeBotBalance balance = new TradeBotBalance(symbol); + balances.Add(balance); + + return balance; + } + + + public T loadJSON(string filename){ + if (File.Exists(Path.Combine(DataDirectory, filename))) + { + return JSON.ReadFrom(Path.Combine(DataDirectory, filename)).To(); + } + + if (typeof(T).IsArray){ + return (T)(object)Array.CreateInstance(typeof(T).GetElementType(), 0); + } + + return Activator.CreateInstance(); + } + + public void saveJSON(object o, string filename){ + JSONConverter.From(o).WriteTo(Path.Combine(DataDirectory, filename),true); + } + + + public string DataDirectory { + get { return BaseDataPath; } + } + + public void Log(string format,params object[] args){ + Logger.Log(format,args); + } + + public void DumpBalances(){ + foreach (TradeBotBalance balance in balances){ + Log(balance.ToString()); + } + } + + + public virtual int SchedulingIntervall() + { + return 60; + } + + public virtual void Balancing(){ + + if (BasicBotSetup.Enabled) + { + TradeBotBalance bMarket = getBalance(BasicBotSetup.MarketSymbol); + TradeBotBalance bBase = getBalance(BasicBotSetup.BaseSymbol); + bool charged = false; + + if (BasicBotSetup.ChargeBaseBalance != 0){ + bBase.CurrentBalance += BasicBotSetup.ChargeBaseBalance; + charged = true; + } + if (BasicBotSetup.ChargeMarketBalance != 0){ + bMarket.CurrentBalance += BasicBotSetup.ChargeMarketBalance; + charged = true; + } + + if (charged){ + Log("Charged balances: {0,11:####0.00000000} {1} / {2,11:####0.00000000} {3}", BasicBotSetup.ChargeBaseBalance, BasicBotSetup.BaseSymbol, BasicBotSetup.ChargeMarketBalance, BasicBotSetup.MarketSymbol); + balanceWriter.WriteLine("{0}\t{1}\t{2}\t{3,12:0.00000000}\t{4,12:0.00000000}\t{5,12:0.00000000}\t{6,12:0.00000000}\t{7,12:0.00000000}", DateTime.Now, "-", "CHARGE", BasicBotSetup.ChargeMarketBalance,BasicBotSetup.ChargeBaseBalance,0,bBase.CurrentBalance,bMarket.CurrentBalance); + balanceWriter.Flush(); + + BasicBotSetup.ChargeMarketBalance = 0; + BasicBotSetup.ChargeBaseBalance = 0; + } + + foreach (Order order in this.__orders) + { + TradingBotEnvironment.TradingConnection.refreshOrder(order); + if (!order.IsOpen) + { + bBase.CurrentBalance -= order.PayedFees; + if (order.OrderType == OrderType.BUY){ + bBase.CurrentBalance -= order.PayedPrice; + bMarket.CurrentBalance += order.FilledVolume; + } else { + bBase.CurrentBalance += order.PayedPrice; + bMarket.CurrentBalance -= order.FilledVolume; + } + this.RemoveOrder(order); + + Log("Order has been closed: {5} {0,11:####0.00000000} {1} / {2,11:####0.00000000} {3} [ FEE: {4,11:####0.00000000} {3} ]", + order.FilledVolume, + BasicBotSetup.MarketSymbol, + order.PayedPrice, + BasicBotSetup.BaseSymbol, + order.PayedFees, + order.OrderType + ); + + balanceWriter.WriteLine("{0}\t{1}\t{2}\t{3:#0.00000000}\t{4:#0.00000000}\t{5:#0.00000000}\t{6:#0.00000000}\t{7:#0.00000000}", DateTime.Now, order.OrderID, order.OrderType, order.FilledVolume,order.PayedPrice,order.PayedFees,bBase.CurrentBalance,bMarket.CurrentBalance); + balanceWriter.Flush(); + } + } + + Save(); + } + } + + } +} diff --git a/TradingBotEnvironment.cs b/TradingBotEnvironment.cs new file mode 100644 index 0000000..c7c91e1 --- /dev/null +++ b/TradingBotEnvironment.cs @@ -0,0 +1,115 @@ +using System; +using sharp.trading; +using System.IO; +using System.Timers; +using System.Threading; +using System.Collections.Generic; + +namespace sharp.tradebot +{ + public delegate void PeriodicWorker(); + + public class TradingBotEnvironment : MarshalByRefObject + { + public string BaseDataDirectory { get; private set; } + public TradingConnection TradingConnection { get; private set; } + + Thread periodicsThread; + bool contPeriodics = true; + + List periodicTasks = new List(); + + + public TradingBotEnvironment(TradingConnection connection,string baseDataDirectory){ + BaseDataDirectory = baseDataDirectory; + TradingConnection = connection; + periodicsThread = new Thread(periodics); + periodicsThread.Start(); + } + + public void RegisterPeriodic(PeriodicWorker worker,int timeout){ + lock (periodicTasks){ + periodicTasks.Add(new PeriodicTask(timeout,worker)); + } + } + + public void UnregisterPeriodic(PeriodicWorker worker) + { + lock (periodicTasks) + { + foreach (PeriodicTask ptask in periodicTasks.ToArray()){ + if (ptask.worker == worker){ + periodicTasks.Remove(ptask); + } + } + } + } + + public void Stop(){ + lock (this){ + this.contPeriodics = false; + } + this.periodicsThread.Join(); + } + + private void periodics(){ + + Thread.Sleep(1000); + + while (true){ + + lock (this){ + if (!contPeriodics) + { + break; + } + } + + lock (periodicTasks){ + foreach (PeriodicTask ptask in periodicTasks){ + ptask.Check(); + } + } + + Thread.Sleep(250); + } + } + + class PeriodicTask { + public DateTime lastRun; + public Int64 interval; + public PeriodicWorker worker; + + public PeriodicTask(Int64 interval,PeriodicWorker worker){ + this.worker = worker; + this.interval = interval; + this.lastRun = DateTime.Now; + } + + public void Check() + { + if ((this.lastRun == null) || ((DateTime.Now - this.lastRun).TotalSeconds >= this.interval)) + { + this.lastRun = DateTime.Now; + try + { + lock (this.worker.Target){ + if (!typeof(TradingBot).IsInstanceOfType(this.worker.Target) || ((TradingBot)this.worker.Target).IsPrepared){ + try { + this.worker(); + } catch (Exception e){ + Console.WriteLine("Exception: {0}",e); + } + } + } + } catch (Exception e){ + Console.WriteLine("Exception: {0}",e.ToString()); + } + } + } + + + } + + } +} diff --git a/logging/FileLogger.cs b/logging/FileLogger.cs new file mode 100644 index 0000000..125cc60 --- /dev/null +++ b/logging/FileLogger.cs @@ -0,0 +1,26 @@ +using System; +using System.IO; +namespace sharp.trading.logging +{ + public class FileLogger : Logger + { + TextWriter writer; + + public FileLogger(string filename) + { + this.writer = new StreamWriter(new FileStream(filename, FileMode.Append)); + } + + protected override void log(string line) + { + this.writer.WriteLine(line); + this.writer.Flush(); + } + + public override void Close() + { + this.writer.Close(); + base.Close(); + } + } +} diff --git a/logging/Logger.cs b/logging/Logger.cs new file mode 100644 index 0000000..edeb9fa --- /dev/null +++ b/logging/Logger.cs @@ -0,0 +1,30 @@ +using System; +using System.IO; +using System.Text; +using System.Diagnostics; + +namespace sharp.trading.logging +{ + public abstract class Logger : MarshalByRefObject + { + + protected abstract void log(string line); + + public void Log(string format, params object[] args) + { + string preformat = string.Format(format, args); + string postformat = string.Format("{0} {1} {2}", DateTime.Now.ToLocalTime(), Process.GetCurrentProcess().Id, preformat); + lock (this) + { + log(postformat); + } + } + + public void Log(Exception e){ + Log("An Exception occured: {0} in {1}", e.ToString(),e.StackTrace); + } + + public virtual void Close(){ + } + } +} diff --git a/sharp.tradebot.csproj b/sharp.tradebot.csproj new file mode 100644 index 0000000..9ac66d5 --- /dev/null +++ b/sharp.tradebot.csproj @@ -0,0 +1,60 @@ + + + + Debug + AnyCPU + {798D4516-84F8-436D-BD7F-17AD288C6776} + Library + sharp.tradebot + sharp.tradebot + v4.7 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + false + + + true + bin\Release + prompt + 4 + false + + + + + + + + + + + + + + + + + {CAAC53CC-671C-4B1E-8403-1E53D1D40D66} + sharp.trading + + + {D9342117-3249-4D8B-87C9-51A50676B158} + sharp.json + + + {97CA3CA9-98B3-4492-B072-D7A5995B68E9} + sharp.extensions + + + + + + + \ No newline at end of file