Skip to content

Local Dependencies

During development, it is often useful to test a package in a real project before publishing it to the registry. In this section, we will demonstrate how to use local dependencies by integrating the barhelper package into the expertdemo project.

We will implement a simple SMA-based trading strategy with the following logic:

  • Entry condition: Enter long when sma1 crosses above sma2.
  • Entry filter: Only enter if close > sma1 > sma2 > sma3. This filter can be toggled on or off.
  • Exit condition: Exit only when sma1 crosses below sma2.
  • Parameter validation: Ensure that sma1.period < sma2.period < sma3.period.

We will use the CrossUp function from barhelper before it is published to the registry, and we will retrieve SMA values using the KnitPkgSMA indicator. As a result, expertdemo will no longer depend on the calc package.


Why Use Local Dependencies

Even with a complete suite of unit tests (and we know you always write them), you may want to validate your package in a real project before publishing. In other cases, you might want to integrate a package into a consumer project while still developing it. KnitPkg supports this workflow through local dependencies.


Adding a Local Dependency

To add a local dependency, manually edit the knitpkg.yaml manifest and specify the path to the local package directory. You can use:

  • A relative path (must start with ./ or ../ and use / as separator)
  • A file URI (must start with file://, supports both / and \, and can be absolute or relative)

Here’s how to add barhelper as a local dependency to expertdemo:

dependencies:
    '@douglasrechia/barhelper': ../../Scripts/barhelper

Should You Declare an Indirect Dependency?

Since barhelper depends on bar, the bar package is available to expertdemo as an indirect dependency. But should we declare it explicitly?

Let’s consider:

  • barhelper exposes a function (CrossUp) whose signature uses ITimeSeries, a type declared in bar. This is a dependency in the API, not just the implementation.
  • If barhelper used bar only internally, and its API did not expose any symbols from bar, then expertdemo would not need to depend on bar.
  • expertdemo also uses symbols from bar directly (e.g., BarWatcher, BarMqlRates), unrelated to barhelper.

From this, we conclude that an indirect dependency should be declared explicitly if:

  • The API of a direct dependency uses symbols from the indirect dependency
  • The consumer project uses symbols from the indirect dependency directly

Therefore, expertdemo should declare bar as a direct dependency. However, this also means that expertdemo becomes responsible for defining the version of bar to be used — either with a version range or an exact match.

In practice, this setup would work with or without declaring bar, but being explicit has its benefits.

Forcing a Specific Version of an Indirect Dependency

If you decide not to declare an indirect dependency explicitly in the dependencies section, but still want to control which version is used, you can use the overrides field in your manifest.

This allows you to force a specific version of a dependency that is required transitively by another package.

Here’s an example:

overrides:
    '@douglasrechia/bar': 1.0.0
dependencies:
    '@douglasrechia/calc': ^1.0.0

In this case, even though @douglasrechia/bar is not listed as a direct dependency, the override ensures that version 1.0.0 will be used if calc (or any other dependency) requires bar.

This is useful when:

  • You want to avoid declaring a dependency explicitly
  • You need to resolve version conflicts
  • You want to ensure reproducibility across builds

For more details, see Version Conflicts and Overrides.


Adjusting Dependencies to Use the sma Indicator

Let’s add bar as a direct dependency:

kp add @douglasrechia/bar

We will use CrossUp to detect SMA crossovers. This function expects ITimeSeries inputs, which we will construct using the KnitPkgSMA indicator. This eliminates the need for the calc package.

Final dependency list in expertdemo:

dependencies:
  '@douglasrechia/bar': ^1.0.0
  '@douglasrechia/barhelper': ../../Scripts/barhelper

Order of Dependency Declarations Matters

In the example above, the dependency on @douglasrechia/bar was declared before @douglasrechia/barhelper. This is intentional.

KnitPkg resolves dependencies in the order they appear in the manifest. If barhelper is declared first, KnitPkg will resolve its transitive dependency on bar before seeing the version constraint declared by expertdemo. As a result, the version of bar used will be the one required by barhelper, not the one specified by expertdemo.

Declaring @douglasrechia/bar before @douglasrechia/barhelper ensures that the version of bar is locked according to the version range specified by expertdemo.

For more details, see Version Conflicts and Overrides.


Updating the Entrypoint

The expertdemo project uses Flat mode, and its entrypoint is KnitPkgExpertDemo.mqh.

Update the includes:

/* @knitpkg:include "douglasrechia/bar/BarWatcher.mqh" */
/* @knitpkg:include "douglasrechia/bar/BarMqlRates.mqh" */
/* @knitpkg:include "douglasrechia/bar/TimeSeriesArray.mqh" */
/* @knitpkg:include "douglasrechia/barhelper/Cross.mqh" */

Installing Dependencies

Run:

kp install

You should see output like this:

alt text

@douglasrechia/barhelper appears in the dependency tree. Let’s confirm in MetaEditor:

alt text

The IntelliSense confirms that CrossUp is available via the generated flat file.


Updating expertdemo

We’ll now implement the SMA strategy using CrossUp. Here’s the updated OnNewBar() function:

void OnNewBar()
  {
//--- Get latest 5 bars data
   myBar.Refresh(5);

//--- Get the 5 most recent values of SMA into the TimeSeries.
//--- Do it for the three SMAs.
   double sma1array[];
   ArraySetAsSeries(sma1array, true);
   CopyBuffer(sma1handle, 0, 0, 5, sma1array);
   douglasrechia::TimeSeriesArray<double> sma1(sma1array);

   double sma2array[];
   ArraySetAsSeries(sma2array, true);
   CopyBuffer(sma2handle, 0, 0, 5, sma2array);
   douglasrechia::TimeSeriesArray<double> sma2(sma2array);

   double sma3array[];
   ArraySetAsSeries(sma3array, true);
   CopyBuffer(sma3handle, 0, 0, 5, sma3array);
   douglasrechia::TimeSeriesArray<double> sma3(sma3array);

   if(!positionInfo.Select(_Symbol))
     {
      //--- No position. Check filter conditions.
      if(!filterEnabled || (myBar.Close(1) > sma1.ValueAtShift(1) &&
                            sma1.ValueAtShift(1) > sma2.ValueAtShift(1) &&
                            sma2.ValueAtShift(1) > sma3.ValueAtShift(1)))
        {
         //--- Open position if Short term sma1 crosses up Mid term sma2
         if(douglasrechia::CrossUp(sma1, sma2, 1))
           {
            Print(StringFormat("[%s] Let's do it", TimeToString(myBar.Time(0))));
            trade.Buy(1);
           }
        }
     }
   else
     {
      //--- Position is found. Close it if Short term sma1 crosses down Mid term sma2.
      if(douglasrechia::CrossUp(sma2, sma1, 1))
        {
         Print(StringFormat("[%s] Time to go", TimeToString(myBar.Time(0))));
         trade.PositionClose(_Symbol);
        }
     }

  }

You can find the full source here:

Here’s the equity curve for EURUSD H4 (2025-01-01 to 2025-12-31, with default parameters but filter off):

alt text


Additional Improvements

Let’s improve the code by abstracting the SMA buffer-to-series logic into a helper function in barhelper.

We’ll create a new function NewTimeSeriesFromIndicator() in barhelper:

TimeSeriesArray<double>* NewTimeSeriesFromIndicator(int indicatorHandler, int indicatorBufferNum, int startPos, int count)
  {
   double array[];
   ArraySetAsSeries(array, true);
   if(CopyBuffer(indicatorHandler, indicatorBufferNum, startPos, count, array) == -1)
      return NULL;

   return new TimeSeriesArray<double>(array);
  }

This function should be placed in knitpkg/include/douglasrechia/barhelper/IndicatorSeries.mqh, which is available here.

After updating barhelper, we can simplify OnNewBar() like this:

//--- Get the 5 most recent values of SMA into the TimeSeries.
//--- Do it for the three SMAs.
   douglasrechia::TimeSeriesArray<double>* sma1 =
      douglasrechia::NewTimeSeriesFromIndicator(sma1handle, 0, 0, 5);

   douglasrechia::TimeSeriesArray<double>* sma2 =
      douglasrechia::NewTimeSeriesFromIndicator(sma2handle, 0, 0, 5);

   douglasrechia::TimeSeriesArray<double>* sma3 =
      douglasrechia::NewTimeSeriesFromIndicator(sma3handle, 0, 0, 5);

This encapsulates the buffer-copy logic into a reusable abstraction. The final expert code is available here:

Note

Applicable to packages only: always run kp checkinstall to verify that the directives are correct.

Applicable to all project types except packages: kp install is required every time you update any @knitpkg:include directive in the entrypoint header.

Tip

Execute kp build --no-locked to run install + compile at one shot; --no-locked is required because the build command installs in --locked mode by default and we have local dependencies. See kp build.


Congratulations! You’ve learned how to use local dependencies, manage indirect dependencies, and improve code abstraction using KnitPkg.