Click here to Skip to main content
15,885,216 members
Articles / Desktop Programming / WPF

WPF Automated Trading Application

Rate me:
Please Sign up or sign in to vote.
4.95/5 (30 votes)
14 Dec 2014CPOL15 min read 63.7K   5.5K   94   21
WPF automated trading application

Image 1

Introduction

Here, I present the design of a standalone desktop application that contains independently running services and a responsive user interface aimed at running a trading strategy. The specific use of the Mediator pattern is the keystone in this construction and could be recycled for building other types of concurrent systems.

Features

  • Receive and process live quotes
  • Develop buy and sell signals according to a trading strategy
  • Initiate buy and sell orders with trading venue
  • Listen for and receive confirmation of trade execution
  • Monitor trading activity with a responsive user interface
  • Archive quotes, trades and logs for post trade analysis

Background

This project was started with the idea of running a trend-following trading strategy on the Bitcoin/Euro exchange rate. It was geared to use the trading venue Kraken.com which at the time offered the most advanced types of orders. Since no profits were ever recorded from running this program, I added a mock Exchange client so that people could run it without having to open an account at Kraken. The original Kraken API client is also provided and if you already have an account, you can add the security keys in the application's configuration file and try some real trading.

The trading game plan applied is a trend following strategy developed by George Kleinman and baptized the Natural Numbers Method (N#M). It uses the Weighted Moving Average (WMA) of the price to identify trends and relies on the empirical observation that markets behave differently at prices that end with zero; what Kleinman calls Natural Numbers. When the price crosses the moving average (Setup), we expect that a new trend is forming. A Setup is confirmed if the market then opens above (below) the highest (lowest) price of the Setup bar. Upon confirmation, we place an order at the next Natural Number. This process works in both directions.

Design

The base for the project is a WPF application that leverages the Mediator pattern to build loosely coupled and independently running components that are not aware of each other, but that can communicate in real time. They communicate by sending messages to the Mediator which proceeds to call all the methods that registered for these messages. In this regard, much was taken from the MVVM Disciples (http://wpfdisciples.wordpress.com/).

Image 2

The main screen of the application is itself fragmented into independent panels that communicate with the other services through the Mediator. We will refer to this ensemble of panels as a single UI Component.

The other components of the application each serve a specific role in the trading system. The QuoteService fetches trade data from the exchange, packages it at regular time intervals and sends a message to the mediator notifying that data has arrived. The StrategyService, which has registered to that specific message, processes the data and decides when to open or close positions and when to place or shift stoploss orders. When such events occur, the StrategyService sends the appropriate message to the Mediator where the BrokerageService registered its interest. The BrokerageService places orders with the exchange, listens until they are canceled or closed, and informs the Mediator whenever the status of an order is altered. The StrategyService registers to messages sent from the BrokerageService to keep track of the number of contracts that are effectively trading. Throughout this process, the UI Component listens to messages originating from all the trading services and updates tables, graphs and a log console for the user to follow the trading activity.

Mediator and ObservableObject

The components which are linked to the Mediator are wrapped in an ObservableObject class which carries a static instance of the Mediator. Methods to be registered for a given message are decorated with an attribute which specifies which message to subscribe to and the type of parameter the method expects. When a component needs to notify other components of an event, it calls the method NotifyColleagues of the Mediator with the message in question and the data object attached with it. Upon reception of the message, the Mediator proceeds to call all the methods registered for that message.

For example:

The StrategyService registers a method for the NewPriceData message that runs the new data through the core algorithm and eventually alters the trading position.

C#
[MediatorMessageSink(MediatorMessages.NewPriceData, ParameterType = typeof(PriceData))]
public void NewPriceDataReceived(PriceData priceData)
{
   TreatNewPriceData(priceData);
}

The UIComponent also registers a method for the NewPriceData message in order to update the graph when new price points arrive.

C#
[MediatorMessageSink(MediatorMessages.NewPriceData, ParameterType = typeof(PriceData))]
public void UpdateGraphData(PriceData chartData)
{
    App.Current.Dispatcher.Invoke(DispatcherPriority.Normal, new Action(
              () =>
              {
                  candleStickCollection.Add(chartData.CandleStick);
                  wmaCollection.Add(chartData.WmaPoint);
              }));

 }

The message originates from the QuoteService when it has processed a new batch of trade data.

C#
Mediator.NotifyColleagues<PriceData>(MediatorMessages.NewPriceData, 
    new PriceData(candleStick, WMAPoints.Last(), true));

Composable Parts and MEF

The parts of the system that communicate with the trading venue and with the backend data store have been abstracted behind corresponding interfaces. This allows for greater testability and to eventually extend the system to use other third party services. The dependency injection is done with Managed Extensibility Framework (MEF).

Exchange Clients

The IExchangeClient interface represents a point of entry to the trading venue. It defines a set of functions that need to be called by other components in the system but hides the actual details. So far, there are two client implementations: KrakenClient which is the real client for the Kraken API and KrakenClientMock which simulates the trading activity.

The use of Managed Extensibility Framework (MEF) allows handling different clients like plugins. Any class that implements IExchangeClient and which is decorated with the appropriate attribute will be identified and featured in the list of possible clients.

Image 3

The mapping and matching work is done in the MEFLoader class. The ControlViewModel uses MEFLoader to populate the list of exchanges. When an exchange is selected, an instance of a corresponding exchange-client is created and passed to the parts of the application which communicate with the trading venue (QuoteService and BrokerageService).

Data Repositories

In order to allow for post-trading analysis, the application was initially set up to store objects in a database. It has been extended to add the option of mock repositories that do not actually do any database operations. The use of either method can be configured in the configuration file by specifying mock or database in RepositoryConfigurationType setting:

XML
<appSettings>
    <add key="KrakenBaseAddress" value="https://api.kraken.com" />
    <add key="KrakenApiVersion" value="0" />
    <add key="KrakenKey" value="xxx" />
    <add key="KrakenSecret" value="xxx"/>
    <add key="KrakenClientMaxRetries" value="5" />
    <add key="PositionEpsilon" value="0.0000025"/>
    <add key="RepositoryConfigurationType" value="mock"/>
  </appSettings>

Services which need to access the data repositories get them indirectly through the MEFLoader which looks at the configuration file to know which implementation to choose from.

QuoteService

The QuoteService is a real-time component that fetches trade data from the venue, packages it into candlesticks (open, high, low, and close), calculates the WMA and relays this data to the Mediator. The venue in question is an instance of IExchangeClient. It can be the real KrakenClient or the KrakenClientMock which emulates the behavior of the online exchange.

The Natural Numbers Method relies on the Weighted Moving Average (WMA) which is calculated using past data. For the N period WMA, it is necessary to know the last N points. The CatchUpWithConnector method of the QuoteService instructs the exchange client to fetch trade data that goes back far enough to calculate the WMA. This allows starting the strategy right away instead of having to wait N periods. However, Kraken does not allow querying trade data that goes back too far. Upon starting the application, one would have to wait N periods before being able to calculate the first WMA point and initiate trading. If N is high and if the period is long, this is a problem. I have written a bot, separate from this trading application, which archives trades from Kraken along with a WebService that allows querying this archive. The KrakenClient fetches data from this WebService while KrakenClientMock fetches it from a local cache of simulated trades.

The QuoteService also registers a method for the StartQuoteBot message which is triggered from the ControlViewModel after CatchUpWithConnector has completed. The registered method launches the core functionality of the QuoteService (Run()) in a separate thread. The Run method instantiates a timer and the callback function to execute when the timer elapses. When the timer elapses, the callback function will be executed in a separate thread. The callback function loads recent prices from the trading venue, calculates the weighted moving average, notifies other components that new data has arrived and resets the timer.

Finally, the Stop method, which registers for the StopQuoteBot message, stops the timer and sends the StopStrategyService message to the Mediator.

StrategyService

The StrategyService contains the core algorithm behind the trading strategy. It listens to incoming data from the QuoteService, runs this data through the algorithm and eventually signals the BrokerageService to take action. By also listening to messages from the BrokerageService, it keeps track of the trading position with the exchange. When signaled to stop, it will order the BrokerageService to close any open position.

Avoiding Message Acknowledgment Loops

The trading method only allows for one position at a time, be it long or short. Until this limit is reached, the trading algorithm signals orders in response to market conditions. Therefore, it is important to keep track of the actual position with the exchange. However, trade orders are placed asynchronously by the BrokerageService which is completely independent from the StrategyService. The variable OngoingContracts keeps track of the position and can only be modified by the function which registers for the Mediator Message UpdateOngoingContracts. This message is sent by the BrokerageService whenever an order goes through with the exchange.

C#
[MediatorMessageSink(MediatorMessages.UpdateOngoingContracts, ParameterType = typeof(decimal))]
public void UpdateOngoingContracts(decimal ongoingContractsIncrement)
{
    lock (OngoingContractsLock)
    {
        OngoingContracts += ongoingContractsIncrement;

     //Sometimes an order is considered to be executed 
     //even when a very small fraction of it is not filled. 
     //We allow for a small error Epsilon (~ 0.001 EUR at the time of writing)

       if (Math.Abs(OngoingContracts) < PositionEpsilon)
       {
           OngoingContracts = 0;
       }
     }
}

Time elapses between the moment the StrategyService sends an order to the moment it receives an order execution acknowledgment in the form of the UpdateOngoingContracts message. If the position counter were adjusted only upon receiving execution acknowledgments, the system would keep spewing out orders during the time the order is being executed, potentially resulting in too many orders being placed. One solution would be to keep two counters: one for sent in orders and one for executed orders. The solution used in the system at hand is to enforce the limit directly in the BrokerageService by guaranteeing that only one opening order be placed at a given time. Placing an opening order while another one is already placed requires canceling the first one before sending out the new one.

BrokerageService

The BrokerageService monitors the position of the trading system and places orders through the IExchangeClient. It is able to follow the status of an order until it is executed or canceled at which points it notifies the Mediator. It responds to the three instructions OpenPosition, ClosePosition and ShiftStopLoss.

Position

In our system, a position is the combination of an opening order, a closing order and an emergency order. Two opening orders cannot coexist and a closing order cannot be placed until the corresponding opening order is closed. This solves the common Message Acknowledgment Loop pitfall. The emergency order is a market order that mirrors the opening order and that can be placed to exit a position at any time.

The private Position class, defined inside the BrokerageService class, exposes three properties for the OpeningOrder, the ClosingOrder and the EmergencyOrder. In the set methods of these properties, we call the UpdateOngoingContracts method.

C#
Order openingOrder;
public Order OpeningOrder
{
   get
   {
       return openingOrder;
   }
   set
   {
       openingOrder = value;                  
       if(value!=null)
       {    
         _brokerageService.Mediator.NotifyColleagues<Order>
         (MediatorMessages.UpdateOrder, openingOrder);
       }
       UpdateOngoingContracts(openingOrder);
    }

 }

UpdateOngoingContracts sends a message to the Mediator signifying that the number of ongoing contracts has changed.

C#
private void UpdateOngoingContracts(Order order)
{
    if (order != null && order.VolumeExecuted.HasValue)
    {
        int sign = 0;
        switch (order.Type)
        {
           case "buy":
               //positive
               sign = 1;
               break;
           case "sell":
               //negative
               sign = -1;
               break;
        }

        decimal ongoingContractsIncrement = sign * order.VolumeExecuted.Value;


       _brokerageService.Mediator.NotifyColleagues<decimal>
(MediatorMessages.UpdateOngoingContracts, ongoingContractsIncrement);

    }
 }

OpenPosition

The BrokerageService registers the method OpenPositionReceived for the Mediator message OpenPosition. This method spins a new thread and tries to cancel the current opening order before opening a new one.

C#
[MediatorMessageSink(MediatorMessages.OpenPosition, ParameterType = typeof(OpenPositionData))]
public void OpenPositionReceived(OpenPositionData openPositionData)
{
    Task task = new Task(() =>
    {
        Log(LogEntryImportance.Info, "OpenPosition message received", true);
        if (openPositionData != null)
        {
           //Cancel opening order
           bool cancelOpeningOrderRes = CancelOpeningOrder();
           if (!cancelOpeningOrderRes)
           {
              Log(LogEntryImportance.Error, "Unable to cancel current opening order. 
              Cannot open new Position", true);
           }
           else
           {
              Log(LogEntryImportance.Info, "Opening Position...", true);
              OpenPosition(openPositionData);
           }
        }
        else
        {
           Log(LogEntryImportance.Info, "OpenPositionData is null. 
           Cannot open new Position...", true);
        }
    });
    task.Start();
}

OpenPosition() uses the OrderFactory to create an order with the data sent by the StrategyService. It then sets that order as the OpeningOrder of the position. Thus if another message arrives while the order is being placed, the BrokerageService will know that there is already an OpeningOrder in the chamber. It then proceeds to actually place the order by calling the client’s PlaceOrder method. Regardless of the implementation (real or mock), PlaceOrder will return when the order is either closed, canceled or when an exception occurred. As soon as the method PlaceOrder returns, the OpeningOrder of the Position is updated. If the opening order was successfully placed, the method then proceeds to place the closing or stoploss order. Again, this will keep spinning asynchronously until the order reaches a stable status. Before and after placing an opening or a closing order, the corresponding field in the BrokerageService’s Position is always updated.

C#
private void OpenPosition(OpenPositionData openPositionData)
{
    try
    {
         BrokerPosition = new Position(this);
         BrokerPosition.Direction = openPositionData.Direction;

         //Create opening order
         Order openingOrder = _orderFactory.CreateOpeningOrder
         (openPositionData.Direction, KrakenOrderType.stop_loss, 
         openPositionData.EnteringPrice, openPositionData.Volume, 
         openPositionData.CandleStickId, openPositionData.ConfirmationId, 
         validateOnly: openPositionData.ValidateOnly);
         UpdateOpeningOrder(openingOrder);
         //Place opening order and wait until closed or canceled
         Log(LogEntryImportance.Info, "Placing opening order...", true);
         PlaceOrderResult openingOrderResult = _client.PlaceOrder(openingOrder, true);
         openingOrder = openingOrderResult.Order;
         UpdateOpeningOrder(openingOrder);
         bool ok = false;

         ... Handle opening-order result ...

         if (!ok) return;

         //if nothing went wrong, place exiting order
         Order closingOrder = _orderFactory.CreateStopLossOrder
         (openingOrder, openPositionData.ExitingPrice, openPositionData.ValidateOnly);
         UpdateClosingOrder(closingOrder);
         //Place closing order and wait until closed or canceled
         Log(LogEntryImportance.Info, "Placing closing order...", true);
         PlaceOrderResult closingOrderResult = _client.PlaceOrder(closingOrder, true);
         closingOrder = closingOrderResult.Order;
         UpdateClosingOrder(closingOrder);

         ... Handle closing-order result...
                
                      
    }
   catch (Exception ex)
   {
         Log(LogEntryImportance.Error, string.Format("An exception occurred in OpenPosition at line {0}. {1} {2}", ex.LineNumber(), ex.Message, ((ex.InnerException != null) ? ex.InnerException.Message : "")), true);
    }
}

ClosePosition

The BrokerageService registers the method ClosePositionReceived for the Mediator message ClosePosition. This method runs asynchronously and eventually cancels the opening or closing order and places the emergency order.

C#
[MediatorMessageSink(MediatorMessages.ClosePosition, ParameterType = typeof(string))]
public void ClosePositionReceived(string message)
{
    Task task = new Task(() =>
    {
       Log(LogEntryImportance.Info, "Closing Position...", true);

       //Cancel opening order
       bool cancelOpeningOrderRes = CancelOpeningOrder();

       //cancel closing order
       bool cancelClosingOrder = CancelClosingOrder();

       //execute emergency exit order
       if (BrokerPosition != null && BrokerPosition.EmergencyExitOrder != null)
       {
          Order emergencyExitOrder = BrokerPosition.EmergencyExitOrder;

          PlaceOrderResult emergencyExitOrderResult = _client.PlaceOrder(emergencyExitOrder, true);

          emergencyExitOrder = emergencyExitOrderResult.Order;

          //update PositionView and PanelView
          UpdateEmergencyOrder(emergencyExitOrder);

          ... Handle emergency order result ...

       }      
       else
       {
          Log(LogEntryImportance.Info, "No emergency order to execute.", true);
       }

       Log(LogEntryImportance.Info, "Position closed.", true);
                
       BrokerPosition = null;
     });
     task.Start();
} 

ShiftPositionLimits

The ShiftPositionLimits method cancels the current closing order and places a new one either higher or lower. It is called asynchronously by the method that registers for the Mediator message ShiftPositionLimits.

C#
private void ShiftPositionLimits(ShiftPositionLimitsData shiftPositionLimitsData)
{
    Log(LogEntryImportance.Info, "In ShiftPositionLimits", true);
    try
    {
        //Cancel current stoploss order
        bool cancelClosingOrderRes = CancelClosingOrder();

        if (cancelClosingOrderRes)
        {
           //create new stop loss order
           Order newStopLossOrder = _orderFactory.CreateStopLossOrder
           (BrokerPosition.OpeningOrder, shiftPositionLimitsData.NewLimitPrice, 
           shiftPositionLimitsData.ValidateOnly);
           UpdateClosingOrder(newStopLossOrder);
           //place order and wait
           Log(LogEntryImportance.Info, "Placing new closing order...", true);
           PlaceOrderResult placeOrderResult = _client.PlaceOrder(newStopLossOrder, true);
           newStopLossOrder = placeOrderResult.Order;
           UpdateClosingOrder(newStopLossOrder);

           ... Handle place-order result ...

         }
         else
         {
            Log(LogEntryImportance.Error, 
            "Unable to cancel current closing order. Cannot shift limits", true);
         }
    }
    catch (Exception ex)
    {
        Log(LogEntryImportance.Error, string.Format
        ("An exception occurred in ShiftPositionLimits at line {0}. {1} {2}", 
        ex.LineNumber(), ex.Message, 
        ((ex.InnerException != null) ? ex.InnerException.Message : "")), true);
    }
}

UI Component

The main window is composed of independent Views that each has its own ViewModel. With a given set of Views, composing the MainWindow is only a matter of fitting them into a grid in XAML. With this design, it is possible to work on one part of the interface without modifying the overall frame. It also allows for greater testability and integration with Blend.

The ViewModels, which contain the underlying functionality of a View, are wrapped in the ObservableObject class linking them to the Mediator. User actions are transmitted to the trading services by sending specific messages for which the services register methods. Reciprocally, messages coming from the trading services may be translated into visual updates of a View.

MVVM

As preconized by the MVVM pattern, the underlying functionality of a View is handled in the ViewModel with which it communicates using the WPF data binding mechanism. The correspondence between a View and its ViewModel is declared in the app.xaml file:

XML
<img src="file:///C:\Users\ARRIV_~1\AppData\Local\Temp\msohtmlclip1\01\clip_image001.emz" />
<Application x:Class="TradingApp.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:vm="clr-namespace:TradingApp.ViewModel"
             xmlns:v="clr-namespace:TradingApp.View">
    <Application.Resources>
        <DataTemplate DataType="{x:Type vm:ControlViewViewModel}">
            <v:ControlView />
        </DataTemplate>
        <DataTemplate DataType="{x:Type vm:GraphViewViewModel}">
            <v:GraphView />
        </DataTemplate>
        <DataTemplate DataType="{x:Type vm:LogViewViewModel}">
            <v:LogView />
        </DataTemplate>
        <DataTemplate DataType="{x:Type vm:PanelViewViewModel}">
            <v:PanelView />
        </DataTemplate>
        <DataTemplate DataType="{x:Type vm:PositionViewViewModel}">
            <v:PositionView />
        </DataTemplate>
    </Application.Resources>
</Application>

Given a set of Views, composing the MainWindow is just a matter of fitting them into a grid in XAML. The following extract shows how instances of the ControlViewModel and PositionViewModel are passed to ContentPresenters and fitted directly within a Grid to form the left column of the MainWindow.

XML
<Grid>
   <Grid.RowDefinitions>
      <RowDefinition Height="55*"/>
      <RowDefinition Height="45*"/>
   </Grid.RowDefinitions>
   <ContentPresenter Content="{Binding ControlVM}" Margin="3,0,3,0" /> 
   <GridSplitter  .../>
   <ContentPresenter Content="{Binding PositionVM}" Margin="3,0,3,0" Grid.Row="1" />
</Grid>

ViewModels as ObservableObjects

ViewModels inherit from ViewModelBase which inherits from ObservableObject, allowing them to register methods with the Mediator and indirectly communicate with the other components of the application.

Dynamic properties are linked to the ViewModel by WPF bindings.

XML
<TextBox Grid.Row="3" Grid.Column="1"  Height="23" TextWrapping="Wrap" 
    Text="{Binding WmaPeriod}" IsReadOnly="{Binding Busy}"  Width="100"/>

In the setter function of the underlying ViewModel property, a message is sent to the Mediator notifying that the value has changed along with the new value.

C#
public int WmaPeriod
{
   get
   {
      return wmaPeriod;
   }
   set
   {
      OldPeriod = wmaPeriod;
      wmaPeriod = value;

      Mediator.NotifyColleagues<int>(MediatorMessages.WmaPeriodChanged, wmaPeriod);

      base.RaisePropertyChanged(() => this.WmaPeriod);
   }
}

Services that are interested in this change of value may register a method for the corresponding message.

C#
[MediatorMessageSink(MediatorMessages.WmaPeriodChanged, ParameterType = typeof(int))]
public void SetWmaPeriod(int wmaPeriod)
{
    WmaPeriod = wmaPeriod;
} 

This work is tedious because one needs to create a message for every editable property and register methods to the Mediator from any class that uses that value. This is one of the shortcomings of this design.

Using the Application

Data Repositories

The application can be configured to use different sets of repositories in the data access layer. As seen in a previous section, there are mock repositories and database repositories. Using the database repositories requires actually creating the database (SQL script provided) and updating the connection string. However, in a first attempt to run the app, I would recommend using the mock configuration which doesn’t require any prior operations.

Exchange Clients

If you already have an account with Kraken, you can obtain a pair of public and private keys which you could specify in the configuration file. However, using the mock client is probably a good idea while getting familiar with the app.

XML
<appSettings>
    <add key="KrakenBaseAddress" value="https://api.kraken.com" />
    <add key="KrakenApiVersion" value="0" />
    <add key="KrakenKey" value="xxx" />
    <add key="KrakenSecret" value="xxx"/>
    <add key="KrakenClientMaxRetries" value="5" />
    <add key="PositionEpsilon" value="0.0000025"/>
    <add key="RepositoryConfigurationType" value="mock"/>
</appSettings>

The mock client models trading activity in a simplistic manner. The arrival of new trades is modeled as a non-homogeneous Poisson process. Trade volumes are modeled as independent random variables following a log normal distribution where the mean and standard deviation are estimated from historic data depending on day of week and hour of day. The price process is modeled as a geometric Brownian motion where mean and standard deviation are set arbitrarily. Indeed, the geometric Brownian motion (often used to model financial assets) is a very bad fit for the price of bitcoin which is very erratic and highly volatile.

It would be interesting to try to fit other models. I can make the trade data available upon request (more than a year of trades from Kraken).

Variables

  • Interval: Frequency of the algorithm. At the end of every time interval, price data will be fetched from the exchange, aggregated into a candlestick and passed to the strategy service for further processing.
  • WMA period: The number of candlesticks to include in the calculation of the Weighted Moving Average.Example with Interval = 5 minutes and WMA period = 180; The WMA will be calculated with data reaching back 180 * 5 = 900 minutes = 15 hours. Each point will be separated by a 5 min interval. The QuoteBot timer is set to 5 minutes.
  • NN Interval: Modulo that will be applied to the price to find the next ‘Natural Number’. Example with NN interval = 10; Setup confirmed at 392 in an up-trend , the next natural number is 400. And we move the limits by increments of 10.
  • Position Size: size of each new position expressed Euro. For example, if Position Size = 20 and we want to open a long position, we will place an order for 20 Euro worth of bitcoin.
  • Enable orders: If selected, the trading strategy will instruct the brokerage service to place orders with the exchange (mock or real). If not selected, nothing happens.

Debugging

The application uses log4net and a rolling file appender to record events and errors. Since the application leverages multithreading to run concurrent trading services, it can be quite complicated to follow the sequence of events. In the file tradingapp.log, the thread ID is indicated in front of each log line which can be very useful for debugging.

Conclusion

Most systematic trading platforms are comprised of independent components that handle quote-processing, run-time algorithmic processing, brokerage activity and run-time performance monitoring. In this paper, we described the design of a lightweight monolithic WPF application that handles these features and allows running an automated trading strategy. Bigger organizations might use service oriented architecture to physically separate each of these constituents and reduce the number of failing points in the system.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionPennedObjects.RateLimiting Assembly Missing Pin
rob.evans5-May-22 9:42
rob.evans5-May-22 9:42 
QuestionI cannot build the project. Got errors Pin
Member 1486909624-Jun-20 15:43
Member 1486909624-Jun-20 15:43 
Questionvote of 5 Pin
Beginner Luck16-Jun-16 23:07
professionalBeginner Luck16-Jun-16 23:07 
QuestionUnable to start project Pin
Member 1228463316-Jun-16 15:05
Member 1228463316-Jun-16 15:05 
AnswerRe: Unable to start project Pin
kvuong825-Dec-16 20:48
kvuong825-Dec-16 20:48 
GeneralRe: Unable to start project Pin
Martin Arrivets9-Jan-17 6:03
Martin Arrivets9-Jan-17 6:03 
GeneralRe: Unable to start project Pin
fredVias27-Mar-18 11:44
fredVias27-Mar-18 11:44 
GeneralRe: Unable to start project Pin
hemigueti13-Sep-18 9:58
hemigueti13-Sep-18 9:58 
QuestionNeed one information Pin
Tridip Bhattacharjee13-Jul-15 21:55
professionalTridip Bhattacharjee13-Jul-15 21:55 
AnswerRe: Need one information Pin
Martin Arrivets14-Jul-15 0:09
Martin Arrivets14-Jul-15 0:09 
GeneralRe: Need one information Pin
Tridip Bhattacharjee14-Jul-15 21:44
professionalTridip Bhattacharjee14-Jul-15 21:44 
GeneralRe: Need one information Pin
Martin Arrivets23-Jul-15 3:49
Martin Arrivets23-Jul-15 3:49 
QuestionMore information about application Pin
kiquenet.com16-Jun-15 2:14
professionalkiquenet.com16-Jun-15 2:14 
AnswerRe: More information about application Pin
Martin Arrivets16-Jun-15 6:24
Martin Arrivets16-Jun-15 6:24 
GeneralMy vote of 5 Pin
DrABELL18-Jan-15 12:59
DrABELL18-Jan-15 12:59 
GeneralRe: My vote of 5 Pin
Martin Arrivets19-Jan-15 20:09
Martin Arrivets19-Jan-15 20:09 
QuestionOne comment Pin
Sacha Barber14-Dec-14 10:14
Sacha Barber14-Dec-14 10:14 
AnswerRe: One comment Pin
Martin Arrivets15-Dec-14 0:13
Martin Arrivets15-Dec-14 0:13 
GeneralRe: One comment Pin
Sacha Barber15-Dec-14 4:20
Sacha Barber15-Dec-14 4:20 
QuestionI recognize that guy Pin
Sacha Barber14-Dec-14 9:57
Sacha Barber14-Dec-14 9:57 
Questiondownload missing Pin
matthijsdz14-Dec-14 7:38
matthijsdz14-Dec-14 7:38 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.