Menu

#7 non trading days in TradingDateTimeAxis

open
nobody
None
5
2007-04-30
2007-04-30
Anonymous
No

TradingDateTimeAxis is actually removing only saturdays and sundays. I'd like non trading days (apart form weekends) to be removed as I get trend lines errors.

Discussion

  • Nobody/Anonymous

    Logged In: NO

    Hi,

    I've just modified TradingTimeAxis in order to allow non trading days to be removed from the axis...

    best,
    Davide.

    /*
    * NPlot - A charting library for .NET
    *
    * TradingDateTimeAxis.cs
    * Copyright (C) 2003-2006 Matt Howlett and others.
    * All rights reserved.
    *
    * Redistribution and use in source and binary forms, with or without modification,
    * are permitted provided that the following conditions are met:
    *
    * 1. Redistributions of source code must retain the above copyright notice, this
    * list of conditions and the following disclaimer.
    * 2. Redistributions in binary form must reproduce the above copyright notice,
    * this list of conditions and the following disclaimer in the documentation
    * and/or other materials provided with the distribution.
    * 3. Neither the name of NPlot nor the names of its contributors may
    * be used to endorse or promote products derived from this software without
    * specific prior written permission.
    *
    * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
    * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
    * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
    * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
    * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
    * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
    * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
    * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
    * OF THE POSSIBILITY OF SUCH DAMAGE.
    */

    using System;
    using System.Drawing;
    using System.Collections;

    namespace NPlot
    {

    /// <summary>
    /// Provides a DateTime axis that removes non-trading days.
    /// </summary>
    public class TradingDateTimeAxis : DateTimeAxis
    {
    /// <summary>
    /// a list of non trading days
    /// </summary>
    private ArrayList exceptionalNonTradingDays;
    /// <summary>
    /// add a day to the list of non trading days
    /// </summary>
    /// <param name="dayToAdd">non trading day to add</param>
    public void AddNonTradingDay(DateTime dayToAdd)
    {
    if (!exceptionalNonTradingDays.Contains(dayToAdd))
    {
    exceptionalNonTradingDays.Add(dayToAdd);
    }
    }

    // we keep shadow "virtual" copies of WorldMin/Max for speed
    // which are already remapped, so it is essential that changes
    // to WorldMin/Max are captured here

    /// <summary>
    /// The axis world min value.
    /// </summary>
    public override double WorldMin
    {
    get
    {
    return base.WorldMin;
    }
    set
    {
    base.WorldMin = value;
    virtualWorldMin_ = SparseWorldRemap(value);
    }
    }
    private double virtualWorldMin_ = double.NaN;

    /// <summary>
    /// The axis world max value.
    /// </summary>
    public override double WorldMax
    {
    get
    {
    return base.WorldMax;
    }
    set
    {
    base.WorldMax = value;
    virtualWorldMax_ = SparseWorldRemap(value);
    }
    }
    private double virtualWorldMax_ = double.NaN;

    /// <summary>
    /// Optional time at which trading begins.
    /// All data points earlied than that (same day) will be collapsed.
    /// </summary>
    public virtual TimeSpan StartTradingTime
    {
    get
    {
    return new TimeSpan(startTradingTime_);
    }
    set
    {
    startTradingTime_ = value.Ticks;
    tradingTimeSpan_ = endTradingTime_ - startTradingTime_;
    }
    }
    private long startTradingTime_;

    /// <summary>
    /// Optional time at which trading ends.
    /// All data points later than that (same day) will be collapsed.
    /// </summary>
    public virtual TimeSpan EndTradingTime
    {
    get
    {
    return new TimeSpan(endTradingTime_);
    }
    set
    {
    endTradingTime_ = value.Ticks;
    tradingTimeSpan_ = endTradingTime_ - startTradingTime_;
    }
    }
    private long endTradingTime_;

    private long tradingTimeSpan_;

    /// <summary>
    /// Get whether or not this axis is linear.
    /// </summary>
    public override bool IsLinear
    {
    get
    {
    return false;
    }
    }

    /// <summary>
    /// Constructor
    /// </summary>
    public TradingDateTimeAxis() : base()
    {
    exceptionalNonTradingDays = new ArrayList();
    Init();
    }

    /// <summary>
    /// Constructor
    /// </summary>
    public TradingDateTimeAxis(ArrayList nonTradingDays) : base()
    {
    exceptionalNonTradingDays = nonTradingDays;
    Init();
    }

    /// <summary>
    /// Copy Constructor
    /// </summary>
    /// <param name="a">construct a TradingDateTimeAxis based on this provided axis.</param>
    public TradingDateTimeAxis(Axis a) : base(a)
    {
    exceptionalNonTradingDays = new ArrayList();
    Init();
    if (a is TradingDateTimeAxis)
    DoClone((TradingDateTimeAxis)a, this);
    else if (a is DateTimeAxis)
    DoClone((DateTimeAxis)a, this);
    else
    {
    DoClone(a, this);
    this.NumberFormat = null;
    }
    }

    /// <summary>
    /// Helper function for constructors.
    /// </summary>
    private void Init()
    {
    startTradingTime_ = 0;
    endTradingTime_ = TimeSpan.TicksPerDay;
    tradingTimeSpan_ = endTradingTime_ - startTradingTime_;
    virtualWorldMin_ = SparseWorldRemap(WorldMin);
    virtualWorldMax_ = SparseWorldRemap(WorldMax);
    }

    /// <summary>
    /// Deep copy of DateTimeAxis.
    /// </summary>
    /// <returns>A copy of the DateTimeAxis Class.</returns>
    public override object Clone()
    {
    TradingDateTimeAxis a = new TradingDateTimeAxis();
    // ensure that this isn't being called on a derived type. If it is, then oh no!
    if (this.GetType() != a.GetType())
    {
    throw new NPlotException( "Clone not defined in derived type. Help!" );
    }
    DoClone( this, a );
    return a;
    }

    /// <summary>
    /// Helper method for Clone.
    /// </summary>
    /// <param name="a">The cloned target object.</param>
    /// <param name="b">The cloned source object.</param>
    protected static void DoClone(TradingDateTimeAxis b, TradingDateTimeAxis a)
    {
    DateTimeAxis.DoClone(b, a);
    a.startTradingTime_ = b.startTradingTime_;
    a.endTradingTime_ = b.endTradingTime_;
    a.tradingTimeSpan_ = b.tradingTimeSpan_;
    a.WorldMin = b.WorldMin;
    a.WorldMax = b.WorldMax;
    a.exceptionalNonTradingDays = b.exceptionalNonTradingDays;
    }

    /// <summary>
    /// World to physical coordinate transform.
    /// </summary>
    /// <param name="coord">The coordinate value to transform.</param>
    /// <param name="physicalMin">The physical position corresponding to the world minimum of the axis.</param>
    /// <param name="physicalMax">The physical position corresponding to the world maximum of the axis.</param>
    /// <param name="clip">if false, then physical value may extend outside worldMin / worldMax. If true, the physical value returned will be clipped to physicalMin or physicalMax if it lies outside this range.</param>
    /// <returns>The transformed coordinates.</returns>
    /// <remarks>Not sure how much time is spent in this often called function. If it's lots, then
    /// worth optimizing (there is scope to do so).</remarks>
    public override PointF WorldToPhysical(
    double coord,
    PointF physicalMin,
    PointF physicalMax,
    bool clip)
    {

    // (1) account for reversed axis. Could be tricky and move
    // this out, but would be a little messy.

    PointF _physicalMin;
    PointF _physicalMax;

    if (this.Reversed)
    {
    _physicalMin = physicalMax;
    _physicalMax = physicalMin;
    }
    else
    {
    _physicalMin = physicalMin;
    _physicalMax = physicalMax;
    }

    // (2) if want clipped value, return extrema if outside range.

    if (clip)
    {
    if (WorldMin < WorldMax)
    {
    if (coord > WorldMax)
    {
    return _physicalMax;
    }
    if (coord < WorldMin)
    {
    return _physicalMin;
    }
    }
    else
    {
    if (coord < WorldMax)
    {
    return _physicalMax;
    }
    if (coord > WorldMin)
    {
    return _physicalMin;
    }
    }
    }

    // (3) we are inside range or don't want to clip.

    coord = SparseWorldRemap(coord);
    double range = virtualWorldMax_ - virtualWorldMin_;
    double prop = (double)((coord - virtualWorldMin_) / range);
    //double range = WorldMax - WorldMin;
    //double prop = (double)((coord - WorldMin) / range);
    //if (range1 != range)
    // range1 = range;

    // Force clipping at bounding box largeClip times that of real bounding box
    // anyway. This is effectively at infinity.
    const double largeClip = 100.0;
    if (prop > largeClip && clip)
    prop = largeClip;

    if (prop < -largeClip && clip)
    prop = -largeClip;

    if (range == 0)
    {
    if (coord >= virtualWorldMin_)
    prop = largeClip;

    if (coord < virtualWorldMin_)
    prop = -largeClip;
    }

    // calculate the physical coordinate.
    PointF offset = new PointF(
    (float)(prop * (_physicalMax.X - _physicalMin.X)),
    (float)(prop * (_physicalMax.Y - _physicalMin.Y)));

    return new PointF(_physicalMin.X + offset.X, _physicalMin.Y + offset.Y);
    }

    /// <summary>
    /// Transforms a physical coordinate to an axis world
    /// coordinate given the physical extremites of the axis.
    /// </summary>
    /// <param name="p">the point to convert</param>
    /// <param name="physicalMin">the physical minimum extremity of the axis</param>
    /// <param name="physicalMax">the physical maximum extremity of the axis</param>
    /// <param name="clip">whether or not to clip the world value to lie in the range of the axis if it is outside.</param>
    /// <returns></returns>
    public override double PhysicalToWorld(
    PointF p,
    PointF physicalMin,
    PointF physicalMax,
    bool clip)
    {
    // (1) account for reversed axis. Could be tricky and move
    // this out, but would be a little messy.

    PointF _physicalMin;
    PointF _physicalMax;

    if (this.Reversed)
    {
    _physicalMin = physicalMax;
    _physicalMax = physicalMin;
    }
    else
    {
    _physicalMin = physicalMin;
    _physicalMax = physicalMax;
    }

    // normalised axis dir vector
    float axis_X = _physicalMax.X - _physicalMin.X;
    float axis_Y = _physicalMax.Y - _physicalMin.Y;
    float len = (float)Math.Sqrt(axis_X * axis_X + axis_Y * axis_Y);
    axis_X /= len;
    axis_Y /= len;

    // point relative to axis physical minimum.
    PointF posRel = new PointF(p.X - _physicalMin.X, p.Y - _physicalMin.Y);

    // dist of point projection on axis, normalised.
    float prop = (axis_X * posRel.X + axis_Y * posRel.Y) / len;

    //double world = prop * (WorldMax - WorldMin) + WorldMin;
    double world = prop * (virtualWorldMax_ - virtualWorldMin_) + virtualWorldMin_;
    world = ReverseSparseWorldRemap(world);

    // if want clipped value, return extrema if outside range.
    if (clip)
    {
    world = Math.Max(world, WorldMin);
    world = Math.Min(world, WorldMax);
    }

    return world;
    }

    /// <summary>
    /// Remap a world coordinate into a "virtual" world, where non-trading dates and times are collapsed.
    /// </summary>
    /// <remarks>
    /// This code works under asumption that there are exactly 24*60*60 seconds in a day
    /// This is strictly speaking not correct but apparently .NET 2.0 does not count leap seconds.
    /// Luckilly, Ticks == 0 =~= 0001-01-01T00:00 =~= Monday
    /// First tried a version fully on floating point arithmetic,
    /// but failed hopelessly due to rounding errors.
    /// </remarks>
    /// <param name="coord">world coordinate to transform.</param>
    /// <returns>equivalent virtual world coordinate.</returns>
    protected double SparseWorldRemap(double coord)
    {
    long ticks = (long)coord;
    /*
    * removing non trading days..
    * davide morelli
    * */
    int numberOfDaysToRemove = 0;
    foreach (DateTime day in exceptionalNonTradingDays)
    {
    if (day.Ticks < ticks)
    numberOfDaysToRemove++;
    }
    long whole_days = ticks / TimeSpan.TicksPerDay;
    long ticks_in_last_day = ticks % TimeSpan.TicksPerDay;
    long full_weeks = whole_days / 7;
    long days_in_last_week = whole_days % 7;
    if (days_in_last_week >= 5)
    {
    days_in_last_week = 5;
    ticks_in_last_day = 0;
    }
    if (ticks_in_last_day < startTradingTime_) ticks_in_last_day = startTradingTime_;
    else if (ticks_in_last_day > endTradingTime_) ticks_in_last_day = endTradingTime_;
    ticks_in_last_day -= startTradingTime_;
    // removing non trading days...
    long whole_working_days = (full_weeks * 5 + days_in_last_week) - numberOfDaysToRemove;
    long working_ticks = whole_working_days * tradingTimeSpan_;
    long new_ticks = working_ticks + ticks_in_last_day;
    return (double)new_ticks;
    }

    /// <summary>
    /// Remaps a "virtual" world coordinates back to true world coordinates.
    /// </summary>
    /// <param name="coord">virtual world coordinate to transform.</param>
    /// <returns>equivalent world coordinate.</returns>
    protected double ReverseSparseWorldRemap(double coord)
    {
    long ticks = (long)coord;
    //ticks += startTradingTime_;
    /*
    * removing non trading days..
    * davide morelli
    * */
    int numberOfDaysToRemove = 0;
    foreach (DateTime day in exceptionalNonTradingDays)
    {
    if (SparseWorldRemap(day.Ticks) < ticks)
    numberOfDaysToRemove++;
    }
    ticks += numberOfDaysToRemove * tradingTimeSpan_;

    long ticks_in_last_day = ticks % tradingTimeSpan_;
    ticks /= tradingTimeSpan_;
    long full_weeks = ticks / 5;
    long week_part = ticks % 5;

    long day_ticks = (full_weeks * 7 + week_part) * TimeSpan.TicksPerDay;
    return (double)(day_ticks + ticks_in_last_day + startTradingTime_);
    }

    /// <summary>
    /// Adds a delta amount to the given world coordinate in such a way that
    /// all "sparse gaps" are skipped. In other words, the returned value is
    /// in delta distance from the given in the "virtual" world.
    /// </summary>
    /// <param name="coord">world coordinate to shift.</param>
    /// <param name="delta">shif amount in "virtual" units.</param>
    /// <returns></returns>
    public double SparseWorldAdd(double coord, double delta)
    {
    return ReverseSparseWorldRemap(SparseWorldRemap(coord) + delta);
    }

    /// <summary>
    /// World extent in virtual (sparse) units.
    /// </summary>
    public double SparseWorldLength
    {
    get
    {
    return SparseWorldRemap(WorldMax) - SparseWorldRemap(WorldMin);
    }
    }

    /// <summary>
    /// Check whether the given coordinate falls within defined trading hours.
    /// </summary>
    /// <param name="coord">world coordinate in ticks to check.</param>
    /// <returns>true if in trading hours, false if in non-trading gap.</returns>
    public bool WithinTradingHours(double coord)
    {
    long ticks = (long)coord;
    long whole_days = ticks / TimeSpan.TicksPerDay;
    long ticks_in_last_day = ticks % TimeSpan.TicksPerDay;
    long days_in_last_week = whole_days % 7;
    if (days_in_last_week >= 5)
    return false;

    if (ticks_in_last_day < startTradingTime_) return false;
    if (ticks_in_last_day >= endTradingTime_) return false;

    return true;
    }

    /// <summary>
    /// Check whether the given coordinate falls on trading days.
    /// </summary>
    /// <param name="coord">world coordinate in ticks to check.</param>
    /// <returns>true if on Mon - Fri.</returns>
    public bool OnTradingDays(double coord)
    {
    long ticks = (long)coord;
    long whole_days = ticks / TimeSpan.TicksPerDay;
    long days_in_last_week = whole_days % 7;
    return (days_in_last_week < 5);
    }

    /// <summary>
    /// Determines the positions of all Large and Small ticks.
    /// </summary>
    /// <remarks>
    /// The method WorldTickPositions_FirstPass() from the base works just fine, except that it
    /// does not account for non-trading gaps in time, therefore, when less than two days are visible
    /// an own algorithm is used (to show intraday time). Otherwise the base class implementation is used
    /// but the output is corrected to remove ticks on non-trading days (Sat, Sun).
    /// </remarks>
    /// <param name="physicalMin">The physical position corresponding to the world minimum of the axis.</param>
    /// <param name="physicalMax">The physical position corresponding to the world maximum of the axis.</param>
    /// <param name="largeTickPositions">ArrayList containing the positions of the large ticks.</param>
    /// <param name="smallTickPositions">null</param>
    internal override void WorldTickPositions_FirstPass(
    Point physicalMin,
    Point physicalMax,
    out ArrayList largeTickPositions,
    out ArrayList smallTickPositions
    )
    {
    if (LargeTickStep != TimeSpan.Zero || SparseWorldLength > 2.0 * (double)tradingTimeSpan_) // utilise base class
    {
    ArrayList largeTickPositions_FirstPass;
    base.WorldTickPositions_FirstPass(physicalMin, physicalMax, out largeTickPositions_FirstPass, out smallTickPositions);

    if (largeTickPositions_FirstPass.Count < 2)
    {
    // leave it alone, whatever that single tick may be (better something than nothing...)
    largeTickPositions = largeTickPositions_FirstPass;
    }
    else if ((double)largeTickPositions_FirstPass[1] - (double)largeTickPositions_FirstPass[0] > 27.0 * (double)TimeSpan.TicksPerDay)
    {
    // For distances between ticks in months or longer, just accept all ticks
    largeTickPositions = largeTickPositions_FirstPass;
    }
    else
    {
    // for daily ticks, ignore non-trading hours but obey (skip) non-trading days
    largeTickPositions = new ArrayList();
    foreach (object tick in largeTickPositions_FirstPass)
    {
    if (OnTradingDays((double)tick))
    largeTickPositions.Add(tick);
    }
    }
    }
    else // intraday ticks, own algorithm
    {
    smallTickPositions = null;
    largeTickPositions = new ArrayList();

    TimeSpan timeLength = new TimeSpan((long)SparseWorldLength);
    DateTime worldMinDate = new DateTime( (long)this.WorldMin );
    DateTime worldMaxDate = new DateTime( (long)this.WorldMax );

    DateTime currentTickDate;
    long skip; // in time ticks

    // The following if-else flow establishes currentTickDate to the beginning of series
    // and skip to the optimal distance between ticks

    // if less than 10 minutes, then large ticks on second spacings.

    if ( timeLength < new TimeSpan(0,0,10,0,0) )
    {
    this.LargeTickLabelType_ = LargeTickLabelType.hourMinuteSeconds;

    int secondsSkip;

    if (timeLength < new TimeSpan( 0,0,0,10,0 ) )
    secondsSkip = 1;
    else if ( timeLength < new TimeSpan(0,0,0,20,0) )
    secondsSkip = 2;
    else if ( timeLength < new TimeSpan(0,0,0,50,0) )
    secondsSkip = 5;
    else if ( timeLength < new TimeSpan(0,0,2,30,0) )
    secondsSkip = 15;
    else
    secondsSkip = 30;

    int second = worldMinDate.Second;
    second -= second % secondsSkip;

    currentTickDate = new DateTime(
    worldMinDate.Year,
    worldMinDate.Month,
    worldMinDate.Day,
    worldMinDate.Hour,
    worldMinDate.Minute,
    second,0 );

    skip = secondsSkip * TimeSpan.TicksPerSecond;
    }

    // Less than 2 hours, then large ticks on minute spacings.

    else if ( timeLength < new TimeSpan(0,2,0,0,0) )
    {
    this.LargeTickLabelType_ = LargeTickLabelType.hourMinute;

    int minuteSkip;

    if ( timeLength < new TimeSpan(0,0,10,0,0) )
    minuteSkip = 1;
    else if ( timeLength < new TimeSpan(0,0,20,0,0) )
    minuteSkip = 2;
    else if ( timeLength < new TimeSpan(0,0,50,0,0) )
    minuteSkip = 5;
    else if ( timeLength < new TimeSpan(0,2,30,0,0) )
    minuteSkip = 15;
    else //( timeLength < new TimeSpan( 0,5,0,0,0) )
    minuteSkip = 30;

    int minute = worldMinDate.Minute;
    minute -= minute % minuteSkip;

    currentTickDate = new DateTime(
    worldMinDate.Year,
    worldMinDate.Month,
    worldMinDate.Day,
    worldMinDate.Hour,
    minute,0,0 );

    skip = minuteSkip * TimeSpan.TicksPerMinute;
    }

    // Else large ticks on hour spacings.

    else
    {
    this.LargeTickLabelType_ = LargeTickLabelType.hourMinute;

    int hourSkip;
    if (timeLength < new TimeSpan(0, 10, 0, 0, 0))
    hourSkip = 1;
    else if (timeLength < new TimeSpan(0, 20, 0, 0, 0))
    hourSkip = 2;
    else
    hourSkip = 6;

    int hour = worldMinDate.Hour;
    hour -= hour % hourSkip;

    currentTickDate = new DateTime(
    worldMinDate.Year,
    worldMinDate.Month,
    worldMinDate.Day,
    hour, 0, 0, 0);

    skip = hourSkip * TimeSpan.TicksPerHour;
    }

    // place ticks

    while (currentTickDate < worldMaxDate)
    {
    double world = (double)currentTickDate.Ticks;

    if (!WithinTradingHours(world))
    {
    // add gap boundary instead
    world = ReverseSparseWorldRemap(SparseWorldRemap(world)); // moves forward
    long gap = (long)world;
    gap -= gap % skip;
    currentTickDate = new DateTime(gap);
    }

    if (world >= this.WorldMin && world <= this.WorldMax)
    {
    largeTickPositions.Add(world);
    }

    currentTickDate = currentTickDate.AddTicks(skip);
    }
    }
    }

    /// <summary>
    /// Get an appropriate label name, given the DateTime of a label
    /// </summary>
    /// <param name="tickDate">the DateTime to get the label name for</param>
    /// <returns>A label name appropriate to the supplied DateTime.</returns>
    protected override string LargeTickLabel(DateTime tickDate)
    {
    string label;

    if ( this.NumberFormat == null
    && (LargeTickLabelType_ == LargeTickLabelType.hourMinute ||
    LargeTickLabelType_ == LargeTickLabelType.hourMinuteSeconds)
    && tickDate.TimeOfDay == StartTradingTime)
    {
    // in such case always show the day date
    label = (tickDate.Day).ToString();
    label += " ";
    label += tickDate.ToString("MMM");
    }
    else
    {
    label = base.LargeTickLabel(tickDate);
    }
    return label;
    }
    }
    }

     
  • Nobody/Anonymous

    Logged In: NO

    I found another bug in the last correction posted here: the ticks labels were being created even for no tradding days.
    This little problem was corrected, just change the method "OnTradingDays" code of the class "TradingDateTimeAxis", for this:

    public bool OnTradingDays(double coord)
    {
    long ticks = (long)coord;
    long whole_days = ticks / TimeSpan.TicksPerDay;
    long days_in_last_week = whole_days % 7;
    bool retorno=true;
    if (!(days_in_last_week < 5))
    {
    retorno = false;
    }
    else
    {
    DateTime aux = new DateTime(ticks);
    foreach (DateTime dt in exceptionalNonTradingDays)
    {
    if (aux == dt)
    {
    retorno = false;
    break;
    }
    }
    }
    return retorno;
    }

    Att.
    André Novais
    afnovais@gmail.com

     

Log in to post a comment.

Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.