Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,37 @@ if(OPTIONX_BUILD_EXAMPLES)
endif()
target_link_libraries(telegram_archive_parser_smoke PRIVATE ${EXAMPLE_LIBS} optionx_cpp)

if(NOT TARGET tiny-process-library::tiny-process-library)
add_subdirectory(
${CMAKE_CURRENT_SOURCE_DIR}/external/tg-client-stdio/external/cpp/tiny-process-library
EXCLUDE_FROM_ALL
)
endif()

add_executable(telegram_live_bridge_smoke examples/telegram_live_bridge_smoke.cpp)
target_compile_features(telegram_live_bridge_smoke PRIVATE cxx_std_17)
target_include_directories(telegram_live_bridge_smoke PRIVATE
${EXAMPLE_INCLUDE_DIRS}
${EXAMPLE_DEPS_INCLUDE_DIRS}
${CMAKE_CURRENT_SOURCE_DIR}/external/tg-client-stdio/include
)
target_link_directories(telegram_live_bridge_smoke PRIVATE ${EXAMPLE_LIBRARY_DIRS})
target_compile_definitions(
telegram_live_bridge_smoke PRIVATE
${EXAMPLE_DEFINES}
LOGIT_BASE_PATH="${LOGIT_BASE_PATH_FWD}"
)
if(MINGW)
target_compile_options(telegram_live_bridge_smoke PRIVATE -Wa,-mbig-obj)
elseif(MSVC)
target_compile_options(telegram_live_bridge_smoke PRIVATE /bigobj)
endif()
target_link_libraries(telegram_live_bridge_smoke PRIVATE
${EXAMPLE_LIBS}
optionx_cpp
tiny-process-library::tiny-process-library
)

add_executable(protocol_v1_bridge_smoke examples/protocol_v1_bridge_smoke.cpp)
target_compile_features(protocol_v1_bridge_smoke PRIVATE cxx_std_17)

Expand Down
4 changes: 4 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ Currently maintained examples:
records through the parser and keeps executable signals, outcomes and
diagnostics separate. It uses deterministic fixtures and needs no Telegram
credentials.
- `telegram_live_bridge_smoke.cpp` starts tg-client-stdio, listens to a live
Telegram chat, and feeds raw messages through the C++ Telegram signal bridge.
It requires an authorized worker session and accepts settings from `.env` or
command-line options.
- `metatrader_file_bridge_smoke.cpp` runs the C++ side of the MetaTrader
Common\Files bridge against a temporary command/event layout.
- `metatrader_file_command_writer_smoke.cpp` demonstrates the C++ command-writer
Expand Down
300 changes: 300 additions & 0 deletions examples/telegram_live_bridge_smoke.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
/// \file telegram_live_bridge_smoke.cpp
/// \brief Runs the Telegram worker and C++ signal bridge against a live chat.

#include "example_utils.hpp"

#include <optionx_cpp/bridges/telegram.hpp>
#include <tg_client_stdio/worker_client.hpp>

#include <chrono>
#include <condition_variable>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>

namespace {

struct Options {
std::string worker_root;
std::string python;
std::string chat;
std::string marker;
std::string api_id;
std::string api_hash;
std::string session;
std::string proxy;
std::chrono::seconds timeout{300};
};

void load_env_file(const std::filesystem::path& path);
Options parse_options(int argc, char** argv);
bool wait_for_signal(
std::condition_variable& condition,
std::mutex& mutex,
bool& matched,
bool& failed,
std::chrono::seconds timeout);
std::string required_setting(const std::string& value, const char* name);

} // namespace

