Skip to main content

Creating Actionable Signals

A good trading signal is actionable. It tells the trader:
  • Where to enter
  • Where to place the stop loss
  • Where to take profits
Always include Entry, Stop Loss, and at least one Take Profit in your signals.

Signal Structure

Complete BUY Signal

{
  "points": [
    {
      "time": 1765540800,
      "type": "low",
      "price": 1.1725,
      "label": "BUY",
      "color": "#3b82f6",
      "shape": "arrowUp",
      "size": 2
    },
    {
      "time": 1765540800,
      "type": "low",
      "price": 1.1695,
      "label": "SL",
      "color": "#ef4444",
      "shape": "square",
      "size": 1
    },
    {
      "time": 1765540800,
      "type": "high",
      "price": 1.1755,
      "label": "TP1",
      "color": "#22c55e",
      "shape": "circle",
      "size": 1
    },
    {
      "time": 1765540800,
      "type": "high",
      "price": 1.1785,
      "label": "TP2",
      "color": "#22c55e",
      "shape": "circle",
      "size": 1
    },
    {
      "time": 1765540800,
      "type": "high",
      "price": 1.1815,
      "label": "TP3",
      "color": "#22c55e",
      "shape": "circle",
      "size": 1
    }
  ],
  "metadata": {
    "signal_type": "BUY",
    "entry": 1.1725,
    "sl": 1.1695,
    "tp1": 1.1755,
    "tp2": 1.1785,
    "tp3": 1.1815,
    "risk_pips": 30,
    "risk_reward": "1:3"
  }
}

Risk Management

Calculate Risk/Reward

def calculate_risk_reward(entry, stop_loss, take_profit):
    """Calculate risk/reward ratio."""
    risk = abs(entry - stop_loss)
    reward = abs(take_profit - entry)
    return reward / risk if risk > 0 else 0

# Example
entry = 1.1725
sl = 1.1695
tp = 1.1815

rr = calculate_risk_reward(entry, sl, tp)
print(f"Risk/Reward: 1:{rr:.1f}")  # Output: Risk/Reward: 1:3.0

Minimum R:R Requirements

Signal QualityMinimum R:R
Standard1:1
Good1:2
Excellent1:3+
Signals with R:R below 1:1 are generally not worth taking. Consider filtering these out.

Visual Hierarchy

Size Recommendations

ElementSizeWhy
Entry2Most important - catches attention
Stop Loss1Secondary - always visible
Take Profits1Secondary - shows targets

Color Consistency

Always use consistent colors:
ElementColorHex
BUY EntryBlue#3b82f6
SELL EntryOrange#f97316
Stop LossRed#ef4444
Take ProfitGreen#22c55e
Neutral/InfoYellow#eab308

Label Best Practices

DO

  • Use short labels: BUY, SELL, TP1, SL
  • Be consistent across signals
  • Include level numbers for TPs: TP1, TP2, TP3

DON’T

  • Avoid long labels: Buy Entry Here at Support
  • Don’t use special characters that may not render
  • Don’t mix naming conventions

Time Alignment

Always use the time value from the /bars endpoint. Never calculate timestamps manually.
# CORRECT
bars = get_bars("EURUSD", 60)
signal_time = bars.iloc[-1]["time"]  # Use time from API

# WRONG
signal_time = int(datetime.now().timestamp())  # May not align with bars

Multiple Signals on Same Bar

You can place multiple points on the same bar by using the same time value:
points = [
    {"time": bar_time, "type": "low", "price": entry, "label": "BUY", ...},
    {"time": bar_time, "type": "low", "price": sl, "label": "SL", ...},
    {"time": bar_time, "type": "high", "price": tp1, "label": "TP1", ...},
]

Signal Spacing

For visual clarity, add small offsets to prices:
def create_buy_signal(bar, entry_price, sl_price, tp_prices):
    """Create a BUY signal with proper spacing."""
    points = []

    # Entry - slightly below the low
    points.append({
        "time": int(bar["time"]),
        "type": "low",
        "price": entry_price,
        "label": "BUY",
        "color": "#3b82f6",
        "shape": "arrowUp",
        "size": 2
    })

    # SL - with offset below entry
    points.append({
        "time": int(bar["time"]),
        "type": "low",
        "price": sl_price,
        "label": "SL",
        "color": "#ef4444",
        "shape": "square",
        "size": 1
    })

    # TPs - with offsets above entry
    for i, tp in enumerate(tp_prices, 1):
        points.append({
            "time": int(bar["time"]),
            "type": "high",
            "price": tp,
            "label": f"TP{i}",
            "color": "#22c55e",
            "shape": "circle",
            "size": 1
        })

    return points

Metadata for Analytics

Include metadata to track signal performance:
metadata = {
    # Signal details
    "signal_type": "BUY",
    "entry_price": 1.1725,
    "stop_loss": 1.1695,
    "take_profits": [1.1755, 1.1785, 1.1815],

    # Risk metrics
    "risk_pips": 30,
    "reward_pips": 90,
    "risk_reward": "1:3",

    # Strategy info
    "strategy": "ICT Order Block",
    "timeframe": "H1",
    "confluence_factors": ["OB", "FVG", "HTF Trend"],

    # Confidence
    "confidence": "high",  # low, medium, high

    # Timestamp
    "generated_at": datetime.utcnow().isoformat()
}

Filter Low-Quality Signals

Before submitting, filter out weak signals:
def is_quality_signal(entry, sl, tp, min_rr=1.5):
    """Check if signal meets quality criteria."""
    rr = calculate_risk_reward(entry, sl, tp)
    return rr >= min_rr

# Filter signals
quality_signals = [
    s for s in all_signals
    if is_quality_signal(s["entry"], s["sl"], s["tp"])
]

Update Frequency

TimeframeRecommended Update
M1Every 1 minute
M5Every 5 minutes
M15Every 15 minutes
H1Every hour
H4Every 4 hours
D1Once per day
Don’t update too frequently - it wastes API calls and can cause visual flickering.

Error Handling

Always handle API errors gracefully:
def safe_submit(points, max_retries=3):
    """Submit with retry logic."""
    for attempt in range(max_retries):
        try:
            return submit_indicator(points)
        except requests.exceptions.HTTPError as e:
            if e.response.status_code == 429:
                # Rate limited - wait and retry
                wait = 60 * (attempt + 1)
                print(f"Rate limited. Waiting {wait}s...")
                time.sleep(wait)
            elif e.response.status_code >= 500:
                # Server error - wait and retry
                time.sleep(5 * (attempt + 1))
            else:
                raise
    raise Exception("Max retries exceeded")

Checklist

Before deploying your indicator:
1

Verify time alignment

Signals appear on the correct candles
2

Check visual hierarchy

Entry is prominent, TPs/SL are secondary
3

Validate R:R ratios

All signals meet minimum R:R requirements
4

Test on multiple timeframes

Indicator works on H1, H4, D1
5

Add error handling

Gracefully handle API errors
6

Include metadata

Track signal performance

Next Steps