/// <summary>Update the price data</summary> public static string UpdatePriceData() { return(Str.Build( "insert or replace into PriceData ( ", "rowid,", "[", nameof(PriceData.AskPrice), "],", "[", nameof(PriceData.BidPrice), "],", "[", nameof(PriceData.AvrSpread), "],", "[", nameof(PriceData.LotSize), "],", "[", nameof(PriceData.PipSize), "],", "[", nameof(PriceData.PipValue), "],", "[", nameof(PriceData.VolumeMin), "],", "[", nameof(PriceData.VolumeStep), "],", "[", nameof(PriceData.VolumeMax), "])", " values (", "1,", // rowid "?,", // AskPrice "?,", // BidPrice "?,", // AvrSpread "?,", // LotSize "?,", // PipSize "?,", // PipValue "?,", // VolumeMin "?,", // VolumeStep "?)" // VolumeMax )); }
/// <summary>Create a table of support and resistance levels</summary> public static string SnRLevelsTable() { return(Str.Build( "create table if not exists SnRLevels (\n", "[", nameof(SnRLevel.Id), "] text unique,\n", "[", nameof(SnRLevel.Price), "] real,\n", "[", nameof(SnRLevel.WidthPips), "] real,\n", "[", nameof(SnRLevel.MaxTimeFrame), "] int)" )); }
/// <summary>Create a table of candles for a time frame</summary> public static string CandleTable(ETimeFrame time_frame) { return(Str.Build( "create table if not exists ", time_frame, " (\n", "[", nameof(Candle.Timestamp), "] integer unique,\n", "[", nameof(Candle.Open), "] real,\n", "[", nameof(Candle.High), "] real,\n", "[", nameof(Candle.Low), "] real,\n", "[", nameof(Candle.Close), "] real,\n", "[", nameof(Candle.Median), "] real,\n", "[", nameof(Candle.Volume), "] real)" )); }
/// <summary>Return all the candles in the given time range</summary> public IEnumerable <InMsg.CandleData> EnumCandleData(DateTimeOffset t0, DateTimeOffset t1) { var ts = "[" + nameof(Candle.Timestamp) + "]"; var sql = Str.Build("select * from {0} where ", ts, " >= ? and ", ts, " <= ? order by ", ts); var args = new object[] { t0.Ticks, t1.Ticks }; foreach (var tf in TimeFrames) { foreach (var candle in m_db.EnumRows <Candle>(sql.Fmt(tf), 1, args)) { yield return(new InMsg.CandleData(SymbolCode, tf, candle)); } } }
/// <summary>Reset the log file</summary> private void ResetLog() { using (var log = new StreamWriter(new FileStream(LogFilepath, FileMode.Create, FileAccess.Write, FileShare.Read))) { log.WriteLine(Str.Build( "Succeeded, ", "TradeType, ", "Profit, ", "RtR, ", string.Join(", ", Features.Select(x => x.Label)), string.Empty)); } m_reset_log = false; }
/// <summary>Insert a support and resistance level into the DB</summary> public static string UpdateSnRLevel() { return(Str.Build( "insert or replace into SnRLevels (", "[", nameof(SnRLevel.Id), "],", "[", nameof(SnRLevel.Price), "],", "[", nameof(SnRLevel.WidthPips), "],", "[", nameof(SnRLevel.MaxTimeFrame), "]", ") values (", "?,", // Id "?,", // Price "?,", // WidthPips "?)" // MaxTimeFrame )); }
/// <summary>Create a price data table</summary> public static string PriceDataTable() { // Note: this doesn't store the price data history, only the last received price data return(Str.Build( "create table if not exists PriceData (\n", "[", nameof(PriceData.AskPrice), "] real,\n", "[", nameof(PriceData.BidPrice), "] real,\n", "[", nameof(PriceData.AvrSpread), "] real,\n", "[", nameof(PriceData.LotSize), "] real,\n", "[", nameof(PriceData.PipSize), "] real,\n", "[", nameof(PriceData.PipValue), "] real,\n", "[", nameof(PriceData.VolumeMin), "] real,\n", "[", nameof(PriceData.VolumeStep), "] real,\n", "[", nameof(PriceData.VolumeMax), "] real)" )); }
/// <summary>Add the results of a virtual trade to the log</summary> private void LogPrediction(Prediction pred) { if (m_reset_log) { ResetLog(); } var trade = pred.Trade; var features = pred.Features; using (var log = new StreamWriter(new FileStream(LogFilepath, FileMode.Append, FileAccess.Write, FileShare.Read))) { log.WriteLine(Str.Build( trade.Result == Trade.EResult.HitTP ? 1 : 0, ", ", trade.TradeType, ", ", (trade.Result == Trade.EResult.HitTP ? trade.PeakProfit : -trade.PeakLoss) * trade.Volume, ", ", trade.RtR, ", ", string.Join(", ", features.Select(x => x.Value)), string.Empty)); } }
/// <summary>Insert or replace a candle in table 'time_frame'</summary> public static string InsertCandle(ETimeFrame time_frame) { return(Str.Build( "insert or replace into ", time_frame, " (", "[", nameof(Candle.Timestamp), "],", "[", nameof(Candle.Open), "],", "[", nameof(Candle.High), "],", "[", nameof(Candle.Low), "],", "[", nameof(Candle.Close), "],", "[", nameof(Candle.Median), "],", "[", nameof(Candle.Volume), "])", " values (", "?,", // Timestamp "?,", // Open "?,", // High "?,", // Low "?,", // Close "?,", // Median "?)" // Volume )); }
/// <summary>Return the index of the candle at or immediately before 'time_stamp'. Note: the returned indices will be in the range (-Count, 0]</summary> public int IndexAt(TFTime time_stamp) { var ticks = time_stamp.ExactTicks; // If the time stamp is within the cached range, binary search the cache for the index position if (CachedTimeRange.Contains(ticks)) { var idx = m_cache.BinarySearch(x => x.Timestamp.CompareTo(ticks)); if (idx < 0) { idx = ~idx; } return((m_index_range.Begi + idx) + FirstIdx); } // Otherwise use database queries to determine the index else { var sql = Str.Build("select count(*)-1 from ", TimeFrame, " where [", nameof(Candle.Timestamp), "] <= ? order by [", nameof(Candle.Timestamp), "]"); var idx = m_db.ExecuteScalar(sql, 1, new object[] { ticks }); return(idx + FirstIdx); } }
/// <summary>The raw data. Idx = 0 is the oldest, Idx = Count is the latest</summary> public Candle this[PosIdx pos_idx] { get { Debug.Assert(pos_idx >= 0 && pos_idx < Count); // Shift the cached range if needed if (!m_index_range.Contains(pos_idx)) { m_cache.Clear(); // Otherwise, reload the cache centred on the requested index var new_range = new Range((long)pos_idx - CacheSize / 2, (long)pos_idx + CacheSize / 2); if (new_range.Beg < 0) { new_range = new_range.Shift(0 - new_range.Beg); } if (new_range.End > Count) { new_range = new_range.Shift(Count - new_range.End); } if (new_range.Beg < 0) { new_range.Beg = 0; } // Populate the cache from the database // Order by timestamp so that the oldest is first, and the newest is at the end. var sql = Str.Build("select * from ", TimeFrame, " order by [", nameof(Candle.Timestamp), "] limit ?,?"); m_cache.AddRange(m_db.EnumRows <Candle>(sql, 1, new object[] { new_range.Begi, new_range.Sizei })); m_index_range = new_range; } return(m_cache[(int)pos_idx - m_index_range.Begi]); } }
/// <summary>Look for predictions with each new data element</summary> protected override void UpdateFeatureValues(DataEventArgs args) { // Require at least 3 candles if (Instrument.Count < 3) { return; } Features.Clear(); // Find the approximate candle size var msc = Instrument.MedianCandleSize(-100, 0); // Get the last few candles // Make decisions based on 'B', the last closed candle. var A = Instrument[0]; var a_type = A.Type(msc); var B = Instrument[-1]; var b_type = A.Type(msc); var C = Instrument[-2]; var c_type = A.Type(msc); // The age of 'A' (normalised) var a_age = Instrument.AgeOf(A, clamped: true); // Measure the strength of the trend leading up to 'B' (but not including) var preceding_trend = Instrument.MeasureTrend(-5, -1); // Opposing trend var opposing_trend = !c_type.IsIndecision() && !b_type.IsIndecision() && // Bodies are a reasonable size (C.Sign >= 0) != (B.Sign >= 0); // Opposite sign // Engulfing: A trend, ending with a reasonable sized body, // followed by a bigger body in the opposite direction. if ((opposing_trend) && // Opposing trend directions (c_type.IsTrend() && b_type.IsTrend()) && // Both trend-ish candles (B.BodyLength > 1.20 * C.BodyLength) && // B is bigger than 120% C (Math.Abs(B.Open - C.Close) < 0.05 * B.TotalLength) && // B.Open is fairly close to C.Close (Math.Abs(preceding_trend) > 0.8) && // There was a trend leading into B (Math.Sign(preceding_trend) != B.Sign)) // The trend was the opposite of B { Features.Add(new Feature("CandlePattern", B.Sign, Str.Build( "Engulfing: A trend, ending with a reasonable sized body, followed by a bigger body in the opposite direction.\n", " C:{0} B:{1} A:{2}\n".Fmt(c_type, b_type, a_type), " Preceding trend: {0}\n".Fmt(preceding_trend), ""))); return; } // Trend, indecision, trend: if ((b_type.IsIndecision()) && // A hammer, spinning top, or doji (Math.Abs(preceding_trend) > 0.8)) // A trend leading into the indecision { // This could be a continuation or a reversal. Need to look at 'A' to decide if (!a_type.IsIndecision()) // If A is not an indecision candle as well { // Use the indecision candle total length to decide established trend. // Measure relative to B.BodyCentre. // The stronger the indication, the least old 'A' has to be var dist = Math.Abs(A.Close - B.BodyCentre); var frac = Maths.Frac(B.TotalLength, dist, B.TotalLength * 2.0); if (a_age >= 1.0 - Maths.Frac(0.0, dist, 1.0)) { var reversal = Math.Sign(preceding_trend) != A.Sign; Features.Add(new Feature("CandlePattern", A.Sign, Str.Build( "Hammer, Spinning top, or doji: and a trend leading into the indecision\n", " C:{0} B:{1} A:{2}\n".Fmt(c_type, b_type, a_type), " Preceding trend: {0}\n".Fmt(preceding_trend), ""))); return; } } } //// Tweezers //if ((C.Type == Candle.EType.MarubozuWeakening && B.Type == Candle.EType.MarubozuStrengthening) && // (Math.Abs(preceding_trend) > 0.8)) //{ // Forecast = B.Sign > 0 ? TradeType.Buy : TradeType.Sell; // Comments = Str.Build( // "Tweezers pattern", // " C:{0} B:{1} A:{2}\n".Fmt(c_type, b_type, a_type), // " Preceding trend: {0}\n".Fmt(preceding_trend), // ""); // return; //} //// Continuing trend //if ((Math.Abs(preceding_trend) > 0.8) && // Preceding trend // (B.IsTrend && B.Sign == Math.Sign(preceding_trend))) //{ // Forecast = B.Sign > 0 ? TradeType.Buy: TradeType.Sell; // Comments = "Continuing trend"; // return; //} //// Consolidation //if ((Math.Abs(preceding_trend) < 0.5) && // !B.IsIndecision && // !B.IsTrend) //{ // Forecast = null; // Comments = "Consolidation, no trend"; // return; //} // no idea Features.Add(new Feature("CandlePattern", 0.0)); }
/// <summary> /// Request candle data for a time range. /// 'diff_cache' means only request time ranges that appear missing in the local instrument cache db</summary> public void RequestTimeRange(DateTimeOffset t0, DateTimeOffset t1, bool diff_cache) { // The request message var msg = new OutMsg.RequestInstrumentHistory(SymbolCode, TimeFrame); // If the DB is currently empty, request the whole range if (!diff_cache || Count == 0) { msg.TimeRanges.Add(t0.Ticks); msg.TimeRanges.Add(t1.Ticks); } // Otherwise, request time ranges that are missing from the DB else { var time_ranges = new List <Range>(); // One time frame unit var one = Misc.TimeFrameToTicks(1.0, TimeFrame); var ts = "[" + nameof(Candle.Timestamp) + "]"; // If the oldest candle in the DB is newer than 't0', request from t0 to Oldest if (Oldest.TimestampUTC > t0) { Debug.Assert(!Equals(Oldest, Candle.Default)); time_ranges.Add(new Range(t0.Ticks, Oldest.Timestamp)); } // Scan the DB for time ranges of missing data. var sql = Str.Build( // Returns pairs of timestamps that are the ranges of missing data. $"select {ts} from {TimeFrame} where {ts} >= ? and {ts} <= ? and {ts}+? not in (select {ts} from {TimeFrame}) union all ", $"select {ts} from {TimeFrame} where {ts} >= ? and {ts} <= ? and {ts}-? not in (select {ts} from {TimeFrame}) ", $"order by {ts}"); var args = new object[] { t0.Ticks, t1.Ticks, one, t0.Ticks, t1.Ticks, one, }; // Note: ignore the first and last because they are the implicit ranges -inf to first, and last -> +inf foreach (var hole in m_db.EnumRows <long>(sql, 1, args).Skip(1).TakeAllBut(1).InPairs()) { time_ranges.Add(new Range(hole.Item1, hole.Item2)); } // If the newest candle in the DB is older than 't1', request from Latest to t1 if (Latest.TimestampUTC < t1) { Debug.Assert(!Equals(Latest, Candle.Default)); time_ranges.Add(new Range(Latest.Timestamp, t1.Ticks)); } // Exclude the known forex non trading hours foreach (var time_range in time_ranges) { foreach (var hole in Model.Sessions.ClipToTradingHours(time_range)) { Debug.Assert(hole.Size != 0); msg.TimeRanges.Add(hole.Beg); msg.TimeRanges.Add(hole.End); } } } // Post the request Model.Post(msg); }
/// <summary>Remove a support and resistance level from the DB</summary> public static string RemoveSnRLevel() { return(Str.Build("delete from SnRLevels where [", nameof(SnRLevel.Id), "] = ?")); }