int main(int argc, char** argv) {
try {
std::cout.setf(std::ios::unitbuf);
const auto env_file_argument = optionx::examples::option_value(
argc, argv, "--env-file");
const auto env_file = env_file_argument.empty()
? optionx::examples::env_or("TG_CLIENT_STDIO_ENV_FILE")
: env_file_argument;
if (!env_file.empty()) {
load_env_file(env_file);
}

const auto options = parse_options(argc, argv);
if (options.chat.empty()) {
throw std::invalid_argument(
"chat is required; use --chat or TG_CLIENT_STDIO_TEST_CHAT");
}
const auto api_id = required_setting(
options.api_id, "TG_CLIENT_STDIO_API_ID");
const auto api_hash = required_setting(
options.api_hash, "TG_CLIENT_STDIO_API_HASH");
const auto session = required_setting(
options.session, "TG_CLIENT_STDIO_SESSION");
const auto worker_root = required_setting(
options.worker_root, "TG_CLIENT_STDIO_WORKER_ROOT");

std::vector<std::string> command{
options.python,
"-m",
"tg_client_stdio_worker",
"--backend",
"telethon",
"--api-id",
api_id,
"--api-hash",
api_hash,
"--session",
session,
};
if (!options.proxy.empty()) {
command.insert(command.end(), {"--proxy", options.proxy});
}

tg_client_stdio::WorkerProcessConfig worker_config;
worker_config.command = std::move(command);
worker_config.working_directory = worker_root;
worker_config.on_stderr = [](const std::string& text) {
std::cerr << "[tg-worker] " << text;
};

tg_client_stdio::WorkerClient worker;
if (!worker.start(std::move(worker_config))) {
std::cerr << "failed to start tg-client-stdio worker\n";
return 2;
}

const auto auth = worker.get_auth_status();
if (!auth.authorized) {
std::cerr << "Telegram session is not authorized\n";
worker.stop();
return 2;
}

auto source = std::make_shared<
optionx::bridges::telegram::TelegramWorkerMessageSource<
tg_client_stdio::WorkerClient>>(
worker,
optionx::bridges::telegram::TelegramWorkerSourceConfig{
{options.chat},
{},
});
optionx::bridges::telegram::TelegramSignalBridge bridge(source);

auto config = std::make_unique<
optionx::bridges::telegram::TelegramSignalBridgeConfig>();
config->bridge_id = 9101;
config->fixed_amount = 1.0;
if (!bridge.configure(std::move(config))) {
std::cerr << "failed to configure Telegram signal bridge\n";
worker.stop();
return 2;
}

std::mutex state_mutex;
std::condition_variable state_condition;
bool matched = false;
bool failed = false;
std::int64_t next_signal_id = 0;

bridge.on_signal_id() = [&next_signal_id]() {
return ++next_signal_id;
};
bridge.on_status_update() = [&state_condition, &state_mutex, &failed](
const optionx::BridgeStatusUpdate& update) {
std::cout << "status=" << optionx::to_str(update.status);
if (!update.message.empty()) {
std::cout << " message=" << update.message;
}
std::cout << '\n';
if (update.status == optionx::BridgeStatus::SERVER_START_FAILED ||
update.status == optionx::BridgeStatus::CONNECTION_ERROR) {
std::lock_guard<std::mutex> lock(state_mutex);
failed = true;
state_condition.notify_all();
}
};
bridge.on_signal_report() = [](const optionx::BridgeSignalReport& report) {
std::cout << "report=" << report.reason_code
<< " status=" << optionx::to_str(report.status);
if (!report.message.empty()) {
std::cout << " message=" << report.message;
}
std::cout << '\n';
};
bridge.on_trade_signal() = [
&state_condition,
&state_mutex,
&matched,
&options](std::unique_ptr<optionx::TradeSignal> signal) {
std::cout << "signal=" << signal->symbol
<< " direction=" << optionx::to_str(signal->order_type)
<< " duration=" << signal->duration
<< " name=" << signal->signal_name
<< " text=" << signal->comment << '\n';
if (options.marker.empty() ||
signal->comment.find(options.marker) != std::string::npos) {
std::lock_guard<std::mutex> lock(state_mutex);
matched = true;
state_condition.notify_all();
}
};

bridge.run();
const auto success = wait_for_signal(
state_condition,
state_mutex,
matched,
failed,
options.timeout);
bridge.shutdown();
worker.stop();

std::cout << "matched=" << (success ? "true" : "false") << '\n';
return success ? 0 : 1;
}
catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 2;
}
}

