Extending MAHORAGA-Next

Code examples for common customizations.

For a full customization guide, see Strategies. This page provides copy-paste examples.

Signal Format

Data sources return signals in this format:

{
  symbol: "AAPL",
  source: "my_source",
  source_detail: "my_source_v1",
  sentiment: 0.65,        // -1 to 1, weighted
  raw_sentiment: 0.72,    // Before source weight applied
  volume: 42,             // Mention count
  freshness: 0.9,         // Time decay factor
  source_weight: 0.9,     // How much to trust this source
  reason: "MySource: 42 mentions, 65% bullish",
  timestamp: 1707500000000
}
      

Example: News API Integration

Write a standalone Gatherer module in your strategy directory:

// src/strategy/my-strategy/gatherers/news.ts
import type { Gatherer, StrategyContext } from "../../types";
import { extractTickers } from "../default/helpers/ticker";
import { calculateTimeDecay } from "../default/helpers/sentiment";

export const newsGatherer: Gatherer = {
  name: "news_api",
  gather: async (ctx: StrategyContext) => {
    const signals = [];
    try {
      const res = await fetch("https://newsapi.example/finance", {
        headers: { Authorization: `Bearer ${ctx.env.NEWS_API_KEY}` },
      });
      const data = await res.json();

      for (const article of data.articles) {
        const tickers = extractTickers(article.title + " " + article.description);
        const sentiment = article.title.toLowerCase().includes("surge") ? 0.7
          : article.title.toLowerCase().includes("drop") ? -0.5 : 0.1;

        for (const symbol of tickers) {
          signals.push({
            symbol,
            source: "news_api",
            source_detail: article.source.name,
            sentiment: sentiment * 0.8,
            raw_sentiment: sentiment,
            volume: 1,
            freshness: calculateTimeDecay(new Date(article.publishedAt).getTime() / 1000),
            source_weight: 0.8,
            reason: `News: ${article.title.slice(0, 60)}...`,
            timestamp: Date.now(),
          });
        }
      }
    } catch (error) {
      ctx.log("NewsAPI", "error", { message: String(error) });
    }
    return signals;
  },
};
      

Example: Custom Exit Logic

Write a selectExits function that returns sell candidates:

// src/strategy/my-strategy/rules/exits.ts
import type { SellCandidate, StrategyContext } from "../../types";
import type { Account, Position } from "../../../core/types";

export function selectExits(
  ctx: StrategyContext,
  positions: Position[],
  _account: Account
): SellCandidate[] {
  const exits: SellCandidate[] = [];

  for (const pos of positions) {
    const entry = ctx.positionEntries[pos.symbol];
    if (!entry) continue;

    const plPct = ((pos.current_price - entry.entry_price) / entry.entry_price) * 100;
    const peakPct = ((entry.peak_price - entry.entry_price) / entry.entry_price) * 100;

    // Trailing stop: sell if up 5%+ but dropped 2% from peak
    if (plPct > 5 && plPct < peakPct - 2) {
      exits.push({ symbol: pos.symbol, reason: "Trailing stop triggered" });
    }
  }

  return exits;
}
      

Example: Multi-Source Confirmation

Filter entries to only trade when multiple sources agree:

// In your selectEntries function
export function selectEntries(ctx, research, positions, account) {
  // Group signals by symbol
  const bySymbol = new Map();
  for (const signal of ctx.signals) {
    const arr = bySymbol.get(signal.symbol) || [];
    arr.push(signal);
    bySymbol.set(signal.symbol, arr);
  }

  // Only consider symbols with 2+ sources agreeing
  const confirmed = [...bySymbol.entries()]
    .filter(([_, signals]) => new Set(signals.map(s => s.source)).size >= 2)
    .filter(([_, signals]) => {
      const avg = signals.reduce((a, b) => a + b.sentiment, 0) / signals.length;
      return avg >= 0.5;
    });

  // Return BuyCandidate[] for confirmed symbols...
}
      

Ideas for Extension

Tip: Start simple. Add one data source, test it thoroughly, then add more complexity.