' . "\n\n" . '
4-6 sentences explaining what is happening, why it matters, and how traders may be positioned. Include brief 90-day context.
' . "\n\n" . 'One compact paragraph (2-4 sentences) summarizing 4H trend, momentum, volatility, and structure.
' . "\n\n" . 'Recommendation: Buy / Hold / Sell
' . "\n" . 'Confidence: HIGH / MEDIUM / LOW
' . "\n" . 'Entry Price: $[value]
' . "\n" . 'Target Price (1st): $[value]
' . "\n" . 'Stop-Loss: $[value]
' . "\n" . 'Risk/Reward Ratio: 1:[value]
' . "\n" . 'Reasoning: 4-5 sentences: confluence + invalidation + main risk + 90-day alignment.
' . "\n\n" . '' . "\n"
. ' {insert ONLY valid JSON - no trailing commas - containing:' . "\n"
. ' timeframe metadata,' . "\n"
. ' indicator outputs (including MACD line/signal/histogram),' . "\n"
. ' support/resistance levels,' . "\n"
. ' long-term summary values,' . "\n"
. ' and final signal.' . "\n"
. ' Do NOT include the full raw OHLC array.}' . "\n"
. ' ' . "\n"
. '' . "\n\n"
. '====================================================' . "\n"
. 'STYLE GUIDELINES' . "\n"
. '====================================================' . "\n"
. '- No Markdown.' . "\n"
. '- No code blocks.' . "\n"
. '- No CSS beyond the image tag.' . "\n"
. '- Do not invent extra fields.' . "\n"
. '- All numeric values must be exact decimals.' . "\n\n"
. '====================================================' . "\n"
. 'GOAL' . "\n"
. '====================================================' . "\n"
. 'Produce a professional-grade, investor-ready technical analysis in clean HTML with a' . "\n"
. 'justified Buy/Hold/Sell signal based on 4H candles, informed - but not overridden - by' . "\n"
. '90-day trend context, with both plain-English interpretation and advanced precision.' . "\n\n"
. 'Cryptocurrency: ' . $safe_coin_id . "\n\n"
. 'DATA:' . "\n"
. $json_data;
return $prompt_text;
}
/**
* Get the system prompt for all AI providers.
*
* @return string System prompt text.
*/
public static function get_system_prompt() {
return 'You are a professional cryptocurrency technical analyst specializing in swing trading signals.';
}
/**
* Send prompt to OpenAI.
*
* @param string $prompt_text The full user prompt.
* @return string|WP_Error Response text or WP_Error.
*/
private static function send_to_openai( $prompt_text ) {
$api_key = get_option( 'adamca_ai_api_key', '' );
$model_name = get_option( 'adamca_ai_model', 'gpt-4o' );
// GPT-5 models use the Responses API.
if ( 0 === strpos( $model_name, 'gpt-5' ) ) {
$combined_prompt = self::get_system_prompt() . "\n\n" . $prompt_text;
return self::send_to_openai_responses( $combined_prompt, $api_key, $model_name );
}
$request_url = 'https://api.openai.com/v1/chat/completions';
$request_body = wp_json_encode( array(
'model' => $model_name,
'messages' => array(
array( 'role' => 'system', 'content' => self::get_system_prompt() ),
array( 'role' => 'user', 'content' => $prompt_text ),
),
'max_tokens' => 8096,
) );
$response = wp_remote_post( $request_url, array(
'timeout' => 140,
'headers' => array(
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $api_key,
),
'body' => $request_body,
) );
if ( is_wp_error( $response ) ) {
return $response;
}
$status_code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
return new WP_Error(
'adamca_openai_error',
sprintf(
/* translators: %d: HTTP status code */
__( 'OpenAI returned HTTP %d.', 'adams-crypto-analysis' ),
$status_code
)
);
}
$response_body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( empty( $response_body['choices'][0]['message']['content'] ) ) {
return new WP_Error( 'adamca_openai_empty', __( 'OpenAI returned an empty response.', 'adams-crypto-analysis' ) );
}
return $response_body['choices'][0]['message']['content'];
}
/**
* Send prompt to OpenAI Responses API (GPT-5 models).
*
* @param string $prompt_text The full user prompt.
* @param string $api_key OpenAI API key.
* @param string $model_name Model name (e.g. gpt-5, gpt-5-mini).
* @return string|WP_Error Response text or WP_Error.
*/
private static function send_to_openai_responses( $prompt_text, $api_key, $model_name ) {
$request_url = 'https://api.openai.com/v1/responses';
$request_body = wp_json_encode( array(
'model' => $model_name,
'input' => array(
array(
'role' => 'user',
'content' => array(
array(
'type' => 'input_text',
'text' => $prompt_text,
),
),
),
),
'reasoning' => array(
'effort' => 'low',
),
'text' => array(
'verbosity' => 'high',
),
'max_output_tokens' => 9096,
) );
self::log_openai_debug( 'Responses request', array(
'endpoint' => $request_url,
'model' => $model_name,
'payload_chars' => mb_strlen( $request_body ),
'payload_bytes' => strlen( $request_body ),
'payload' => $request_body,
) );
$response = wp_remote_post( $request_url, array(
'timeout' => 120,
'headers' => array(
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $api_key,
),
'body' => $request_body,
) );
if ( is_wp_error( $response ) ) {
self::log_openai_debug( 'Responses transport error', array(
'error' => $response->get_error_message(),
) );
return $response;
}
$status_code = wp_remote_retrieve_response_code( $response );
$raw_body = wp_remote_retrieve_body( $response );
self::log_openai_debug( 'Responses raw response', array(
'status_code' => $status_code,
'body' => $raw_body,
) );
if ( 200 !== $status_code ) {
return new WP_Error(
'adamca_openai_error',
sprintf(
/* translators: %d: HTTP status code */
__( 'OpenAI returned HTTP %d.', 'adams-crypto-analysis' ),
$status_code
)
);
}
$response_body = json_decode( $raw_body, true );
if ( ! is_array( $response_body ) ) {
$json_error = function_exists( 'json_last_error_msg' ) ? json_last_error_msg() : 'Unknown JSON decode error';
self::log_openai_debug( 'Responses JSON decode error', array(
'json_error' => $json_error,
'raw_body' => $raw_body,
) );
return new WP_Error( 'adamca_openai_parse_error', __( 'OpenAI returned an unreadable response.', 'adams-crypto-analysis' ) );
}
self::log_openai_debug( 'Responses parsed JSON', array(
'json_decode_ok' => true,
'has_output_text' => isset( $response_body['output_text'] ),
'output_text_type' => isset( $response_body['output_text'] ) ? gettype( $response_body['output_text'] ) : 'absent',
'has_output' => isset( $response_body['output'] ),
'output_count' => isset( $response_body['output'] ) && is_array( $response_body['output'] ) ? count( $response_body['output'] ) : 0,
'top_level_keys' => is_array( $response_body ) ? array_keys( $response_body ) : array(),
) );
$response_text = self::extract_openai_responses_text( $response_body );
self::log_openai_debug( 'Responses extraction result', array(
'parsed_text_length' => mb_strlen( $response_text ),
'parsed_text_empty' => '' === $response_text,
) );
if ( '' === $response_text ) {
self::log_openai_debug( 'Responses full response body (empty result)', array(
'response_body' => $response_body,
) );
return new WP_Error( 'adamca_openai_empty', __( 'OpenAI returned an empty response.', 'adams-crypto-analysis' ) );
}
return $response_text;
}
/**
* Extract assistant text from OpenAI Responses API payload.
*
* Handles both the top-level `output_text` convenience field and the nested
* `output[].content[].text` structure that OpenAI may return.
*
* @param array $response_body Decoded JSON response body.
* @return string
*/
private static function extract_openai_responses_text( $response_body ) {
if ( ! is_array( $response_body ) ) {
return '';
}
// 1. Prefer the top-level output_text convenience field if present and non-empty.
if ( ! empty( $response_body['output_text'] ) ) {
if ( is_array( $response_body['output_text'] ) ) {
$output_text_parts = array();
foreach ( $response_body['output_text'] as $output_text_item ) {
$normalized = self::extract_openai_text_value( $output_text_item );
if ( '' !== $normalized ) {
$output_text_parts[] = $normalized;
}
}
$joined = trim( implode( "\n", $output_text_parts ) );
if ( '' !== $joined ) {
return $joined;
}
}
if ( is_string( $response_body['output_text'] ) ) {
$trimmed = trim( $response_body['output_text'] );
if ( '' !== $trimmed ) {
return $trimmed;
}
}
}
// 2. Fall back to iterating output[] -> content[] -> text.
if ( empty( $response_body['output'] ) || ! is_array( $response_body['output'] ) ) {
return '';
}
$text_parts = array();
foreach ( $response_body['output'] as $output_item ) {
if ( ! is_array( $output_item ) ) {
continue;
}
// Some output items may carry a text field directly (e.g. type=output_text at item level).
if ( isset( $output_item['type'] ) && 'output_text' === $output_item['type']
&& isset( $output_item['text'] ) ) {
$normalized_text = self::extract_openai_text_value( $output_item['text'] );
if ( '' !== $normalized_text ) {
$text_parts[] = $normalized_text;
}
continue;
}
if ( empty( $output_item['content'] ) || ! is_array( $output_item['content'] ) ) {
continue;
}
foreach ( $output_item['content'] as $content_item ) {
if ( ! is_array( $content_item ) ) {
continue;
}
// Accept content blocks with type "output_text" or any block that has a text field.
$is_text_type = isset( $content_item['type'] ) && in_array( $content_item['type'], array( 'output_text', 'text' ), true );
$has_text = isset( $content_item['text'] );
if ( $is_text_type || $has_text ) {
if ( $has_text ) {
$normalized_text = self::extract_openai_text_value( $content_item['text'] );
if ( '' !== $normalized_text ) {
$text_parts[] = $normalized_text;
}
}
}
}
}
return trim( implode( "\n", $text_parts ) );
}
/**
* Normalize OpenAI Responses text payload variants to a plain string.
*
* @param mixed $value Raw text value from Responses JSON.
* @return string
*/
private static function extract_openai_text_value( $value ) {
if ( is_string( $value ) ) {
return trim( $value );
}
if ( is_array( $value ) ) {
if ( isset( $value['value'] ) && is_string( $value['value'] ) ) {
return trim( $value['value'] );
}
if ( isset( $value['text'] ) && is_string( $value['text'] ) ) {
return trim( $value['text'] );
}
$parts = array();
foreach ( $value as $sub_value ) {
$normalized = self::extract_openai_text_value( $sub_value );
if ( '' !== $normalized ) {
$parts[] = $normalized;
}
}
return trim( implode( "\n", $parts ) );
}
return '';
}
/**
* Log OpenAI-only debug details when explicitly enabled.
*
* @param string $label Log label.
* @param array $context Log context payload.
* @return void
*/
private static function log_openai_debug( $label, $context ) {
// Safe debug mode is disabled by default because prompts can include large data payloads.
// Enable as needed: add_filter( 'adamca_openai_safe_debug_mode', '__return_true' );
$enabled = (bool) apply_filters( 'adamca_openai_safe_debug_mode', false );
if ( ! $enabled ) {
return;
}
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log( 'ADAMCA OpenAI Debug [' . $label . ']: ' . wp_json_encode( $context ) );
}
/**
* Send prompt to xAI / Grok.
*
* @param string $prompt_text The full user prompt.
* @return string|WP_Error Response text or WP_Error.
*/
private static function send_to_grok_xai( $prompt_text ) {
$api_key = get_option( 'adamca_ai_api_key', '' );
$model_name = get_option( 'adamca_ai_model', 'grok-3' );
$request_url = 'https://api.x.ai/v1/chat/completions';
$body_array = array(
'model' => $model_name,
'messages' => array(
array( 'role' => 'system', 'content' => self::get_system_prompt() ),
array( 'role' => 'user', 'content' => $prompt_text ),
),
'max_tokens' => 4096,
'temperature' => 0.3,
'stream' => false,
);
$request_body = wp_json_encode( $body_array );
$response = wp_remote_post( $request_url, array(
'timeout' => 120,
'headers' => array(
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $api_key,
),
'body' => $request_body,
) );
if ( is_wp_error( $response ) ) {
return $response;
}
$status_code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
return new WP_Error(
'adamca_xai_error',
sprintf(
/* translators: %d: HTTP status code */
__( 'xAI returned HTTP %d.', 'adams-crypto-analysis' ),
$status_code
)
);
}
$response_body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( empty( $response_body['choices'][0]['message']['content'] ) ) {
return new WP_Error( 'adamca_xai_empty', __( 'xAI returned an empty response.', 'adams-crypto-analysis' ) );
}
return $response_body['choices'][0]['message']['content'];
}
/**
* Send prompt to Anthropic / Claude.
*
* @param string $prompt_text The full user prompt.
* @return string|WP_Error Response text or WP_Error.
*/
private static function send_to_anthropic( $prompt_text ) {
$api_key = get_option( 'adamca_ai_api_key', '' );
$model_name = get_option( 'adamca_ai_model', 'claude-opus-4-5' );
$request_url = 'https://api.anthropic.com/v1/messages';
$request_body = wp_json_encode( array(
'model' => $model_name,
'max_tokens' => 4096,
'system' => self::get_system_prompt(),
'messages' => array(
array( 'role' => 'user', 'content' => $prompt_text ),
),
) );
$response = wp_remote_post( $request_url, array(
'timeout' => 120,
'headers' => array(
'Content-Type' => 'application/json',
'x-api-key' => $api_key,
'anthropic-version' => '2023-06-01',
),
'body' => $request_body,
) );
if ( is_wp_error( $response ) ) {
return $response;
}
$status_code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
return new WP_Error(
'adamca_anthropic_error',
sprintf(
/* translators: %d: HTTP status code */
__( 'Anthropic returned HTTP %d.', 'adams-crypto-analysis' ),
$status_code
)
);
}
$response_body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( empty( $response_body['content'][0]['text'] ) ) {
return new WP_Error( 'adamca_anthropic_empty', __( 'Anthropic returned an empty response.', 'adams-crypto-analysis' ) );
}
return $response_body['content'][0]['text'];
}
/**
* Strip accidental markdown fencing from AI response and inject signal image.
*
* @param string $raw_response Raw response text from AI.
* @return string Cleaned HTML.
*/
public static function clean_response( $raw_response ) {
$html = preg_replace( '/^```html\s*|^```\s*|```\s*$/m', '', trim( $raw_response ) );
// Remove any AI-generated
';
// Insert after