namespace {

void load_env_file(const std::filesystem::path& path) {
std::ifstream input(path);
if (!input) {
throw std::runtime_error("could not open env file: " + path.string());
}

std::string line;
while (std::getline(input, line)) {
const auto first = line.find_first_not_of(" \t");
if (first == std::string::npos || line[first] == '#') {
continue;
}
const auto separator = line.find('=', first);
if (separator == std::string::npos) {
continue;
}
auto key = line.substr(first, separator - first);
auto value = line.substr(separator + 1);
const auto key_end = key.find_last_not_of(" \t");
key.resize(key_end == std::string::npos ? 0 : key_end + 1);
const auto value_first = value.find_first_not_of(" \t");
value = value_first == std::string::npos
? std::string()
: value.substr(value_first);
if (value.size() >= 2 &&
((value.front() == '"' && value.back() == '"') ||
(value.front() == '\'' && value.back() == '\''))) {
value = value.substr(1, value.size() - 2);
}
if (key.empty() || std::getenv(key.c_str()) != nullptr) {
continue;
}
#ifdef _WIN32
_putenv_s(key.c_str(), value.c_str());
#else
setenv(key.c_str(), value.c_str(), 0);
#endif
}
}

Options parse_options(const int argc, char** argv) {
Options options;
options.worker_root = optionx::examples::env_or("TG_CLIENT_STDIO_WORKER_ROOT");
options.python = optionx::examples::env_or(
"TG_CLIENT_STDIO_PYTHON",
"python");
options.chat = optionx::examples::env_or("TG_CLIENT_STDIO_TEST_CHAT");
options.marker = optionx::examples::env_or("TG_CLIENT_STDIO_BRIDGE_MARKER");
options.api_id = optionx::examples::env_or("TG_CLIENT_STDIO_API_ID");
options.api_hash = optionx::examples::env_or("TG_CLIENT_STDIO_API_HASH");
options.session = optionx::examples::env_or("TG_CLIENT_STDIO_SESSION");
options.proxy = optionx::examples::env_or("TG_CLIENT_STDIO_PROXY");

const auto assign = [&](const char* name, std::string& target) {
const auto value = optionx::examples::option_value(argc, argv, name);
if (!value.empty()) {
target = value;
}
};
assign("--worker-root", options.worker_root);
assign("--python", options.python);
assign("--chat", options.chat);
assign("--marker", options.marker);
assign("--api-id", options.api_id);
assign("--api-hash", options.api_hash);
assign("--session", options.session);
assign("--proxy", options.proxy);

const auto timeout = optionx::examples::option_value(argc, argv, "--timeout");
if (!timeout.empty()) {
const auto seconds = std::stoi(timeout);
if (seconds <= 0) {
throw std::invalid_argument("timeout must be positive");
}
options.timeout = std::chrono::seconds(seconds);
}
return options;
}

bool wait_for_signal(
std::condition_variable& condition,
std::mutex& mutex,
bool& matched,
bool& failed,
const std::chrono::seconds timeout) {
std::unique_lock<std::mutex> lock(mutex);
condition.wait_for(lock, timeout, [&matched, &failed]() {
return matched || failed;
});
return matched && !failed;
}

std::string required_setting(
const std::string& value,
const char* name) {
if (value.empty()) {
throw std::invalid_argument(std::string(name) + " is required");
}
return value;
}

} // namespace
24 changes: 22 additions & 2 deletions guides/telegram-bridge-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,16 +256,36 @@ empty successful exports.
The first parser should be deterministic and testable:

- per-source regex rules;
- symbol normalization;
- a universal symbol pattern with source-specific overrides, preserving broker
prefixes and suffixes such as `xEURUSD-OTC`;
- symbol normalization that removes presentation whitespace but does not
silently strip execution-symbol affixes;
- explicit OTC market classification; an unqualified symbol is never assumed
to be a regular-market asset. Common OTC spellings (`EURUSD_OTC`,
`EURUSD-OTC`, `EURUSDOTC`, `EURUSD OTC`) are canonicalized to
`EURUSD_OTC` before execution. The suffix remains configurable for a
source-specific execution alias;
- direction aliases (`BUY`, `SELL`, `CALL`, `PUT`, arrows);
- separate direction-token rules so a source can map custom words or emoji;
- expiry parsing (`5m`, `M5`, `00:05`, local broker wording);
- optional signal name from message, chat title or rule name;
- optional amount/sizing only when explicitly configured;
- diagnostics for ambiguous or missing fields.

Raw Telegram text remains UTF-8. The parser treats known emoji sequences as
semantic tokens and accepts common variation-selector forms; it does not
require a second UTF-32 parser or normalize away the original text. A result
marker such as `✅`, `❌` or `Profit` has precedence over an overlapping signal
candidate, while a signal on another line of the same message remains
parseable. Result details such as payout, amount and statistics are optional;
missing details must not turn a recognizable result into a new executable
signal.

Martingale parsing should be deferred until the base signal/outcome model is
stable. The parser may preserve raw martingale hints in diagnostics or metadata
without turning them into executable sizing decisions.
without turning them into executable sizing decisions. Any future policy that
keeps only the first step or emits martingale steps must be an explicit
opt-in execution policy, not an implicit parser side effect.

## Outcomes

Expand Down
Loading
Loading