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 Quality Minimum R:R Standard 1:1 Good 1:2 Excellent 1:3+
Signals with R:R below 1:1 are generally not worth taking. Consider filtering these out.
Visual Hierarchy
Size Recommendations
Element Size Why Entry 2 Most important - catches attention Stop Loss 1 Secondary - always visible Take Profits 1 Secondary - shows targets
Color Consistency
Always use consistent colors:
Element Color Hex BUY Entry Blue #3b82f6SELL Entry Orange #f97316Stop Loss Red #ef4444Take Profit Green #22c55eNeutral/Info Yellow #eab308
Label Best Practices
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
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
Timeframe Recommended Update M1 Every 1 minute M5 Every 5 minutes M15 Every 15 minutes H1 Every hour H4 Every 4 hours D1 Once 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:
Verify time alignment
Signals appear on the correct candles
Check visual hierarchy
Entry is prominent, TPs/SL are secondary
Validate R:R ratios
All signals meet minimum R:R requirements
Test on multiple timeframes
Indicator works on H1, H4, D1
Add error handling
Gracefully handle API errors
Include metadata
Track signal performance
Next Steps
Continuous Integration Set up automated signal updates
Python SDK Complete Python reference